From c96ffc5f597111c9ac46bc6a0f5652e94814c844 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 20 Mar 2019 13:16:09 -0600 Subject: [PATCH 1/4] Added overall status line to ddrescue-tui --- .bin/Scripts/functions/ddrescue.py | 13 +++++++++++++ .bin/Scripts/settings/ddrescue.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/.bin/Scripts/functions/ddrescue.py b/.bin/Scripts/functions/ddrescue.py index d37e3207..50a52a62 100644 --- a/.bin/Scripts/functions/ddrescue.py +++ b/.bin/Scripts/functions/ddrescue.py @@ -261,6 +261,7 @@ class RecoveryState(): self.rescued = 0 self.resumed = False self.started = False + self.status = 'Inactive' self.total_size = 0 if mode not in ('clone', 'image'): raise GenericError('Unsupported mode') @@ -768,6 +769,13 @@ def menu_main(state): # Show menu while True: + # Update status + if state.finished: + state.status = ' Finished' + else: + state.status = ' Inactive' + update_sidepane(state) + # Update entries for opt in main_options: opt['Name'] = '[{}] {}'.format( @@ -922,6 +930,7 @@ def run_ddrescue(state, pass_settings): """Run ddrescue pass.""" return_code = -1 aborted = False + state.status = ' In Progress' if state.finished: clear_screen() @@ -1036,6 +1045,9 @@ def run_ddrescue(state, pass_settings): # Done if str(return_code) != '0': # 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... ') # Cleanup @@ -1292,6 +1304,7 @@ def update_sidepane(state): output.append(' {BLUE}Cloning Status{CLEAR}'.format(**COLORS)) else: output.append(' {BLUE}Imaging Status{CLEAR}'.format(**COLORS)) + output.append(state.status) output.append('─────────────────────') # Overall progress diff --git a/.bin/Scripts/settings/ddrescue.py b/.bin/Scripts/settings/ddrescue.py index 620b7214..63812e8f 100644 --- a/.bin/Scripts/settings/ddrescue.py +++ b/.bin/Scripts/settings/ddrescue.py @@ -1,5 +1,7 @@ # Wizard Kit: Settings - ddrescue-tui +from collections import OrderedDict + # General RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs'] USAGE = """ {script_name} clone [source [destination]] From fa05c93bf82bb60f43843e84c4f997bfc7641393 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 20 Mar 2019 15:21:19 -0600 Subject: [PATCH 2/4] Added EToC line to ddrescue-tui side-pane * Is not shown in main menu --- .bin/Scripts/functions/ddrescue.py | 61 ++++++++++++++++++++++++++++++ .bin/Scripts/functions/tmux.py | 15 ++++++++ .bin/Scripts/settings/ddrescue.py | 11 ++++++ 3 files changed, 87 insertions(+) diff --git a/.bin/Scripts/functions/ddrescue.py b/.bin/Scripts/functions/ddrescue.py index 50a52a62..63266278 100644 --- a/.bin/Scripts/functions/ddrescue.py +++ b/.bin/Scripts/functions/ddrescue.py @@ -1,7 +1,9 @@ # Wizard Kit: Functions - ddrescue-tui +import datetime import pathlib import psutil +import pytz import re import signal import stat @@ -262,6 +264,7 @@ class RecoveryState(): self.resumed = False self.started = False self.status = 'Inactive' + self.timezone = pytz.timezone(LINUX_TIME_ZONE) self.total_size = 0 if mode not in ('clone', 'image'): raise GenericError('Unsupported mode') @@ -1324,6 +1327,64 @@ def update_sidepane(state): output.extend(bp.status) output.append(' ') + # EToC + etoc = 'Unknown' + 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)) + if 'In Progress' in state.status: + 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'): + etoc = 'N/A' + else: + 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=state.timezone) + etoc = now + etoc_delta + etoc = etoc.strftime('%Y-%m-%d %H:%M %Z') + except Exception: + # Ignore and leave as current string + pass + + # Finally add EToC + if 'N/A' in etoc.upper(): + output.append('{YELLOW}N/A{CLEAR}'.format(**COLORS)) + else: + output.append(etoc) + # Add line-endings output = ['{}\n'.format(line) for line in output] diff --git a/.bin/Scripts/functions/tmux.py b/.bin/Scripts/functions/tmux.py index e2d1b333..81522268 100644 --- a/.bin/Scripts/functions/tmux.py +++ b/.bin/Scripts/functions/tmux.py @@ -10,6 +10,21 @@ def create_file(filepath): 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): """Get target, or current, pane size, returns tuple.""" x = -1 diff --git a/.bin/Scripts/settings/ddrescue.py b/.bin/Scripts/settings/ddrescue.py index 63812e8f..9b51edd7 100644 --- a/.bin/Scripts/settings/ddrescue.py +++ b/.bin/Scripts/settings/ddrescue.py @@ -1,5 +1,7 @@ # Wizard Kit: Settings - ddrescue-tui +import re + from collections import OrderedDict # General @@ -33,6 +35,15 @@ DDRESCUE_SETTINGS = { '--timeout': {'Enabled': True, 'Value': '5m', }, '-vvvv': {'Enabled': True, 'Hidden': True, }, } +REGEX_REMAINING_TIME = re.compile( + r'remaining time:' + r'\s*((?P\d+)d)?' + r'\s*((?P\d+)h)?' + r'\s*((?P\d+)m)?' + r'\s*((?P\d+)s)?' + r'\s*(?Pn/a)?', + re.IGNORECASE + ) if __name__ == '__main__': From 28cea9697eae687dc484a38333c34d761144cd8f Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 20 Mar 2019 15:45:27 -0600 Subject: [PATCH 3/4] Moved ddrescue-tui EToC code to its own function * Limited refresh rate to every 5 seconds --- .bin/Scripts/functions/ddrescue.py | 112 ++++++++++++++++------------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/.bin/Scripts/functions/ddrescue.py b/.bin/Scripts/functions/ddrescue.py index 63266278..9727bb71 100644 --- a/.bin/Scripts/functions/ddrescue.py +++ b/.bin/Scripts/functions/ddrescue.py @@ -256,6 +256,7 @@ class RecoveryState(): self.block_pairs = [] self.current_pass = 0 self.current_pass_str = '0: Initializing' + self.etoc = '' self.settings = DDRESCUE_SETTINGS.copy() self.finished = False self.panes = {} @@ -403,6 +404,66 @@ class RecoveryState(): elif self.current_pass == 2: 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 % 5 != 0: + # Limit updates to every 5 seconds + 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): """Update overall progress using block_pairs.""" self.rescued = 0 @@ -1328,62 +1389,17 @@ def update_sidepane(state): output.append(' ') # EToC - etoc = 'Unknown' 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)) - if 'In Progress' in state.status: - 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'): - etoc = 'N/A' - else: - 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=state.timezone) - etoc = now + etoc_delta - etoc = etoc.strftime('%Y-%m-%d %H:%M %Z') - except Exception: - # Ignore and leave as current string - pass - - # Finally add EToC - if 'N/A' in etoc.upper(): + state.update_etoc() + if 'N/A' in state.etoc.upper(): output.append('{YELLOW}N/A{CLEAR}'.format(**COLORS)) else: - output.append(etoc) + output.append(state.etoc) # Add line-endings output = ['{}\n'.format(line) for line in output] From c022d3f9c66835fa9f203db79ab8fbccce0d0901 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 20 Mar 2019 15:48:01 -0600 Subject: [PATCH 4/4] Set ddrescue-tui EToC refresh rate in settings --- .bin/Scripts/functions/ddrescue.py | 4 ++-- .bin/Scripts/settings/ddrescue.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.bin/Scripts/functions/ddrescue.py b/.bin/Scripts/functions/ddrescue.py index 9727bb71..545d08e0 100644 --- a/.bin/Scripts/functions/ddrescue.py +++ b/.bin/Scripts/functions/ddrescue.py @@ -416,8 +416,8 @@ class RecoveryState(): elif 'In Progress' not in self.status: # Don't update when EToC is hidden return - if now.second % 5 != 0: - # Limit updates to every 5 seconds + if now.second % ETOC_REFRESH_RATE != 0: + # Limit updates based on settings/ddrescue.py return self.etoc = 'Unknown' diff --git a/.bin/Scripts/settings/ddrescue.py b/.bin/Scripts/settings/ddrescue.py index 9b51edd7..675019ca 100644 --- a/.bin/Scripts/settings/ddrescue.py +++ b/.bin/Scripts/settings/ddrescue.py @@ -35,6 +35,7 @@ DDRESCUE_SETTINGS = { '--timeout': {'Enabled': True, 'Value': '5m', }, '-vvvv': {'Enabled': True, 'Hidden': True, }, } +ETOC_REFRESH_RATE = 30 # in seconds REGEX_REMAINING_TIME = re.compile( r'remaining time:' r'\s*((?P\d+)d)?'