class BackupTask(Thread): ''' Backup the blockchain. ''' def __init__(self): ''' Initialize the backup task. >>> backup_task = BackupTask() >>> backup_task is not None True >>> backup_task.__init__() >>> type(backup_task.locale_tz) <class 'zoneinfo.ZoneInfo.Atlantic/Reykjavik'> >>> backup_task._interrupted False >>> backup_task.manager is None True >>> backup_task.log_name 'blockchain_backup.bitcoin.backup.log' ''' Thread.__init__(self) self._interrupted = False self.log = Log() self.log_name = os.path.basename(get_log_path()) self.manager = None self.to_backup_dir = None self.backup_formatted_time = None self.locale_tz = ZoneInfo(TIME_ZONE) def interrupt(self): ''' Set to true when user clicks the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.interrupt() >>> backup_task._interrupted True ''' self._interrupted = True if self.manager: self.manager.update_progress(backup_utils.STOPPING_BACKUP) def is_interrupted(self): ''' Returns true if user clicked the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.is_interrupted() False ''' return self._interrupted def run(self): ''' Start the backup task. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.run() True >>> backup_task.is_interrupted() False ''' self.log('started BackupTask') ok = True try: self.manager = BitcoinManager(self.log_name) self.manager.update_menu(constants.DISABLE_ITEM) # be sure to check for updates regularly check_for_updates() ok = self.backup() if self.is_interrupted(): self.interrupt_backup() else: if ok: self.manager.update_header(backup_utils.FINISHED) self.manager.update_notice(backup_utils.WALLET_REMINDER) self.manager.update_subnotice('') self.manager.update_menu(constants.ENABLE_ITEM) self.log('starting to update the blockchain') self.manager.update_location(constants.SYNC_URL) else: notice_and_button = '{}{}'.format( constants.CLOSE_WINDOW_NOW, get_ok_button()) self.manager.update_header(backup_utils.HEADER_ERROR) self.manager.update_notice(notice_and_button) self.manager.update_subnotice(backup_utils.CONTACT_US) self.manager.update_progress('') self.manager.update_menu(constants.ENABLE_ITEM) except: # 'bare except' because it catches more than "except Exception" ok = False error = format_exc() self.log(error) if self.manager: self.manager.update_notice(backup_utils.UNEXPECTED_ERROR) if DEBUG: self.manager.update_progress(error) else: self.manager.update_progress(' ') self.manager.update_menu(constants.ENABLE_ITEM) self.log('finished BackupTask') return ok def backup(self): ''' Backup now regardless when the last backup ran. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.backup() True >>> backup_task.is_interrupted() False ''' ok = True backup_level = 1 if not os.path.exists(self.manager.data_dir): os.makedirs(self.manager.data_dir) self.manager.update_progress(backup_utils.STARTING) if not self.is_interrupted(): result = backup_utils.prep_backup(self.manager.data_dir) self.to_backup_dir, self.backup_formatted_time, backup_level = result if os.path.exists(self.to_backup_dir): self.manager.update_progress(backup_utils.STARTING) else: self.manager.update_progress(backup_utils.STARTING_INITIAL) try: backup_process = backup_pid = None if not self.is_interrupted(): self.log('starting backup') backup_process, backup_pid = backup_utils.start_backup( self.manager.data_dir, self.to_backup_dir) if backup_process is not None or backup_pid is not None: if not self.is_interrupted(): backup_utils.wait_for_backup( backup_process, self.is_interrupted, update_progress=self.manager.update_progress) if not self.is_interrupted(): backup_utils.stop_backup(backup_process, backup_pid) else: self.manager.update_progress(backup_utils.NO_MEM_ERROR) ok = False if not self.is_interrupted(): backup_utils.finish_backup(self.manager.data_dir, self.to_backup_dir, self.backup_formatted_time, backup_level) except: # 'bare except' because it catches more than "except Exception" backup_utils.add_backup_flag(self.to_backup_dir, self.backup_formatted_time) ok = False self.log(format_exc()) if ok and not self.is_interrupted(): backup_utils.save_all_metadata(self.manager.data_dir, self.to_backup_dir) self.log('saved all metadata') if ok and not self.is_interrupted(): # remove the old debug logs; we back them up in case there's an error for entry in os.scandir(self.manager.data_dir): if entry.name.startswith(constants.DEBUG_PREFIX) and \ entry.name.endswith(constants.LOG_SUFFIX): os.remove(entry.path) self.manager.update_subnotice('') self.manager.update_progress(backup_utils.FINISHED_REMINDER) return ok def interrupt_backup(self): ''' End user interrupts the backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.to_backup_dir, backup_task.backup_formatted_time, __ = backup_utils.prep_backup(backup_task.manager.data_dir) >>> backup_process, backup_pid = backup_utils.start_backup(backup_task.manager.data_dir, ... backup_task.to_backup_dir) >>> test_utils.start_fake_backup() >>> backup_task.interrupt_backup() True ''' MAX_SECS = 3 seconds = 0 self.log('interrupting backup') if self.to_backup_dir is not None: # remove all files that suggest this backup is complete backup_utils.delete_last_updated_files(self.to_backup_dir) # add a flag that we started to use this dir to backup backup_utils.add_backup_flag(self.to_backup_dir, self.backup_formatted_time) try: bin_dir = os.path.join(virtualenv_dir(), 'bin') args = [ os.path.join(bin_dir, 'killmatch'), constants.BACKUP_PROGRAM ] attempts = 0 while backup_utils.is_backup_running() and attempts < 3: result = command.run(*args).stdout self.log(f'result of stopping backup: {result}') if backup_utils.is_backup_running(): sleep(3) attempts += 1 except CalledProcessError as cpe: self.log(cpe) self.log(format_exc()) while seconds < MAX_SECS: self.manager.update_header(backup_utils.STOPPED_BACKUP_HEADER) self.manager.update_progress(backup_utils.STOPPED_BACKUP_PROGRESS) self.manager.notify_close_window() sleep(1) seconds += 1 # return value is for testing purposes only return not backup_utils.is_backup_running()
class RestoreTask(Thread): ''' Restore the blackchain. Errors users might see that prompt them to need a restore: Error loading block database. / Do you want to rebuild the block database now? ''' RESTORE_DONE = 'Restore done' RESTORE_FINISHED = '<br/> Finished restore. You are ready to continue using Bitcoin Core.' STOPPED_RESTORE = 'Restore stopped on your request' RESTORE_BACKUPS_OK = '<strong>Backups were temporarily disabled.</strong> After you successfully <a href="/bitcoin/access_wallet/">Access your wallet</a> or <a href="/bitcoin/update/">Update</a> the blockchain, then you should <a href="/bitcoin/change_backup_status/">re-enable the backups</a>.' RESTORE_WARNING = 'Review the details below:' RESTORE_UNABLE_TO_START = 'Unable to start restore -- is there enough memory and disk space?' RESTORE_ERROR = 'Unexpected error while restoring files. Check logs for details.' RESTORE_UNEXPECTED_ERROR = 'Unexpected error occurred while restoring blockchain.' STOPPING_RESTORE = 'Stopping restore as you requested' STOPPED_RESTORE = "Stopped restoring Bitcoin blockchain" STOP_RESTORE_NOT_COMPLETE = "It's very likely Bitcoin Core will not operate until you complete the restore." STOP_RESTORE_UNEXPECTED_ERROR = 'Unexpected error occurred while stopping restore.' def __init__(self, restore_dir): ''' Initialize the restore task. >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task is not None True >>> restore_task.__init__(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.restore_dir '/tmp/bitcoin/data/testnet3/backups/level1' >>> restore_task._interrupted False >>> restore_task.manager is None True >>> restore_task.log_name 'blockchain_backup.bitcoin.restore.log' ''' Thread.__init__(self) self.restore_dir = restore_dir self._interrupted = False self.log = get_log() self.log_name = os.path.basename(get_log_path()) self.manager = None def interrupt(self): ''' Set to true when user clicks the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.interrupt() >>> restore_task._interrupted True ''' self._interrupted = True if self.manager: self.manager.update_progress(self.STOPPING_RESTORE) def is_interrupted(self): ''' Returns true if user clicked the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.is_interrupted() False ''' return self._interrupted def run(self): ''' Start the restore task. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.run() True >>> restore_task.is_interrupted() False >>> restore_task = RestoreTask('/bad/bitcoin/data/testnet3/backups/level1') >>> restore_task.run() False >>> restore_task.is_interrupted() False ''' ok = False try: self.log('started RestoreTask') self.manager = BitcoinManager(self.log_name) if os.path.exists(self.manager.data_dir) and os.path.exists( self.restore_dir): ok = self.restore() if self.is_interrupted(): self.interrupt_restore() self.log('finished RestoreTask') else: ok = False self.log( f'data dir exists: {os.path.exists(self.manager.data_dir)}' ) self.log(f'restore exists: {os.path.exists(self.restore_dir)}') self.log('RestoreTask terminated') self.manager.update_progress(self.RESTORE_UNEXPECTED_ERROR) self.manager.update_menu(constants.ENABLE_ITEM) except FileNotFoundError as fne: FILE_NOT_FOUND_PREFIX = '[Errno 2] No such file or directory: ' ok = False error = str(fne) i = error.find(FILE_NOT_FOUND_PREFIX) if i >= 0: error = error[len(FILE_NOT_FOUND_PREFIX):] self.log(f'file not found: {error}') self.remove_last_updated_file() if self.manager: self.manager.update_notice(error) self.manager.update_progress(self.RESTORE_UNEXPECTED_ERROR) self.manager.update_menu(constants.ENABLE_ITEM) except: # 'bare except' because it catches more than "except Exception" ok = False self.log(format_exc()) if self.manager: self.manager.update_progress(self.RESTORE_UNEXPECTED_ERROR) self.manager.update_menu(constants.ENABLE_ITEM) return ok def restore(self): ''' Restore blockchain from newest backup, or the backup the user selected. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.restore() True >>> restore_task.is_interrupted() False ''' ok = True self.log(f'starting restoration from {self.restore_dir}') self.manager.update_menu(constants.DISABLE_ITEM) bitcoin_utils.check_for_updates(force=True, reason='restore') ok = self.restore_files_and_dirs() if ok and not self.is_interrupted(): # pass the args because delete_extra_files is recursive ok = self.delete_extra_files(self.restore_dir, self.manager.data_dir) if ok and not self.is_interrupted(): ok = self.restore_metadata() if ok and not self.is_interrupted(): self.restore_bcb_state() if not self.is_interrupted(): if ok: # we don't want to warn that a backup is needed # just after we restore from a backup set_last_backed_up_time(now()) else: self.remove_last_updated_file() if self.is_interrupted(): self.log('restore stopped by user') self.manager.update_header(self.RESTORE_STOPPED) self.manager.update_notice(self.RESTORE_WARNING) self.manager.update_subnotice(' ') else: if ok: self.log('finished bulk restore') state.set_backups_enabled(False) self.log( 'stopping backups after restore until user verifies everything ok' ) self.manager.update_header(self.RESTORE_DONE) self.manager.notify_done(notice=self.RESTORE_FINISHED) self.manager.update_subnotice(self.RESTORE_BACKUPS_OK) else: self.manager.update_notice(self.RESTORE_WARNING) self.manager.update_subnotice(' ') self.manager.update_menu(constants.ENABLE_ITEM) return ok def restore_files_and_dirs(self): ''' Restore files and directories to a previous state of the blockchain. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_dir = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1') >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.restore_files_and_dirs() True >>> test_utils.stop_restore() >>> test_utils.start_fake_restore() >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.restore_files_and_dirs() True >>> test_utils.stop_restore() ''' ok = True try: if bitcoin_utils.is_restore_running(): restore_pid = get_pid(constants.RESTORE_PROGRAM) restore_process = None self.log('{} is already running using pid: {}'.format( constants.RESTORE_PROGRAM, restore_pid)) else: self.log('starting restore') restore_process = self.start_restore() restore_pid = None if restore_process is not None or restore_pid is not None: self.wait_for_restore(restore_process) self.stop_restore(restore_process, restore_pid) else: self.manager.update_progress(self.RESTORE_UNABLE_TO_START) ok = False except: # 'bare except' because it catches more than "except Exception" ok = False self.log(format_exc()) if not ok: self.manager.update_progress(self.RESTORE_ERROR) return ok def start_restore(self): ''' Start restoring the files and directories. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_process = restore_task.start_restore() >>> restore_process is not None True >>> test_utils.stop_restore() ''' # do NOT use the --delete flag -- the backups dir itself would be deleted args = [] # restore program is a link to safecopy bin_dir = os.path.join(virtualenv_dir(), 'bin') args.append(os.path.join(bin_dir, constants.RESTORE_PROGRAM)) args.append('--exclude') args.append(bitcoin_utils.get_excluded_files()) args.append('--verbose') args.append('--quick') args.append(f'{self.restore_dir}/*') args.append(self.manager.data_dir) restore_process = Popen(args, stdout=PIPE, universal_newlines=True) return restore_process def wait_for_restore(self, restore_process): ''' Wait for the restore to finish and display data while waiting. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_process = restore_task.start_restore() >>> restore_process is not None True >>> restore_task.wait_for_restore(restore_process) >>> test_utils.stop_restore() >>> restore_task.wait_for_restore(None) >>> test_utils.stop_restore() ''' def show_line(line): if line is not None and line.startswith('Copying:'): index = line.rfind(os.sep) if index > 0: line = f'<strong>Copying: </strong>{line[index + 1:]}' self.manager.update_progress(line) self.log('starting to wait for restore') if restore_process is None: log_path = os.path.join(BASE_LOG_DIR, whoami(), 'bcb-restore.log') # wait until the log appears while bitcoin_utils.is_restore_running( ) and not self.is_interrupted(): if not os.path.exists(log_path): sleep(1) # then display the restore details while bitcoin_utils.is_restore_running( ) and not self.is_interrupted(): with open(log_path, 'rt') as restore_log: show_line(restore_log.readline()) else: while (restore_process.poll() is None and not self.is_interrupted()): show_line(restore_process.stdout.readline()) self.log('finished waiting for restore') def stop_restore(self, restore_process, restore_pid): ''' Stop restore. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_process = restore_task.start_restore() >>> restore_process is not None True >>> restore_task.stop_restore(restore_process, None) >>> test_utils.start_fake_restore() >>> restore_pid = get_pid(constants.RESTORE_PROGRAM) >>> restore_task.stop_restore(None, restore_pid) ''' try: if restore_process is None: if bitcoin_utils.is_restore_running(): bin_dir = os.path.join(virtualenv_dir(), 'bin') args = [ os.path.join(bin_dir, 'killmatch'), '"{} --exclude {}"'.format( constants.RESTORE_PROGRAM, bitcoin_utils.get_excluded_files()) ] result = command.run(*args).stdout self.log(f'killing restore result: {result}') try: pid, returncode = os.waitpid(restore_pid, os.P_WAIT) self.log(f'waitpid {pid} return code: {returncode}') except ChildProcessError: self.log('restore_pid already dead') else: # if bcb-restore hasn't stopped yet, then kill it if restore_process.poll() is None: self.log('killing restore') restore_process.terminate() # wait until restore terminates restore_process.wait() self.log(f'restore return code: {restore_process.returncode}') except: # 'bare except' because it catches more than "except Exception" self.log(f'error while stopping restore\n{format_exc()}') self.log(f'error while stopping restore\n{format_exc()}') def delete_extra_files(self, from_dir, to_dir): ''' Delete files that are not part of the current blockchain. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> from_dir = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1') >>> restore_task = RestoreTask(from_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.delete_extra_files(from_dir, restore_task.manager.data_dir) True ''' BACKUP_SUBDIR = bitcoin_utils.get_backup_subdir() EXCLUDED_FILES = bitcoin_utils.get_excluded_files() ok = True try: to_entries = os.scandir(to_dir) for entry in to_entries: if entry.name in [EXCLUDED_FILES]: self.log(f'skipping {entry.name}') elif entry.name.startswith(constants.LAST_UPDATED_PREFIX): os.remove(entry.path) self.log(f'deleted {entry.path}') elif entry.is_file(): from_file = os.path.join(from_dir, entry.name) if not os.path.exists(from_file): os.remove(entry.path) self.log(f'deleted {entry.path}') elif entry.is_dir() and entry.name != BACKUP_SUBDIR: from_file = os.path.join(from_dir, entry.name) if os.path.exists(from_file): ok = self.delete_extra_files( os.path.join(from_dir, entry.name), entry.path) else: rmtree(entry.path) self.log(f'deleted dir tree: {entry.path}') if not ok or self.is_interrupted(): break except: # 'bare except' because it catches more than "except Exception" ok = False self.log(format_exc()) if not ok and not self.is_interrupted(): self.manager.update_progress(self.RESTORE_ERROR) return ok def restore_metadata(self): ''' Restore the metadata in the data dir. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_dir = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1') >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.restore_files_and_dirs() True >>> restore_task.restore_metadata() True ''' self.log('starting to restore metadata') missing_files = [] bad_metadata_files = [] ok = True json_filename = os.path.join(self.restore_dir, constants.METADATA_FILENAME) if os.path.exists(json_filename): with open(json_filename, 'r') as json_file: lines = json_file.readlines() for line in lines: path, stats = json.loads(line) full_path = os.path.join(self.manager.data_dir, path) if os.path.exists(full_path): try: os.utime(full_path, (stats['st_atime'], stats['st_mtime'])) os.chmod(full_path, stats['st_mode']) os.chown(full_path, stats['st_uid'], stats['st_gid']) except: # 'bare except' because it catches more than "except Exception" bad_metadata_files.append(full_path) self.log(f'Unable to set metadata for {full_path}') self.log(format_exc()) else: missing_files.append(full_path) if self.is_interrupted(): break else: missing_files.append(json_filename) if (bad_metadata_files or missing_files) and not self.is_interrupted(): self.report_errors(bad_metadata_files, missing_files) ok = False if ok and not self.is_interrupted(): self.log('finished restoring metadata') return ok def restore_bcb_state(self): ''' Restore the state of the blockchain_backup database. Don't change the preferences as the user may have changed them since the last backup. >>> from shutil import copyfile >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> dst_state_ok_json = None >>> dst_state_json = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1/blockchain_backup_database/state.json') >>> if os.path.exists(dst_state_json): ... dst_state_ok_json = copyfile(dst_state_json, dst_state_json + '.ok') >>> copyfile(os.path.join(gettempdir(), 'bitcoin/state.json'), dst_state_json) '/tmp/bitcoin/data/testnet3/backups/level1/blockchain_backup_database/state.json' >>> original_last_block_updated = state.get_last_block_updated() >>> original_last_known_block = state.get_last_known_block() >>> original_last_block_time = state.get_last_block_time() >>> original_last_backup_level = state.get_last_backup_level() >>> restore_dir = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1') >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.restore_bcb_state() True >>> original_last_block_updated != state.get_last_block_updated() True >>> original_last_known_block != state.get_last_known_block() True >>> original_last_block_time != state.get_last_block_time() True >>> if dst_state_ok_json is not None and os.path.exists(dst_state_ok_json): ... x = copyfile(dst_state_ok_json, dst_state_json) ... os.remove(dst_state_ok_json) >>> test_utils.init_database() >>> restore_dir = '/bad/bitcoin/data/testnet3/backups/level1' >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.restore_bcb_state() False ''' ok = False blockchain_backup_db_restore_dir = os.path.join( self.restore_dir, constants.BLOCKCHAIN_BACKUP_DB_DIR) full_path = os.path.join(blockchain_backup_db_restore_dir, constants.STATE_BACKUP_FILENAME) if os.path.exists(full_path): try: with open(full_path, 'r') as infile: json_data = json.loads(infile.read()) fields = json_data[0]['fields'] state.set_last_block_updated(fields['last_block_updated']) state.set_last_known_block(fields['last_known_block']) state.set_last_block_time(fields['last_block_time']) ok = True self.log('restored database state') except: # 'bare except' because it catches more than "except Exception" # restoring the blockchain_backup state is not # critical to maintaining the blockchain self.log(format_exc()) else: self.log( f'Unable to restore state because {full_path} does not exist') return ok def interrupt_restore(self): ''' User wants the restoration interrupted. This is not recommended because it almost always leaves the blockchain in unusable shape. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> restore_dir = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1') >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> test_utils.start_fake_restore() >>> restore_task.interrupt_restore() True ''' max_secs = 3 seconds = 0 try: bin_dir = os.path.join(virtualenv_dir(), 'bin') args = [ os.path.join(bin_dir, 'killmatch'), constants.RESTORE_PROGRAM ] args = [ os.path.join('/usr/local/bin', 'killmatch'), constants.RESTORE_PROGRAM ] self.log(f'args: {args}') attempts = 0 while bitcoin_utils.is_restore_running() and attempts < 5: command.run(*args) if bitcoin_utils.is_restore_running(): sleep(3) attempts += 1 except CalledProcessError as cpe: self.log(cpe) self.log(format_exc()) # a new page was displayed so give socketio time to connect while seconds < max_secs: self.manager.update_header(self.STOPPED_RESTORE) self.manager.update_subnotice(self.STOP_RESTORE_NOT_COMPLETE) self.manager.notify_done() sleep(1) seconds += 1 # return value is for testing purposes only return not bitcoin_utils.is_restore_running() def remove_last_updated_file(self): ''' Remove the last updated file so we don't try to restore from this directory again and will use it to backup the next time. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.setup_tmp_dir() >>> subdir_existed = test_utils.home_bitcoin_dir_exists() >>> restore_dir = os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1') >>> restore_task = RestoreTask(restore_dir) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> restore_task.remove_last_updated_file() True >>> timestamp = '2019-03-29 16:48' >>> filename = os.path.join(restore_dir, '{}{}'.format(constants.LAST_UPDATED_PREFIX, timestamp)) >>> with open(filename, "wt") as output_file: ... output_file.write(timestamp) 16 ''' ok = False filenames = os.listdir(self.restore_dir) for filename in filenames: if filename.startswith(constants.LAST_UPDATED_PREFIX): os.remove(os.path.join(self.restore_dir, filename)) ok = True break # don't allow any more backups until the user tells us it's ok state.set_backups_enabled(False) return ok def report_errors(self, bad_metadata_files, missing_files): ''' Report any errors detected during restoration. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.setup_tmp_dir() >>> subdir_existed = test_utils.home_bitcoin_dir_exists() >>> restore_task = RestoreTask(os.path.join(gettempdir(), 'bitcoin/data/testnet3/backups/level1')) >>> restore_task.manager = BitcoinManager(restore_task.log_name) >>> bad_metadata_files = None >>> missing_files = None >>> restore_task.report_errors(bad_metadata_files, missing_files) ['Please contact [email protected] for assistance.'] >>> bad_metadata_files = [os.path.join(gettempdir(), 'bitcoin/data/testnet3/blocks')] >>> missing_files = None >>> restore_task.report_errors(bad_metadata_files, missing_files) ['Please contact [email protected] for assistance.', 'The following files could not have their metadata restored:', '/tmp/bitcoin/data/testnet3/blocks'] >>> bad_metadata_files = None >>> missing_files = [os.path.join(gettempdir(), 'bitcoin/data/testnet3/chainstate/2560245.ldb')] >>> restore_task.report_errors(bad_metadata_files, missing_files) ['Please contact [email protected] for assistance.', 'The following files are missing:', '/tmp/bitcoin/data/testnet3/chainstate/2560245.ldb'] >>> bad_metadata_files = [os.path.join(gettempdir(), 'bitcoin/data/testnet3/blocks')] >>> missing_files = [os.path.join(gettempdir(), 'bitcoin/data/testnet3/chainstate/2560245.ldb')] >>> restore_task.report_errors(bad_metadata_files, missing_files) ['Please contact [email protected] for assistance.', 'The following files could not have their metadata restored:', '/tmp/bitcoin/data/testnet3/blocks', 'The following files are missing:', '/tmp/bitcoin/data/testnet3/chainstate/2560245.ldb'] >>> test_utils.delete_home_bitcoin_subdir(subdir_existed) ''' lines = [] lines.append('Please contact [email protected] for assistance.') self.remove_last_updated_file() if bad_metadata_files: lines.append( 'The following files could not have their metadata restored:') for filename in bad_metadata_files: lines.append(filename) if missing_files: lines.append('The following files are missing:') for filename in missing_files: lines.append(filename) if lines: for line in lines: self.log(line) if not self.is_interrupted(): self.manager.update_progress('\n'.join(lines)) # only returning the lines so tests can verify everything's working return lines
class BackupTask(Thread): ''' Backup the blockchain. ''' STARTING = 'Starting to back up the blockchain.' STARTING_INITIAL = 'Configuring new backup level -- this could take a long time' COPYING = '<strong>Copying: </strong>{}' FINISHED = 'Finished backup of the blockchain.' FINISHED_REMINDER = "Finished backing up the blockchain.<p>Don't forget to backup your wallet." WALLET_REMINDER = 'Do not forget to back up your wallet.' NO_MEM_ERROR = 'Unable to start backup -- is there enough memory and disk space?' HEADER_ERROR = 'Error while backing up the blockchain.' UNEXPECTED_ERROR = 'Unexpected error occurred during backup.' CONTACT_US = 'Contact [email protected]' STOPPING_BACKUP = 'Stopping backup as you requested' STOPPED_BACKUP_HEADER = "Stopped backing up Bitcoin blockchain" BACKUP_INCOMPLETE = "Don't forget that the latest backup is incomplete." DIR_UNUSABLE = 'It cannot be used to restore the blockchain.' STOPPED_BACKUP_PROGRESS = f'{BACKUP_INCOMPLETE} {DIR_UNUSABLE}' def __init__(self): ''' Initialize the backup task. >>> backup_task = BackupTask() >>> backup_task is not None True >>> backup_task.__init__() >>> type(backup_task.locale_tz) <class 'pytz.tzfile.Atlantic/Reykjavik'> >>> backup_task._interrupted False >>> backup_task.manager is None True >>> backup_task.log_name 'blockchain_backup.bitcoin.backup.log' ''' Thread.__init__(self) self._interrupted = False self.log = get_log() self.log_name = os.path.basename(get_log_path()) self.manager = None self.to_backup_dir = None self.backup_level = 1 self.backup_formatted_time = None self.locale_tz = timezone(TIME_ZONE) def interrupt(self): ''' Set to true when user clicks the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.interrupt() >>> backup_task._interrupted True ''' self._interrupted = True if self.manager: self.manager.update_progress(self.STOPPING_BACKUP) def is_interrupted(self): ''' Returns true if user clicked the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.is_interrupted() False ''' return self._interrupted def run(self): ''' Start the backup task. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.run() True >>> backup_task.is_interrupted() False ''' self.log('started BackupTask') ok = True try: self.manager = BitcoinManager(self.log_name) self.manager.update_menu(constants.DISABLE_ITEM) # be sure to check for updates regularly bitcoin_utils.check_for_updates() ok = self.backup() if self.is_interrupted(): self.interrupt_backup() else: if ok: self.manager.update_header(self.FINISHED) self.manager.update_notice(self.WALLET_REMINDER) self.manager.update_subnotice('') self.manager.update_menu(constants.ENABLE_ITEM) self.log('starting to update the blockchain') self.manager.update_location(constants.SYNC_URL) else: notice_and_button = '{}{}'.format( constants.CLOSE_WINDOW_NOW, bitcoin_utils.get_ok_button()) self.manager.update_header(self.HEADER_ERROR) self.manager.update_notice(notice_and_button) self.manager.update_subnotice(self.CONTACT_US) self.manager.update_progress('') self.manager.update_menu(constants.ENABLE_ITEM) except: # 'bare except' because it catches more than "except Exception" ok = False error = format_exc() self.log(error) if self.manager: self.manager.update_notice(self.UNEXPECTED_ERROR) if DEBUG: self.manager.update_progress(error) else: self.manager.update_progress(' ') self.manager.update_menu(constants.ENABLE_ITEM) self.log('finished BackupTask') return ok def backup(self): ''' Backup now regardless when the last backup ran. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.backup() True >>> backup_task.is_interrupted() False ''' ok = True if not os.path.exists(self.manager.data_dir): os.makedirs(self.manager.data_dir) self.manager.update_progress(self.STARTING) if not self.is_interrupted(): self.prep_backup() try: backup_process = backup_pid = None if not self.is_interrupted(): self.log('starting backup') backup_process, backup_pid = self.start_backup() if backup_process is not None or backup_pid is not None: if not self.is_interrupted(): self.wait_for_backup(backup_process) if not self.is_interrupted(): self.stop_backup(backup_process, backup_pid) else: self.manager.update_progress(self.NO_MEM_ERROR) ok = False if not self.is_interrupted(): self.finish_backup() except: # 'bare except' because it catches more than "except Exception" self.add_backup_flag() ok = False self.log(format_exc()) if ok and not self.is_interrupted(): self.save_all_metadata() self.log('saved all metadata') if ok and not self.is_interrupted(): # remove the old debug logs; we back them up in case there's an error for entry in os.scandir(self.manager.data_dir): if entry.name.startswith(constants.DEBUG_PREFIX) and \ entry.name.endswith(constants.LOG_SUFFIX): os.remove(entry.path) self.manager.update_subnotice('') self.manager.update_progress(self.FINISHED_REMINDER) return ok def prep_backup(self): ''' Prepare to backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_task.backup_formatted_time is not None True >>> backup_task.to_backup_dir.startswith('/') True ''' backup_time = now() time_stamp = backup_time.isoformat(sep=' ') if not self.manager.rename_logs(time_stamp=time_stamp): self.log('no logs to rename') self.backup_formatted_time = bitcoin_utils.format_time(time_stamp) self.to_backup_dir, last_backup_time = self.get_backup_dir_and_time() if os.path.exists(self.to_backup_dir): self.manager.update_progress(self.STARTING) self.log(f'backup {os.path.basename(self.to_backup_dir)}') self.log(f'last backed up {last_backup_time}') else: self.manager.update_progress(self.STARTING_INITIAL) os.makedirs(self.to_backup_dir) self.log(f'created new backup dir: {self.to_backup_dir}') # flag that we're using this dir to backup self.add_backup_flag() # remove the last backup file if it exists bitcoin_utils.delete_last_updated_files(self.to_backup_dir) def start_backup(self): ''' Start backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_process, backup_pid = backup_task.start_backup() >>> backup_process is not None True >>> backup_pid is None True >>> test_utils.stop_backup() ''' bin_dir = os.path.join(virtualenv_dir(), 'bin') data_dir = self.manager.data_dir if not data_dir.endswith(os.sep): data_dir += os.sep if is_backup_running(): backup_process = None backup_pid = get_pid(constants.BACKUP_PROGRAM) self.log('{} is already running using pid: {}'.format( constants.BACKUP_PROGRAM, backup_pid)) else: backup_pid = None args = [] # "bcb-backup" is a link to safecopy so we can distinguish it when we kill it args.append(os.path.join(bin_dir, constants.BACKUP_PROGRAM)) args.append('--exclude') args.append(get_excluded_files()) args.append('--verbose') args.append('--quick') args.append('--delete') args.append(f'{data_dir}*') args.append(self.to_backup_dir) # Popen appears to report "'list' object has no attribute 'split'" # the docs state Popen should pass a sequence as the first arg backup_process = Popen(args, stdout=PIPE, universal_newlines=True) return backup_process, backup_pid def wait_for_backup(self, backup_process): ''' Wait for the backup to finish and display data while waiting. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_process, backup_pid = backup_task.start_backup() >>> backup_process is not None True >>> backup_pid is None True >>> backup_task.wait_for_backup(backup_process) >>> test_utils.stop_backup() >>> backup_task.wait_for_backup(None) >>> test_utils.stop_backup() ''' def show_line(line): if line is not None and line.startswith('Copying:'): index = line.rfind(os.sep) if index > 0: line = self.COPYING.format(line[index + 1:]) if line != self.COPYING: self.manager.update_progress(line) self.log('starting to wait for backup') if backup_process is None: log_path = os.path.join(BASE_LOG_DIR, whoami(), 'bcb-backup.log') # wait until the log appears while (is_backup_running() and not self.is_interrupted()): if not os.path.exists(log_path): sleep(1) # then display the backup details while (is_backup_running() and not self.is_interrupted()): with open(log_path, 'rt') as backup_log: show_line(backup_log.readline()) else: while (backup_process.poll() is None and not self.is_interrupted()): show_line(backup_process.stdout.readline()) if self.is_interrupted(): self.log('waiting for backup interrupted') else: self.log('finished waiting for backup') def stop_backup(self, backup_process, backup_pid): ''' Stop backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_process, backup_pid = backup_task.start_backup() >>> backup_process is not None True >>> backup_pid is None True >>> backup_task.stop_backup(backup_process, backup_pid) >>> test_utils.start_fake_backup() >>> backup_pid = get_pid(constants.BACKUP_PROGRAM) >>> backup_task.stop_backup(None, backup_pid) ''' try: if backup_process is None and backup_pid is not None: if is_backup_running(): bin_dir = os.path.join(virtualenv_dir(), 'bin') args = [ os.path.join(bin_dir, 'killmatch'), '"{} --exclude {}"'.format(constants.BACKUP_PROGRAM, get_excluded_files()) ] result = command.run(*args).stdout self.log(f'killing backup result: {result}') try: pid, returncode = os.waitpid(backup_pid, os.P_WAIT) self.log(f'waitpid {pid} return code: {returncode}') except ChildProcessError: self.log('backup_pid already dead') else: # if bcb-backup hasn't stopped yet, then kill it if backup_process is None: self.log('not back process active') else: if backup_process.poll() is None: self.log('killing backup') backup_process.terminate() # wait until backup terminates backup_process.wait() self.log( f'backup return code: {backup_process.returncode}') except: # 'bare except' because it catches more than "except Exception" self.log(f'error while stopping backup\n{format_exc()}') def finish_backup(self): ''' Finish the backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_process, backup_pid = backup_task.start_backup() >>> backup_process is not None True >>> backup_pid is None True >>> backup_task.finish_backup() >>> test_utils.stop_backup() ''' # make sure all the files starting with dot are copied using # standard python because argparse in backup tries to expand the filename for entry in os.scandir(self.manager.data_dir): if entry.name.startswith('.') and entry.name != '.walletlock': to_path = os.path.join(self.to_backup_dir, entry.name) copyfile(entry.path, to_path) # add a last backup file and remove the semaphore file showing we're updating this dir last_updated_filename = os.path.join( self.to_backup_dir, '{}{}'.format(constants.LAST_UPDATED_PREFIX, self.get_backup_formatted_time())) with open(last_updated_filename, 'wt') as f: f.write(self.get_backup_formatted_time()) updating_filename = self.get_backup_flag_name() if updating_filename and os.path.exists(updating_filename): os.remove(updating_filename) # save the last backup time in the database backup_time = datetime.strptime(self.get_backup_formatted_time(), '%Y-%m-%d %H:%M') state.set_last_backed_up_time(self.locale_tz.localize(backup_time)) state.set_last_backup_level(self.backup_level) self.save_bcb_database() def interrupt_backup(self): ''' End user interrupts the backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_process, backup_pid = backup_task.start_backup() >>> test_utils.start_fake_backup() >>> backup_task.interrupt_backup() True ''' MAX_SECS = 3 seconds = 0 self.log('interrupting backup') if self.to_backup_dir is not None: # remove all files that suggest this backup is complete bitcoin_utils.delete_last_updated_files(self.to_backup_dir) # add a flag that we started to use this dir to backup self.add_backup_flag() try: bin_dir = os.path.join(virtualenv_dir(), 'bin') args = [ os.path.join(bin_dir, 'killmatch'), constants.BACKUP_PROGRAM ] attempts = 0 while is_backup_running() and attempts < 3: result = command.run(*args).stdout self.log(f'result of stopping backup: {result}') if is_backup_running(): sleep(3) attempts += 1 except CalledProcessError as cpe: self.log(cpe) self.log(format_exc()) # a new page was displayed so give socketio time to connect while seconds < MAX_SECS: self.manager.update_header(self.STOPPED_BACKUP_HEADER) self.manager.update_progress(self.STOPPED_BACKUP_PROGRESS) self.manager.notify_close_window() sleep(1) seconds += 1 # return value is for testing purposes only return not bitcoin_utils.is_backup_running() def add_backup_flag(self): ''' Add a flag so we know we were updating this backup dir. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> updating_filename = backup_task.add_backup_flag() >>> os.path.exists(updating_filename) True >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> updating_filename = backup_task.add_backup_flag() >>> updating_filename is None True ''' updating_filename = self.get_backup_flag_name() if updating_filename: with open(updating_filename, 'wt') as output_file: output_file.write(self.get_backup_formatted_time()) return updating_filename def get_backup_flag_name(self): ''' Add a flag so we know we were updating this backup dir. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.get_backup_flag_name() is None True >>> backup_task.prep_backup() >>> backup_task.get_backup_flag_name().startswith('/') True ''' if self.to_backup_dir is None: backup_flag_name = None self.log('warning: backup dir not defined so no backup flag file') else: backup_flag_name = os.path.join( self.to_backup_dir, '{}{}'.format(constants.UPDATING_PREFIX, self.get_backup_formatted_time())) return backup_flag_name def get_backup_formatted_time(self): ''' Get the formatted time for the backup. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.get_backup_formatted_time() is not None True ''' if self.backup_formatted_time is None: backup_time = now() self.log(f'backup_time: {backup_time}') self.backup_formatted_time = bitcoin_utils.format_time( backup_time.isoformat(sep=' ')) self.log(f'backup_formatted time: {self.backup_formatted_time}') return self.backup_formatted_time def get_backup_dir_and_time(self): ''' Get the oldest backup dir or the one that's only been partially backed up. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_dirname, oldest_backup_time = backup_task.get_backup_dir_and_time() >>> backup_dirname.startswith('/') True >>> isinstance(oldest_backup_time, datetime) True ''' self.log('getting low level backup dir') backup_dirname = None oldest_backed_up_time = now() + timedelta(days=1) backup_dir = get_backup_dir() if os.path.exists(backup_dir): backup_dirname, oldest_backed_up_time = self.search_entries( backup_dir, oldest_backed_up_time) else: self.log(f'creating new backup parent: {backup_dir}') os.makedirs(backup_dir) backup_dirname = os.path.join( backup_dir, '{}{}'.format(constants.BACKUPS_LEVEL_PREFIX, '1')) oldest_backed_up_time = now() self.log('never backed up') i = backup_dirname.find(constants.BACKUPS_LEVEL_PREFIX) if i > 0: self.backup_level = int( backup_dirname[i + len(constants.BACKUPS_LEVEL_PREFIX):]) self.log(f'backup dirname: {backup_dirname}') self.log(f'oldest_backed_up_time: {oldest_backed_up_time}') return backup_dirname, oldest_backed_up_time def search_entries(self, backup_dir, oldest_backed_up_time): ''' Scan the backup directories. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_dir = '/tmp/bitcoin/data-with-blocks/testnet3/backups/' >>> oldest_backup = now() - timedelta(days=10) >>> backup_dirname, oldest_backup_time = backup_task.search_entries( ... backup_dir, oldest_backup) >>> backup_dirname.startswith('/') True >>> isinstance(oldest_backup_time, datetime) True ''' total_backup_levels = 0 found_partial = False backup_dirname = os.path.join(backup_dir, f'{constants.BACKUPS_LEVEL_PREFIX}1') entries = os.scandir(backup_dir) for entry in entries: # look inside each backup level if entry.is_dir() and entry.name.startswith( constants.BACKUPS_LEVEL_PREFIX): total_backup_levels += 1 backed_up_time = None backup_with_timestamp = False filenames = os.listdir(entry.path) for filename in filenames: # for a partial backup if filename.startswith(constants.UPDATING_PREFIX): backup_dirname = entry.path backed_up_time = seconds_to_datetime( os.path.getmtime(entry.path)) backup_with_timestamp = True found_partial = True self.log( f'found a partial updating backup in {entry.name}') break # or for the file that includes the last backup date elif filename.startswith(constants.LAST_UPDATED_PREFIX): backup_with_timestamp = True older_date_found, oldest_backed_up_time = self.compare_dates( filename[len(constants.LAST_UPDATED_PREFIX):], oldest_backed_up_time) if older_date_found: backup_dirname = entry.path backed_up_time = oldest_backed_up_time # if there is no backup timestamp, then this is a partial backup if not backup_with_timestamp: found_partial = True if not found_partial and backed_up_time is None: older_date_found, oldest_backed_up_time = self.compare_dates( seconds_to_datetime(os.path.getmtime(entry.path)), oldest_backed_up_time) if older_date_found: backup_dirname = entry.path backed_up_time = oldest_backed_up_time if found_partial: break if get_backup_levels() > total_backup_levels and not found_partial: backup_dirname = os.path.join( backup_dir, '{}{}'.format(constants.BACKUPS_LEVEL_PREFIX, total_backup_levels + 1)) return backup_dirname, oldest_backed_up_time def compare_dates(self, backed_up_on, oldest_backed_up_time): ''' Compare the dates to find the oldest one. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backed_up = now() >>> oldest_backup = now() - timedelta(days=10) >>> older_date_found, oldest_backed_up_time = backup_task.compare_dates( ... backed_up, oldest_backup) >>> older_date_found False >>> isinstance(oldest_backed_up_time, datetime) True ''' def get_date_with_tz(original_date): try: new_date = self.locale_tz.localize(original_date) except: # 'bare except' because it catches more than "except Exception" new_date = original_date return new_date older_date_found = False oldest_backed_up_time = get_date_with_tz(oldest_backed_up_time) if isinstance(backed_up_on, str): backed_up_time = get_date_with_tz( datetime.strptime(backed_up_on, '%Y-%m-%d %H:%M')) else: backed_up_time = get_date_with_tz(backed_up_on) if backed_up_time < oldest_backed_up_time: oldest_backed_up_time = backed_up_time older_date_found = True return older_date_found, oldest_backed_up_time def save_all_metadata(self): ''' Save all the metadata for the data dir to a json file. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> backup_task.save_all_metadata() >>> json_filename = os.path.join(backup_task.to_backup_dir, ... constants.METADATA_FILENAME) >>> os.path.exists(json_filename) True ''' root_dir = self.manager.data_dir if not root_dir.endswith(os.sep): root_dir += os.sep json_filename = os.path.join(self.to_backup_dir, constants.METADATA_FILENAME) with open(json_filename, 'w') as json_file: self.save_metadata(root_dir, root_dir, json_file) def save_metadata(self, root_dir, starting_dir, json_file): ''' Save the metadata from the starting_dir. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> root_dir = backup_task.manager.data_dir >>> json_filename = os.path.join('/tmp', constants.METADATA_FILENAME) >>> with open(json_filename, 'w') as json_file: ... backup_task.save_metadata(root_dir, root_dir, json_file) >>> os.path.exists(json_filename) True ''' entries = os.scandir(starting_dir) for entry in entries: # there's no need to change the status of wallets if (entry.name.startswith(constants.DEFAULT_BACKUPS_DIR) or entry.name.startswith('wallet') or entry.name == '.walletlock'): pass else: # remove the root dir so if the directory is moved, everything still works path = entry.path.replace(root_dir, '') stat_result = os.stat(entry.path) stats_dict = { 'st_mode': stat_result.st_mode, 'st_ino': stat_result.st_ino, 'st_dev': stat_result.st_dev, 'st_nlink': stat_result.st_nlink, 'st_uid': stat_result.st_uid, 'st_gid': stat_result.st_gid, 'st_size': stat_result.st_size, 'st_atime': stat_result.st_atime, 'st_mtime': stat_result.st_mtime, 'st_ctime': stat_result.st_ctime } if is_windows(): stats_dict[ 'st_file_attributes'] = stat_result.st_file_attributes file_stats = json.dumps([path, stats_dict]) json_file.write(f'{file_stats}\n') if entry.is_dir() and entry.name != 'wallets': self.save_metadata(root_dir, entry.path, json_file) def save_bcb_database(self): ''' Save the blockchain_backup database. >>> from blockchain_backup.settings import DATABASE_NAME >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> blockchain_backup_db_backup_dir = backup_task.save_bcb_database() >>> blockchain_backup_db_backup_dir is not None True >>> os.path.exists(os.path.join(blockchain_backup_db_backup_dir, DATABASE_NAME)) True ''' try: database_filename = DATABASE_PATH blockchain_backup_db_backup_dir = os.path.join( self.to_backup_dir, constants.BLOCKCHAIN_BACKUP_DB_DIR) self.log( f'copying {database_filename} to {blockchain_backup_db_backup_dir}' ) if os.path.exists(database_filename): if not os.path.exists(blockchain_backup_db_backup_dir): os.makedirs(blockchain_backup_db_backup_dir) copy(database_filename, blockchain_backup_db_backup_dir) self.save_state(blockchain_backup_db_backup_dir) else: self.log(f'no such file: {database_filename}') self.log('saved blockchain_backup database') except: # 'bare except' because it catches more than "except Exception" # saving the blockchain_backup database is not # critical to maintaining the blockchain self.log(format_exc()) blockchain_backup_db_backup_dir = None return blockchain_backup_db_backup_dir def save_state(self, blockchain_backup_db_backup_dir): ''' Save the state of the database in a json file so it can easily be restored. >>> from blockchain_backup.settings import DATABASE_NAME >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> backup_task = BackupTask() >>> backup_task.manager = BitcoinManager(backup_task.log_name) >>> backup_task.prep_backup() >>> blockchain_backup_db_backup_dir = '/tmp' >>> backup_task.save_state(blockchain_backup_db_backup_dir) >>> os.path.exists(os.path.join(blockchain_backup_db_backup_dir, constants.STATE_BACKUP_FILENAME)) True ''' try: data = serialize('json', State.objects.all(), indent=4) full_path = os.path.join(blockchain_backup_db_backup_dir, constants.STATE_BACKUP_FILENAME) with open(full_path, 'w') as outfile: outfile.write(data) except: # 'bare except' because it catches more than "except Exception" # saving the blockchain_backup state is not # critical to maintaining the blockchain self.log(format_exc())
class UpdateTask(Thread): ''' Update the blockchain. Stope to backup the blockchain according the the user's preferences. ''' STOPPING_UPDATE = 'Waiting for Bitcoin Core to stop' STOPPED_UPDATE = 'Update stopped on your request' STOP_UPDATE_FOR_BACKUP = 'Stopping update so backup can start.' UPDATE_UNEXPECTED_ERROR = 'Unexpected error occurred during update.' ERROR_STARTING_BITCOIND = 'Unable to start bitcoind -- is Bitcoin-QT or BitcoinD already running?' BITCOIND_ERROR = '<br/> The Bitcoin Core program, bitcoind, reported a serious error.' BITCOIND_ERROR_LABEL = '<strong>The Bitcoin Core program, bitcoind, Error:</strong>' def __init__(self): ''' Initialize the update task. >>> update_task = UpdateTask() >>> update_task is not None True >>> update_task.__init__() >>> update_task._interrupted False >>> update_task.manager is None True >>> update_task.log_name 'blockchain_backup.bitcoin.update.log' ''' self._interrupted = False self.manager = None self.log = get_log() self.log_name = os.path.basename(get_log_path()) self.current_block = state.get_last_block_updated() Thread.__init__(self) def interrupt(self): ''' Set to true when user clicks the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.interrupt() >>> update_task._interrupted True ''' self._interrupted = True if self.manager: self.manager.update_progress(self.STOPPING_UPDATE) # try to stop bitcoind quickly command_args = self.manager.get_bitcoin_cli_cmd('stop') try: background(*command_args) # but if it doesn't work, that's ok; # more robust efforts will be made elsewhere except: # 'bare except' because it catches more than "except Exception" self.log(format_exc()) def is_interrupted(self): ''' Returns true if user clicked the Stop button. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.is_interrupted() False ''' return self._interrupted def run(self): ''' Start the update task. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.run() >>> update_task.is_interrupted() False ''' self.log('started UpdateTask') try: need_backup = False ok = False error = None self.manager = BitcoinManager(self.log_name) if not os.path.exists(self.manager.data_dir): os.makedirs(self.manager.data_dir) try: if self.is_interrupted(): ok = True self.log('self interrupted before update started') else: # don't start the update if a backup needs to be run UNKNOWN_BLOCKS = -1 if need_to_backup(self.manager.data_dir, UNKNOWN_BLOCKS): ok = True need_backup = True else: ok, need_backup = self.update() except Exception: self.log(format_exc()) if self.current_block > 0: state.set_last_block_updated(self.current_block) self.manager.update_menu(constants.ENABLE_ITEM) if need_backup and not self.is_interrupted(): self.log('starting backup') self.manager.update_location(constants.BACKUP_URL) else: self.report_update_stopped(ok, error) except: # 'bare except' because it catches more than "except Exception" ok = False need_backup = False self.log(format_exc()) self.log('ended UpdateTask') def update(self): ''' Update the blockchain using bitcoind. Returns whether the update ended successfully and whether a backup should start. If any errors while running, bitcoind, disable automatic backups so the user can decide how to proceed. ''' ok = need_backup = False error_message = None self.manager.update_menu(constants.DISABLE_ITEM) self.manager.rename_logs() try: bitcoind_process, bitcoind_pid = self.start_bitcoind() if not self.is_interrupted(): if bitcoind_process is None and bitcoind_pid is None: self.manager.update_notice(self.ERROR_STARTING_BITCOIND) self.manager.update_progress(self.UPDATE_UNEXPECTED_ERROR) ok = False else: need_backup = self.wait_while_updating(bitcoind_process) ok, error_message = self.stop_bitcoind(bitcoind_process, bitcoind_pid, need_backup) except BitcoinException as be: ok = False error_message = str(be) self.log(error_message) except: # 'bare except' because it catches more than "except Exception" self.log(format_exc()) # sometimes bitcoin exits with a non-zero return code, # but it was still ok, so check the logs ok, error_message = self.manager.bitcoin_finished_ok( is_bitcoind_running) if ok: if not need_backup: self.manager.update_progress(' ') elif error_message is not None: # don't allow any more backups until the user tells us it's ok state.set_backups_enabled(False) self.log('error while updating so stopping backups') if is_bitcoind_running(): self.log('retry stopping bitcoind without showing progress') self.retry_stopping(show_progress=False) self.manager.update_subnotice( f'{self.BITCOIND_ERROR_LABEL} {error_message}') return ok, need_backup def start_bitcoind(self): ''' Start bitcoind as a daemon. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> need_backup = False >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> bitcoind_process, bitcoind_pid = update_task.start_bitcoind() >>> update_task.stop_bitcoind(bitcoind_process, bitcoind_pid, need_backup) (False, ' Error opening block database.\\n') ''' if is_bitcoind_running(): bitcoind_process = None bitcoind_pid = get_pid(bitcoind()) if bitcoind_pid is None: sleep(5) bitcoind_pid = get_pid(bitcoind()) self.log(f'bitcoind is already running using pid: {bitcoind_pid}') else: bitcoind_pid = None command_args = [] if self.manager.bin_dir is None: command_args.append(bitcoind()) ok = True else: cmd = os.path.join(self.manager.bin_dir, bitcoind()) command_args.append(cmd) ok = os.path.exists(cmd) if ok: extra_args = preferences.get_extra_args() use_test_net = '-testnet' in extra_args if self.manager.data_dir is not None: data_dir = self.manager.data_dir if use_test_net and data_dir.endswith( constants.TEST_NET_SUBDIR): data_dir = data_dir[:data_dir.rfind(constants. TEST_NET_SUBDIR)] command_args.append(f'-datadir={data_dir}') # don't allow any interaction with the user's wallet command_args.append('-disablewallet') if extra_args: for extra_arg in extra_args: command_args.append(extra_arg) command_args.append('-daemon') try: bitcoind_process = Popen(command_args) self.log( f'bitcoind started: {bitcoind_process is not None}') except FileNotFoundError as fnfe: raise BitcoinException(str(fnfe)) else: bitcoind_process = None self.log( f'{bitcoind()} does not exist in {self.manager.bin_dir}') state.set_start_access_time(now()) return bitcoind_process, bitcoind_pid def wait_while_updating(self, bitcoind_process): ''' Wait for the blockchain to be updated. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> bitcoind_process, bitcoind_pid = update_task.start_bitcoind() >>> need_backup = update_task.wait_while_updating(bitcoind_process) >>> print(need_backup) False >>> update_task.stop_bitcoind(bitcoind_process, bitcoind_pid, need_backup) (False, ' Error opening block database.\\n') ''' def get_secs_to_wait(): ''' Wait longer if no real data available yet. ''' if self.current_block > 0: secs_to_wait = WAIT_SECONDS else: secs_to_wait = WAIT_SECONDS * 2 return secs_to_wait WAIT_SECONDS = 30 # seconds self.log('waiting while updating blockchain') # give the system a few seconds to get it started secs = 0 while (not is_bitcoind_running() and secs < (WAIT_SECONDS * 6) and not self.is_interrupted()): sleep(WAIT_SECONDS) secs += WAIT_SECONDS self.current_block = self.manager.get_current_block() need_backup = need_to_backup(self.manager.data_dir, self.current_block) secs_to_wait = get_secs_to_wait() while (is_bitcoind_running() and not need_backup and not self.is_interrupted()): try: if bitcoind_process is None: sleep(secs_to_wait) else: bitcoind_process.wait(secs_to_wait) except TimeoutExpired: pass if is_bitcoind_running() and not self.is_interrupted(): self.current_block = self.manager.get_current_block() need_backup = need_to_backup(self.manager.data_dir, self.current_block) secs_to_wait = get_secs_to_wait() self.log(f'is_bitcoind_running: {is_bitcoind_running()}') self.log(f'need_backup: {need_backup}') self.log(f'is_interrupted: {self.is_interrupted()}') self.log(f'finished waiting; need backup: {need_backup}') return need_backup def stop_bitcoind(self, bitcoind_process, bitcoind_pid, need_backup): ''' Stop bitcoind and determine if it ended properly. Returns: True if shutdown successful; otherwise False. Any error message or None. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> need_backup = False >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> bitcoind_process, bitcoind_pid = update_task.start_bitcoind() >>> ok, error_message = update_task.stop_bitcoind(bitcoind_process, bitcoind_pid, need_backup) >>> print(ok) False >>> print(error_message) Error opening block database. <BLANKLINE> ''' def update_progress(): # only update progress if it hasn't been blanked at an earlier time last_progress_update = self.manager.get_last_progress_update() if last_progress_update is not None and last_progress_update.strip( ): self.manager.update_progress(self.STOPPING_UPDATE) self.manager.update_progress(self.STOPPING_UPDATE) self.wait_for_shutdown(bitcoind_process, bitcoind_pid, need_backup) self.retry_stopping() update_progress() ok, error_message, seconds = self.wait_for_status() update_progress() if not ok: self.report_error(bitcoind_process, bitcoind_pid, error_message, seconds) if error_message is not None: ok = False self.manager.update_progress(' ') state.set_last_access_time(now()) self.log( f'end wait_for_bitcoin: ok: {ok} error: {error_message} bitcoin running: {is_bitcoind_running()}' ) return ok, error_message def wait_for_shutdown(self, bitcoind_process, bitcoind_pid, need_backup): ''' Wait for bitcoind to shutdown. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> need_backup = False >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> bitcoind_process, bitcoind_pid = update_task.start_bitcoind() >>> update_task.wait_for_shutdown(bitcoind_process, bitcoind_pid, need_backup) ''' try: if is_bitcoind_running(): # get the last block number before we shut down self.current_block = self.manager.get_current_block( show_progress=False) if need_backup: self.manager.update_subnotice(self.STOP_UPDATE_FOR_BACKUP) self.manager.send_bitcoin_cli_cmd('stop', max_attempts=1) # wait until bitcoind terminates if bitcoind_process is None: try: pid, returncode = os.waitpid(bitcoind_pid, os.P_WAIT) self.log(f'waitpid {pid} return code: {returncode}') except ChildProcessError: self.log('update_pid already dead') else: bitcoind_process.wait() self.log( f'bitcoind return code: {bitcoind_process.returncode}') except: # 'bare except' because it catches more than "except Exception" self.log(format_exc()) def retry_stopping(self, show_progress=True): ''' Retry sending the stop command. At times, the process might end, but bitcoin itself is still running. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> need_backup = False >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> update_task.retry_stopping() ''' MAX_SECONDS = 30 seconds = 0 while is_bitcoind_running(): sleep(1) seconds += 1 if seconds > MAX_SECONDS: seconds = 0 self.manager.send_bitcoin_cli_cmd('stop') if show_progress: self.manager.update_progress(self.STOPPING_UPDATE) def wait_for_status(self): ''' Wait for bitcoin to clean up. Returns True if bitcoin shutdown successfully; otherwise, False. Error message from bitcoind if this is one; otherwise, None. Seconds waiting. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> need_backup = False >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> update_task.wait_for_status() (True, None, 0) ''' WAIT_SECONDS = 10 MAX_SECONDS = 60 # if bitcoin is not running, then give it more time to see # if the debug log is updated with the status seconds = 0 ok, error_message = self.manager.check_bitcoin_log(is_bitcoind_running) while (not ok and seconds < MAX_SECONDS and not is_bitcoind_running()): sleep(WAIT_SECONDS) seconds += WAIT_SECONDS ok, error_message = self.manager.check_bitcoin_log( is_bitcoind_running) if seconds >= MAX_SECONDS: self.log(f'waited {seconds} seconds for bitcoin to finish.') self.log(f'is_bitcoind_running: {is_bitcoind_running()}') return ok, error_message, seconds def report_error(self, bitcoind_process, bitcoind_pid, error_message, seconds): ''' Report a serious error about stopping bitcoind. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> bitcoind_process, bitcoind_pid = update_task.start_bitcoind() >>> need_backup = False >>> ok, error_message = update_task.stop_bitcoind(bitcoind_process, bitcoind_pid, need_backup) >>> update_task.report_error(bitcoind_process, bitcoind_pid, error_message, 60) ''' # let the user know a serious error has happened if is_bitcoind_running(): if bitcoind_process is None and bitcoind_pid is None: if error_message is None: self.manager.update_progress( f'Unable to stop bitcoind after {seconds/60} minutes') else: if bitcoind_process is None: os.kill(bitcoind_pid, os.SIGTERM) else: bitcoind_process.terminate() self.log('terminated bitcoin process') else: # clear the progress because we're no longer # waiting for bitcoind to shutdown self.manager.update_progress(' ') def report_update_stopped(self, ok, error): ''' Report to the user that bitcoind stopped. >>> from blockchain_backup.bitcoin.tests import utils as test_utils >>> test_utils.init_database() >>> update_task = UpdateTask() >>> update_task.manager = BitcoinManager(update_task.log_name) >>> ok = False >>> update_task.report_update_stopped(ok, 'Unknown error') ''' # a new page might have been displayed # so give socketio time to connect MAX_SECS = 3 seconds = 0 while seconds < MAX_SECS: if ok: self.log('update stopped') self.manager.update_header(self.STOPPED_UPDATE) self.manager.notify_done() else: self.log( 'bitcoind stopped, updating user that everything is not ok' ) if error is None: self.manager.update_header(constants.RESTORE_BITCOIN) self.manager.update_progress(' ') self.manager.update_notice( f'{self.BITCOIND_ERROR}{get_ok_button()}') sleep(1) seconds += 1
class AccessWalletTask(Thread): ''' Run bitcoin-qt. If it's scheduled, backup the blockchain when the user ends the bitcoint-qt. ''' BITCOIN_QT_RUNNING = 'Is BitcoinD or another copy of BitcoinQT already running?' BITCOIN_QT_OTHER_APP_RUNNING = f'Unable to start BitcoinQT. {BITCOIN_QT_RUNNING}' BITCOIN_QT_ERROR = '<br/>Bitcoin-QT reported a serious error.<br/> ' BITCOIN_QT_ERROR_LABEL = '<strong>Bitcoin-QT Error:</strong>' BITCOIN_QT_UNEXPECTED_ERROR = 'Unexpected error occurred while running Bitcoin Core QT.' def __init__(self): Thread.__init__(self) self.log = get_log() self.log_name = os.path.basename(get_log_path()) self.manager = None self.current_block = state.get_last_block_updated() def run(self): self.log('started AccessWalletTask') ok = need_backup = False error_message = None try: self.manager = BitcoinManager(self.log_name) self.manager.update_menu(constants.DISABLE_ITEM) if not os.path.exists(self.manager.data_dir): os.makedirs(self.manager.data_dir) ok, error_message = self.run_qt() if ok: need_backup = bitcoin_utils.need_to_backup(self.manager.data_dir, self.current_block) if need_backup: self.log('need to backup') self.manager.update_location(constants.BACKUP_URL) else: self.log('continuing to update blockchain') self.manager.update_location(constants.SYNC_URL) else: # don't allow any more backups until the user tells us it's ok state.set_backups_enabled(False) if error_message is None: self.log('unexpected error') if DEBUG: notice = format_exc() else: notice = self.BITCOIN_QT_UNEXPECTED_ERROR self.manager.update_progress('{} {}'.format( notice, bitcoin_utils.get_ok_button())) else: self.log('bitcoin-qt error') notice_and_button = '{}{}'.format( constants.RESTORE_BITCOIN, bitcoin_utils.get_ok_button()) self.manager.update_header(self.BITCOIN_QT_ERROR) self.manager.update_notice(notice_and_button) self.manager.update_subnotice(error_message) self.manager.update_menu(constants.ENABLE_ITEM) except Exception: need_backup = False error = format_exc() self.log(error) if self.manager: if DEBUG: self.manager.update_progress(error) else: self.manager.update_progress(self.BITCOIN_QT_UNEXPECTED_ERROR) self.manager.update_menu(constants.ENABLE_ITEM) self.log('finished AccessWalletTask') def run_qt(self): ''' Run bitcon-qt. ''' ok = False error_message = None self.manager.update_menu(constants.DISABLE_ITEM) try: command_args = self.get_launch_args() if command_args is None: ok = False else: self.manager.rename_logs() state.set_start_access_time(now()) self.log(f'starting bitcoin-qt: {command_args}') os.putenv('DISPLAY', ':0.0') if bitcoin_utils.is_bitcoin_qt_running(): bitcoin_pid = get_pid(bitcoin_utils.bitcoin_qt()) bitcoin_process = None else: bitcoin_pid = None bitcoin_process = Popen(command_args) if bitcoin_process is not None or bitcoin_pid is not None: self.wait_for_close(bitcoin_process) state.set_last_access_time(now()) ok = True else: self.manager.update_progress(self.BITCOIN_QT_OTHER_APP_RUNNING) ok = False except CalledProcessError as cpe: ok = False stdout = cpe.stdout if stdout and not isinstance(stdout, str): stdout = stdout.decode() stderr = cpe.stderr if stderr and not isinstance(stderr, str): stderr = stderr.decode() __, error_message, log_message = self.manager.process_bitcoin_cli_error( 'getblockchaininfo', cpe.returncode, stdout, stderr) if error_message is None: error_message = log_message self.log(error_message) except BitcoinException as be: ok = False error_message = str(be) self.log(error_message) except FileNotFoundError as fnfe: ok = False error_message = str(fnfe) self.log(error_message) except Exception: self.log(format_exc()) if ok: # check the logs to make sure everything was ok ok, error_message = self.manager.bitcoin_finished_ok( bitcoin_utils.is_bitcoin_qt_running) if ok: if self.current_block > 0: state.set_last_block_updated(self.current_block) else: if error_message is None: error_message = '' self.manager.update_subnotice('{} {}'.format( self.BITCOIN_QT_ERROR_LABEL, error_message)) self.manager.update_progress(' ') return ok, error_message def get_launch_args(self): ''' Get all the args to start bitcon-qt as a server. ''' ok = False command_args = [] if self.manager.bin_dir is None: command_args.append(bitcoin_utils.bitcoin_qt()) ok = True else: cmd = os.path.join(self.manager.bin_dir, bitcoin_utils.bitcoin_qt()) command_args.append(cmd) ok = os.path.exists(cmd) if not ok: self.log(f'{bitcoin_utils.bitcoin_qt()} does not exist in {self.manager.bin_dir}') if ok: command_args.append('-server') if self.manager.data_dir is not None: data_dir = bitcoin_utils.strip_testnet_from_data_dir(data_dir=self.manager.data_dir) command_args.append(f'-datadir={data_dir}') extra_args = preferences.get_extra_args() if extra_args: for extra_arg in extra_args: command_args.append(extra_arg) else: command_args = None self.log(f'{bitcoin_utils.bitcion_qt()} does not exist in {self.manager.bin_dir}') return command_args def wait_for_close(self, bitcoin_process): ''' Wait for user to close bitcion_qt. ''' initial_wait_seconds = 30 normal_wait_seconds = 10 self.log('waiting for bitcoin-qt to be closed by user') while bitcoin_utils.is_bitcoin_qt_running(): max_secs = initial_wait_seconds secs = 0 # wait for bitcoin-qt while bitcoin_utils.is_bitcoin_qt_running() and (secs < max_secs): try: if bitcoin_process is None: sleep(1) else: bitcoin_process.wait(1) except TimeoutExpired: pass secs += 1 max_secs = normal_wait_seconds if bitcoin_utils.is_bitcoin_qt_running(): current_block = self.manager.get_current_block(show_next_backup_time=False) if (current_block is not None and current_block > self.current_block): self.current_block = current_block self.log(f'current block: {self.current_block}') self.log('finished waiting for bitcoin-qt')