예제 #1
0
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/>&nbsp;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('&nbsp;')
        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('&nbsp;')

        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('&nbsp;')

    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('&nbsp;')

                self.manager.update_notice(
                    f'{self.BITCOIND_ERROR}{get_ok_button()}')

            sleep(1)
            seconds += 1
예제 #2
0
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/>&nbsp;'
    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('&nbsp;')

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