diff --git a/deluge/ui/console/commander.py b/deluge/ui/console/commander.py new file mode 100644 index 000000000..5af4bb908 --- /dev/null +++ b/deluge/ui/console/commander.py @@ -0,0 +1,145 @@ +# +# commander.py +# +# Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from twisted.internet import defer, reactor +import deluge.component as component +from deluge.ui.client import client +from deluge.ui.console import UI_PATH +from colors import strip_colors + +import logging +log = logging.getLogger(__name__) + +class Commander: + def __init__(self, cmds): + self._commands = cmds + self.console = component.get("ConsoleUI") + + def write(self,line): + print(strip_colors(line)) + + def do_command(self, cmd): + """ + Processes a command. + + :param cmd: str, the command string + + """ + if not cmd: + return + cmd, _, line = cmd.partition(' ') + try: + parser = self._commands[cmd].create_parser() + except KeyError: + self.write("{!error!}Unknown command: %s" % cmd) + return + args = self._commands[cmd].split(line) + + # Do a little hack here to print 'command --help' properly + parser._print_help = parser.print_help + def print_help(f=None): + if self.interactive: + self.write(parser.format_help()) + else: + parser._print_help(f) + parser.print_help = print_help + + # Only these commands can be run when not connected to a daemon + not_connected_cmds = ["help", "connect", "quit"] + aliases = [] + for c in not_connected_cmds: + aliases.extend(self._commands[c].aliases) + not_connected_cmds.extend(aliases) + + if not client.connected() and cmd not in not_connected_cmds: + self.write("{!error!}Not connected to a daemon, please use the connect command first.") + return + + try: + options, args = parser.parse_args(args) + except Exception, e: + self.write("{!error!}Error parsing options: %s" % e) + return + + if not getattr(options, '_exit', False): + try: + ret = self._commands[cmd].handle(*args, **options.__dict__) + except Exception, e: + self.write("{!error!}" + str(e)) + log.exception(e) + import traceback + self.write("%s" % traceback.format_exc()) + return defer.succeed(True) + else: + return ret + + def exec_args(self,args,host,port,username,password): + def on_connect(result): + def on_started(result): + def on_started(result): + deferreds = [] + # If we have args, lets process them and quit + # allow multiple commands split by ";" + for arg in args.split(";"): + deferreds.append(defer.maybeDeferred(self.do_command, arg.strip())) + + def on_complete(result): + self.do_command("quit") + + dl = defer.DeferredList(deferreds).addCallback(on_complete) + + # We need to wait for the rpcs in start() to finish before processing + # any of the commands. + self.console.started_deferred.addCallback(on_started) + component.start().addCallback(on_started) + + def on_connect_fail(result): + from deluge.ui.client import DelugeRPCError + if isinstance(result.value,DelugeRPCError): + rm = result.value.exception_msg + else: + rm = result.getErrorMessage() + print "Could not connect to: %s:%d\n %s"%(host,port,rm) + self.do_command("quit") + + if host: + d = client.connect(host,port,username,password) + else: + d = client.connect() + d.addCallback(on_connect) + d.addErrback(on_connect_fail) + diff --git a/deluge/ui/console/main.py b/deluge/ui/console/main.py index cb252d3fb..cdfb35242 100644 --- a/deluge/ui/console/main.py +++ b/deluge/ui/console/main.py @@ -53,6 +53,7 @@ from deluge.ui.console.eventlog import EventLog #import screen import colors from deluge.ui.ui import _UI +from deluge.ui.console import UI_PATH log = logging.getLogger(__name__) @@ -62,11 +63,62 @@ class Console(_UI): def __init__(self): super(Console, self).__init__("console") + group = optparse.OptionGroup(self.parser, "Console Options","These options control how " + "the console connects to the daemon. These options will be " + "used if you pass a command, or if you have autoconnect " + "enabled for the console ui.") + + group.add_option("-d","--daemon",dest="daemon_addr", + action="store",type="str",default="127.0.0.1", + help="Set the address of the daemon to connect to." + " [default: %default]") + group.add_option("-p","--port",dest="daemon_port", + help="Set the port to connect to the daemon on. [default: %default]", + action="store",type="int",default=58846) + group.add_option("-u","--username",dest="daemon_user", + help="Set the username to connect to the daemon with. [default: %default]", + action="store",type="string") + group.add_option("-P","--password",dest="daemon_pass", + help="Set the password to connect to the daemon with. [default: %default]", + action="store",type="string") + self.parser.add_option_group(group) + + self.cmds = load_commands(os.path.join(UI_PATH, 'commands')) + class CommandOptionGroup(optparse.OptionGroup): + def __init__(self, parser, title, description=None, cmds = None): + optparse.OptionGroup.__init__(self,parser,title,description) + self.cmds = cmds + + def format_help(self, formatter): + result = formatter.format_heading(self.title) + formatter.indent() + if self.description: + result += "%s\n"%formatter.format_description(self.description) + for cname in self.cmds: + cmd = self.cmds[cname] + if cmd.interactive_only or cname in cmd.aliases: continue + allnames = [cname] + allnames.extend(cmd.aliases) + cname = "/".join(allnames) + result += formatter.format_heading(" - ".join([cname,cmd.__doc__])) + formatter.indent() + result += "%*s%s\n" % (formatter.current_indent, "", cmd.usage) + formatter.dedent() + formatter.dedent() + return result + cmd_group = CommandOptionGroup(self.parser, "Console Commands", + description="The following commands can be issued at the " + "command line. Commands should be quoted, so, for example, " + "to pause torrent with id 'abc' you would run: '%s " + "\"pause abc\"'"%os.path.basename(sys.argv[0]), + cmds=self.cmds) + self.parser.add_option_group(cmd_group) def start(self): super(Console, self).start() - - ConsoleUI(self.args) + ConsoleUI(self.args,self.cmds,(self.options.daemon_addr, + self.options.daemon_port,self.options.daemon_user, + self.options.daemon_pass)) def start(): Console().start() @@ -88,8 +140,61 @@ class OptionParser(optparse.OptionParser): raise Exception(msg) +class BaseCommand(object): + + usage = 'usage' + interactive_only = False + option_list = tuple() + aliases = [] + + def complete(self, text, *args): + return [] + def handle(self, *args, **options): + pass + + @property + def name(self): + return 'base' + + @property + def epilog(self): + return self.__doc__ + + def split(self, text): + if deluge.common.windows_check(): + text = text.replace('\\', '\\\\') + return shlex.split(text) + + def create_parser(self): + return OptionParser(prog = self.name, + usage = self.usage, + epilog = self.epilog, + option_list = self.option_list) + + +def load_commands(command_dir, exclude=[]): + def get_command(name): + return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')() + + try: + commands = [] + for filename in os.listdir(command_dir): + if filename.split('.')[0] in exclude or filename.startswith('_'): + continue + if not (filename.endswith('.py') or filename.endswith('.pyc')): + continue + cmd = get_command(filename.split('.')[len(filename.split('.')) - 2]) + aliases = [ filename.split('.')[len(filename.split('.')) - 2] ] + aliases.extend(cmd.aliases) + for a in aliases: + commands.append((a, cmd)) + return dict(commands) + except OSError, e: + return {} + + class ConsoleUI(component.Component): - def __init__(self, args=None): + def __init__(self, args=None, cmds = None, daemon = None): component.Component.__init__(self, "ConsoleUI", 2) # keep track of events for the log view @@ -114,6 +219,18 @@ class ConsoleUI(component.Component): if args: args = args[0] self.interactive = False + if not cmds: + print "Sorry, couldn't find any commands" + return + else: + self._commands = cmds + from commander import Commander + cmdr = Commander(cmds) + if daemon: + cmdr.exec_args(args,*daemon) + else: + cmdr.exec_args(args,None,None,None,None) + self.coreconfig = CoreConfig() if self.interactive and not deluge.common.windows_check(): @@ -155,6 +272,44 @@ class ConsoleUI(component.Component): reactor.run() + def start(self): + # Maintain a list of (torrent_id, name) for use in tab completion + if not self.interactive: + self.started_deferred = defer.Deferred() + self.torrents = [] + def on_session_state(result): + def on_torrents_status(torrents): + for torrent_id, status in torrents.items(): + self.torrents.append((torrent_id, status["name"])) + self.started_deferred.callback(True) + + client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status) + client.core.get_session_state().addCallback(on_session_state) + + + def match_torrent(self, string): + """ + Returns a list of torrent_id matches for the string. It will search both + torrent_ids and torrent names, but will only return torrent_ids. + + :param string: str, the string to match on + + :returns: list of matching torrent_ids. Will return an empty list if + no matches are found. + + """ + ret = [] + for tid, name in self.torrents: + if tid.startswith(string) or name.startswith(string): + ret.append(tid) + + return ret + + + def set_batch_write(self, batch): + # only kept for legacy reasons, don't actually do anything + pass + def set_mode(self, mode): reactor.removeReader(self.screen) self.screen = mode @@ -165,4 +320,7 @@ class ConsoleUI(component.Component): component.stop() def write(self, s): - self.events.append(s) + if self.interactive: + self.events.append(s) + else: + print colors.strip_colors(s)