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