Merge branch 'new-ddrescue-arguments' into dev
This commit is contained in:
commit
a3abf03a23
2 changed files with 88 additions and 51 deletions
|
|
@ -16,22 +16,30 @@ TMUX_LAYOUT = OrderedDict({
|
||||||
# ddrescue
|
# ddrescue
|
||||||
AUTO_PASS_THRESHOLDS = {
|
AUTO_PASS_THRESHOLDS = {
|
||||||
# NOTE: The scrape key is set to infinity to force a break
|
# NOTE: The scrape key is set to infinity to force a break
|
||||||
'read': 95,
|
'read-skip': 50,
|
||||||
'trim': 98,
|
'read-full': 95,
|
||||||
'scrape': float('inf'),
|
'trim': 98,
|
||||||
|
'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': False, },
|
||||||
'--retry-passes': {'Selected': True, 'Value': '0', },
|
'--retry-passes': {'Selected': True, 'Value': '0', },
|
||||||
'--reverse': {'Selected': False, },
|
'--reverse': {'Selected': False, },
|
||||||
|
'--skip-size': {'Selected': True, 'Value': '0.001,0.05', }, # 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, },
|
||||||
|
|
@ -39,16 +47,19 @@ DDRESCUE_SETTINGS = {
|
||||||
'Fast': {
|
'Fast': {
|
||||||
'--max-error-rate': {'Selected': True, 'Value': '32MiB', },
|
'--max-error-rate': {'Selected': True, 'Value': '32MiB', },
|
||||||
'--min-read-rate': {'Selected': True, 'Value': '1MiB', },
|
'--min-read-rate': {'Selected': True, 'Value': '1MiB', },
|
||||||
'--reopen-on-error': {'Selected': False, },
|
|
||||||
'--timeout': {'Selected': True, 'Value': '5m', },
|
'--timeout': {'Selected': True, 'Value': '5m', },
|
||||||
},
|
},
|
||||||
'Safe': {
|
'Safe': {
|
||||||
'--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, },
|
|
||||||
'--timeout': {'Selected': False, 'Value': '30m', },
|
'--timeout': {'Selected': False, 'Value': '30m', },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
DDRESCUE_SPECIFIC_PASS_SETTINGS = {
|
||||||
|
'read-skip': ['--no-scrape', '--no-trim', '--cpass=1,2'],
|
||||||
|
'read-full': ['--no-scrape', '--no-trim'],
|
||||||
|
'trim': ['--no-scrape'],
|
||||||
|
}
|
||||||
DRIVE_POWEROFF_TIMEOUT = 90
|
DRIVE_POWEROFF_TIMEOUT = 90
|
||||||
PARTITION_TYPES = {
|
PARTITION_TYPES = {
|
||||||
'GPT': {
|
'GPT': {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@ import psutil
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from wk import cfg, debug, exe, io, log, net, std, tmux
|
from wk import cfg, debug, exe, io, log, net, std, tmux
|
||||||
from wk.cfg.ddrescue import DDRESCUE_SETTINGS
|
from wk.cfg.ddrescue import (
|
||||||
|
DDRESCUE_MAP_TEMPLATE,
|
||||||
|
DDRESCUE_SETTINGS,
|
||||||
|
DDRESCUE_SPECIFIC_PASS_SETTINGS,
|
||||||
|
)
|
||||||
from wk.hw import obj as hw_obj
|
from wk.hw import obj as hw_obj
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -67,6 +71,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)?'
|
||||||
|
|
@ -104,11 +109,11 @@ SETTING_PRESETS = (
|
||||||
'Safe',
|
'Safe',
|
||||||
)
|
)
|
||||||
STATUS_COLORS = {
|
STATUS_COLORS = {
|
||||||
'Passed': 'GREEN',
|
'Passed': 'GREEN',
|
||||||
'Aborted': 'YELLOW',
|
'Aborted': 'YELLOW',
|
||||||
'Skipped': 'YELLOW',
|
'Skipped': 'YELLOW',
|
||||||
'Working': 'YELLOW',
|
'Working': 'YELLOW',
|
||||||
'ERROR': 'RED',
|
'ERROR': 'RED',
|
||||||
}
|
}
|
||||||
TIMEZONE = pytz.timezone(cfg.main.LINUX_TIME_ZONE)
|
TIMEZONE = pytz.timezone(cfg.main.LINUX_TIME_ZONE)
|
||||||
|
|
||||||
|
|
@ -116,29 +121,28 @@ 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 = {}
|
||||||
self.map_path = None
|
self.map_path = None
|
||||||
self.size = source.details['size']
|
self.size = source.details['size']
|
||||||
self.status = OrderedDict({
|
self.status = OrderedDict({
|
||||||
'read': 'Pending',
|
'read-skip': 'Pending',
|
||||||
'trim': 'Pending',
|
'read-full': 'Pending',
|
||||||
'scrape': 'Pending',
|
'trim': '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 path
|
||||||
# Set map file
|
|
||||||
# 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':
|
||||||
|
|
@ -147,7 +151,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(" ", "")}'
|
||||||
|
|
@ -163,7 +167,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()
|
||||||
|
|
@ -296,10 +310,9 @@ class BlockPair():
|
||||||
|
|
||||||
# Mark future passes as skipped if applicable
|
# Mark future passes as skipped if applicable
|
||||||
if percent == 100:
|
if percent == 100:
|
||||||
if pass_name == 'read':
|
status_keys = list(self.status.keys())
|
||||||
self.status['trim'] = 'Skipped'
|
for i in status_keys[status_keys.index(pass_name)+1:]:
|
||||||
if pass_name in ('read', 'trim'):
|
self.status[status_keys[i]] = 'Skipped'
|
||||||
self.status['scrape'] = 'Skipped'
|
|
||||||
|
|
||||||
|
|
||||||
class State():
|
class State():
|
||||||
|
|
@ -1202,22 +1215,38 @@ 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()
|
||||||
or block_pair.destination.is_char_device()):
|
or block_pair.destination.is_char_device()):
|
||||||
cmd.append('--force')
|
cmd.append('--force')
|
||||||
if pass_name == 'read':
|
cmd.extend(DDRESCUE_SPECIFIC_PASS_SETTINGS.get(pass_name, []))
|
||||||
cmd.extend(['--no-trim', '--no-scrape'])
|
|
||||||
elif pass_name == 'trim':
|
# Fix domain size based on starting position
|
||||||
# Allow trimming
|
domain_size = block_pair.size
|
||||||
cmd.append('--no-scrape')
|
if settings_menu.options['--input-position']['Selected']:
|
||||||
elif pass_name == 'scrape':
|
settings_menu.options['--reverse']['Selected'] = False
|
||||||
# Allow trimming and scraping
|
input_position = std.string_to_bytes(
|
||||||
pass
|
settings_menu.options['--input-position']['Value'],
|
||||||
cmd.extend(settings)
|
)
|
||||||
cmd.append(f'--size={block_pair.size}')
|
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):
|
||||||
|
|
@ -1569,6 +1598,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"]}')
|
||||||
|
|
@ -2017,8 +2048,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
|
||||||
|
|
@ -2035,15 +2071,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()
|
||||||
|
|
@ -2130,7 +2157,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(
|
||||||
|
|
@ -2143,7 +2169,7 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run pass(es)
|
# Run pass(es)
|
||||||
for pass_name in ('read', 'trim', 'scrape'):
|
for pass_name in ('read-skip', 'read-full', 'trim', 'scrape'):
|
||||||
abort = False
|
abort = False
|
||||||
|
|
||||||
# Skip to next pass
|
# Skip to next pass
|
||||||
|
|
@ -2158,7 +2184,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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue