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 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