Added initial ddrescue sections

* Very early outline, very broken still
This commit is contained in:
2Shirt 2019-12-10 15:56:12 -07:00
parent 15f1a5bada
commit 7a880e2ee7
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
4 changed files with 345 additions and 0 deletions

21
scripts/ddrescue-tui Executable file
View 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
View 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()

View 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
View 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.")