commit 582bd8e556ea26cc1c7e2ba2aa3a6b8d859fdd2b Author: Damian Johnson atagar@torproject.org Date: Tue Dec 18 08:09:27 2012 -0800
Dropping the interpretor panel
Since introducing the interpretor panel to arm the program's been plagued with stability issues (https://trac.torproject.org/6439). I suspect that the root cause is related to the readline module somehow being imported, though I tried to keep that import away from the cli use cases.
The interpretor panel was a sweet idea and will live on as a separate application. However, as a part of arm it has been surprisingly unused (I've *NEVER* heard of someone making use of it). Oh well... --- README | 2 - armrc.sample | 1 - src/cli/__init__.py | 2 +- src/cli/controller.py | 5 - src/cli/interpretorPanel.py | 137 ------ src/starter.py | 14 +- src/util/__init__.py | 2 +- src/util/torInterpretor.py | 1065 ------------------------------------------- 8 files changed, 6 insertions(+), 1222 deletions(-)
diff --git a/README b/README index e5676f7..4d0b4c1 100644 --- a/README +++ b/README @@ -206,7 +206,6 @@ Layout: logPanel.py - (page 1) displays tor, arm, and stem events configPanel.py - (page 3) editor panel for the tor configuration torrcPanel.py - (page 4) displays torrc and validation - interpretorPanel.py - (page 5) interpretor for control port access
util/ __init__.py @@ -221,7 +220,6 @@ Layout: sysTools.py - helper for system calls, providing client side caching textInput.py - expands the capabilities of text input fields torConfig.py - functions for working with the torrc and config options - torInterpretor.py - provides a shell around raw control port access torTools.py - Stem wrapper, providing caching and derived information uiTools.py - helper functions for presenting the user interface
diff --git a/armrc.sample b/armrc.sample index 5fb9564..458fcbe 100644 --- a/armrc.sample +++ b/armrc.sample @@ -18,7 +18,6 @@ features.panels.show.log true features.panels.show.connection true features.panels.show.config true features.panels.show.torrc true -features.panels.show.interpretor true
# Read the proc contents directly instead of calling ps, netstat, and other # resolvers. This provides very sizable performance benefits (around 90% diff --git a/src/cli/__init__.py b/src/cli/__init__.py index fa23640..b0d6bd8 100644 --- a/src/cli/__init__.py +++ b/src/cli/__init__.py @@ -2,5 +2,5 @@ Panels, popups, and handlers comprising the arm user interface. """
-__all__ = ["configPanel", "controller", "headerPanel", "interpretorPanel", "logPanel", "popups", "torrcPanel", "wizard"] +__all__ = ["configPanel", "controller", "headerPanel", "logPanel", "popups", "torrcPanel", "wizard"]
diff --git a/src/cli/controller.py b/src/cli/controller.py index de77e5a..1f2bf2b 100644 --- a/src/cli/controller.py +++ b/src/cli/controller.py @@ -15,7 +15,6 @@ import cli.headerPanel import cli.logPanel import cli.configPanel import cli.torrcPanel -import cli.interpretorPanel import cli.graphing.graphPanel import cli.graphing.bandwidthStats import cli.graphing.connStats @@ -37,7 +36,6 @@ CONFIG = {"startup.events": "N3", "features.panels.show.connection": True, "features.panels.show.config": True, "features.panels.show.torrc": True, - "features.panels.show.interpretor": True, "features.redrawRate": 5, "features.refreshRate": 5, "features.confirmQuit": True, @@ -99,9 +97,6 @@ def initController(stdscr, startTime): if CONFIG["features.panels.show.torrc"]: pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC, config)])
- if CONFIG["features.panels.show.interpretor"]: - pagePanels.append([cli.interpretorPanel.InterpretorPanel(stdscr)]) - # initializes the controller ARM_CONTROLLER = Controller(stdscr, stickyPanels, pagePanels)
diff --git a/src/cli/interpretorPanel.py b/src/cli/interpretorPanel.py deleted file mode 100644 index 246636c..0000000 --- a/src/cli/interpretorPanel.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -Panel providing raw control port access with syntax hilighting, usage -information, tab completion, and other usability features. -""" - -import curses - -from util import panel, textInput, torInterpretor, torTools, uiTools - -USAGE_INFO = "to use this panel press enter" -PROMPT_LINE = [torInterpretor.PROMPT, (USAGE_INFO, torInterpretor.USAGE_FORMAT)] - -# lazy loaded mapping of interpretor attributes to curses formatting constants -FORMATS = {} - -def getFormat(formatAttr): - """ - Provides the curses drawing attributes for torInterpretor formats. - - Arguments: - formatAttr - list of formatting attributes - """ - - # initializes formats if they haven't yet been loaded - if not FORMATS: - for colorEnum in torInterpretor.Color.values(): - FORMATS[colorEnum] = uiTools.getColor(colorEnum.lower()) - - FORMATS[torInterpretor.Attr.BOLD] = curses.A_BOLD - FORMATS[torInterpretor.Attr.UNDERLINE] = curses.A_UNDERLINE - FORMATS[torInterpretor.Attr.HILIGHT] = curses.A_STANDOUT - - cursesFormatting = curses.A_NORMAL - - for attr in formatAttr: - cursesFormatting |= FORMATS.get(attr, curses.A_NORMAL) - - return cursesFormatting - -class InterpretorPanel(panel.Panel): - def __init__(self, stdscr): - panel.Panel.__init__(self, stdscr, "interpretor", 0) - self.interpretor = torInterpretor.ControlInterpretor() - self.inputCompleter = torInterpretor.TorControlCompleter() - self.isInputMode = False - self.scroll = 0 - - def prompt(self): - """ - Enables the interpretor, prompting for input until the user enters esc or - a blank line. - """ - - self.isInputMode = True - panel.CURSES_LOCK.acquire() - - while self.isInputMode: - self.redraw(True) - - # intercepts input so user can cycle through the history - validator = textInput.BasicValidator() - validator = textInput.HistoryValidator(list(reversed(self.interpretor.getBacklog())), validator) - validator = textInput.TabCompleter(self.inputCompleter.getMatches, validator) - - xOffset = len(torInterpretor.PROMPT[0]) - displayLength = len(self.interpretor.getDisplayContents(PROMPT_LINE)) - if displayLength > self.maxY - 1: - xOffset += 3 # offset for scrollbar - - inputLine = min(self.maxY - 1, displayLength) - inputFormat = getFormat(torInterpretor.INPUT_FORMAT) - input = self.getstr(inputLine, xOffset, "", inputFormat, validator = validator) - if input == None: input = "" - input, isDone = input.strip(), False - - if not input: - # terminate input when we get a blank line - isDone = True - else: - try: - self.interpretor.handleQuery(input) - except torInterpretor.InterpretorClosed: - # Makes our control connection check if its been closed or not - torTools.getConn().isAlive() - - isDone = True - - if isDone: - self.isInputMode = False - self.redraw(True) - - panel.CURSES_LOCK.release() - - def handleKey(self, key): - isKeystrokeConsumed = True - if uiTools.isSelectionKey(key): - self.prompt() - elif uiTools.isScrollKey(key) and not self.isInputMode: - pageHeight = self.getPreferredSize()[0] - 1 - displayLength = len(self.interpretor.getDisplayContents(PROMPT_LINE)) - newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, displayLength) - - if self.scroll != newScroll: - self.scroll = newScroll - self.redraw(True) - else: isKeystrokeConsumed = False - - return isKeystrokeConsumed - - def draw(self, width, height): - # page title - usageMsg = " (enter "/help" for usage or a blank line to stop)" if self.isInputMode else "" - self.addstr(0, 0, "Control Interpretor%s:" % usageMsg, curses.A_STANDOUT) - - xOffset = 0 - displayContents = self.interpretor.getDisplayContents(PROMPT_LINE) - if len(displayContents) > height - 1: - # if we're in input mode then make sure the last line is visible - if self.isInputMode: - self.scroll = len(displayContents) - height + 1 - - xOffset = 3 - self.addScrollBar(self.scroll, self.scroll + height - 1, len(displayContents), 1) - - # draws prior commands and output - drawLine = 1 - for entry in displayContents[self.scroll:]: - cursor = xOffset - - for msg, formatEntry in entry: - format = getFormat(formatEntry) - self.addstr(drawLine, cursor, msg, format) - cursor += len(msg) - - drawLine += 1 - if drawLine >= height: break - diff --git a/src/starter.py b/src/starter.py index 01a6086..2b5063e 100644 --- a/src/starter.py +++ b/src/starter.py @@ -25,7 +25,6 @@ import util.panel import util.procTools import util.sysTools import util.torConfig -import util.torInterpretor import util.torTools import util.uiTools
@@ -55,8 +54,8 @@ CONFIG = {"startup.controlPassword": None, "log.configDescriptions.persistance.saveFailed": util.log.NOTICE, "log.savingDebugLog": util.log.NOTICE}
-OPT = "gpi:s:c:dbe:vh" -OPT_EXPANDED = ["prompt", "interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"] +OPT = "gi:s:c:dbe:vh" +OPT_EXPANDED = ["interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"]
HELP_MSG = """Usage arm [OPTION] Terminal status monitor for Tor relays. @@ -298,7 +297,6 @@ def _dumpConfig(): if __name__ == '__main__': startTime = time.time() param = dict([(key, None) for key in CONFIG.keys()]) - launchPrompt = False isDebugMode = False configPath = DEFAULT_CONFIG # path used for customized configuration
@@ -329,7 +327,6 @@ if __name__ == '__main__': param["startup.interface.port"] = controlPort elif opt in ("-s", "--socket"): param["startup.interface.socket"] = arg - elif opt in ("-p", "--prompt"): launchPrompt = True elif opt in ("-c", "--config"): configPath = arg # sets path of user's config elif opt in ("-d", "--debug"): isDebugMode = True # dumps all logs elif opt in ("-b", "--blind"): @@ -388,7 +385,7 @@ if __name__ == '__main__': # - tor is running (otherwise it would be kinda confusing, "tor is running # but why does arm say that it's shut down?")
- if launchPrompt or util.torTools.isTorRunning(): + if util.torTools.isTorRunning(): config.set("features.allowDetachedStartup", "false")
# revises defaults to match user's configuration @@ -514,8 +511,5 @@ if __name__ == '__main__': if util.uiTools.isUnicodeAvailable(): locale.setlocale(locale.LC_ALL, "")
- if launchPrompt: - util.torInterpretor.showPrompt() - else: - cli.controller.startTorMonitor(time.time() - initTime) + cli.controller.startTorMonitor(time.time() - initTime)
diff --git a/src/util/__init__.py b/src/util/__init__.py index 2b8f928..973527e 100644 --- a/src/util/__init__.py +++ b/src/util/__init__.py @@ -4,5 +4,5 @@ application's status, making cross platform system calls, parsing tor data, and safely working with curses (hiding some of the gory details). """
-__all__ = ["conf", "connections", "enum", "gtkTools", "hostnames", "log", "panel", "procTools", "procName", "sysTools", "textInput", "torConfig", "torInterpretor", "torTools", "uiTools"] +__all__ = ["conf", "connections", "enum", "gtkTools", "hostnames", "log", "panel", "procTools", "procName", "sysTools", "textInput", "torConfig", "torTools", "uiTools"]
diff --git a/src/util/torInterpretor.py b/src/util/torInterpretor.py deleted file mode 100644 index 5023045..0000000 --- a/src/util/torInterpretor.py +++ /dev/null @@ -1,1065 +0,0 @@ -""" -Provides an interactive interpretor for working with the Tor control port. This -adds usability features like IRC style interpretor commands and, when ran -directly, history and tab completion. -""" - -import re -import sys - -import version - -import stem - -from util import connections, enum, hostnames, torConfig, torTools, uiTools - -COLOR_PROMPT = True # provides a colored interpretor prompt -INFO_HOSTNAMES = False # hostname lookups in /info results - -# initial location /write will save to when no path is specified -DEFAULT_WRITE_PATH = "/tmp/torInterpretor_output" - -INIT_MSG = """Arm %s Control Interpretor -Enter "/help" for usage information and "/quit" to stop. -""" % version.VERSION - -TERM_COLORS = ("BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE") - -Color = enum.Enum(*TERM_COLORS) -BgColor = enum.Enum(*["BG_" + color for color in TERM_COLORS]) -Attr = enum.Enum("BOLD", "UNDERLINE", "HILIGHT") - -FG_ENCODING = dict([(Color.values()[i], str(30 + i)) for i in range(8)]) -BG_ENCODING = dict([(BgColor.values()[i], str(40 + i)) for i in range(8)]) -ATTR_ENCODING = {Attr.BOLD: "1", Attr.UNDERLINE: "4", Attr.HILIGHT: "7"} - -PROMPT = (">>> ", (Attr.BOLD, Color.GREEN)) -INPUT_FORMAT = (Color.CYAN, ) -INPUT_INTERPRETOR_FORMAT = (Attr.BOLD, Color.MAGENTA) -INPUT_CMD_FORMAT = (Attr.BOLD, Color.GREEN) -INPUT_ARG_FORMAT = (Attr.BOLD, Color.CYAN) -OUTPUT_FORMAT = (Color.BLUE, ) -USAGE_FORMAT = (Color.CYAN, ) -HELP_FORMAT = (Color.MAGENTA, ) -ERROR_FORMAT = (Attr.BOLD, Color.RED) - -CSI = "\x1B[%sm" -RESET = CSI % "0" - -# limits used for cropping -BACKLOG_LIMIT = 100 -EVENTS_LIMIT = 200 -CONTENT_LIMIT = 20000 - -MULTILINE_UNIMPLEMENTED_NOTICE = "Multi-line control options like this are not yet implemented." - -GENERAL_HELP = """Interpretor commands include: - /help - provides information for interpretor and tor commands/config options - /info - general information for a relay - /find - searches backlog for lines with the given regex - /events - prints events that we've received - /write - saves backlog to a given location - /quit - shuts down the interpretor - -Tor commands include: - GETINFO - queries information from tor - GETCONF, SETCONF, RESETCONF - show or edit a configuration option - SIGNAL - issues control signal to the process (for resetting, stopping, etc) - SETEVENTS - configures the events tor will notify us of - - USEFEATURE - enables custom behavior for the controller - SAVECONF - writes tor's current configuration to our torrc - LOADCONF - loads the given input like it was part of our torrc - MAPADDRESS - replaces requests for one address with another - POSTDESCRIPTOR - adds a relay descriptor to our cache - EXTENDCIRCUIT - create or extend a tor circuit - SETCIRCUITPURPOSE - configures the purpose associated with a circuit - CLOSECIRCUIT - closes the given circuit - ATTACHSTREAM - associates an application's stream with a tor circuit - REDIRECTSTREAM - sets a stream's destination - CLOSESTREAM - closes the given stream - RESOLVE - issues an asynchronous dns or rdns request over tor - TAKEOWNERSHIP - instructs tor to quit when this control connection is closed - PROTOCOLINFO - queries version and controller authentication information - QUIT - disconnect the control connection - -For more information use '/help [OPTION]'.""" - -HELP_HELP = """Provides usage information for the given interpretor, tor command, or tor -configuration option. - -Example: - /help info # provides a description of the '/info' option - /help GETINFO # usage information for tor's GETINFO controller option - /help ExitPolicy # description of tor's ExitPolicy configuration option""" - -HELP_WRITE = """Writes the interpretor's backlog to the given path. If no location is -specified then this saves to the last path specified (initially '%s').""" % DEFAULT_WRITE_PATH - -HELP_EVENTS = """Provides events that we've received belonging to the given event types. If -no types are specified then this provides all the messages that we've -received.""" - -HELP_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_FIND = """Searches the backlog for lines matching a given regular expression pattern. -Results are deduplicated and the matching portions bolded.""" - -HELP_QUIT = """Terminates the interpretor.""" - -HELP_GETINFO = """Queries the tor process for information. Options are... -""" - -HELP_GETCONF = """Provides the current value for a given configuration value. Options include... -""" - -HELP_SETCONF = """Sets the given configuration parameters. Values can be quoted or non-quoted -strings, and reverts the option to 0 or NULL if not provided. - -Examples: - * Sets a contact address and resets our family to NULL - SETCONF MyFamily ContactInfo=foo@bar.com - - * Sets an exit policy that only includes port 80/443 - SETCONF ExitPolicy="accept *:80, accept *:443, reject *:*"""" - -HELP_RESETCONF = """Reverts the given configuration options to their default values. If a value -is provided then this behaves in the same way as SETCONF. - -Examples: - * Returns both of our accounting parameters to their defaults - RESETCONF AccountingMax AccountingStart - - * Uses the default exit policy and sets our nickname to be 'Goomba' - RESETCONF ExitPolicy Nickname=Goomba""" - -HELP_SIGNAL = """Issues a signal that tells the tor process to reload its torrc, dump its -stats, halt, etc. -""" - -SIGNAL_DESCRIPTIONS = ( - ("RELOAD / HUP", "reload our torrc"), - ("SHUTDOWN / INT", "gracefully shut down, waiting 30 seconds if we're a relay"), - ("DUMP / USR1", "logs information about open connections and circuits"), - ("DEBUG / USR2", "makes us log at the DEBUG runlevel"), - ("HALT / TERM", "immediately shut down"), - ("CLEARDNSCACHE", "clears any cached DNS results"), - ("NEWNYM", "clears the DNS cache and uses new circuits for future connections") -) - -HELP_SETEVENTS = """Sets the events that we will receive. This turns off any events that aren't -listed so sending 'SETEVENTS' without any values will turn off all event reporting. - -For Tor versions between 0.1.1.9 and 0.2.2.1 adding 'EXTENDED' causes some -events to give us additional information. After version 0.2.2.1 this is -always on. - -Events include... -""" - -HELP_USEFEATURE = """Customizes the behavior of the control port. Options include... -""" - -HELP_SAVECONF = """Writes Tor's current configuration to its torrc.""" - -HELP_LOADCONF = """Reads the given text like it belonged to our torrc. - -Example: - +LOADCONF - # sets our exit policy to just accept ports 80 and 443 - ExitPolicy accept *:80 - ExitPolicy accept *:443 - ExitPolicy reject *:* - .""" - -HELP_MAPADDRESS = """Replaces future requests for one address with another. - -Example: - MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net""" - -HELP_POSTDESCRIPTOR = """Simulates getting a new relay descriptor.""" - -HELP_EXTENDCIRCUIT = """Extends the given circuit or create a new one if the CircuitID is zero. The -PATH is a comma separated list of fingerprints. If it isn't set then this -uses Tor's normal path selection.""" - -HELP_SETCIRCUITPURPOSE = """Sets the purpose attribute for a circuit.""" - -HELP_CLOSECIRCUIT = """Closes the given circuit. If "IfUnused" is included then this only closes -the circuit if it isn't currently being used.""" - -HELP_ATTACHSTREAM = """Attaches a stream with the given built circuit (tor picks one on its own if -CircuitID is zero). If HopNum is given then this hop is used to exit the -circuit, otherwise the last relay is used.""" - -HELP_REDIRECTSTREAM = """Sets the destination for a given stream. This can only be done after a -stream is created but before it's attached to a circuit.""" - -HELP_CLOSESTREAM = """Closes the given stream, the reason being an integer matching a reason as -per section 6.3 of the tor-spec.""" - -HELP_RESOLVE = """Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if -"mode=reverse" is included. This request is processed in the background and -results in a ADDRMAP event with the response.""" - -HELP_TAKEOWNERSHIP = """Instructs Tor to gracefully shut down when this control connection is closed.""" - -HELP_PROTOCOLINFO = """Provides bootstrapping information that a controller might need when first -starting, like Tor's version and controller authentication. This can be done -before authenticating to the control port.""" - -HELP_OPTIONS = { - "HELP": ("/help [OPTION]", HELP_HELP), - "WRITE": ("/write [PATH]", HELP_WRITE), - "EVENTS": ("/events [types]", HELP_EVENTS), - "INFO": ("/info [relay fingerprint, nickname, or IP address]", HELP_INFO), - "FIND": ("/find PATTERN", HELP_FIND), - "QUIT": ("/quit", HELP_QUIT), - "GETINFO": ("GETINFO OPTION", HELP_GETINFO), - "GETCONF": ("GETCONF OPTION", HELP_GETCONF), - "SETCONF": ("SETCONF PARAM[=VALUE]", HELP_SETCONF), - "RESETCONF": ("RESETCONF PARAM[=VALUE]", HELP_RESETCONF), - "SIGNAL": ("SIGNAL SIG", HELP_SIGNAL), - "SETEVENTS": ("SETEVENTS [EXTENDED] [EVENTS]", HELP_SETEVENTS), - "USEFEATURE": ("USEFEATURE OPTION", HELP_USEFEATURE), - "SAVECONF": ("SAVECONF", HELP_SAVECONF), - "LOADCONF": ("LOADCONF...", HELP_LOADCONF), - "MAPADDRESS": ("MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR", HELP_MAPADDRESS), - "POSTDESCRIPTOR": ("POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...", HELP_POSTDESCRIPTOR), - "EXTENDCIRCUIT": ("EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]", HELP_EXTENDCIRCUIT), - "SETCIRCUITPURPOSE": ("SETCIRCUITPURPOSE CircuitID purpose=general/controller", HELP_SETCIRCUITPURPOSE), - "CLOSECIRCUIT": ("CLOSECIRCUIT CircuitID [IfUnused]", HELP_CLOSECIRCUIT), - "ATTACHSTREAM": ("ATTACHSTREAM StreamID CircuitID [HOP=HopNum]", HELP_ATTACHSTREAM), - "REDIRECTSTREAM": ("REDIRECTSTREAM StreamID Address [Port]", HELP_REDIRECTSTREAM), - "CLOSESTREAM": ("CLOSESTREAM StreamID Reason [Flag]", HELP_CLOSESTREAM), - "RESOLVE": ("RESOLVE [mode=reverse] address", HELP_RESOLVE), - "TAKEOWNERSHIP": ("TAKEOWNERSHIP", HELP_TAKEOWNERSHIP), - "PROTOCOLINFO": ("PROTOCOLINFO [ProtocolVersion]", HELP_PROTOCOLINFO), -} - -class InterpretorClosed(Exception): - """ - Exception raised when the interpretor should be shut down. - """ - - pass - -def format(msg, *attr): - """ - Simple terminal text formatting, using ANSI escape sequences from: - https://secure.wikimedia.org/wikipedia/en/wiki/ANSI_escape_code#CSI_codes - - toolkits providing similar capabilities: - * django.utils.termcolors - https://code.djangoproject.com/browser/django/trunk/django/utils/termcolors.... - - * termcolor - http://pypi.python.org/pypi/termcolor - - * colorama - http://pypi.python.org/pypi/colorama - - Arguments: - msg - string to be formatted - attr - text attributes, this can be Color, BgColor, or Attr enums and are - case insensitive (so strings like "red" are fine) - """ - - encodings = [] - for textAttr in attr: - textAttr, encoding = enum.toCamelCase(textAttr), None - encoding = FG_ENCODING.get(textAttr, encoding) - encoding = BG_ENCODING.get(textAttr, encoding) - encoding = ATTR_ENCODING.get(textAttr, encoding) - if encoding: encodings.append(encoding) - - if encodings: - return (CSI % ";".join(encodings)) + msg + RESET - else: return msg - -class TorControlCompleter: - """ - Command autocompleter, fetching the valid options from the attached Tor - instance. - """ - - def __init__(self): - self._prefix = None - self._prefixMatches = [] - - self.commands = [] - conn = torTools.getConn() - - # adds all of the valid GETINFO options - infoOptions = conn.getInfo("info/names", None) - if infoOptions: - for line in infoOptions.split("\n"): - if " " in line: - # skipping non-existant options mentioned in: - # https://trac.torproject.org/projects/tor/ticket/3844 - - if line.startswith("config/*") or line.startswith("dir-usage"): - continue - - infoOpt = line.split(" ", 1)[0] - - # strips off the ending asterisk if it accepts a value - if infoOpt.endswith("*"): infoOpt = infoOpt[:-1] - - self.commands.append("GETINFO %s" % infoOpt) - else: self.commands.append("GETINFO ") - - # adds all of the valid GETCONF / SETCONF / RESETCONF options - confOptions = conn.getInfo("config/names", None) - if confOptions: - # individual options are '<name> <type>' pairs - confEntries = [opt.split(" ", 1)[0] for opt in confOptions.split("\n")] - self.commands += ["GETCONF %s" % conf for conf in confEntries] - self.commands += ["SETCONF %s " % conf for conf in confEntries] - self.commands += ["RESETCONF %s" % conf for conf in confEntries] - else: - self.commands.append("GETCONF ") - self.commands.append("SETCONF ") - self.commands.append("RESETCONF ") - - # adds all of the valid SETEVENTS options - eventOptions = conn.getInfo("events/names", None) - if eventOptions: - self.commands += ["SETEVENTS %s" % event for event in eventOptions.split(" ")] - else: self.commands.append("SETEVENTS ") - - # adds all of the valid USEFEATURE options - featureOptions = conn.getInfo("features/names", None) - if featureOptions: - self.commands += ["USEFEATURE %s" % feature for feature in featureOptions.split(" ")] - else: self.commands.append("USEFEATURE ") - - # adds all of the valid SIGNAL options - # this can't yet be fetched dynamically, as per: - # https://trac.torproject.org/projects/tor/ticket/3842 - - signals = ("RELOAD", "SHUTDOWN", "DUMP", "DEBUG", "HALT", "HUP", "INT", - "USR1", "USR2", "TERM", "NEWNYM", "CLEARDNSCACHE") - self.commands += ["SIGNAL %s" % sig for sig in signals] - - # shouldn't use AUTHENTICATE since we only provide the prompt with an - # authenticated controller connection - #self.commands.append("AUTHENTICATE") - - # other options - self.commands.append("SAVECONF") - self.commands.append("MAPADDRESS ") - self.commands.append("EXTENDCIRCUIT ") - self.commands.append("SETCIRCUITPURPOSE ") - #self.commands.append("SETROUTERPURPOSE ") # deprecated option - self.commands.append("ATTACHSTREAM ") - self.commands.append("+POSTDESCRIPTOR ") # TODO: needs to support multiline options for this (ugg) - self.commands.append("REDIRECTSTREAM ") - self.commands.append("CLOSESTREAM ") - self.commands.append("CLOSECIRCUIT ") - self.commands.append("RESOLVE ") - self.commands.append("PROTOCOLINFO ") - self.commands.append("+LOADCONF") # TODO: another multiline... - self.commands.append("TAKEOWNERSHIP") - self.commands.append("QUIT") # TODO: give a confirmation when the user does this? - - # adds interpretor commands - for cmd in HELP_OPTIONS: - if HELP_OPTIONS[cmd][0].startswith("/"): - self.commands.append("/" + cmd.lower()) - - # adds help options for the previous commands - baseCmd = set([cmd.split(" ")[0].replace("+", "").replace("/", "") for cmd in self.commands]) - for cmd in baseCmd: - self.commands.append("/help " + cmd) - - # adds /help for tor configuration options - for opt in torConfig.getConfigOptions(): - self.commands.append("/help " + opt) - - def getMatches(self, text): - """ - Provides all options that match the given input. This is case insensetive. - - Arguments: - text - user input text to be matched against - """ - - return [cmd for cmd in self.commands if cmd.lower().startswith(text.lower())] - - def complete(self, text, state): - """ - Provides case insensetive autocompletion options, acting as a functor for - the readlines set_completer function. - """ - - if text != self._prefix: - self._prefix = text - self._prefixMatches = self.getMatches(text) - - # Tab completion fills in the first common prefix which can be - # problematic if they don't all match. For instance completing "Map" will - # result in ["MAPADDRESS", "MapAddress"], which gets truncated to the - # common prefix of just "M" when the user presses tab. - - if len(self._prefixMatches) > 1: - prefixToResult = {} - - for match in self._prefixMatches: - prefix = match[:len(text)] - - if prefix in prefixToResult: - prefixToResult[prefix].append(match) - else: - prefixToResult[prefix] = [match] - - if len(prefixToResult) > 1: - # we have multiple prefixes, pick the one with the most results - self._prefixMatches = None - - for matchSet in prefixToResult.values(): - if not self._prefixMatches or len(self._prefixMatches) < len(matchSet): - self._prefixMatches = matchSet - - if state < len(self._prefixMatches): - return self._prefixMatches[state] - else: return None - -class ControlInterpretor: - """ - Interpretor that handles queries to the control port, providing usability - imporvements like irc style help optoins. This tracks input and responses. - """ - - def __init__(self): - self.backlog = [] # prior requests the user has made - self.contents = [] # (msg, format list) tuples for what's been displayed - self.writePath = DEFAULT_WRITE_PATH # last location we've saved to - self.eventBuffer = [] # unread event messages - self.loggedEvents = [] # event types that we're listening for - - # TODO: Dropping event listening since... - # a. it was a complete hack - # b. said hack isn't worth porting to stem - # c. I'm likely about to separate the interpretor from arm anyway - - #torTools.getConn().addEventListener(TorEventObserver(self.registerEvent)) - - def registerEvent(self, event): - """ - Adds the event to our buffer so it'll be in '/events' output. - """ - - if event.type in self.loggedEvents: - self.eventBuffer.append(event) - - eventsCrop = len(self.eventBuffer) - EVENTS_LIMIT - if eventsCrop > 0: self.eventBuffer = self.eventBuffer[eventsCrop:] - - def getBacklog(self): - """ - Provides the backlog of prior user input. - """ - - return self.backlog - - def getDisplayContents(self, appendPrompt = None): - """ - Provides a list of lines to be displayed, each being a list of (msg, - format) tuples for the content to be displayed. This is ordered as the - oldest to newest. - - Arguments: - appendPrompt - adds the given line to the end - """ - - if appendPrompt: - return self.contents + [appendPrompt] - else: return self.contents - - def doHelp(self, arg, outputEntry): - """ - Performs the '/help' operation, giving usage information for the given - argument or a general summary if there wasn't one. - """ - - arg = arg.upper() - - # If there's multiple arguments then just take the first. This is - # particularly likely if they're trying to query a full command (for - # instance "/help GETINFO version") - - arg = arg.split(" ")[0] - - # strip slash if someone enters an interpretor command (ex. "/help /help") - if arg.startswith("/"): arg = arg[1:] - - if arg: - if arg in HELP_OPTIONS: - # Provides information for the tor or interpretor argument. This bolds - # the usage information and indents the description after it. - usage, description = HELP_OPTIONS[arg] - - outputEntry.append((usage + "\n", OUTPUT_FORMAT + (Attr.BOLD, ))) - - for line in description.split("\n"): - outputEntry.append((" " + line + "\n", OUTPUT_FORMAT)) - - if arg == "GETINFO": - # if this is the GETINFO option then also list the valid options - infoOptions = torTools.getConn().getInfo("info/names", None) - - if infoOptions: - for line in infoOptions.split("\n"): - if line.startswith("config/*") or line.startswith("dir-usage"): - continue - - lineMatch = re.match("^(.+) -- (.+)$", line) - - if lineMatch: - opt, description = lineMatch.groups() - - outputEntry.append(("%-33s" % opt, OUTPUT_FORMAT + (Attr.BOLD, ))) - outputEntry.append((" - %s\n" % description, OUTPUT_FORMAT)) - elif arg == "GETCONF": - # lists all of the configuration options - - confOptions = torTools.getConn().getInfo("config/names", None) - if confOptions: - confEntries = [opt.split(" ", 1)[0] for opt in confOptions.split("\n")] - - # displays two columns of 42 characters - for i in range(0, len(confEntries), 2): - lineEntries = confEntries[i : i+2] - - lineContent = "" - for entry in lineEntries: - lineContent += "%-42s" % entry - - outputEntry.append((lineContent + "\n", OUTPUT_FORMAT)) - - outputEntry.append(("For more information use '/help [CONFIG OPTION]'.", OUTPUT_FORMAT + (Attr.BOLD, ))) - elif arg == "SIGNAL": - # lists descriptions for all of the signals - for signal, description in SIGNAL_DESCRIPTIONS: - outputEntry.append(("%-15s" % signal, OUTPUT_FORMAT + (Attr.BOLD, ))) - outputEntry.append((" - %s\n" % description, OUTPUT_FORMAT)) - elif arg == "SETEVENTS": - # lists all of the event types - eventOptions = torTools.getConn().getInfo("events/names", None) - if eventOptions: - eventEntries = eventOptions.split() - - # displays four columns of 20 characters - for i in range(0, len(eventEntries), 4): - lineEntries = eventEntries[i : i+4] - - lineContent = "" - for entry in lineEntries: - lineContent += "%-20s" % entry - - outputEntry.append((lineContent + "\n", OUTPUT_FORMAT)) - elif arg == "USEFEATURE": - # lists the feature options - featureOptions = torTools.getConn().getInfo("features/names", None) - if featureOptions: - outputEntry.append((featureOptions + "\n", OUTPUT_FORMAT)) - elif arg in ("LOADCONF", "POSTDESCRIPTOR"): - # gives a warning that this option isn't yet implemented - outputEntry.append(("\n" + MULTILINE_UNIMPLEMENTED_NOTICE + "\n", ERROR_FORMAT)) - else: - # check if this is a configuration option - manEntry = torConfig.getConfigDescription(arg) - - if manEntry: - # provides basic usage information in bold, followed an indented - # copy of the man page description (wrapped to eighty characters) - - helpTitle = "%s %s (category: %s)\n" % (manEntry.option, manEntry.argUsage, manEntry.category) - outputEntry.append((helpTitle, OUTPUT_FORMAT + (Attr.BOLD, ))) - - descLines = manEntry.description.split("\n") - - for line in descLines: - if not line: - outputEntry.append(("\n", OUTPUT_FORMAT)) - else: - while line: - drawPortion, line = uiTools.cropStr(line, 88, 4, 4, uiTools.Ending.HYPHEN, True) - outputEntry.append((" %s\n" % drawPortion.strip(), OUTPUT_FORMAT)) - else: - outputEntry.append(("No help information available for '%s'..." % arg, ERROR_FORMAT)) - else: - # provides the GENERAL_HELP with everything bolded except descriptions - for line in GENERAL_HELP.split("\n"): - cmdStart = line.find(" - ") - - if cmdStart != -1: - outputEntry.append((line[:cmdStart], OUTPUT_FORMAT + (Attr.BOLD, ))) - outputEntry.append((line[cmdStart:] + "\n", OUTPUT_FORMAT)) - else: - outputEntry.append((line + "\n", OUTPUT_FORMAT + (Attr.BOLD, ))) - - def doEvents(self, arg, outputEntry): - """ - Performs the '/events' operation, dumping the events that we've received - belonging to the given types. If no types are specified then this provides - all buffered events. - """ - - bufferedEvents = self.eventBuffer - - # if we have an argument then restrict it to those event types - eventTypes = arg.upper().split() - if eventTypes: - bufferedEvents = [event for event in self.eventBuffer if event.type in eventTypes] - - for event in bufferedEvents: - outputEntry.append((event.getDisplayMessage() + "\n", OUTPUT_FORMAT)) - - def doWrite(self, arg, outputEntry): - """ - Performs the '/write' operation, which attempts to save the backlog to a - given path, defaulting to the last location we write to. - """ - - if arg: self.writePath = arg - outputLines = [] - - for line in self.contents: - outputLines.append("".join([msg for msg, _ in line])) - - try: - outputFile = open(self.writePath, "w") - outputFile.write("\n".join(outputLines)) - outputFile.close() - outputEntry.append(("Interpretor backlog written to: %s" % self.writePath, OUTPUT_FORMAT)) - except IOError, exc: - outputEntry.append(("Unable to write to '%s': %s" % (self.writePath, exc), ERROR_FORMAT)) - - def doFind(self, arg, outputEntry): - """ - Performs the '/find' operation, which lists output from the backlog which - matches the given regex. Results are deduplicated and matches are bolded. - """ - - argMatcher = None - - if not arg: - outputEntry.append(("Nothing to match against", ERROR_FORMAT)) - else: - try: argMatcher = re.compile("(%s)" % arg) - except: outputEntry.append(("Unable to compile regex '%s'" % arg, ERROR_FORMAT)) - - if argMatcher: - printedLines = [] - - for line in self.contents: - lineText = "".join([msg for msg, _ in line]) - - # skip if this was user input or a duplicate - if lineText.startswith(PROMPT[0]) or lineText in printedLines: - continue - - match = argMatcher.search(lineText) - if match: - # outputs the matching line, with the match itself bolded - outputEntry.append((lineText[:match.start()], OUTPUT_FORMAT)) - outputEntry.append((match.group(), (OUTPUT_FORMAT + (Attr.BOLD, )))) - outputEntry.append((lineText[match.end():] + "\n", OUTPUT_FORMAT)) - printedLines.append(lineText) - - def doInfo(self, arg, outputEntry): - """ - Performs the '/info' operation, looking up a relay by fingerprint, IP - address, or nickname and printing its descriptor and consensus entries in a - pretty fashion. - """ - - fingerprint, conn = None, torTools.getConn() - - # TODO: also recognize <ip>:<port> entries? - - # determines the fingerprint, leaving it unset and adding an error message - # if unsuccessful - if not arg: - # uses our fingerprint if we're a relay, otherwise gives an error - fingerprint = conn.getInfo("fingerprint", None) - - if not fingerprint: - outputEntry.append(("We aren't a relay, no information to provide", ERROR_FORMAT)) - elif len(arg) == 40 and re.match("^[0-9a-fA-F]+$", arg): - # we got a fingerprint (fourty character hex string) - fingerprint = arg - elif connections.isValidIpAddress(arg): - # we got an ip address, look up the fingerprint - fpMatches = conn.getRelayFingerprint(arg, getAllMatches = True) - - if len(fpMatches) == 0: - outputEntry.append(("No relays found at %s" % arg, ERROR_FORMAT)) - elif len(fpMatches) == 1: - fingerprint = fpMatches[0][1] - else: - outputEntry.append(("Multiple relays at %s, specify which by giving a port" % arg, ERROR_FORMAT)) - - for i in range(len(fpMatches)): - relayEntry = outputEntry[i] - outputEntry.append((" %i. or port: %-5s fingerprint: %s" % (i + 1, relayEntry[0], relayEntry[1]), ERROR_FORMAT)) - else: - # we got something else, treat it as a nickname - fingerprint = conn.getNicknameFingerprint(arg) - - if not fingerprint: - outputEntry.append(("No relay with the nickname of '%s' found" % arg, ERROR_FORMAT)) - - if fingerprint: - consensusEntry = conn.getConsensusEntry(fingerprint) - - # The nickname, address, and port lookups are all based on the consensus - # entry so if this succeeds we should be pretty confident that those - # queries will work too. - - if not consensusEntry: - outputEntry.append(("Unable to find consensus information for %s" % fingerprint, ERROR_FORMAT)) - return - - address, port = conn.getRelayAddress(fingerprint, (None, None)) - - # ... but not sure enough that we won't check - if not address or not port: return - - locale = conn.getInfo("ip-to-country/%s" % address, "??") - - if INFO_HOSTNAMES: - hostname = hostnames.resolve(address, 10) - else: - hostname = None - - # TODO: Most of the following is copied from the _getDetailContent method - # of cli/connections/connEntry.py - useful bits should be refactored. - consensusLines = consensusEntry.split("\n") - - firstLineComp = consensusLines[0].split(" ") - if len(firstLineComp) >= 9: - _, nickname, _, _, pubDate, pubTime, _, orPort, _ = firstLineComp[:9] - else: nickname, pubDate, pubTime, orPort = "", "", "", "" - - flags = "unknown" - if len(consensusLines) >= 2 and consensusLines[1].startswith("s "): - flags = consensusLines[1][2:] - - exitPolicy = conn.getRelayExitPolicy(fingerprint) - - if exitPolicy: policyLabel = exitPolicy.getSummary() - else: policyLabel = "unknown" - - # fetches information from the descriptor if it's available - torVersion, platform, contact = "", "", "" - descriptorEntry = conn.getDescriptorEntry(fingerprint) - - if descriptorEntry: - for descLine in descriptorEntry.split("\n"): - if descLine.startswith("platform"): - # has the tor version and platform, ex: - # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64 - - torVersion = descLine[13:descLine.find(" ", 13)] - platform = descLine[descLine.rfind(" on ") + 4:] - elif descLine.startswith("contact"): - contact = descLine[8:] - - # clears up some highly common obscuring - for alias in (" at ", " AT "): contact = contact.replace(alias, "@") - for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".") - - break # contact lines come after the platform - - headingAttr, infoAttr = (Attr.BOLD, Color.BLUE), () - - outputEntry.append(("%s (%s)\n" % (nickname, fingerprint), infoAttr)) - - hostnameLabel = ", %s" % hostname if hostname else "" - outputEntry.append(("address: ", headingAttr)) - outputEntry.append(("%s:%s (%s%s)\n" % (address, port, locale, hostnameLabel), infoAttr)) - - outputEntry.append(("published: ", headingAttr)) - outputEntry.append(("%s %s" % (pubTime, pubDate) + "\n", infoAttr)) - - if torVersion and platform: - outputEntry.append(("os: ", headingAttr)) - outputEntry.append((platform + "\n", infoAttr)) - - outputEntry.append(("version: ", headingAttr)) - outputEntry.append((torVersion + "\n", infoAttr)) - - outputEntry.append(("flags: ", headingAttr)) - outputEntry.append((flags.replace(" ", ", ") + "\n", infoAttr)) - - outputEntry.append(("exit policy: ", headingAttr)) - outputEntry.append((policyLabel + "\n", infoAttr)) - - if contact: - outputEntry.append(("contact: ", headingAttr)) - outputEntry.append((contact + "\n", infoAttr)) - - def handleQuery(self, input): - """ - Processes the given input. Requests starting with a '/' are special - commands to the interpretor, and anything else is sent to the control port. - This returns an input/output tuple, each entry being a list of lines, each - line having a list of (msg, format) tuples for the content to be displayed. - This raises a InterpretorClosed if the interpretor should be shut down. - - Arguments: - input - user input to be processed - """ - - conn = torTools.getConn() - - # abort if the control connection has been severed - if not conn.isAlive(): - raise InterpretorClosed("Control connection has been closed") - - input = input.strip() - - # appends new input, cropping if too long - self.backlog.append(input) - backlogCrop = len(self.backlog) - BACKLOG_LIMIT - if backlogCrop > 0: self.backlog = self.backlog[backlogCrop:] - - inputEntry, outputEntry = [PROMPT], [] - - # input falls into three general categories: - # - interpretor command which starts with a '/' - # - controller commands handled by torTools (this allows for caching, - # proper handling by the rest of arm, etc) - # - unrecognized controller command, this has the possability of confusing - # arm... - - if " " in input: cmd, arg = input.split(" ", 1) - else: cmd, arg = input, "" - - if cmd.startswith("/"): - # interpretor command - inputEntry.append((input, INPUT_INTERPRETOR_FORMAT)) - - if cmd == "/quit": raise InterpretorClosed() - elif cmd == "/help": self.doHelp(arg, outputEntry) - elif cmd == "/write": self.doWrite(arg, outputEntry) - elif cmd == "/find": self.doFind(arg, outputEntry) - elif cmd == "/events": self.doEvents(arg, outputEntry) - elif cmd == "/info": self.doInfo(arg, outputEntry) - else: - outputEntry.append(("Not yet implemented...", ERROR_FORMAT)) # TODO: implement - - # appends a newline so all interpretor commands have a blank before the prompt - if outputEntry: - lastEntry = outputEntry[-1] - outputEntry[-1] = (lastEntry[0].rstrip() + "\n", lastEntry[1]) - - # TODO: add /help option - else: - # controller command - cmd = cmd.upper() # makes commands uppercase to match the spec - - inputEntry.append((cmd + " ", INPUT_CMD_FORMAT)) - if arg: inputEntry.append((arg, INPUT_ARG_FORMAT)) - - if cmd == "GETINFO": - try: - for param in arg.split(): - response = conn.getInfo(param) - outputEntry.append((response + "\n", OUTPUT_FORMAT)) - except Exception, exc: - outputEntry.append((str(exc), ERROR_FORMAT)) - elif cmd == "SETCONF" or cmd == "RESETCONF": - # arguments can either be '<param>', '<param>=<value>', or - # '<param>="<value>"' entries - paramList = [] - - while arg: - # TODO: I'm a little dubious of this for LineList values (like the - # ExitPolicy) since they're parsed as a single value. However, tor - # seems to be happy to get a single comma separated string (though it - # echos back faithfully rather than being parsed) so leaving this - # alone for now. - - quotedMatch = re.match(r'^(\S+)="([^"]+)"', arg) - nonquotedMatch = re.match(r'^(\S+)=(\S+)', arg) - - if quotedMatch: - # we're dealing with a '<param>="<value>"' entry - param, value = quotedMatch.groups() - - paramList.append((param, value)) - arg = arg[len(param) + len(value) + 3:].strip() - elif nonquotedMatch: - # we're dealing with a '<param>=<value>' entry - param, value = nonquotedMatch.groups() - - paramList.append((param, value)) - arg = arg[len(param) + len(value) + 1:].strip() - else: - # starts with just a param - param = arg.split()[0] - paramList.append((param, None)) - arg = arg[len(param):].strip() - - try: - isReset = cmd == "RESETCONF" - conn.setOptions(paramList, isReset) - except Exception, exc: - outputEntry.append((str(exc), ERROR_FORMAT)) - elif cmd == "SETEVENTS": - return ([], []) # dropping support, see the comment for the event listener above for why - - self.loggedEvents = arg.split() - #allEvents = torTools.getConn().setControllerEvents(self.loggedEvents) - setEvents = set(self.loggedEvents).intersection(allEvents) - - if setEvents: - outputEntry.append(("Successfully set event listening to: %s\n" % ", ".join(setEvents), OUTPUT_FORMAT)) - else: - if not self.loggedEvents: - # we purposefully disabled event listening - outputEntry.append(("Disabled event listening\n", OUTPUT_FORMAT)) - else: - # we tried to set events but they all failed - outputEntry.append(("Unable to set any of the events. Event listening disabled.\n", ERROR_FORMAT)) - elif cmd.replace("+", "") in ("LOADCONF", "POSTDESCRIPTOR"): - # provides a notice that multi-line controller input isn't yet implemented - outputEntry.append((MULTILINE_UNIMPLEMENTED_NOTICE + "\n", ERROR_FORMAT)) - else: - try: - controller = conn.controller - if controller is None: raise stem.SocketClosed() - - response = controller.msg(input) - - if cmd == "QUIT": - raise InterpretorClosed("Closing the connection") - - for _, _, content in response.content(): - outputEntry.append((content + '\n', OUTPUT_FORMAT)) - except Exception, exc: - if isinstance(exc, InterpretorClosed): - raise exc - else: - outputEntry.append((str(exc), ERROR_FORMAT)) - - # converts to lists split on newlines - inputLines = _splitOnNewlines(inputEntry) - outputLines = _splitOnNewlines(outputEntry) - - # appends new contents, cropping if too long - # TODO: it would be nice if InterpretorClosed exceptions were added to the content too - self.contents += inputLines + outputLines - cropLines = len(self.contents) - CONTENT_LIMIT - if cropLines > 0: self.contents = self.contents[cropLines:] - - return (inputLines, outputLines) - -def showPrompt(): - # When displaying the prompt we want the readline module to be imported so - # we get its neat features like history scrollback. HOWEVER, importing - # readline prior to initializing curses causes terminal bugs, most noticeably - # screen resizing. - - import readline - - # For Python 2.6 and earlier cycling history via the readline module with - # up/down is buggy with a color prompt. For more information see: - # http://bugs.python.org/issue12972 - # - # To work around this while keeping a color prompt I'm padding the prompt - # with extra reset encodings so its length is non-rendered higher (around - # sixty characters). There's two ways that this can go wrong... - # - if the user uses up/down to display input longer than this non-rendered - # length then the original bug will manifest (screwed up prompt) - # - if the terminal's width is smaller than the non-rendered prompt length - # then the cursor and some movement will be displaced - - if COLOR_PROMPT: - prompt = format(">>> ", Color.GREEN, Attr.BOLD) - - majorVersion = sys.version_info[0] - minorVersion = sys.version_info[1] - - if majorVersion <= 2 and minorVersion <= 6: - prompt += "\x1b[0m" * 10 - else: - prompt = ">>> " - - input = "" - - # sets up tab autocompetion - torCommands = TorControlCompleter() - readline.parse_and_bind("tab: complete") - readline.set_completer(torCommands.complete) - - # Essentially disables autocompletion by word delimiters. This is because - # autocompletion options are full commands (ex. "GETINFO version") so we want - # "GETINFO" to match to all the options rather than be treated as a complete - # command by itself. - - readline.set_completer_delims("\n") - interpretor = ControlInterpretor() - - print INIT_MSG - - while True: - try: - input = raw_input(prompt) - _, outputEntry = interpretor.handleQuery(input) - except (KeyboardInterrupt, Exception), exc: - if isinstance(exc, InterpretorClosed) and str(exc): - print format(str(exc), *ERROR_FORMAT) - - # moves cursor to the next line and terminates (most commonly - # KeyboardInterrupt and EOFErro) - print - - torTools.NO_SPAWN = True - torTools.getConn().close() - - # stop daemons - hostnames.stop() - - break - - for line in outputEntry: - outputLine = "" - - for msg, msgFormat in line: - outputLine += format(msg, *msgFormat) - - print outputLine - -def _splitOnNewlines(entry): - """ - Splits a list of (msg, format) tuples on newlines into a list of lines. - - Arguments: - entry - list of display tuples - """ - - results, tmpLine = [], [] - entry = list(entry) # shallow copy - - while entry: - msg, msgFormat = entry.pop(0) - - if "\n" in msg: - msg, remainder = msg.split("\n", 1) - entry.insert(0, (remainder, msgFormat)) - - tmpLine.append((msg, msgFormat)) - results.append(tmpLine) - tmpLine = [] - else: - tmpLine.append((msg, msgFormat)) - - if tmpLine: results.append(tmpLine) - return results -
tor-commits@lists.torproject.org