Author: atagar
Date: 2011-03-28 03:21:54 +0000 (Mon, 28 Mar 2011)
New Revision: 24473
Modified:
arm/trunk/armrc.sample
arm/trunk/src/interface/connections/connEntry.py
arm/trunk/src/interface/connections/connPanel.py
arm/trunk/src/util/connections.py
Log:
Identifying local application attached to the control or socks port.
Modified: arm/trunk/armrc.sample
===================================================================
--- arm/trunk/armrc.sample 2011-03-27 23:51:07 UTC (rev 24472)
+++ arm/trunk/armrc.sample 2011-03-28 03:21:54 UTC (rev 24473)
@@ -188,7 +188,7 @@
features.connection.newPanel true
features.connection.listingType 0
features.connection.order 0, 2, 1
-features.connection.refreshRate 10
+features.connection.refreshRate 5
features.connection.markInitialConnections true
features.connection.showExitPort true
features.connection.showColumn.fingerprint true
Modified: arm/trunk/src/interface/connections/connEntry.py
===================================================================
--- arm/trunk/src/interface/connections/connEntry.py 2011-03-27 23:51:07 UTC (rev 24472)
+++ arm/trunk/src/interface/connections/connEntry.py 2011-03-28 03:21:54 UTC (rev 24473)
@@ -18,7 +18,7 @@
# Directory Fetching tor consensus information.
# Control Tor controller (arm, vidalia, etc).
-Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "CLIENT", "PROGRAM", "DIRECTORY", "CONTROL")
+Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "CLIENT", "DIRECTORY", "PROGRAM", "CONTROL")
CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue",
Category.EXIT: "red", Category.CLIENT: "cyan",
Category.PROGRAM: "yellow", Category.DIRECTORY: "magenta",
@@ -199,6 +199,11 @@
self._possibleClient = True
self._possibleDirectory = True
+ # attributes for PROGRAM and CONTROL connections
+ self.appName = None
+ self.appPid = None
+ self.isAppResolving = False
+
conn = torTools.getConn()
myOrPort = conn.getOption("ORPort")
myDirPort = conn.getOption("DirPort")
@@ -278,6 +283,13 @@
return myListing
+ def isUnresolvedApp(self):
+ """
+ True if our display uses application information that hasn't yet been resolved.
+ """
+
+ return self.appName == None and self.getType() in (Category.PROGRAM, Category.CONTROL)
+
def _getListingEntry(self, width, currentTime, listingType):
entryType = self.getType()
@@ -415,6 +427,20 @@
listingType - primary attribute we're listing connections by
"""
+ # for applications show the command/pid
+ if self.getType() in (Category.PROGRAM, Category.CONTROL):
+ displayLabel = ""
+
+ if self.appName:
+ if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid)
+ else: displayLabel = self.appName
+ elif self.isAppResolving:
+ displayLabel = "resolving..."
+ else: displayLabel = "UNKNOWN"
+
+ return displayLabel
+
+ # for everything else display connection/consensus information
dstAddress = self.getDestinationLabel(26, includeLocale = True)
etc, usedSpace = "", 0
if listingType == entries.ListingType.IP_ADDRESS:
Modified: arm/trunk/src/interface/connections/connPanel.py
===================================================================
--- arm/trunk/src/interface/connections/connPanel.py 2011-03-27 23:51:07 UTC (rev 24472)
+++ arm/trunk/src/interface/connections/connPanel.py 2011-03-28 03:21:54 UTC (rev 24473)
@@ -10,7 +10,7 @@
from util import connections, enum, panel, torTools, uiTools
DEFAULT_CONFIG = {"features.connection.listingType": 0,
- "features.connection.refreshRate": 10}
+ "features.connection.refreshRate": 5}
# height of the detail panel content, not counting top and bottom border
DETAILS_HEIGHT = 7
@@ -63,8 +63,15 @@
# it changes.
self._lastResourceFetch = -1
- self._update() # populates initial entries
+ # resolver for the command/pid associated with PROGRAM and CONTROL connections
+ self._appResolver = connections.AppResolver("arm")
+ # rate limits appResolver queries to once per update
+ self.appResolveSinceUpdate = False
+
+ self._update() # populates initial entries
+ self._resolveApps(False) # resolves initial PROGRAM and CONTROL applications
+
# mark the initially exitsing connection uptimes as being estimates
for entry in self._entries:
if isinstance(entry, connEntry.ConnectionEntry):
@@ -155,7 +162,11 @@
# updates content if their's new results, otherwise just redraws
self._update()
self.redraw(True)
- lastDraw += self._config["features.connection.refreshRate"]
+
+ # we may have missed multiple updates due to being paused, showing
+ # another panel, etc so lastDraw might need to jump multiple ticks
+ drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
+ lastDraw += self._config["features.connection.refreshRate"] * drawTicks
def draw(self, width, height):
self.valsLock.acquire()
@@ -191,6 +202,11 @@
for lineNum in range(scrollLoc, len(self._entryLines)):
entryLine = self._entryLines[lineNum]
+ # if this is an unresolved PROGRAM or CONTROL entry then queue up
+ # resolution for the applicaitions they belong to
+ if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp():
+ self._resolveApps()
+
# hilighting if this is the selected line
extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
@@ -218,6 +234,7 @@
connResolver = connections.getResolver("tor")
currentResolutionCount = connResolver.getResolutionCount()
+ self.appResolveSinceUpdate = False
if self._lastResourceFetch != currentResolutionCount:
self.valsLock.acquire()
@@ -304,4 +321,55 @@
self.setSortOrder()
self._lastResourceFetch = currentResolutionCount
self.valsLock.release()
+
+ def _resolveApps(self, flagQuery = True):
+ """
+ Triggers an asynchronous query for all unresolved PROGRAM and CONTROL
+ entries.
+
+ Arguments:
+ flagQuery - sets a flag to prevent further call from being respected
+ until the next update if true
+ """
+
+ if self.appResolveSinceUpdate: return
+
+ # fetch the unresolved PROGRAM and CONTROL lines
+ unresolvedLines = []
+
+ for line in self._entryLines:
+ if isinstance(line, connEntry.ConnectionLine) and line.isUnresolvedApp():
+ unresolvedLines.append(line)
+
+ # Queue up resolution for the unresolved ports (skips if it's still working
+ # on the last query).
+ if not self._appResolver.isResolving:
+ self._appResolver.resolve([line.foreign.getPort() for line in unresolvedLines])
+
+ # The application resolver might have given up querying (for instance, if
+ # the lsof lookups aren't working on this platform or lacks permissions).
+ # The isAppResolving flag lets the unresolved entries indicate if there's
+ # a lookup in progress for them or not.
+
+ for line in unresolvedLines:
+ line.isAppResolving = self._appResolver.isResolving
+
+ # Fetches results. If the query finishes quickly then this is what we just
+ # asked for, otherwise these belong to the last resolution.
+ appResults = self._appResolver.getResults(0.02)
+
+ for line in unresolvedLines:
+ linePort = line.foreign.getPort()
+
+ if linePort in appResults:
+ # sets application attributes if there's a result with this as the
+ # inbound port
+ for inboundPort, outboundPort, cmd, pid in appResults[linePort]:
+ if linePort == inboundPort:
+ line.appName = cmd
+ line.appPid = pid
+ line.isAppResolving = False
+
+ if flagQuery:
+ self.appResolveSinceUpdate = True
Modified: arm/trunk/src/util/connections.py
===================================================================
--- arm/trunk/src/util/connections.py 2011-03-27 23:51:07 UTC (rev 24472)
+++ arm/trunk/src/util/connections.py 2011-03-28 03:21:54 UTC (rev 24473)
@@ -559,3 +559,168 @@
self._cond.notifyAll()
self._cond.release()
+class AppResolver:
+ """
+ Provides the names and pids of appliations attached to the given ports. This
+ stops attempting to query if it fails three times without successfully
+ getting lsof results.
+ """
+
+ def __init__(self, scriptName = "python"):
+ """
+ Constructs a resolver instance.
+
+ Arguments:
+ scriptName - name by which to all our own entries
+ """
+
+ self.scriptName = scriptName
+ self.queryResults = {}
+ self.resultsLock = threading.RLock()
+ self._cond = threading.Condition() # used for pausing when waiting for results
+ self.isResolving = False # flag set if we're in the process of making a query
+ self.failureCount = 0 # -1 if we've made a successful query
+
+ def getResults(self, maxWait=0):
+ """
+ Provides the last queried results. If we're in the process of making a
+ query then we can optionally block for a time to see if it finishes.
+
+ Arguments:
+ maxWait - maximum second duration to block on getting results before
+ returning
+ """
+
+ self._cond.acquire()
+ if self.isResolving and maxWait > 0:
+ self._cond.wait(maxWait)
+ self._cond.release()
+
+ self.resultsLock.acquire()
+ results = dict(self.queryResults)
+ self.resultsLock.release()
+
+ return results
+
+ def resolve(self, ports):
+ """
+ Queues the given listing of ports to be resolved. This clears the last set
+ of results when completed.
+
+ Arguments:
+ ports - list of ports to be resolved to applications
+ """
+
+ if self.failureCount < 3:
+ self.isResolving = True
+ t = threading.Thread(target = self._queryApplications, kwargs = {"ports": ports})
+ t.setDaemon(True)
+ t.start()
+
+ def _queryApplications(self, ports=[]):
+ """
+ Performs an lsof lookup on the given ports to get the command/pid tuples.
+
+ Arguments:
+ ports - list of ports to be resolved to applications
+ """
+
+ # atagar@fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277
+ # COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
+ # tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (ESTABLISHED)
+ # tor 2001 atagar 15u IPv4 22024 0t0 TCP localhost:9051->localhost:51849 (ESTABLISHED)
+ # python 2462 atagar 3u IPv4 14047 0t0 TCP localhost:37277->localhost:9051 (ESTABLISHED)
+ # python 3444 atagar 3u IPv4 22023 0t0 TCP localhost:51849->localhost:9051 (ESTABLISHED)
+
+ if not ports:
+ self.resultsLock.acquire()
+ self.queryResults = {}
+ self.isResolving = False
+ self.resultsLock.release()
+
+ # wakes threads waiting on results
+ self._cond.acquire()
+ self._cond.notifyAll()
+ self._cond.release()
+
+ return
+
+ results = {}
+ lsofArgs = []
+
+ # Uses results from the last query if we have any, otherwise appends the
+ # port to the lsof command. This has the potential for persisting dirty
+ # results but if we're querying by the dynamic port on the local tcp
+ # connections then this should be very rare (and definitely worth the
+ # chance of being able to skip an lsof query altogether).
+ for port in ports:
+ if port in self.queryResults:
+ results[port] = self.queryResults[port]
+ else: lsofArgs.append("-i tcp:%s" % port)
+
+ if lsofArgs:
+ lsofResults = sysTools.call("lsof " + " ".join(lsofArgs))
+ else: lsofResults = None
+
+ if not lsofResults and self.failureCount != -1:
+ # lsof query failed and we aren't yet sure if it's possible to
+ # successfuly get results on this platform
+ self.failureCount += 1
+ self.isResolving = False
+ return
+ elif lsofResults:
+ # (iPort, oPort) tuple for our own process, if it was fetched
+ ourConnection = None
+
+ for line in lsofResults:
+ lineComp = line.split()
+
+ if len(lineComp) == 10 and lineComp[9] == "(ESTABLISHED)":
+ cmd, pid, _, _, _, _, _, _, portMap, _ = lineComp
+
+ if "->" in portMap:
+ iPort, oPort = portMap.split("->")
+ iPort = iPort.replace("localhost:", "")
+ oPort = oPort.replace("localhost:", "")
+
+ # entry belongs to our own process
+ if pid == str(os.getpid()):
+ cmd = self.scriptName
+ ourConnection = (iPort, oPort)
+
+ if iPort.isdigit() and oPort.isdigit():
+ newEntry = (iPort, oPort, cmd, pid)
+
+ # adds the entry under the key of whatever we queried it with
+ # (this might be both the inbound _and_ outbound ports)
+ for portMatch in (iPort, oPort):
+ if portMatch in ports:
+ if portMatch in results:
+ results[portMatch].append(newEntry)
+ else: results[portMatch] = [newEntry]
+
+ # making the lsof call generated an extranious sh entry for our own connection
+ if ourConnection:
+ for ourPort in ourConnection:
+ if ourPort in results:
+ shIndex = None
+
+ for i in range(len(results[ourPort])):
+ if results[ourPort][i][2] == "sh":
+ shIndex = i
+ break
+
+ if shIndex != None:
+ del results[ourPort][shIndex]
+
+ self.resultsLock.acquire()
+ self.failureCount = -1
+ self.queryResults = results
+ self.isResolving = False
+ self.resultsLock.release()
+
+ # wakes threads waiting on results
+ self._cond.acquire()
+ self._cond.notifyAll()
+ self._cond.release()
+