commit
c9c1007eaa
3 changed files with 119 additions and 0 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
# Wizard Kit: Functions - ddrescue-tui
|
# Wizard Kit: Functions - ddrescue-tui
|
||||||
|
|
||||||
|
import datetime
|
||||||
import pathlib
|
import pathlib
|
||||||
import psutil
|
import psutil
|
||||||
|
import pytz
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import stat
|
import stat
|
||||||
|
|
@ -254,6 +256,7 @@ class RecoveryState():
|
||||||
self.block_pairs = []
|
self.block_pairs = []
|
||||||
self.current_pass = 0
|
self.current_pass = 0
|
||||||
self.current_pass_str = '0: Initializing'
|
self.current_pass_str = '0: Initializing'
|
||||||
|
self.etoc = ''
|
||||||
self.settings = DDRESCUE_SETTINGS.copy()
|
self.settings = DDRESCUE_SETTINGS.copy()
|
||||||
self.finished = False
|
self.finished = False
|
||||||
self.panes = {}
|
self.panes = {}
|
||||||
|
|
@ -261,6 +264,8 @@ class RecoveryState():
|
||||||
self.rescued = 0
|
self.rescued = 0
|
||||||
self.resumed = False
|
self.resumed = False
|
||||||
self.started = False
|
self.started = False
|
||||||
|
self.status = 'Inactive'
|
||||||
|
self.timezone = pytz.timezone(LINUX_TIME_ZONE)
|
||||||
self.total_size = 0
|
self.total_size = 0
|
||||||
if mode not in ('clone', 'image'):
|
if mode not in ('clone', 'image'):
|
||||||
raise GenericError('Unsupported mode')
|
raise GenericError('Unsupported mode')
|
||||||
|
|
@ -399,6 +404,66 @@ class RecoveryState():
|
||||||
elif self.current_pass == 2:
|
elif self.current_pass == 2:
|
||||||
self.current_pass_str = '3 "Scraping bad areas"'
|
self.current_pass_str = '3 "Scraping bad areas"'
|
||||||
|
|
||||||
|
def update_etoc(self):
|
||||||
|
"""Search ddrescue output for the current EToC, returns str."""
|
||||||
|
now = datetime.datetime.now(tz=self.timezone)
|
||||||
|
|
||||||
|
# Bail early
|
||||||
|
if 'NEEDS ATTENTION' in self.status:
|
||||||
|
# Just set to N/A (NOTE: this overrules the refresh rate below)
|
||||||
|
self.etoc = 'N/A'
|
||||||
|
return
|
||||||
|
elif 'In Progress' not in self.status:
|
||||||
|
# Don't update when EToC is hidden
|
||||||
|
return
|
||||||
|
if now.second % ETOC_REFRESH_RATE != 0:
|
||||||
|
# Limit updates based on settings/ddrescue.py
|
||||||
|
return
|
||||||
|
|
||||||
|
self.etoc = 'Unknown'
|
||||||
|
etoc_delta = None
|
||||||
|
text = ''
|
||||||
|
|
||||||
|
# Capture main tmux pane
|
||||||
|
try:
|
||||||
|
text = tmux_capture_pane()
|
||||||
|
except Exception:
|
||||||
|
# Ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Search for EToC delta
|
||||||
|
matches = re.findall(r'remaining time:.*$', text, re.MULTILINE)
|
||||||
|
if matches:
|
||||||
|
r = REGEX_REMAINING_TIME.search(matches[-1])
|
||||||
|
if r.group('na'):
|
||||||
|
self.etoc = 'N/A'
|
||||||
|
else:
|
||||||
|
self.etoc = r.string
|
||||||
|
days = r.group('days') if r.group('days') else 0
|
||||||
|
hours = r.group('hours') if r.group('hours') else 0
|
||||||
|
minutes = r.group('minutes') if r.group('minutes') else 0
|
||||||
|
seconds = r.group('seconds') if r.group('seconds') else 0
|
||||||
|
try:
|
||||||
|
etoc_delta = datetime.timedelta(
|
||||||
|
days=int(days),
|
||||||
|
hours=int(hours),
|
||||||
|
minutes=int(minutes),
|
||||||
|
seconds=int(seconds),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# Ignore and leave as raw string
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Calc finish time if EToC delta found
|
||||||
|
if etoc_delta:
|
||||||
|
try:
|
||||||
|
now = datetime.datetime.now(tz=self.timezone)
|
||||||
|
_etoc = now + etoc_delta
|
||||||
|
self.etoc = _etoc.strftime('%Y-%m-%d %H:%M %Z')
|
||||||
|
except Exception:
|
||||||
|
# Ignore and leave as current string
|
||||||
|
pass
|
||||||
|
|
||||||
def update_progress(self):
|
def update_progress(self):
|
||||||
"""Update overall progress using block_pairs."""
|
"""Update overall progress using block_pairs."""
|
||||||
self.rescued = 0
|
self.rescued = 0
|
||||||
|
|
@ -768,6 +833,13 @@ def menu_main(state):
|
||||||
|
|
||||||
# Show menu
|
# Show menu
|
||||||
while True:
|
while True:
|
||||||
|
# Update status
|
||||||
|
if state.finished:
|
||||||
|
state.status = ' Finished'
|
||||||
|
else:
|
||||||
|
state.status = ' Inactive'
|
||||||
|
update_sidepane(state)
|
||||||
|
|
||||||
# Update entries
|
# Update entries
|
||||||
for opt in main_options:
|
for opt in main_options:
|
||||||
opt['Name'] = '[{}] {}'.format(
|
opt['Name'] = '[{}] {}'.format(
|
||||||
|
|
@ -922,6 +994,7 @@ def run_ddrescue(state, pass_settings):
|
||||||
"""Run ddrescue pass."""
|
"""Run ddrescue pass."""
|
||||||
return_code = -1
|
return_code = -1
|
||||||
aborted = False
|
aborted = False
|
||||||
|
state.status = ' In Progress'
|
||||||
|
|
||||||
if state.finished:
|
if state.finished:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
|
|
@ -1036,6 +1109,9 @@ def run_ddrescue(state, pass_settings):
|
||||||
# Done
|
# Done
|
||||||
if str(return_code) != '0':
|
if str(return_code) != '0':
|
||||||
# Pause on errors
|
# Pause on errors
|
||||||
|
state.status = ' {YELLOW}NEEDS ATTENTION{CLEAR}'.format(**COLORS)
|
||||||
|
state.status = state.status.replace('33m', '33;5m')
|
||||||
|
update_sidepane(state)
|
||||||
pause('Press Enter to return to main menu... ')
|
pause('Press Enter to return to main menu... ')
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
|
|
@ -1292,6 +1368,7 @@ def update_sidepane(state):
|
||||||
output.append(' {BLUE}Cloning Status{CLEAR}'.format(**COLORS))
|
output.append(' {BLUE}Cloning Status{CLEAR}'.format(**COLORS))
|
||||||
else:
|
else:
|
||||||
output.append(' {BLUE}Imaging Status{CLEAR}'.format(**COLORS))
|
output.append(' {BLUE}Imaging Status{CLEAR}'.format(**COLORS))
|
||||||
|
output.append(state.status)
|
||||||
output.append('─────────────────────')
|
output.append('─────────────────────')
|
||||||
|
|
||||||
# Overall progress
|
# Overall progress
|
||||||
|
|
@ -1311,6 +1388,19 @@ def update_sidepane(state):
|
||||||
output.extend(bp.status)
|
output.extend(bp.status)
|
||||||
output.append(' ')
|
output.append(' ')
|
||||||
|
|
||||||
|
# EToC
|
||||||
|
if re.search(r'(In Progress|NEEDS ATTENTION)', state.status):
|
||||||
|
if not output[-1].strip():
|
||||||
|
# Last line is empty
|
||||||
|
output.pop()
|
||||||
|
output.append('─────────────────────')
|
||||||
|
output.append('{BLUE}Estimated Pass Finish{CLEAR}'.format(**COLORS))
|
||||||
|
state.update_etoc()
|
||||||
|
if 'N/A' in state.etoc.upper():
|
||||||
|
output.append('{YELLOW}N/A{CLEAR}'.format(**COLORS))
|
||||||
|
else:
|
||||||
|
output.append(state.etoc)
|
||||||
|
|
||||||
# Add line-endings
|
# Add line-endings
|
||||||
output = ['{}\n'.format(line) for line in output]
|
output = ['{}\n'.format(line) for line in output]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,21 @@ def create_file(filepath):
|
||||||
f.write('')
|
f.write('')
|
||||||
|
|
||||||
|
|
||||||
|
def tmux_capture_pane(pane_id=None):
|
||||||
|
"""Capture text from target, or current, pane, returns str."""
|
||||||
|
cmd = ['tmux', 'capture-pane', '-p']
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
text = ''
|
||||||
|
|
||||||
|
# Capture
|
||||||
|
result = run_program(cmd, check=False, encoding='utf-8', errors='ignore')
|
||||||
|
text = result.stdout
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return str(text)
|
||||||
|
|
||||||
|
|
||||||
def tmux_get_pane_size(pane_id=None):
|
def tmux_get_pane_size(pane_id=None):
|
||||||
"""Get target, or current, pane size, returns tuple."""
|
"""Get target, or current, pane size, returns tuple."""
|
||||||
x = -1
|
x = -1
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
# Wizard Kit: Settings - ddrescue-tui
|
# Wizard Kit: Settings - ddrescue-tui
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
# General
|
# General
|
||||||
RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs']
|
RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs']
|
||||||
USAGE = """ {script_name} clone [source [destination]]
|
USAGE = """ {script_name} clone [source [destination]]
|
||||||
|
|
@ -31,6 +35,16 @@ DDRESCUE_SETTINGS = {
|
||||||
'--timeout': {'Enabled': True, 'Value': '5m', },
|
'--timeout': {'Enabled': True, 'Value': '5m', },
|
||||||
'-vvvv': {'Enabled': True, 'Hidden': True, },
|
'-vvvv': {'Enabled': True, 'Hidden': True, },
|
||||||
}
|
}
|
||||||
|
ETOC_REFRESH_RATE = 30 # in seconds
|
||||||
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue