[tor-commits] [stem/master] Integrating python prompt into interpretor

atagar at torproject.org atagar at torproject.org
Mon May 12 06:01:26 UTC 2014


commit 51160196e6b58d4d9bbb1cfd6eb318375300c7f4
Author: Damian Johnson <atagar at 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')





More information about the tor-commits mailing list