diff --git a/scripts/ddrescue-tui b/scripts/ddrescue-tui index 3f648b66..0b8f345f 100755 --- a/scripts/ddrescue-tui +++ b/scripts/ddrescue-tui @@ -8,8 +8,10 @@ if [[ "$os_name" == "Darwin" ]]; then os_name="macOS" fi if [[ "$os_name" != "Linux" ]]; then - echo "This script is not supported under $os_name." 1>&2 - exit 1 + echo "This script is not fully supported under $os_name." 1>&2 + echo "" + echo "Press Enter to continue..." + read -r _dontcare fi source launch-in-tmux diff --git a/scripts/wk/hw/ddrescue.py b/scripts/wk/hw/ddrescue.py index c796fc61..610429e0 100644 --- a/scripts/wk/hw/ddrescue.py +++ b/scripts/wk/hw/ddrescue.py @@ -22,6 +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.hw import obj as hw_obj @@ -56,6 +57,9 @@ CLONE_SETTINGS = { # (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( r'^\s*(?P\S+):\s+' r'(?P\d+)\s+' @@ -89,8 +93,10 @@ PANE_RATIOS = ( ) PLATFORM = std.PLATFORM RECOMMENDED_FSTYPES = re.compile(r'^(ext[234]|ntfs|xfs)$') +if PLATFORM == 'Darwin': + RECOMMENDED_FSTYPES = re.compile(r'^(apfs|hfs.?)$') 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 = ( 'Default', @@ -186,6 +192,7 @@ class BlockPair(): 'ddrescuelog', '--binary-prefixes', '--show-status', + f'--size={self.size}', self.map_path, ] proc = exe.run_program(cmd, check=False) @@ -209,6 +216,7 @@ class BlockPair(): cmd = [ 'ddrescuelog', '--done-status', + f'--size={self.size}', self.map_path, ] proc = exe.run_program(cmd, check=False) @@ -661,6 +669,7 @@ class State(): return sum(pair.size for pair in self.block_pairs) def init_recovery(self, docopt_args): + # pylint: disable=too-many-branches """Select source/dest and set env.""" std.clear_screen() source_parts = [] @@ -681,6 +690,13 @@ class State(): # Set mode self.mode = set_mode(docopt_args) + # Image mode is broken.. + # TODO: Fix image mode + # Definitely for Linux, maybe for macOS + if self.mode == 'Image': + std.print_error("I'm sorry but image mode is currently broken...") + std.abort() + # Select source self.source = get_object(docopt_args['']) if not self.source: @@ -738,7 +754,15 @@ class State(): self.update_progress_pane('Idle') 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 if self.mode == 'Clone': @@ -1217,8 +1241,18 @@ def build_ddrescue_cmd(block_pair, pass_name, settings): # Allow trimming and scraping pass cmd.extend(settings) - cmd.append(block_pair.source) - cmd.append(block_pair.destination) + 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.destination) cmd.append(block_pair.map_path) # Done @@ -1336,7 +1370,8 @@ def build_main_menu(): # Add actions, options, etc for action in MENU_ACTIONS: - menu.add_action(action) + if not (PLATFORM == 'Darwin' and 'Detect drives' in action): + menu.add_action(action) for toggle, selected in MENU_TOGGLES.items(): menu.add_toggle(toggle, {'Selected': selected}) @@ -1387,12 +1422,12 @@ def build_settings_menu(silent=True): # Add default settings menu.add_action('Load Preset') 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()) # Update settings using preset 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()) # Done @@ -1523,11 +1558,14 @@ def fstype_is_ok(path, map_dir=False): # Get fstype if PLATFORM == 'Darwin': - try: - fstype = get_fstype_macos(path) - except (IndexError, TypeError, ValueError): - # Ignore for now - pass + # Check all parent dirs until a mountpoint is found + test_path = pathlib.Path(path) + while test_path: + fstype = get_fstype_macos(test_path) + if fstype != 'UNKNOWN': + break + fstype = None + test_path = test_path.parent elif PLATFORM == 'Linux': cmd = [ 'findmnt', @@ -1595,21 +1633,21 @@ def get_etoc(): 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. - """ - cmd = ['df', path] + # Bail early + if proc.returncode: + return fstype - # Get device based on the path - proc = exe.run_program(cmd, check=False) - dev = proc.stdout.splitlines()[1].split()[0] - - # Get device details - dev = hw_obj.Disk(dev) + # Parse output + match = re.search(rf'{path} \((\w+)', proc.stdout) + if match: + fstype = match.group(1) # Done - return dev.details['fstype'] + return fstype def get_object(path): @@ -1714,7 +1752,9 @@ def get_working_dir( std.print_info('Mounting backup shares...') net.mount_backup_shares(read_write=True) 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): # Acceptable path found working_dir = path @@ -1959,6 +1999,10 @@ def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True): """Power off source drive after a while.""" source_dev = state.source.path.name + # Bail early + if PLATFORM == 'Darwin': + return + # Sleep i = 0 while i < idle_minutes*60: @@ -2061,7 +2105,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) poweroff_thread = exe.start_thread( _poweroff_source_drive, - cfg.ddrescue.DRIVE_POWEROFF_TIMEOUT, + [cfg.ddrescue.DRIVE_POWEROFF_TIMEOUT], ) warning_message = 'Error(s) encountered, see message above' state.update_top_panes() @@ -2116,9 +2160,10 @@ def run_recovery(state, main_menu, settings_menu, dry_run=True): behind=True, lines=12, vertical=True, watch_file=f'{state.log_dir}/smart.out', ) - state.panes['Journal'] = tmux.split_window( - lines=4, vertical=True, cmd='journalctl --dmesg --follow', - ) + if PLATFORM != 'Darwin': + state.panes['Journal'] = tmux.split_window( + lines=4, vertical=True, cmd='journalctl --dmesg --follow', + ) # Run pass(es) for pass_name in ('read', 'trim', 'scrape'): @@ -2239,6 +2284,10 @@ def select_disk_parts(prompt, disk): elif 'Quit' in selection: raise std.GenericAbort() + # Bail early if running under macOS + if PLATFORM == 'Darwin': + return [disk] + # Bail early if child device selected if disk.details.get('parent', False): return [disk] diff --git a/scripts/wk/hw/obj.py b/scripts/wk/hw/obj.py index bb59f912..b59246fc 100644 --- a/scripts/wk/hw/obj.py +++ b/scripts/wk/hw/obj.py @@ -694,6 +694,13 @@ def get_disk_details_macos(path): else: dev['parent'] = dev.pop('ParentWholeDisk', None) + # Fix details if main dev is a child + for child in details['children']: + if path == child['path']: + for key in ('fstype', 'label', 'name', 'size'): + details[key] = child[key] + break + # Done return details diff --git a/scripts/wk/net.py b/scripts/wk/net.py index d2ba5c5d..d797c470 100644 --- a/scripts/wk/net.py +++ b/scripts/wk/net.py @@ -62,11 +62,17 @@ def mount_backup_shares(read_write=False): # Prep mount point 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: if not mount_point.exists(): # Script should be run as user so sudo is required run_program(['sudo', 'mkdir', '-p', mount_point]) + if PLATFORM == 'Darwin': + run_program( + ['sudo', 'chown', f'{os.getuid()}:{os.getgid()}', mount_point], + ) except OSError: # Assuming permission denied under macOS pass @@ -108,7 +114,6 @@ def mount_network_share(details, mount_point=None, read_write=False): # Build OS-specific command if PLATFORM == 'Darwin': cmd = [ - 'sudo', 'mount', '-t', 'smbfs', '-o', f'{"rw" if read_write else "ro"}', diff --git a/setup/macos/update-base-image b/setup/macos/update-base-image index d51e06aa..6aaa26a3 100755 --- a/setup/macos/update-base-image +++ b/setup/macos/update-base-image @@ -129,4 +129,5 @@ hdiutil detach "${WK_IMAGE_DEV}" # Convert to compressed read-only image echo "Converting to read-only image..." +hdiutil resize -sectors min "${OUT_NAME}.sparsebundle" hdiutil convert -format UDZO -o "${OUT_NAME}.dmg" "${OUT_NAME}.sparsebundle"