From 31555ee5ed84bbaf7c2c7193c140823a0b334b1f Mon Sep 17 00:00:00 2001 From: Calum Lind Date: Sat, 1 Apr 2017 11:40:15 +0100 Subject: [PATCH] [UI] Further refactoring of the connection managers * Add host Edit button to WebUI. * Updated and fixed associated tests. * Refactored related gtkui code to better understand code flow. * Removed dead code in gtkui. --- deluge/common.py | 34 + deluge/tests/common_web.py | 2 +- deluge/tests/test_authmanager.py | 4 +- deluge/tests/test_client.py | 3 +- deluge/tests/test_rpcserver.py | 2 +- deluge/tests/test_ui_entry.py | 6 +- deluge/tests/test_web_api.py | 14 +- deluge/ui/client.py | 24 +- deluge/ui/console/modes/connectionmanager.py | 42 +- deluge/ui/gtkui/connectionmanager.py | 888 ++++++++---------- .../gtkui/glade/connection_manager.addhost.ui | 28 +- .../glade/connection_manager.askpassword.ui | 98 -- deluge/ui/gtkui/glade/connection_manager.ui | 32 +- deluge/ui/gtkui/gtkui.py | 202 ++-- deluge/ui/hostlist.py | 166 ++-- .../web/js/deluge-all/AddConnectionWindow.js | 2 +- .../ui/web/js/deluge-all/ConnectionManager.js | 48 +- .../web/js/deluge-all/EditConnectionWindow.js | 114 +++ deluge/ui/web/json_api.py | 78 +- 19 files changed, 822 insertions(+), 965 deletions(-) delete mode 100644 deluge/ui/gtkui/glade/connection_manager.askpassword.ui create mode 100644 deluge/ui/web/js/deluge-all/EditConnectionWindow.js diff --git a/deluge/common.py b/deluge/common.py index 2d71df04a..4efb1af2d 100644 --- a/deluge/common.py +++ b/deluge/common.py @@ -997,6 +997,40 @@ def create_localclient_account(append=False): os.fsync(_file.fileno()) +def get_localhost_auth(): + """Grabs the localclient auth line from the 'auth' file and creates a localhost uri. + + Returns: + tuple: With the username and password to login as. + + """ + from deluge.configmanager import get_config_dir + auth_file = get_config_dir('auth') + if not os.path.exists(auth_file): + from deluge.common import create_localclient_account + create_localclient_account() + + with open(auth_file) as auth: + for line in auth: + line = line.strip() + if line.startswith('#') or not line: + # This is a comment or blank line + continue + + lsplit = line.split(':') + + if len(lsplit) == 2: + username, password = lsplit + elif len(lsplit) == 3: + username, password, level = lsplit + else: + log.error('Your auth file is malformed: Incorrect number of fields!') + continue + + if username == 'localclient': + return (username, password) + + def set_env_variable(name, value): """ :param name: environment variable name diff --git a/deluge/tests/common_web.py b/deluge/tests/common_web.py index 1ca5fb68d..3f93f3b66 100644 --- a/deluge/tests/common_web.py +++ b/deluge/tests/common_web.py @@ -66,7 +66,7 @@ class WebServerTestBase(BaseTestCase, DaemonBase): self.deluge_web = DelugeWeb(daemon=False) - host = list(self.deluge_web.web_api.hostlist.get_hosts_info2()[0]) + host = list(self.deluge_web.web_api.hostlist.config['hosts'][0]) host[2] = self.listen_port self.deluge_web.web_api.hostlist.config['hosts'][0] = tuple(host) self.host_id = host[0] diff --git a/deluge/tests/test_authmanager.py b/deluge/tests/test_authmanager.py index 2d25d0f33..e62ce9364 100644 --- a/deluge/tests/test_authmanager.py +++ b/deluge/tests/test_authmanager.py @@ -8,8 +8,8 @@ from __future__ import unicode_literals import deluge.component as component +from deluge.common import get_localhost_auth from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AuthManager -from deluge.ui import hostlist from .basetest import BaseTestCase @@ -25,6 +25,6 @@ class AuthManagerTestCase(BaseTestCase): def test_authorize(self): self.assertEqual( - self.auth.authorize(*hostlist.get_localhost_auth()), + self.auth.authorize(*get_localhost_auth()), AUTH_LEVEL_ADMIN ) diff --git a/deluge/tests/test_client.py b/deluge/tests/test_client.py index 84d7fd9f6..a5a85ff7c 100644 --- a/deluge/tests/test_client.py +++ b/deluge/tests/test_client.py @@ -11,10 +11,9 @@ from twisted.internet import defer import deluge.component as component from deluge import error -from deluge.common import AUTH_LEVEL_NORMAL +from deluge.common import AUTH_LEVEL_NORMAL, get_localhost_auth from deluge.core.authmanager import AUTH_LEVEL_ADMIN from deluge.ui.client import Client, DaemonSSLProxy, client -from deluge.ui.hostlist import get_localhost_auth from .basetest import BaseTestCase from .daemon_base import DaemonBase diff --git a/deluge/tests/test_rpcserver.py b/deluge/tests/test_rpcserver.py index dc74f850e..0afcfd63b 100644 --- a/deluge/tests/test_rpcserver.py +++ b/deluge/tests/test_rpcserver.py @@ -11,11 +11,11 @@ from __future__ import unicode_literals import deluge.component as component import deluge.error +from deluge.common import get_localhost_auth from deluge.core import rpcserver from deluge.core.authmanager import AuthManager from deluge.core.rpcserver import DelugeRPCProtocol, RPCServer from deluge.log import setup_logger -from deluge.ui.hostlist import get_localhost_auth from .basetest import BaseTestCase diff --git a/deluge/tests/test_ui_entry.py b/deluge/tests/test_ui_entry.py index 272a523e9..e3d69ddea 100644 --- a/deluge/tests/test_ui_entry.py +++ b/deluge/tests/test_ui_entry.py @@ -23,9 +23,8 @@ import deluge.ui.console import deluge.ui.console.cmdline.commands.quit import deluge.ui.console.main import deluge.ui.web.server -from deluge.common import utf8_encode_structure +from deluge.common import get_localhost_auth, utf8_encode_structure from deluge.ui import ui_entry -from deluge.ui.hostlist import get_localhost_auth from deluge.ui.web.server import DelugeWeb from . import common @@ -334,7 +333,7 @@ class ConsoleUIWithDaemonBaseTestCase(UIWithDaemonBaseTestCase): def set_up(self): # Avoid calling reactor.shutdown after commands are executed by main.exec_args() - self.patch(deluge.ui.console.cmdline.commands.quit, 'reactor', common.ReactorOverride()) + deluge.ui.console.main.reactor = common.ReactorOverride() return UIWithDaemonBaseTestCase.set_up(self) @defer.inlineCallbacks @@ -344,7 +343,6 @@ class ConsoleUIWithDaemonBaseTestCase(UIWithDaemonBaseTestCase): [username] + ['--password'] + [password] + ['status']) fd = StringFileDescriptor(sys.stdout) self.patch(sys, 'stdout', fd) - self.patch(deluge.ui.console.main, 'reactor', common.ReactorOverride()) yield self.exec_command() diff --git a/deluge/tests/test_web_api.py b/deluge/tests/test_web_api.py index 6fb7d1008..7baabc056 100644 --- a/deluge/tests/test_web_api.py +++ b/deluge/tests/test_web_api.py @@ -91,17 +91,17 @@ class WebAPITestCase(WebServerTestBase): def test_get_host(self): self.assertFalse(self.deluge_web.web_api._get_host('invalid_id')) - conn = list(self.deluge_web.web_api.hostlist.get_hosts_info2()[0]) - self.assertEqual(self.deluge_web.web_api._get_host(conn[0]), conn) + conn = list(self.deluge_web.web_api.hostlist.get_hosts_info()[0]) + self.assertEqual(self.deluge_web.web_api._get_host(conn[0]), conn[0:4]) def test_add_host(self): conn = ['abcdef', '10.0.0.1', 0, 'user123', 'pass123'] self.assertFalse(self.deluge_web.web_api._get_host(conn[0])) # Add valid host - ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4]) - self.assertEqual(ret[0], True) - conn[0] = ret[1] - self.assertEqual(self.deluge_web.web_api._get_host(conn[0]), conn) + result, host_id = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4]) + self.assertEqual(result, True) + conn[0] = host_id + self.assertEqual(self.deluge_web.web_api._get_host(conn[0]), conn[0:4]) # Add already existing host ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4]) @@ -115,7 +115,7 @@ class WebAPITestCase(WebServerTestBase): def test_remove_host(self): conn = ['connection_id', '', 0, '', ''] self.deluge_web.web_api.hostlist.config['hosts'].append(conn) - self.assertEqual(self.deluge_web.web_api._get_host(conn[0]), conn) + self.assertEqual(self.deluge_web.web_api._get_host(conn[0]), conn[0:4]) # Remove valid host self.assertTrue(self.deluge_web.web_api.remove_host(conn[0])) self.assertFalse(self.deluge_web.web_api._get_host(conn[0])) diff --git a/deluge/ui/client.py b/deluge/ui/client.py index 564bf625d..172a6d68b 100644 --- a/deluge/ui/client.py +++ b/deluge/ui/client.py @@ -17,11 +17,10 @@ import sys from twisted.internet import defer, reactor, ssl from twisted.internet.protocol import ClientFactory -import deluge.common from deluge import error +from deluge.common import get_localhost_auth, get_version from deluge.decorators import deprecated from deluge.transfer import DelugeTransferProtocol -from deluge.ui.hostlist import get_localhost_auth RPC_RESPONSE = 1 RPC_ERROR = 2 @@ -384,8 +383,7 @@ class DaemonSSLProxy(DaemonProxy): def authenticate(self, username, password): log.debug('%s.authenticate: %s', self.__class__.__name__, username) login_deferred = defer.Deferred() - d = self.call('daemon.login', username, password, - client_version=deluge.common.get_version()) + d = self.call('daemon.login', username, password, client_version=get_version()) d.addCallbacks(self.__on_login, self.__on_login_fail, callbackArgs=[username, login_deferred], errbackArgs=[login_deferred]) return login_deferred @@ -619,17 +617,14 @@ class Client(object): self.stop_standalone() def start_daemon(self, port, config): - """ - Starts a daemon process. + """Starts a daemon process. - :param port: the port for the daemon to listen on - :type port: int - :param config: the path to the current config folder - :type config: str - :returns: True if started, False if not - :rtype: bool + Args: + port (int): Port for the daemon to listen on. + config (str): Config path to pass to daemon. - :raises OSError: received from subprocess.call() + Returns: + bool: True is successfully started the daemon, False otherwise. """ # subprocess.popen does not work with unicode args (with non-ascii characters) on windows @@ -644,13 +639,12 @@ class Client(object): 'the deluged package is installed, or added to your PATH.')) else: log.exception(ex) - raise ex except Exception as ex: log.error('Unable to start daemon!') log.exception(ex) - return False else: return True + return False def is_localhost(self): """ diff --git a/deluge/ui/console/modes/connectionmanager.py b/deluge/ui/console/modes/connectionmanager.py index 705279f8b..cb53e28be 100644 --- a/deluge/ui/console/modes/connectionmanager.py +++ b/deluge/ui/console/modes/connectionmanager.py @@ -13,7 +13,7 @@ import logging import deluge.component as component from deluge.decorators import overrides -from deluge.ui.client import Client, client +from deluge.ui.client import client from deluge.ui.console.modes.basemode import BaseMode from deluge.ui.console.widgets.popup import InputPopup, PopupsHandler, SelectablePopup from deluge.ui.hostlist import HostList @@ -48,7 +48,7 @@ class ConnectionManager(BaseMode, PopupsHandler): space_below=True) self.push_popup(popup, clear=True) - for host_entry in self.hostlist.get_host_info(): + for host_entry in self.hostlist.get_hosts_info(): host_id, hostname, port, user = host_entry args = {'data': host_id, 'foreground': 'red'} state = 'Offline' @@ -64,34 +64,13 @@ class ConnectionManager(BaseMode, PopupsHandler): self.refresh() def update_hosts_status(self): - """Updates the host status""" - def on_connect(result, c, host_id): - def on_info(info, c): - self.statuses[host_id] = info - self.update_select_host_popup() - c.disconnect() - def on_info_fail(reason, c): - if host_id in self.statuses: - del self.statuses[host_id] - c.disconnect() - - d = c.daemon.info() - d.addCallback(on_info, c) - d.addErrback(on_info_fail, c) - - def on_connect_failed(reason, host_id): - if host_id in self.statuses: - del self.statuses[host_id] + for host_entry in self.hostlist.get_hosts_info(): + def on_host_status(status_info): + self.statuses[status_info[0]] = status_info self.update_select_host_popup() - for host_entry in self.hostlist.get_hosts_info2(): - c = Client() - host_id, host, port, user, password = host_entry - log.debug('Connect: host=%s, port=%s, user=%s, pass=%s', host, port, user, password) - d = c.connect(host, port, user, password) - d.addCallback(on_connect, c, host_id) - d.addErrback(on_connect_failed, host_id) + self.hostlist.get_host_status(host_entry[0]).addCallback(on_host_status) def _on_connected(self, result): d = component.get('ConsoleUI').start_console() @@ -108,12 +87,9 @@ class ConnectionManager(BaseMode, PopupsHandler): def _host_selected(self, selected_host, *args, **kwargs): if selected_host in self.statuses: - for host_entry in self.hostlist.get_hosts_info(): - if host_entry[0] == selected_host: - __, host, port, user, password = host_entry - d = client.connect(host, port, user, password) - d.addCallback(self._on_connected) - d.addErrback(self._on_connect_fail) + d = self.hostlist.connect_host(selected_host) + d.addCallback(self._on_connected) + d.addErrback(self._on_connect_fail) def _do_add(self, result, **kwargs): if not result or kwargs.get('close', False): diff --git a/deluge/ui/gtkui/connectionmanager.py b/deluge/ui/gtkui/connectionmanager.py index 21dcc0f9c..cfffaa29c 100644 --- a/deluge/ui/gtkui/connectionmanager.py +++ b/deluge/ui/gtkui/connectionmanager.py @@ -14,14 +14,14 @@ import os from socket import gaierror, gethostbyname import gtk -from twisted.internet import reactor +from twisted.internet import defer, reactor import deluge.component as component -from deluge.common import resource_filename +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.gtkui.common import get_clipboard_text, get_deluge_icon +from deluge.ui.gtkui.common import get_clipboard_text from deluge.ui.gtkui.dialogs import AuthenticationDialog, ErrorDialog from deluge.ui.hostlist import DEFAULT_PORT, HostList @@ -68,7 +68,6 @@ def cell_render_status(column, cell, model, row, data): pixbuf = None if status in HOSTLIST_STATUS: pixbuf = HOSTLIST_PIXBUFS[HOSTLIST_STATUS.index(status)] - cell.set_property('pixbuf', pixbuf) @@ -76,7 +75,7 @@ class ConnectionManager(component.Component): def __init__(self): component.Component.__init__(self, 'ConnectionManager') self.gtkui_config = ConfigManager('gtkui.conf') - + self.hostlist = HostList() self.running = False # Component overrides @@ -93,92 +92,408 @@ class ConnectionManager(component.Component): # Public methods def show(self): - """ - Show the ConnectionManager dialog. - """ - # Get the gtk builder file for the connection manager + """Show the ConnectionManager dialog.""" self.builder = gtk.Builder() - # The main dialog self.builder.add_from_file(resource_filename( 'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.ui'))) - # The add host dialog - self.builder.add_from_file(resource_filename( - 'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.addhost.ui'))) - # The ask password dialog - self.builder.add_from_file(resource_filename( - 'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.askpassword.ui'))) - - # Setup the ConnectionManager dialog self.connection_manager = self.builder.get_object('connection_manager') self.connection_manager.set_transient_for(component.get('MainWindow').window) - self.askpassword_dialog = self.builder.get_object('askpassword_dialog') - self.askpassword_dialog.set_transient_for(self.connection_manager) - self.askpassword_dialog.set_icon(get_deluge_icon()) - self.askpassword_dialog_entry = self.builder.get_object('askpassword_dialog_entry') - - self.hostlist_config = HostList() - self.hostlist = self.builder.get_object('hostlist') - # Create status pixbufs if not HOSTLIST_PIXBUFS: for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): HOSTLIST_PIXBUFS.append( - self.connection_manager.render_icon( - stock_id, gtk.ICON_SIZE_MENU - ) - ) + self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU)) - # Create the host list gtkliststore - # id-hash, hostname, port, username, password, status, version - self.liststore = gtk.ListStore(str, str, int, str, str, str, str) + # Setup the hostlist liststore and treeview + self.treeview = self.builder.get_object('treeview_hostlist') + self.liststore = self.builder.get_object('liststore_hostlist') - # Setup host list treeview - self.hostlist.set_model(self.liststore) render = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn(_('Status'), render) column.set_cell_data_func(render, cell_render_status, HOSTLIST_COL_STATUS) - self.hostlist.append_column(column) + self.treeview.append_column(column) + render = gtk.CellRendererText() column = gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST) - column.set_cell_data_func( - render, cell_render_host, (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER)) + 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.hostlist.append_column(column) - render = gtk.CellRendererText() - column = gtk.TreeViewColumn(_('Version'), render, text=HOSTLIST_COL_VERSION) - self.hostlist.append_column(column) + 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.hostlist.get_selection().connect( - 'changed', self.on_hostlist_selection_changed - ) - - # Load any saved host entries - self.__load_hostlist() - self.__load_options() - self.__update_list() + self.treeview.get_selection().connect('changed', self.on_hostlist_selection_changed) + # Set running True before update status call. self.running = True - # Trigger the on_selection_changed code and select the first host - # if possible - self.hostlist.get_selection().unselect_all() - if len(self.liststore) > 0: - self.hostlist.get_selection().select_path(0) + + 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 - - # Save the toggle options - self.__save_options() - self.connection_manager.destroy() del self.builder del self.connection_manager del self.liststore - del self.hostlist + 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: + row[HOSTLIST_COL_STATUS] = status_info[1] + 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_stock( + gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) + 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: + gethostbyname(host) + 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_stock( + gtk.STOCK_STOP, gtk.ICON_SIZE_MENU) + self.builder.get_object('label_startdaemon').set_text_with_mnemonic(_('_Stop Daemon')) + self.builder.get_object('button_startdaemon').set_sensitive(True) + 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) + 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(0.5, 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.RESPONSE_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.RESPONSE_OK: + self.__connect(host_id, dialog.get_username(), dialog.get_password()) + return dialog.run().addCallback(dialog_finished) + + elif reason.trap(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.8, 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 = 4 + 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.RESPONSE_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( + 'deluge.ui.gtkui', 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: + self.liststore.append([host_id, hostname, port, username, password, 'Offline', '']) + 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] + + if status == 'Connected': + def on_disconnect(reason): + self._update_host_status() + client.disconnect().addCallback(on_disconnect) + return + + 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() + + 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(d): + """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()).addCallback(on_daemon_status_change) + + 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() @@ -194,456 +509,3 @@ class ConnectionManager(component.Component): self.builder.get_object('entry_username').set_text(parsed.username) if parsed.password: self.builder.get_object('entry_password').set_text(parsed.password) - - def __load_hostlist(self): - """Load saved host entries""" - status = version = '' - for host_entry in self.hostlist_config.get_hosts_info2(): - host_id, host, port, username, password = host_entry - self.liststore.append([host_id, host, port, username, password, status, version]) - - def __get_host_row(self, host_id): - """Get the row in the liststore for the host_id. - - Args: - host_id (str): The host id. - - Returns: - list: The listsrore row with host details. - - """ - for row in self.liststore: - if host_id == row[HOSTLIST_COL_ID]: - return row - return None - - def __update_list(self): - """Updates the host status""" - if not hasattr(self, 'liststore'): - # This callback was probably fired after the window closed - return - - def on_connect(result, c, host_id): - # Return if the deferred callback was done after the dialog was closed - if not self.running: - return - row = self.__get_host_row(host_id) - - def on_info(info, c): - if not self.running: - return - if row: - row[HOSTLIST_COL_STATUS] = 'Online' - row[HOSTLIST_COL_VERSION] = info - self.__update_buttons() - c.disconnect() - - def on_info_fail(reason, c): - if not self.running: - return - if row: - row[HOSTLIST_COL_STATUS] = 'Offline' - self.__update_buttons() - c.disconnect() - - d = c.daemon.info() - d.addCallback(on_info, c) - d.addErrback(on_info_fail, c) - - def on_connect_failed(reason, host_id): - if not self.running: - return - row = self.__get_host_row(host_id) - if row: - row[HOSTLIST_COL_STATUS] = 'Offline' - row[HOSTLIST_COL_VERSION] = '' - self.__update_buttons() - - for row in self.liststore: - host_id = row[HOSTLIST_COL_ID] - host = row[HOSTLIST_COL_HOST] - port = row[HOSTLIST_COL_PORT] - user = row[HOSTLIST_COL_USER] - - try: - ip = gethostbyname(host) - except gaierror as ex: - log.error('Error resolving host %s to ip: %s', host, ex.args[1]) - continue - - host_info = (ip, port, 'localclient' if not user and host in LOCALHOST else user) - if client.connected() and host_info == client.connection_info(): - def on_info(info, row): - if not self.running: - return - log.debug('Client connected, query info: %s', info) - row[HOSTLIST_COL_VERSION] = info - self.__update_buttons() - - row[HOSTLIST_COL_STATUS] = 'Connected' - log.debug('Query daemon info') - client.daemon.info().addCallback(on_info, row) - continue - - # Create a new Client instance - c = Client() - d = c.connect(host, port, skip_authentication=True) - d.addCallback(on_connect, c, host_id) - d.addErrback(on_connect_failed, host_id) - - def __load_options(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 __save_options(self): - """ - Set options in gtkui config from the toggle buttons. - """ - 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 __update_buttons(self): - """Updates the buttons states.""" - if len(self.liststore) == 0: - # There is nothing in the list - self.builder.get_object('button_startdaemon').set_sensitive(False) - self.builder.get_object('button_connect').set_sensitive(False) - self.builder.get_object('button_removehost').set_sensitive(False) - self.builder.get_object('image_startdaemon').set_from_stock( - gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) - self.builder.get_object('label_startdaemon').set_text_with_mnemonic('_Start Daemon') - - model, row = self.hostlist.get_selection().get_selected() - if not row: - self.builder.get_object('button_edithost').set_sensitive(False) - return - - self.builder.get_object('button_edithost').set_sensitive(True) - self.builder.get_object('button_startdaemon').set_sensitive(True) - self.builder.get_object('button_connect').set_sensitive(True) - self.builder.get_object('button_removehost').set_sensitive(True) - - # Get some values about the selected host - __, host, port, user, password, status, __ = model[row] - - try: - ip = gethostbyname(host) - except gaierror as ex: - log.error('Error resolving host %s to ip: %s', row[HOSTLIST_COL_HOST], ex.args[1]) - return - - log.debug('Status: %s', status) - # Check to see if we have a localhost entry selected - localhost = host in LOCALHOST - - # See if this is the currently connected host - if status == 'Connected': - # Display a disconnect button if we're connected to this host - self.builder.get_object('button_connect').set_label('gtk-disconnect') - self.builder.get_object('button_removehost').set_sensitive(False) - else: - self.builder.get_object('button_connect').set_label('gtk-connect') - if status == 'Offline' and not localhost: - self.builder.get_object('button_connect').set_sensitive(False) - - # Check to see if the host is online - if status == 'Connected' or status == 'Online': - self.builder.get_object('image_startdaemon').set_from_stock( - gtk.STOCK_STOP, gtk.ICON_SIZE_MENU) - self.builder.get_object('label_startdaemon').set_text_with_mnemonic(_('_Stop Daemon')) - - # Update the start daemon button if the selected host is localhost - if localhost and status == 'Offline': - # The localhost is not online - self.builder.get_object('image_startdaemon').set_from_stock( - gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) - self.builder.get_object('label_startdaemon').set_text_with_mnemonic(_('_Start Daemon')) - - if client.connected() and (ip, port, user) == client.connection_info(): - # If we're connected, we can stop the dameon - self.builder.get_object('button_startdaemon').set_sensitive(True) - elif user and password: - # In this case we also have all the info to shutdown the daemon - self.builder.get_object('button_startdaemon').set_sensitive(True) - else: - # Can't stop non localhost daemons, specially without the necessary info - self.builder.get_object('button_startdaemon').set_sensitive(False) - - def start_daemon(self, port, config): - """ - Attempts to start a daemon process and will show an ErrorDialog if unable - to. - """ - try: - return client.start_daemon(port, config) - except OSError as ex: - from errno import ENOENT - if ex.errno == ENOENT: - ErrorDialog( - _('Unable to start daemon!'), - _('Deluge cannot find the `deluged` executable, check that ' - 'the deluged package is installed, or added to your PATH.')).run() - - return False - else: - raise ex - except Exception: - import traceback - import sys - tb = sys.exc_info() - ErrorDialog( - _('Unable to start daemon!'), - _('Please examine the details for more information.'), - details=traceback.format_exc(tb[2])).run() - - # Signal handlers - def __connect(self, host_id, host, port, username, password, - skip_authentication=False, try_counter=0): - def do_connect(*args): - d = client.connect(host, port, username, password, skip_authentication) - d.addCallback(self.__on_connected, host_id) - d.addErrback(self.__on_connected_failed, host_id, host, port, - username, password, try_counter) - return d - - if client.connected(): - return client.disconnect().addCallback(do_connect) - else: - return do_connect() - - def __on_connected(self, daemon_info, 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.RESPONSE_OK) - component.start() - - def __on_connected_failed(self, reason, host_id, host, port, user, password, - 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, host, port, user): - if response_id == gtk.RESPONSE_OK: - self.__connect(host_id, host, port, - user and user or dialog.get_username(), - dialog.get_password()) - d = dialog.run().addCallback(dialog_finished, host, port, user) - return d - - elif reason.trap(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, host, port, user, password, - try_counter=try_counter - 1) - - msg = str(reason.value) - if not self.builder.get_object('chk_autostart').get_active(): - 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): - model, row = self.hostlist.get_selection().get_selected() - if not row: - return - - status = model[row][HOSTLIST_COL_STATUS] - - # If status is connected then connect button disconnects instead. - if status == 'Connected': - def on_disconnect(reason): - self.__update_list() - client.disconnect().addCallback(on_disconnect) - return - - host_id, host, port, user, password, __, __ = model[row] - try_counter = 0 - auto_start = self.builder.get_object('chk_autostart').get_active() - if status == 'Offline' and auto_start and host in LOCALHOST: - if not self.start_daemon(port, get_config_dir()): - log.debug('Failed to auto-start daemon') - return - try_counter = 6 - - return self.__connect(host_id, host, port, user, password, try_counter=try_counter) - - def on_button_close_clicked(self, widget): - self.connection_manager.response(gtk.RESPONSE_CLOSE) - - def on_button_addhost_clicked(self, widget): - log.debug('on_button_addhost_clicked') - dialog = self.builder.get_object('addhost_dialog') - dialog.set_transient_for(self.connection_manager) - dialog.set_position(gtk.WIN_POS_CENTER_ON_PARENT) - 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') - button_addhost_save = self.builder.get_object('button_addhost_save') - button_addhost_save.hide() - button_addhost_add = self.builder.get_object('button_addhost_add') - button_addhost_add.show() - response = dialog.run() - if response == 1: - username = username_entry.get_text() - password = password_entry.get_text() - hostname = hostname_entry.get_text() - port = port_spinbutton.get_value_as_int() - - try: - host_id = self.hostlist_config.add_host(hostname, port, username, password) - except ValueError as ex: - ErrorDialog(_('Error Adding Host'), ex, parent=dialog).run() - else: - self.liststore.append([host_id, hostname, port, username, password, 'Offline', '']) - - # Update the status of the hosts - self.__update_list() - - username_entry.set_text('') - password_entry.set_text('') - hostname_entry.set_text('') - port_spinbutton.set_value(DEFAULT_PORT) - dialog.hide() - - def on_button_edithost_clicked(self, widget=None): - log.debug('on_button_edithost_clicked') - model, row = self.hostlist.get_selection().get_selected() - status = model[row][HOSTLIST_COL_STATUS] - host_id = model[row][HOSTLIST_COL_ID] - - if status == 'Connected': - def on_disconnect(reason): - self.__update_list() - client.disconnect().addCallback(on_disconnect) - return - - dialog = self.builder.get_object('addhost_dialog') - dialog.set_transient_for(self.connection_manager) - dialog.set_position(gtk.WIN_POS_CENTER_ON_PARENT) - 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') - button_addhost_save = self.builder.get_object('button_addhost_save') - button_addhost_save.show() - button_addhost_add = self.builder.get_object('button_addhost_add') - button_addhost_add.hide() - - username_entry.set_text(self.liststore[row][HOSTLIST_COL_USER]) - password_entry.set_text(self.liststore[row][HOSTLIST_COL_PASS]) - hostname_entry.set_text(self.liststore[row][HOSTLIST_COL_HOST]) - port_spinbutton.set_value(self.liststore[row][HOSTLIST_COL_PORT]) - - response = dialog.run() - - if response == 2: - username = username_entry.get_text() - password = password_entry.get_text() - hostname = hostname_entry.get_text() - port = port_spinbutton.get_value_as_int() - - try: - self.hostlist_config.update_host(host_id, hostname, port, username, password) - except ValueError as ex: - ErrorDialog(_('Error Updating Host'), ex, parent=dialog).run() - else: - self.liststore[row] = host_id, hostname, port, username, password, '', '' - - # Update the status of the hosts - self.__update_list() - - username_entry.set_text('') - password_entry.set_text('') - hostname_entry.set_text('') - port_spinbutton.set_value(DEFAULT_PORT) - dialog.hide() - - def on_button_removehost_clicked(self, widget): - log.debug('on_button_removehost_clicked') - # Get the selected rows - model, row = self.hostlist.get_selection().get_selected() - self.hostlist_config.remove_host(model[row][HOSTLIST_COL_ID]) - self.liststore.remove(row) - # Update the hostlist - self.__update_list() - - def on_button_startdaemon_clicked(self, widget): - log.debug('on_button_startdaemon_clicked') - if self.liststore.iter_n_children(None) < 1: - # There is nothing in the list, so lets create a localhost entry - try: - self.hostlist_config.add_default_host() - except ValueError as ex: - log.error('Error adding default host: %s', ex) - - # ..and start the daemon. - self.start_daemon(DEFAULT_PORT, get_config_dir()) - return - - paths = self.hostlist.get_selection().get_selected_rows()[1] - if len(paths) < 1: - return - - __, host, port, user, password, status, __ = self.liststore[paths[0]] - - if host not in LOCALHOST: - return - - if status in ('Online', 'Connected'): - # We need to stop this daemon - # Call the shutdown method on the daemon - def on_daemon_shutdown(d): - # Update display to show change - reactor.callLater(0.8, self.__update_list) - if client.connected() and client.connection_info() == (host, port, user): - client.daemon.shutdown().addCallback(on_daemon_shutdown) - elif user and password: - # Create a new client instance - c = Client() - - def on_connect(d, c): - log.debug('on_connect') - c.daemon.shutdown().addCallback(on_daemon_shutdown) - - c.connect(host, port, user, password).addCallback(on_connect, c) - - elif status == 'Offline': - self.start_daemon(port, get_config_dir()) - reactor.callLater(0.8, self.__update_list) - - def on_button_refresh_clicked(self, widget): - self.__update_list() - - def on_hostlist_row_activated(self, tree, path, view_column): - self.on_button_connect_clicked() - - def on_hostlist_selection_changed(self, treeselection): - self.__update_buttons() - - def on_askpassword_dialog_connect_button_clicked(self, widget): - log.debug('on on_askpassword_dialog_connect_button_clicked') - self.askpassword_dialog.response(gtk.RESPONSE_OK) - - def on_askpassword_dialog_entry_activate(self, entry): - self.askpassword_dialog.response(gtk.RESPONSE_OK) diff --git a/deluge/ui/gtkui/glade/connection_manager.addhost.ui b/deluge/ui/gtkui/glade/connection_manager.addhost.ui index 21972260b..595465f3e 100644 --- a/deluge/ui/gtkui/glade/connection_manager.addhost.ui +++ b/deluge/ui/gtkui/glade/connection_manager.addhost.ui @@ -13,7 +13,7 @@ 5 Add Host True - center + center-on-parent True dialog @@ -43,14 +43,14 @@ - gtk-add + _Save False True True True True True - True + True False @@ -58,20 +58,6 @@ 1 - - - gtk-save - False - True - True - True - - - False - False - 2 - - False @@ -114,7 +100,7 @@ False True True - + @@ -253,15 +239,11 @@ 2 - - - button_addhost_cancel - button_addhost_add - button_addhost_save + button_addhost_add diff --git a/deluge/ui/gtkui/glade/connection_manager.askpassword.ui b/deluge/ui/gtkui/glade/connection_manager.askpassword.ui deleted file mode 100644 index 6025ac018..000000000 --- a/deluge/ui/gtkui/glade/connection_manager.askpassword.ui +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - False - 5 - Password Required - True - center-on-parent - 320 - True - dialog - True - - - True - False - 2 - - - True - False - end - - - gtk-connect - False - True - True - True - True - - - - False - False - 0 - - - - - False - True - end - 0 - - - - - True - False - - - True - False - gtk-dialog-authentication - 6 - - - True - True - 0 - - - - - True - True - False - - True - True - False - False - True - True - - - True - True - 1 - - - - - True - True - 1 - - - - - - askpassword_dialog_connect_button - - - diff --git a/deluge/ui/gtkui/glade/connection_manager.ui b/deluge/ui/gtkui/glade/connection_manager.ui index d69b78015..af12310c4 100644 --- a/deluge/ui/gtkui/glade/connection_manager.ui +++ b/deluge/ui/gtkui/glade/connection_manager.ui @@ -46,10 +46,11 @@ automatic automatic - + 80 True True + liststore_hostlist @@ -242,12 +243,12 @@ end - gtk-close + _Close False True True True - True + True @@ -258,12 +259,12 @@ - gtk-connect + C_onnect False True True True - True + True @@ -303,6 +304,7 @@ True False True + True @@ -318,6 +320,7 @@ True False True + True @@ -333,6 +336,7 @@ True False True + True @@ -368,4 +372,22 @@ button_connect + + + + + + + + + + + + + + + + + + diff --git a/deluge/ui/gtkui/gtkui.py b/deluge/ui/gtkui/gtkui.py index 708356cca..c40dfa41c 100644 --- a/deluge/ui/gtkui/gtkui.py +++ b/deluge/ui/gtkui/gtkui.py @@ -21,7 +21,7 @@ pygtk.require('2.0') # NOQA: E402 # isort:imports-thirdparty from gobject import set_prgname -from gtk import RESPONSE_OK, RESPONSE_YES +from gtk import RESPONSE_YES from gtk.gdk import WINDOWING, threads_enter, threads_init, threads_leave from twisted.internet import defer, gtk2reactor from twisted.internet.error import ReactorAlreadyInstalledError @@ -38,12 +38,12 @@ except ReactorAlreadyInstalledError as ex: import deluge.component as component from deluge.common import fsize, fspeed, get_default_download_dir, osx_check, windows_check from deluge.configmanager import ConfigManager, get_config_dir -from deluge.error import AuthenticationRequired, BadLoginError, DaemonRunningError +from deluge.error import DaemonRunningError from deluge.ui.client import client from deluge.ui.gtkui.addtorrentdialog import AddTorrentDialog from deluge.ui.gtkui.common import associate_magnet_links from deluge.ui.gtkui.connectionmanager import ConnectionManager -from deluge.ui.gtkui.dialogs import AuthenticationDialog, ErrorDialog, YesNoDialog +from deluge.ui.gtkui.dialogs import YesNoDialog from deluge.ui.gtkui.filtertreeview import FilterTreeView from deluge.ui.gtkui.ipcinterface import IPCInterface, process_args from deluge.ui.gtkui.mainwindow import MainWindow @@ -61,7 +61,7 @@ from deluge.ui.sessionproxy import SessionProxy from deluge.ui.tracker_icons import TrackerIcons from deluge.ui.translations_util import set_language, setup_translations -set_prgname(b'deluge') +set_prgname('deluge'.encode('utf8')) log = logging.getLogger(__name__) try: @@ -220,7 +220,7 @@ class GtkUI(object): # Setup RPC stats logging # daemon_bps: time, bytes_sent, bytes_recv self.daemon_bps = (0, 0, 0) - self.rpc_stats = LoopingCall(self.print_rpc_stats) + self.rpc_stats = LoopingCall(self.log_rpc_stats) self.closing = False # Twisted catches signals to terminate, so have it call a pre_shutdown method. @@ -238,8 +238,8 @@ class GtkUI(object): # Initialize gdk threading threads_enter() reactor.run() - # Reactor is not running. Any async callbacks (Deferreds) can no longer - # be processed from this point on. + # Reactor no longer running so async callbacks (Deferreds) cannot be + # processed after this point. threads_leave() def shutdown(self, *args, **kwargs): @@ -267,11 +267,12 @@ class GtkUI(object): reactor.stop() - # Restart the application after closing if MainWindow attribute set. + # Restart the application after closing if MainWindow restart attribute set. if component.get('MainWindow').restart: os.execv(sys.argv[0], sys.argv) - def print_rpc_stats(self): + def log_rpc_stats(self): + """Log RPC statistics for thinclient mode.""" if not client.connected(): return @@ -290,144 +291,73 @@ class GtkUI(object): log.debug('_on_reactor_start') self.mainwindow.first_show() - if self.config['standalone']: - def on_dialog_response(response): - if response != RESPONSE_YES: - # The user does not want to turn Standalone Mode off, so just quit - self.mainwindow.quit() - return + if not self.config['standalone']: + return self._start_thinclient() + + err_msg = '' + try: + client.start_standalone() + except DaemonRunningError: + err_msg = _('A Deluge daemon (deluged) is already running.\n' + 'To use Standalone mode, stop local daemon and restart Deluge.') + except ImportError as ex: + if 'No module named libtorrent' in ex.message: + err_msg = _('Only Thin Client mode is available because libtorrent is not installed.\n' + 'To use Standalone mode, please install libtorrent package.') + else: + log.exception(ex) + err_msg = _('Only Thin Client mode is available due to unknown Import Error.\n' + 'To use Standalone mode, please see logs for error details.') + except Exception as ex: + log.exception(ex) + err_msg = _('Only Thin Client mode is available due to unknown Import Error.\n' + 'To use Standalone mode, please see logs for error details.') + else: + component.start() + return + + def on_dialog_response(response): + """User response to switching mode dialog.""" + if response == RESPONSE_YES: # Turning off standalone self.config['standalone'] = False - self.__start_thinclient() + self._start_thinclient() + else: + # User want keep Standalone Mode so just quit. + self.mainwindow.quit() - try: - try: - client.start_standalone() - except DaemonRunningError: - d = YesNoDialog( - _('Switch to Thin Client Mode?'), - _('A Deluge daemon process (deluged) is already running. ' - 'To use Standalone mode, stop this daemon and restart Deluge.' - '\n\n' - 'Continue in Thin Client mode?')).run() - d.addCallback(on_dialog_response) - except ImportError as ex: - if 'No module named libtorrent' in ex.message: - d = YesNoDialog( - _('Switch to Thin Client Mode?'), - _('Only Thin Client mode is available because libtorrent is not installed.' - '\n\n' - 'To use Deluge Standalone mode, please install libtorrent.')).run() - d.addCallback(on_dialog_response) - else: - raise ex - else: - component.start() - return - except Exception: - import traceback - tb = sys.exc_info() - ed = ErrorDialog( - _('Error Starting Core'), - _('An error occurred starting the core component required to run Deluge in Standalone mode.' - '\n\n' - 'Please see the details below for more information.'), details=traceback.format_exc(tb[2])).run() + # An error occurred so ask user to switch from Standalone to Thin Client mode. + err_msg += '\n\n' + _('Continue in Thin Client mode?') + d = YesNoDialog(_('Change User Interface Mode'), err_msg).run() + d.addCallback(on_dialog_response) - def on_ed_response(response): - d = YesNoDialog( - _('Switch to Thin Client Mode?'), - _('Unable to start Standalone mode would you like to continue in Thin Client mode?') - ).run() - d.addCallback(on_dialog_response) - ed.addCallback(on_ed_response) - else: + def _start_thinclient(self): + """Start the gtkui in thinclient mode""" + if log.isEnabledFor(logging.DEBUG): self.rpc_stats.start(10) - self.__start_thinclient() - def __start_thinclient(self): + # Check to see if we need to start the localhost daemon + if self.config['autostart_localhost']: + port = 0 + for host_config in self.connectionmanager.hostlist.config['hosts']: + if host_config[1] in self.connectionmanager.LOCALHOST: + port = host_config[2] + log.debug('Autostarting localhost: %s', host_config[0:3]) + + if port: + self.connectionmanager.start_daemon(port, get_config_dir()) + # Autoconnect to a host if self.config['autoconnect']: - - def update_connection_manager(): - if not self.connectionmanager.running: - return - self.connectionmanager.builder.get_object('button_refresh').emit('clicked') - - def close_connection_manager(): - if not self.connectionmanager.running: - return - self.connectionmanager.builder.get_object('button_close').emit('clicked') - - for host_config in self.connectionmanager.config['hosts']: - hostid, host, port, user, passwd = host_config - if hostid == self.config['autoconnect_host_id']: - try_connect = True - # Check to see if we need to start the localhost daemon - if self.config['autostart_localhost'] and host in ('localhost', '127.0.0.1'): - log.debug('Autostarting localhost:%s', host) - try_connect = client.start_daemon( - port, get_config_dir() - ) - log.debug('Localhost started: %s', try_connect) - if not try_connect: - ErrorDialog( - _('Error Starting Daemon'), - _('There was an error starting the daemon ' - 'process. Try running it from a console ' - 'to see if there is an error.') - ).run() - - # Daemon Started, let's update it's info - reactor.callLater(0.5, update_connection_manager) - - def on_connect(connector): - component.start() - reactor.callLater(0.2, update_connection_manager) - reactor.callLater(0.5, close_connection_manager) - - def on_connect_fail(reason, try_counter, - host, port, user, passwd): - if not try_counter: - return - - if reason.check(AuthenticationRequired, BadLoginError): - log.debug('PasswordRequired exception') - dialog = AuthenticationDialog(reason.value.message, reason.value.username) - - def dialog_finished(response_id, host, port): - if response_id == RESPONSE_OK: - reactor.callLater( - 0.5, do_connect, try_counter - 1, - host, port, dialog.get_username(), - dialog.get_password()) - dialog.run().addCallback(dialog_finished, host, port) - return - - log.info('Connection to host failed..') - log.info('Retrying connection.. Retries left: ' - '%s', try_counter) - reactor.callLater(0.5, update_connection_manager) - reactor.callLater(0.5, do_connect, try_counter - 1, - host, port, user, passwd) - - def do_connect(try_counter, host, port, user, passwd): - log.debug('Trying to connect to %s@%s:%s', - user, host, port) - d = client.connect(host, port, user, passwd) - d.addCallback(on_connect) - d.addErrback(on_connect_fail, try_counter, - host, port, user, passwd) - - if try_connect: - reactor.callLater( - 0.5, do_connect, 6, host, port, user, passwd - ) + for host_config in self.connectionmanager.hostlist.config['hosts']: + host_id, host, port, user, __ = host_config + if host_id == self.config['autoconnect_host_id']: + log.debug('Trying to connect to %s@%s:%s', user, host, port) + reactor.callLater(0.3, self.connectionmanager._connect, host_id, try_counter=6) break if self.config['show_connection_manager_on_start']: - if windows_check(): - # Call to simulate() required to workaround showing daemon status (see #2813) - reactor.simulate() + # Dialog is blocking so call last. self.connectionmanager.show() def __on_disconnect(self): diff --git a/deluge/ui/hostlist.py b/deluge/ui/hostlist.py index 5eb43889e..90cd17a9c 100644 --- a/deluge/ui/hostlist.py +++ b/deluge/ui/hostlist.py @@ -7,19 +7,19 @@ # See LICENSE for more details. # -""" -The UI hostlist module contains methods useful for adding, removing and lookingup host in hostlist.conf. -""" from __future__ import unicode_literals import logging -import os import time from hashlib import sha1 from socket import gaierror, gethostbyname +from twisted.internet import defer + +from deluge.common import get_localhost_auth from deluge.config import Config from deluge.configmanager import get_config_dir +from deluge.ui.client import Client, client log = logging.getLogger(__name__) @@ -29,45 +29,12 @@ LOCALHOST = ('127.0.0.1', 'localhost') def default_hostlist(): - """Create a new hosts for hostlist with a localhost entry""" + """Create a new hosts key for hostlist with a localhost entry""" host_id = sha1(str(time.time()).encode('utf8')).hexdigest() username, password = get_localhost_auth() return {'hosts': [(host_id, DEFAULT_HOST, DEFAULT_PORT, username, password)]} -def get_localhost_auth(): - """Grabs the localclient auth line from the 'auth' file and creates a localhost uri. - - Returns: - tuple: With the username and password to login as. - - """ - auth_file = get_config_dir('auth') - if not os.path.exists(auth_file): - from deluge.common import create_localclient_account - create_localclient_account() - - with open(auth_file) as auth: - for line in auth: - line = line.strip() - if line.startswith('#') or not line: - # This is a comment or blank line - continue - - lsplit = line.split(':') - - if len(lsplit) == 2: - username, password = lsplit - elif len(lsplit) == 3: - username, password, level = lsplit - else: - log.error('Your auth file is malformed: Incorrect number of fields!') - continue - - if username == 'localclient': - return (username, password) - - def validate_host_info(hostname, port): """Checks that hostname and port are valid. @@ -84,25 +51,25 @@ def validate_host_info(hostname, port): except gaierror as ex: raise ValueError('Host %s: %s', hostname, ex.args[1]) - try: - int(port) - except ValueError: + if not isinstance(port, int): raise ValueError('Invalid port. Must be an integer') def _migrate_config_1_to_2(config): - localclient_username, localclient_password = get_localhost_auth() - if not localclient_username: - # Nothing to do here, there's no auth file - return - for idx, (__, host, __, username, __) in enumerate(config['hosts'][:]): - if host in LOCALHOST and not username: - config['hosts'][idx][3] = localclient_username - config['hosts'][idx][4] = localclient_password - return config + """Mirgrates old hostlist config files to new format""" + localclient_username, localclient_password = get_localhost_auth() + if not localclient_username: + # Nothing to do here, there's no auth file + return + for idx, (__, host, __, username, __) in enumerate(config['hosts'][:]): + if host in LOCALHOST and not username: + config['hosts'][idx][3] = localclient_username + config['hosts'][idx][4] = localclient_password + return config class HostList(object): + """This class contains methods for adding, removing and looking up hosts in hostlist.conf.""" def __init__(self): self.config = Config('hostlist.conf', default_hostlist(), config_dir=get_config_dir(), file_version=2) self.config.run_converter((0, 1), 2, _migrate_config_1_to_2) @@ -152,31 +119,87 @@ class HostList(object): def get_host_info(self, host_id): """Get the host details for host_id. - Includes password details! + Args: + host_id (str): The host id to get info on. + + Returns: + list: A list of (host_id, hostname, port, username). """ for host_entry in self.config['hosts']: if host_entry[0] == host_id: - return host_entry + return host_entry[0:4] else: return [] def get_hosts_info(self): - """Get all the hosts in the hostlist + """Get information of all the hosts in the hostlist. + + Returns: + list of lists: Host information in the format [(host_id, hostname, port, username)]. - Excluding password details. """ - return [host[0:4 + 1] for host in self.config['hosts']] + return [host_entry[0:4] for host_entry in self.config['hosts']] - def get_hosts_info2(self): - """Get all the hosts in the hostlist + def get_host_status(self, host_id): + """Gets the current status (online/offline) of the host + + Args: + host_id (str): The host id to check status of. + + Returns: + tuple: A tuple of strings (host_id, status, version). - Excluding password details. """ - return [host for host in self.config['hosts']] + status_offline = (host_id, 'Offline', '') + + def on_connect(result, c, host_id): + """Successfully connected to a daemon""" + def on_info(info, c): + c.disconnect() + return host_id, 'Online', info + + def on_info_fail(reason, c): + c.disconnect() + return status_offline + + return c.daemon.info().addCallback(on_info, c).addErrback(on_info_fail, c) + + def on_connect_failed(reason, host_id): + """Connection to daemon failed""" + log.debug('Host status failed for %s: %s', host_id, reason) + return status_offline + + try: + host_id, host, port, user = self.get_host_info(host_id) + except ValueError: + log.warning('Problem getting host_id info from hostlist') + return status_offline + + try: + ip = gethostbyname(host) + except gaierror as ex: + log.error('Error resolving host %s to ip: %s', host, ex.args[1]) + return status_offline + + host_conn_info = (ip, port, 'localclient' if not user and host in LOCALHOST else user) + if client.connected() and host_conn_info == client.connection_info(): + # Currently connected to host_id daemon. + def on_info(info, host_id): + log.debug('Client connected, query info: %s', info) + return host_id, 'Connected', info + + return client.daemon.info().addCallback(on_info, host_id) + else: + # Attempt to connect to daemon with host_id details. + c = Client() + d = c.connect(host, port, skip_authentication=True) + d.addCallback(on_connect, c, host_id) + d.addErrback(on_connect_failed, host_id) + return d def update_host(self, host_id, hostname, port, username, password): - """Update the host with new details. + """Update the supplied host id with new connection details. Args: host_id (str): The host id to update. @@ -192,13 +215,23 @@ class HostList(object): if (not password and not username or username == 'localclient') and hostname in LOCALHOST: username, password = get_localhost_auth() - for host_entry in self.config['hosts']: + for idx, host_entry in enumerate(self.config['hosts']): if host_id == host_entry[0]: - host_entry = host_id, hostname, port, username, password + self.config['hosts'][idx] = host_id, hostname, port, username, password + self.config.save() return True return False def remove_host(self, host_id): + """Removes the host entry from hostlist config. + + Args: + host_id (str): The host id to remove. + + Returns: + bool: True is successfully removed, False otherwise. + + """ for host_entry in self.config['hosts']: if host_id == host_entry[0]: self.config['hosts'].remove(host_entry) @@ -209,3 +242,12 @@ class HostList(object): def add_default_host(self): self.add_host(DEFAULT_HOST, DEFAULT_PORT, *get_localhost_auth()) + + def connect_host(self, host_id): + """Connect to host daemon""" + for host_entry in self.config['hosts']: + if host_entry[0] == host_id: + __, host, port, username, password = host_entry + return client.connect(host, port, username, password) + + return defer.fail(Exception('Bad host id')) diff --git a/deluge/ui/web/js/deluge-all/AddConnectionWindow.js b/deluge/ui/web/js/deluge-all/AddConnectionWindow.js index a72b81706..92256c693 100644 --- a/deluge/ui/web/js/deluge-all/AddConnectionWindow.js +++ b/deluge/ui/web/js/deluge-all/AddConnectionWindow.js @@ -78,7 +78,7 @@ Deluge.AddConnectionWindow = Ext.extend(Ext.Window, { onAddClick: function() { var values = this.form.getForm().getValues(); - deluge.client.web.add_host(values.host, values.port, values.username, values.password, { + deluge.client.web.add_host(values.host, Number(values.port), values.username, values.password, { success: function(result) { if (!result[0]) { Ext.MessageBox.show({ diff --git a/deluge/ui/web/js/deluge-all/ConnectionManager.js b/deluge/ui/web/js/deluge-all/ConnectionManager.js index b6ac56973..1e807be52 100644 --- a/deluge/ui/web/js/deluge-all/ConnectionManager.js +++ b/deluge/ui/web/js/deluge-all/ConnectionManager.js @@ -91,6 +91,13 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { iconCls: 'icon-add', handler: this.onAddClick, scope: this + }, { + id: 'cm-edit', + cls: 'x-btn-text-icon', + text: _('Edit'), + iconCls: 'icon-edit', + handler: this.onEditClick, + scope: this }, { id: 'cm-remove', cls: 'x-btn-text-icon', @@ -164,27 +171,25 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { var button = this.buttons[1], status = record.get('status'); // Update the Connect/Disconnect button - if (status == 'Connected') { - button.enable(); + button.enable(); + if (status.toLowerCase() == 'connected') { button.setText(_('Disconnect')); - } else if (status == 'Offline') { - button.disable(); } else { - button.enable(); button.setText(_('Connect')); + if (status.toLowerCase() != 'online') button.disable(); } // Update the Stop/Start Daemon button - if (status == 'Offline') { + if (status.toLowerCase() == 'connected' || status.toLowerCase() == 'online') { + this.stopHostButton.enable(); + this.stopHostButton.setText(_('Stop Daemon')); + } else { if (record.get('host') == '127.0.0.1' || record.get('host') == 'localhost') { this.stopHostButton.enable(); this.stopHostButton.setText(_('Start Daemon')); } else { this.stopHostButton.disable(); } - } else { - this.stopHostButton.enable(); - this.stopHostButton.setText(_('Stop Daemon')); } }, @@ -192,13 +197,25 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { onAddClick: function(button, e) { if (!this.addWindow) { this.addWindow = new Deluge.AddConnectionWindow(); - this.addWindow.on('hostadded', this.onHostAdded, this); + this.addWindow.on('hostadded', this.onHostChange, this); } this.addWindow.show(); }, // private - onHostAdded: function() { + onEditClick: function(button, e) { + var connection = this.list.getSelectedRecords()[0]; + if (!connection) return; + + if (!this.editWindow) { + this.editWindow = new Deluge.EditConnectionWindow(); + this.editWindow.on('hostedited', this.onHostChange, this); + } + this.editWindow.show(connection); + }, + + // private + onHostChange: function() { this.loadHosts(); }, @@ -212,7 +229,7 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { var selected = this.list.getSelectedRecords()[0]; if (!selected) return; - if (selected.get('status') == 'Connected') { + if (selected.get('status').toLowerCase() == 'connected') { deluge.client.web.disconnect({ success: function(result) { this.update(this); @@ -248,8 +265,8 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { // private onGetHostStatus: function(host) { var record = this.list.getStore().getById(host[0]); - record.set('status', host[3]) - record.set('version', host[4]) + record.set('status', host[1]) + record.set('version', host[2]) record.commit(); if (this.list.getSelectedRecords()[0] == record) this.updateButtons(record); }, @@ -312,11 +329,13 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { // private onSelectionChanged: function(list, selections) { if (selections[0]) { + this.editHostButton.enable(); this.removeHostButton.enable(); this.stopHostButton.enable(); this.stopHostButton.setText(_('Stop Daemon')); this.updateButtons(this.list.getRecord(selections[0])); } else { + this.editHostButton.disable(); this.removeHostButton.disable(); this.stopHostButton.disable(); } @@ -328,6 +347,7 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { if (!this.addHostButton) { var bbar = this.panel.getBottomToolbar(); this.addHostButton = bbar.items.get('cm-add'); + this.editHostButton = bbar.items.get('cm-edit'); this.removeHostButton = bbar.items.get('cm-remove'); this.stopHostButton = bbar.items.get('cm-stop'); } diff --git a/deluge/ui/web/js/deluge-all/EditConnectionWindow.js b/deluge/ui/web/js/deluge-all/EditConnectionWindow.js new file mode 100644 index 000000000..07b883236 --- /dev/null +++ b/deluge/ui/web/js/deluge-all/EditConnectionWindow.js @@ -0,0 +1,114 @@ +/*! + * Deluge.EditConnectionWindow.js + * + * Copyright (c) Damien Churchill 2009-2010 + * + * 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. + */ +Ext.ns('Deluge'); + +/** + * @class Deluge.EditConnectionWindow + * @extends Ext.Window + */ +Deluge.EditConnectionWindow = Ext.extend(Ext.Window, { + + title: _('Edit Connection'), + iconCls: 'x-deluge-add-window-icon', + + layout: 'fit', + width: 300, + height: 195, + constrainHeader: true, + bodyStyle: 'padding: 10px 5px;', + closeAction: 'hide', + + initComponent: function() { + Deluge.EditConnectionWindow.superclass.initComponent.call(this); + + this.addEvents('hostedited'); + + this.addButton(_('Close'), this.hide, this); + this.addButton(_('Edit'), this.onEditClick, this); + + this.on('hide', this.onHide, this); + + this.form = this.add({ + xtype: 'form', + defaultType: 'textfield', + baseCls: 'x-plain', + labelWidth: 60, + items: [{ + fieldLabel: _('Host:'), + labelSeparator : '', + name: 'host', + anchor: '75%', + value: '' + }, { + xtype: 'spinnerfield', + fieldLabel: _('Port:'), + labelSeparator : '', + name: 'port', + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: 0, + maxValue: 65535 + }, + anchor: '40%', + value: 58846 + }, { + fieldLabel: _('Username:'), + labelSeparator : '', + name: 'username', + anchor: '75%', + value: '' + }, { + fieldLabel: _('Password:'), + labelSeparator : '', + anchor: '75%', + name: 'password', + inputType: 'password', + value: '' + }] + }); + }, + + show: function(connection) { + Deluge.EditConnectionWindow.superclass.show.call(this); + + this.form.getForm().findField('host').setValue(connection.get('host')); + this.form.getForm().findField('port').setValue(connection.get('port')); + this.form.getForm().findField('username').setValue(connection.get('user')); + this.host_id = connection.id + }, + + onEditClick: function() { + var values = this.form.getForm().getValues(); + deluge.client.web.edit_host(this.host_id, values.host, Number(values.port), values.username, values.password, { + success: function(result) { + if (!result) { + console.log(result) + Ext.MessageBox.show({ + title: _('Error'), + msg: String.format(_('Unable to edit host')), + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + } else { + this.fireEvent('hostedited'); + } + this.hide(); + }, + scope: this + }); + }, + + onHide: function() { + this.form.getForm().reset(); + } +}); diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py index 62ccd7709..d31c778f1 100644 --- a/deluge/ui/web/json_api.py +++ b/deluge/ui/web/json_api.py @@ -164,7 +164,8 @@ class JSON(resource.Resource, component.Component): except AuthError: error = {'message': 'Not authenticated', 'code': 1} except Exception as ex: - log.error('Error calling method `%s`', method) + log.error('Error calling method `%s`: %s', method, ex) + log.exception(ex) error = {'message': '%s: %s' % (ex.__class__.__name__, str(ex)), 'code': 3} return request_id, result, error @@ -404,11 +405,10 @@ class WebApi(JSONComponent): self.sessionproxy.stop() return defer.succeed(True) - def _connect_daemon(self, host='localhost', port=58846, username='', password=''): + def _connect_daemon(self, host_id): """ Connects the client to a daemon """ - d = client.connect(host, port, username, password) def on_client_connected(connection_id): """ @@ -420,7 +420,7 @@ class WebApi(JSONComponent): self.start() return d - return d.addCallback(on_client_connected) + return self.hostlist.connect_host(host_id).addCallback(on_client_connected) @export def connect(self, host_id): @@ -432,10 +432,7 @@ class WebApi(JSONComponent): :returns: the methods the daemon supports :rtype: list """ - host = self._get_host(host_id) - if host: - return self._connect_daemon(*host[1:]) - return defer.fail(Exception('Bad host id')) + return self._connect_daemon(host_id) @export def connected(self): @@ -716,7 +713,7 @@ class WebApi(JSONComponent): Return the hosts in the hostlist. """ log.debug('get_hosts called') - return self.hostlist.get_hosts() + [''] + return self.hostlist.get_hosts_info() @export def get_host_status(self, host_id): @@ -725,46 +722,13 @@ class WebApi(JSONComponent): :param host_id: the hash id of the host :type host_id: string + """ - def response(status, info=None): - return host_id, host, port, status, info + def response(result): + log.critical('%s', result) + return result - try: - host_id, host, port, user, password = self._get_host(host_id) - except TypeError: - host = None - port = None - return response('Offline') - - def on_connect(connected, c, host_id): - def on_info(info, c): - c.disconnect() - return response('Online', info) - - def on_info_fail(reason, c): - c.disconnect() - return response('Offline') - - if not connected: - return response('Offline') - - return c.daemon.info().addCallback(on_info, c).addErrback(on_info_fail, c) - - def on_connect_failed(reason, host_id): - return response('Offline') - - if client.connected() and (host, port, 'localclient' if not - user and host in ('127.0.0.1', 'localhost') else - user) == client.connection_info(): - def on_info(info): - return response('Connected', info) - - return client.daemon.info().addCallback(on_info) - else: - c = Client() - d = c.connect(host, port, user, password) - d.addCallback(on_connect, c, host_id).addErrback(on_connect_failed, host_id) - return d + return self.hostlist.get_host_status(host_id).addCallback(response) @export def add_host(self, host, port, username='', password=''): @@ -787,15 +751,33 @@ class WebApi(JSONComponent): else: return True, host_id + @export + def edit_host(self, host_id, host, port, username='', password=''): + """Edit host details in the hostlist. + + Args: + host_id (str): The host identifying hash. + host (str): The IP or hostname of the deluge daemon. + port (int): The port of the deluge daemon. + username (str): The username to login to the daemon with. + password (str): The password to login to the daemon with. + + Returns: + bool: True if succesful, False otherwise. + + """ + return self.hostlist.update_host(host_id, host, port, username, password) + @export def remove_host(self, host_id): - """Removes a host from the list. + """Removes a host from the hostlist. Args: host_id (str): The host identifying hash. Returns: bool: True if succesful, False otherwise. + """ return self.hostlist.remove_host(host_id)