[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Integrating python prompt into interpretor
commit 51160196e6b58d4d9bbb1cfd6eb318375300c7f4
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
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')
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits