tor-commits
Threads by month
- ----- 2026 -----
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
July 2011
- 16 participants
- 868 discussions
17 Jul '11
commit b03b2c90f44fd2b332cba3dd9a7296916d9d5f62
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri May 20 09:28:17 2011 -0700
fix: Paused accounting info not visible
When the interface was paused the pause buffer instance of graph stats lacked
accounting information, causing it to be displayed as "Unknown".
---
src/cli/graphing/bandwidthStats.py | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
diff --git a/src/cli/graphing/bandwidthStats.py b/src/cli/graphing/bandwidthStats.py
index 9be52e8..d378c45 100644
--- a/src/cli/graphing/bandwidthStats.py
+++ b/src/cli/graphing/bandwidthStats.py
@@ -76,6 +76,8 @@ class BandwidthStats(graphPanel.GraphStats):
def clone(self, newCopy=None):
if not newCopy: newCopy = BandwidthStats(self.inputConfig)
+ newCopy.accountingLastUpdated = self.accountingLastUpdated
+ newCopy.accountingInfo = self.accountingInfo
return graphPanel.GraphStats.clone(self, newCopy)
def resetListener(self, conn, eventType):
1
0
commit 13c7bd39c0d5459dc90b0903e6c2b16a27e17859
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri May 20 17:02:17 2011 -0700
fix: Graph wasn't able to be paused
When paused the graph would look paused, but if the screen was redrawn (such
as by switching pages or resizing) then the graph would update. The reason for
this was that the pause buffer was updating just like the primary instance.
---
src/cli/graphing/graphPanel.py | 7 ++++---
1 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index 615118a..b48f82b 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -72,6 +72,7 @@ class GraphStats(TorCtl.PostEventListener):
# panel to be redrawn when updated (set when added to GraphPanel)
self._graphPanel = None
self.isSelected = False
+ self.isPauseBuffer = False
# tracked stats
self.tick = 0 # number of processed events
@@ -114,6 +115,7 @@ class GraphStats(TorCtl.PostEventListener):
newCopy.maxSecondary = dict(self.maxSecondary)
newCopy.primaryCounts = copy.deepcopy(self.primaryCounts)
newCopy.secondaryCounts = copy.deepcopy(self.secondaryCounts)
+ newCopy.isPauseBuffer = True
return newCopy
def eventTick(self):
@@ -187,7 +189,7 @@ class GraphStats(TorCtl.PostEventListener):
pass
def bandwidth_event(self, event):
- self.eventTick()
+ if not self.isPauseBuffer: self.eventTick()
def _processEvent(self, primary, secondary):
"""
@@ -422,7 +424,6 @@ class GraphPanel(panel.Panel):
"""
stats._graphPanel = self
- stats.isPaused = True
self.stats[label] = stats
def setStats(self, label):
@@ -444,5 +445,5 @@ class GraphPanel(panel.Panel):
if attr == "stats":
# uses custom clone method to copy GraphStats instances
return dict([(key, self.stats[key].clone()) for key in self.stats])
- else: return panel.Panel.copyAttr(self, isPause)
+ else: return panel.Panel.copyAttr(self, attr)
1
0
17 Jul '11
commit cc9e375a079c7f8816bf0564ad2a82ef1ffb039e
Author: Damian Johnson <atagar(a)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)
1
0
[arm/release] fix: --debug flag broke if config-text unavailable
by atagar@torproject.org 17 Jul '11
by atagar@torproject.org 17 Jul '11
17 Jul '11
commit d6df7862474e728c31fea1edd9613a3e64dc73f8
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat May 21 13:59:20 2011 -0700
fix: --debug flag broke if config-text unavailable
When in debug mode arm attempted to log the results of 'GETINFO config-text'
without taking into account that this call may fail (this is a new config
option so this happens quite a bit). Caught by trogdor
---
src/starter.py | 5 +++--
1 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/starter.py b/src/starter.py
index 9268351..0454f9e 100644
--- a/src/starter.py
+++ b/src/starter.py
@@ -193,8 +193,9 @@ def _dumpConfig():
# dumps tor's version and configuration
torConfigEntry = "Tor (%s) Configuration:\n" % conn.getInfo("version")
- for line in conn.getInfo("config-text").split("\n"):
- if " " in line: key, value = line.split(" ", 1)
+ for line in conn.getInfo("config-text", "").split("\n"):
+ if not line: continue
+ elif " " in line: key, value = line.split(" ", 1)
else: key, value = line, ""
if key in PRIVATE_TORRC_ENTRIES:
1
0
17 Jul '11
commit 13ba8d61c35bb7ceff0514a68cd6ee99b55059c4
Author: Damian Johnson <atagar(a)torproject.org>
Date: Wed May 25 08:17:58 2011 -0700
fix: Man page had author's hard coded home
The debug flag referenced /home/atagar rather than ~ (caught by kuhkatz)
---
arm.1 | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/arm.1 b/arm.1
index 32a03cc..6a1fa19 100644
--- a/arm.1
+++ b/arm.1
@@ -31,7 +31,7 @@ user provided configuration file (default is \fB~/.armrc\fR)
.TP
\fB\-d\fR, \fB\-\-debug\fR
-writes all arm logs to /home/atagar/.arm/log
+writes all arm logs to ~/.arm/log
.TP
\fB\-b\fR, \fB\-\-blind\fR
1
0
17 Jul '11
commit 3941336e68bb6fd16b2fe4c6c8dd955262a90286
Author: Damian Johnson <atagar(a)torproject.org>
Date: Wed May 25 09:39:59 2011 -0700
Adding an option for reconnecting to Tor
If Tor is shut down then restarted this provides an option so the user can
press 'r' to reconnect to the new instance. Use cases are...
Success -> log and display message indicating success
Password Required -> prompt for password
Failure -> display message with a description of the problem
---
src/cli/controller.py | 16 +++++++++++-----
src/cli/headerPanel.py | 34 +++++++++++++++++++++++++++++++---
src/starter.py | 10 +++-------
src/util/connections.py | 18 ++++++++++++++++++
src/util/torTools.py | 45 +++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 108 insertions(+), 15 deletions(-)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index d9895e6..e94f7f6 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -18,7 +18,7 @@ import cli.graphing.connStats
import cli.graphing.resourceStats
import cli.connections.connPanel
-from util import connections, conf, enum, log, panel, sysTools, torConfig, torTools, uiTools
+from util import connections, conf, enum, log, panel, sysTools, torConfig, torTools
ARM_CONTROLLER = None
@@ -346,7 +346,7 @@ def heartbeatCheck(isUnresponsive):
conn = torTools.getConn()
lastHeartbeat = conn.getHeartbeat()
- if conn.isAlive() and "BW" in conn.getControllerEvents() and lastHeartbeat != 0:
+ if conn.isAlive() and "BW" in conn.getControllerEvents():
if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
isUnresponsive = True
log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
@@ -357,15 +357,21 @@ def heartbeatCheck(isUnresponsive):
return isUnresponsive
-def connResetListener(_, eventType):
+def connResetListener(conn, eventType):
"""
- Pauses connection resolution when tor's shut down, and resumes if started
- again.
+ Pauses connection resolution when tor's shut down, and resumes with the new
+ pid if started again.
"""
if connections.isResolverAlive("tor"):
resolver = connections.getResolver("tor")
resolver.setPaused(eventType == torTools.State.CLOSED)
+
+ if eventType == torTools.State.INIT:
+ torPid = conn.getMyPid()
+
+ if torPid and torPid != resolver.getPid():
+ resolver.setPid(torPid)
def startTorMonitor(startTime):
"""
diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py
index 102ef64..decf975 100644
--- a/src/cli/headerPanel.py
+++ b/src/cli/headerPanel.py
@@ -19,6 +19,8 @@ import time
import curses
import threading
+import cli.popups
+
from util import log, panel, sysTools, torTools, uiTools
# minimum width for which panel attempts to double up contents (two columns to
@@ -33,7 +35,9 @@ FLAG_COLORS = {"Authority": "white", "BadExit": "red", "BadDirectory": "red
VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green",
"old": "red", "unrecommended": "red", "unknown": "cyan"}
-DEFAULT_CONFIG = {"features.showFdUsage": False,
+DEFAULT_CONFIG = {"startup.interface.ipAddress": "127.0.0.1",
+ "startup.interface.port": 9051,
+ "features.showFdUsage": False,
"log.fdUsageSixtyPercent": log.NOTICE,
"log.fdUsageNinetyPercent": log.WARN}
@@ -107,6 +111,29 @@ class HeaderPanel(panel.Panel, threading.Thread):
if self.vals["tor/orPort"]: return 4 if isWide else 6
else: return 3 if isWide else 4
+ def handleKey(self, key):
+ isKeystrokeConsumed = True
+
+ if key in (ord('r'), ord('R')) and not self._isTorConnected:
+ try:
+ ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"]
+ tmpConn, authType, authValue = torTools.getConnectionComponents(ctlAddr, ctlPort)
+
+ if authType == torTools.AUTH_TYPE.PASSWORD:
+ authValue = cli.popups.inputPrompt("Controller Password: ")
+ if not authValue: raise IOError() # cancel reconnection
+
+ tmpConn.authenticate(authValue)
+ torTools.getConn().init(tmpConn)
+ log.log(log.NOTICE, "Reconnected to Tor's control port")
+ cli.popups.showMsg("Tor reconnected", 1)
+ except Exception, exc:
+ # displays notice for failed connection attempt
+ if exc.args: cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
+ else: isKeystrokeConsumed = False
+
+ return isKeystrokeConsumed
+
def draw(self, width, height):
self.valsLock.acquire()
isWide = width + 1 >= MIN_DUAL_COL_WIDTH
@@ -233,7 +260,8 @@ class HeaderPanel(panel.Panel, threading.Thread):
else:
statusTime = torTools.getConn().getStatus()[1]
statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime))
- self.addfstr(2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % statusTimeLabel)
+ msg = "<b><red>Tor Disconnected</red></b> (%s) - press r to reconnect" % statusTimeLabel
+ self.addfstr(2 if isWide else 4, 0, msg)
# Undisplayed / Line 3 Right (exit policy)
if isWide:
@@ -313,7 +341,7 @@ class HeaderPanel(panel.Panel, threading.Thread):
self._cond.notifyAll()
self._cond.release()
- def resetListener(self, conn, eventType):
+ def resetListener(self, _, eventType):
"""
Updates static parameters on tor reload (sighup) events.
diff --git a/src/starter.py b/src/starter.py
index 0454f9e..b41b18b 100644
--- a/src/starter.py
+++ b/src/starter.py
@@ -11,7 +11,6 @@ import sys
import time
import getopt
import locale
-import socket
import platform
import version
@@ -327,7 +326,7 @@ if __name__ == '__main__':
# sets up TorCtl connection, prompting for the passphrase if necessary and
# sending problems to stdout if they arise
- TorCtl.INCORRECT_PASSWORD_MSG = "Controller password found in '%s' was incorrect" % configPath
+ TorCtl.TorCtl.INCORRECT_PASSWORD_MSG = "Controller password found in '%s' was incorrect" % configPath
authPassword = config.get("startup.controlPassword", CONFIG["startup.controlPassword"])
conn = TorCtl.TorCtl.connect(controlAddr, controlPort, authPassword)
if conn == None:
@@ -344,12 +343,9 @@ if __name__ == '__main__':
# making this a much bigger hack).
try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.connect((controlAddr, controlPort))
- tmpConn = TorCtl.TorCtl.Connection(s)
+ tmpConn, authType, cookiePath = util.torTools.getConnectionComponents(controlAddr, controlPort)
- if tmpConn.get_auth_type() == TorCtl.TorCtl.AUTH_TYPE.COOKIE:
- cookiePath = tmpConn.get_auth_cookie_path()
+ if authType == util.torTools.AUTH_TYPE.COOKIE:
torPid = util.torTools.getPid(controlPort)
if torPid and cookiePath[0] != "/":
diff --git a/src/util/connections.py b/src/util/connections.py
index c090893..47aa8af 100644
--- a/src/util/connections.py
+++ b/src/util/connections.py
@@ -538,6 +538,24 @@ class ConnectionResolver(threading.Thread):
return self._resolutionCounter
+ def getPid(self):
+ """
+ Provides the pid used to narrow down connection resolution. This is an
+ empty string if undefined.
+ """
+
+ return self.processPid
+
+ def setPid(self, processPid):
+ """
+ Sets the pid used to narrow down connection resultions.
+
+ Arguments:
+ processPid - pid for the process we're fetching connections for
+ """
+
+ self.processPid = processPid
+
def setPaused(self, isPause):
"""
Allows or prevents further connection resolutions (this still makes use of
diff --git a/src/util/torTools.py b/src/util/torTools.py
index 12af772..d7ffa4d 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -25,6 +25,9 @@ from util import enum, log, procTools, sysTools, uiTools
# CLOSED - control port closed
State = enum.Enum("INIT", "CLOSED")
+# enums for authentication on the tor control port
+AUTH_TYPE = enum.Enum("NONE", "COOKIE", "PASSWORD")
+
# Addresses of the default directory authorities for tor version 0.2.3.0-alpha
# (this comes from the dirservers array in src/or/config.c).
DIR_SERVERS = [("86.59.21.38", "80"), # tor26
@@ -301,6 +304,45 @@ def parseVersion(versionStr):
return result
+def getConnectionComponents(controlAddr="127.0.0.1", controlPort=9051):
+ """
+ Provides an uninitiated torctl connection for the control port. This returns
+ a tuple with the...
+ (torctl connection, authType, authValue)
+
+ The authValue corresponds to the cookie path if using an authentication
+ cookie. Otherwise this is the empty string. This raises an IOError if unable
+ to connect.
+
+ Arguments:
+ controlAddr - ip address belonging to the controller
+ controlPort - port belonging to the controller
+ """
+
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect((controlAddr, controlPort))
+ conn = TorCtl.Connection(s)
+ authType, authValue = conn.get_auth_type(), ""
+
+ if authType == TorCtl.AUTH_TYPE.COOKIE:
+ authValue = conn.get_auth_cookie_path()
+
+ # converts to our enum type
+ if authType == TorCtl.AUTH_TYPE.NONE:
+ authType = AUTH_TYPE.NONE
+ elif authType == TorCtl.AUTH_TYPE.COOKIE:
+ authType = AUTH_TYPE.COOKIE
+ elif authType == TorCtl.AUTH_TYPE.PASSWORD:
+ authType = AUTH_TYPE.PASSWORD
+
+ return (conn, authType, authValue)
+ except socket.error, exc:
+ if "Connection refused" in exc.args:
+ raise IOError("Connection refused. Is the ControlPort enabled?")
+ else: raise IOError("Failed to establish socket: %s" % exc)
+ except Exception, exc: raise IOError(exc)
+
def getConn():
"""
Singleton constructor for a Controller. Be aware that this starts as being
@@ -385,6 +427,9 @@ class Controller(TorCtl.PostEventListener):
self.conn.add_event_listener(self)
for listener in self.eventListeners: self.conn.add_event_listener(listener)
+ # registers this as our first heartbeat
+ self._updateHeartbeat()
+
# reset caches for ip -> fingerprint lookups
self._fingerprintMappings = None
self._fingerprintLookupCache = {}
1
0
17 Jul '11
commit 8e90af9c9eb4af4f3fa12d7f12c19279314d8b64
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 9 22:04:21 2011 -0700
fix: Config value changes weren't being reflected
When a value was edited in the configuration panel the listed value didn't
change (though it did in other places like the detail panel). This was due to a
cached copy of the display element. Flushing this cache when editing values.
---
src/cli/configPanel.py | 3 +++
1 files changed, 3 insertions(+), 0 deletions(-)
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index 37b7ddb..b6fba8b 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -293,6 +293,9 @@ class ConfigPanel(panel.Panel):
torTools.getConn().setOption(configOption, newValue)
+ # forces the label to be remade with the new value
+ selection.labelCache = None
+
# resets the isDefault flag
customOptions = torConfig.getCustomOptions()
selection.fields[Field.IS_DEFAULT] = not configOption in customOptions
1
0
17 Jul '11
commit ea8799aab65ad6509972ddebcdd0c3eb7f3e9769
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 9 19:14:28 2011 -0700
fix: Never show conn details if panel's empty
The connection panel could get stuck in the 'show connection details' mode if
the panel's contents became empty while we were showing something. This
wouldn't crash (like with the issue addressed in the previous fix), but the
panel title would be wrong. This reverts the 'show details' flag if we don't
have any contents.
---
src/cli/connections/connPanel.py | 5 ++++-
1 files changed, 4 insertions(+), 1 deletions(-)
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 36687ac..8ecffc2 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -151,7 +151,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
if isChanged: self.redraw(True)
- elif uiTools.isSelectionKey(key) and self._entries:
+ elif uiTools.isSelectionKey(key):
self._showDetails = not self._showDetails
self.redraw(True)
elif key == ord('s') or key == ord('S'):
@@ -248,6 +248,9 @@ class ConnectionPanel(panel.Panel, threading.Thread):
def draw(self, width, height):
self.valsLock.acquire()
+ # if we don't have any contents then refuse to show details
+ if not self._entries: self._showDetails = False
+
# extra line when showing the detail panel is for the bottom border
detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
1
0
17 Jul '11
commit 52998db3600cdc42e4290b9b3fa5547390a2893a
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 9 22:02:03 2011 -0700
Moving config value editing to the config panel
---
src/cli/configPanel.py | 46 ++++++++++++++++++++++++++++++++++++++
src/cli/controller.py | 57 ------------------------------------------------
src/cli/popups.py | 14 ++++++-----
3 files changed, 54 insertions(+), 63 deletions(-)
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index 31a78ab..37b7ddb 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -11,6 +11,7 @@ import popups
from util import conf, enum, panel, torTools, torConfig, uiTools
DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
+ "features.config.prepopulateEditValues": True,
"features.config.state.showPrivateOptions": False,
"features.config.state.showVirtualOptions": False,
"features.config.state.colWidth.option": 25,
@@ -135,6 +136,14 @@ class ConfigEntry():
return self.labelCache
+ def isUnset(self):
+ """
+ True if we have no value, false otherwise.
+ """
+
+ confValue = torTools.getConn().getOption(self.get(Field.OPTION), [], True)
+ return not bool(confValue)
+
def _getValue(self):
"""
Provides the current value of the configuration entry, taking advantage of
@@ -256,6 +265,43 @@ class ConfigPanel(panel.Panel):
isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight)
if isChanged: self.redraw(True)
+ elif uiTools.isSelectionKey(key):
+ # Prompts the user to edit the selected configuration value. The
+ # interface is locked to prevent updates between setting the value
+ # and showing any errors.
+
+ panel.CURSES_LOCK.acquire()
+ try:
+ selection = self.getSelection()
+ configOption = selection.get(Field.OPTION)
+ if selection.isUnset(): initialValue = ""
+ else: initialValue = selection.get(Field.VALUE)
+
+ promptMsg = "%s Value (esc to cancel): " % configOption
+ isPrepopulated = self._config["features.config.prepopulateEditValues"]
+ newValue = popups.inputPrompt(promptMsg, initialValue if isPrepopulated else "")
+
+ if newValue != None and newValue != initialValue:
+ try:
+ if selection.get(Field.TYPE) == "Boolean":
+ # if the value's a boolean then allow for 'true' and 'false' inputs
+ if newValue.lower() == "true": newValue = "1"
+ elif newValue.lower() == "false": newValue = "0"
+ elif selection.get(Field.TYPE) == "LineList":
+ # setOption accepts list inputs when there's multiple values
+ newValue = newValue.split(",")
+
+ torTools.getConn().setOption(configOption, newValue)
+
+ # resets the isDefault flag
+ customOptions = torConfig.getCustomOptions()
+ selection.fields[Field.IS_DEFAULT] = not configOption in customOptions
+
+ self.redraw(True)
+ except Exception, exc:
+ popups.showMsg("%s (press any key)" % exc)
+ finally:
+ panel.CURSES_LOCK.release()
elif key == ord('a') or key == ord('A'):
self.showAll = not self.showAll
self.redraw(True)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 3d40631..63b3b86 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -98,7 +98,6 @@ PAGES = [
CONFIG = {"log.torrc.readFailed": log.WARN,
"features.graph.type": 1,
- "features.config.prepopulateEditValues": True,
"queries.refreshRate.rate": 5,
"log.torEventTypeUnrecognized": log.NOTICE,
"features.graph.bw.prepopulate": True,
@@ -1101,62 +1100,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panel.CURSES_LOCK.release()
panels["config"].redraw(True)
- elif page == 2 and uiTools.isSelectionKey(key):
- # let the user edit the configuration value, unchanged if left blank
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- selection = panels["config"].getSelection()
- configOption = selection.get(configPanel.Field.OPTION)
- titleMsg = "%s Value (esc to cancel): " % configOption
- panels["control"].setMsg(titleMsg)
- panels["control"].redraw(True)
-
- displayWidth = panels["control"].getPreferredSize()[1]
- initialValue = selection.get(configPanel.Field.VALUE)
-
- # initial input for the text field
- initialText = ""
- if CONFIG["features.config.prepopulateEditValues"] and initialValue != "<none>":
- initialText = initialValue
-
- newConfigValue = panels["control"].getstr(0, len(titleMsg), initialText)
-
- # it would be nice to quit on esc, but looks like this might not be possible...
- if newConfigValue != None and newConfigValue != initialValue:
- conn = torTools.getConn()
-
- # if the value's a boolean then allow for 'true' and 'false' inputs
- if selection.get(configPanel.Field.TYPE) == "Boolean":
- if newConfigValue.lower() == "true": newConfigValue = "1"
- elif newConfigValue.lower() == "false": newConfigValue = "0"
-
- try:
- if selection.get(configPanel.Field.TYPE) == "LineList":
- newConfigValue = newConfigValue.split(",")
-
- conn.setOption(configOption, newConfigValue)
-
- # resets the isDefault flag
- customOptions = torConfig.getCustomOptions()
- selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
-
- panels["config"].redraw(True)
- except Exception, exc:
- errorMsg = "%s (press any key)" % exc
- panels["control"].setMsg(uiTools.cropStr(errorMsg, displayWidth), curses.A_STANDOUT)
- panels["control"].redraw(True)
-
- curses.cbreak() # wait indefinitely for key presses (no timeout)
- stdscr.getch()
- curses.halfdelay(REFRESH_RATE * 10)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
diff --git a/src/cli/popups.py b/src/cli/popups.py
index 81b4589..2b02685 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -44,32 +44,33 @@ def finalize():
controller.refresh()
panel.CURSES_LOCK.release()
-def inputPrompt(msg):
+def inputPrompt(msg, initialValue = ""):
"""
Prompts the user to enter a string on the control line (which usually
displays the page number and basic controls).
Arguments:
- msg - message to prompt the user for input with
+ msg - message to prompt the user for input with
+ initialValue - initial value of the field
"""
panel.CURSES_LOCK.acquire()
controlPanel = controller.getPanel("control")
controlPanel.setMsg(msg)
controlPanel.redraw(True)
- userInput = controlPanel.getstr(0, len(msg))
+ userInput = controlPanel.getstr(0, len(msg), initialValue)
controlPanel.revertMsg()
panel.CURSES_LOCK.release()
return userInput
-def showMsg(msg, maxWait, attr = curses.A_STANDOUT):
+def showMsg(msg, maxWait = -1, attr = curses.A_STANDOUT):
"""
Displays a single line message on the control line for a set time. Pressing
any key will end the message.
Arguments:
msg - message to be displayed to the user
- maxWait - time to show the message
+ maxWait - time to show the message, indefinite if -1
attr - attributes with which to draw the message
"""
@@ -78,7 +79,8 @@ def showMsg(msg, maxWait, attr = curses.A_STANDOUT):
controlPanel.setMsg(msg, attr)
controlPanel.redraw(True)
- curses.halfdelay(maxWait * 10)
+ if maxWait == -1: curses.cbreak()
+ else: curses.halfdelay(maxWait * 10)
controller.getScreen().getch()
controlPanel.revertMsg()
curses.halfdelay(controller.REFRESH_RATE * 10)
1
0
17 Jul '11
commit e11f75cff8e5623606242537f4e0e2f26c311cf7
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue May 10 09:10:18 2011 -0700
Moving the config save dialog to the config panel
---
src/cli/configPanel.py | 117 +++++++++++++++++++++++++++++++++++++++++++
src/cli/controller.py | 129 ------------------------------------------------
2 files changed, 117 insertions(+), 129 deletions(-)
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index b6fba8b..f5ff442 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -3,9 +3,11 @@ Panel presenting the configuration state for tor or arm. Options can be edited
and the resulting configuration files saved.
"""
+import os
import curses
import threading
+import controller
import popups
from util import conf, enum, panel, torTools, torConfig, uiTools
@@ -320,6 +322,121 @@ class ConfigPanel(panel.Panel):
# converts labels back to enums
resultEnums = [getFieldFromLabel(label) for label in results]
self.setSortOrder(resultEnums)
+ elif key == ord('w') or key == ord('W'):
+ # display a popup for saving the current configuration
+ configLines = torConfig.getCustomOptions(True)
+ popup, width, height = popups.init(len(configLines) + 2)
+ if not popup: return
+
+ try:
+ # displayed options (truncating the labels if there's limited room)
+ if width >= 30: selectionOptions = ("Save", "Save As...", "Cancel")
+ else: selectionOptions = ("Save", "Save As", "X")
+
+ # checks if we can show options beside the last line of visible content
+ isOptionLineSeparate = False
+ lastIndex = min(height - 2, len(configLines) - 1)
+
+ # if we don't have room to display the selection options and room to
+ # grow then display the selection options on its own line
+ if width < (30 + len(configLines[lastIndex])):
+ popup.setHeight(height + 1)
+ popup.redraw(True) # recreates the window instance
+ newHeight, _ = popup.getPreferredSize()
+
+ if newHeight > height:
+ height = newHeight
+ isOptionLineSeparate = True
+
+ key, selection = 0, 2
+ while not uiTools.isSelectionKey(key):
+ # if the popup has been resized then recreate it (needed for the
+ # proper border height)
+ newHeight, newWidth = popup.getPreferredSize()
+ if (height, width) != (newHeight, newWidth):
+ height, width = newHeight, newWidth
+ popup.redraw(True)
+
+ # if there isn't room to display the popup then cancel it
+ if height <= 2:
+ selection = 2
+ break
+
+ popup.win.erase()
+ popup.win.box()
+ popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
+
+ visibleConfigLines = height - 3 if isOptionLineSeparate else height - 2
+ for i in range(visibleConfigLines):
+ line = uiTools.cropStr(configLines[i], width - 2)
+
+ if " " in line:
+ option, arg = line.split(" ", 1)
+ popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green"))
+ popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan"))
+ else:
+ popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green"))
+
+ # draws 'T' between the lower left and the covered panel's scroll bar
+ if width > 1: popup.win.addch(height - 1, 1, curses.ACS_TTEE)
+
+ # draws selection options (drawn right to left)
+ drawX = width - 1
+ for i in range(len(selectionOptions) - 1, -1, -1):
+ optionLabel = selectionOptions[i]
+ drawX -= (len(optionLabel) + 2)
+
+ # if we've run out of room then drop the option (this will only
+ # occure on tiny displays)
+ if drawX < 1: break
+
+ selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+ popup.addstr(height - 2, drawX, "[")
+ popup.addstr(height - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
+ popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]")
+
+ drawX -= 1 # space gap between the options
+
+ popup.win.refresh()
+
+ key = controller.getScreen().getch()
+ if key == curses.KEY_LEFT: selection = max(0, selection - 1)
+ elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
+
+ if selection in (0, 1):
+ loadedTorrc = torConfig.getTorrc()
+ try: configLocation = loadedTorrc.getConfigLocation()
+ except IOError: configLocation = ""
+
+ if selection == 1:
+ # prompts user for a configuration location
+ configLocation = popups.inputPrompt("Save to (esc to cancel): ", configLocation)
+ if configLocation: configLocation = os.path.abspath(configLocation)
+
+ if configLocation:
+ try:
+ # make dir if the path doesn't already exist
+ baseDir = os.path.dirname(configLocation)
+ if not os.path.exists(baseDir): os.makedirs(baseDir)
+
+ # saves the configuration to the file
+ configFile = open(configLocation, "w")
+ configFile.write("\n".join(configLines))
+ configFile.close()
+
+ # reloads the cached torrc if overwriting it
+ if configLocation == loadedTorrc.getConfigLocation():
+ try:
+ loadedTorrc.load()
+ panels["torrc"]._lastContentHeightArgs = None
+ except IOError: pass
+
+ msg = "Saved configuration to %s" % configLocation
+ except (IOError, OSError), exc:
+ msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
+
+ popups.showMsg(msg, 2)
+ finally: popups.finalize()
else: isKeystrokeConsumed = False
self.valsLock.release()
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 63b3b86..c4b9442 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -971,135 +971,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
finally:
panel.CURSES_LOCK.release()
- elif page == 2 and (key == ord('w') or key == ord('W')):
- # display a popup for saving the current configuration
- panel.CURSES_LOCK.acquire()
- try:
- configLines = torConfig.getCustomOptions(True)
-
- # lists event types
- popup = panels["popup"]
- popup.height = len(configLines) + 3
- popup.recreate(stdscr)
- displayHeight, displayWidth = panels["popup"].getPreferredSize()
-
- # displayed options (truncating the labels if there's limited room)
- if displayWidth >= 30: selectionOptions = ("Save", "Save As...", "Cancel")
- else: selectionOptions = ("Save", "Save As", "X")
-
- # checks if we can show options beside the last line of visible content
- lastIndex = min(displayHeight - 3, len(configLines) - 1)
- isOptionLineSeparate = displayWidth < (30 + len(configLines[lastIndex]))
-
- # if we're showing all the content and have room to display selection
- # options besides the text then shrink the popup by a row
- if not isOptionLineSeparate and displayHeight == len(configLines) + 3:
- popup.height -= 1
- popup.recreate(stdscr)
-
- key, selection = 0, 2
- while not uiTools.isSelectionKey(key):
- # if the popup has been resized then recreate it (needed for the
- # proper border height)
- newHeight, newWidth = panels["popup"].getPreferredSize()
- if (displayHeight, displayWidth) != (newHeight, newWidth):
- displayHeight, displayWidth = newHeight, newWidth
- popup.recreate(stdscr)
-
- # if there isn't room to display the popup then cancel it
- if displayHeight <= 2:
- selection = 2
- break
-
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
-
- visibleConfigLines = displayHeight - 3 if isOptionLineSeparate else displayHeight - 2
- for i in range(visibleConfigLines):
- line = uiTools.cropStr(configLines[i], displayWidth - 2)
-
- if " " in line:
- option, arg = line.split(" ", 1)
- popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green"))
- popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan"))
- else:
- popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green"))
-
- # draws 'T' between the lower left and the covered panel's scroll bar
- if displayWidth > 1: popup.win.addch(displayHeight - 1, 1, curses.ACS_TTEE)
-
- # draws selection options (drawn right to left)
- drawX = displayWidth - 1
- for i in range(len(selectionOptions) - 1, -1, -1):
- optionLabel = selectionOptions[i]
- drawX -= (len(optionLabel) + 2)
-
- # if we've run out of room then drop the option (this will only
- # occure on tiny displays)
- if drawX < 1: break
-
- selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
- popup.addstr(displayHeight - 2, drawX, "[")
- popup.addstr(displayHeight - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
- popup.addstr(displayHeight - 2, drawX + len(optionLabel) + 1, "]")
-
- drawX -= 1 # space gap between the options
-
- popup.refresh()
-
- key = stdscr.getch()
- if key == curses.KEY_LEFT: selection = max(0, selection - 1)
- elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
-
- if selection in (0, 1):
- loadedTorrc = torConfig.getTorrc()
- try: configLocation = loadedTorrc.getConfigLocation()
- except IOError: configLocation = ""
-
- if selection == 1:
- # prompts user for a configuration location
- promptMsg = "Save to (esc to cancel): "
- panels["control"].setMsg(promptMsg)
- panels["control"].redraw(True)
- configLocation = panels["control"].getstr(0, len(promptMsg), configLocation)
- if configLocation: configLocation = os.path.abspath(configLocation)
-
- if configLocation:
- try:
- # make dir if the path doesn't already exist
- baseDir = os.path.dirname(configLocation)
- if not os.path.exists(baseDir): os.makedirs(baseDir)
-
- # saves the configuration to the file
- configFile = open(configLocation, "w")
- configFile.write("\n".join(configLines))
- configFile.close()
-
- # reloads the cached torrc if overwriting it
- if configLocation == loadedTorrc.getConfigLocation():
- try:
- loadedTorrc.load()
- panels["torrc"]._lastContentHeightArgs = None
- except IOError: pass
-
- msg = "Saved configuration to %s" % configLocation
- except (IOError, OSError), exc:
- msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
-
- panels["control"].setMsg(msg, curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-
- # reverts popup dimensions
- popup.height = 9
- popup.recreate(stdscr, 80)
- finally:
- panel.CURSES_LOCK.release()
-
- panels["config"].redraw(True)
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
1
0