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