commit 51160196e6b58d4d9bbb1cfd6eb318375300c7f4 Author: Damian Johnson atagar@torproject.org Date: Sun May 11 15:37:55 2014 -0700
Integrating python prompt into interpretor
Our prompt provided two modes: a python prompt or raw controller prompt. But why not combine the two?
We now provide a python interpretor (like IDLE), but with the following...
* A connection to Tor is available via our 'controller' variable.
* Events we've received via SETEVENTS is available via our 'events' variable.
* Controller commands we recognized are passed directly to Tor (like our prior prompt).
* Python support can be toggled via '/python'. If it's disabled this is effectively our prior prompt.
* Starting tor without prompting if it isn't running. Tor only takes a fraction of a second to bootstrap to a state we can use, so asking the user is more of a hassle than help. --- stem/interpretor/__init__.py | 48 ++++++++---------- stem/interpretor/arguments.py | 6 +-- stem/interpretor/commands.py | 94 +++++++++++++++++++++++++++--------- stem/interpretor/help.py | 2 +- stem/interpretor/settings.cfg | 76 ++++++++++++++--------------- test/unit/interpretor/arguments.py | 6 +-- test/unit/interpretor/commands.py | 3 +- 7 files changed, 130 insertions(+), 105 deletions(-)
diff --git a/stem/interpretor/__init__.py b/stem/interpretor/__init__.py index d095cda..643767d 100644 --- a/stem/interpretor/__init__.py +++ b/stem/interpretor/__init__.py @@ -8,7 +8,6 @@ features such as tab completion, history, and IRC-style functions (like /help).
__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
-import code import os import sys
@@ -70,11 +69,7 @@ def main(): print msg('msg.tor_unavailable') sys.exit(1) else: - user_input = raw_input(msg('msg.start_tor_prompt')) - print # extra newline - - if user_input.lower() not in ('y', 'yes'): - sys.exit() + print msg('msg.starting_tor')
stem.process.launch_tor_with_config( config = { @@ -100,25 +95,22 @@ def main(): sys.exit(1)
with controller: - if args.python_prompt: - console = code.InteractiveConsole({ - 'controller': controller, - 'stem': stem, - 'stem.control': stem.control, - }) - console.interact(msg('msg.python_banner', version = controller.get_info('version'))) - else: - autocompleter = stem.interpretor.autocomplete.Autocompleter(controller) - readline.parse_and_bind('tab: complete') - readline.set_completer(autocompleter.complete) - readline.set_completer_delims('\n') - - interpretor = stem.interpretor.commands.ControlInterpretor(controller) - - while True: - try: - user_input = raw_input(PROMPT) - print interpretor.run_command(user_input) - except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc: - print # move cursor to the following line - break + autocompleter = stem.interpretor.autocomplete.Autocompleter(controller) + readline.parse_and_bind('tab: complete') + readline.set_completer(autocompleter.complete) + readline.set_completer_delims('\n') + + interpretor = stem.interpretor.commands.ControlInterpretor(controller) + print msg('msg.startup_banner') + + while True: + try: + prompt = '... ' if interpretor.is_multiline_context else PROMPT + user_input = raw_input(prompt) + response = interpretor.run_command(user_input) + + if response is not None: + print response + except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc: + print # move cursor to the following line + break diff --git a/stem/interpretor/arguments.py b/stem/interpretor/arguments.py index 9c23e0f..38ef41f 100644 --- a/stem/interpretor/arguments.py +++ b/stem/interpretor/arguments.py @@ -17,16 +17,14 @@ DEFAULT_ARGS = { 'user_provided_port': False, 'control_socket': '/var/run/tor/control', 'user_provided_socket': False, - 'python_prompt': False, 'print_help': False, }
-OPT = 'i:s:ph' +OPT = 'i:s:h'
OPT_EXPANDED = [ 'interface=', 'socket=', - 'python', 'help', ]
@@ -70,8 +68,6 @@ def parse(argv): elif opt in ('-s', '--socket'): args['control_socket'] = arg args['user_provided_socket'] = True - elif opt in ('-p', '--python'): - args['python_prompt'] = True elif opt in ('-h', '--help'): args['print_help'] = True
diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py index a8a5492..4b90fce 100644 --- a/stem/interpretor/commands.py +++ b/stem/interpretor/commands.py @@ -2,14 +2,16 @@ Handles making requests and formatting the responses. """
+import code import re
import stem +import stem.control import stem.interpretor.help import stem.util.connection import stem.util.tor_tools
-from stem.interpretor import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, msg +from stem.interpretor import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg from stem.util.term import format
@@ -77,16 +79,30 @@ def _get_fingerprint(arg, controller): raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
-class ControlInterpretor(object): +class ControlInterpretor(code.InteractiveConsole): """ Handles issuing requests and providing nicely formed responses, with support for special irc style subcommands. """
def __init__(self, controller): - self._controller = controller self._received_events = []
+ code.InteractiveConsole.__init__(self, { + 'stem': stem, + 'stem.control': stem.control, + 'controller': controller, + 'events': self._received_events + }) + + self._controller = controller + self._run_python_commands = True + + # Indicates if we're processing a multiline command, such as conditional + # block or loop. + + self.is_multiline_context = False + def register_event(self, event): """ Adds the event to our buffer so it'll be in '/events' output. @@ -178,7 +194,31 @@ class ControlInterpretor(object):
return '\n'.join(lines)
- def run_command(self, command): + def do_python(self, arg): + """ + Performs the '/python' operation, toggling if we accept python commands or + not. + """ + + if not arg: + status = 'enabled' if self._run_python_commands else 'disabled' + return format('Python support is presently %s.' % status, *STANDARD_OUTPUT) + elif arg.lower() == 'enable': + self._run_python_commands = True + elif arg.lower() == 'disable': + self._run_python_commands = False + else: + return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT) + + if self._run_python_commands: + response = "Python support enabled, we'll now run non-interpretor commands as python." + else: + response = "Python support disabled, we'll now pass along all commands to tor." + + return format(response, *STANDARD_OUTPUT) + + @uses_settings + def run_command(self, command, config): """ Runs the given command. Requests starting with a '/' are special commands to the interpretor, and anything else is sent to the control port. @@ -195,8 +235,6 @@ class ControlInterpretor(object): if not self._controller.is_alive(): raise stem.SocketClosed()
- command = command.strip() - # Commands fall into three categories: # # * Interpretor commands. These start with a '/'. @@ -207,20 +245,24 @@ class ControlInterpretor(object): # # * Other tor commands. We pass these directly on to the control port.
- if ' ' in command: - cmd, arg = command.split(' ', 1) - else: - cmd, arg = command, '' + cmd, arg = command.strip(), '' + + if ' ' in cmd: + cmd, arg = cmd.split(' ', 1)
output = ''
if cmd.startswith('/'): + cmd = cmd.lower() + if cmd == '/quit': raise stem.SocketClosed() elif cmd == '/events': output = self.do_events(arg) elif cmd == '/info': output = self.do_info(arg) + elif cmd == '/python': + output = self.do_python(arg) elif cmd == '/help': output = self.do_help(arg) else: @@ -292,27 +334,31 @@ class ControlInterpretor(object): if arg: events = arg.split() self._controller.add_event_listener(self.register_event, *events) - output = format('Listing for %s events\n' % ', '.join(events), *STANDARD_OUTPUT) + output = format(msg('msg.listening_to_events', events = ', '.join(events)), *STANDARD_OUTPUT) else: - output = format('Disabled event listening\n', *STANDARD_OUTPUT) + output = format('Disabled event listening', *STANDARD_OUTPUT) except stem.ControllerError as exc: output = format(str(exc), *ERROR_OUTPUT) elif cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'): # provides a notice that multi-line controller input isn't yet implemented output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + elif cmd == 'QUIT': + self._controller.msg(command) + raise stem.SocketClosed() else: - try: - response = self._controller.msg(command) - - if cmd == 'QUIT': - raise stem.SocketClosed() - - output = format(str(response), *STANDARD_OUTPUT) - except stem.ControllerError as exc: - if isinstance(exc, stem.SocketClosed): - raise exc - else: - output = format(str(exc), *ERROR_OUTPUT) + is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events' + + if self._run_python_commands and not is_tor_command: + self.is_multiline_context = code.InteractiveConsole.push(self, command) + return + else: + try: + output = format(str(self._controller.msg(command)), *STANDARD_OUTPUT) + except stem.ControllerError as exc: + if isinstance(exc, stem.SocketClosed): + raise exc + else: + output = format(str(exc), *ERROR_OUTPUT)
output += '\n' # give ourselves an extra line before the next prompt
diff --git a/stem/interpretor/help.py b/stem/interpretor/help.py index b6b84fc..b7909a9 100644 --- a/stem/interpretor/help.py +++ b/stem/interpretor/help.py @@ -67,7 +67,7 @@ def _response(controller, arg, config): description = config.get('help.description.%s' % arg.lower(), '')
for line in description.splitlines(): - output += format(' ' + line + '\n', *STANDARD_OUTPUT) + output += format(' ' + line, *STANDARD_OUTPUT) + '\n'
output += '\n'
diff --git a/stem/interpretor/settings.cfg b/stem/interpretor/settings.cfg index 8782e66..f03ac76 100644 --- a/stem/interpretor/settings.cfg +++ b/stem/interpretor/settings.cfg @@ -11,61 +11,46 @@ msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
msg.help -|Interactive interpretor for Tor. This can provide you with either direct -|access to Tor's control interface or a python prompt connected to Tor... +|Interactive interpretor for Tor. This provides you with direct access +|to Tor's control interface via either python or direct requests. | -|-------------------------------------------------------------------------------- -| -| % prompt -| >>> GETINFO version -| 0.2.5.1-alpha-dev (git-245ecfff36c0cecc) -| -| >>> /help GETINFO -| [ description of tor's GETINFO command ] +| -i, --interface [ADDRESS:]PORT change control interface from {address}:{port} +| -s, --socket SOCKET_PATH attach using unix domain socket if present, +| SOCKET_PATH defaults to: {socket} +| -h, --help presents this help | -| >>> /quit + +msg.startup_banner +|Welcome to Stem's interpretor prompt. This provides you with direct access to +|Tor's control interface. | -|-------------------------------------------------------------------------------- +|This acts like a standard python interpretor with a Tor connection available +|via your 'controller' variable... | -| % prompt --python | >>> controller.get_info('version') | '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)' | -| >>> for port in controller.get_ports(stem.control.Listener.CONTROL): -| ... print 'you have a control port available on %s' % port -| ... -| you have a control port available on 9051 -| -| >>> quit() -| -|-------------------------------------------------------------------------------- +|You can also issue requests directly to Tor... | -| -i, --interface [ADDRESS:]PORT change control interface from {address}:{port} -| -s, --socket SOCKET_PATH attach using unix domain socket if present, -| SOCKET_PATH defaults to: {socket} -| -p, --python provides a python interpretor connected to -| Tor rather than attaching directly -| -h, --help presents this help -| - -msg.python_banner -|Welcome to Stem's interpretor prompt. You presently have a connection to Tor -|available via your 'controller' variable. For example... +| >>> GETINFO version +| 0.2.5.1-alpha-dev (git-245ecfff36c0cecc) | -| >>> print controller.get_info('version') -| {version} +|For more information run '/help'. |
msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
-msg.start_tor_prompt -|Tor isn't running. Would you like to start up an instance for our interpretor -|to connect to? +msg.starting_tor +|Tor isn't running. Starting a temporary Tor instance for our interpretor to +|interact with. This will have a minimal non-relaying configuration, and be +|shut down when you're done. | -|Tor will have a minimal, non-relaying configuration and be shut down when -|you're done. +|-------------------------------------------------------------------------------- | -|Start Tor (yes / no)? + +msg.listening_to_events +|Listening for {events} events. You can print events we've received with +|'/events', and also interact with them via the 'events' variable.
################# # OUTPUT OF /HELP # @@ -75,9 +60,10 @@ msg.start_tor_prompt
help.general |Interpretor commands include: -| /help - provides information for interpretor and tor commands/config options +| /help - provides information for interpretor and tor commands | /events - prints events that we've received | /info - general information for a relay +| /python - enable or disable support for running python commands | /quit - shuts down the interpretor | |Tor commands include: @@ -109,6 +95,7 @@ help.general help.usage HELP => /help [OPTION] help.usage EVENTS => /events [types] help.usage INFO => /info [relay fingerprint, nickname, or IP address] +help.usage PYTHON => /python [enable,disable] help.usage QUIT => /quit help.usage GETINFO => GETINFO OPTION help.usage GETCONF => GETCONF OPTION @@ -150,6 +137,13 @@ help.description.info |Provides general information for a relay that's currently in the consensus. |If no relay is specified then this provides information on ourselves.
+help.description.python +|Enables or disables support for running python commands. This determines how +|we treat commands this interpretor doesn't recognize... +| +|* If enabled then unrecognized commands are executed as python. +|* If disabled then unrecognized commands are passed along to tor. + help.description.quit |Terminates the interpretor.
diff --git a/test/unit/interpretor/arguments.py b/test/unit/interpretor/arguments.py index 1141877..ab835c4 100644 --- a/test/unit/interpretor/arguments.py +++ b/test/unit/interpretor/arguments.py @@ -22,9 +22,6 @@ class TestArgumentParsing(unittest.TestCase): args = parse(['--socket', '/tmp/my_socket']) self.assertEqual('/tmp/my_socket', args.control_socket)
- args = parse(['--python']) - self.assertEqual(True, args.python_prompt) - args = parse(['--help']) self.assertEqual(True, args.print_help)
@@ -32,8 +29,7 @@ class TestArgumentParsing(unittest.TestCase): args = parse(['-i', '1643']) self.assertEqual(1643, args.control_port)
- args = parse(['-ps', '~/.tor/socket']) - self.assertEqual(True, args.python_prompt) + args = parse(['-s', '~/.tor/socket']) self.assertEqual('~/.tor/socket', args.control_socket)
def test_that_we_reject_unrecognized_arguments(self): diff --git a/test/unit/interpretor/commands.py b/test/unit/interpretor/commands.py index 0466e30..1d03779 100644 --- a/test/unit/interpretor/commands.py +++ b/test/unit/interpretor/commands.py @@ -205,13 +205,14 @@ class TestInterpretorCommands(unittest.TestCase): controller = Mock() interpretor = ControlInterpretor(controller)
- self.assertEqual('\x1b[34mListing for BW events\n\x1b[0m\n', interpretor.run_command('SETEVENTS BW')) + self.assertEqual("\x1b[34mListening for BW events. You can print events we've received with\n'/events', and also interact with them via the 'events' variable.\x1b[0m\n", interpretor.run_command('SETEVENTS BW')) controller.add_event_listener.assert_called_with(interpretor.register_event, 'BW')
def test_raw_commands(self): controller = Mock() controller.msg.return_value = 'response' interpretor = ControlInterpretor(controller) + interpretor.do_python('disable')
self.assertEqual('\x1b[34mresponse\x1b[0m\n', interpretor.run_command('NEW_COMMAND spiffyness')) controller.msg.assert_called_with('NEW_COMMAND spiffyness')