Added initial ddrescue sections
* Very early outline, very broken still
This commit is contained in:
parent
15f1a5bada
commit
7a880e2ee7
4 changed files with 345 additions and 0 deletions
21
scripts/ddrescue-tui
Executable file
21
scripts/ddrescue-tui
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
## Wizard Kit: ddrescue TUI Launcher
|
||||
|
||||
# Check if running under Linux
|
||||
os_name="$(uname -s)"
|
||||
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
|
||||
fi
|
||||
|
||||
source ./launch-in-tmux
|
||||
|
||||
SESSION_NAME="ddrescue-tui"
|
||||
WINDOW_NAME="ddrescue TUI"
|
||||
TMUX_CMD="./ddrescue-tui.py"
|
||||
|
||||
launch_in_tmux "$@"
|
||||
14
scripts/ddrescue-tui.py
Executable file
14
scripts/ddrescue-tui.py
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wizard Kit: ddrescue TUI"""
|
||||
# vim: sts=2 sw=2 ts=2
|
||||
|
||||
import wk
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
wk.hw.ddrescue.main()
|
||||
except SystemExit:
|
||||
raise
|
||||
except: #pylint: disable=bare-except
|
||||
wk.std.major_exception()
|
||||
58
scripts/wk/cfg/ddrescue.py
Normal file
58
scripts/wk/cfg/ddrescue.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""WizardKit: Config - ddrescue"""
|
||||
# pylint: disable=bad-whitespace,line-too-long
|
||||
# vim: sts=2 sw=2 ts=2
|
||||
|
||||
import re
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
# General
|
||||
MAP_DIR = '/Backups/ddrescue-tui'
|
||||
RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs']
|
||||
RECOMMENDED_MAP_FSTYPES = ['cifs', 'ext2', 'ext3', 'ext4', 'vfat', 'xfs']
|
||||
|
||||
# Layout
|
||||
SIDE_PANE_WIDTH = 21
|
||||
TMUX_LAYOUT = OrderedDict({
|
||||
'Source': {'y': 2, 'Check': True},
|
||||
'Started': {'x': SIDE_PANE_WIDTH, 'Check': True},
|
||||
'Progress': {'x': SIDE_PANE_WIDTH, 'Check': True},
|
||||
})
|
||||
|
||||
# ddrescue
|
||||
AUTO_PASS_1_THRESHOLD = 95
|
||||
AUTO_PASS_2_THRESHOLD = 98
|
||||
DDRESCUE_SETTINGS = {
|
||||
'--binary-prefixes': {'Enabled': True, 'Hidden': True, },
|
||||
'--data-preview': {'Enabled': True, 'Value': '5', 'Hidden': True, },
|
||||
'--idirect': {'Enabled': True, },
|
||||
'--odirect': {'Enabled': True, },
|
||||
'--max-read-rate': {'Enabled': False, 'Value': '1MiB', },
|
||||
'--min-read-rate': {'Enabled': True, 'Value': '64KiB', },
|
||||
'--reopen-on-error': {'Enabled': True, },
|
||||
'--retry-passes': {'Enabled': True, 'Value': '0', },
|
||||
'--test-mode': {'Enabled': False, 'Value': 'test.map', },
|
||||
'--timeout': {'Enabled': True, 'Value': '5m', },
|
||||
'-vvvv': {'Enabled': True, 'Hidden': True, },
|
||||
}
|
||||
ETOC_REFRESH_RATE = 30 # in seconds
|
||||
REGEX_DDRESCUE_LOG = re.compile(
|
||||
r'^\s*(?P<key>\S+):\s+'
|
||||
r'(?P<size>\d+)\s+'
|
||||
r'(?P<unit>[PTGMKB])i?B?',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
REGEX_REMAINING_TIME = re.compile(
|
||||
r'remaining time:'
|
||||
r'\s*((?P<days>\d+)d)?'
|
||||
r'\s*((?P<hours>\d+)h)?'
|
||||
r'\s*((?P<minutes>\d+)m)?'
|
||||
r'\s*((?P<seconds>\d+)s)?'
|
||||
r'\s*(?P<na>n/a)?',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("This file is not meant to be called directly.")
|
||||
252
scripts/wk/hw/ddrescue.py
Normal file
252
scripts/wk/hw/ddrescue.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""WizardKit: ddrescue TUI"""
|
||||
# pylint: disable=too-many-lines
|
||||
# vim: sts=2 sw=2 ts=2
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import plistlib
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from collections import OrderedDict
|
||||
from docopt import docopt
|
||||
|
||||
from wk import cfg, debug, exe, graph, log, net, std, tmux
|
||||
from wk.hw import obj as hw_obj
|
||||
from wk.hw import sensors as hw_sensors
|
||||
|
||||
|
||||
# STATIC VARIABLES
|
||||
DOCSTRING = f'''{cfg.main.KIT_NAME_FULL}: ddrescue TUI
|
||||
|
||||
Usage:
|
||||
ddrescue-tui
|
||||
ddrescue-tui (clone|image) [<source> [<destination>]]
|
||||
ddrescue-tui (-h | --help)
|
||||
|
||||
Options:
|
||||
-h --help Show this page
|
||||
'''
|
||||
LOG = logging.getLogger(__name__)
|
||||
MENU_ACTIONS = (
|
||||
'Start',
|
||||
std.color_string(['Change settings', '(experts only)'], [None, 'YELLOW']),
|
||||
'Quit')
|
||||
MENU_TOGGLES = {
|
||||
'Auto continue (if recovery % over threshold)': True,
|
||||
'Retry (mark non-rescued sectors "non-tried")': False,
|
||||
'Reverse direction': False,
|
||||
}
|
||||
STATUS_COLORS = {
|
||||
'Passed': 'GREEN',
|
||||
'Aborted': 'YELLOW',
|
||||
'Skipped': 'YELLOW',
|
||||
'Working': 'YELLOW',
|
||||
'ERROR': 'RED',
|
||||
}
|
||||
|
||||
|
||||
# Classes
|
||||
class State():
|
||||
"""Object for tracking hardware diagnostic data."""
|
||||
def __init__(self):
|
||||
#TODO
|
||||
self.block_pairs = []
|
||||
self.disks = []
|
||||
self.layout = cfg.ddrescue.TMUX_LAYOUT.copy()
|
||||
self.log_dir = None
|
||||
self.panes = {}
|
||||
|
||||
# Init tmux and start a background process to maintain layout
|
||||
self.init_tmux()
|
||||
exe.start_thread(self.fix_tmux_layout_loop)
|
||||
|
||||
def fix_tmux_layout(self, forced=True):
|
||||
# pylint: disable=unused-argument
|
||||
"""Fix tmux layout based on cfg.ddrescue.TMUX_LAYOUT."""
|
||||
try:
|
||||
tmux.fix_layout(self.panes, self.layout, forced=forced)
|
||||
except RuntimeError:
|
||||
# Assuming self.panes changed while running
|
||||
pass
|
||||
|
||||
def fix_tmux_layout_loop(self):
|
||||
"""Fix tmux layout on a loop.
|
||||
|
||||
NOTE: This should be called as a thread.
|
||||
"""
|
||||
while True:
|
||||
self.fix_tmux_layout(forced=False)
|
||||
std.sleep(1)
|
||||
|
||||
def init_tmux(self):
|
||||
"""Initialize tmux layout."""
|
||||
tmux.kill_all_panes()
|
||||
|
||||
# Source / Dest
|
||||
self.update_top_panes()
|
||||
|
||||
# Started
|
||||
self.panes['Started'] = tmux.split_window(
|
||||
lines=cfg.ddrescue.TMUX_SIDE_WIDTH,
|
||||
target_id=self.panes['Top'],
|
||||
text=std.color_string(
|
||||
['Started', time.strftime("%Y-%m-%d %H:%M %Z")],
|
||||
['BLUE', None],
|
||||
sep='\n',
|
||||
),
|
||||
)
|
||||
|
||||
# Progress
|
||||
self.panes['Progress'] = tmux.split_window(
|
||||
lines=cfg.ddrescue.TMUX_SIDE_WIDTH,
|
||||
text=' ',
|
||||
)
|
||||
|
||||
def save_debug_reports(self):
|
||||
"""Save debug reports to disk."""
|
||||
LOG.info('Saving debug reports')
|
||||
debug_dir = pathlib.Path(f'{self.log_dir}/debug')
|
||||
if not debug_dir.exists():
|
||||
debug_dir.mkdir()
|
||||
|
||||
# State (self)
|
||||
with open(f'{debug_dir}/state.report', 'a') as _f:
|
||||
_f.write('\n'.join(debug.generate_object_report(self)))
|
||||
|
||||
# Block pairs
|
||||
for _bp in self.block_pairs:
|
||||
with open(f'{debug_dir}/bp_part#.report', 'a') as _f:
|
||||
_f.write('\n'.join(debug.generate_object_report(_bp)))
|
||||
|
||||
def update_progress_pane(self):
|
||||
"""Update progress pane."""
|
||||
report = []
|
||||
width = cfg.ddrescue.TMUX_SIDE_WIDTH
|
||||
|
||||
#TODO
|
||||
|
||||
# Write to progress file
|
||||
out_path = pathlib.Path(f'{self.log_dir}/progress.out')
|
||||
with open(out_path, 'w') as _f:
|
||||
_f.write('\n'.join(report))
|
||||
|
||||
def update_top_panes(self, text):
|
||||
"""(Re)create top source/destination panes."""
|
||||
#TODO
|
||||
self.panes['Source'] = tmux.split_window(
|
||||
behind=True,
|
||||
lines=2,
|
||||
vertical=True,
|
||||
text=std.color_string(
|
||||
['Source', f'TODO'],
|
||||
['BLUE', None],
|
||||
sep='\n',
|
||||
),
|
||||
)
|
||||
|
||||
# Destination
|
||||
self.panes['Destination'] = tmux.split_window(
|
||||
percent=50,
|
||||
vertical=False,
|
||||
target_id=self.panes['Source'],
|
||||
text=std.color_string(
|
||||
['Destination', f'TODO'],
|
||||
['BLUE', None],
|
||||
sep='\n',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Functions
|
||||
def build_main_menu():
|
||||
"""Build main menu, returns wk.std.Menu."""
|
||||
menu = std.Menu(title=std.color_string('ddrescue TUI: Main Menu', 'GREEN'))
|
||||
|
||||
# Add actions, options, etc
|
||||
for action in MENU_ACTIONS:
|
||||
menu.add_action(action)
|
||||
for toggle, selected in MENU_TOGGLES.items():
|
||||
menu.add_toggle(toggle, {'Selected': selected})
|
||||
menu.actions['Start']['Separator'] = True
|
||||
|
||||
# Done
|
||||
return menu
|
||||
|
||||
|
||||
def build_settings_menu():
|
||||
"""Build settings menu, returns wk.std.Menu."""
|
||||
#TODO Fixme
|
||||
title_text = [
|
||||
std.color_string('ddrescue TUI: Exper Settings', 'GREEN'),
|
||||
' ',
|
||||
std.color_string(
|
||||
['These settings can cause', 'MAJOR DAMAGE', 'to drives'],
|
||||
['YELLOW', 'RED', 'YELLOW'],
|
||||
),
|
||||
'Please read the manual before making changes',
|
||||
]
|
||||
menu = std.Menu(title='\n'join(title_text))
|
||||
|
||||
# Add actions, options, etc
|
||||
menu.add_action('Main Menu')
|
||||
for name, details in cfg.ddrescue.DDRESCUE_SETTINGS.items():
|
||||
menu.add_option(name, details)
|
||||
menu.actions['Main Menu']['Separator'] = True
|
||||
|
||||
# Done
|
||||
return menu
|
||||
|
||||
|
||||
def main():
|
||||
# pylint: disable=too-many-branches
|
||||
"""Main function for hardware diagnostics."""
|
||||
args = docopt(DOCSTRING)
|
||||
log.update_log_path(dest_name='ddrescue-TUI', timestamp=True)
|
||||
|
||||
# Safety check
|
||||
if 'TMUX' not in os.environ:
|
||||
LOG.error('tmux session not found')
|
||||
raise RuntimeError('tmux session not found')
|
||||
|
||||
# Init
|
||||
atexit.register(tmux.kill_all_panes)
|
||||
main_menu = build_main_menu()
|
||||
settings_menu = build_settings_menu()
|
||||
state = State()
|
||||
|
||||
# Show menu
|
||||
while True:
|
||||
action = None
|
||||
selection = menu.advanced_select()
|
||||
|
||||
# Start diagnostics
|
||||
if 'Start' in selection:
|
||||
run_diags(state, menu, quick_mode=False)
|
||||
|
||||
# Quit
|
||||
if 'Quit' in selection:
|
||||
break
|
||||
|
||||
|
||||
def run_recovery(state, main_menu, settings_menu):
|
||||
"""Run recovery passes."""
|
||||
aborted = False
|
||||
atexit.register(state.save_debug_reports)
|
||||
state.init_recovery(menu)
|
||||
|
||||
# TODO
|
||||
# Run ddrescue
|
||||
|
||||
# Done
|
||||
state.save_debug_reports()
|
||||
atexit.unregister(state.save_debug_reports)
|
||||
std.pause('Press Enter to return to main menu...')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("This file is not meant to be called directly.")
|
||||
Loading…
Reference in a new issue