def __init__(self, api_port, api_key, error_handler): QObject.__init__(self, None) self._logger = logging.getLogger(self.__class__.__name__) self.base_path = get_base_path() if not is_frozen(): self.base_path = os.path.join(get_base_path(), "..") root_state_dir = get_root_state_directory() self.version_history = VersionHistory(root_state_dir) self.core_process = None self.api_port = api_port self.api_key = api_key self.events_manager = EventRequestManager(self.api_port, self.api_key, error_handler) self.shutting_down = False self.should_stop_on_shutdown = False self.use_existing_core = True self.is_core_running = False self.core_traceback = None self.core_traceback_timestamp = 0 self.check_state_timer = QTimer() self.check_state_timer.setSingleShot(True) connect(self.check_state_timer.timeout, self.check_core_ready)
def test_get_disposable_state_directories(tmpdir_factory): tmpdir = tmpdir_factory.mktemp("scenario") root_state_dir = Path(tmpdir) # Scenario: multiple versions of state directory exists, <major.minor.version>. Then on disposable directories # based on the current version show all other directories except the last version. major_versions = [8, 7] minor_versions = list(range(10)) patch_versions = list(range(3)) last_version = "8.9.2" last_version_dir = root_state_dir / "8.9" second_last_version_dir = root_state_dir / "8.8" version_history = {"last_version": last_version, "history": dict()} base_install_ts = time.time() - 1000 # some timestamp in the past # Create state directories for all older versions for major in major_versions: for minor in reversed(minor_versions): for patch in patch_versions: version = f"{major}.{minor}.{patch}" version_dir = f"{major}.{minor}" # Set install time in order of version. i.e. newer version are installed later version_install_ts = base_install_ts + major * 100 + minor * 10 + patch version_history["history"][version_install_ts] = version # Create an empty version directory if does not exist (root_state_dir / version_dir).mkdir(exist_ok=True) unused1 = root_state_dir / "unused_v8.9_1234567" unused2 = root_state_dir / "unused_v9.0_7654321" unused1.mkdir() unused2.mkdir() # Write the version history file before checking disposable directories (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(version_history)) code_version_id = "9.0.0" history = VersionHistory(root_state_dir, code_version_id) # Case 1: Skip last two versions, then those two last directories will not returned as disposable dirs. disposable_dirs = history.get_disposable_state_directories() assert last_version_dir in disposable_dirs assert second_last_version_dir in disposable_dirs assert unused1 in disposable_dirs assert unused2 in disposable_dirs # Case 2: Skip only one version disposable_dirs = history.get_disposable_state_directories( skip_versions=1, include_unused=False) assert last_version_dir not in disposable_dirs assert second_last_version_dir in disposable_dirs assert unused1 not in disposable_dirs assert unused2 not in disposable_dirs
def __init__(self): QWidget.__init__(self) self.settings = None self.version_history = VersionHistory(get_root_state_directory()) self.lang_list = sorted([ lang_name for lang_name, lang_code in AVAILABLE_TRANSLATIONS.items() ]) self.lang_list.insert(0, tr("System default"))
def test_installed_versions_and_removal(tmpdir_factory): tmpdir = tmpdir_factory.mktemp("install_version_test") root_state_dir = Path(tmpdir) # create current version directory code_version_id = "8.9.10" current_version_dir = root_state_dir / "8.9" current_version_dir.mkdir() major_versions = [7, 8] minor_versions = [5, 6, 7, 8] version_history = {"last_version": "7.8", "history": dict()} base_install_ts = time.time() - 1000 # some timestamp in the past for major in major_versions: for minor in minor_versions: version_str = f"{major}.{minor}" (root_state_dir / version_str).mkdir(exist_ok=True) # Set install time in order of version. i.e. newer version are installed later version_install_ts = base_install_ts + major * 100 + minor * 10 version_history["history"][version_install_ts] = version_str (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(version_history)) history = VersionHistory(root_state_dir, code_version_id) # 1. Default values installed_versions = history.get_installed_versions() assert history.code_version in installed_versions assert len(installed_versions) == len(major_versions) * len( minor_versions) + 1 # including the current version # 2. exclude current version installed_versions = history.get_installed_versions( with_code_version=False) assert history.code_version not in installed_versions assert len(installed_versions) == len(major_versions) * len( minor_versions) # the current version not included # 3. Delete a few versions history.versions[7, 5].delete_state() history.versions[7, 6].delete_state() installed_versions = history.get_installed_versions( with_code_version=False) assert current_version_dir not in installed_versions assert len( installed_versions) == len(major_versions) * len(minor_versions) - 2
def test_get_last_upgradable_version_based_on_dir(tmpdir): """ Scenario: 5 versions in the history file, but state directory only for one of those exists. The second version in the list has higher version than the current one, and has dir too. Test that only the most recent lower version will be selected as the upgrade source. """ root_state_dir = Path(tmpdir) json_dict = {"last_version": "100.1.1", "history": dict()} json_dict["history"]["1"] = "100.1.1" # no dir - bad json_dict["history"]["2"] = "99.2.3" # dir in place, earlier than 3 - bad (root_state_dir / "102.1").mkdir() json_dict["history"][ "3"] = "102.1.0" # version OK, got dir, same major/minor version as us - ignore (root_state_dir / "99.2").mkdir() json_dict["history"][ "4"] = "92.3.4" # dir in place, more recent than 2, - good (root_state_dir / "92.3").mkdir() json_dict["history"]["5"] = "200.2.3" # version higher than code version (root_state_dir / "200.2").mkdir() json_dict["history"]["6"] = "94.3.4" # version OK, no dir - bad (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(json_dict)) history = VersionHistory(root_state_dir, code_version_id="102.1.1") assert history.code_version.can_be_copied_from is not None assert history.code_version.can_be_copied_from.version_str == "92.3.4"
def upgrade_state_dir(root_state_dir: Path, update_status_callback=None, interrupt_upgrade_event=None): # Before any upgrade, prepare a separate state directory for the update version so it does not # affect the older version state directory. This allows for safe rollback. version_history = VersionHistory(root_state_dir) version_history.fork_state_directory_if_necessary() version_history.save_if_necessary() state_dir = version_history.code_version.directory if not state_dir.exists(): return config = TriblerConfig.load(file=state_dir / CONFIG_FILE_NAME, state_dir=state_dir, reset_config_on_error=True) channels_dir = config.chant.get_path_as_absolute('channels_dir', config.state_dir) primary_private_key_path = config.state_dir / KeyComponent.get_private_key_filename( config) primary_public_key_path = config.state_dir / config.trustchain.ec_keypair_pubfilename primary_key = KeyComponent.load_or_create(primary_private_key_path, primary_public_key_path) upgrader = TriblerUpgrader(state_dir, channels_dir, primary_key, update_status_callback=update_status_callback, interrupt_upgrade_event=interrupt_upgrade_event) upgrader.run()
async def start_tribler(): # Check if we are already running a Tribler instance process_checker = ProcessChecker(root_state_dir) if process_checker.already_running: return process_checker.create_lock_file() # Before any upgrade, prepare a separate state directory for the update version so it does not # affect the older version state directory. This allows for safe rollback. version_history = VersionHistory(root_state_dir) version_history.fork_state_directory_if_necessary() version_history.save_if_necessary() state_dir = version_history.code_version.directory config = TriblerConfig(state_dir, config_file=state_dir / CONFIG_FILENAME, reset_config_on_error=True) if not config.get_core_error_reporting_requires_user_consent(): SentryReporter.global_strategy = SentryStrategy.SEND_ALLOWED config.set_api_http_port(int(api_port)) # If the API key is set to an empty string, it will remain disabled if config.get_api_key() not in ('', api_key): config.set_api_key(api_key) config.write( ) # Immediately write the API key so other applications can use it config.set_api_http_enabled(True) priority_order = config.get_cpu_priority_order() set_process_priority(pid=os.getpid(), priority_order=priority_order) global trace_logger # Enable tracer if --trace-debug or --trace-exceptions flag is present in sys.argv trace_logger = check_and_enable_code_tracing('core', config.get_log_dir()) session = Session(config, core_test_mode=core_test_mode) signal.signal(signal.SIGTERM, lambda signum, stack: shutdown(session, signum, stack)) await session.start()
def test_read_write_version_history(tmpdir): root_path = Path(tmpdir) history = VersionHistory(root_path, code_version_id='100.100.100') assert history.root_state_dir == root_path assert history.file_path == root_path / "version_history.json" assert history.file_data == {"last_version": None, "history": {}} # If there is no version history file, no information about last version is available assert history.last_run_version is None assert not history.versions assert history.code_version.version_str == '100.100.100' assert history.code_version.major_minor == (100, 100) # Saving and loading the version again history.save() history2 = VersionHistory(root_path, code_version_id='100.100.100') # version was not added to history as the state directory does not exist assert history2.last_run_version is None assert not history2.versions assert not history2.versions_by_number assert not history2.versions_by_time state_dir: Path = root_path / "100.100" state_dir.mkdir() history3 = VersionHistory(root_path, code_version_id='100.100.100') assert history3.last_run_version is not None assert history3.last_run_version.version_str == '100.100.100' assert history3.last_run_version.directory == state_dir assert len(history3.versions) == 1 assert (100, 100) in history3.versions assert history3.versions[100, 100] == history3.last_run_version assert history3.versions_by_number == [history3.last_run_version] assert history3.versions_by_time == [history3.last_run_version]
def __init__(self): QWidget.__init__(self) self.settings = None self.saved_dialog = None self.version_history = VersionHistory(get_root_state_directory())
class SettingsPage(AddBreadcrumbOnShowMixin, QWidget): """ This class is responsible for displaying and adjusting the settings present in Tribler. """ def __init__(self): QWidget.__init__(self) self.settings = None self.saved_dialog = None self.version_history = VersionHistory(get_root_state_directory()) def initialize_settings_page(self): self.window().settings_tab.initialize() connect(self.window().settings_tab.clicked_tab_button, self.clicked_tab_button) connect(self.window().settings_save_button.clicked, self.save_settings) connect(self.window().download_location_chooser_button.clicked, self.on_choose_download_dir_clicked) connect(self.window().watch_folder_chooser_button.clicked, self.on_choose_watch_dir_clicked) connect(self.window().channel_autocommit_checkbox.stateChanged, self.on_channel_autocommit_checkbox_changed) connect(self.window().family_filter_checkbox.stateChanged, self.on_family_filter_checkbox_changed) connect(self.window().developer_mode_enabled_checkbox.stateChanged, self.on_developer_mode_checkbox_changed) connect(self.window().use_monochrome_icon_checkbox.stateChanged, self.on_use_monochrome_icon_checkbox_changed) connect(self.window().download_settings_anon_checkbox.stateChanged, self.on_anon_download_state_changed) connect(self.window().log_location_chooser_button.clicked, self.on_choose_log_dir_clicked) connect(self.window().btn_remove_old_state_dir.clicked, self.on_remove_version_dirs) checkbox_style = get_checkbox_style() for checkbox in [ self.window().family_filter_checkbox, self.window().channel_autocommit_checkbox, self.window().always_ask_location_checkbox, self.window().developer_mode_enabled_checkbox, self.window().use_monochrome_icon_checkbox, self.window().download_settings_anon_checkbox, self.window().download_settings_anon_seeding_checkbox, self.window().lt_utp_checkbox, self.window().watchfolder_enabled_checkbox, self.window().allow_exit_node_checkbox, self.window().developer_mode_enabled_checkbox, self.window().checkbox_enable_network_statistics, self.window().checkbox_enable_resource_log, self.window().download_settings_add_to_channel_checkbox, ]: checkbox.setStyleSheet(checkbox_style) self.update_stacked_widget_height() def on_channel_autocommit_checkbox_changed(self, _): self.window().gui_settings.setValue( "autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) def on_family_filter_checkbox_changed(self, _): self.window().gui_settings.setValue( "family_filter", self.window().family_filter_checkbox.isChecked()) def on_developer_mode_checkbox_changed(self, _): self.window().gui_settings.setValue( "debug", self.window().developer_mode_enabled_checkbox.isChecked()) self.window().left_menu_button_debug.setHidden( not self.window().developer_mode_enabled_checkbox.isChecked()) def on_use_monochrome_icon_checkbox_changed(self, _): use_monochrome_icon = self.window( ).use_monochrome_icon_checkbox.isChecked() self.window().gui_settings.setValue("use_monochrome_icon", use_monochrome_icon) self.window().update_tray_icon(use_monochrome_icon) def on_anon_download_state_changed(self, _): if self.window().download_settings_anon_checkbox.isChecked(): self.window().download_settings_anon_seeding_checkbox.setChecked( True) self.window().download_settings_anon_seeding_checkbox.setEnabled( not self.window().download_settings_anon_checkbox.isChecked()) def on_choose_download_dir_clicked(self, checked): previous_download_path = self.window().download_location_input.text( ) or "" download_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the download location", previous_download_path, QFileDialog.ShowDirsOnly) if not download_dir: return self.window().download_location_input.setText(download_dir) def on_choose_watch_dir_clicked(self, checked): if self.window().watchfolder_enabled_checkbox.isChecked(): previous_watch_dir = self.window().watchfolder_location_input.text( ) or "" watch_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the watch folder", previous_watch_dir, QFileDialog.ShowDirsOnly) if not watch_dir: return self.window().watchfolder_location_input.setText(watch_dir) def on_choose_log_dir_clicked(self, checked): previous_log_dir = self.window().log_location_input.text() or "" log_dir = QFileDialog.getExistingDirectory( self.window(), tr("Please select the log directory"), previous_log_dir, QFileDialog.ShowDirsOnly) if not log_dir or log_dir == previous_log_dir: return is_writable, error = is_dir_writable(log_dir) if not is_writable: gui_error_message = f"<i>{log_dir}</i> is not writable. [{error}]" ConfirmationDialog.show_message(self.window(), tr("Insufficient Permissions"), gui_error_message, "OK") else: self.window().log_location_input.setText(log_dir) def initialize_with_settings(self, settings): if not settings: return self.settings = settings = settings["settings"] gui_settings = self.window().gui_settings self.window().settings_stacked_widget.show() self.window().settings_tab.show() self.window().settings_save_button.show() # General settings self.window().family_filter_checkbox.setChecked( get_gui_setting(gui_settings, 'family_filter', True, is_bool=True)) self.window().use_monochrome_icon_checkbox.setChecked( get_gui_setting(gui_settings, "use_monochrome_icon", False, is_bool=True)) self.window().download_location_input.setText( settings['download_defaults']['saveas']) self.window().always_ask_location_checkbox.setChecked( get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True)) self.window().download_settings_anon_checkbox.setChecked( settings['download_defaults']['anonymity_enabled']) self.window().download_settings_anon_seeding_checkbox.setChecked( settings['download_defaults']['safeseeding_enabled']) self.window().download_settings_add_to_channel_checkbox.setChecked( settings['download_defaults']['add_download_to_channel']) self.window().watchfolder_enabled_checkbox.setChecked( settings['watch_folder']['enabled']) self.window().watchfolder_location_input.setText( settings['watch_folder']['directory']) # Channel settings self.window().channel_autocommit_checkbox.setChecked( get_gui_setting(gui_settings, "autocommit_enabled", True, is_bool=True)) # Log directory self.window().log_location_input.setText( settings['general']['log_dir']) # Connection settings self.window().lt_proxy_type_combobox.setCurrentIndex( settings['libtorrent']['proxy_type']) if settings['libtorrent']['proxy_server']: proxy_server = settings['libtorrent']['proxy_server'].split(":") self.window().lt_proxy_server_input.setText(proxy_server[0]) self.window().lt_proxy_port_input.setText(proxy_server[1]) if settings['libtorrent']['proxy_auth']: proxy_auth = settings['libtorrent']['proxy_auth'].split(":") self.window().lt_proxy_username_input.setText(proxy_auth[0]) self.window().lt_proxy_password_input.setText(proxy_auth[1]) self.window().lt_utp_checkbox.setChecked(settings['libtorrent']['utp']) max_conn_download = settings['libtorrent']['max_connections_download'] if max_conn_download == -1: max_conn_download = 0 self.window().max_connections_download_input.setText( str(max_conn_download)) self.window().api_port_input.setText( f"{get_gui_setting(gui_settings, 'api_port', DEFAULT_API_PORT)}") # Bandwidth settings self.window().upload_rate_limit_input.setText( str(settings['libtorrent']['max_upload_rate'] // 1024)) self.window().download_rate_limit_input.setText( str(settings['libtorrent']['max_download_rate'] // 1024)) # Seeding settings getattr( self.window(), "seeding_" + settings['download_defaults']['seeding_mode'] + "_radio").setChecked(True) self.window().seeding_time_input.setText( seconds_to_hhmm_string( settings['download_defaults']['seeding_time'])) ind = self.window().seeding_ratio_combobox.findText( str(settings['download_defaults']['seeding_ratio'])) if ind != -1: self.window().seeding_ratio_combobox.setCurrentIndex(ind) # Anonymity settings self.window().allow_exit_node_checkbox.setChecked( settings['tunnel_community']['exitnode_enabled']) self.window().number_hops_slider.setValue( int(settings['download_defaults']['number_hops'])) connect(self.window().number_hops_slider.valueChanged, self.update_anonymity_cost_label) self.update_anonymity_cost_label( int(settings['download_defaults']['number_hops'])) # Data settings self.load_settings_data_tab() # Debug self.window().developer_mode_enabled_checkbox.setChecked( get_gui_setting(gui_settings, "debug", False, is_bool=True)) self.window().checkbox_enable_resource_log.setChecked( settings['resource_monitor']['enabled']) cpu_priority = 1 if 'cpu_priority' in settings['resource_monitor']: cpu_priority = int(settings['resource_monitor']['cpu_priority']) self.window().slider_cpu_level.setValue(cpu_priority) self.window().cpu_priority_value.setText( f"Current Priority = {cpu_priority}") connect(self.window().slider_cpu_level.valueChanged, self.show_updated_cpu_priority) self.window().checkbox_enable_network_statistics.setChecked( settings['ipv8']['statistics']) def _version_dir_checkbox(self, state_dir, enabled=True): dir_size = sum(f.stat().st_size for f in state_dir.glob('**/*')) text = f"{state_dir} {format_size(dir_size)}" checkbox = QCheckBox(text) checkbox.setEnabled(enabled) return checkbox def load_settings_data_tab(self): self.refresh_current_version_checkbox() self.refresh_old_version_checkboxes() def refresh_current_version_checkbox(self): get_root_state_directory() code_version_dir = self.version_history.code_version.directory self.refresh_version_checkboxes(self.window().state_dir_current, [code_version_dir], enabled=False) def refresh_old_version_checkboxes(self): get_root_state_directory() old_state_dirs = self.version_history.get_disposable_state_directories( ) self.refresh_version_checkboxes(self.window().state_dir_list, old_state_dirs, enabled=True) def refresh_version_checkboxes(self, parent, old_state_dirs, enabled=True): version_dirs_layout = parent.layout() for checkbox in parent.findChildren(QCheckBox): version_dirs_layout.removeWidget(checkbox) checkbox.setParent(None) checkbox.deleteLater() for state_dir in old_state_dirs: version_dir_checkbox = self._version_dir_checkbox(state_dir, enabled=enabled) version_dirs_layout.addWidget(version_dir_checkbox) def on_remove_version_dirs(self, _): root_version_dir = str(get_root_state_directory()) def dir_from_checkbox_text(checkbox): # eg text: "/home/<user>/.Tribler/v7.8 5 GB" state_dir = checkbox.text().rpartition(" ")[0] if not state_dir.startswith(root_version_dir): return None # safety check just for case state_dir = state_dir[len(root_version_dir):] if state_dir.startswith('/'): state_dir = state_dir[1:] return state_dir dirs_selected_for_deletion = [] for checkbox in self.window().state_dir_list.findChildren(QCheckBox): if checkbox.isChecked(): state_dir = dir_from_checkbox_text(checkbox) if state_dir: dirs_selected_for_deletion.append(state_dir) if self.on_confirm_remove_version_dirs(dirs_selected_for_deletion): remove_state_dirs(root_version_dir, dirs_selected_for_deletion) self.refresh_old_version_checkboxes() def on_confirm_remove_version_dirs(self, selected_versions): message_box = QMessageBox() message_box.setIcon(QMessageBox.Question) if selected_versions: version_dirs_str = "\n- ".join(selected_versions) versions_info = tr("Versions: \n- %s") % version_dirs_str title = tr("Confirm delete older versions?") message_body = (tr("Are you sure to remove the selected versions? " "\nYou can not undo this action." "\n\n ") % versions_info) message_buttons = QMessageBox.No | QMessageBox.Yes else: title = tr("No versions selected") message_body = tr("Select a version to delete.") message_buttons = QMessageBox.Close message_box.setWindowTitle(title) message_box.setText(message_body) message_box.setStandardButtons(message_buttons) user_choice = message_box.exec_() return user_choice == QMessageBox.Yes def update_anonymity_cost_label(self, value): html_text = tr( "<html><head/><body><p>Download with <b>%d</b> hop(s) of anonymity. " "When you download a file of 200 Megabyte, you will pay roughly <b>%d</b>" "Megabyte of bandwidth tokens.</p></body></html>") % ( value, 400 * (value - 1) + 200, ) self.window().anonymity_costs_label.setText(html_text) def show_updated_cpu_priority(self, value): self.window().cpu_priority_value.setText( tr("Current Priority = %s") % value) def load_settings(self): self.window().settings_stacked_widget.hide() self.window().settings_tab.hide() self.window().settings_save_button.hide() TriblerNetworkRequest("settings", self.initialize_with_settings) def clicked_tab_button(self, tab_button_name): if tab_button_name == "settings_general_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_GENERAL) elif tab_button_name == "settings_connection_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_CONNECTION) elif tab_button_name == "settings_bandwidth_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_BANDWIDTH) elif tab_button_name == "settings_seeding_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_SEEDING) elif tab_button_name == "settings_anonymity_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_ANONYMITY) elif tab_button_name == "settings_data_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_DATA) elif tab_button_name == "settings_debug_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_DEBUG) self.update_stacked_widget_height() def update_stacked_widget_height(self): """ Update the height of the settings tab. This is required since the height of a QStackedWidget is by default the height of the largest page. This messes up the scroll bar. """ for index in range(self.window().settings_stacked_widget.count()): if index == self.window().settings_stacked_widget.currentIndex(): self.window().settings_stacked_widget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred) else: self.window().settings_stacked_widget.setSizePolicy( QSizePolicy.Ignored, QSizePolicy.Ignored) self.window().settings_stacked_widget.adjustSize() def save_settings(self, checked): # Create a dictionary with all available settings settings_data = { 'general': {}, 'Tribler': {}, 'download_defaults': {}, 'libtorrent': {}, 'watch_folder': {}, 'tunnel_community': {}, 'trustchain': {}, 'resource_monitor': {}, 'ipv8': {}, 'chant': {}, } settings_data['download_defaults']['saveas'] = self.window( ).download_location_input.text() settings_data['general']['log_dir'] = self.window( ).log_location_input.text() settings_data['watch_folder']['enabled'] = self.window( ).watchfolder_enabled_checkbox.isChecked() if settings_data['watch_folder']['enabled']: settings_data['watch_folder']['directory'] = self.window( ).watchfolder_location_input.text() settings_data['libtorrent']['proxy_type'] = self.window( ).lt_proxy_type_combobox.currentIndex() if (self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0): try: settings_data['libtorrent']['proxy_server'] = "{}:{}".format( self.window().lt_proxy_server_input.text(), int(self.window().lt_proxy_port_input.text()), ) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid proxy port number"), tr("You've entered an invalid format for the proxy port number. Please enter a whole number." ), ) return else: settings_data['libtorrent']['proxy_server'] = ":" if self.window().lt_proxy_username_input.text() and self.window( ).lt_proxy_password_input.text(): settings_data['libtorrent']['proxy_auth'] = "{}:{}".format( self.window().lt_proxy_username_input.text(), self.window().lt_proxy_password_input.text(), ) else: settings_data['libtorrent']['proxy_auth'] = ":" settings_data['libtorrent']['utp'] = self.window( ).lt_utp_checkbox.isChecked() try: max_conn_download = int( self.window().max_connections_download_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid number of connections"), tr("You've entered an invalid format for the maximum number of connections. " "Please enter a whole number."), ) return if max_conn_download == 0: max_conn_download = -1 settings_data['libtorrent'][ 'max_connections_download'] = max_conn_download try: if self.window().upload_rate_limit_input.text(): user_upload_rate_limit = int( float(self.window().upload_rate_limit_input.text()) * 1024) if user_upload_rate_limit < MAX_LIBTORRENT_RATE_LIMIT: settings_data['libtorrent'][ 'max_upload_rate'] = user_upload_rate_limit else: raise ValueError if self.window().download_rate_limit_input.text(): user_download_rate_limit = int( float(self.window().download_rate_limit_input.text()) * 1024) if user_download_rate_limit < MAX_LIBTORRENT_RATE_LIMIT: settings_data['libtorrent'][ 'max_download_rate'] = user_download_rate_limit else: raise ValueError except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid value for bandwidth limit"), tr("You've entered an invalid value for the maximum upload/download rate. \n" "The rate is specified in KB/s and the value permitted is between 0 and %d KB/s.\n" "Note that the decimal values are truncated.") % (MAX_LIBTORRENT_RATE_LIMIT / 1024), ) return try: if self.window().api_port_input.text(): api_port = int(self.window().api_port_input.text()) if api_port <= 0 or api_port >= 65536: raise ValueError() self.window().gui_settings.setValue("api_port", api_port) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid value for api port"), tr("Please enter a valid port for the api (between 0 and 65536)" ), ) return seeding_modes = ['forever', 'time', 'never', 'ratio'] selected_mode = 'forever' for seeding_mode in seeding_modes: if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked(): selected_mode = seeding_mode break settings_data['download_defaults']['seeding_mode'] = selected_mode settings_data['download_defaults']['seeding_ratio'] = float( self.window().seeding_ratio_combobox.currentText()) try: settings_data['download_defaults'][ 'seeding_time'] = string_to_seconds( self.window().seeding_time_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid seeding time"), tr("You've entered an invalid format for the seeding time (expected HH:MM)" ), ) return settings_data['tunnel_community']['exitnode_enabled'] = self.window( ).allow_exit_node_checkbox.isChecked() settings_data['download_defaults']['number_hops'] = self.window( ).number_hops_slider.value() settings_data['download_defaults']['anonymity_enabled'] = self.window( ).download_settings_anon_checkbox.isChecked() settings_data['download_defaults'][ 'safeseeding_enabled'] = self.window( ).download_settings_anon_seeding_checkbox.isChecked() settings_data['download_defaults'][ 'add_download_to_channel'] = self.window( ).download_settings_add_to_channel_checkbox.isChecked() settings_data['resource_monitor']['enabled'] = self.window( ).checkbox_enable_resource_log.isChecked() settings_data['resource_monitor']['cpu_priority'] = int( self.window().slider_cpu_level.value()) # network statistics settings_data['ipv8']['statistics'] = self.window( ).checkbox_enable_network_statistics.isChecked() self.window().settings_save_button.setEnabled(False) # TODO: do it in RESTful style, on the REST return JSON instead # In case the default save dir has changed, add it to the top of the list of last download locations. # Otherwise, the user could absentmindedly click through the download dialog and start downloading into # the last used download dir, and not into the newly designated default download dir. if self.settings['download_defaults']['saveas'] != settings_data[ 'download_defaults']['saveas']: self.window().update_recent_download_locations( settings_data['download_defaults']['saveas']) self.settings = settings_data TriblerNetworkRequest("settings", self.on_settings_saved, method='POST', raw_data=json.dumps(settings_data)) def on_settings_saved(self, data): if not data: return # Now save the GUI settings self.window().gui_settings.setValue( "family_filter", self.window().family_filter_checkbox.isChecked()) self.window().gui_settings.setValue( "autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) self.window().gui_settings.setValue( "ask_download_settings", self.window().always_ask_location_checkbox.isChecked()) self.window().gui_settings.setValue( "use_monochrome_icon", self.window().use_monochrome_icon_checkbox.isChecked()) self.saved_dialog = ConfirmationDialog( TriblerRequestManager.window, tr("Settings saved"), tr("Your settings have been saved."), [(tr("CLOSE"), BUTTON_TYPE_NORMAL)], ) connect(self.saved_dialog.button_clicked, self.on_dialog_cancel_clicked) self.saved_dialog.show() self.window().fetch_settings() def on_dialog_cancel_clicked(self, _): self.window().settings_save_button.setEnabled(True) self.saved_dialog.close_dialog() self.saved_dialog = None
def test_coverage(tmpdir): root_state_dir = Path(tmpdir) v1 = TriblerVersion(root_state_dir, "7.3.1a") assert repr(v1) == '<TriblerVersion{7.3.1a}>' with pytest.raises(VersionError, match='Cannot rename root directory'): v1.rename_directory("foo") v2 = TriblerVersion(root_state_dir, "7.8.1") assert v2.directory == root_state_dir / "7.8" v2.directory.mkdir() v2.rename_directory("renamed") assert list(root_state_dir.glob("renamed7.8_*")) assert v2.directory == root_state_dir / "7.8" v2.directory.mkdir() (v2.directory / "foobar.txt").write_text("abc") v2.delete_state() assert list(root_state_dir.glob("deleted_v7.8_*")) v2.directory = Path(DUMMY_STATE_DIR) size = v2.calc_state_size() assert size > 0 v3 = TriblerVersion(root_state_dir, "7.7") v3.directory.mkdir() v3.deleted = True v3.delete_state() assert v3.directory.exists() v3.deleted = False v3.delete_state() assert not v3.directory.exists() v4 = TriblerVersion(root_state_dir, "7.5.1a") v4.directory.mkdir() (v4.directory / 'triblerd.conf').write_text("abc") v5 = TriblerVersion(root_state_dir, "7.6.1b") v5.directory.mkdir() with pytest.raises(VersionError, match='Directory for version 7.6.1b already exists'): v5.copy_state_from(v4) v5.copy_state_from(v4, overwrite=True) assert (v5.directory / 'triblerd.conf').read_text() == "abc" (root_state_dir / "version_history.json").write_text('{"last_version": "7.7"}') with pytest.raises(VersionError, match="Invalid history file structure"): VersionHistory(root_state_dir) (root_state_dir / "version_history.json").write_text( '{"last_version": "7.7", "history": {"1": "7.3.1a", "2": "7.7", "3": "7.5.1a", "4": "7.6.1b", "5": "7.8.1"}}' ) (root_state_dir / "sqlite").mkdir() (root_state_dir / "channels").mkdir() (root_state_dir / 'triblerd.conf').write_text("abc") history = VersionHistory(root_state_dir) assert history.code_version.version_str == tribler_core.version.version_id assert repr(history) == "<VersionHistory[(7, 6), (7, 5), (7, 3)]>" dirs = history.get_disposable_state_directories() names = [d.name for d in dirs] assert len(names) == 5 for name in names: assert name in ('7.5', '7.6', 'channels', 'sqlite') or name.startswith("deleted_v7.8_") remove_state_dirs(root_state_dir, names) assert not (root_state_dir / "7.5").exists() assert not (root_state_dir / "7.6").exists() assert not (root_state_dir / "channels").exists() assert not (root_state_dir / "sqlite").exists()
def test_fork_state_directory(tmpdir_factory): # Scenario 1: the last used version has the same major/minor number as the code version, dir in place # no forking should happen, but version_history should be updated nonetheless tmpdir = tmpdir_factory.mktemp("scenario1") root_state_dir = Path(tmpdir) json_dict = {"last_version": "120.1.1", "history": dict()} json_dict["history"]["2"] = "120.1.1" state_dir = root_state_dir / "120.1" state_dir.mkdir() (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(json_dict)) code_version_id = "120.1.2" history = VersionHistory(root_state_dir, code_version_id) assert history.last_run_version is not None assert history.last_run_version.directory == state_dir assert history.last_run_version != history.code_version assert history.code_version.directory == state_dir assert history.code_version.version_str != history.last_run_version.version_str assert not history.code_version.should_be_copied assert not history.code_version.should_recreate_directory forked_from = history.fork_state_directory_if_necessary() assert forked_from is None history_saved = history.save_if_necessary() assert history_saved history2 = VersionHistory(root_state_dir, code_version_id) assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id # Scenario 2: the last used version minor is lower than the code version, directory exists # normal upgrade scenario, dir should be forked and version_history should be updated tmpdir = tmpdir_factory.mktemp("scenario2") root_state_dir = Path(tmpdir) json_dict = {"last_version": "120.1.1", "history": dict()} json_dict["history"]["1"] = "120.1.1" state_dir = root_state_dir / "120.1" state_dir.mkdir() (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(json_dict)) code_version_id = "120.3.2" history = VersionHistory(root_state_dir, code_version_id) assert history.last_run_version is not None assert history.last_run_version.directory == state_dir assert history.code_version != history.last_run_version assert history.code_version.directory != state_dir assert history.code_version.version_str != history.last_run_version.version_str assert history.code_version.should_be_copied assert not history.code_version.should_recreate_directory assert not history.code_version.directory.exists() forked_from = history.fork_state_directory_if_necessary() assert history.code_version.directory.exists() assert forked_from is not None and forked_from.version_str == "120.1.1" history_saved = history.save_if_necessary() assert history_saved history2 = VersionHistory(root_state_dir, code_version_id) assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id # Scenario 3: upgrade from 7.3 (unversioned dir) # dir should be forked and version_history should be created tmpdir = tmpdir_factory.mktemp("scenario3") root_state_dir = Path(tmpdir) (root_state_dir / "triblerd.conf").write_text("foo") # 7.3 presence marker code_version_id = "120.3.2" history = VersionHistory(root_state_dir, code_version_id) assert history.last_run_version is not None assert history.last_run_version.directory == root_state_dir assert history.code_version != history.last_run_version assert history.code_version.directory != root_state_dir assert history.code_version.should_be_copied assert history.code_version.can_be_copied_from is not None assert history.code_version.can_be_copied_from.version_str == "7.3" assert not history.code_version.should_recreate_directory assert not history.code_version.directory.exists() forked_from = history.fork_state_directory_if_necessary() assert history.code_version.directory.exists() assert forked_from is not None and forked_from.version_str == "7.3" history_saved = history.save_if_necessary() assert history_saved history2 = VersionHistory(root_state_dir, code_version_id) assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id # Scenario 4: the user tried to upgrade to some tribler version, but failed. Now he tries again with # higher patch version of the same major/minor version. # The most recently used dir with major/minor version lower than the code version should be forked, # while the previous code version state directory should be renamed to a backup. tmpdir = tmpdir_factory.mktemp("scenario4") root_state_dir = Path(tmpdir) json_dict = {"last_version": "120.2.1", "history": dict()} # The user was on 120.2 json_dict["history"]["1"] = "120.2.0" state_dir_1 = root_state_dir / "120.2" state_dir_1.mkdir() # The user tried 120.3, they did not like it json_dict["history"]["2"] = "120.3.0" state_dir_2 = root_state_dir / "120.3" state_dir_2.mkdir() # The user returned to 120.2 and continued to use it json_dict["history"]["3"] = "120.2.1" (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(json_dict)) # Now user tries 120.3.2 which has a higher patch version than his previous attempt at 120.3 series code_version_id = "120.3.2" history = VersionHistory(root_state_dir, code_version_id) assert history.last_run_version is not None assert history.last_run_version.directory == state_dir_1 assert history.code_version != history.last_run_version assert history.code_version.directory != root_state_dir assert history.code_version.should_be_copied assert history.code_version.can_be_copied_from is not None assert history.code_version.can_be_copied_from.version_str == "120.2.1" assert history.code_version.directory.exists() assert history.code_version.should_recreate_directory forked_from = history.fork_state_directory_if_necessary() assert history.code_version.directory.exists() assert forked_from is not None and forked_from.version_str == "120.2.1" history_saved = history.save_if_necessary() assert history_saved # Check that the older 120.3 directory is not deleted, but instead renamed as a backup assert list(root_state_dir.glob("unused_v120.3_*")) history2 = VersionHistory(root_state_dir, code_version_id) assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id # Scenario 5: normal upgrade scenario, but from 7.4.x version (dir includes patch number) tmpdir = tmpdir_factory.mktemp("scenario5") root_state_dir = Path(tmpdir) json_dict = {"last_version": "7.4.4", "history": dict()} json_dict["history"]["2"] = "7.4.4" state_dir = root_state_dir / "7.4.4" state_dir.mkdir() (root_state_dir / VERSION_HISTORY_FILENAME).write_text( json.dumps(json_dict)) code_version_id = "7.5.1" history = VersionHistory(root_state_dir, code_version_id) assert history.last_run_version is not None assert history.last_run_version.directory == state_dir assert history.code_version != history.last_run_version assert history.code_version.directory != root_state_dir assert history.code_version.should_be_copied assert history.code_version.can_be_copied_from is not None assert history.code_version.can_be_copied_from.version_str == "7.4.4" assert not history.code_version.directory.exists() assert not history.code_version.should_recreate_directory forked_from = history.fork_state_directory_if_necessary() assert history.code_version.directory.exists() assert forked_from is not None and forked_from.version_str == "7.4.4" history_saved = history.save_if_necessary() assert history_saved history2 = VersionHistory(root_state_dir, code_version_id) assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id
# Check whether we need to start the core or the user interface if parsed_args.core: from tribler_core.check_os import should_kill_other_tribler_instances should_kill_other_tribler_instances(root_state_dir) logger.info('Running Core' + ' in gui_test_mode' if parsed_args.gui_test_mode else '') load_logger_config('tribler-core', root_state_dir) # Check if we are already running a Tribler instance process_checker = ProcessChecker(root_state_dir) if process_checker.already_running: logger.info('Core is already running, exiting') sys.exit(1) process_checker.create_lock_file() version_history = VersionHistory(root_state_dir) state_dir = version_history.code_version.directory try: start_core.run_tribler_core(api_port, api_key, state_dir, gui_test_mode=parsed_args.gui_test_mode) finally: logger.info('Remove lock file') process_checker.remove_lock_file() else: from tribler_gui.utilities import get_translator logger.info('Running GUI' + ' in gui_test_mode' if parsed_args.gui_test_mode else '') # Workaround for macOS Big Sur, see https://github.com/Tribler/tribler/issues/5728 if sys.platform == "darwin": logger.info('Enabling a workaround for macOS Big Sur')
class CoreManager(QObject): """ The CoreManager is responsible for managing the Tribler core (starting/stopping). When we are running the GUI tests, a fake API will be started. """ tribler_stopped = pyqtSignal() core_state_update = pyqtSignal(str) def __init__(self, api_port, api_key, error_handler): QObject.__init__(self, None) self._logger = logging.getLogger(self.__class__.__name__) self.base_path = get_base_path() if not is_frozen(): self.base_path = os.path.join(get_base_path(), "..") root_state_dir = get_root_state_directory() self.version_history = VersionHistory(root_state_dir) self.core_process = None self.api_port = api_port self.api_key = api_key self.events_manager = EventRequestManager(self.api_port, self.api_key, error_handler) self.shutting_down = False self.should_stop_on_shutdown = False self.use_existing_core = True self.is_core_running = False self.core_traceback = None self.core_traceback_timestamp = 0 self.check_state_timer = QTimer() self.check_state_timer.setSingleShot(True) connect(self.check_state_timer.timeout, self.check_core_ready) def on_core_read_ready(self): raw_output = bytes(self.core_process.readAll()) decoded_output = raw_output.decode(errors="replace") if b'Traceback' in raw_output: self.core_traceback = decoded_output self.core_traceback_timestamp = int(round(time.time() * 1000)) print(decoded_output.strip()) # noqa: T001 def on_core_finished(self, exit_code, exit_status): if self.shutting_down and self.should_stop_on_shutdown: self.on_finished() elif not self.shutting_down and exit_code != 0: # Stop the event manager loop if it is running if self.events_manager.connect_timer and self.events_manager.connect_timer.isActive( ): self.events_manager.connect_timer.stop() exception_msg = ( f"The Tribler core has unexpectedly finished " f"with exit code {exit_code} and status: {exit_status}!") if self.core_traceback: exception_msg += "\n\n%s\n(Timestamp: %d, traceback timestamp: %d)" % ( self.core_traceback, int(round(time.time() * 1000)), self.core_traceback_timestamp, ) raise RuntimeError(exception_msg) def start(self, core_args=None, core_env=None): """ First test whether we already have a Tribler process listening on port <CORE_API_PORT>. If so, use that one and don't start a new, fresh session. """ def on_request_error(_): self.use_existing_core = False self.start_tribler_core(core_args=core_args, core_env=core_env) versions_to_delete = self.should_cleanup_old_versions() if versions_to_delete: for version in versions_to_delete: version.delete_state() # Connect to the events manager only after the cleanup is done self.events_manager.connect() connect(self.events_manager.reply.error, on_request_error) # Determine if we have notify the user to wait for the directory fork to finish if self.version_history.code_version.should_be_copied: # There is going to be a directory fork, so we extend the core connection timeout and notify the user self.events_manager.remaining_connection_attempts = 1200 self.events_manager.change_loading_text.emit( "Copying data from previous Tribler version, please wait") def should_cleanup_old_versions(self) -> List[TriblerVersion]: # Skip old version check popup when running fake core, eg. during GUI tests # or during deployment tests since it blocks the tests with a popup dialog if START_FAKE_API or SKIP_VERSION_CLEANUP: return [] if self.version_history.last_run_version == self.version_history.code_version: return [] disposable_versions = self.version_history.get_disposable_versions( skip_versions=2) if not disposable_versions: return [] storage_info = "" claimable_storage = 0 for version in disposable_versions: state_size = version.calc_state_size() claimable_storage += state_size storage_info += f"{version.version_str} \t {format_size(state_size)}\n" # Show a question to the user asking if the user wants to remove the old data. title = "Delete state directories for old versions?" message_body = tr( "Press 'Yes' to remove state directories for older versions of Tribler " "and reclaim %s of storage space. " "Tribler used those directories during upgrades from previous versions. " "Now those directories can be safely deleted. \n\n" "If unsure, press 'No'. " "You will be able to remove those directories from the Settings->Data page later." ) % format_size(claimable_storage) user_choice = self._show_question_box(title, message_body, storage_info, default_button=QMessageBox.Yes) if user_choice == QMessageBox.Yes: return disposable_versions return [] def _show_question_box(self, title, body, additional_text, default_button=None): message_box = QMessageBox() message_box.setIcon(QMessageBox.Question) message_box.setWindowTitle(title) message_box.setText(body) message_box.setInformativeText(additional_text) message_box.setStandardButtons(QMessageBox.No | QMessageBox.Yes) if default_button: message_box.setDefaultButton(default_button) return message_box.exec_() def start_tribler_core(self, core_args=None, core_env=None): if not START_FAKE_API: if not core_env: core_env = QProcessEnvironment.systemEnvironment() core_env.insert("CORE_PROCESS", "1") core_env.insert("CORE_BASE_PATH", self.base_path) core_env.insert("CORE_API_PORT", f"{self.api_port}") core_env.insert("CORE_API_KEY", self.api_key.decode('utf-8')) if not core_args: core_args = sys.argv self.core_process = QProcess() self.core_process.setProcessEnvironment(core_env) self.core_process.setReadChannel(QProcess.StandardOutput) self.core_process.setProcessChannelMode(QProcess.MergedChannels) connect(self.core_process.readyRead, self.on_core_read_ready) connect(self.core_process.finished, self.on_core_finished) self.core_process.start(sys.executable, core_args) self.check_core_ready() def check_core_ready(self): TriblerNetworkRequest("state", self.on_received_state, capture_core_errors=False, priority=QNetworkRequest.HighPriority) def on_received_state(self, state): if not state or 'state' not in state or state['state'] not in [ 'STARTED', 'EXCEPTION' ]: self.check_state_timer.start(50) return self.core_state_update.emit(state['readable_state']) if state['state'] == 'STARTED': self.events_manager.connect(reschedule_on_err=False) self.is_core_running = True elif state['state'] == 'EXCEPTION': raise RuntimeError(state['last_exception']) def stop(self, stop_app_on_shutdown=True): if self.core_process or self.is_core_running: self.events_manager.shutting_down = True TriblerNetworkRequest("shutdown", lambda _: None, method="PUT", priority=QNetworkRequest.HighPriority) if stop_app_on_shutdown: self.should_stop_on_shutdown = True def on_finished(self): self.tribler_stopped.emit() if self.shutting_down: QApplication.quit()