[tor-commits] [arm/release] Finishing arm rewrite [insert fireworks here!]

atagar at torproject.org atagar at torproject.org
Sun Jul 17 06:08:20 UTC 2011


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





More information about the tor-commits mailing list