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

View file

@ -22,7 +22,7 @@ import psutil
import pytz
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
@ -67,6 +67,7 @@ DDRESCUE_LOG_REGEX = re.compile(
r'.*\(\s*(?P<percent>\d+\.?\d*)%\)$',
re.IGNORECASE,
)
INITIAL_SKIP_MIN = 64 * 1024 # This is ddrescue's minimum accepted value
REGEX_REMAINING_TIME = re.compile(
r'remaining time:'
r'\s*((?P<days>\d+)d)?'
@ -118,12 +119,14 @@ TIMEZONE = pytz.timezone(cfg.main.LINUX_TIME_ZONE)
# Classes
class BlockPair():
"""Object for tracking source to dest recovery data."""
# pylint: disable=too-many-instance-attributes
def __init__(self, source, destination, model, working_dir):
"""Initialize BlockPair()
NOTE: source should be a wk.hw.obj.Disk() object
and destination should be a pathlib.Path() object.
"""
self.sector_size = source.details.get('phy-sec', 512)
self.source = source.path
self.destination = destination
self.map_data = {}
@ -134,12 +137,9 @@ class BlockPair():
'trim': 'Pending',
'scrape': 'Pending',
})
self.view_proc = 'Disabled'
if 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ:
# Enable opening ddrescueview during recovery
self.view_proc = None
self.view_map = 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ
# Set map file
# Set map path
# e.g. '(Clone|Image)_Model[_p#]_Size[_Label].map'
map_name = model if model else 'None'
if source.details['bus'] == 'Image':
@ -148,7 +148,7 @@ class BlockPair():
part_num = re.sub(r"^.*?(\d+)$", r"\1", source.path.name)
map_name += f'_p{part_num}'
size_str = std.bytes_to_string(
size=source.details["size"],
size=self.size,
use_binary=False,
)
map_name += f'_{size_str.replace(" ", "")}'
@ -164,7 +164,17 @@ class BlockPair():
else:
# Cloning
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
self.set_initial_status()
@ -263,10 +273,13 @@ class BlockPair():
'trim': 'Pending',
'scrape': 'Pending',
})
if 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ:
# Enable opening ddrescueview during recovery
self.view_proc = None
self.map_path.touch()
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
self.set_initial_status()
@ -1287,7 +1300,7 @@ def build_block_pair_report(block_pairs, settings):
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."""
cmd = ['sudo', 'ddrescue']
if (block_pair.destination.is_block_device()
@ -1301,8 +1314,31 @@ def build_ddrescue_cmd(block_pair, pass_name, settings):
elif pass_name == 'scrape':
# Allow trimming and scraping
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':
# Use Raw disks if possible
for dev in (block_pair.source, block_pair.destination):
@ -1689,6 +1725,8 @@ def get_ddrescue_settings(settings_menu):
# Check menu selections
for name, details in settings_menu.options.items():
if name == '--skip-size':
continue
if details['Selected']:
if 'Value' in details:
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)
return
# Start ddrescue
# Start ddrescue and ddrescueview (if enabled)
proc = exe.popen_program(cmd)
if block_pair.view_map:
exe.popen_program(
['ddrescueview', '-r', '5s', block_pair.map_path],
pipe=True,
)
# ddrescue loop
_i = 0
@ -2172,15 +2215,6 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
std.print_error(warning_message)
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:
# Clear ddrescue 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']:
details['Selected'] = False
state.retry_all_passes()
settings = get_ddrescue_settings(settings_menu)
# Start SMART/Journal
state.panes['SMART'] = tmux.split_window(
@ -2295,7 +2328,7 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True):
attempted_recovery = True
state.mark_started()
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):
is_missing_source_or_destination(state)
abort = True