diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
commit | 2e2851dc13d73352530dd4495c7e05603b2e520d (patch) | |
tree | 622b9cd8e5d32091c9aa9e4937b533975a40356c /deluge/ui/gtk3/connectionmanager.py | |
parent | Initial commit. (diff) | |
download | deluge-2e2851dc13d73352530dd4495c7e05603b2e520d.tar.xz deluge-2e2851dc13d73352530dd4495c7e05603b2e520d.zip |
Adding upstream version 2.1.2~dev0+20240219.upstream/2.1.2_dev0+20240219upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/ui/gtk3/connectionmanager.py')
-rw-r--r-- | deluge/ui/gtk3/connectionmanager.py | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/deluge/ui/gtk3/connectionmanager.py b/deluge/ui/gtk3/connectionmanager.py new file mode 100644 index 0000000..b53dd8e --- /dev/null +++ b/deluge/ui/gtk3/connectionmanager.py @@ -0,0 +1,560 @@ +# +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import os +from socket import gaierror, getaddrinfo +from urllib.parse import urlparse + +from gi.repository import Gtk +from twisted.internet import defer, reactor + +import deluge.component as component +from deluge.common import resource_filename, windows_check +from deluge.configmanager import ConfigManager, get_config_dir +from deluge.error import AuthenticationRequired, BadLoginError, IncompatibleClient +from deluge.ui.client import Client, client +from deluge.ui.hostlist import DEFAULT_PORT, LOCALHOST, HostList + +from .common import get_clipboard_text +from .dialogs import AuthenticationDialog, ErrorDialog + +log = logging.getLogger(__name__) + +HOSTLIST_COL_ID = 0 +HOSTLIST_COL_HOST = 1 +HOSTLIST_COL_PORT = 2 +HOSTLIST_COL_USER = 3 +HOSTLIST_COL_PASS = 4 +HOSTLIST_COL_STATUS = 5 +HOSTLIST_COL_VERSION = 6 +HOSTLIST_COL_STATUS_I18N = 7 + +HOSTLIST_ICONS = { + 'offline': 'action-unavailable-symbolic', + 'online': 'network-server-symbolic', + 'connected': 'network-transmit-receive-symbolic', +} +STATUS_I18N = { + 'offline': _('Offline'), + 'online': _('Online'), + 'connected': _('Connected'), +} + + +def cell_render_host(column, cell, model, row, data): + host, port, username = model.get(row, *data) + text = host + ':' + str(port) + if username: + text = username + '@' + text + cell.set_property('text', text) + + +def cell_render_status_icon(column, cell, model, row, data): + status = model[row][data] + status = status if status else 'offline' + icon_name = HOSTLIST_ICONS.get(status, None) + cell.set_property('icon-name', icon_name) + + +class ConnectionManager(component.Component): + def __init__(self): + component.Component.__init__(self, 'ConnectionManager') + self.gtkui_config = ConfigManager('gtk3ui.conf') + self.hostlist = HostList() + self.running = False + + # Component overrides + def start(self): + pass + + def stop(self): + # Close this dialog when we are shutting down + if self.running: + self.connection_manager.response(Gtk.ResponseType.CLOSE) + + def shutdown(self): + pass + + # Public methods + def show(self): + """Show the ConnectionManager dialog.""" + self.builder = Gtk.Builder() + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'connection_manager.ui') + ) + ) + self.connection_manager = self.builder.get_object('connection_manager') + self.connection_manager.set_transient_for(component.get('MainWindow').window) + + # Setup the hostlist liststore and treeview + self.treeview = self.builder.get_object('treeview_hostlist') + self.treeview.set_tooltip_column(HOSTLIST_COL_STATUS_I18N) + self.liststore = self.builder.get_object('liststore_hostlist') + + render = Gtk.CellRendererPixbuf() + column = Gtk.TreeViewColumn(_('Status'), render) + column.set_cell_data_func(render, cell_render_status_icon, HOSTLIST_COL_STATUS) + self.treeview.append_column(column) + + render = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST) + host_data = (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER) + column.set_cell_data_func(render, cell_render_host, host_data) + column.set_expand(True) + self.treeview.append_column(column) + + column = Gtk.TreeViewColumn( + _('Version'), Gtk.CellRendererText(), text=HOSTLIST_COL_VERSION + ) + self.treeview.append_column(column) + + # Load any saved host entries + self._load_liststore() + # Set widgets to values from gtkui config. + self._load_widget_config() + self._update_widget_buttons() + + # Connect the signals to the handlers + self.builder.connect_signals(self) + self.treeview.get_selection().connect( + 'changed', self.on_hostlist_selection_changed + ) + + # Set running True before update status call. + self.running = True + + if windows_check(): + # Call to simulate() required to workaround showing daemon status (see #2813) + reactor.simulate() + self._update_host_status() + + # Trigger the on_selection_changed code and select the first host if possible + self.treeview.get_selection().unselect_all() + if len(self.liststore): + self.treeview.get_selection().select_path(0) + + # Run the dialog + self.connection_manager.run() + + # Dialog closed so cleanup. + self.running = False + self.connection_manager.destroy() + del self.builder + del self.connection_manager + del self.liststore + del self.treeview + + def _load_liststore(self): + """Load saved host entries""" + for host_entry in self.hostlist.get_hosts_info(): + host_id, host, port, username = host_entry + self.liststore.append([host_id, host, port, username, '', '', '', '']) + + def _load_widget_config(self): + """Set the widgets to show the correct options from the config.""" + self.builder.get_object('chk_autoconnect').set_active( + self.gtkui_config['autoconnect'] + ) + self.builder.get_object('chk_autostart').set_active( + self.gtkui_config['autostart_localhost'] + ) + self.builder.get_object('chk_donotshow').set_active( + not self.gtkui_config['show_connection_manager_on_start'] + ) + + def _update_host_status(self): + """Updates the host status""" + if not self.running: + # Callback likely fired after the window closed. + return + + def on_host_status(status_info, row): + if self.running and row: + status = status_info[1].lower() + row[HOSTLIST_COL_STATUS] = status + row[HOSTLIST_COL_STATUS_I18N] = STATUS_I18N[status] + row[HOSTLIST_COL_VERSION] = status_info[2] + self._update_widget_buttons() + + deferreds = [] + for row in self.liststore: + host_id = row[HOSTLIST_COL_ID] + d = self.hostlist.get_host_status(host_id) + try: + d.addCallback(on_host_status, row) + except AttributeError: + on_host_status(d, row) + else: + deferreds.append(d) + defer.DeferredList(deferreds) + + def _update_widget_buttons(self): + """Updates the dialog button states.""" + self.builder.get_object('button_refresh').set_sensitive(len(self.liststore)) + self.builder.get_object('button_startdaemon').set_sensitive(False) + self.builder.get_object('button_connect').set_sensitive(False) + self.builder.get_object('button_connect').set_label(_('C_onnect')) + self.builder.get_object('button_edithost').set_sensitive(False) + self.builder.get_object('button_removehost').set_sensitive(False) + self.builder.get_object('button_startdaemon').set_sensitive(False) + self.builder.get_object('image_startdaemon').set_from_icon_name( + 'system-run-symbolic', Gtk.IconSize.BUTTON + ) + self.builder.get_object('label_startdaemon').set_text_with_mnemonic( + _('_Start Daemon') + ) + + model, row = self.treeview.get_selection().get_selected() + if row: + self.builder.get_object('button_edithost').set_sensitive(True) + self.builder.get_object('button_removehost').set_sensitive(True) + else: + return + + # Get selected host info. + __, host, port, __, __, status, __, __ = model[row] + + try: + getaddrinfo(host, None) + except gaierror as ex: + log.error( + 'Error resolving host %s to ip: %s', row[HOSTLIST_COL_HOST], ex.args[1] + ) + self.builder.get_object('button_connect').set_sensitive(False) + return + + log.debug('Host Status: %s, %s', host, status) + + # Check to see if the host is online + if status == 'connected' or status == 'online': + self.builder.get_object('button_connect').set_sensitive(True) + self.builder.get_object('image_startdaemon').set_from_icon_name( + 'process-stop-symbolic', Gtk.IconSize.MENU + ) + self.builder.get_object('label_startdaemon').set_text_with_mnemonic( + _('_Stop Daemon') + ) + self.builder.get_object('button_startdaemon').set_sensitive(False) + if status == 'connected': + # Display a disconnect button if we're connected to this host + self.builder.get_object('button_connect').set_label(_('_Disconnect')) + self.builder.get_object('button_removehost').set_sensitive(False) + # Currently can only stop daemon when connected to it + self.builder.get_object('button_startdaemon').set_sensitive(True) + elif host in LOCALHOST: + # If localhost we can start the dameon. + self.builder.get_object('button_startdaemon').set_sensitive(True) + + def start_daemon(self, port, config): + """Attempts to start local daemon process and will show an ErrorDialog if not. + + Args: + port (int): Port for the daemon to listen on. + config (str): Config path to pass to daemon. + + Returns: + bool: True is successfully started the daemon, False otherwise. + + """ + if client.start_daemon(port, config): + log.debug('Localhost daemon started') + reactor.callLater(1, self._update_host_status) + return True + else: + ErrorDialog( + _('Unable to start daemon!'), + _('Check deluged package is installed and logs for further details'), + ).run() + return False + + # Signal handlers + def _connect(self, host_id, username=None, password=None, try_counter=0): + def do_connect(result, username=None, password=None, *args): + log.debug('Attempting to connect to daemon...') + for host_entry in self.hostlist.config['hosts']: + if host_entry[0] == host_id: + __, host, port, host_user, host_pass = host_entry + + username = username if username else host_user + password = password if password else host_pass + + d = client.connect(host, port, username, password) + d.addCallback(self._on_connect, host_id) + d.addErrback(self._on_connect_fail, host_id, try_counter) + return d + + if client.connected(): + return client.disconnect().addCallback(do_connect, username, password) + else: + return do_connect(None, username, password) + + def _on_connect(self, daemon_info, host_id): + log.debug('Connected to daemon: %s', host_id) + if self.gtkui_config['autoconnect']: + self.gtkui_config['autoconnect_host_id'] = host_id + if self.running: + # When connected to a client, and then trying to connect to another, + # this component will be stopped(while the connect deferred is + # running), so, self.connection_manager will be deleted. + # If that's not the case, close the dialog. + self.connection_manager.response(Gtk.ResponseType.OK) + component.start() + + def _on_connect_fail(self, reason, host_id, try_counter): + log.debug('Failed to connect: %s', reason.value) + + if reason.check(AuthenticationRequired, BadLoginError): + log.debug('PasswordRequired exception') + dialog = AuthenticationDialog(reason.value.message, reason.value.username) + + def dialog_finished(response_id): + if response_id == Gtk.ResponseType.OK: + self._connect(host_id, dialog.get_username(), dialog.get_password()) + + return dialog.run().addCallback(dialog_finished) + + elif reason.check(IncompatibleClient): + return ErrorDialog(_('Incompatible Client'), reason.value.message).run() + + if try_counter: + log.info('Retrying connection.. Retries left: %s', try_counter) + return reactor.callLater( + 0.5, self._connect, host_id, try_counter=try_counter - 1 + ) + + msg = str(reason.value) + if not self.gtkui_config['autostart_localhost']: + msg += '\n' + _( + 'Auto-starting the daemon locally is not enabled. ' + 'See "Options" on the "Connection Manager".' + ) + ErrorDialog(_('Failed To Connect'), msg).run() + + def on_button_connect_clicked(self, widget=None): + """Button handler for connect to or disconnect from daemon.""" + model, row = self.treeview.get_selection().get_selected() + if not row: + return + + host_id, host, port, __, __, status, __, __ = model[row] + # If status is connected then connect button disconnects instead. + if status == 'connected': + + def on_disconnect(reason): + self._update_host_status() + + return client.disconnect().addCallback(on_disconnect) + + try_counter = 0 + auto_start = self.builder.get_object('chk_autostart').get_active() + if auto_start and host in LOCALHOST and status == 'offline': + # Start the local daemon and then connect with retries set. + if self.start_daemon(port, get_config_dir()): + try_counter = 6 + else: + # Don't attempt to connect to offline daemon. + return + + self._connect(host_id, try_counter=try_counter) + + def on_button_close_clicked(self, widget): + self.connection_manager.response(Gtk.ResponseType.CLOSE) + + def _run_addhost_dialog(self, edit_host_info=None): + """Create and runs the add host dialog. + + Supplying edit_host_info changes the dialog to an edit dialog. + + Args: + edit_host_info (list): A list of (host, port, user, pass) to edit. + + Returns: + list: The new host info values (host, port, user, pass). + + """ + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'connection_manager.addhost.ui') + ) + ) + dialog = self.builder.get_object('addhost_dialog') + dialog.set_transient_for(self.connection_manager) + hostname_entry = self.builder.get_object('entry_hostname') + port_spinbutton = self.builder.get_object('spinbutton_port') + username_entry = self.builder.get_object('entry_username') + password_entry = self.builder.get_object('entry_password') + + if edit_host_info: + dialog.set_title(_('Edit Host')) + hostname_entry.set_text(edit_host_info[0]) + port_spinbutton.set_value(edit_host_info[1]) + username_entry.set_text(edit_host_info[2]) + password_entry.set_text(edit_host_info[3]) + + response = dialog.run() + new_host_info = [] + if response: + new_host_info.append(hostname_entry.get_text()) + new_host_info.append(port_spinbutton.get_value_as_int()) + new_host_info.append(username_entry.get_text()) + new_host_info.append(password_entry.get_text()) + + dialog.destroy() + return new_host_info + + def on_button_addhost_clicked(self, widget): + log.debug('on_button_addhost_clicked') + host_info = self._run_addhost_dialog() + if host_info: + hostname, port, username, password = host_info + try: + host_id = self.hostlist.add_host(hostname, port, username, password) + except ValueError as ex: + ErrorDialog(_('Error Adding Host'), ex).run() + else: + status = 'offline' + version = '' + self.liststore.append( + [ + host_id, + hostname, + port, + username, + password, + status, + version, + STATUS_I18N[status], + ] + ) + self._update_host_status() + + def on_button_edithost_clicked(self, widget=None): + log.debug('on_button_edithost_clicked') + model, row = self.treeview.get_selection().get_selected() + status = model[row][HOSTLIST_COL_STATUS] + host_id = model[row][HOSTLIST_COL_ID] + host_info = [ + self.liststore[row][HOSTLIST_COL_HOST], + self.liststore[row][HOSTLIST_COL_PORT], + self.liststore[row][HOSTLIST_COL_USER], + self.liststore[row][HOSTLIST_COL_PASS], + ] + + new_host_info = self._run_addhost_dialog(edit_host_info=host_info) + if new_host_info: + hostname, port, username, password = new_host_info + try: + self.hostlist.update_host(host_id, hostname, port, username, password) + except ValueError as ex: + ErrorDialog(_('Error Updating Host'), ex).run() + else: + self.liststore[row] = ( + host_id, + hostname, + port, + username, + password, + '', + '', + '', + ) + self._update_host_status() + + if status == 'connected': + + def on_disconnect(reason): + self._update_host_status() + + client.disconnect().addCallback(on_disconnect) + + def on_button_removehost_clicked(self, widget): + log.debug('on_button_removehost_clicked') + # Get the selected rows + model, row = self.treeview.get_selection().get_selected() + self.hostlist.remove_host(model[row][HOSTLIST_COL_ID]) + self.liststore.remove(row) + # Update the hostlist + self._update_host_status() + + def on_button_startdaemon_clicked(self, widget): + log.debug('on_button_startdaemon_clicked') + if not self.liststore.iter_n_children(None): + # There is nothing in the list, so lets create a localhost entry + try: + self.hostlist.add_default_host() + except ValueError as ex: + log.error('Error adding default host: %s', ex) + else: + self.start_daemon(DEFAULT_PORT, get_config_dir()) + finally: + return + + paths = self.treeview.get_selection().get_selected_rows()[1] + if len(paths): + __, host, port, user, password, status, __, __ = self.liststore[paths[0]] + else: + return + + if host not in LOCALHOST: + return + + def on_daemon_status_change(result): + """Daemon start/stop callback""" + reactor.callLater(0.7, self._update_host_status) + + if status in ('online', 'connected'): + # Button will stop the daemon if status is online or connected. + def on_connect(d, c): + """Client callback to call daemon shutdown""" + c.daemon.shutdown().addCallback(on_daemon_status_change) + + if client.connected() and (host, port, user) == client.connection_info(): + client.daemon.shutdown().addCallback(on_daemon_status_change) + elif user and password: + c = Client() + c.connect(host, port, user, password).addCallback(on_connect, c) + else: + # Otherwise button will start the daemon. + self.start_daemon(port, get_config_dir()) + + def on_button_refresh_clicked(self, widget): + self._update_host_status() + + def on_hostlist_row_activated(self, tree, path, view_column): + self.on_button_connect_clicked() + + def on_hostlist_selection_changed(self, treeselection): + self._update_widget_buttons() + + def on_chk_toggled(self, widget): + self.gtkui_config['autoconnect'] = self.builder.get_object( + 'chk_autoconnect' + ).get_active() + self.gtkui_config['autostart_localhost'] = self.builder.get_object( + 'chk_autostart' + ).get_active() + self.gtkui_config[ + 'show_connection_manager_on_start' + ] = not self.builder.get_object('chk_donotshow').get_active() + + def on_entry_host_paste_clipboard(self, widget): + text = get_clipboard_text() + log.debug('on_entry_proxy_host_paste-clipboard: got paste: %s', text) + text = text if '//' in text else '//' + text + parsed = urlparse(text) + if parsed.hostname: + widget.set_text(parsed.hostname) + widget.emit_stop_by_name('paste-clipboard') + if parsed.port: + self.builder.get_object('spinbutton_port').set_value(parsed.port) + if parsed.username: + self.builder.get_object('entry_username').set_text(parsed.username) + if parsed.password: + self.builder.get_object('entry_password').set_text(parsed.password) |