summaryrefslogtreecommitdiffstats
path: root/deluge/ui/gtk3/connectionmanager.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/ui/gtk3/connectionmanager.py')
-rw-r--r--deluge/ui/gtk3/connectionmanager.py560
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)