Enable ddrescue-tui under macOS

Currently limited to only cloning whole disks, not select partitions.
We need to specify the --size due to a bug under macOS; it fails to
detect the size of the device/partition and reports 8192 PiB instead.
This commit is contained in:
2Shirt 2021-04-13 02:18:17 -06:00
parent 535455813c
commit d781038e88
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
3 changed files with 81 additions and 32 deletions

View file

@ -8,8 +8,10 @@ if [[ "$os_name" == "Darwin" ]]; then
os_name="macOS" os_name="macOS"
fi fi
if [[ "$os_name" != "Linux" ]]; then if [[ "$os_name" != "Linux" ]]; then
echo "This script is not supported under $os_name." 1>&2 echo "This script is not fully supported under $os_name." 1>&2
exit 1 echo ""
echo "Press Enter to continue..."
read -r _dontcare
fi fi
source launch-in-tmux source launch-in-tmux

View file

@ -22,6 +22,7 @@ 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.hw import obj as hw_obj from wk.hw import obj as hw_obj
@ -56,6 +57,9 @@ CLONE_SETTINGS = {
# (5, 1) ## Clone source partition #5 to destination partition #1 # (5, 1) ## Clone source partition #5 to destination partition #1
], ],
} }
if std.PLATFORM == 'Darwin':
DDRESCUE_SETTINGS['Default']['--idirect'] = {'Selected': False, 'Hidden': True}
DDRESCUE_SETTINGS['Default']['--odirect'] = {'Selected': False, 'Hidden': True}
DDRESCUE_LOG_REGEX = re.compile( DDRESCUE_LOG_REGEX = re.compile(
r'^\s*(?P<key>\S+):\s+' r'^\s*(?P<key>\S+):\s+'
r'(?P<size>\d+)\s+' r'(?P<size>\d+)\s+'
@ -89,8 +93,10 @@ PANE_RATIOS = (
) )
PLATFORM = std.PLATFORM PLATFORM = std.PLATFORM
RECOMMENDED_FSTYPES = re.compile(r'^(ext[234]|ntfs|xfs)$') RECOMMENDED_FSTYPES = re.compile(r'^(ext[234]|ntfs|xfs)$')
if PLATFORM == 'Darwin':
RECOMMENDED_FSTYPES = re.compile(r'^(apfs|hfs.?)$')
RECOMMENDED_MAP_FSTYPES = re.compile( RECOMMENDED_MAP_FSTYPES = re.compile(
r'^(apfs|cifs|ext[234]|hfs.?|ntfs|vfat|xfs)$' r'^(apfs|cifs|ext[234]|hfs.?|ntfs|smbfs|vfat|xfs)$'
) )
SETTING_PRESETS = ( SETTING_PRESETS = (
'Default', 'Default',
@ -186,6 +192,7 @@ class BlockPair():
'ddrescuelog', 'ddrescuelog',
'--binary-prefixes', '--binary-prefixes',
'--show-status', '--show-status',
f'--size={self.size}',
self.map_path, self.map_path,
] ]
proc = exe.run_program(cmd, check=False) proc = exe.run_program(cmd, check=False)
@ -209,6 +216,7 @@ class BlockPair():
cmd = [ cmd = [
'ddrescuelog', 'ddrescuelog',
'--done-status', '--done-status',
f'--size={self.size}',
self.map_path, self.map_path,
] ]
proc = exe.run_program(cmd, check=False) proc = exe.run_program(cmd, check=False)
@ -660,6 +668,7 @@ class State():
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):
# pylint: disable=too-many-branches
"""Select source/dest and set env.""" """Select source/dest and set env."""
std.clear_screen() std.clear_screen()
source_parts = [] source_parts = []
@ -735,7 +744,15 @@ class State():
self.update_progress_pane('Idle') self.update_progress_pane('Idle')
self.confirm_selections('Start recovery?') self.confirm_selections('Start recovery?')
# TODO: Unmount source and/or destination under macOS # Unmount source and/or destination under macOS
if PLATFORM == 'Darwin':
for disk in (self.source, self.destination):
cmd = ['diskutil', 'unmountDisk', disk.path]
try:
exe.run_program(cmd)
except subprocess.CalledProcessError:
std.print_error('Failed to unmount source and/or destination')
std.abort()
# Prep destination # Prep destination
if self.mode == 'Clone': if self.mode == 'Clone':
@ -1187,6 +1204,16 @@ def build_ddrescue_cmd(block_pair, pass_name, settings):
# Allow trimming and scraping # Allow trimming and scraping
pass pass
cmd.extend(settings) cmd.extend(settings)
cmd.append(f'--size={block_pair.size}')
if PLATFORM == 'Darwin':
# Use Raw disks if possible
for dev in (block_pair.source, block_pair.destination):
raw_dev = pathlib.Path(dev.with_name(f'r{dev.name}'))
if raw_dev.exists():
cmd.append(raw_dev)
else:
cmd.append(dev)
else:
cmd.append(block_pair.source) cmd.append(block_pair.source)
cmd.append(block_pair.destination) cmd.append(block_pair.destination)
cmd.append(block_pair.map_path) cmd.append(block_pair.map_path)
@ -1306,6 +1333,7 @@ def build_main_menu():
# Add actions, options, etc # Add actions, options, etc
for action in MENU_ACTIONS: for action in MENU_ACTIONS:
if not (PLATFORM == 'Darwin' and 'Detect drives' in action):
menu.add_action(action) menu.add_action(action)
for toggle, selected in MENU_TOGGLES.items(): for toggle, selected in MENU_TOGGLES.items():
menu.add_toggle(toggle, {'Selected': selected}) menu.add_toggle(toggle, {'Selected': selected})
@ -1357,12 +1385,12 @@ def build_settings_menu(silent=True):
# Add default settings # Add default settings
menu.add_action('Load Preset') menu.add_action('Load Preset')
menu.add_action('Main Menu') menu.add_action('Main Menu')
for name, details in cfg.ddrescue.DDRESCUE_SETTINGS['Default'].items(): for name, details in DDRESCUE_SETTINGS['Default'].items():
menu.add_option(name, details.copy()) menu.add_option(name, details.copy())
# Update settings using preset # Update settings using preset
if preset != 'Default': if preset != 'Default':
for name, details in cfg.ddrescue.DDRESCUE_SETTINGS[preset].items(): for name, details in DDRESCUE_SETTINGS[preset].items():
menu.options[name].update(details.copy()) menu.options[name].update(details.copy())
# Done # Done
@ -1493,11 +1521,14 @@ def fstype_is_ok(path, map_dir=False):
# Get fstype # Get fstype
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
try: # Check all parent dirs until a mountpoint is found
fstype = get_fstype_macos(path) test_path = pathlib.Path(path)
except (IndexError, TypeError, ValueError): while test_path:
# Ignore for now fstype = get_fstype_macos(test_path)
pass if fstype != 'UNKNOWN':
break
fstype = None
test_path = test_path.parent
elif PLATFORM == 'Linux': elif PLATFORM == 'Linux':
cmd = [ cmd = [
'findmnt', 'findmnt',
@ -1565,21 +1596,21 @@ def get_etoc():
def get_fstype_macos(path): def get_fstype_macos(path):
"""Get fstype for path under macOS, returns str. """Get fstype for path under macOS, returns str."""
fstype = 'UNKNOWN'
proc = exe.run_program(['mount'], check=False)
NOTE: This method is not very effecient. # Bail early
""" if proc.returncode:
cmd = ['df', path] return fstype
# Get device based on the path # Parse output
proc = exe.run_program(cmd, check=False) match = re.search(rf'{path} \((\w+)', proc.stdout)
dev = proc.stdout.splitlines()[1].split()[0] if match:
fstype = match.group(1)
# Get device details
dev = hw_obj.Disk(dev)
# Done # Done
return dev.details['fstype'] return fstype
def get_object(path): def get_object(path):
@ -1688,7 +1719,9 @@ def get_working_dir(mode, destination, force_local=False):
std.print_info('Mounting backup shares...') std.print_info('Mounting backup shares...')
net.mount_backup_shares(read_write=True) net.mount_backup_shares(read_write=True)
for server in cfg.net.BACKUP_SERVERS: for server in cfg.net.BACKUP_SERVERS:
path = pathlib.Path(f'/Backups/{server}') path = pathlib.Path(
f'/{"Volumes" if PLATFORM == "Darwin" else "Backups"}/{server}',
)
if path.exists() and fstype_is_ok(path, map_dir=True): if path.exists() and fstype_is_ok(path, map_dir=True):
# Acceptable path found # Acceptable path found
working_dir = path working_dir = path
@ -1922,6 +1955,10 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
"""Power off source drive after a while.""" """Power off source drive after a while."""
source_dev = state.source.path.name source_dev = state.source.path.name
# Bail early
if PLATFORM == 'Darwin':
return
# Sleep # Sleep
i = 0 i = 0
while i < idle_minutes*60: while i < idle_minutes*60:
@ -2024,7 +2061,7 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True):
# 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_thread = exe.start_thread(
_poweroff_source_drive, _poweroff_source_drive,
cfg.ddrescue.DRIVE_POWEROFF_TIMEOUT, [cfg.ddrescue.DRIVE_POWEROFF_TIMEOUT],
) )
warning_message = 'Error(s) encountered, see message above' warning_message = 'Error(s) encountered, see message above'
state.update_top_panes() state.update_top_panes()
@ -2079,6 +2116,7 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True):
behind=True, lines=12, vertical=True, behind=True, lines=12, vertical=True,
watch_file=f'{state.log_dir}/smart.out', watch_file=f'{state.log_dir}/smart.out',
) )
if PLATFORM != 'Darwin':
state.panes['Journal'] = tmux.split_window( state.panes['Journal'] = tmux.split_window(
lines=4, vertical=True, cmd='journalctl --dmesg --follow', lines=4, vertical=True, cmd='journalctl --dmesg --follow',
) )
@ -2202,6 +2240,10 @@ def select_disk_parts(prompt, disk):
elif 'Quit' in selection: elif 'Quit' in selection:
raise std.GenericAbort() raise std.GenericAbort()
# Bail early if running under macOS
if PLATFORM == 'Darwin':
return [disk]
# Bail early if child device selected # Bail early if child device selected
if disk.details.get('parent', False): if disk.details.get('parent', False):
return [disk] return [disk]

View file

@ -62,11 +62,17 @@ def mount_backup_shares(read_write=False):
# Prep mount point # Prep mount point
if PLATFORM in ('Darwin', 'Linux'): if PLATFORM in ('Darwin', 'Linux'):
mount_point = pathlib.Path(f'/Backups/{name}') mount_point = pathlib.Path(
f'/{"Volumes" if PLATFORM == "Darwin" else "Backups"}/{name}',
)
try: try:
if not mount_point.exists(): if not mount_point.exists():
# Script should be run as user so sudo is required # Script should be run as user so sudo is required
run_program(['sudo', 'mkdir', '-p', mount_point]) run_program(['sudo', 'mkdir', '-p', mount_point])
if PLATFORM == 'Darwin':
run_program(
['sudo', 'chown', f'{os.getuid()}:{os.getgid()}', mount_point],
)
except OSError: except OSError:
# Assuming permission denied under macOS # Assuming permission denied under macOS
pass pass
@ -108,7 +114,6 @@ def mount_network_share(details, mount_point=None, read_write=False):
# Build OS-specific command # Build OS-specific command
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
cmd = [ cmd = [
'sudo',
'mount', 'mount',
'-t', 'smbfs', '-t', 'smbfs',
'-o', f'{"rw" if read_write else "ro"}', '-o', f'{"rw" if read_write else "ro"}',