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:
parent
535455813c
commit
d781038e88
3 changed files with 81 additions and 32 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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"}',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue