Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
249e868c3d
1 changed files with 163 additions and 46 deletions
|
|
@ -39,6 +39,12 @@ Options:
|
||||||
--force-local-map Skip mounting shares and save map to local drive
|
--force-local-map Skip mounting shares and save map to local drive
|
||||||
--start-fresh Ignore previous runs and start new recovery
|
--start-fresh Ignore previous runs and start new recovery
|
||||||
'''
|
'''
|
||||||
|
DETECT_DRIVES_NOTICE = '''
|
||||||
|
This option will force the drive controllers to rescan for devices.
|
||||||
|
The method used is not 100% reliable and may cause issues. If you see
|
||||||
|
any script errors or crashes after running this option then please
|
||||||
|
restart the computer and try again.
|
||||||
|
'''
|
||||||
CLONE_SETTINGS = {
|
CLONE_SETTINGS = {
|
||||||
'Source': None,
|
'Source': None,
|
||||||
'Destination': None,
|
'Destination': None,
|
||||||
|
|
@ -70,6 +76,7 @@ LOG = logging.getLogger(__name__)
|
||||||
MENU_ACTIONS = (
|
MENU_ACTIONS = (
|
||||||
'Start',
|
'Start',
|
||||||
f'Change settings {std.color_string("(experts only)", "YELLOW")}',
|
f'Change settings {std.color_string("(experts only)", "YELLOW")}',
|
||||||
|
f'Detect drives {std.color_string("(experts only)", "YELLOW")}',
|
||||||
'Quit')
|
'Quit')
|
||||||
MENU_TOGGLES = {
|
MENU_TOGGLES = {
|
||||||
'Auto continue (if recovery % over threshold)': True,
|
'Auto continue (if recovery % over threshold)': True,
|
||||||
|
|
@ -391,10 +398,10 @@ class State():
|
||||||
with open(settings_file, 'r') as _f:
|
with open(settings_file, 'r') as _f:
|
||||||
try:
|
try:
|
||||||
settings = json.loads(_f.read())
|
settings = json.loads(_f.read())
|
||||||
except (OSError, json.JSONDecodeError):
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
LOG.error('Failed to load clone settings')
|
LOG.error('Failed to load clone settings')
|
||||||
std.print_error('Invalid clone settings detected.')
|
std.print_error('Invalid clone settings detected.')
|
||||||
raise std.GenericAbort()
|
raise std.GenericAbort() from err
|
||||||
|
|
||||||
# Check settings
|
# Check settings
|
||||||
if settings:
|
if settings:
|
||||||
|
|
@ -438,9 +445,9 @@ class State():
|
||||||
try:
|
try:
|
||||||
with open(settings_file, 'w') as _f:
|
with open(settings_file, 'w') as _f:
|
||||||
json.dump(settings, _f)
|
json.dump(settings, _f)
|
||||||
except OSError:
|
except OSError as err:
|
||||||
std.print_error('Failed to save clone settings')
|
std.print_error('Failed to save clone settings')
|
||||||
raise std.GenericAbort()
|
raise std.GenericAbort() from err
|
||||||
|
|
||||||
def add_clone_block_pairs(self):
|
def add_clone_block_pairs(self):
|
||||||
"""Add device to device block pairs and set settings if necessary."""
|
"""Add device to device block pairs and set settings if necessary."""
|
||||||
|
|
@ -647,11 +654,11 @@ class State():
|
||||||
|
|
||||||
def get_rescued_size(self):
|
def get_rescued_size(self):
|
||||||
"""Get total rescued size from all block pairs, returns int."""
|
"""Get total rescued size from all block pairs, returns int."""
|
||||||
return sum([pair.get_rescued_size() for pair in self.block_pairs])
|
return sum(pair.get_rescued_size() for pair in self.block_pairs)
|
||||||
|
|
||||||
def get_total_size(self):
|
def get_total_size(self):
|
||||||
"""Get total size of all block_pairs in bytes, returns int."""
|
"""Get total size of all block_pairs in bytes, returns int."""
|
||||||
return sum([pair.size for pair in self.block_pairs])
|
return sum(pair.size for pair in self.block_pairs)
|
||||||
|
|
||||||
def init_recovery(self, docopt_args):
|
def init_recovery(self, docopt_args):
|
||||||
"""Select source/dest and set env."""
|
"""Select source/dest and set env."""
|
||||||
|
|
@ -763,12 +770,12 @@ class State():
|
||||||
"""Check if all block_pairs meet the pass threshold, returns bool."""
|
"""Check if all block_pairs meet the pass threshold, returns bool."""
|
||||||
threshold = cfg.ddrescue.AUTO_PASS_THRESHOLDS[pass_name]
|
threshold = cfg.ddrescue.AUTO_PASS_THRESHOLDS[pass_name]
|
||||||
return all(
|
return all(
|
||||||
[p.get_percent_recovered() >= threshold for p in self.block_pairs],
|
p.get_percent_recovered() >= threshold for p in self.block_pairs
|
||||||
)
|
)
|
||||||
|
|
||||||
def pass_complete(self, pass_name):
|
def pass_complete(self, pass_name):
|
||||||
"""Check if all block_pairs completed pass_name, returns bool."""
|
"""Check if all block_pairs completed pass_name, returns bool."""
|
||||||
return all([p.pass_complete(pass_name) for p in self.block_pairs])
|
return all(p.pass_complete(pass_name) for p in self.block_pairs)
|
||||||
|
|
||||||
def post_to_osticket(self):
|
def post_to_osticket(self):
|
||||||
"""Post results to osTicket."""
|
"""Post results to osTicket."""
|
||||||
|
|
@ -929,15 +936,15 @@ class State():
|
||||||
"""Run safety checks for destination and abort if necessary."""
|
"""Run safety checks for destination and abort if necessary."""
|
||||||
try:
|
try:
|
||||||
self.destination.safety_checks()
|
self.destination.safety_checks()
|
||||||
except hw_obj.CriticalHardwareError:
|
except hw_obj.CriticalHardwareError as err:
|
||||||
std.print_error(
|
std.print_error(
|
||||||
f'Critical error(s) detected for: {self.destination.path}',
|
f'Critical error(s) detected for: {self.destination.path}',
|
||||||
)
|
)
|
||||||
raise std.GenericAbort()
|
raise std.GenericAbort() from err
|
||||||
|
|
||||||
def safety_check_size(self):
|
def safety_check_size(self):
|
||||||
"""Run size safety check and abort if necessary."""
|
"""Run size safety check and abort if necessary."""
|
||||||
required_size = sum([pair.size for pair in self.block_pairs])
|
required_size = sum(pair.size for pair in self.block_pairs)
|
||||||
settings = self._load_settings() if self.mode == 'Clone' else {}
|
settings = self._load_settings() if self.mode == 'Clone' else {}
|
||||||
|
|
||||||
# Increase required_size if necessary
|
# Increase required_size if necessary
|
||||||
|
|
@ -1067,6 +1074,8 @@ class State():
|
||||||
|
|
||||||
def update_top_panes(self):
|
def update_top_panes(self):
|
||||||
"""(Re)create top source/destination panes."""
|
"""(Re)create top source/destination panes."""
|
||||||
|
source_exists = True
|
||||||
|
dest_exists = True
|
||||||
width = tmux.get_pane_size()[0]
|
width = tmux.get_pane_size()[0]
|
||||||
width = int(width / 2) - 1
|
width = int(width / 2) - 1
|
||||||
|
|
||||||
|
|
@ -1096,6 +1105,15 @@ class State():
|
||||||
# Done
|
# Done
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
# Check source/dest existance
|
||||||
|
if self.source:
|
||||||
|
source_exists = self.source.path.exists()
|
||||||
|
if self.destination:
|
||||||
|
if isinstance(self.destination, hw_obj.Disk):
|
||||||
|
dest_exists = self.destination.path.exists()
|
||||||
|
else:
|
||||||
|
dest_exists = self.destination.exists()
|
||||||
|
|
||||||
# Kill destination pane
|
# Kill destination pane
|
||||||
if 'Destination' in self.panes:
|
if 'Destination' in self.panes:
|
||||||
tmux.kill_pane(self.panes.pop('Destination'))
|
tmux.kill_pane(self.panes.pop('Destination'))
|
||||||
|
|
@ -1107,9 +1125,9 @@ class State():
|
||||||
tmux.respawn_pane(
|
tmux.respawn_pane(
|
||||||
self.panes['Source'],
|
self.panes['Source'],
|
||||||
text=std.color_string(
|
text=std.color_string(
|
||||||
['Source', source_str],
|
['Source', '' if source_exists else ' (Missing)', '\n', source_str],
|
||||||
['BLUE', None],
|
['BLUE', 'RED', None, None],
|
||||||
sep='\n',
|
sep='',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1122,9 +1140,9 @@ class State():
|
||||||
vertical=False,
|
vertical=False,
|
||||||
target_id=self.panes['Source'],
|
target_id=self.panes['Source'],
|
||||||
text=std.color_string(
|
text=std.color_string(
|
||||||
['Destination', dest_str],
|
['Destination', '' if dest_exists else ' (Missing)', '\n', dest_str],
|
||||||
['BLUE', None],
|
['BLUE', 'RED', None, None],
|
||||||
sep='\n',
|
sep='',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1167,7 +1185,7 @@ def build_block_pair_report(block_pairs, settings):
|
||||||
['BLUE', None],
|
['BLUE', None],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if any([pair.get_rescued_size() > 0 for pair in block_pairs]):
|
if any(pair.get_rescued_size() > 0 for pair in block_pairs):
|
||||||
notes.append(
|
notes.append(
|
||||||
std.color_string(
|
std.color_string(
|
||||||
['NOTE:', 'Resume data loaded from map file(s).'],
|
['NOTE:', 'Resume data loaded from map file(s).'],
|
||||||
|
|
@ -1445,25 +1463,6 @@ def check_destination_health(destination):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def check_for_missing_items(state):
|
|
||||||
"""Check if source or destination dissapeared."""
|
|
||||||
items = {
|
|
||||||
'Source': state.source,
|
|
||||||
'Destination': state.destination,
|
|
||||||
}
|
|
||||||
for name, item in items.items():
|
|
||||||
if not item:
|
|
||||||
continue
|
|
||||||
if hasattr(item, 'path'):
|
|
||||||
if not item.path.exists():
|
|
||||||
std.print_error(f'{name} disappeared')
|
|
||||||
elif hasattr(item, 'exists'):
|
|
||||||
if not item.exists():
|
|
||||||
std.print_error(f'{name} disappeared')
|
|
||||||
else:
|
|
||||||
LOG.error('Unknown %s type: %s', name, item)
|
|
||||||
|
|
||||||
|
|
||||||
def clean_working_dir(working_dir):
|
def clean_working_dir(working_dir):
|
||||||
"""Clean working directory to ensure a fresh recovery session.
|
"""Clean working directory to ensure a fresh recovery session.
|
||||||
|
|
||||||
|
|
@ -1575,7 +1574,7 @@ def get_etoc():
|
||||||
output = tmux.capture_pane()
|
output = tmux.capture_pane()
|
||||||
|
|
||||||
# Search for EToC delta
|
# Search for EToC delta
|
||||||
matches = re.findall(f'remaining time:.*$', output, re.MULTILINE)
|
matches = re.findall(r'remaining time:.*$', output, re.MULTILINE)
|
||||||
if matches:
|
if matches:
|
||||||
match = REGEX_REMAINING_TIME.search(matches[-1])
|
match = REGEX_REMAINING_TIME.search(matches[-1])
|
||||||
if match.group('na'):
|
if match.group('na'):
|
||||||
|
|
@ -1710,9 +1709,9 @@ def get_working_dir(
|
||||||
if mode == 'Image':
|
if mode == 'Image':
|
||||||
try:
|
try:
|
||||||
path = pathlib.Path(destination).resolve()
|
path = pathlib.Path(destination).resolve()
|
||||||
except TypeError:
|
except TypeError as err:
|
||||||
std.print_error(f'Invalid destination: {destination}')
|
std.print_error(f'Invalid destination: {destination}')
|
||||||
raise std.GenericAbort()
|
raise std.GenericAbort() from err
|
||||||
if path.exists() and fstype_is_ok(path, map_dir=False):
|
if path.exists() and fstype_is_ok(path, map_dir=False):
|
||||||
working_dir = path
|
working_dir = path
|
||||||
elif mode == 'Clone' and not force_local:
|
elif mode == 'Clone' and not force_local:
|
||||||
|
|
@ -1749,6 +1748,61 @@ def get_working_dir(
|
||||||
return working_dir
|
return working_dir
|
||||||
|
|
||||||
|
|
||||||
|
def is_missing_source_or_destination(state):
|
||||||
|
"""Check if source or destination dissapeared, returns bool."""
|
||||||
|
missing = False
|
||||||
|
items = {
|
||||||
|
'Source': state.source,
|
||||||
|
'Destination': state.destination,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check items
|
||||||
|
for name, item in items.items():
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
if hasattr(item, 'path'):
|
||||||
|
if not item.path.exists():
|
||||||
|
missing = True
|
||||||
|
std.print_error(f'{name} disappeared')
|
||||||
|
elif hasattr(item, 'exists'):
|
||||||
|
if not item.exists():
|
||||||
|
missing = True
|
||||||
|
std.print_error(f'{name} disappeared')
|
||||||
|
else:
|
||||||
|
LOG.error('Unknown %s type: %s', name, item)
|
||||||
|
|
||||||
|
# Update top panes
|
||||||
|
state.update_top_panes()
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def source_or_destination_changed(state):
|
||||||
|
"""Verify the source and destination objects are still valid."""
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
# Compare objects
|
||||||
|
for obj in (state.source, state.destination):
|
||||||
|
if not obj:
|
||||||
|
changed = True
|
||||||
|
elif hasattr(obj, 'exists'):
|
||||||
|
# Assuming dest path
|
||||||
|
changed = changed or not obj.exists()
|
||||||
|
elif isinstance(obj, hw_obj.Disk):
|
||||||
|
compare_dev = hw_obj.Disk(obj.path)
|
||||||
|
for key in ('model', 'serial'):
|
||||||
|
changed = changed or obj.details[key] != compare_dev.details[key]
|
||||||
|
|
||||||
|
# Update top panes
|
||||||
|
state.update_top_panes()
|
||||||
|
|
||||||
|
# Done
|
||||||
|
if changed:
|
||||||
|
std.print_error('Source and/or Destination changed')
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
"""Main function for ddrescue TUI."""
|
"""Main function for ddrescue TUI."""
|
||||||
|
|
@ -1770,12 +1824,11 @@ def main():
|
||||||
try:
|
try:
|
||||||
state.init_recovery(args)
|
state.init_recovery(args)
|
||||||
except (FileNotFoundError, std.GenericAbort):
|
except (FileNotFoundError, std.GenericAbort):
|
||||||
check_for_missing_items(state)
|
is_missing_source_or_destination(state)
|
||||||
std.abort()
|
std.abort()
|
||||||
|
|
||||||
# Show menu
|
# Show menu
|
||||||
while True:
|
while True:
|
||||||
action = None
|
|
||||||
selection = main_menu.advanced_select()
|
selection = main_menu.advanced_select()
|
||||||
|
|
||||||
# Change settings
|
# Change settings
|
||||||
|
|
@ -1788,6 +1841,17 @@ def main():
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Detect drives
|
||||||
|
if 'Detect drives' in selection[0]:
|
||||||
|
std.clear_screen()
|
||||||
|
std.print_warning(DETECT_DRIVES_NOTICE)
|
||||||
|
if std.ask('Are you sure you proceed?'):
|
||||||
|
std.print_standard('Forcing controllers to rescan for devices...')
|
||||||
|
cmd = 'echo "- - -" | sudo tee /sys/class/scsi_host/host*/scan'
|
||||||
|
exe.run_program(cmd, check=False, shell=True)
|
||||||
|
if source_or_destination_changed(state):
|
||||||
|
std.abort()
|
||||||
|
|
||||||
# Start recovery
|
# Start recovery
|
||||||
if 'Start' in selection:
|
if 'Start' in selection:
|
||||||
std.clear_screen()
|
std.clear_screen()
|
||||||
|
|
@ -1890,10 +1954,43 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
|
||||||
# pylint: disable=too-many-statements
|
# pylint: disable=too-many-statements
|
||||||
"""Run ddrescue using passed settings."""
|
"""Run ddrescue using passed settings."""
|
||||||
cmd = build_ddrescue_cmd(block_pair, pass_name, settings)
|
cmd = build_ddrescue_cmd(block_pair, pass_name, settings)
|
||||||
|
poweroff_source_after_idle = True
|
||||||
state.update_progress_pane('Active')
|
state.update_progress_pane('Active')
|
||||||
std.clear_screen()
|
std.clear_screen()
|
||||||
warning_message = ''
|
warning_message = ''
|
||||||
|
|
||||||
|
def _poweroff_source_drive(idle_minutes=120):
|
||||||
|
"""Power off source drive after a while."""
|
||||||
|
source_dev = state.source.path.name
|
||||||
|
|
||||||
|
# Sleep
|
||||||
|
i = 0
|
||||||
|
while i < idle_minutes*60:
|
||||||
|
if not poweroff_source_after_idle:
|
||||||
|
# Countdown canceled, exit without powering-down drives
|
||||||
|
return
|
||||||
|
if i % 600 == 0 and i > 0:
|
||||||
|
if i == 600:
|
||||||
|
std.print_standard(' ', flush=True)
|
||||||
|
std.print_warning(
|
||||||
|
f'Powering off source in {int((idle_minutes*60-i)/60)} minutes...',
|
||||||
|
)
|
||||||
|
std.sleep(5)
|
||||||
|
i += 5
|
||||||
|
|
||||||
|
# Power off drive
|
||||||
|
cmd = f'echo 1 | sudo tee /sys/block/{source_dev}/device/delete'
|
||||||
|
proc = exe.run_program(cmd, check=False, shell=True)
|
||||||
|
if proc.returncode:
|
||||||
|
LOG.error('Failed to poweroff source %s', state.source.path)
|
||||||
|
std.print_error(f'Failed to poweroff source {state.source.path}')
|
||||||
|
else:
|
||||||
|
LOG.info('Powered off source %s', state.source.path)
|
||||||
|
std.print_error(f'Powered off source {state.source.path}')
|
||||||
|
std.print_standard(
|
||||||
|
'Press Enter to return to main menu...', end='', flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
def _update_smart_pane():
|
def _update_smart_pane():
|
||||||
"""Update SMART pane every 30 seconds."""
|
"""Update SMART pane every 30 seconds."""
|
||||||
state.source.update_smart_details()
|
state.source.update_smart_details()
|
||||||
|
|
@ -1928,6 +2025,7 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
|
||||||
if warning_message:
|
if warning_message:
|
||||||
# Error detected on destination, stop recovery
|
# Error detected on destination, stop recovery
|
||||||
exe.stop_process(proc)
|
exe.stop_process(proc)
|
||||||
|
std.print_error(warning_message)
|
||||||
break
|
break
|
||||||
if _i % 60 == 0:
|
if _i % 60 == 0:
|
||||||
# Clear ddrescue pane
|
# Clear ddrescue pane
|
||||||
|
|
@ -1965,7 +2063,9 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
|
||||||
# Check result
|
# Check result
|
||||||
if proc.poll():
|
if proc.poll():
|
||||||
# True if return code is non-zero (poll() returns None if still running)
|
# True if return code is non-zero (poll() returns None if still running)
|
||||||
|
poweroff_thread = exe.start_thread(_poweroff_source_drive)
|
||||||
warning_message = 'Error(s) encountered, see message above'
|
warning_message = 'Error(s) encountered, see message above'
|
||||||
|
state.update_top_panes()
|
||||||
if warning_message:
|
if warning_message:
|
||||||
print(' ')
|
print(' ')
|
||||||
print(' ')
|
print(' ')
|
||||||
|
|
@ -1977,15 +2077,32 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
|
||||||
if str(proc.poll()) != '0':
|
if str(proc.poll()) != '0':
|
||||||
state.update_progress_pane('NEEDS ATTENTION')
|
state.update_progress_pane('NEEDS ATTENTION')
|
||||||
std.pause('Press Enter to return to main menu...')
|
std.pause('Press Enter to return to main menu...')
|
||||||
|
|
||||||
|
# Stop source poweroff countdown
|
||||||
|
std.print_standard('Stopping device poweroff countdown...', flush=True)
|
||||||
|
poweroff_source_after_idle = False
|
||||||
|
poweroff_thread.join()
|
||||||
|
|
||||||
|
# Done
|
||||||
raise std.GenericAbort()
|
raise std.GenericAbort()
|
||||||
|
|
||||||
|
|
||||||
def run_recovery(state, main_menu, settings_menu, dry_run=True):
|
def run_recovery(state, main_menu, settings_menu, dry_run=True):
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
"""Run recovery passes."""
|
"""Run recovery passes."""
|
||||||
atexit.register(state.save_debug_reports)
|
atexit.register(state.save_debug_reports)
|
||||||
attempted_recovery = False
|
attempted_recovery = False
|
||||||
auto_continue = False
|
auto_continue = False
|
||||||
|
|
||||||
|
# Bail early
|
||||||
|
if is_missing_source_or_destination(state):
|
||||||
|
std.print_standard('')
|
||||||
|
std.pause('Press Enter to return to main menu...')
|
||||||
|
return
|
||||||
|
if source_or_destination_changed(state):
|
||||||
|
std.print_standard('')
|
||||||
|
std.abort()
|
||||||
|
|
||||||
# Get settings
|
# Get settings
|
||||||
for name, details in main_menu.toggles.items():
|
for name, details in main_menu.toggles.items():
|
||||||
if 'Auto continue' in name and details['Selected']:
|
if 'Auto continue' in name and details['Selected']:
|
||||||
|
|
@ -2022,7 +2139,7 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True):
|
||||||
try:
|
try:
|
||||||
run_ddrescue(state, pair, pass_name, settings, dry_run=dry_run)
|
run_ddrescue(state, pair, pass_name, settings, dry_run=dry_run)
|
||||||
except (FileNotFoundError, KeyboardInterrupt, std.GenericAbort):
|
except (FileNotFoundError, KeyboardInterrupt, std.GenericAbort):
|
||||||
check_for_missing_items(state)
|
is_missing_source_or_destination(state)
|
||||||
abort = True
|
abort = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -2094,7 +2211,7 @@ def select_disk(prompt, skip_disk=None):
|
||||||
|
|
||||||
def select_disk_parts(prompt, disk):
|
def select_disk_parts(prompt, disk):
|
||||||
"""Select disk parts from list, returns list of Disk()."""
|
"""Select disk parts from list, returns list of Disk()."""
|
||||||
title = std.color_string(f'ddrescue TUI: Partition Selection', 'GREEN')
|
title = std.color_string('ddrescue TUI: Partition Selection', 'GREEN')
|
||||||
title += f'\n\nDisk: {disk.path} {disk.description}'
|
title += f'\n\nDisk: {disk.path} {disk.description}'
|
||||||
menu = std.Menu(title)
|
menu = std.Menu(title)
|
||||||
menu.separator = ' '
|
menu.separator = ' '
|
||||||
|
|
@ -2117,7 +2234,7 @@ def select_disk_parts(prompt, disk):
|
||||||
for option in menu.options.values():
|
for option in menu.options.values():
|
||||||
option['Selected'] = False
|
option['Selected'] = False
|
||||||
elif 'Proceed' in selection:
|
elif 'Proceed' in selection:
|
||||||
if any([option['Selected'] for option in menu.options.values()]):
|
if any(option['Selected'] for option in menu.options.values()):
|
||||||
# At least one partition/device selected/device selected
|
# At least one partition/device selected/device selected
|
||||||
break
|
break
|
||||||
elif 'Quit' in selection:
|
elif 'Quit' in selection:
|
||||||
|
|
@ -2176,7 +2293,7 @@ def select_path(prompt):
|
||||||
)
|
)
|
||||||
menu.separator = ' '
|
menu.separator = ' '
|
||||||
menu.add_action('Quit')
|
menu.add_action('Quit')
|
||||||
menu.add_option(f'Current directory')
|
menu.add_option('Current directory')
|
||||||
menu.add_option('Enter manually')
|
menu.add_option('Enter manually')
|
||||||
path = None
|
path = None
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue