Esempio n. 1
0
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/>&nbsp;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
Esempio n. 2
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