Beispiel #1
0
class WebApi(JSONComponent):
    """
    The component that implements all the methods required for managing
    the web interface. The complete web json interface also exposes all the
    methods available from the core RPC.
    """

    XSS_VULN_KEYS = ['name', 'message', 'comment', 'tracker_status', 'peers']

    def __init__(self):
        super(WebApi, self).__init__('Web', depend=['SessionProxy'])
        self.hostlist = HostList()
        self.core_config = CoreConfig()
        self.event_queue = EventQueue()
        try:
            self.sessionproxy = component.get('SessionProxy')
        except KeyError:
            self.sessionproxy = SessionProxy()

    def disable(self):
        client.deregister_event_handler('PluginEnabledEvent',
                                        self._json.get_remote_methods)
        client.deregister_event_handler('PluginDisabledEvent',
                                        self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').stop()
        else:
            client.disconnect()
            client.set_disconnect_callback(None)

    def enable(self):
        client.register_event_handler('PluginEnabledEvent',
                                      self._json.get_remote_methods)
        client.register_event_handler('PluginDisabledEvent',
                                      self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').start()
        else:
            client.set_disconnect_callback(self._on_client_disconnect)
            default_host_id = component.get(
                'DelugeWeb').config['default_daemon']
            if default_host_id:
                return self.connect(default_host_id)

        return defer.succeed(True)

    def _on_client_connect(self, *args):
        """Handles client successfully connecting to the daemon.

        Invokes retrieving the method names and starts webapi and plugins.

        """
        d_methods = self._json.get_remote_methods()
        component.get('Web.PluginManager').start()
        self.start()
        return d_methods

    def _on_client_connect_fail(self, result, host_id):
        log.error(
            'Unable to connect to daemon, check host_id "%s" is correct.',
            host_id)

    def _on_client_disconnect(self, *args):
        component.get('Web.PluginManager').stop()
        return self.stop()

    def start(self):
        self.core_config.start()
        return self.sessionproxy.start()

    def stop(self):
        self.core_config.stop()
        self.sessionproxy.stop()
        return defer.succeed(True)

    @export
    def connect(self, host_id):
        """Connect the web client to a daemon.

        Args:
            host_id (str): The id of the daemon in the host list.

        Returns:
            Deferred: List of methods the daemon supports.
        """
        d = self.hostlist.connect_host(host_id)
        d.addCallback(self._on_client_connect)
        d.addErrback(self._on_client_connect_fail, host_id)
        return d

    @export
    def connected(self):
        """
        The current connection state.

        :returns: True if the client is connected
        :rtype: boolean
        """
        return client.connected()

    @export
    def disconnect(self):
        """
        Disconnect the web interface from the connected daemon.
        """
        d = client.disconnect()

        def on_disconnect(reason):
            return str(reason)

        d.addCallback(on_disconnect)
        return d

    @export
    def update_ui(self, keys, filter_dict):
        """
        Gather the information required for updating the web interface.

        :param keys: the information about the torrents to gather
        :type keys: list
        :param filter_dict: the filters to apply when selecting torrents.
        :type filter_dict: dictionary
        :returns: The torrent and UI information.
        :rtype: dictionary
        """
        d = Deferred()
        ui_info = {
            'connected': client.connected(),
            'torrents': None,
            'filters': None,
            'stats': {
                'max_download':
                self.core_config.get('max_download_speed'),
                'max_upload':
                self.core_config.get('max_upload_speed'),
                'max_num_connections':
                self.core_config.get('max_connections_global'),
            },
        }

        if not client.connected():
            d.callback(ui_info)
            return d

        def got_stats(stats):
            ui_info['stats']['num_connections'] = stats['num_peers']
            ui_info['stats']['upload_rate'] = stats['payload_upload_rate']
            ui_info['stats']['download_rate'] = stats['payload_download_rate']
            ui_info['stats']['download_protocol_rate'] = (
                stats['download_rate'] - stats['payload_download_rate'])
            ui_info['stats']['upload_protocol_rate'] = (
                stats['upload_rate'] - stats['payload_upload_rate'])
            ui_info['stats']['dht_nodes'] = stats['dht_nodes']
            ui_info['stats']['has_incoming_connections'] = stats[
                'has_incoming_connections']

        def got_filters(filters):
            ui_info['filters'] = filters

        def got_free_space(free_space):
            ui_info['stats']['free_space'] = free_space

        def got_external_ip(external_ip):
            ui_info['stats']['external_ip'] = external_ip

        def got_torrents(torrents):
            ui_info['torrents'] = torrents

        def on_complete(result):
            d.callback(ui_info)

        d1 = component.get('SessionProxy').get_torrents_status(
            filter_dict, keys)
        d1.addCallback(got_torrents)

        d2 = client.core.get_filter_tree()
        d2.addCallback(got_filters)

        d3 = client.core.get_session_status([
            'num_peers',
            'payload_download_rate',
            'payload_upload_rate',
            'download_rate',
            'upload_rate',
            'dht_nodes',
            'has_incoming_connections',
        ])
        d3.addCallback(got_stats)

        d4 = client.core.get_free_space(
            self.core_config.get('download_location'))
        d4.addCallback(got_free_space)

        d5 = client.core.get_external_ip()
        d5.addCallback(got_external_ip)

        dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True)
        dl.addCallback(on_complete)
        return d

    def _on_got_files(self, torrent, d):
        files = torrent.get('files')
        file_progress = torrent.get('file_progress')
        file_priorities = torrent.get('file_priorities')

        paths = []
        info = {}
        for index, torrent_file in enumerate(files):
            path = xml_escape(torrent_file['path'])
            paths.append(path)
            torrent_file['progress'] = file_progress[index]
            torrent_file['priority'] = file_priorities[index]
            torrent_file['index'] = index
            torrent_file['path'] = path
            info[path] = torrent_file

            # update the directory info
            dirname = os.path.dirname(path)
            while dirname:
                dirinfo = info.setdefault(dirname, {})
                dirinfo['size'] = dirinfo.get('size', 0) + torrent_file['size']
                if 'priority' not in dirinfo:
                    dirinfo['priority'] = torrent_file['priority']
                else:
                    if dirinfo['priority'] != torrent_file['priority']:
                        dirinfo['priority'] = 9

                progresses = dirinfo.setdefault('progresses', [])
                progresses.append(torrent_file['size'] *
                                  torrent_file['progress'] / 100)
                dirinfo['progress'] = sum(progresses) / dirinfo['size'] * 100
                dirinfo['path'] = dirname
                dirname = os.path.dirname(dirname)

        def walk(path, item):
            if item['type'] == 'dir':
                item.update(info[path])
                return item
            else:
                item.update(info[path])
                return item

        file_tree = FileTree2(paths)
        file_tree.walk(walk)
        d.callback(file_tree.get_tree())

    def _on_torrent_status(self, torrent, d):
        for key in self.XSS_VULN_KEYS:
            try:
                if key == 'peers':
                    for peer in torrent[key]:
                        peer['client'] = xml_escape(peer['client'])
                else:
                    torrent[key] = xml_escape(torrent[key])
            except KeyError:
                pass
        d.callback(torrent)

    @export
    def get_torrent_status(self, torrent_id, keys):
        """Get the status for a torrent, filtered by status keys."""
        main_deferred = Deferred()
        d = component.get('SessionProxy').get_torrent_status(torrent_id, keys)
        d.addCallback(self._on_torrent_status, main_deferred)
        return main_deferred

    @export
    def get_torrent_files(self, torrent_id):
        """
        Gets the files for a torrent in tree format

        :param torrent_id: the id of the torrent to retrieve.
        :type torrent_id: string
        :returns: The torrents files in a tree
        :rtype: dictionary
        """
        main_deferred = Deferred()
        d = component.get('SessionProxy').get_torrent_status(
            torrent_id, FILES_KEYS)
        d.addCallback(self._on_got_files, main_deferred)
        return main_deferred

    @export
    def download_torrent_from_url(self, url, cookie=None):
        """
        Download a torrent file from a URL to a temporary directory.

        :param url: the URL of the torrent
        :type url: string
        :returns: the temporary file name of the torrent file
        :rtype: string
        """
        def on_download_success(result):
            log.debug('Successfully downloaded %s to %s', url, result)
            return result

        def on_download_fail(result):
            log.error('Failed to add torrent from url %s', url)
            return result

        tempdir = tempfile.mkdtemp(prefix='delugeweb-')
        tmp_file = os.path.join(tempdir, url.split('/')[-1])
        log.debug('filename: %s', tmp_file)
        headers = {}
        if cookie:
            headers['Cookie'] = cookie
            log.debug('cookie: %s', cookie)
        d = httpdownloader.download_file(url, tmp_file, headers=headers)
        d.addCallbacks(on_download_success, on_download_fail)
        return d

    @export
    def get_torrent_info(self, filename):
        """
        Return information about a torrent on the filesystem.

        :param filename: the path to the torrent
        :type filename: string

        :returns: information about the torrent:

        ::

            {
                "name": the torrent name,
                "files_tree": the files the torrent contains,
                "info_hash" the torrents info_hash
            }

        :rtype: dictionary
        """
        try:
            torrent_info = TorrentInfo(filename.strip(), 2)
            return torrent_info.as_dict('name', 'info_hash', 'files_tree')
        except Exception as ex:
            log.error(ex)
            return False

    @export
    def get_magnet_info(self, uri):
        """Parse a magnet URI for hash and name."""
        return get_magnet_info(uri)

    @export
    def add_torrents(self, torrents):
        """
        Add torrents by file

        :param torrents: A list of dictionaries containing the torrent \
            path and torrent options to add with.
        :type torrents: list

        ::

            json_api.web.add_torrents([{
                "path": "/tmp/deluge-web/some-torrent-file.torrent",
                "options": {"download_location": "/home/deluge/"}
            }])

        """
        deferreds = []

        for torrent in torrents:
            if is_magnet(torrent['path']):
                log.info(
                    'Adding torrent from magnet uri `%s` with options `%r`',
                    torrent['path'],
                    torrent['options'],
                )
                d = client.core.add_torrent_magnet(torrent['path'],
                                                   torrent['options'])
                deferreds.append(d)
            else:
                filename = os.path.basename(torrent['path'])
                with open(torrent['path'], 'rb') as _file:
                    fdump = b64encode(_file.read())
                log.info(
                    'Adding torrent from file `%s` with options `%r`',
                    filename,
                    torrent['options'],
                )
                d = client.core.add_torrent_file_async(filename, fdump,
                                                       torrent['options'])
                deferreds.append(d)
        return DeferredList(deferreds, consumeErrors=False)

    def _get_host(self, host_id):
        """Information about a host from supplied host id.

        Args:
            host_id (str): The id of the host.

        Returns:
            list: The host information, empty list if not found.

        """
        return list(self.hostlist.get_host_info(host_id))

    @export
    def get_hosts(self):
        """
        Return the hosts in the hostlist.
        """
        log.debug('get_hosts called')
        return self.hostlist.get_hosts_info()

    @export
    def get_host_status(self, host_id):
        """
        Returns the current status for the specified host.

        :param host_id: the hash id of the host
        :type host_id: string

        """
        def response(result):
            return result

        return self.hostlist.get_host_status(host_id).addCallback(response)

    @export
    def add_host(self, host, port, username='', password=''):
        """Adds a host to the list.

        Args:
            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:
            tuple: A tuple of (bool, str). If True will contain the host_id, otherwise
                if False will contain the error message.
        """
        try:
            host_id = self.hostlist.add_host(host, port, username, password)
        except ValueError as ex:
            return False, str(ex)
        else:
            return True, host_id

    @export
    def edit_host(self, host_id, host, port, username='', password=''):
        """Edit host details in the hostlist.

        Args:
            host_id (str): The host identifying hash.
            host (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            bool: True if successful, False otherwise.

        """
        return self.hostlist.update_host(host_id, host, port, username,
                                         password)

    @export
    def remove_host(self, host_id):
        """Removes a host from the hostlist.

        Args:
            host_id (str): The host identifying hash.

        Returns:
            bool: True if successful, False otherwise.

        """
        return self.hostlist.remove_host(host_id)

    @export
    def start_daemon(self, port):
        """
        Starts a local daemon.
        """
        client.start_daemon(port, get_config_dir())

    @export
    def stop_daemon(self, host_id):
        """
        Stops a running daemon.

        :param host_id: the hash id of the host
        :type host_id: string
        """
        main_deferred = Deferred()
        host = self._get_host(host_id)
        if not host:
            main_deferred.callback((False, _('Daemon does not exist')))
            return main_deferred

        try:

            def on_connect(connected, c):
                if not connected:
                    main_deferred.callback((False, _('Daemon not running')))
                    return
                c.daemon.shutdown()
                main_deferred.callback((True, ))

            def on_connect_failed(reason):
                main_deferred.callback((False, reason))

            host, port, user, password = host[1:5]
            c = Client()
            d = c.connect(host, port, user, password)
            d.addCallback(on_connect, c)
            d.addErrback(on_connect_failed)
        except Exception:
            main_deferred.callback((False, 'An error occurred'))
        return main_deferred

    @export
    def get_config(self):
        """
        Get the configuration dictionary for the web interface.

        :rtype: dictionary
        :returns: the configuration
        """
        config = component.get('DelugeWeb').config.config.copy()
        del config['sessions']
        del config['pwd_salt']
        del config['pwd_sha1']
        return config

    @export
    def set_config(self, config):
        """
        Sets the configuration dictionary for the web interface.

        :param config: The configuration options to update
        :type config: dictionary
        """
        web_config = component.get('DelugeWeb').config
        for key in config:
            if key in ['sessions', 'pwd_salt', 'pwd_sha1']:
                log.warning('Ignored attempt to overwrite web config key: %s',
                            key)
                continue
            web_config[key] = config[key]

    @export
    def get_plugins(self):
        """All available and enabled plugins within WebUI.

        Note:
            This does not represent all plugins from deluge.client.core.

        Returns:
            dict: A dict containing 'available_plugins' and 'enabled_plugins' lists.

        """

        return {
            'enabled_plugins':
            list(component.get('Web.PluginManager').plugins),
            'available_plugins':
            component.get('Web.PluginManager').available_plugins,
        }

    @export
    def get_plugin_info(self, name):
        """Get the details for a plugin."""
        return component.get('Web.PluginManager').get_plugin_info(name)

    @export
    def get_plugin_resources(self, name):
        """Get the resource data files for a plugin."""
        return component.get('Web.PluginManager').get_plugin_resources(name)

    @export
    def upload_plugin(self, filename, path):
        """Upload a plugin to config."""
        main_deferred = Deferred()

        shutil.copyfile(path,
                        os.path.join(get_config_dir(), 'plugins', filename))
        component.get('Web.PluginManager').scan_for_plugins()

        if client.is_localhost():
            client.core.rescan_plugins()
            return True
        with open(path, 'rb') as _file:
            plugin_data = b64encode(_file.read())

        def on_upload_complete(*args):
            client.core.rescan_plugins()
            component.get('Web.PluginManager').scan_for_plugins()
            main_deferred.callback(True)

        def on_upload_error(*args):
            main_deferred.callback(False)

        d = client.core.upload_plugin(filename, plugin_data)
        d.addCallback(on_upload_complete)
        d.addErrback(on_upload_error)
        return main_deferred

    @export
    def register_event_listener(self, event):
        """
        Add a listener to the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.add_listener(__request__.session_id, event)

    @export
    def deregister_event_listener(self, event):
        """
        Remove an event listener from the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.remove_listener(__request__.session_id, event)

    @export
    def get_events(self):
        """
        Retrieve the pending events for the session.
        """
        return self.event_queue.get_events(__request__.session_id)
Beispiel #2
0
class ConnectionManager(BaseMode, PopupsHandler):
    def __init__(self, stdscr, encoding=None):
        PopupsHandler.__init__(self)
        self.statuses = {}
        self.all_torrents = None
        self.hostlist = HostList()
        self.update_hosts_status()
        BaseMode.__init__(self, stdscr, encoding=encoding)
        self.update_select_host_popup()

    def update_select_host_popup(self):
        selected_index = self.popup.current_selection() if self.popup else None

        popup = SelectablePopup(self,
                                _('Select Host'),
                                self._host_selected,
                                border_off_west=1,
                                active_wrap=True)
        popup.add_header("{!white,black,bold!}'Q'=%s, 'a'=%s, 'D'=%s" %
                         (_('Quit'), _('Add Host'), _('Delete Host')),
                         space_below=True)
        self.push_popup(popup, clear=True)

        for host_entry in self.hostlist.get_hosts_info():
            host_id, hostname, port, user = host_entry
            args = {'data': host_id, 'foreground': 'red'}
            state = 'Offline'
            if host_id in self.statuses:
                state = 'Online'
                args.update({
                    'data': self.statuses[host_id],
                    'foreground': 'green'
                })
            host_str = '%s:%d [%s]' % (hostname, port, state)
            self.popup.add_line(host_id,
                                host_str,
                                selectable=True,
                                use_underline=True,
                                **args)

        if selected_index:
            self.popup.set_selection(selected_index)
        self.inlist = True
        self.refresh()

    def update_hosts_status(self):
        for host_entry in self.hostlist.get_hosts_info():

            def on_host_status(status_info):
                self.statuses[status_info[0]] = status_info
                self.update_select_host_popup()

            self.hostlist.get_host_status(
                host_entry[0]).addCallback(on_host_status)

    def _on_connected(self, result):
        def on_console_start(result):
            component.get('ConsoleUI').set_mode('TorrentList')

        d = component.get('ConsoleUI').start_console()
        d.addCallback(on_console_start)

    def _on_connect_fail(self, result):
        self.report_message('Failed to connect!', result)
        self.refresh()
        if hasattr(result, 'getTraceback'):
            log.exception(result)

    def _host_selected(self, selected_host, *args, **kwargs):
        if selected_host in self.statuses:
            d = self.hostlist.connect_host(selected_host)
            d.addCallback(self._on_connected)
            d.addErrback(self._on_connect_fail)

    def _do_add(self, result, **kwargs):
        if not result or kwargs.get('close', False):
            self.pop_popup()
        else:
            self.add_host(result['hostname']['value'], result['port']['value'],
                          result['username']['value'],
                          result['password']['value'])

    def add_popup(self):
        self.inlist = False
        popup = InputPopup(
            self,
            _('Add Host (Up & Down arrows to navigate, Esc to cancel)'),
            border_off_north=1,
            border_off_east=1,
            close_cb=self._do_add)
        popup.add_text_input('hostname', _('Hostname:'))
        popup.add_text_input('port', _('Port:'))
        popup.add_text_input('username', _('Username:'******'password', _('Password:'******'Adding host: %s', hostname)
        try:
            self.hostlist.add_host(hostname, port, username, password)
        except ValueError as ex:
            self.report_message(_('Error adding host'),
                                '%s: %s' % (hostname, ex))
        else:
            self.update_select_host_popup()

    def delete_host(self, host_id):
        log.info('Deleting host: %s', host_id)
        self.hostlist.remove_host(host_id)
        self.update_select_host_popup()

    @overrides(component.Component)
    def start(self):
        self.refresh()

    @overrides(component.Component)
    def update(self):
        self.update_hosts_status()

    @overrides(BaseMode)
    def pause(self):
        self.pop_popup()
        BaseMode.pause(self)

    @overrides(BaseMode)
    def resume(self):
        BaseMode.resume(self)
        self.refresh()

    @overrides(BaseMode)
    def refresh(self):
        if self.mode_paused():
            return

        self.stdscr.erase()
        self.draw_statusbars()
        self.stdscr.noutrefresh()

        if not self.popup:
            self.update_select_host_popup()

        self.popup.refresh()
        curses.doupdate()

    @overrides(BaseMode)
    def on_resize(self, rows, cols):
        BaseMode.on_resize(self, rows, cols)

        if self.popup:
            self.popup.handle_resize()

        self.stdscr.erase()
        self.refresh()

    @overrides(BaseMode)
    def read_input(self):
        c = self.stdscr.getch()

        if is_printable_chr(c):
            if chr(c) == 'Q':
                component.get('ConsoleUI').quit()
            elif self.inlist:
                if chr(c) == 'q':
                    return
                elif chr(c) == 'D':
                    host_id = self.popup.current_selection()[1]
                    self.delete_host(host_id)
                    return
                elif chr(c) == 'a':
                    self.add_popup()
                    return

        if self.popup:
            if self.popup.handle_read(c) and self.popup.closed():
                self.pop_popup()
            self.refresh()
Beispiel #3
0
class ConnectionManager(component.Component):
    def __init__(self):
        component.Component.__init__(self, 'ConnectionManager')
        self.gtkui_config = ConfigManager('gtkui.conf')
        self.hostlist = HostList()
        self.running = False

    # Component overrides
    def start(self):
        pass

    def stop(self):
        # Close this dialog when we are shutting down
        if self.running:
            self.connection_manager.response(gtk.RESPONSE_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('deluge.ui.gtkui',
                              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)

        # Create status pixbufs
        if not HOSTLIST_PIXBUFS:
            for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT):
                HOSTLIST_PIXBUFS.append(
                    self.connection_manager.render_icon(
                        stock_id, gtk.ICON_SIZE_MENU))

        # Setup the hostlist liststore and treeview
        self.treeview = self.builder.get_object('treeview_hostlist')
        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,
                                  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:
                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(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.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.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.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(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)
class ConnectionManager(component.Component):
    def __init__(self):
        component.Component.__init__(self, 'ConnectionManager')
        self.gtkui_config = ConfigManager('gtkui.conf')
        self.hostlist = HostList()
        self.running = False

    # Component overrides
    def start(self):
        pass

    def stop(self):
        # Close this dialog when we are shutting down
        if self.running:
            self.connection_manager.response(gtk.RESPONSE_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(
            'deluge.ui.gtkui', 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)

        # Create status pixbufs
        if not HOSTLIST_PIXBUFS:
            for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT):
                HOSTLIST_PIXBUFS.append(
                    self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU))

        # Setup the hostlist liststore and treeview
        self.treeview = self.builder.get_object('treeview_hostlist')
        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, 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:
                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(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.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.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.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(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)
class ConnectionManager(BaseMode, PopupsHandler):

    def __init__(self, stdscr, encoding=None):
        PopupsHandler.__init__(self)
        self.statuses = {}
        self.all_torrents = None
        self.hostlist = HostList()
        self.update_hosts_status()
        BaseMode.__init__(self, stdscr, encoding=encoding)
        self.update_select_host_popup()

    def update_select_host_popup(self):
        selected_index = self.popup.current_selection() if self.popup else None

        popup = SelectablePopup(
            self, _('Select Host'), self._host_selected, border_off_west=1, active_wrap=True)
        popup.add_header(
            "{!white,black,bold!}'Q'=%s, 'a'=%s, 'D'=%s" %
            (_('Quit'), _('Add Host'), _('Delete Host')), space_below=True)
        self.push_popup(popup, clear=True)

        for host_entry in self.hostlist.get_hosts_info():
            host_id, hostname, port, user = host_entry
            args = {'data': host_id, 'foreground': 'red'}
            state = 'Offline'
            if host_id in self.statuses:
                state = 'Online'
                args.update({'data': self.statuses[host_id], 'foreground': 'green'})
            host_str = '%s:%d [%s]' % (hostname, port, state)
            self.popup.add_line(host_id, host_str, selectable=True, use_underline=True, **args)

        if selected_index:
            self.popup.set_selection(selected_index)
        self.inlist = True
        self.refresh()

    def update_hosts_status(self):
        for host_entry in self.hostlist.get_hosts_info():
            def on_host_status(status_info):
                self.statuses[status_info[0]] = status_info
                self.update_select_host_popup()
            self.hostlist.get_host_status(host_entry[0]).addCallback(on_host_status)

    def _on_connected(self, result):
        def on_console_start(result):
            component.get('ConsoleUI').set_mode('TorrentList')
        d = component.get('ConsoleUI').start_console()
        d.addCallback(on_console_start)

    def _on_connect_fail(self, result):
        self.report_message('Failed to connect!', result)
        self.refresh()
        if hasattr(result, 'getTraceback'):
            log.exception(result)

    def _host_selected(self, selected_host, *args, **kwargs):
        if selected_host in self.statuses:
            d = self.hostlist.connect_host(selected_host)
            d.addCallback(self._on_connected)
            d.addErrback(self._on_connect_fail)

    def _do_add(self, result, **kwargs):
        if not result or kwargs.get('close', False):
            self.pop_popup()
        else:
            self.add_host(result['hostname']['value'], result['port']['value'],
                          result['username']['value'], result['password']['value'])

    def add_popup(self):
        self.inlist = False
        popup = InputPopup(
            self,
            _('Add Host (Up & Down arrows to navigate, Esc to cancel)'),
            border_off_north=1,
            border_off_east=1,
            close_cb=self._do_add)
        popup.add_text_input('hostname', _('Hostname:'))
        popup.add_text_input('port', _('Port:'))
        popup.add_text_input('username', _('Username:'******'password', _('Password:'******'Adding host: %s', hostname)
        try:
            self.hostlist.add_host(hostname, port, username, password)
        except ValueError as ex:
            self.report_message(_('Error adding host'), '%s: %s' % (hostname, ex))
        else:
            self.update_select_host_popup()

    def delete_host(self, host_id):
        log.info('Deleting host: %s', host_id)
        self.hostlist.remove_host(host_id)
        self.update_select_host_popup()

    @overrides(component.Component)
    def start(self):
        self.refresh()

    @overrides(component.Component)
    def update(self):
        self.update_hosts_status()

    @overrides(BaseMode)
    def pause(self):
        self.pop_popup()
        BaseMode.pause(self)

    @overrides(BaseMode)
    def resume(self):
        BaseMode.resume(self)
        self.refresh()

    @overrides(BaseMode)
    def refresh(self):
        if self.mode_paused():
            return

        self.stdscr.erase()
        self.draw_statusbars()
        self.stdscr.noutrefresh()

        if not self.popup:
            self.update_select_host_popup()

        self.popup.refresh()
        curses.doupdate()

    @overrides(BaseMode)
    def on_resize(self, rows, cols):
        BaseMode.on_resize(self, rows, cols)

        if self.popup:
            self.popup.handle_resize()

        self.stdscr.erase()
        self.refresh()

    @overrides(BaseMode)
    def read_input(self):
        c = self.stdscr.getch()

        if is_printable_chr(c):
            if chr(c) == 'Q':
                component.get('ConsoleUI').quit()
            elif self.inlist:
                if chr(c) == 'q':
                    return
                elif chr(c) == 'D':
                    host_id = self.popup.current_selection()[1]
                    self.delete_host(host_id)
                    return
                elif chr(c) == 'a':
                    self.add_popup()
                    return

        if self.popup:
            if self.popup.handle_read(c) and self.popup.closed():
                self.pop_popup()
            self.refresh()
Beispiel #6
0
class WebApi(JSONComponent):
    """
    The component that implements all the methods required for managing
    the web interface. The complete web json interface also exposes all the
    methods available from the core RPC.
    """

    def __init__(self):
        super(WebApi, self).__init__('Web', depend=['SessionProxy'])
        self.hostlist = HostList()
        self.core_config = CoreConfig()
        self.event_queue = EventQueue()
        try:
            self.sessionproxy = component.get('SessionProxy')
        except KeyError:
            self.sessionproxy = SessionProxy()

    def disable(self):
        client.deregister_event_handler('PluginEnabledEvent', self._json.get_remote_methods)
        client.deregister_event_handler('PluginDisabledEvent', self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').stop()
        else:
            client.disconnect()
            client.set_disconnect_callback(None)

    def enable(self):
        client.register_event_handler('PluginEnabledEvent', self._json.get_remote_methods)
        client.register_event_handler('PluginDisabledEvent', self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').start()
        else:
            client.set_disconnect_callback(self._on_client_disconnect)
            default_host_id = component.get('DelugeWeb').config['default_daemon']
            if default_host_id:
                return self.connect(default_host_id)

        return defer.succeed(True)

    def _on_client_connect(self, *args):
        """Handles client successfully connecting to the daemon.

        Invokes retrieving the method names and starts webapi and plugins.

        """
        d_methods = self._json.get_remote_methods()
        component.get('Web.PluginManager').start()
        self.start()
        return d_methods

    def _on_client_disconnect(self, *args):
        component.get('Web.PluginManager').stop()
        return self.stop()

    def start(self):
        self.core_config.start()
        return self.sessionproxy.start()

    def stop(self):
        self.core_config.stop()
        self.sessionproxy.stop()
        return defer.succeed(True)

    @export
    def connect(self, host_id):
        """Connect the web client to a daemon.

        Args:
            host_id (str): The id of the daemon in the host list.

        Returns:
            Deferred: List of methods the daemon supports.
        """
        return self.hostlist.connect_host(host_id).addCallback(self._on_client_connect)

    @export
    def connected(self):
        """
        The current connection state.

        :returns: True if the client is connected
        :rtype: booleon
        """
        return client.connected()

    @export
    def disconnect(self):
        """
        Disconnect the web interface from the connected daemon.
        """
        d = client.disconnect()

        def on_disconnect(reason):
            return str(reason)
        d.addCallback(on_disconnect)
        return d

    @export
    def update_ui(self, keys, filter_dict):
        """
        Gather the information required for updating the web interface.

        :param keys: the information about the torrents to gather
        :type keys: list
        :param filter_dict: the filters to apply when selecting torrents.
        :type filter_dict: dictionary
        :returns: The torrent and ui information.
        :rtype: dictionary
        """
        d = Deferred()
        ui_info = {
            'connected': client.connected(),
            'torrents': None,
            'filters': None,
            'stats': {
                'max_download': self.core_config.get('max_download_speed'),
                'max_upload': self.core_config.get('max_upload_speed'),
                'max_num_connections': self.core_config.get('max_connections_global')
            }
        }

        if not client.connected():
            d.callback(ui_info)
            return d

        def got_stats(stats):
            ui_info['stats']['num_connections'] = stats['num_peers']
            ui_info['stats']['upload_rate'] = stats['payload_upload_rate']
            ui_info['stats']['download_rate'] = stats['payload_download_rate']
            ui_info['stats']['download_protocol_rate'] = stats['download_rate'] - stats['payload_download_rate']
            ui_info['stats']['upload_protocol_rate'] = stats['upload_rate'] - stats['payload_upload_rate']
            ui_info['stats']['dht_nodes'] = stats['dht_nodes']
            ui_info['stats']['has_incoming_connections'] = stats['has_incoming_connections']

        def got_filters(filters):
            ui_info['filters'] = filters

        def got_free_space(free_space):
            ui_info['stats']['free_space'] = free_space

        def got_external_ip(external_ip):
            ui_info['stats']['external_ip'] = external_ip

        def got_torrents(torrents):
            ui_info['torrents'] = torrents

        def on_complete(result):
            d.callback(ui_info)

        d1 = component.get('SessionProxy').get_torrents_status(filter_dict, keys)
        d1.addCallback(got_torrents)

        d2 = client.core.get_filter_tree()
        d2.addCallback(got_filters)

        d3 = client.core.get_session_status([
            'num_peers',
            'payload_download_rate',
            'payload_upload_rate',
            'download_rate',
            'upload_rate',
            'dht_nodes',
            'has_incoming_connections'
        ])
        d3.addCallback(got_stats)

        d4 = client.core.get_free_space(self.core_config.get('download_location'))
        d4.addCallback(got_free_space)

        d5 = client.core.get_external_ip()
        d5.addCallback(got_external_ip)

        dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True)
        dl.addCallback(on_complete)
        return d

    def _on_got_files(self, torrent, d):
        files = torrent.get('files')
        file_progress = torrent.get('file_progress')
        file_priorities = torrent.get('file_priorities')

        paths = []
        info = {}
        for index, torrent_file in enumerate(files):
            path = torrent_file['path']
            paths.append(path)
            torrent_file['progress'] = file_progress[index]
            torrent_file['priority'] = file_priorities[index]
            torrent_file['index'] = index
            torrent_file['path'] = path
            info[path] = torrent_file

            # update the directory info
            dirname = os.path.dirname(path)
            while dirname:
                dirinfo = info.setdefault(dirname, {})
                dirinfo['size'] = dirinfo.get('size', 0) + torrent_file['size']
                if 'priority' not in dirinfo:
                    dirinfo['priority'] = torrent_file['priority']
                else:
                    if dirinfo['priority'] != torrent_file['priority']:
                        dirinfo['priority'] = 9

                progresses = dirinfo.setdefault('progresses', [])
                progresses.append(torrent_file['size'] * torrent_file['progress'] / 100)
                dirinfo['progress'] = sum(progresses) / dirinfo['size'] * 100
                dirinfo['path'] = dirname
                dirname = os.path.dirname(dirname)

        def walk(path, item):
            if item['type'] == 'dir':
                item.update(info[path])
                return item
            else:
                item.update(info[path])
                return item

        file_tree = FileTree2(paths)
        file_tree.walk(walk)
        d.callback(file_tree.get_tree())

    @export
    def get_torrent_status(self, torrent_id, keys):
        return component.get('SessionProxy').get_torrent_status(torrent_id, keys)

    @export
    def get_torrent_files(self, torrent_id):
        """
        Gets the files for a torrent in tree format

        :param torrent_id: the id of the torrent to retrieve.
        :type torrent_id: string
        :returns: The torrents files in a tree
        :rtype: dictionary
        """
        main_deferred = Deferred()
        d = component.get('SessionProxy').get_torrent_status(torrent_id, FILES_KEYS)
        d.addCallback(self._on_got_files, main_deferred)
        return main_deferred

    @export
    def download_torrent_from_url(self, url, cookie=None):
        """
        Download a torrent file from a url to a temporary directory.

        :param url: the url of the torrent
        :type url: string
        :returns: the temporary file name of the torrent file
        :rtype: string
        """

        def on_download_success(result):
            log.debug('Successfully downloaded %s to %s', url, result)
            return result

        def on_download_fail(result):
            log.error('Failed to add torrent from url %s', url)
            return result

        tempdir = tempfile.mkdtemp(prefix='delugeweb-')
        tmp_file = os.path.join(tempdir, url.split('/')[-1])
        log.debug('filename: %s', tmp_file)
        headers = {}
        if cookie:
            headers['Cookie'] = cookie
            log.debug('cookie: %s', cookie)
        d = httpdownloader.download_file(url, tmp_file, headers=headers)
        d.addCallbacks(on_download_success, on_download_fail)
        return d

    @export
    def get_torrent_info(self, filename):
        """
        Return information about a torrent on the filesystem.

        :param filename: the path to the torrent
        :type filename: string

        :returns: information about the torrent:

        ::

            {
                "name": the torrent name,
                "files_tree": the files the torrent contains,
                "info_hash" the torrents info_hash
            }

        :rtype: dictionary
        """
        try:
            torrent_info = TorrentInfo(filename.strip(), 2)
            return torrent_info.as_dict('name', 'info_hash', 'files_tree')
        except Exception as ex:
            log.error(ex)
            return False

    @export
    def get_magnet_info(self, uri):
        return get_magnet_info(uri)

    @export
    def add_torrents(self, torrents):
        """
        Add torrents by file

        :param torrents: A list of dictionaries containing the torrent \
            path and torrent options to add with.
        :type torrents: list

        ::

            json_api.web.add_torrents([{
                "path": "/tmp/deluge-web/some-torrent-file.torrent",
                "options": {"download_location": "/home/deluge/"}
            }])

        """
        deferreds = []

        for torrent in torrents:
            if is_magnet(torrent['path']):
                log.info('Adding torrent from magnet uri `%s` with options `%r`',
                         torrent['path'], torrent['options'])
                d = client.core.add_torrent_magnet(torrent['path'], torrent['options'])
                deferreds.append(d)
            else:
                filename = os.path.basename(torrent['path'])
                with open(torrent['path'], 'rb') as _file:
                    fdump = base64.encodestring(_file.read())
                log.info('Adding torrent from file `%s` with options `%r`',
                         filename, torrent['options'])
                d = client.core.add_torrent_file(filename, fdump, torrent['options'])
                deferreds.append(d)
        return DeferredList(deferreds, consumeErrors=False)

    def _get_host(self, host_id):
        """Information about a host from supplied host id.

        Args:
            host_id (str): The id of the host.

        Returns:
            list: The host information, empty list if not found.

        """
        return list(self.hostlist.get_host_info(host_id))

    @export
    def get_hosts(self):
        """
        Return the hosts in the hostlist.
        """
        log.debug('get_hosts called')
        return self.hostlist.get_hosts_info()

    @export
    def get_host_status(self, host_id):
        """
        Returns the current status for the specified host.

        :param host_id: the hash id of the host
        :type host_id: string

        """
        def response(result):
            return result

        return self.hostlist.get_host_status(host_id).addCallback(response)

    @export
    def add_host(self, host, port, username='', password=''):
        """Adds a host to the list.

        Args:
            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:
            tuple: A tuple of (bool, str). If True will contain the host_id, otherwise
                if False will contain the error message.
        """
        try:
            host_id = self.hostlist.add_host(host, port, username, password)
        except ValueError as ex:
            return False, str(ex)
        else:
            return True, host_id

    @export
    def edit_host(self, host_id, host, port, username='', password=''):
        """Edit host details in the hostlist.

        Args:
            host_id (str): The host identifying hash.
            host (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            bool: True if succesful, False otherwise.

        """
        return self.hostlist.update_host(host_id, host, port, username, password)

    @export
    def remove_host(self, host_id):
        """Removes a host from the hostlist.

        Args:
            host_id (str): The host identifying hash.

        Returns:
            bool: True if succesful, False otherwise.

        """
        return self.hostlist.remove_host(host_id)

    @export
    def start_daemon(self, port):
        """
        Starts a local daemon.
        """
        client.start_daemon(port, get_config_dir())

    @export
    def stop_daemon(self, host_id):
        """
        Stops a running daemon.

        :param host_id: the hash id of the host
        :type host_id: string
        """
        main_deferred = Deferred()
        host = self._get_host(host_id)
        if not host:
            main_deferred.callback((False, _('Daemon does not exist')))
            return main_deferred

        try:
            def on_connect(connected, c):
                if not connected:
                    main_deferred.callback((False, _('Daemon not running')))
                    return
                c.daemon.shutdown()
                main_deferred.callback((True, ))

            def on_connect_failed(reason):
                main_deferred.callback((False, reason))

            host, port, user, password = host[1:5]
            c = Client()
            d = c.connect(host, port, user, password)
            d.addCallback(on_connect, c)
            d.addErrback(on_connect_failed)
        except Exception:
            main_deferred.callback((False, 'An error occurred'))
        return main_deferred

    @export
    def get_config(self):
        """
        Get the configuration dictionary for the web interface.

        :rtype: dictionary
        :returns: the configuration
        """
        config = component.get('DelugeWeb').config.config.copy()
        del config['sessions']
        del config['pwd_salt']
        del config['pwd_sha1']
        return config

    @export
    def set_config(self, config):
        """
        Sets the configuration dictionary for the web interface.

        :param config: The configuration options to update
        :type config: dictionary
        """
        web_config = component.get('DelugeWeb').config
        for key in config:
            if key in ['sessions', 'pwd_salt', 'pwd_sha1']:
                log.warn('Ignored attempt to overwrite web config key: %s', key)
                continue
            web_config[key] = config[key]

    @export
    def get_plugins(self):
        """All available and enabled plugins within WebUI.

        Note:
            This does not represent all plugins from deluge.client.core.

        Returns:
            dict: A dict containing 'available_plugins' and 'enabled_plugins' lists.

        """

        return {
            'enabled_plugins': list(component.get('Web.PluginManager').plugins),
            'available_plugins': component.get('Web.PluginManager').available_plugins
        }

    @export
    def get_plugin_info(self, name):
        return component.get('Web.PluginManager').get_plugin_info(name)

    @export
    def get_plugin_resources(self, name):
        return component.get('Web.PluginManager').get_plugin_resources(name)

    @export
    def upload_plugin(self, filename, path):
        main_deferred = Deferred()

        shutil.copyfile(path, os.path.join(get_config_dir(), 'plugins', filename))
        component.get('Web.PluginManager').scan_for_plugins()

        if client.is_localhost():
            client.core.rescan_plugins()
            return True
        with open(path, 'rb') as _file:
            plugin_data = base64.encodestring(_file.read())

        def on_upload_complete(*args):
            client.core.rescan_plugins()
            component.get('Web.PluginManager').scan_for_plugins()
            main_deferred.callback(True)

        def on_upload_error(*args):
            main_deferred.callback(False)

        d = client.core.upload_plugin(filename, plugin_data)
        d.addCallback(on_upload_complete)
        d.addErrback(on_upload_error)
        return main_deferred

    @export
    def register_event_listener(self, event):
        """
        Add a listener to the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.add_listener(__request__.session_id, event)

    @export
    def deregister_event_listener(self, event):
        """
        Remove an event listener from the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.remove_listener(__request__.session_id, event)

    @export
    def get_events(self):
        """
        Retrieve the pending events for the session.
        """
        return self.event_queue.get_events(__request__.session_id)