Merge remote-tracking branch 'upstream/new-ddrescue-arguments' into new-ddrescue-arguments

This commit is contained in:
2Shirt 2022-03-08 15:21:34 -07:00
commit 981abbb8c2
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
2 changed files with 68 additions and 27 deletions

View file

@ -21,18 +21,25 @@ AUTO_PASS_THRESHOLDS = {
'trim': 98, 'trim': 98,
'scrape': float('inf'), 'scrape': float('inf'),
} }
DDRESCUE_MAP_TEMPLATE = '''# Mapfile. Created by {name}
0x0 ? 1
0x0 {size:#x} ?
'''
DDRESCUE_SETTINGS = { DDRESCUE_SETTINGS = {
'Default': { 'Default': {
'--binary-prefixes': {'Selected': True, 'Hidden': True, }, '--binary-prefixes': {'Selected': True, 'Hidden': True, },
'--complete-only': {'Selected': True, 'Hidden': True, },
'--data-preview': {'Selected': True, 'Value': '5', 'Hidden': True, }, '--data-preview': {'Selected': True, 'Value': '5', 'Hidden': True, },
'--idirect': {'Selected': True, }, '--idirect': {'Selected': True, },
'--odirect': {'Selected': True, }, '--odirect': {'Selected': True, },
'--input-position': {'Selected': False, 'Value': '0', },
'--max-error-rate': {'Selected': True, 'Value': '100MiB', }, '--max-error-rate': {'Selected': True, 'Value': '100MiB', },
'--max-read-rate': {'Selected': False, 'Value': '1MiB', }, '--max-read-rate': {'Selected': False, 'Value': '1MiB', },
'--min-read-rate': {'Selected': True, 'Value': '64KiB', }, '--min-read-rate': {'Selected': True, 'Value': '64KiB', },
'--reopen-on-error': {'Selected': True, }, '--reopen-on-error': {'Selected': True, },
'--retry-passes': {'Selected': True, 'Value': '0', }, '--retry-passes': {'Selected': True, 'Value': '0', },
'--reverse': {'Selected': False, }, '--reverse': {'Selected': False, },
'--skip-size': {'Selected': True, 'Value': '0.0001,0.01', }, # Percentages of source size
'--test-mode': {'Selected': False, 'Value': 'test.map', }, '--test-mode': {'Selected': False, 'Value': 'test.map', },
'--timeout': {'Selected': True, 'Value': '30m', }, '--timeout': {'Selected': True, 'Value': '30m', },
'-vvvv': {'Selected': True, 'Hidden': True, }, '-vvvv': {'Selected': True, 'Hidden': True, },
@ -47,6 +54,7 @@ DDRESCUE_SETTINGS = {
'--max-read-rate': {'Selected': True, 'Value': '64MiB', }, '--max-read-rate': {'Selected': True, 'Value': '64MiB', },
'--min-read-rate': {'Selected': True, 'Value': '1KiB', }, '--min-read-rate': {'Selected': True, 'Value': '1KiB', },
'--reopen-on-error': {'Selected': True, }, '--reopen-on-error': {'Selected': True, },
'--skip-size': {'Selected': True, 'Value': '0.001,0.05', }, # Percentages of source size
'--timeout': {'Selected': False, 'Value': '30m', }, '--timeout': {'Selected': False, 'Value': '30m', },
}, },
} }

View file

@ -22,7 +22,7 @@ import psutil
import pytz import pytz
from wk import cfg, debug, exe, io, log, net, osticket, std, tmux from wk import cfg, debug, exe, io, log, net, osticket, std, tmux
from wk.cfg.ddrescue import DDRESCUE_SETTINGS from wk.cfg.ddrescue import DDRESCUE_MAP_TEMPLATE, DDRESCUE_SETTINGS
from wk.hw import obj as hw_obj from wk.hw import obj as hw_obj
@ -67,6 +67,7 @@ DDRESCUE_LOG_REGEX = re.compile(
r'.*\(\s*(?P<percent>\d+\.?\d*)%\)$', r'.*\(\s*(?P<percent>\d+\.?\d*)%\)$',
re.IGNORECASE, re.IGNORECASE,
) )
INITIAL_SKIP_MIN = 64 * 1024 # This is ddrescue's minimum accepted value
REGEX_REMAINING_TIME = re.compile( REGEX_REMAINING_TIME = re.compile(
r'remaining time:' r'remaining time:'
r'\s*((?P<days>\d+)d)?' r'\s*((?P<days>\d+)d)?'
@ -118,12 +119,14 @@ TIMEZONE = pytz.timezone(cfg.main.LINUX_TIME_ZONE)
# Classes # Classes
class BlockPair(): class BlockPair():
"""Object for tracking source to dest recovery data.""" """Object for tracking source to dest recovery data."""
# pylint: disable=too-many-instance-attributes
def __init__(self, source, destination, model, working_dir): def __init__(self, source, destination, model, working_dir):
"""Initialize BlockPair() """Initialize BlockPair()
NOTE: source should be a wk.hw.obj.Disk() object NOTE: source should be a wk.hw.obj.Disk() object
and destination should be a pathlib.Path() object. and destination should be a pathlib.Path() object.
""" """
self.sector_size = source.details.get('phy-sec', 512)
self.source = source.path self.source = source.path
self.destination = destination self.destination = destination
self.map_data = {} self.map_data = {}
@ -134,12 +137,9 @@ class BlockPair():
'trim': 'Pending', 'trim': 'Pending',
'scrape': 'Pending', 'scrape': 'Pending',
}) })
self.view_proc = 'Disabled' self.view_map = 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ
if 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ:
# Enable opening ddrescueview during recovery
self.view_proc = None
# Set map file # Set map path
# e.g. '(Clone|Image)_Model[_p#]_Size[_Label].map' # e.g. '(Clone|Image)_Model[_p#]_Size[_Label].map'
map_name = model if model else 'None' map_name = model if model else 'None'
if source.details['bus'] == 'Image': if source.details['bus'] == 'Image':
@ -148,7 +148,7 @@ class BlockPair():
part_num = re.sub(r"^.*?(\d+)$", r"\1", source.path.name) part_num = re.sub(r"^.*?(\d+)$", r"\1", source.path.name)
map_name += f'_p{part_num}' map_name += f'_p{part_num}'
size_str = std.bytes_to_string( size_str = std.bytes_to_string(
size=source.details["size"], size=self.size,
use_binary=False, use_binary=False,
) )
map_name += f'_{size_str.replace(" ", "")}' map_name += f'_{size_str.replace(" ", "")}'
@ -164,7 +164,17 @@ class BlockPair():
else: else:
# Cloning # Cloning
self.map_path = pathlib.Path(f'{working_dir}/Clone_{map_name}.map') self.map_path = pathlib.Path(f'{working_dir}/Clone_{map_name}.map')
self.map_path.touch()
# Create map file if needed
# NOTE: We need to set the domain size for --complete-only to work
if not self.map_path.exists():
self.map_path.write_text(
data=DDRESCUE_MAP_TEMPLATE.format(
name=cfg.main.KIT_NAME_FULL,
size=self.size,
),
encoding='utf-8',
)
# Set initial status # Set initial status
self.set_initial_status() self.set_initial_status()
@ -263,10 +273,13 @@ class BlockPair():
'trim': 'Pending', 'trim': 'Pending',
'scrape': 'Pending', 'scrape': 'Pending',
}) })
if 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ: self.map_path.write_text(
# Enable opening ddrescueview during recovery data=DDRESCUE_MAP_TEMPLATE.format(
self.view_proc = None name=cfg.main.KIT_NAME_FULL,
self.map_path.touch() size=self.size,
),
encoding='utf-8',
)
# Set initial status # Set initial status
self.set_initial_status() self.set_initial_status()
@ -1287,7 +1300,7 @@ def build_block_pair_report(block_pairs, settings):
return report return report
def build_ddrescue_cmd(block_pair, pass_name, settings): def build_ddrescue_cmd(block_pair, pass_name, settings_menu):
"""Build ddrescue cmd using passed details, returns list.""" """Build ddrescue cmd using passed details, returns list."""
cmd = ['sudo', 'ddrescue'] cmd = ['sudo', 'ddrescue']
if (block_pair.destination.is_block_device() if (block_pair.destination.is_block_device()
@ -1301,8 +1314,31 @@ def build_ddrescue_cmd(block_pair, pass_name, settings):
elif pass_name == 'scrape': elif pass_name == 'scrape':
# Allow trimming and scraping # Allow trimming and scraping
pass pass
cmd.extend(settings)
cmd.append(f'--size={block_pair.size}') # Fix domain size based on starting position
domain_size = block_pair.size
if settings_menu.options['--input-position']['Selected']:
settings_menu.options['--reverse']['Selected'] = False
input_position = std.string_to_bytes(
settings_menu.options['--input-position']['Value'],
)
domain_size -= input_position
cmd.append(f'--size={domain_size}')
# Determine skip sizes
if settings_menu.options['--skip-size']['Selected']:
skip_sizes = settings_menu.options['--skip-size']['Value'].split(',')
skip_sizes = [float(s) for s in skip_sizes]
initial_skip = max(INITIAL_SKIP_MIN, int(block_pair.size * skip_sizes[0]))
max_skip = min(int(block_pair.size * skip_sizes[1]), domain_size)
max_skip = max(INITIAL_SKIP_MIN, max_skip)
cmd.append(f'--skip-size={initial_skip},{max_skip}')
cmd.extend(get_ddrescue_settings(settings_menu))
# Add source physical sector size (if possible)
cmd.append(f'--sector-size={block_pair.sector_size}')
# Add block pair and map file
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
# Use Raw disks if possible # Use Raw disks if possible
for dev in (block_pair.source, block_pair.destination): for dev in (block_pair.source, block_pair.destination):
@ -1689,6 +1725,8 @@ def get_ddrescue_settings(settings_menu):
# Check menu selections # Check menu selections
for name, details in settings_menu.options.items(): for name, details in settings_menu.options.items():
if name == '--skip-size':
continue
if details['Selected']: if details['Selected']:
if 'Value' in details: if 'Value' in details:
settings.append(f'{name}={details["Value"]}') settings.append(f'{name}={details["Value"]}')
@ -2154,8 +2192,13 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
LOG.info('ddrescue cmd: %s', cmd) LOG.info('ddrescue cmd: %s', cmd)
return return
# Start ddrescue # Start ddrescue and ddrescueview (if enabled)
proc = exe.popen_program(cmd) proc = exe.popen_program(cmd)
if block_pair.view_map:
exe.popen_program(
['ddrescueview', '-r', '5s', block_pair.map_path],
pipe=True,
)
# ddrescue loop # ddrescue loop
_i = 0 _i = 0
@ -2172,15 +2215,6 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
std.print_error(warning_message) std.print_error(warning_message)
break break
# Open ddrescueview
## NOTE: This needs to be started a bit into the recovery since it needs
## a non-zero map file to read
if not block_pair.view_proc and _i > 1:
block_pair.view_proc = exe.popen_program(
['ddrescueview', '-r', '5s', block_pair.map_path],
pipe=True,
)
if _i % 60 == 0: if _i % 60 == 0:
# Clear ddrescue pane # Clear ddrescue pane
tmux.clear_pane() tmux.clear_pane()
@ -2267,7 +2301,6 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True):
if 'Retry' in name and details['Selected']: if 'Retry' in name and details['Selected']:
details['Selected'] = False details['Selected'] = False
state.retry_all_passes() state.retry_all_passes()
settings = get_ddrescue_settings(settings_menu)
# Start SMART/Journal # Start SMART/Journal
state.panes['SMART'] = tmux.split_window( state.panes['SMART'] = tmux.split_window(
@ -2295,7 +2328,7 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True):
attempted_recovery = True attempted_recovery = True
state.mark_started() state.mark_started()
try: try:
run_ddrescue(state, pair, pass_name, settings, dry_run=dry_run) run_ddrescue(state, pair, pass_name, settings_menu, dry_run=dry_run)
except (FileNotFoundError, KeyboardInterrupt, std.GenericAbort): except (FileNotFoundError, KeyboardInterrupt, std.GenericAbort):
is_missing_source_or_destination(state) is_missing_source_or_destination(state)
abort = True abort = True