commit cc9e375a079c7f8816bf0564ad2a82ef1ffb039e Author: Damian Johnson atagar@torproject.org Date: Thu May 19 19:03:58 2011 -0700
Finishing arm rewrite [insert fireworks here!]
This is a full rewrite of the arm controller, the functionality that orchestrates the whole arm UI. It's also last piece of the old codebase to be rewritten... so this is it - I'm done with the refactoring project that's been my main endeavor for the last year!
/me does a not-so-little happy dance :) --- armrc.sample | 9 +- src/cli/configPanel.py | 2 +- src/cli/controller.py | 875 +++++++++++++++++++++----------------------- src/cli/descriptorPopup.py | 5 +- src/cli/popups.py | 51 ++-- src/starter.py | 8 +- 6 files changed, 459 insertions(+), 491 deletions(-)
diff --git a/armrc.sample b/armrc.sample index 58fe5c2..9f75ccc 100644 --- a/armrc.sample +++ b/armrc.sample @@ -35,6 +35,12 @@ features.logFile # this is only displayed when we're running out. features.showFdUsage false
+# Seconds to wait on user input before refreshing content +features.redrawRate 5 + +# Confirms promt to confirm when quiting if true +features.confirmQuit true + # Paremters for the log panel # --------------------------- # showDateDividers @@ -218,8 +224,6 @@ cache.armLog.trimSize 200
# Runlevels at which arm logs its events log.startTime INFO -log.refreshRate DEBUG -log.highCpuUsage WARN log.configEntryNotFound NONE log.configEntryUndefined NOTICE log.configEntryTypeError NOTICE @@ -272,4 +276,5 @@ log.stats.failedPsResolution INFO log.savingDebugLog NOTICE log.fdUsageSixtyPercent NOTICE log.fdUsageNinetyPercent WARN +log.unknownTorPid WARN
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py index 13e4343..c44d295 100644 --- a/src/cli/configPanel.py +++ b/src/cli/configPanel.py @@ -398,7 +398,7 @@ class ConfigPanel(panel.Panel):
popup.win.refresh()
- key = controller.getScreen().getch() + key = controller.getController().getScreen().getch() if key == curses.KEY_LEFT: selection = max(0, selection - 1) elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
diff --git a/src/cli/controller.py b/src/cli/controller.py index 4916faf..a1ec58a 100644 --- a/src/cli/controller.py +++ b/src/cli/controller.py @@ -1,193 +1,363 @@ -#!/usr/bin/env python -# controller.py -- arm interface (curses monitor for relay status) -# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html) - """ -Curses (terminal) interface for the arm relay status monitor. +Main interface loop for arm, periodically redrawing the screen and issuing +user input to the proper panels. """
-import os -import math import time import curses +import threading
-import popups -import headerPanel -import graphing.graphPanel -import logPanel -import configPanel -import torrcPanel - +import cli.popups +import cli.headerPanel +import cli.logPanel +import cli.configPanel +import cli.torrcPanel +import cli.graphing.graphPanel +import cli.graphing.bandwidthStats +import cli.graphing.connStats +import cli.graphing.resourceStats import cli.connections.connPanel -import cli.connections.connEntry -import cli.connections.entries -from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools -import graphing.bandwidthStats -import graphing.connStats -import graphing.resourceStats
-# TODO: controller should be its own object that can be refreshed - until that -# emulating via a 'refresh' flag -REFRESH_FLAG = False +from util import connections, conf, enum, log, panel, sysTools, torConfig, torTools
-def refresh(): - global REFRESH_FLAG - REFRESH_FLAG = True +ARM_CONTROLLER = None
-# new panel params and accessors (this is part of the new controller apis) -PANELS = {} -STDSCR = None -IS_PAUSED = False -PAGE = 0 +CONFIG = {"startup.events": "N3", + "startup.blindModeEnabled": False, + "features.redrawRate": 5, + "features.confirmQuit": True, + "features.graph.type": 1, + "features.graph.bw.prepopulate": True, + "log.startTime": log.INFO, + "log.torEventTypeUnrecognized": log.NOTICE, + "log.configEntryUndefined": log.NOTICE, + "log.unknownTorPid": log.WARN} + +GraphStat = enum.Enum("BANDWIDTH", "CONNECTIONS", "SYSTEM_RESOURCES")
-def getScreen(): - return STDSCR +# maps 'features.graph.type' config values to the initial types +GRAPH_INIT_STATS = {1: GraphStat.BANDWIDTH, 2: GraphStat.CONNECTIONS, 3: GraphStat.SYSTEM_RESOURCES}
-def getPage(): +def getController(): """ - Provides the number belonging to this page. Page numbers start at one. + Provides the arm controller instance. """
- return PAGE + 1 + return ARM_CONTROLLER
-def getPanel(name): +def initController(stdscr, startTime): """ - Provides the panel with the given identifier. + Spawns the controller, and related panels for it.
Arguments: - name - name of the panel to be fetched + stdscr - curses window """
- return PANELS[name] + global ARM_CONTROLLER + config = conf.getConfig("arm") + + # initializes the panels + stickyPanels = [cli.headerPanel.HeaderPanel(stdscr, startTime, config), + LabelPanel(stdscr)] + pagePanels = [] + + # first page: graph and log + expandedEvents = cli.logPanel.expandEvents(CONFIG["startup.events"]) + pagePanels.append([cli.graphing.graphPanel.GraphPanel(stdscr), + cli.logPanel.LogPanel(stdscr, expandedEvents, config)]) + + # second page: connections + if not CONFIG["startup.blindModeEnabled"]: + pagePanels.append([cli.connections.connPanel.ConnectionPanel(stdscr, config)]) + + # third page: config + pagePanels.append([cli.configPanel.ConfigPanel(stdscr, cli.configPanel.State.TOR, config)]) + + # fourth page: torrc + pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC, config)]) + + # initializes the controller + ARM_CONTROLLER = Controller(stdscr, stickyPanels, pagePanels) + + # additional configuration for the graph panel + graphPanel = ARM_CONTROLLER.getPanel("graph") + + # statistical monitors for graph + bwStats = cli.graphing.bandwidthStats.BandwidthStats(config) + graphPanel.addStats(GraphStat.BANDWIDTH, bwStats) + graphPanel.addStats(GraphStat.SYSTEM_RESOURCES, cli.graphing.resourceStats.ResourceStats()) + if not CONFIG["startup.blindModeEnabled"]: + graphPanel.addStats(GraphStat.CONNECTIONS, cli.graphing.connStats.ConnStats()) + + # sets graph based on config parameter + try: + initialStats = GRAPH_INIT_STATS.get(CONFIG["features.graph.type"]) + graphPanel.setStats(initialStats) + except ValueError: pass # invalid stats, maybe connections when in blind mode + + # prepopulates bandwidth values from state file + if CONFIG["features.graph.bw.prepopulate"]: + isSuccessful = bwStats.prepopulateFromState() + if isSuccessful: graphPanel.updateInterval = 4
-def getPanels(page = None): +class LabelPanel(panel.Panel): """ - Provides all panels or all panels from a given page. - - Arguments: - page - page number of the panels to be fetched, all panels if undefined + Panel that just displays a single line of text. """
- panelSet = [] - if page == None: - # fetches all panel names - panelSet = list(PAGE_S) - for pagePanels in PAGES: - panelSet += pagePanels - else: panelSet = PAGES[page - 1] + def __init__(self, stdscr): + panel.Panel.__init__(self, stdscr, "msg", 0, 1) + self.msgText = "" + self.msgAttr = curses.A_NORMAL
- return [getPanel(name) for name in panelSet] - -CONFIRM_QUIT = True -REFRESH_RATE = 5 # seconds between redrawing screen - -# enums for message in control label -CTL_HELP, CTL_PAUSED = range(2) - -# panel order per page -PAGE_S = ["header", "control"] # sticky (ie, always available) page -PAGES = [ - ["graph", "log"], - ["conn"], - ["config"], - ["torrc"]] - -CONFIG = {"features.graph.type": 1, - "queries.refreshRate.rate": 5, - "log.torEventTypeUnrecognized": log.NOTICE, - "features.graph.bw.prepopulate": True, - "log.startTime": log.INFO, - "log.refreshRate": log.DEBUG, - "log.highCpuUsage": log.WARN, - "log.configEntryUndefined": log.NOTICE} + def setMessage(self, msg, attr = None): + """ + Sets the message being displayed by the panel. + + Arguments: + msg - string to be displayed + attr - attribute for the label, normal text if undefined + """ + + if attr == None: attr = curses.A_NORMAL + self.msgText = msg + self.msgAttr = attr + + def draw(self, width, height): + self.addstr(0, 0, self.msgText, self.msgAttr)
-class ControlPanel(panel.Panel): - """ Draws single line label for interface controls. """ +class Controller: + """ + Tracks the global state of the interface + """
- def __init__(self, stdscr, isBlindMode): - panel.Panel.__init__(self, stdscr, "control", 0, 1) - self.msgText = CTL_HELP # message text to be displyed - self.msgAttr = curses.A_NORMAL # formatting attributes - self.page = 1 # page number currently being displayed - self.resolvingCounter = -1 # count of resolver when starting (-1 if we aren't working on a batch) - self.isBlindMode = isBlindMode + def __init__(self, stdscr, stickyPanels, pagePanels): + """ + Creates a new controller instance. Panel lists are ordered as they appear, + top to bottom on the page. + + Arguments: + stdscr - curses window + stickyPanels - panels shown at the top of each page + pagePanels - list of pages, each being a list of the panels on it + """ + + self._screen = stdscr + self._stickyPanels = stickyPanels + self._pagePanels = pagePanels + self._page = 0 + self._isPaused = False + self._forceRedraw = False + self.setMsg() # initializes our control message + + def getScreen(self): + """ + Provides our curses window. + """ + + return self._screen
- def setMsg(self, msgText, msgAttr=curses.A_NORMAL): + def getPage(self): """ - Sets the message and display attributes. If msgType matches CTL_HELP or - CTL_PAUSED then uses the default message for those statuses. + Provides the number belonging to this page. Page numbers start at zero. """
- self.msgText = msgText - self.msgAttr = msgAttr + return self._page
- def revertMsg(self): - self.setMsg(CTL_PAUSED if IS_PAUSED else CTL_HELP) + def nextPage(self): + """ + Increments the page number. + """ + + self._page = (self._page + 1) % len(self._pagePanels) + self._forceRedraw = True + self.setMsg()
- def draw(self, width, height): - msgText = self.msgText - msgAttr = self.msgAttr - barTab = 2 # space between msgText and progress bar - barWidthMax = 40 # max width to progress bar - barWidth = -1 # space between "[ ]" in progress bar (not visible if -1) - barProgress = 0 # cells to fill - - if msgText == CTL_HELP: - msgAttr = curses.A_NORMAL + def prevPage(self): + """ + Decrements the page number. + """ + + self._page = (self._page - 1) % len(self._pagePanels) + self._forceRedraw = True + self.setMsg() + + def isPaused(self): + """ + True if the interface is paused, false otherwise. + """ + + return self._isPaused + + def setPaused(self, isPause): + """ + Sets the interface to be paused or unpaused. + """ + + if isPause != self._isPaused: + self._isPaused = isPause + self._forceRedraw = True + self.setMsg()
- if self.resolvingCounter != -1: - if hostnames.isPaused() or not hostnames.isResolving(): - # done resolving dns batch - self.resolvingCounter = -1 - curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate - else: - batchSize = hostnames.getRequestCount() - self.resolvingCounter - entryCount = batchSize - hostnames.getPendingCount() - if batchSize > 0: progress = 100 * entryCount / batchSize - else: progress = 0 - - additive = "or l " if self.page == 2 else "" - batchSizeDigits = int(math.log10(batchSize)) + 1 - entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount - #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive) - msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress) - - barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab) - barProgress = barWidth * entryCount / batchSize + for panelImpl in self.getAllPanels(): + panelImpl.setPaused(isPause) + + def getPanel(self, name): + """ + Provides the panel with the given identifier. This returns None if no such + panel exists. + + Arguments: + name - name of the panel to be fetched + """ + + for panelImpl in self.getAllPanels(): + if panelImpl.getName() == name: + return panelImpl + + return None + + def getStickyPanels(self): + """ + Provides the panels visibile at the top of every page. + """ + + return list(self._stickyPanels) + + def getDisplayPanels(self, includeSticky = True): + """ + Provides all panels belonging to the current page and sticky content above + it. This is ordered they way they are presented (top to bottom) on the + page. + + Arguments: + includeSticky - includes sticky panels in the results if true + """ + + if includeSticky: + return self._stickyPanels + self._pagePanels[self._page] + else: + return list(self._pagePanels[self._page]) + + def getDaemonPanels(self): + """ + Provides thread panels. + """ + + threadPanels = [] + for panelImpl in self.getAllPanels(): + if isinstance(panelImpl, threading.Thread): + threadPanels.append(panelImpl) + + return threadPanels + + def getAllPanels(self): + """ + Provides all panels in the interface. + """ + + allPanels = list(self._stickyPanels) + + for page in self._pagePanels: + allPanels += list(page) + + return allPanels + + def requestRedraw(self): + """ + Requests that all content is redrawn when the interface is next rendered. + """ + + self._forceRedraw = True + + def isRedrawRequested(self, clearFlag = False): + """ + True if a full redraw has been requested, false otherwise. + + Arguments: + clearFlag - request clears the flag if true + """ + + returnValue = self._forceRedraw + if clearFlag: self._forceRedraw = False + return returnValue + + def setMsg(self, msg = None, attr = None, redraw = False): + """ + Sets the message displayed in the interfaces control panel. This uses our + default prompt if no arguments are provided. + + Arguments: + msg - string to be displayed + attr - attribute for the label, normal text if undefined + redraw - redraws right away if true, otherwise redraws when display + content is next normally drawn + """ + + if msg == None: + msg = ""
- if self.resolvingCounter == -1: - currentPage = self.page - pageCount = len(PAGES) - - if self.isBlindMode: - if currentPage >= 2: currentPage -= 1 - pageCount -= 1 - - msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount) - elif msgText == CTL_PAUSED: - msgText = "Paused" - msgAttr = curses.A_STANDOUT - - self.addstr(0, 0, msgText, msgAttr) - if barWidth > -1: - xLoc = len(msgText) + barTab - self.addstr(0, xLoc, "[", curses.A_BOLD) - self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red")) - self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD) + if attr == None: + if not self._isPaused: + msg = "page %i / %i - q: quit, p: pause, h: page help" % (self._page + 1, len(self._pagePanels)) + attr = curses.A_NORMAL + else: + msg = "Paused" + attr = curses.A_STANDOUT + + controlPanel = self.getPanel("msg") + controlPanel.setMessage(msg, attr) + + if redraw: controlPanel.redraw(True) + else: self._forceRedraw = True
-def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False): +def shutdownDaemons(): """ - Resets the isPaused state of panels. If overwrite is True then this pauses - reguardless of the monitor is paused or not. + Stops and joins on worker threads. """
- allPanels = list(PAGE_S) - for pagePanels in PAGES: - allPanels += pagePanels + # prevents further worker threads from being spawned + torTools.NO_SPAWN = True + + # stops panel daemons + control = getController() + for panelImpl in control.getDaemonPanels(): panelImpl.stop() + for panelImpl in control.getDaemonPanels(): panelImpl.join() + + # joins on TorCtl event thread + torTools.getConn().close() + + # joins on utility daemon threads - this might take a moment since the + # internal threadpools being joined might be sleeping + resourceTrackers = sysTools.RESOURCE_TRACKERS.values() + resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None + for tracker in resourceTrackers: tracker.stop() + if resolver: resolver.stop() # sets halt flag (returning immediately) + for tracker in resourceTrackers: tracker.join() + if resolver: resolver.join() # joins on halted resolver + +def heartbeatCheck(isUnresponsive): + """ + Logs if its been ten seconds since the last BW event. + + Arguments: + isUnresponsive - flag for if we've indicated to be responsive or not + """
- for key in allPanels: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S)) + conn = torTools.getConn() + lastHeartbeat = conn.getHeartbeat() + if conn.isAlive() and "BW" in conn.getControllerEvents() and lastHeartbeat != 0: + if not isUnresponsive and (time.time() - lastHeartbeat) >= 10: + isUnresponsive = True + log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat)) + elif isUnresponsive and (time.time() - lastHeartbeat) < 10: + # really shouldn't happen (meant Tor froze for a bit) + isUnresponsive = False + log.log(log.NOTICE, "Relay resumed") + + return isUnresponsive
-def connResetListener(conn, eventType): +def connResetListener(_, eventType): """ Pauses connection resolution when tor's shut down, and resumes if started again. @@ -197,369 +367,164 @@ def connResetListener(conn, eventType): resolver = connections.getResolver("tor") resolver.setPaused(eventType == torTools.State.CLOSED)
-def selectiveRefresh(panels, page): - """ - This forces a redraw of content on the currently active page (should be done - after changing pages, popups, or anything else that overwrites panels). +def startTorMonitor(startTime): """ + Initializes the interface and starts the main draw loop.
- for panelKey in PAGES[page]: - panels[panelKey].redraw(True) - -def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode): - """ - Starts arm interface reflecting information on provided control port. - - stdscr - curses window - startTime - unix time for when arm was started - loggedEvents - event types we've been configured to log - isBlindMode - flag to indicate if the user's turned off connection lookups + Arguments: + startTime - unix time for when arm was started """
- global PANELS, STDSCR, REFRESH_FLAG, PAGE, IS_PAUSED - STDSCR = stdscr - - # loads config for various interface components + # initializes interface configs config = conf.getConfig("arm") config.update(CONFIG) - graphing.graphPanel.loadConfig(config) - cli.connections.connEntry.loadConfig(config)
- # adds events needed for arm functionality to the torTools REQ_EVENTS mapping - # (they're then included with any setControllerEvents call, and log a more - # helpful error if unavailable) - torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" + cli.graphing.graphPanel.loadConfig(config) + cli.connections.connEntry.loadConfig(config)
- if not isBlindMode: - torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections" + # attempts to fetch the tor pid, warning if unsuccessful (this is needed for + # checking its resource usage, among other things) + conn = torTools.getConn() + torPid = conn.getMyPid()
- # pauses/unpauses connection resolution according to if tor's connected or not - torTools.getConn().addStatusListener(connResetListener) + if not torPid: + msg = "Unable to determine Tor's pid. Some information, like its resource usage will be unavailable." + log.log(CONFIG["log.unknownTorPid"], msg)
- curses.halfdelay(REFRESH_RATE * 10) # uses getch call as timer for REFRESH_RATE seconds - try: curses.use_default_colors() # allows things like semi-transparent backgrounds (call can fail with ERR) - except curses.error: pass + # adds events needed for arm functionality to the torTools REQ_EVENTS + # mapping (they're then included with any setControllerEvents call, and log + # a more helpful error if unavailable)
- # attempts to make the cursor invisible (not supported in all terminals) - try: curses.curs_set(0) - except curses.error: pass + torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function"
- # attempts to determine tor's current pid (left as None if unresolveable, logging an error later) - torPid = torTools.getConn().getMyPid() + if not CONFIG["startup.blindModeEnabled"]: + torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections" + + # Configures connection resoultions. This is paused/unpaused according to + # if Tor's connected or not. + conn.addStatusListener(connResetListener) + + if torPid: + # use the tor pid to help narrow connection results + torCmdName = sysTools.getProcessName(torPid, "tor") + connections.getResolver(torCmdName, torPid, "tor") + else: connections.getResolver("tor") + + # hack to display a better (arm specific) notice if all resolvers fail + connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
# loads the torrc and provides warnings in case of validation errors try: loadedTorrc = torConfig.getTorrc() loadedTorrc.load(True) loadedTorrc.logValidationIssues() - except: pass - - # minor refinements for connection resolver - if not isBlindMode: - if torPid: - # use the tor pid to help narrow connection results - torCmdName = sysTools.getProcessName(torPid, "tor") - resolver = connections.getResolver(torCmdName, torPid, "tor") - else: - resolver = connections.getResolver("tor") - - # hack to display a better (arm specific) notice if all resolvers fail - connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)" - - panels = { - "header": headerPanel.HeaderPanel(stdscr, startTime, config), - "graph": graphing.graphPanel.GraphPanel(stdscr), - "log": logPanel.LogPanel(stdscr, loggedEvents, config)} - - # TODO: later it would be good to set the right 'top' values during initialization, - # but for now this is just necessary for the log panel (and a hack in the log...) - - # TODO: bug from not setting top is that the log panel might attempt to draw - # before being positioned - the following is a quick hack til rewritten - panels["log"].setPaused(True) - - panels["conn"] = cli.connections.connPanel.ConnectionPanel(stdscr, config) - - panels["control"] = ControlPanel(stdscr, isBlindMode) - panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config) - panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config) - - # provides error if pid coulnd't be determined (hopefully shouldn't happen...) - if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing") - - # statistical monitors for graph - panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config)) - panels["graph"].addStats("system resources", graphing.resourceStats.ResourceStats()) - if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats()) - - # sets graph based on config parameter - graphType = CONFIG["features.graph.type"] - if graphType == 0: panels["graph"].setStats(None) - elif graphType == 1: panels["graph"].setStats("bandwidth") - elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections") - elif graphType == 3: panels["graph"].setStats("system resources") - - # prepopulates bandwidth values from state file - if CONFIG["features.graph.bw.prepopulate"]: - isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState() - if isSuccessful: panels["graph"].updateInterval = 4 - - # tells Tor to listen to the events we're interested - #panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set - panels["log"].setLoggedEvents(loggedEvents) # strips any that couldn't be set + except IOError: pass
# provides a notice about any event types tor supports but arm doesn't - missingEventTypes = logPanel.getMissingEventTypes() + missingEventTypes = cli.logPanel.getMissingEventTypes() + if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.log(CONFIG["log.torEventTypeUnrecognized"], "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)))
- PANELS = panels - - # tells revised panels to run as daemons - panels["header"].start() - panels["log"].start() - panels["conn"].start() + try: + curses.wrapper(drawTorMonitor, startTime) + except KeyboardInterrupt: + pass # skip printing stack trace in case of keyboard interrupt + +def drawTorMonitor(stdscr, startTime): + """ + Main draw loop context.
- isUnresponsive = False # true if it's been over ten seconds since the last BW event (probably due to Tor closing) - isPaused = False # if true updates are frozen - overrideKey = None # immediately runs with this input rather than waiting for the user if set - page = 0 + Arguments: + stdscr - curses window + startTime - unix time for when arm was started + """
- PAGE = page + initController(stdscr, startTime) + control = getController()
# provides notice about any unused config keys - for key in config.getUnusedKeys(): + for key in conf.getConfig("arm").getUnusedKeys(): log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
- lastPerformanceLog = 0 # ensures we don't do performance logging too frequently - redrawStartTime = time.time() + # tells daemon panels to start + for panelImpl in control.getDaemonPanels(): panelImpl.start()
- # TODO: popups need to force the panels it covers to redraw (or better, have - # a global refresh function for after changing pages, popups, etc) - - initTime = time.time() - startTime - log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime) - - # attributes to give a WARN level event if arm's resource usage is too high - isResourceWarningGiven = False - lastResourceCheck = startTime + # allows for background transparency + try: curses.use_default_colors() + except curses.error: pass
- lastSize = None + # makes the cursor invisible + try: curses.curs_set(0) + except curses.error: pass
- # sets initial visiblity for the pages - for entry in PAGE_S: panels[entry].setVisible(True) + # logs the initialization time + msg = "arm started (initialization took %0.3f seconds)" % (time.time() - startTime) + log.log(CONFIG["log.startTime"], msg)
- for i in range(len(PAGES)): - isVisible = i == page - for entry in PAGES[i]: panels[entry].setVisible(isVisible) + # main draw loop + overrideKey = None # uses this rather than waiting on user input + isUnresponsive = False # flag for heartbeat responsiveness check
- # TODO: come up with a nice, clean method for other threads to immediately - # terminate the draw loop and provide a stacktrace while True: - # tried only refreshing when the screen was resized but it caused a - # noticeable lag when resizing and didn't have an appreciable effect - # on system usage + displayPanels = control.getDisplayPanels() + isUnresponsive = heartbeatCheck(isUnresponsive)
- panel.CURSES_LOCK.acquire() - try: - redrawStartTime = time.time() - - # gives panels a chance to take advantage of the maximum bounds - # originally this checked in the bounds changed but 'recreate' is a no-op - # if panel properties are unchanged and checking every redraw is more - # resilient in case of funky changes (such as resizing during popups) - - # hack to make sure header picks layout before using the dimensions below - #panels["header"].getPreferredSize() - - startY = 0 - for panelKey in PAGE_S[:2]: - #panels[panelKey].recreate(stdscr, -1, startY) - panels[panelKey].setParent(stdscr) - panels[panelKey].setWidth(-1) - panels[panelKey].setTop(startY) - startY += panels[panelKey].getHeight() - - for panelSet in PAGES: - tmpStartY = startY - - for panelKey in panelSet: - #panels[panelKey].recreate(stdscr, -1, tmpStartY) - panels[panelKey].setParent(stdscr) - panels[panelKey].setWidth(-1) - panels[panelKey].setTop(tmpStartY) - tmpStartY += panels[panelKey].getHeight() - - # provides a notice if there's been ten seconds since the last BW event - lastHeartbeat = torTools.getConn().getHeartbeat() - if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0: - if not isUnresponsive and (time.time() - lastHeartbeat) >= 10: - isUnresponsive = True - log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat)) - elif isUnresponsive and (time.time() - lastHeartbeat) < 10: - # really shouldn't happen (meant Tor froze for a bit) - isUnresponsive = False - log.log(log.NOTICE, "Relay resumed") - - # TODO: part two of hack to prevent premature drawing by log panel - if page == 0 and not isPaused: panels["log"].setPaused(False) - - # I haven't the foggiest why, but doesn't work if redrawn out of order... - for panelKey in (PAGE_S + PAGES[page]): - newSize = stdscr.getmaxyx() - isResize = lastSize != newSize - lastSize = newSize - - if panelKey != "control": - panels[panelKey].redraw(isResize) - else: - panels[panelKey].redraw(True) - - stdscr.refresh() - - currentTime = time.time() - if currentTime - lastPerformanceLog >= CONFIG["queries.refreshRate.rate"]: - cpuTotal = sum(os.times()[:3]) - pythonCpuAvg = cpuTotal / (currentTime - startTime) - sysCallCpuAvg = sysTools.getSysCpuUsage() - totalCpuAvg = pythonCpuAvg + sysCallCpuAvg - - if sysCallCpuAvg > 0.00001: - log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%% (python), %0.3f%% (system calls), %0.3f%% (total)" % (currentTime - redrawStartTime, 100 * pythonCpuAvg, 100 * sysCallCpuAvg, 100 * totalCpuAvg)) - else: - # with the proc enhancements the sysCallCpuAvg is usually zero - log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%%" % (currentTime - redrawStartTime, 100 * totalCpuAvg)) - - lastPerformanceLog = currentTime - - # once per minute check if the sustained cpu usage is above 5%, if so - # then give a warning (and if able, some advice for lowering it) - # TODO: disabling this for now (scrolling causes cpu spikes for quick - # redraws, ie this is usually triggered by user input) - if False and not isResourceWarningGiven and currentTime > (lastResourceCheck + 60): - if totalCpuAvg >= 0.05: - msg = "Arm's cpu usage is high (averaging %0.3f%%)." % (100 * totalCpuAvg) - - if not isBlindMode: - msg += " You could lower it by dropping the connection data (running as "arm -b")." - - log.log(CONFIG["log.highCpuUsage"], msg) - isResourceWarningGiven = True - - lastResourceCheck = currentTime - finally: - panel.CURSES_LOCK.release() - - # wait for user keyboard input until timeout (unless an override was set) + # sets panel visability + for panelImpl in control.getAllPanels(): + panelImpl.setVisible(panelImpl in displayPanels) + + # panel placement + occupiedContent = 0 + for panelImpl in displayPanels: + panelImpl.setTop(occupiedContent) + occupiedContent += panelImpl.getHeight() + + # redraws visible content + forceRedraw = control.isRedrawRequested(True) + for panelImpl in displayPanels: + panelImpl.redraw(forceRedraw) + + stdscr.refresh() + + # wait for user keyboard input until timeout, unless an override was set if overrideKey: - key = overrideKey - overrideKey = None + key, overrideKey = overrideKey, None else: + curses.halfdelay(CONFIG["features.redrawRate"] * 10) key = stdscr.getch()
- if key == ord('q') or key == ord('Q'): - quitConfirmed = not CONFIRM_QUIT - + if key == curses.KEY_RIGHT: + control.nextPage() + elif key == curses.KEY_LEFT: + control.prevPage() + elif key == ord('p') or key == ord('P'): + control.setPaused(not control.isPaused()) + elif key == ord('q') or key == ord('Q'): # provides prompt to confirm that arm should exit - if CONFIRM_QUIT: + if CONFIG["features.confirmQuit"]: msg = "Are you sure (q again to confirm)?" - confirmationKey = popups.showMsg(msg, attr = curses.A_BOLD) + confirmationKey = cli.popups.showMsg(msg, attr = curses.A_BOLD) quitConfirmed = confirmationKey in (ord('q'), ord('Q')) + else: quitConfirmed = True
if quitConfirmed: - # quits arm - # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable" - # this appears to be a python bug: http://bugs.python.org/issue3014 - # (haven't seen this is quite some time... mysteriously resolved?) - - torTools.NO_SPAWN = True # prevents further worker threads from being spawned - - # stops panel daemons - panels["header"].stop() - panels["conn"].stop() - panels["log"].stop() - - panels["header"].join() - panels["conn"].join() - panels["log"].join() - - torTools.getConn().close() # joins on TorCtl event thread - - # joins on utility daemon threads - this might take a moment since - # the internal threadpools being joined might be sleeping - resourceTrackers = sysTools.RESOURCE_TRACKERS.values() - resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None - for tracker in resourceTrackers: tracker.stop() - if resolver: resolver.stop() # sets halt flag (returning immediately) - hostnames.stop() # halts and joins on hostname worker thread pool - for tracker in resourceTrackers: tracker.join() - if resolver: resolver.join() # joins on halted resolver - + shutdownDaemons() break - elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT: - # switch page - if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES) - else: page = (page + 1) % len(PAGES) - - # skip connections listing if it's disabled - if page == 1 and isBlindMode: - if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES) - else: page = (page + 1) % len(PAGES) - - # pauses panels that aren't visible to prevent events from accumilating - # (otherwise they'll wait on the curses lock which might get demanding) - setPauseState(panels, isPaused, page) - - # prevents panels on other pages from redrawing - for i in range(len(PAGES)): - isVisible = i == page - for entry in PAGES[i]: panels[entry].setVisible(isVisible) - - PAGE = page - - panels["control"].page = page + 1 - - # TODO: this redraw doesn't seem necessary (redraws anyway after this - # loop) - look into this when refactoring - panels["control"].redraw(True) - - selectiveRefresh(panels, page) - elif key == ord('p') or key == ord('P'): - # toggles update freezing - panel.CURSES_LOCK.acquire() - try: - isPaused = not isPaused - IS_PAUSED = isPaused - setPauseState(panels, isPaused, page) - panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP) - finally: - panel.CURSES_LOCK.release() - - selectiveRefresh(panels, page) elif key == ord('x') or key == ord('X'): # provides prompt to confirm that arm should issue a sighup msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?" - confirmationKey = popups.showMsg(msg, attr = curses.A_BOLD) + confirmationKey = cli.popups.showMsg(msg, attr = curses.A_BOLD)
if confirmationKey in (ord('x'), ord('X')): try: torTools.getConn().reload() except IOError, exc: log.log(log.ERR, "Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc)) elif key == ord('h') or key == ord('H'): - overrideKey = popups.showHelpPopup() + overrideKey = cli.popups.showHelpPopup() else: - for pagePanel in getPanels(page + 1): - isKeystrokeConsumed = pagePanel.handleKey(key) + for panelImpl in displayPanels: + isKeystrokeConsumed = panelImpl.handleKey(key) if isKeystrokeConsumed: break - - if REFRESH_FLAG: - REFRESH_FLAG = False - selectiveRefresh(panels, page) - -def startTorMonitor(startTime, loggedEvents, isBlindMode): - try: - curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode) - except KeyboardInterrupt: - pass # skip printing stack trace in case of keyboard interrupt
diff --git a/src/cli/descriptorPopup.py b/src/cli/descriptorPopup.py index 2a51905..e73e9fc 100644 --- a/src/cli/descriptorPopup.py +++ b/src/cli/descriptorPopup.py @@ -104,7 +104,8 @@ def showDescriptorPopup(connectionPanel): """
# hides the title of the first panel on the page - topPanel = controller.getPanels(controller.getPage())[0] + contorl = controller.getController() + topPanel = control.getDisplayPanels(False)[0] topPanel.setTitleVisible(False) topPanel.redraw(True)
@@ -139,7 +140,7 @@ def showDescriptorPopup(connectionPanel):
try: draw(popup, properties) - key = controller.getScreen().getch() + key = control.getScreen().getch()
if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')): # closes popup diff --git a/src/cli/popups.py b/src/cli/popups.py index dd56bfa..7ed5302 100644 --- a/src/cli/popups.py +++ b/src/cli/popups.py @@ -4,7 +4,7 @@ Functions for displaying popups in the interface.
import curses
-import controller +import cli.controller
from util import panel, uiTools
@@ -21,10 +21,10 @@ def init(height = -1, width = -1): width - maximum width of the popup """
- topSize = controller.getPanel("header").getHeight() - topSize += controller.getPanel("control").getHeight() + control = cli.controller.getController() + topSize = sum(stickyPanel.getHeight() for stickyPanel in control.getStickyPanels())
- popup = panel.Panel(controller.getScreen(), "popup", topSize, height, width) + popup = panel.Panel(control.getScreen(), "popup", topSize, height, width) popup.setVisible(True)
# Redraws the popup to prepare a subwindow instance. If none is spawned then @@ -41,7 +41,7 @@ def finalize(): the rest of the display. """
- controller.refresh() + cli.controller.getController().requestRedraw() panel.CURSES_LOCK.release()
def inputPrompt(msg, initialValue = ""): @@ -55,11 +55,12 @@ def inputPrompt(msg, initialValue = ""): """
panel.CURSES_LOCK.acquire() - controlPanel = controller.getPanel("control") - controlPanel.setMsg(msg) - controlPanel.redraw(True) - userInput = controlPanel.getstr(0, len(msg), initialValue) - controlPanel.revertMsg() + control = cli.controller.getController() + msgPanel = control.getPanel("msg") + msgPanel.setMessage(msg) + msgPanel.redraw(True) + userInput = msgPanel.getstr(0, len(msg), initialValue) + control.setMsg() panel.CURSES_LOCK.release() return userInput
@@ -75,15 +76,13 @@ def showMsg(msg, maxWait = -1, attr = curses.A_STANDOUT): """
panel.CURSES_LOCK.acquire() - controlPanel = controller.getPanel("control") - controlPanel.setMsg(msg, attr) - controlPanel.redraw(True) + control = cli.controller.getController() + control.setMsg(msg, attr, True)
if maxWait == -1: curses.cbreak() else: curses.halfdelay(maxWait * 10) - keyPress = controller.getScreen().getch() - controlPanel.revertMsg() - curses.halfdelay(controller.REFRESH_RATE * 10) + keyPress = control.getScreen().getch() + control.setMsg() panel.CURSES_LOCK.release()
return keyPress @@ -100,8 +99,8 @@ def showHelpPopup():
exitKey = None try: - pageNum = controller.getPage() - pagePanels = controller.getPanels(pageNum) + control = cli.controller.getController() + pagePanels = control.getDisplayPanels()
# the first page is the only one with multiple panels, and it looks better # with the log entries first, so reversing the order @@ -113,7 +112,7 @@ def showHelpPopup():
# test doing afterward in case of overwriting popup.win.box() - popup.addstr(0, 0, "Page %i Commands:" % pageNum, curses.A_STANDOUT) + popup.addstr(0, 0, "Page %i Commands:" % (control.getPage() + 1), curses.A_STANDOUT)
for i in range(len(helpOptions)): if i / 2 >= height - 2: break @@ -142,8 +141,7 @@ def showHelpPopup():
popup.win.refresh() curses.cbreak() - exitKey = controller.getScreen().getch() - curses.halfdelay(controller.REFRESH_RATE * 10) + exitKey = control.getScreen().getch() finally: finalize()
if not uiTools.isSelectionKey(exitKey) and \ @@ -202,7 +200,7 @@ def showSortDialog(title, options, oldSelection, optionColors):
popup.win.refresh()
- key = controller.getScreen().getch() + key = cli.controller.getController().getScreen().getch() if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1) elif key == curses.KEY_RIGHT: @@ -220,8 +218,6 @@ def showSortDialog(title, options, oldSelection, optionColors): selectionOptions.remove(selection) cursorLoc = min(cursorLoc, len(selectionOptions) - 1) elif key == 27: break # esc - cancel - - curses.halfdelay(controller.REFRESH_RATE * 10) # reset normal pausing behavior finally: finalize()
if len(newSelections) == len(oldSelection): @@ -278,7 +274,8 @@ def showMenu(title, options, oldSelection):
try: # hides the title of the first panel on the page - topPanel = controller.getPanels(controller.getPage())[0] + control = cli.controller.getController() + topPanel = control.getDisplayPanels(False)[0] topPanel.setTitleVisible(False) topPanel.redraw(True)
@@ -298,12 +295,10 @@ def showMenu(title, options, oldSelection):
popup.win.refresh()
- key = controller.getScreen().getch() + key = control.getScreen().getch() if key == curses.KEY_UP: selection = max(0, selection - 1) elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1) elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel - - curses.halfdelay(controller.REFRESH_RATE * 10) # reset normal pausing behavior finally: topPanel.setTitleVisible(True) finalize() diff --git a/src/starter.py b/src/starter.py index be97d0e..84abbca 100644 --- a/src/starter.py +++ b/src/starter.py @@ -295,9 +295,11 @@ if __name__ == '__main__': for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.procTools, util.sysTools, util.torConfig, util.torTools, util.uiTools): utilModule.loadConfig(config)
- # overwrites undefined parameters with defaults + # snycs config and parameters, saving changed config options and overwriting + # undefined parameters with defaults for key in param.keys(): if param[key] == None: param[key] = CONFIG[key] + else: config.set(key, str(param[key]))
# validates that input has a valid ip address and port controlAddr = param["startup.interface.ipAddress"] @@ -312,7 +314,7 @@ if __name__ == '__main__':
# validates and expands log event flags try: - expandedEvents = cli.logPanel.expandEvents(param["startup.events"]) + cli.logPanel.expandEvents(param["startup.events"]) except ValueError, exc: for flag in str(exc): print "Unrecognized event flag: %s" % flag @@ -395,5 +397,5 @@ if __name__ == '__main__': procName.renameProcess("arm\0%s" % "\0".join(sys.argv[1:])) except: pass
- cli.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"]) + cli.controller.startTorMonitor(time.time() - initTime)
tor-commits@lists.torproject.org