[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.
This commit is contained in:
Calum Lind 2017-04-01 11:40:15 +01:00
commit 31555ee5ed
19 changed files with 822 additions and 965 deletions

View file

@ -997,6 +997,40 @@ def create_localclient_account(append=False):
os.fsync(_file.fileno()) 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): def set_env_variable(name, value):
""" """
:param name: environment variable name :param name: environment variable name

View file

@ -66,7 +66,7 @@ class WebServerTestBase(BaseTestCase, DaemonBase):
self.deluge_web = DelugeWeb(daemon=False) 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 host[2] = self.listen_port
self.deluge_web.web_api.hostlist.config['hosts'][0] = tuple(host) self.deluge_web.web_api.hostlist.config['hosts'][0] = tuple(host)
self.host_id = host[0] self.host_id = host[0]

View file

@ -8,8 +8,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import deluge.component as component import deluge.component as component
from deluge.common import get_localhost_auth
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AuthManager from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AuthManager
from deluge.ui import hostlist
from .basetest import BaseTestCase from .basetest import BaseTestCase
@ -25,6 +25,6 @@ class AuthManagerTestCase(BaseTestCase):
def test_authorize(self): def test_authorize(self):
self.assertEqual( self.assertEqual(
self.auth.authorize(*hostlist.get_localhost_auth()), self.auth.authorize(*get_localhost_auth()),
AUTH_LEVEL_ADMIN AUTH_LEVEL_ADMIN
) )

View file

@ -11,10 +11,9 @@ from twisted.internet import defer
import deluge.component as component import deluge.component as component
from deluge import error 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.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.ui.client import Client, DaemonSSLProxy, client from deluge.ui.client import Client, DaemonSSLProxy, client
from deluge.ui.hostlist import get_localhost_auth
from .basetest import BaseTestCase from .basetest import BaseTestCase
from .daemon_base import DaemonBase from .daemon_base import DaemonBase

View file

@ -11,11 +11,11 @@ from __future__ import unicode_literals
import deluge.component as component import deluge.component as component
import deluge.error import deluge.error
from deluge.common import get_localhost_auth
from deluge.core import rpcserver from deluge.core import rpcserver
from deluge.core.authmanager import AuthManager from deluge.core.authmanager import AuthManager
from deluge.core.rpcserver import DelugeRPCProtocol, RPCServer from deluge.core.rpcserver import DelugeRPCProtocol, RPCServer
from deluge.log import setup_logger from deluge.log import setup_logger
from deluge.ui.hostlist import get_localhost_auth
from .basetest import BaseTestCase from .basetest import BaseTestCase

View file

@ -23,9 +23,8 @@ import deluge.ui.console
import deluge.ui.console.cmdline.commands.quit import deluge.ui.console.cmdline.commands.quit
import deluge.ui.console.main import deluge.ui.console.main
import deluge.ui.web.server 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 import ui_entry
from deluge.ui.hostlist import get_localhost_auth
from deluge.ui.web.server import DelugeWeb from deluge.ui.web.server import DelugeWeb
from . import common from . import common
@ -334,7 +333,7 @@ class ConsoleUIWithDaemonBaseTestCase(UIWithDaemonBaseTestCase):
def set_up(self): def set_up(self):
# Avoid calling reactor.shutdown after commands are executed by main.exec_args() # 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) return UIWithDaemonBaseTestCase.set_up(self)
@defer.inlineCallbacks @defer.inlineCallbacks
@ -344,7 +343,6 @@ class ConsoleUIWithDaemonBaseTestCase(UIWithDaemonBaseTestCase):
[username] + ['--password'] + [password] + ['status']) [username] + ['--password'] + [password] + ['status'])
fd = StringFileDescriptor(sys.stdout) fd = StringFileDescriptor(sys.stdout)
self.patch(sys, 'stdout', fd) self.patch(sys, 'stdout', fd)
self.patch(deluge.ui.console.main, 'reactor', common.ReactorOverride())
yield self.exec_command() yield self.exec_command()

View file

@ -91,17 +91,17 @@ class WebAPITestCase(WebServerTestBase):
def test_get_host(self): def test_get_host(self):
self.assertFalse(self.deluge_web.web_api._get_host('invalid_id')) self.assertFalse(self.deluge_web.web_api._get_host('invalid_id'))
conn = list(self.deluge_web.web_api.hostlist.get_hosts_info2()[0]) conn = list(self.deluge_web.web_api.hostlist.get_hosts_info()[0])
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])
def test_add_host(self): def test_add_host(self):
conn = ['abcdef', '10.0.0.1', 0, 'user123', 'pass123'] conn = ['abcdef', '10.0.0.1', 0, 'user123', 'pass123']
self.assertFalse(self.deluge_web.web_api._get_host(conn[0])) self.assertFalse(self.deluge_web.web_api._get_host(conn[0]))
# Add valid host # Add valid host
ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4]) result, host_id = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
self.assertEqual(ret[0], True) self.assertEqual(result, True)
conn[0] = ret[1] conn[0] = host_id
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])
# Add already existing host # Add already existing host
ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4]) 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): def test_remove_host(self):
conn = ['connection_id', '', 0, '', ''] conn = ['connection_id', '', 0, '', '']
self.deluge_web.web_api.hostlist.config['hosts'].append(conn) 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 # Remove valid host
self.assertTrue(self.deluge_web.web_api.remove_host(conn[0])) self.assertTrue(self.deluge_web.web_api.remove_host(conn[0]))
self.assertFalse(self.deluge_web.web_api._get_host(conn[0])) self.assertFalse(self.deluge_web.web_api._get_host(conn[0]))

View file

@ -17,11 +17,10 @@ import sys
from twisted.internet import defer, reactor, ssl from twisted.internet import defer, reactor, ssl
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ClientFactory
import deluge.common
from deluge import error from deluge import error
from deluge.common import get_localhost_auth, get_version
from deluge.decorators import deprecated from deluge.decorators import deprecated
from deluge.transfer import DelugeTransferProtocol from deluge.transfer import DelugeTransferProtocol
from deluge.ui.hostlist import get_localhost_auth
RPC_RESPONSE = 1 RPC_RESPONSE = 1
RPC_ERROR = 2 RPC_ERROR = 2
@ -384,8 +383,7 @@ class DaemonSSLProxy(DaemonProxy):
def authenticate(self, username, password): def authenticate(self, username, password):
log.debug('%s.authenticate: %s', self.__class__.__name__, username) log.debug('%s.authenticate: %s', self.__class__.__name__, username)
login_deferred = defer.Deferred() login_deferred = defer.Deferred()
d = self.call('daemon.login', username, password, d = self.call('daemon.login', username, password, client_version=get_version())
client_version=deluge.common.get_version())
d.addCallbacks(self.__on_login, self.__on_login_fail, callbackArgs=[username, login_deferred], d.addCallbacks(self.__on_login, self.__on_login_fail, callbackArgs=[username, login_deferred],
errbackArgs=[login_deferred]) errbackArgs=[login_deferred])
return login_deferred return login_deferred
@ -619,17 +617,14 @@ class Client(object):
self.stop_standalone() self.stop_standalone()
def start_daemon(self, port, config): def start_daemon(self, port, config):
""" """Starts a daemon process.
Starts a daemon process.
:param port: the port for the daemon to listen on Args:
:type port: int port (int): Port for the daemon to listen on.
:param config: the path to the current config folder config (str): Config path to pass to daemon.
:type config: str
:returns: True if started, False if not
:rtype: bool
: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 # 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.')) 'the deluged package is installed, or added to your PATH.'))
else: else:
log.exception(ex) log.exception(ex)
raise ex
except Exception as ex: except Exception as ex:
log.error('Unable to start daemon!') log.error('Unable to start daemon!')
log.exception(ex) log.exception(ex)
return False
else: else:
return True return True
return False
def is_localhost(self): def is_localhost(self):
""" """

View file

@ -13,7 +13,7 @@ import logging
import deluge.component as component import deluge.component as component
from deluge.decorators import overrides 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.modes.basemode import BaseMode
from deluge.ui.console.widgets.popup import InputPopup, PopupsHandler, SelectablePopup from deluge.ui.console.widgets.popup import InputPopup, PopupsHandler, SelectablePopup
from deluge.ui.hostlist import HostList from deluge.ui.hostlist import HostList
@ -48,7 +48,7 @@ class ConnectionManager(BaseMode, PopupsHandler):
space_below=True) space_below=True)
self.push_popup(popup, clear=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 host_id, hostname, port, user = host_entry
args = {'data': host_id, 'foreground': 'red'} args = {'data': host_id, 'foreground': 'red'}
state = 'Offline' state = 'Offline'
@ -64,34 +64,13 @@ class ConnectionManager(BaseMode, PopupsHandler):
self.refresh() self.refresh()
def update_hosts_status(self): 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): for host_entry in self.hostlist.get_hosts_info():
if host_id in self.statuses: def on_host_status(status_info):
del self.statuses[host_id] self.statuses[status_info[0]] = status_info
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]
self.update_select_host_popup() self.update_select_host_popup()
for host_entry in self.hostlist.get_hosts_info2(): self.hostlist.get_host_status(host_entry[0]).addCallback(on_host_status)
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)
def _on_connected(self, result): def _on_connected(self, result):
d = component.get('ConsoleUI').start_console() d = component.get('ConsoleUI').start_console()
@ -108,12 +87,9 @@ class ConnectionManager(BaseMode, PopupsHandler):
def _host_selected(self, selected_host, *args, **kwargs): def _host_selected(self, selected_host, *args, **kwargs):
if selected_host in self.statuses: if selected_host in self.statuses:
for host_entry in self.hostlist.get_hosts_info(): d = self.hostlist.connect_host(selected_host)
if host_entry[0] == selected_host: d.addCallback(self._on_connected)
__, host, port, user, password = host_entry d.addErrback(self._on_connect_fail)
d = client.connect(host, port, user, password)
d.addCallback(self._on_connected)
d.addErrback(self._on_connect_fail)
def _do_add(self, result, **kwargs): def _do_add(self, result, **kwargs):
if not result or kwargs.get('close', False): if not result or kwargs.get('close', False):

View file

@ -14,14 +14,14 @@ import os
from socket import gaierror, gethostbyname from socket import gaierror, gethostbyname
import gtk import gtk
from twisted.internet import reactor from twisted.internet import defer, reactor
import deluge.component as component 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.configmanager import ConfigManager, get_config_dir
from deluge.error import AuthenticationRequired, BadLoginError, IncompatibleClient from deluge.error import AuthenticationRequired, BadLoginError, IncompatibleClient
from deluge.ui.client import Client, client 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.gtkui.dialogs import AuthenticationDialog, ErrorDialog
from deluge.ui.hostlist import DEFAULT_PORT, HostList from deluge.ui.hostlist import DEFAULT_PORT, HostList
@ -68,7 +68,6 @@ def cell_render_status(column, cell, model, row, data):
pixbuf = None pixbuf = None
if status in HOSTLIST_STATUS: if status in HOSTLIST_STATUS:
pixbuf = HOSTLIST_PIXBUFS[HOSTLIST_STATUS.index(status)] pixbuf = HOSTLIST_PIXBUFS[HOSTLIST_STATUS.index(status)]
cell.set_property('pixbuf', pixbuf) cell.set_property('pixbuf', pixbuf)
@ -76,7 +75,7 @@ class ConnectionManager(component.Component):
def __init__(self): def __init__(self):
component.Component.__init__(self, 'ConnectionManager') component.Component.__init__(self, 'ConnectionManager')
self.gtkui_config = ConfigManager('gtkui.conf') self.gtkui_config = ConfigManager('gtkui.conf')
self.hostlist = HostList()
self.running = False self.running = False
# Component overrides # Component overrides
@ -93,92 +92,408 @@ class ConnectionManager(component.Component):
# Public methods # Public methods
def show(self): def show(self):
""" """Show the ConnectionManager dialog."""
Show the ConnectionManager dialog.
"""
# Get the gtk builder file for the connection manager
self.builder = gtk.Builder() self.builder = gtk.Builder()
# The main dialog
self.builder.add_from_file(resource_filename( self.builder.add_from_file(resource_filename(
'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.ui'))) '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 = self.builder.get_object('connection_manager')
self.connection_manager.set_transient_for(component.get('MainWindow').window) 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 # Create status pixbufs
if not HOSTLIST_PIXBUFS: if not HOSTLIST_PIXBUFS:
for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT):
HOSTLIST_PIXBUFS.append( HOSTLIST_PIXBUFS.append(
self.connection_manager.render_icon( self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU))
stock_id, gtk.ICON_SIZE_MENU
)
)
# Create the host list gtkliststore # Setup the hostlist liststore and treeview
# id-hash, hostname, port, username, password, status, version self.treeview = self.builder.get_object('treeview_hostlist')
self.liststore = gtk.ListStore(str, str, int, str, str, str, str) self.liststore = self.builder.get_object('liststore_hostlist')
# Setup host list treeview
self.hostlist.set_model(self.liststore)
render = gtk.CellRendererPixbuf() render = gtk.CellRendererPixbuf()
column = gtk.TreeViewColumn(_('Status'), render) column = gtk.TreeViewColumn(_('Status'), render)
column.set_cell_data_func(render, cell_render_status, HOSTLIST_COL_STATUS) 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() render = gtk.CellRendererText()
column = gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST) column = gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST)
column.set_cell_data_func( host_data = (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER)
render, cell_render_host, (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER)) column.set_cell_data_func(render, cell_render_host, host_data)
column.set_expand(True) column.set_expand(True)
self.hostlist.append_column(column) self.treeview.append_column(column)
render = gtk.CellRendererText()
column = gtk.TreeViewColumn(_('Version'), render, text=HOSTLIST_COL_VERSION) column = gtk.TreeViewColumn(_('Version'), gtk.CellRendererText(), text=HOSTLIST_COL_VERSION)
self.hostlist.append_column(column) 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 # Connect the signals to the handlers
self.builder.connect_signals(self) self.builder.connect_signals(self)
self.hostlist.get_selection().connect( self.treeview.get_selection().connect('changed', self.on_hostlist_selection_changed)
'changed', self.on_hostlist_selection_changed
)
# Load any saved host entries
self.__load_hostlist()
self.__load_options()
self.__update_list()
# Set running True before update status call.
self.running = True self.running = True
# Trigger the on_selection_changed code and select the first host
# if possible if windows_check():
self.hostlist.get_selection().unselect_all() # Call to simulate() required to workaround showing daemon status (see #2813)
if len(self.liststore) > 0: reactor.simulate()
self.hostlist.get_selection().select_path(0) 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 # Run the dialog
self.connection_manager.run() self.connection_manager.run()
# Dialog closed so cleanup.
self.running = False self.running = False
# Save the toggle options
self.__save_options()
self.connection_manager.destroy() self.connection_manager.destroy()
del self.builder del self.builder
del self.connection_manager del self.connection_manager
del self.liststore 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): def on_entry_host_paste_clipboard(self, widget):
text = get_clipboard_text() text = get_clipboard_text()
@ -194,456 +509,3 @@ class ConnectionManager(component.Component):
self.builder.get_object('entry_username').set_text(parsed.username) self.builder.get_object('entry_username').set_text(parsed.username)
if parsed.password: if parsed.password:
self.builder.get_object('entry_password').set_text(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)

View file

@ -13,7 +13,7 @@
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="title" translatable="yes">Add Host</property> <property name="title" translatable="yes">Add Host</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center</property> <property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property> <property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<child internal-child="vbox"> <child internal-child="vbox">
@ -43,14 +43,14 @@
</child> </child>
<child> <child>
<object class="GtkButton" id="button_addhost_add"> <object class="GtkButton" id="button_addhost_add">
<property name="label">gtk-add</property> <property name="label">_Save</property>
<property name="use_action_appearance">False</property> <property name="use_action_appearance">False</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="can_default">True</property> <property name="can_default">True</property>
<property name="has_default">True</property> <property name="has_default">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="use_stock">True</property> <property name="use_underline">True</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -58,20 +58,6 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkButton" id="button_addhost_save">
<property name="label">gtk-save</property>
<property name="use_action_appearance">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -114,7 +100,7 @@
<property name="secondary_icon_activatable">False</property> <property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property> <property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property> <property name="secondary_icon_sensitive">True</property>
<signal name="paste-clipboard" handler="on_entry_host_paste_clipboard" /> <signal name="paste-clipboard" handler="on_entry_host_paste_clipboard" swapped="no"/>
</object> </object>
</child> </child>
</object> </object>
@ -253,15 +239,11 @@
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child>
<placeholder/>
</child>
</object> </object>
</child> </child>
<action-widgets> <action-widgets>
<action-widget response="0">button_addhost_cancel</action-widget> <action-widget response="0">button_addhost_cancel</action-widget>
<action-widget response="1">button_addhost_add</action-widget> <action-widget response="0">button_addhost_add</action-widget>
<action-widget response="2">button_addhost_save</action-widget>
</action-widgets> </action-widgets>
</object> </object>
</interface> </interface>

View file

@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="2.24"/>
<!-- interface-naming-policy project-wide -->
<object class="GtkDialog" id="askpassword_dialog">
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="title" translatable="yes">Password Required</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<child internal-child="vbox">
<object class="GtkVBox" id="dialog-vbox7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="dialog-action_area7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="askpassword_dialog_connect_button">
<property name="label">gtk-connect</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_askpassword_dialog_connect_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage" id="askpassword_dialog_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-authentication</property>
<property name="icon-size">6</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="askpassword_dialog_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">False</property>
<property name="invisible_char">●</property>
<property name="truncate_multiline">True</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="1">askpassword_dialog_connect_button</action-widget>
</action-widgets>
</object>
</interface>

View file

@ -46,10 +46,11 @@
<property name="hscrollbar_policy">automatic</property> <property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property> <property name="vscrollbar_policy">automatic</property>
<child> <child>
<object class="GtkTreeView" id="hostlist"> <object class="GtkTreeView" id="treeview_hostlist">
<property name="height_request">80</property> <property name="height_request">80</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="model">liststore_hostlist</property>
<signal name="row-activated" handler="on_hostlist_row_activated" swapped="no"/> <signal name="row-activated" handler="on_hostlist_row_activated" swapped="no"/>
</object> </object>
</child> </child>
@ -242,12 +243,12 @@
<property name="layout_style">end</property> <property name="layout_style">end</property>
<child> <child>
<object class="GtkButton" id="button_close"> <object class="GtkButton" id="button_close">
<property name="label">gtk-close</property> <property name="label">_Close</property>
<property name="use_action_appearance">False</property> <property name="use_action_appearance">False</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="use_stock">True</property> <property name="use_underline">True</property>
<signal name="clicked" handler="on_button_close_clicked" swapped="no"/> <signal name="clicked" handler="on_button_close_clicked" swapped="no"/>
</object> </object>
<packing> <packing>
@ -258,12 +259,12 @@
</child> </child>
<child> <child>
<object class="GtkButton" id="button_connect"> <object class="GtkButton" id="button_connect">
<property name="label">gtk-connect</property> <property name="label">C_onnect</property>
<property name="use_action_appearance">False</property> <property name="use_action_appearance">False</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="use_stock">True</property> <property name="use_underline">True</property>
<signal name="clicked" handler="on_button_connect_clicked" swapped="no"/> <signal name="clicked" handler="on_button_connect_clicked" swapped="no"/>
</object> </object>
<packing> <packing>
@ -303,6 +304,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="on_chk_toggled" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -318,6 +320,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="on_chk_toggled" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -333,6 +336,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="on_chk_toggled" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -368,4 +372,22 @@
<action-widget response="0">button_connect</action-widget> <action-widget response="0">button_connect</action-widget>
</action-widgets> </action-widgets>
</object> </object>
<object class="GtkListStore" id="liststore_hostlist">
<columns>
<!-- column-name host_id -->
<column type="gchararray"/>
<!-- column-name hostname -->
<column type="gchararray"/>
<!-- column-name port -->
<column type="gint"/>
<!-- column-name username -->
<column type="gchararray"/>
<!-- column-name password -->
<column type="gchararray"/>
<!-- column-name status -->
<column type="gchararray"/>
<!-- column-name version -->
<column type="gchararray"/>
</columns>
</object>
</interface> </interface>

View file

@ -21,7 +21,7 @@ pygtk.require('2.0') # NOQA: E402
# isort:imports-thirdparty # isort:imports-thirdparty
from gobject import set_prgname 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 gtk.gdk import WINDOWING, threads_enter, threads_init, threads_leave
from twisted.internet import defer, gtk2reactor from twisted.internet import defer, gtk2reactor
from twisted.internet.error import ReactorAlreadyInstalledError from twisted.internet.error import ReactorAlreadyInstalledError
@ -38,12 +38,12 @@ except ReactorAlreadyInstalledError as ex:
import deluge.component as component import deluge.component as component
from deluge.common import fsize, fspeed, get_default_download_dir, osx_check, windows_check from deluge.common import fsize, fspeed, get_default_download_dir, osx_check, windows_check
from deluge.configmanager import ConfigManager, get_config_dir 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.client import client
from deluge.ui.gtkui.addtorrentdialog import AddTorrentDialog from deluge.ui.gtkui.addtorrentdialog import AddTorrentDialog
from deluge.ui.gtkui.common import associate_magnet_links from deluge.ui.gtkui.common import associate_magnet_links
from deluge.ui.gtkui.connectionmanager import ConnectionManager 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.filtertreeview import FilterTreeView
from deluge.ui.gtkui.ipcinterface import IPCInterface, process_args from deluge.ui.gtkui.ipcinterface import IPCInterface, process_args
from deluge.ui.gtkui.mainwindow import MainWindow 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.tracker_icons import TrackerIcons
from deluge.ui.translations_util import set_language, setup_translations from deluge.ui.translations_util import set_language, setup_translations
set_prgname(b'deluge') set_prgname('deluge'.encode('utf8'))
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
try: try:
@ -220,7 +220,7 @@ class GtkUI(object):
# Setup RPC stats logging # Setup RPC stats logging
# daemon_bps: time, bytes_sent, bytes_recv # daemon_bps: time, bytes_sent, bytes_recv
self.daemon_bps = (0, 0, 0) 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 self.closing = False
# Twisted catches signals to terminate, so have it call a pre_shutdown method. # Twisted catches signals to terminate, so have it call a pre_shutdown method.
@ -238,8 +238,8 @@ class GtkUI(object):
# Initialize gdk threading # Initialize gdk threading
threads_enter() threads_enter()
reactor.run() reactor.run()
# Reactor is not running. Any async callbacks (Deferreds) can no longer # Reactor no longer running so async callbacks (Deferreds) cannot be
# be processed from this point on. # processed after this point.
threads_leave() threads_leave()
def shutdown(self, *args, **kwargs): def shutdown(self, *args, **kwargs):
@ -267,11 +267,12 @@ class GtkUI(object):
reactor.stop() 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: if component.get('MainWindow').restart:
os.execv(sys.argv[0], sys.argv) 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(): if not client.connected():
return return
@ -290,144 +291,73 @@ class GtkUI(object):
log.debug('_on_reactor_start') log.debug('_on_reactor_start')
self.mainwindow.first_show() self.mainwindow.first_show()
if self.config['standalone']: if not self.config['standalone']:
def on_dialog_response(response): return self._start_thinclient()
if response != RESPONSE_YES:
# The user does not want to turn Standalone Mode off, so just quit err_msg = ''
self.mainwindow.quit() try:
return 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 # Turning off standalone
self.config['standalone'] = False self.config['standalone'] = False
self.__start_thinclient() self._start_thinclient()
else:
# User want keep Standalone Mode so just quit.
self.mainwindow.quit()
try: # An error occurred so ask user to switch from Standalone to Thin Client mode.
try: err_msg += '\n\n' + _('Continue in Thin Client mode?')
client.start_standalone() d = YesNoDialog(_('Change User Interface Mode'), err_msg).run()
except DaemonRunningError: d.addCallback(on_dialog_response)
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()
def on_ed_response(response): def _start_thinclient(self):
d = YesNoDialog( """Start the gtkui in thinclient mode"""
_('Switch to Thin Client Mode?'), if log.isEnabledFor(logging.DEBUG):
_('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:
self.rpc_stats.start(10) 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 # Autoconnect to a host
if self.config['autoconnect']: if self.config['autoconnect']:
for host_config in self.connectionmanager.hostlist.config['hosts']:
def update_connection_manager(): host_id, host, port, user, __ = host_config
if not self.connectionmanager.running: if host_id == self.config['autoconnect_host_id']:
return log.debug('Trying to connect to %s@%s:%s', user, host, port)
self.connectionmanager.builder.get_object('button_refresh').emit('clicked') reactor.callLater(0.3, self.connectionmanager._connect, host_id, try_counter=6)
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
)
break break
if self.config['show_connection_manager_on_start']: if self.config['show_connection_manager_on_start']:
if windows_check(): # Dialog is blocking so call last.
# Call to simulate() required to workaround showing daemon status (see #2813)
reactor.simulate()
self.connectionmanager.show() self.connectionmanager.show()
def __on_disconnect(self): def __on_disconnect(self):

View file

@ -7,19 +7,19 @@
# See LICENSE for more details. # 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 from __future__ import unicode_literals
import logging import logging
import os
import time import time
from hashlib import sha1 from hashlib import sha1
from socket import gaierror, gethostbyname from socket import gaierror, gethostbyname
from twisted.internet import defer
from deluge.common import get_localhost_auth
from deluge.config import Config from deluge.config import Config
from deluge.configmanager import get_config_dir from deluge.configmanager import get_config_dir
from deluge.ui.client import Client, client
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -29,45 +29,12 @@ LOCALHOST = ('127.0.0.1', 'localhost')
def default_hostlist(): 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() host_id = sha1(str(time.time()).encode('utf8')).hexdigest()
username, password = get_localhost_auth() username, password = get_localhost_auth()
return {'hosts': [(host_id, DEFAULT_HOST, DEFAULT_PORT, username, password)]} 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): def validate_host_info(hostname, port):
"""Checks that hostname and port are valid. """Checks that hostname and port are valid.
@ -84,25 +51,25 @@ def validate_host_info(hostname, port):
except gaierror as ex: except gaierror as ex:
raise ValueError('Host %s: %s', hostname, ex.args[1]) raise ValueError('Host %s: %s', hostname, ex.args[1])
try: if not isinstance(port, int):
int(port)
except ValueError:
raise ValueError('Invalid port. Must be an integer') raise ValueError('Invalid port. Must be an integer')
def _migrate_config_1_to_2(config): def _migrate_config_1_to_2(config):
localclient_username, localclient_password = get_localhost_auth() """Mirgrates old hostlist config files to new format"""
if not localclient_username: localclient_username, localclient_password = get_localhost_auth()
# Nothing to do here, there's no auth file if not localclient_username:
return # Nothing to do here, there's no auth file
for idx, (__, host, __, username, __) in enumerate(config['hosts'][:]): return
if host in LOCALHOST and not username: for idx, (__, host, __, username, __) in enumerate(config['hosts'][:]):
config['hosts'][idx][3] = localclient_username if host in LOCALHOST and not username:
config['hosts'][idx][4] = localclient_password config['hosts'][idx][3] = localclient_username
return config config['hosts'][idx][4] = localclient_password
return config
class HostList(object): class HostList(object):
"""This class contains methods for adding, removing and looking up hosts in hostlist.conf."""
def __init__(self): def __init__(self):
self.config = Config('hostlist.conf', default_hostlist(), config_dir=get_config_dir(), file_version=2) 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) 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): def get_host_info(self, host_id):
"""Get the host details for 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']: for host_entry in self.config['hosts']:
if host_entry[0] == host_id: if host_entry[0] == host_id:
return host_entry return host_entry[0:4]
else: else:
return [] return []
def get_hosts_info(self): 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): def get_host_status(self, host_id):
"""Get all the hosts in the hostlist """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): 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: Args:
host_id (str): The host id to update. 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: if (not password and not username or username == 'localclient') and hostname in LOCALHOST:
username, password = get_localhost_auth() 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]: 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 True
return False return False
def remove_host(self, host_id): 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']: for host_entry in self.config['hosts']:
if host_id == host_entry[0]: if host_id == host_entry[0]:
self.config['hosts'].remove(host_entry) self.config['hosts'].remove(host_entry)
@ -209,3 +242,12 @@ class HostList(object):
def add_default_host(self): def add_default_host(self):
self.add_host(DEFAULT_HOST, DEFAULT_PORT, *get_localhost_auth()) 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'))

View file

@ -78,7 +78,7 @@ Deluge.AddConnectionWindow = Ext.extend(Ext.Window, {
onAddClick: function() { onAddClick: function() {
var values = this.form.getForm().getValues(); 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) { success: function(result) {
if (!result[0]) { if (!result[0]) {
Ext.MessageBox.show({ Ext.MessageBox.show({

View file

@ -91,6 +91,13 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, {
iconCls: 'icon-add', iconCls: 'icon-add',
handler: this.onAddClick, handler: this.onAddClick,
scope: this scope: this
}, {
id: 'cm-edit',
cls: 'x-btn-text-icon',
text: _('Edit'),
iconCls: 'icon-edit',
handler: this.onEditClick,
scope: this
}, { }, {
id: 'cm-remove', id: 'cm-remove',
cls: 'x-btn-text-icon', cls: 'x-btn-text-icon',
@ -164,27 +171,25 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, {
var button = this.buttons[1], status = record.get('status'); var button = this.buttons[1], status = record.get('status');
// Update the Connect/Disconnect button // Update the Connect/Disconnect button
if (status == 'Connected') { button.enable();
button.enable(); if (status.toLowerCase() == 'connected') {
button.setText(_('Disconnect')); button.setText(_('Disconnect'));
} else if (status == 'Offline') {
button.disable();
} else { } else {
button.enable();
button.setText(_('Connect')); button.setText(_('Connect'));
if (status.toLowerCase() != 'online') button.disable();
} }
// Update the Stop/Start Daemon button // 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') { if (record.get('host') == '127.0.0.1' || record.get('host') == 'localhost') {
this.stopHostButton.enable(); this.stopHostButton.enable();
this.stopHostButton.setText(_('Start Daemon')); this.stopHostButton.setText(_('Start Daemon'));
} else { } else {
this.stopHostButton.disable(); 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) { onAddClick: function(button, e) {
if (!this.addWindow) { if (!this.addWindow) {
this.addWindow = new Deluge.AddConnectionWindow(); this.addWindow = new Deluge.AddConnectionWindow();
this.addWindow.on('hostadded', this.onHostAdded, this); this.addWindow.on('hostadded', this.onHostChange, this);
} }
this.addWindow.show(); this.addWindow.show();
}, },
// private // 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(); this.loadHosts();
}, },
@ -212,7 +229,7 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, {
var selected = this.list.getSelectedRecords()[0]; var selected = this.list.getSelectedRecords()[0];
if (!selected) return; if (!selected) return;
if (selected.get('status') == 'Connected') { if (selected.get('status').toLowerCase() == 'connected') {
deluge.client.web.disconnect({ deluge.client.web.disconnect({
success: function(result) { success: function(result) {
this.update(this); this.update(this);
@ -248,8 +265,8 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, {
// private // private
onGetHostStatus: function(host) { onGetHostStatus: function(host) {
var record = this.list.getStore().getById(host[0]); var record = this.list.getStore().getById(host[0]);
record.set('status', host[3]) record.set('status', host[1])
record.set('version', host[4]) record.set('version', host[2])
record.commit(); record.commit();
if (this.list.getSelectedRecords()[0] == record) this.updateButtons(record); if (this.list.getSelectedRecords()[0] == record) this.updateButtons(record);
}, },
@ -312,11 +329,13 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, {
// private // private
onSelectionChanged: function(list, selections) { onSelectionChanged: function(list, selections) {
if (selections[0]) { if (selections[0]) {
this.editHostButton.enable();
this.removeHostButton.enable(); this.removeHostButton.enable();
this.stopHostButton.enable(); this.stopHostButton.enable();
this.stopHostButton.setText(_('Stop Daemon')); this.stopHostButton.setText(_('Stop Daemon'));
this.updateButtons(this.list.getRecord(selections[0])); this.updateButtons(this.list.getRecord(selections[0]));
} else { } else {
this.editHostButton.disable();
this.removeHostButton.disable(); this.removeHostButton.disable();
this.stopHostButton.disable(); this.stopHostButton.disable();
} }
@ -328,6 +347,7 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, {
if (!this.addHostButton) { if (!this.addHostButton) {
var bbar = this.panel.getBottomToolbar(); var bbar = this.panel.getBottomToolbar();
this.addHostButton = bbar.items.get('cm-add'); this.addHostButton = bbar.items.get('cm-add');
this.editHostButton = bbar.items.get('cm-edit');
this.removeHostButton = bbar.items.get('cm-remove'); this.removeHostButton = bbar.items.get('cm-remove');
this.stopHostButton = bbar.items.get('cm-stop'); this.stopHostButton = bbar.items.get('cm-stop');
} }

View file

@ -0,0 +1,114 @@
/*!
* Deluge.EditConnectionWindow.js
*
* Copyright (c) Damien Churchill 2009-2010 <damoxc@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.
*/
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();
}
});

View file

@ -164,7 +164,8 @@ class JSON(resource.Resource, component.Component):
except AuthError: except AuthError:
error = {'message': 'Not authenticated', 'code': 1} error = {'message': 'Not authenticated', 'code': 1}
except Exception as ex: 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} error = {'message': '%s: %s' % (ex.__class__.__name__, str(ex)), 'code': 3}
return request_id, result, error return request_id, result, error
@ -404,11 +405,10 @@ class WebApi(JSONComponent):
self.sessionproxy.stop() self.sessionproxy.stop()
return defer.succeed(True) 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 Connects the client to a daemon
""" """
d = client.connect(host, port, username, password)
def on_client_connected(connection_id): def on_client_connected(connection_id):
""" """
@ -420,7 +420,7 @@ class WebApi(JSONComponent):
self.start() self.start()
return d return d
return d.addCallback(on_client_connected) return self.hostlist.connect_host(host_id).addCallback(on_client_connected)
@export @export
def connect(self, host_id): def connect(self, host_id):
@ -432,10 +432,7 @@ class WebApi(JSONComponent):
:returns: the methods the daemon supports :returns: the methods the daemon supports
:rtype: list :rtype: list
""" """
host = self._get_host(host_id) return self._connect_daemon(host_id)
if host:
return self._connect_daemon(*host[1:])
return defer.fail(Exception('Bad host id'))
@export @export
def connected(self): def connected(self):
@ -716,7 +713,7 @@ class WebApi(JSONComponent):
Return the hosts in the hostlist. Return the hosts in the hostlist.
""" """
log.debug('get_hosts called') log.debug('get_hosts called')
return self.hostlist.get_hosts() + [''] return self.hostlist.get_hosts_info()
@export @export
def get_host_status(self, host_id): def get_host_status(self, host_id):
@ -725,46 +722,13 @@ class WebApi(JSONComponent):
:param host_id: the hash id of the host :param host_id: the hash id of the host
:type host_id: string :type host_id: string
""" """
def response(status, info=None): def response(result):
return host_id, host, port, status, info log.critical('%s', result)
return result
try: return self.hostlist.get_host_status(host_id).addCallback(response)
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
@export @export
def add_host(self, host, port, username='', password=''): def add_host(self, host, port, username='', password=''):
@ -787,15 +751,33 @@ class WebApi(JSONComponent):
else: else:
return True, host_id 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 @export
def remove_host(self, host_id): def remove_host(self, host_id):
"""Removes a host from the list. """Removes a host from the hostlist.
Args: Args:
host_id (str): The host identifying hash. host_id (str): The host identifying hash.
Returns: Returns:
bool: True if succesful, False otherwise. bool: True if succesful, False otherwise.
""" """
return self.hostlist.remove_host(host_id) return self.hostlist.remove_host(host_id)