commit 0016b2be0fac62ae840a1259ee65269defa3654c Author: Damian Johnson atagar@torproject.org Date: Wed Aug 24 10:07:24 2011 -0700
Interpretor panel for raw control port access
This adds a new page to the interface, providing an interactive interpretor that lets the user send/receive control commands. This includes history search and will be expanded to have other usability features (tab completion, search, etc). --- README | 1 + src/cli/__init__.py | 2 +- src/cli/controller.py | 3 + src/cli/interpretorPanel.py | 232 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 1 deletions(-)
diff --git a/README b/README index 16d0965..cbaf2f7 100644 --- a/README +++ b/README @@ -193,6 +193,7 @@ Layout: logPanel.py - (page 1) displays tor, arm, and torctl 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 diff --git a/src/cli/__init__.py b/src/cli/__init__.py index b0d6bd8..fa23640 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", "logPanel", "popups", "torrcPanel", "wizard"] +__all__ = ["configPanel", "controller", "headerPanel", "interpretorPanel", "logPanel", "popups", "torrcPanel", "wizard"]
diff --git a/src/cli/controller.py b/src/cli/controller.py index d2194ca..bc69e79 100644 --- a/src/cli/controller.py +++ b/src/cli/controller.py @@ -15,6 +15,7 @@ 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 @@ -97,6 +98,8 @@ def initController(stdscr, startTime): if CONFIG["features.panels.show.torrc"]: pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC, config)])
+ 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 new file mode 100644 index 0000000..b7c3fe0 --- /dev/null +++ b/src/cli/interpretorPanel.py @@ -0,0 +1,232 @@ +""" +Panel providing raw control port access with syntax hilighting, usage +information, tab completion, and other usability features. +""" + +import curses + +from util import enum, panel, textInput, torTools, uiTools + +from TorCtl import TorCtl + +Formats = enum.Enum("PROMPT", "INPUT", "INPUT_INTERPRETOR", "INPUT_CMD", "INPUT_ARG", "OUTPUT", "USAGE", "HELP", "ERROR") + +PROMPT = ">>> " +USAGE_INFO = "to use this panel press enter" + +# limits used for cropping +COMMAND_BACKLOG = 100 +LINES_BACKLOG = 2000 + +class InterpretorPanel(panel.Panel): + def __init__(self, stdscr): + panel.Panel.__init__(self, stdscr, "interpretor", 0) + self.isInputMode = False + self.scroll = 0 + self.formats = {} # lazy loaded curses formatting constants + self.previousCommands = [] # user input, newest to oldest + + # contents of the panel (oldest to newest), each line is a list of (msg, + # format enum) tuples + + self.contents = [[(PROMPT, Formats.PROMPT), (USAGE_INFO, Formats.USAGE)]] + + def prompt(self): + """ + Enables the interpretor, prompting for input until the user enters esc or + a blank line. + """ + + if not self.formats: self._initFormats() + 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(self.previousCommands, validator) + + xOffset = len(PROMPT) + if len(self.contents) > self.maxY - 1: + xOffset += 3 # offset for scrollbar + + input = self.getstr(min(self.maxY - 1, len(self.contents)), xOffset, "", self.formats[Formats.INPUT], validator = validator) + + isDone = self.handleQuery(input) + + if isDone: + self.isInputMode = False + self.redraw(True) + + panel.CURSES_LOCK.release() + + 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 a boolean to indicate if the interpretor should terminate or + not. + + Arguments: + input - user input to be processed + """ + + if not input or not input.strip(): return True + input = input.strip() + inputEntry, outputEntry = [(PROMPT, Formats.PROMPT)], [] + conn = torTools.getConn() + + # 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 input.startswith("/"): + # interpretor command + inputEntry.append((input, Formats.INPUT_INTERPRETOR)) + outputEntry.append(("Not yet implemented...", Formats.ERROR)) # TODO: implement + + # TODO: add /help option + # TODO: add /write option + else: + # controller command + if " " in input: cmd, arg = input.split(" ", 1) + else: cmd, arg = input, "" + + inputEntry.append((cmd + " ", Formats.INPUT_CMD)) + if arg: inputEntry.append((arg, Formats.INPUT_ARG)) + + if cmd.upper() == "GETINFO": + try: + response = conn.getInfo(arg, suppressExc = False) + outputEntry.append((response, Formats.OUTPUT)) + except Exception, exc: + outputEntry.append((str(exc), Formats.ERROR)) + elif cmd.upper() == "SETCONF": + if "=" in arg: + param, value = arg.split("=", 1) + + try: + conn.setOption(param.strip(), value.strip()) + except Exception, exc: + outputEntry.append((str(exc), Formats.ERROR)) + else: + # TODO: resets the attribute + outputEntry.append(("Not yet implemented...", Formats.ERROR)) # TODO: implement + else: + try: + response = conn.getTorCtl().sendAndRecv("%s\r\n" % input) + + for entry in response: + # Response entries are tuples with the response code, body, and + # extra info. For instance: + # ('250', 'version=0.2.2.23-alpha (git-b85eb949b528f4d7)', None) + + if len(entry) == 3: + outputEntry.append((entry[1], Formats.OUTPUT)) + except Exception, exc: + outputEntry.append((str(exc), Formats.ERROR)) + + self.previousCommands.insert(0, input) + self.previousCommands = self.previousCommands[:COMMAND_BACKLOG] + + promptEntry = self.contents.pop() # removes old prompt entry + self.contents += _splitOnNewlines(inputEntry) + self.contents += _splitOnNewlines(outputEntry) + self.contents.append(promptEntry) + + # if too long then crop lines + cropLines = len(self.contents) - LINES_BACKLOG + if cropLines > 0: self.contents = self.contents[cropLines:] + + return False + + def handleKey(self, key): + # TODO: allow contents to be searched (with hilighting?) + + isKeystrokeConsumed = True + if uiTools.isSelectionKey(key): + self.prompt() + elif uiTools.isScrollKey(key) and not self.isInputMode: + pageHeight = self.getPreferredSize()[0] - 1 + newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, len(self.contents)) + + if self.scroll != newScroll: + self.scroll = newScroll + self.redraw(True) + else: isKeystrokeConsumed = False + + return isKeystrokeConsumed + + def draw(self, width, height): + if not self.formats: self._initFormats() + + # 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 + if len(self.contents) > height - 1: + # if we're in input mode then make sure the last line is visible + if self.isInputMode: + self.scroll = len(self.contents) - height + 1 + + xOffset = 3 + self.addScrollBar(self.scroll, self.scroll + height - 1, len(self.contents), 1) + + # draws prior commands and output + drawLine = 1 + for entry in self.contents[self.scroll:]: + cursor = xOffset + + for msg, formatEntry in entry: + format = self.formats.get(formatEntry, curses.A_NORMAL) + self.addstr(drawLine, cursor, msg, format) + cursor += len(msg) + + drawLine += 1 + if drawLine >= height: break + + def _initFormats(self): + self.formats[Formats.PROMPT] = curses.A_BOLD | uiTools.getColor("green") + self.formats[Formats.INPUT] = uiTools.getColor("cyan") + self.formats[Formats.INPUT_INTERPRETOR] = curses.A_BOLD | uiTools.getColor("magenta") + self.formats[Formats.INPUT_CMD] = curses.A_BOLD | uiTools.getColor("green") + self.formats[Formats.INPUT_ARG] = curses.A_BOLD | uiTools.getColor("cyan") + self.formats[Formats.OUTPUT] = uiTools.getColor("blue") + self.formats[Formats.USAGE] = uiTools.getColor("cyan") + self.formats[Formats.HELP] = uiTools.getColor("magenta") + self.formats[Formats.ERROR] = curses.A_BOLD | uiTools.getColor("red") + +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, format = entry.pop(0) + + if "\n" in msg: + msg, remainder = msg.split("\n", 1) + entry.insert(0, (remainder, format)) + + tmpLine.append((msg, format)) + results.append(tmpLine) + tmpLine = [] + else: + tmpLine.append((msg, format)) + + if tmpLine: results.append(tmpLine) + return results +