Add overall recovery status to side-pane
* --test-mode disabled by default * Fixed bug that prevented escaping auto_run via Ctrl-c * Fixed no-trim / no-scrape flag handling * Only proceed device(s) have been selected in menu_select_children
This commit is contained in:
parent
1f63f91144
commit
cd955fe1fc
1 changed files with 69 additions and 46 deletions
|
|
@ -14,23 +14,25 @@ from operator import itemgetter
|
||||||
|
|
||||||
# STATIC VARIABLES
|
# STATIC VARIABLES
|
||||||
RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs']
|
RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs']
|
||||||
AUTO_NEXT_PASS_1_THRESHOLD = 85
|
AUTO_NEXT_PASS_1_THRESHOLD = 90
|
||||||
AUTO_NEXT_PASS_2_THRESHOLD = 98
|
AUTO_NEXT_PASS_2_THRESHOLD = 98
|
||||||
DDRESCUE_SETTINGS = {
|
DDRESCUE_SETTINGS = {
|
||||||
'--binary-prefixes': {'Enabled': True, 'Hidden': True},
|
'--binary-prefixes': {'Enabled': True, 'Hidden': True},
|
||||||
'--data-preview': {'Enabled': True, 'Hidden': True},
|
'--data-preview': {'Enabled': True, 'Hidden': True},
|
||||||
'--idirect': {'Enabled': True},
|
'--idirect': {'Enabled': True},
|
||||||
'--odirect': {'Enabled': True},
|
'--odirect': {'Enabled': True},
|
||||||
'--max-read-rate': {'Enabled': False, 'Value': '128MiB'},
|
'--max-read-rate': {'Enabled': False, 'Value': '4MiB'},
|
||||||
'--min-read-rate': {'Enabled': True, 'Value': '64KiB'},
|
'--min-read-rate': {'Enabled': True, 'Value': '64KiB'},
|
||||||
'--reopen-on-error': {'Enabled': True},
|
'--reopen-on-error': {'Enabled': True},
|
||||||
'--retry-passes=': {'Enabled': True, 'Value': '0'},
|
'--retry-passes=': {'Enabled': True, 'Value': '0'},
|
||||||
'--test-mode=': {'Enabled': True, 'Value': 'some.map'},
|
'--test-mode=': {'Enabled': False, 'Value': 'some.map'},
|
||||||
'--timeout=': {'Enabled': True, 'Value': '5m'},
|
'--timeout=': {'Enabled': True, 'Value': '5m'},
|
||||||
'-vvvv': {'Enabled': True, 'Hidden': True},
|
'-vvvv': {'Enabled': True, 'Hidden': True},
|
||||||
}
|
}
|
||||||
REGEX_MAP_DATA = re.compile(r'^\s*(?P<key>\S+):.*\(\s*(?P<value>\d+\.?\d*)%.*')
|
REGEX_MAP_DATA = re.compile(r'^\s*(?P<key>\S+):.*\(\s*(?P<value>\d+\.?\d*)%.*')
|
||||||
REGEX_MAP_STATUS = re.compile(r'.*current status:\s+(?P<status>.*)')
|
REGEX_MAP_STATUS = re.compile(r'.*current status:\s+(?P<status>.*)')
|
||||||
|
STATUS_COLOR_CLEAR = ('Pending',)
|
||||||
|
STATUS_COLOR_YELLOW = ('Skipped', 'Unknown', 'Working')
|
||||||
USAGE = """ {script_name} clone [source [destination]]
|
USAGE = """ {script_name} clone [source [destination]]
|
||||||
{script_name} image [source [destination]]
|
{script_name} image [source [destination]]
|
||||||
(e.g. {script_name} clone /dev/sda /dev/sdb)
|
(e.g. {script_name} clone /dev/sda /dev/sdb)
|
||||||
|
|
@ -238,9 +240,9 @@ def get_status_color(s, t_success=99, t_warn=90):
|
||||||
# Status is either in lists below or will default to red
|
# Status is either in lists below or will default to red
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if s in ('Pending',):
|
if s in STATUS_COLOR_CLEAR:
|
||||||
color = COLORS['CLEAR']
|
color = COLORS['CLEAR']
|
||||||
elif s in ('Skipped', 'Unknown', 'Working'):
|
elif s in STATUS_COLOR_YELLOW:
|
||||||
color = COLORS['YELLOW']
|
color = COLORS['YELLOW']
|
||||||
elif p_recovered >= t_success:
|
elif p_recovered >= t_success:
|
||||||
color = COLORS['GREEN']
|
color = COLORS['GREEN']
|
||||||
|
|
@ -445,6 +447,7 @@ def menu_main(source, dest):
|
||||||
while auto_run or first_run:
|
while auto_run or first_run:
|
||||||
first_run = False
|
first_run = False
|
||||||
run_ddrescue(source, dest, settings)
|
run_ddrescue(source, dest, settings)
|
||||||
|
update_progress(source, end_run=True)
|
||||||
if current_pass == 'Done':
|
if current_pass == 'Done':
|
||||||
# "Pass Done" i.e. all passes done
|
# "Pass Done" i.e. all passes done
|
||||||
break
|
break
|
||||||
|
|
@ -459,6 +462,8 @@ def menu_main(source, dest):
|
||||||
elif (current_pass == 'Pass 2' and
|
elif (current_pass == 'Pass 2' and
|
||||||
min_status < AUTO_NEXT_PASS_2_THRESHOLD):
|
min_status < AUTO_NEXT_PASS_2_THRESHOLD):
|
||||||
auto_run = False
|
auto_run = False
|
||||||
|
else:
|
||||||
|
auto_run = False
|
||||||
# Update current pass for next iteration
|
# Update current pass for next iteration
|
||||||
current_pass = source['Current Pass']
|
current_pass = source['Current Pass']
|
||||||
|
|
||||||
|
|
@ -493,11 +498,14 @@ def menu_select_children(source):
|
||||||
|
|
||||||
# Show Menu
|
# Show Menu
|
||||||
while True:
|
while True:
|
||||||
|
one_or_more_devs_selected = False
|
||||||
# Update entries
|
# Update entries
|
||||||
for dev in dev_options:
|
for dev in dev_options:
|
||||||
dev['Name'] = '{} {}'.format(
|
if dev['Selected']:
|
||||||
'*' if dev['Selected'] else ' ',
|
one_or_more_devs_selected = True
|
||||||
dev['Base Name'])
|
dev['Name'] = '* {}'.format(dev['Base Name'])
|
||||||
|
else:
|
||||||
|
dev['Name'] = ' {}'.format(dev['Base Name'])
|
||||||
|
|
||||||
selection = menu_select(
|
selection = menu_select(
|
||||||
title='Please select part(s) to image',
|
title='Please select part(s) to image',
|
||||||
|
|
@ -517,7 +525,7 @@ def menu_select_children(source):
|
||||||
if dev_options[0]['Selected']:
|
if dev_options[0]['Selected']:
|
||||||
for dev in dev_options[1:]:
|
for dev in dev_options[1:]:
|
||||||
dev['Selected'] = False
|
dev['Selected'] = False
|
||||||
elif selection == 'P':
|
elif selection == 'P' and one_or_more_devs_selected:
|
||||||
break
|
break
|
||||||
elif selection == 'Q':
|
elif selection == 'Q':
|
||||||
abort_ddrescue_tui()
|
abort_ddrescue_tui()
|
||||||
|
|
@ -747,22 +755,11 @@ def run_ddrescue(source, dest, settings):
|
||||||
current_pass = source['Current Pass']
|
current_pass = source['Current Pass']
|
||||||
return_code = None
|
return_code = None
|
||||||
|
|
||||||
# Set pass options
|
if current_pass == 'Done':
|
||||||
if current_pass == 'Pass 1':
|
|
||||||
settings.extend(['--no-trim', '--no-scrape'])
|
|
||||||
elif current_pass == 'Pass 2':
|
|
||||||
# Allow trimming
|
|
||||||
settings.append('--no-scrape')
|
|
||||||
elif current_pass == 'Pass 3':
|
|
||||||
# Allow trimming and scraping
|
|
||||||
pass
|
|
||||||
elif current_pass == 'Done':
|
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_warning('Recovery already completed?')
|
print_warning('Recovery already completed?')
|
||||||
pause('Press Enter to return to main menu...')
|
pause('Press Enter to return to main menu...')
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
raise GenericError("This shouldn't happen?")
|
|
||||||
|
|
||||||
# Set device(s) to clone/image
|
# Set device(s) to clone/image
|
||||||
source[current_pass]['Status'] = 'Working'
|
source[current_pass]['Status'] = 'Working'
|
||||||
|
|
@ -803,6 +800,14 @@ def run_ddrescue(source, dest, settings):
|
||||||
cmd = [
|
cmd = [
|
||||||
'ddrescue', *settings, s_dev['Dev Path'],
|
'ddrescue', *settings, s_dev['Dev Path'],
|
||||||
s_dev['Dest Paths']['Image'], s_dev['Dest Paths']['Map']]
|
s_dev['Dest Paths']['Image'], s_dev['Dest Paths']['Map']]
|
||||||
|
if current_pass == 'Pass 1':
|
||||||
|
cmd.extend(['--no-trim', '--no-scrape'])
|
||||||
|
elif current_pass == 'Pass 2':
|
||||||
|
# Allow trimming
|
||||||
|
cmd.append('--no-scrape')
|
||||||
|
elif current_pass == 'Pass 3':
|
||||||
|
# Allow trimming and scraping
|
||||||
|
pass
|
||||||
|
|
||||||
# Start ddrescue
|
# Start ddrescue
|
||||||
try:
|
try:
|
||||||
|
|
@ -813,7 +818,9 @@ def run_ddrescue(source, dest, settings):
|
||||||
# ddrescue_proc = popen_program(cmd)
|
# ddrescue_proc = popen_program(cmd)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
ddrescue_proc.wait(timeout=30)
|
ddrescue_proc.wait(timeout=10)
|
||||||
|
sleep(2)
|
||||||
|
update_progress(source)
|
||||||
break
|
break
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
update_progress(source)
|
update_progress(source)
|
||||||
|
|
@ -832,8 +839,7 @@ def run_ddrescue(source, dest, settings):
|
||||||
print_error('Error(s) encountered, see message above.')
|
print_error('Error(s) encountered, see message above.')
|
||||||
break
|
break
|
||||||
|
|
||||||
# Cleanup
|
# Done
|
||||||
update_progress(source, end_run=True)
|
|
||||||
if str(return_code) != '0':
|
if str(return_code) != '0':
|
||||||
# Pause on errors
|
# Pause on errors
|
||||||
pause('Press Enter to return to main menu... ')
|
pause('Press Enter to return to main menu... ')
|
||||||
|
|
@ -1066,7 +1072,7 @@ def tmux_splitw(*args):
|
||||||
|
|
||||||
|
|
||||||
def update_progress(source, end_run=False):
|
def update_progress(source, end_run=False):
|
||||||
"""Update progress file."""
|
"""Update progress for source dev(s) and update status pane file."""
|
||||||
current_pass = source['Current Pass']
|
current_pass = source['Current Pass']
|
||||||
pass_complete_for_all_devs = True
|
pass_complete_for_all_devs = True
|
||||||
total_recovery = True
|
total_recovery = True
|
||||||
|
|
@ -1085,16 +1091,6 @@ def update_progress(source, end_run=False):
|
||||||
else:
|
else:
|
||||||
next_pass = 'Done'
|
next_pass = 'Done'
|
||||||
|
|
||||||
if 'Progress Out' not in source:
|
|
||||||
source['Progress Out'] = '{}/progress.out'.format(
|
|
||||||
global_vars['LogDir'])
|
|
||||||
output = []
|
|
||||||
if source['Type'] == 'Clone':
|
|
||||||
output.append(' {BLUE}Cloning Status{CLEAR}'.format(**COLORS))
|
|
||||||
else:
|
|
||||||
output.append(' {BLUE}Imaging Status{CLEAR}'.format(**COLORS))
|
|
||||||
output.append('─────────────────────')
|
|
||||||
|
|
||||||
# Update children progress
|
# Update children progress
|
||||||
for child in source['Children']:
|
for child in source['Children']:
|
||||||
if os.path.exists(child['Dest Paths']['Map']):
|
if os.path.exists(child['Dest Paths']['Map']):
|
||||||
|
|
@ -1129,17 +1125,18 @@ def update_progress(source, end_run=False):
|
||||||
elif os.path.exists(source['Dest Paths']['Map']):
|
elif os.path.exists(source['Dest Paths']['Map']):
|
||||||
# Cloning/Imaging whole device
|
# Cloning/Imaging whole device
|
||||||
map_data = read_map_file(source['Dest Paths']['Map'])
|
map_data = read_map_file(source['Dest Paths']['Map'])
|
||||||
source[current_pass]['Done'] = map_data['pass completed']
|
if current_pass != 'Done':
|
||||||
source[current_pass]['Status'] = map_data['rescued']
|
source[current_pass]['Done'] = map_data['pass completed']
|
||||||
|
source[current_pass]['Status'] = map_data['rescued']
|
||||||
|
try:
|
||||||
|
source[current_pass]['Min Status'] = min(
|
||||||
|
source[current_pass]['Min Status'],
|
||||||
|
source[current_pass]['Status'])
|
||||||
|
except TypeError:
|
||||||
|
# Force 0% to disable auto-continue
|
||||||
|
source[current_pass]['Min Status'] = 0
|
||||||
|
pass_complete_for_all_devs &= source[current_pass]['Done']
|
||||||
source['Recovered Size'] = map_data['rescued']/100 * source['Size']
|
source['Recovered Size'] = map_data['rescued']/100 * source['Size']
|
||||||
try:
|
|
||||||
source[current_pass]['Min Status'] = min(
|
|
||||||
source[current_pass]['Min Status'],
|
|
||||||
source[current_pass]['Status'])
|
|
||||||
except TypeError:
|
|
||||||
# Force 0% to disable auto-continue
|
|
||||||
source[current_pass]['Min Status'] = 0
|
|
||||||
pass_complete_for_all_devs &= source[current_pass]['Done']
|
|
||||||
total_recovery &= map_data['full recovery']
|
total_recovery &= map_data['full recovery']
|
||||||
else:
|
else:
|
||||||
# Cloning/Imaging whole device and map missing
|
# Cloning/Imaging whole device and map missing
|
||||||
|
|
@ -1161,7 +1158,30 @@ def update_progress(source, end_run=False):
|
||||||
elif pass_complete_for_all_devs:
|
elif pass_complete_for_all_devs:
|
||||||
# Ready for next pass?
|
# Ready for next pass?
|
||||||
source['Current Pass'] = next_pass
|
source['Current Pass'] = next_pass
|
||||||
source[current_pass]['Done'] = True
|
if current_pass != 'Done':
|
||||||
|
source[current_pass]['Done'] = True
|
||||||
|
|
||||||
|
# Start building output lines
|
||||||
|
if 'Progress Out' not in source:
|
||||||
|
source['Progress Out'] = '{}/progress.out'.format(
|
||||||
|
global_vars['LogDir'])
|
||||||
|
output = []
|
||||||
|
if source['Type'] == 'Clone':
|
||||||
|
output.append(' {BLUE}Cloning Status{CLEAR}'.format(**COLORS))
|
||||||
|
else:
|
||||||
|
output.append(' {BLUE}Imaging Status{CLEAR}'.format(**COLORS))
|
||||||
|
output.append('─────────────────────')
|
||||||
|
|
||||||
|
# Overall progress
|
||||||
|
recovered_p = (source['Recovered Size'] / source['Total Size']) * 100
|
||||||
|
recovered_s = human_readable_size(source['Recovered Size'])
|
||||||
|
output.append('{BLUE}Overall Progress{CLEAR}'.format(**COLORS))
|
||||||
|
output.append('Recovered:{s_color}{recovered_p:>9.2f} %{CLEAR}'.format(
|
||||||
|
s_color=get_status_color(recovered_p),
|
||||||
|
recovered_p=recovered_p,
|
||||||
|
**COLORS))
|
||||||
|
output.append('{:>21}'.format(recovered_s))
|
||||||
|
output.append('─────────────────────')
|
||||||
|
|
||||||
# Main device
|
# Main device
|
||||||
if source['Type'] == 'Clone':
|
if source['Type'] == 'Clone':
|
||||||
|
|
@ -1207,6 +1227,9 @@ def update_progress(source, end_run=False):
|
||||||
s_color=get_status_color(child[p_num]['Status']),
|
s_color=get_status_color(child[p_num]['Status']),
|
||||||
s_display=s_display,
|
s_display=s_display,
|
||||||
**COLORS))
|
**COLORS))
|
||||||
|
p = (child.get('Recovered Size', 0) / child['Size']) * 100
|
||||||
|
output.append('Recovered:{s_color}{p:>9.2f} %{CLEAR}'.format(
|
||||||
|
s_color=get_status_color(p), p=p, **COLORS))
|
||||||
output.append(' ')
|
output.append(' ')
|
||||||
else:
|
else:
|
||||||
# Whole device
|
# Whole device
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue