コード例 #1
0
class BackupTask(Thread):
    '''
        Backup the blockchain.
    '''
    def __init__(self):
        '''
            Initialize the backup task.

            >>> backup_task = BackupTask()
            >>> backup_task is not None
            True
            >>> backup_task.__init__()
            >>> type(backup_task.locale_tz)
            <class 'zoneinfo.ZoneInfo.Atlantic/Reykjavik'>
            >>> backup_task._interrupted
            False
            >>> backup_task.manager is None
            True
            >>> backup_task.log_name
            'blockchain_backup.bitcoin.backup.log'
        '''
        Thread.__init__(self)

        self._interrupted = False

        self.log = Log()
        self.log_name = os.path.basename(get_log_path())

        self.manager = None
        self.to_backup_dir = None
        self.backup_formatted_time = None

        self.locale_tz = ZoneInfo(TIME_ZONE)

    def interrupt(self):
        '''
            Set to true when user clicks the Stop button.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.interrupt()
            >>> backup_task._interrupted
            True
        '''
        self._interrupted = True
        if self.manager:
            self.manager.update_progress(backup_utils.STOPPING_BACKUP)

    def is_interrupted(self):
        '''
            Returns true if user clicked the Stop button.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.is_interrupted()
            False
        '''
        return self._interrupted

    def run(self):
        '''
            Start the backup task.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.run()
            True
            >>> backup_task.is_interrupted()
            False
        '''

        self.log('started BackupTask')

        ok = True
        try:
            self.manager = BitcoinManager(self.log_name)
            self.manager.update_menu(constants.DISABLE_ITEM)

            # be sure to check for updates regularly
            check_for_updates()

            ok = self.backup()
            if self.is_interrupted():
                self.interrupt_backup()

            else:
                if ok:
                    self.manager.update_header(backup_utils.FINISHED)
                    self.manager.update_notice(backup_utils.WALLET_REMINDER)
                    self.manager.update_subnotice('')
                    self.manager.update_menu(constants.ENABLE_ITEM)

                    self.log('starting to update the blockchain')
                    self.manager.update_location(constants.SYNC_URL)
                else:
                    notice_and_button = '{}{}'.format(
                        constants.CLOSE_WINDOW_NOW, get_ok_button())
                    self.manager.update_header(backup_utils.HEADER_ERROR)
                    self.manager.update_notice(notice_and_button)
                    self.manager.update_subnotice(backup_utils.CONTACT_US)
                    self.manager.update_progress('')
                    self.manager.update_menu(constants.ENABLE_ITEM)

        except:  # 'bare except' because it catches more than "except Exception"
            ok = False
            error = format_exc()
            self.log(error)
            if self.manager:
                self.manager.update_notice(backup_utils.UNEXPECTED_ERROR)
                if DEBUG:
                    self.manager.update_progress(error)
                else:
                    self.manager.update_progress('&nbsp;')
                self.manager.update_menu(constants.ENABLE_ITEM)

        self.log('finished BackupTask')

        return ok

    def backup(self):
        '''
            Backup now regardless when the last backup ran.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.backup()
            True
            >>> backup_task.is_interrupted()
            False
        '''
        ok = True
        backup_level = 1

        if not os.path.exists(self.manager.data_dir):
            os.makedirs(self.manager.data_dir)

        self.manager.update_progress(backup_utils.STARTING)

        if not self.is_interrupted():
            result = backup_utils.prep_backup(self.manager.data_dir)
            self.to_backup_dir, self.backup_formatted_time, backup_level = result
            if os.path.exists(self.to_backup_dir):
                self.manager.update_progress(backup_utils.STARTING)
            else:
                self.manager.update_progress(backup_utils.STARTING_INITIAL)

        try:
            backup_process = backup_pid = None

            if not self.is_interrupted():
                self.log('starting backup')
                backup_process, backup_pid = backup_utils.start_backup(
                    self.manager.data_dir, self.to_backup_dir)

            if backup_process is not None or backup_pid is not None:

                if not self.is_interrupted():
                    backup_utils.wait_for_backup(
                        backup_process,
                        self.is_interrupted,
                        update_progress=self.manager.update_progress)

                if not self.is_interrupted():
                    backup_utils.stop_backup(backup_process, backup_pid)

            else:
                self.manager.update_progress(backup_utils.NO_MEM_ERROR)
                ok = False

            if not self.is_interrupted():
                backup_utils.finish_backup(self.manager.data_dir,
                                           self.to_backup_dir,
                                           self.backup_formatted_time,
                                           backup_level)

        except:  # 'bare except' because it catches more than "except Exception"
            backup_utils.add_backup_flag(self.to_backup_dir,
                                         self.backup_formatted_time)
            ok = False
            self.log(format_exc())

        if ok and not self.is_interrupted():
            backup_utils.save_all_metadata(self.manager.data_dir,
                                           self.to_backup_dir)
            self.log('saved all metadata')

        if ok and not self.is_interrupted():
            # remove the old debug logs; we back them up in case there's an error
            for entry in os.scandir(self.manager.data_dir):
                if entry.name.startswith(constants.DEBUG_PREFIX) and \
                   entry.name.endswith(constants.LOG_SUFFIX):
                    os.remove(entry.path)
            self.manager.update_subnotice('')
            self.manager.update_progress(backup_utils.FINISHED_REMINDER)

        return ok

    def interrupt_backup(self):
        '''
            End user interrupts the backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.to_backup_dir, backup_task.backup_formatted_time, __ = backup_utils.prep_backup(backup_task.manager.data_dir)
            >>> backup_process, backup_pid = backup_utils.start_backup(backup_task.manager.data_dir,
            ...                                                        backup_task.to_backup_dir)
            >>> test_utils.start_fake_backup()
            >>> backup_task.interrupt_backup()
            True
        '''

        MAX_SECS = 3

        seconds = 0

        self.log('interrupting backup')
        if self.to_backup_dir is not None:
            # remove all files that suggest this backup is complete
            backup_utils.delete_last_updated_files(self.to_backup_dir)
            # add a flag that we started to use this dir to backup
            backup_utils.add_backup_flag(self.to_backup_dir,
                                         self.backup_formatted_time)

        try:
            bin_dir = os.path.join(virtualenv_dir(), 'bin')
            args = [
                os.path.join(bin_dir, 'killmatch'), constants.BACKUP_PROGRAM
            ]

            attempts = 0
            while backup_utils.is_backup_running() and attempts < 3:
                result = command.run(*args).stdout
                self.log(f'result of stopping backup: {result}')
                if backup_utils.is_backup_running():
                    sleep(3)
                    attempts += 1
        except CalledProcessError as cpe:
            self.log(cpe)
            self.log(format_exc())

        while seconds < MAX_SECS:
            self.manager.update_header(backup_utils.STOPPED_BACKUP_HEADER)
            self.manager.update_progress(backup_utils.STOPPED_BACKUP_PROGRESS)
            self.manager.notify_close_window()

            sleep(1)
            seconds += 1

        # return value is for testing purposes only
        return not backup_utils.is_backup_running()
コード例 #2
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
コード例 #3
0
class BackupTask(Thread):
    '''
        Backup the blockchain.
    '''
    STARTING = 'Starting to back up the blockchain.'
    STARTING_INITIAL = 'Configuring new backup level -- this could take a long time'
    COPYING = '<strong>Copying: </strong>{}'
    FINISHED = 'Finished backup of the blockchain.'
    FINISHED_REMINDER = "Finished backing up the blockchain.<p>Don't forget to backup your wallet."
    WALLET_REMINDER = 'Do not forget to back up your wallet.'
    NO_MEM_ERROR = 'Unable to start backup -- is there enough memory and disk space?'
    HEADER_ERROR = 'Error while backing up the blockchain.'
    UNEXPECTED_ERROR = 'Unexpected error occurred during backup.'
    CONTACT_US = 'Contact [email protected]'

    STOPPING_BACKUP = 'Stopping backup as you requested'
    STOPPED_BACKUP_HEADER = "Stopped backing up Bitcoin blockchain"
    BACKUP_INCOMPLETE = "Don't forget that the latest backup is incomplete."
    DIR_UNUSABLE = 'It cannot be used to restore the blockchain.'
    STOPPED_BACKUP_PROGRESS = f'{BACKUP_INCOMPLETE} {DIR_UNUSABLE}'

    def __init__(self):
        '''
            Initialize the backup task.

            >>> backup_task = BackupTask()
            >>> backup_task is not None
            True
            >>> backup_task.__init__()
            >>> type(backup_task.locale_tz)
            <class 'pytz.tzfile.Atlantic/Reykjavik'>
            >>> backup_task._interrupted
            False
            >>> backup_task.manager is None
            True
            >>> backup_task.log_name
            'blockchain_backup.bitcoin.backup.log'
        '''
        Thread.__init__(self)

        self._interrupted = False

        self.log = get_log()
        self.log_name = os.path.basename(get_log_path())

        self.manager = None
        self.to_backup_dir = None
        self.backup_level = 1
        self.backup_formatted_time = None

        self.locale_tz = timezone(TIME_ZONE)

    def interrupt(self):
        '''
            Set to true when user clicks the Stop button.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.interrupt()
            >>> backup_task._interrupted
            True
        '''
        self._interrupted = True
        if self.manager:
            self.manager.update_progress(self.STOPPING_BACKUP)

    def is_interrupted(self):
        '''
            Returns true if user clicked the Stop button.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.is_interrupted()
            False
        '''
        return self._interrupted

    def run(self):
        '''
            Start the backup task.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.run()
            True
            >>> backup_task.is_interrupted()
            False
        '''

        self.log('started BackupTask')

        ok = True
        try:
            self.manager = BitcoinManager(self.log_name)
            self.manager.update_menu(constants.DISABLE_ITEM)

            # be sure to check for updates regularly
            bitcoin_utils.check_for_updates()

            ok = self.backup()
            if self.is_interrupted():
                self.interrupt_backup()

            else:
                if ok:
                    self.manager.update_header(self.FINISHED)
                    self.manager.update_notice(self.WALLET_REMINDER)
                    self.manager.update_subnotice('')
                    self.manager.update_menu(constants.ENABLE_ITEM)

                    self.log('starting to update the blockchain')
                    self.manager.update_location(constants.SYNC_URL)
                else:
                    notice_and_button = '{}{}'.format(
                        constants.CLOSE_WINDOW_NOW,
                        bitcoin_utils.get_ok_button())
                    self.manager.update_header(self.HEADER_ERROR)
                    self.manager.update_notice(notice_and_button)
                    self.manager.update_subnotice(self.CONTACT_US)
                    self.manager.update_progress('')
                    self.manager.update_menu(constants.ENABLE_ITEM)

        except:  # 'bare except' because it catches more than "except Exception"
            ok = False
            error = format_exc()
            self.log(error)
            if self.manager:
                self.manager.update_notice(self.UNEXPECTED_ERROR)
                if DEBUG:
                    self.manager.update_progress(error)
                else:
                    self.manager.update_progress('&nbsp;')
                self.manager.update_menu(constants.ENABLE_ITEM)

        self.log('finished BackupTask')

        return ok

    def backup(self):
        '''
            Backup now regardless when the last backup ran.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.backup()
            True
            >>> backup_task.is_interrupted()
            False
        '''
        ok = True

        if not os.path.exists(self.manager.data_dir):
            os.makedirs(self.manager.data_dir)

        self.manager.update_progress(self.STARTING)

        if not self.is_interrupted():
            self.prep_backup()

        try:
            backup_process = backup_pid = None

            if not self.is_interrupted():
                self.log('starting backup')
                backup_process, backup_pid = self.start_backup()

            if backup_process is not None or backup_pid is not None:

                if not self.is_interrupted():
                    self.wait_for_backup(backup_process)

                if not self.is_interrupted():
                    self.stop_backup(backup_process, backup_pid)

            else:
                self.manager.update_progress(self.NO_MEM_ERROR)
                ok = False

            if not self.is_interrupted():
                self.finish_backup()
        except:  # 'bare except' because it catches more than "except Exception"
            self.add_backup_flag()
            ok = False
            self.log(format_exc())

        if ok and not self.is_interrupted():
            self.save_all_metadata()
            self.log('saved all metadata')

        if ok and not self.is_interrupted():
            # remove the old debug logs; we back them up in case there's an error
            for entry in os.scandir(self.manager.data_dir):
                if entry.name.startswith(constants.DEBUG_PREFIX) and \
                entry.name.endswith(constants.LOG_SUFFIX):
                    os.remove(entry.path)
            self.manager.update_subnotice('')
            self.manager.update_progress(self.FINISHED_REMINDER)

        return ok

    def prep_backup(self):
        '''
            Prepare to backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_task.backup_formatted_time is not None
            True
            >>> backup_task.to_backup_dir.startswith('/')
            True
        '''
        backup_time = now()
        time_stamp = backup_time.isoformat(sep=' ')

        if not self.manager.rename_logs(time_stamp=time_stamp):
            self.log('no logs to rename')

        self.backup_formatted_time = bitcoin_utils.format_time(time_stamp)

        self.to_backup_dir, last_backup_time = self.get_backup_dir_and_time()
        if os.path.exists(self.to_backup_dir):
            self.manager.update_progress(self.STARTING)
            self.log(f'backup {os.path.basename(self.to_backup_dir)}')
            self.log(f'last backed up {last_backup_time}')
        else:
            self.manager.update_progress(self.STARTING_INITIAL)
            os.makedirs(self.to_backup_dir)
            self.log(f'created new backup dir: {self.to_backup_dir}')

        # flag that we're using this dir to backup
        self.add_backup_flag()

        # remove the last backup file if it exists
        bitcoin_utils.delete_last_updated_files(self.to_backup_dir)

    def start_backup(self):
        '''
            Start backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_process, backup_pid = backup_task.start_backup()
            >>> backup_process is not None
            True
            >>> backup_pid is None
            True
            >>> test_utils.stop_backup()
        '''
        bin_dir = os.path.join(virtualenv_dir(), 'bin')
        data_dir = self.manager.data_dir
        if not data_dir.endswith(os.sep):
            data_dir += os.sep

        if is_backup_running():
            backup_process = None
            backup_pid = get_pid(constants.BACKUP_PROGRAM)
            self.log('{} is already running using pid: {}'.format(
                constants.BACKUP_PROGRAM, backup_pid))
        else:
            backup_pid = None

            args = []
            # "bcb-backup" is a link to safecopy so we can distinguish it when we kill it
            args.append(os.path.join(bin_dir, constants.BACKUP_PROGRAM))
            args.append('--exclude')
            args.append(get_excluded_files())
            args.append('--verbose')
            args.append('--quick')
            args.append('--delete')
            args.append(f'{data_dir}*')
            args.append(self.to_backup_dir)

            # Popen appears to report "'list' object has no attribute 'split'"
            # the docs state Popen should pass a sequence as the first arg
            backup_process = Popen(args, stdout=PIPE, universal_newlines=True)

        return backup_process, backup_pid

    def wait_for_backup(self, backup_process):
        '''
            Wait for the backup to finish and display data while waiting.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_process, backup_pid = backup_task.start_backup()
            >>> backup_process is not None
            True
            >>> backup_pid is None
            True
            >>> backup_task.wait_for_backup(backup_process)
            >>> test_utils.stop_backup()
            >>> backup_task.wait_for_backup(None)
            >>> test_utils.stop_backup()
        '''
        def show_line(line):
            if line is not None and line.startswith('Copying:'):
                index = line.rfind(os.sep)
                if index > 0:
                    line = self.COPYING.format(line[index + 1:])
                if line != self.COPYING:
                    self.manager.update_progress(line)

        self.log('starting to wait for backup')

        if backup_process is None:
            log_path = os.path.join(BASE_LOG_DIR, whoami(), 'bcb-backup.log')

            # wait until the log appears
            while (is_backup_running() and not self.is_interrupted()):
                if not os.path.exists(log_path):
                    sleep(1)

            # then display the backup details
            while (is_backup_running() and not self.is_interrupted()):
                with open(log_path, 'rt') as backup_log:
                    show_line(backup_log.readline())
        else:
            while (backup_process.poll() is None
                   and not self.is_interrupted()):
                show_line(backup_process.stdout.readline())

        if self.is_interrupted():
            self.log('waiting for backup interrupted')
        else:
            self.log('finished waiting for backup')

    def stop_backup(self, backup_process, backup_pid):
        '''
            Stop backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_process, backup_pid = backup_task.start_backup()
            >>> backup_process is not None
            True
            >>> backup_pid is None
            True
            >>> backup_task.stop_backup(backup_process, backup_pid)
            >>> test_utils.start_fake_backup()
            >>> backup_pid = get_pid(constants.BACKUP_PROGRAM)
            >>> backup_task.stop_backup(None, backup_pid)
        '''
        try:
            if backup_process is None and backup_pid is not None:
                if is_backup_running():
                    bin_dir = os.path.join(virtualenv_dir(), 'bin')
                    args = [
                        os.path.join(bin_dir, 'killmatch'),
                        '"{} --exclude {}"'.format(constants.BACKUP_PROGRAM,
                                                   get_excluded_files())
                    ]
                    result = command.run(*args).stdout
                    self.log(f'killing backup result: {result}')

                try:
                    pid, returncode = os.waitpid(backup_pid, os.P_WAIT)
                    self.log(f'waitpid {pid} return code: {returncode}')
                except ChildProcessError:
                    self.log('backup_pid already dead')
            else:
                # if bcb-backup hasn't stopped yet, then kill it
                if backup_process is None:
                    self.log('not back process active')
                else:
                    if backup_process.poll() is None:
                        self.log('killing backup')
                        backup_process.terminate()

                    # wait until backup terminates
                    backup_process.wait()
                    self.log(
                        f'backup return code: {backup_process.returncode}')
        except:  # 'bare except' because it catches more than "except Exception"
            self.log(f'error while stopping backup\n{format_exc()}')

    def finish_backup(self):
        '''
            Finish the backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_process, backup_pid = backup_task.start_backup()
            >>> backup_process is not None
            True
            >>> backup_pid is None
            True
            >>> backup_task.finish_backup()
            >>> test_utils.stop_backup()
        '''

        # make sure all the files starting with dot are copied using
        # standard python because argparse in backup tries to expand the filename
        for entry in os.scandir(self.manager.data_dir):
            if entry.name.startswith('.') and entry.name != '.walletlock':
                to_path = os.path.join(self.to_backup_dir, entry.name)
                copyfile(entry.path, to_path)

        # add a last backup file and remove the semaphore file showing we're updating this dir
        last_updated_filename = os.path.join(
            self.to_backup_dir,
            '{}{}'.format(constants.LAST_UPDATED_PREFIX,
                          self.get_backup_formatted_time()))
        with open(last_updated_filename, 'wt') as f:
            f.write(self.get_backup_formatted_time())

        updating_filename = self.get_backup_flag_name()
        if updating_filename and os.path.exists(updating_filename):
            os.remove(updating_filename)

        # save the last backup time in the database
        backup_time = datetime.strptime(self.get_backup_formatted_time(),
                                        '%Y-%m-%d %H:%M')
        state.set_last_backed_up_time(self.locale_tz.localize(backup_time))
        state.set_last_backup_level(self.backup_level)

        self.save_bcb_database()

    def interrupt_backup(self):
        '''
            End user interrupts the backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_process, backup_pid = backup_task.start_backup()

            >>> test_utils.start_fake_backup()
            >>> backup_task.interrupt_backup()
            True
        '''

        MAX_SECS = 3

        seconds = 0

        self.log('interrupting backup')
        if self.to_backup_dir is not None:
            # remove all files that suggest this backup is complete
            bitcoin_utils.delete_last_updated_files(self.to_backup_dir)
            # add a flag that we started to use this dir to backup
            self.add_backup_flag()

        try:
            bin_dir = os.path.join(virtualenv_dir(), 'bin')
            args = [
                os.path.join(bin_dir, 'killmatch'), constants.BACKUP_PROGRAM
            ]

            attempts = 0
            while is_backup_running() and attempts < 3:
                result = command.run(*args).stdout
                self.log(f'result of stopping backup: {result}')
                if is_backup_running():
                    sleep(3)
                    attempts += 1
        except CalledProcessError as cpe:
            self.log(cpe)
            self.log(format_exc())

        # a new page was displayed so give socketio time to connect
        while seconds < MAX_SECS:
            self.manager.update_header(self.STOPPED_BACKUP_HEADER)
            self.manager.update_progress(self.STOPPED_BACKUP_PROGRESS)
            self.manager.notify_close_window()

            sleep(1)
            seconds += 1

        # return value is for testing purposes only
        return not bitcoin_utils.is_backup_running()

    def add_backup_flag(self):
        '''
            Add a flag so we know we were updating this backup dir.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> updating_filename = backup_task.add_backup_flag()
            >>> os.path.exists(updating_filename)
            True
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> updating_filename = backup_task.add_backup_flag()
            >>> updating_filename is None
            True
        '''
        updating_filename = self.get_backup_flag_name()
        if updating_filename:
            with open(updating_filename, 'wt') as output_file:
                output_file.write(self.get_backup_formatted_time())

        return updating_filename

    def get_backup_flag_name(self):
        '''
            Add a flag so we know we were updating this backup dir.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.get_backup_flag_name() is None
            True
            >>> backup_task.prep_backup()
            >>> backup_task.get_backup_flag_name().startswith('/')
            True
        '''
        if self.to_backup_dir is None:
            backup_flag_name = None
            self.log('warning: backup dir not defined so no backup flag file')
        else:
            backup_flag_name = os.path.join(
                self.to_backup_dir,
                '{}{}'.format(constants.UPDATING_PREFIX,
                              self.get_backup_formatted_time()))

        return backup_flag_name

    def get_backup_formatted_time(self):
        '''
            Get the formatted time for the backup.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.get_backup_formatted_time() is not None
            True
        '''

        if self.backup_formatted_time is None:
            backup_time = now()
            self.log(f'backup_time: {backup_time}')
            self.backup_formatted_time = bitcoin_utils.format_time(
                backup_time.isoformat(sep=' '))
            self.log(f'backup_formatted time: {self.backup_formatted_time}')

        return self.backup_formatted_time

    def get_backup_dir_and_time(self):
        '''
            Get the oldest backup dir or the one
            that's only been partially backed up.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_dirname, oldest_backup_time = backup_task.get_backup_dir_and_time()
            >>> backup_dirname.startswith('/')
            True
            >>> isinstance(oldest_backup_time, datetime)
            True
        '''
        self.log('getting low level backup dir')

        backup_dirname = None
        oldest_backed_up_time = now() + timedelta(days=1)

        backup_dir = get_backup_dir()
        if os.path.exists(backup_dir):
            backup_dirname, oldest_backed_up_time = self.search_entries(
                backup_dir, oldest_backed_up_time)

        else:
            self.log(f'creating new backup parent: {backup_dir}')
            os.makedirs(backup_dir)
            backup_dirname = os.path.join(
                backup_dir, '{}{}'.format(constants.BACKUPS_LEVEL_PREFIX, '1'))
            oldest_backed_up_time = now()
            self.log('never backed up')

        i = backup_dirname.find(constants.BACKUPS_LEVEL_PREFIX)
        if i > 0:
            self.backup_level = int(
                backup_dirname[i + len(constants.BACKUPS_LEVEL_PREFIX):])

        self.log(f'backup dirname: {backup_dirname}')
        self.log(f'oldest_backed_up_time: {oldest_backed_up_time}')
        return backup_dirname, oldest_backed_up_time

    def search_entries(self, backup_dir, oldest_backed_up_time):
        '''
            Scan the backup directories.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_dir = '/tmp/bitcoin/data-with-blocks/testnet3/backups/'
            >>> oldest_backup = now() - timedelta(days=10)
            >>> backup_dirname, oldest_backup_time = backup_task.search_entries(
            ...    backup_dir, oldest_backup)
            >>> backup_dirname.startswith('/')
            True
            >>> isinstance(oldest_backup_time, datetime)
            True
        '''

        total_backup_levels = 0
        found_partial = False
        backup_dirname = os.path.join(backup_dir,
                                      f'{constants.BACKUPS_LEVEL_PREFIX}1')

        entries = os.scandir(backup_dir)
        for entry in entries:
            # look inside each backup level
            if entry.is_dir() and entry.name.startswith(
                    constants.BACKUPS_LEVEL_PREFIX):
                total_backup_levels += 1
                backed_up_time = None
                backup_with_timestamp = False
                filenames = os.listdir(entry.path)
                for filename in filenames:
                    # for a partial backup
                    if filename.startswith(constants.UPDATING_PREFIX):
                        backup_dirname = entry.path
                        backed_up_time = seconds_to_datetime(
                            os.path.getmtime(entry.path))
                        backup_with_timestamp = True
                        found_partial = True
                        self.log(
                            f'found a partial updating backup in {entry.name}')
                        break

                    # or for the file that includes the last backup date
                    elif filename.startswith(constants.LAST_UPDATED_PREFIX):
                        backup_with_timestamp = True
                        older_date_found, oldest_backed_up_time = self.compare_dates(
                            filename[len(constants.LAST_UPDATED_PREFIX):],
                            oldest_backed_up_time)
                        if older_date_found:
                            backup_dirname = entry.path
                            backed_up_time = oldest_backed_up_time

                # if there is no backup timestamp, then this is a partial backup
                if not backup_with_timestamp:
                    found_partial = True

                if not found_partial and backed_up_time is None:
                    older_date_found, oldest_backed_up_time = self.compare_dates(
                        seconds_to_datetime(os.path.getmtime(entry.path)),
                        oldest_backed_up_time)
                    if older_date_found:
                        backup_dirname = entry.path
                        backed_up_time = oldest_backed_up_time

            if found_partial:
                break

        if get_backup_levels() > total_backup_levels and not found_partial:
            backup_dirname = os.path.join(
                backup_dir, '{}{}'.format(constants.BACKUPS_LEVEL_PREFIX,
                                          total_backup_levels + 1))

        return backup_dirname, oldest_backed_up_time

    def compare_dates(self, backed_up_on, oldest_backed_up_time):
        '''
            Compare the dates to find the oldest one.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backed_up = now()
            >>> oldest_backup = now() - timedelta(days=10)
            >>> older_date_found, oldest_backed_up_time = backup_task.compare_dates(
            ...    backed_up, oldest_backup)
            >>> older_date_found
            False
            >>> isinstance(oldest_backed_up_time, datetime)
            True
        '''
        def get_date_with_tz(original_date):
            try:
                new_date = self.locale_tz.localize(original_date)
            except:  # 'bare except' because it catches more than "except Exception"
                new_date = original_date

            return new_date

        older_date_found = False
        oldest_backed_up_time = get_date_with_tz(oldest_backed_up_time)
        if isinstance(backed_up_on, str):
            backed_up_time = get_date_with_tz(
                datetime.strptime(backed_up_on, '%Y-%m-%d %H:%M'))
        else:
            backed_up_time = get_date_with_tz(backed_up_on)

        if backed_up_time < oldest_backed_up_time:
            oldest_backed_up_time = backed_up_time
            older_date_found = True

        return older_date_found, oldest_backed_up_time

    def save_all_metadata(self):
        '''
            Save all the metadata for the data dir to a json file.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> backup_task.save_all_metadata()
            >>> json_filename = os.path.join(backup_task.to_backup_dir,
            ...                              constants.METADATA_FILENAME)
            >>> os.path.exists(json_filename)
            True
        '''
        root_dir = self.manager.data_dir
        if not root_dir.endswith(os.sep):
            root_dir += os.sep

        json_filename = os.path.join(self.to_backup_dir,
                                     constants.METADATA_FILENAME)
        with open(json_filename, 'w') as json_file:
            self.save_metadata(root_dir, root_dir, json_file)

    def save_metadata(self, root_dir, starting_dir, json_file):
        '''
            Save the metadata from the starting_dir.

            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> root_dir = backup_task.manager.data_dir
            >>> json_filename = os.path.join('/tmp', constants.METADATA_FILENAME)
            >>> with open(json_filename, 'w') as json_file:
            ...     backup_task.save_metadata(root_dir, root_dir, json_file)
            >>> os.path.exists(json_filename)
            True
        '''

        entries = os.scandir(starting_dir)
        for entry in entries:
            # there's no need to change the status of wallets
            if (entry.name.startswith(constants.DEFAULT_BACKUPS_DIR)
                    or entry.name.startswith('wallet')
                    or entry.name == '.walletlock'):

                pass

            else:
                # remove the root dir so if the directory is moved, everything still works
                path = entry.path.replace(root_dir, '')

                stat_result = os.stat(entry.path)
                stats_dict = {
                    'st_mode': stat_result.st_mode,
                    'st_ino': stat_result.st_ino,
                    'st_dev': stat_result.st_dev,
                    'st_nlink': stat_result.st_nlink,
                    'st_uid': stat_result.st_uid,
                    'st_gid': stat_result.st_gid,
                    'st_size': stat_result.st_size,
                    'st_atime': stat_result.st_atime,
                    'st_mtime': stat_result.st_mtime,
                    'st_ctime': stat_result.st_ctime
                }
                if is_windows():
                    stats_dict[
                        'st_file_attributes'] = stat_result.st_file_attributes

                file_stats = json.dumps([path, stats_dict])
                json_file.write(f'{file_stats}\n')

                if entry.is_dir() and entry.name != 'wallets':
                    self.save_metadata(root_dir, entry.path, json_file)

    def save_bcb_database(self):
        '''
            Save the blockchain_backup database.

            >>> from blockchain_backup.settings import DATABASE_NAME
            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> blockchain_backup_db_backup_dir = backup_task.save_bcb_database()
            >>> blockchain_backup_db_backup_dir is not None
            True
            >>> os.path.exists(os.path.join(blockchain_backup_db_backup_dir, DATABASE_NAME))
            True
        '''
        try:
            database_filename = DATABASE_PATH
            blockchain_backup_db_backup_dir = os.path.join(
                self.to_backup_dir, constants.BLOCKCHAIN_BACKUP_DB_DIR)
            self.log(
                f'copying {database_filename} to {blockchain_backup_db_backup_dir}'
            )

            if os.path.exists(database_filename):
                if not os.path.exists(blockchain_backup_db_backup_dir):
                    os.makedirs(blockchain_backup_db_backup_dir)

                copy(database_filename, blockchain_backup_db_backup_dir)

                self.save_state(blockchain_backup_db_backup_dir)
            else:
                self.log(f'no such file: {database_filename}')

            self.log('saved blockchain_backup database')
        except:  # 'bare except' because it catches more than "except Exception"
            # saving the blockchain_backup database is not
            # critical to maintaining the blockchain
            self.log(format_exc())
            blockchain_backup_db_backup_dir = None

        return blockchain_backup_db_backup_dir

    def save_state(self, blockchain_backup_db_backup_dir):
        '''
            Save the state of the database
            in a json file so it can easily
            be restored.

            >>> from blockchain_backup.settings import DATABASE_NAME
            >>> from blockchain_backup.bitcoin.tests import utils as test_utils
            >>> test_utils.init_database()
            >>> backup_task = BackupTask()
            >>> backup_task.manager = BitcoinManager(backup_task.log_name)
            >>> backup_task.prep_backup()
            >>> blockchain_backup_db_backup_dir = '/tmp'
            >>> backup_task.save_state(blockchain_backup_db_backup_dir)
            >>> os.path.exists(os.path.join(blockchain_backup_db_backup_dir, constants.STATE_BACKUP_FILENAME))
            True
        '''
        try:
            data = serialize('json', State.objects.all(), indent=4)
            full_path = os.path.join(blockchain_backup_db_backup_dir,
                                     constants.STATE_BACKUP_FILENAME)
            with open(full_path, 'w') as outfile:
                outfile.write(data)
        except:  # 'bare except' because it catches more than "except Exception"
            # saving the blockchain_backup state is not
            # critical to maintaining the blockchain
            self.log(format_exc())
コード例 #4
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
コード例 #5
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')