[or-cvs] r20678: {arm} More issues discussed on irc. fix: made netstat lookups a be (in arm/trunk: . interface)
atagar at seul.org
atagar at seul.org
Mon Sep 28 06:56:21 UTC 2009
Author: atagar
Date: 2009-09-28 02:56:21 -0400 (Mon, 28 Sep 2009)
New Revision: 20678
Added:
arm/trunk/interface/connResolver.py
Modified:
arm/trunk/ChangeLog
arm/trunk/TODO
arm/trunk/arm.py
arm/trunk/interface/bandwidthMonitor.py
arm/trunk/interface/connCountMonitor.py
arm/trunk/interface/connPanel.py
arm/trunk/interface/controller.py
arm/trunk/interface/cpuMemMonitor.py
arm/trunk/interface/logPanel.py
Log:
More issues discussed on irc.
fix: made netstat lookups a best-effort service, separate from draw thread (caught by arma and StrangeCharm)
fix: using ps as final fallback if otherwise unable to determine pid (suggested by Sebastian)
fix: appends tor's pwd if torrc path is relative (caught by arma)
Modified: arm/trunk/ChangeLog
===================================================================
--- arm/trunk/ChangeLog 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/ChangeLog 2009-09-28 06:56:21 UTC (rev 20678)
@@ -1,6 +1,13 @@
CHANGE LOG
-9/27/09 - version 1.1.2
+9/28/09 - version 1.1.3
+More issues discussed on irc.
+
+ * fix: made netstat lookups a best-effort service, separate from draw thread (caught by arma and StrangeCharm)
+ * fix: using ps as final fallback if otherwise unable to determine pid (suggested by Sebastian)
+ * fix: appends tor's pwd if torrc path is relative (caught by arma)
+
+9/27/09 - version 1.1.2 (r20674)
Few issues discussed on irc.
* added: changelog and cleaned up todo documents (requested by arma)
Modified: arm/trunk/TODO
===================================================================
--- arm/trunk/TODO 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/TODO 2009-09-28 06:56:21 UTC (rev 20678)
@@ -1,11 +1,7 @@
TODO
- Bugs
- * make netstat lookups a best-effort service (separate from draw thread)
- Call appears to be heavier than expected and causing display to be
- unusable on especially active relays (like directory servers).
- caught by arma and StrangeCharm, notify coderman for testing
- * Mac OSX and BSD may have issues with netstat options
+ * Mac OSX and BSD have issues with netstat options
Reported that they aren't cross platform. Possibly use lsof as a
fallback if an issue's detected.
caught by Christopher Davis
@@ -13,14 +9,8 @@
Not sure how to address this - problem is that the calls to 'host' can
take a while to time out. Might need another thread to kill the calls?
Or forcefully terminate thread if it's taking too long (might be noisy)?
- * connection details covers right side
* version labels provided on Debian are longer than expected
caught by hexa
- * unable to load torrc if it was loaded via a relative path
- When tor's started via "tor -f <relative path>" we don't know what it's
- relative of - check to see if there's a way of finding the pwd of
- another process.
- caught by arma
* new connections don't have uptime tracked when not visible
Previous fix attempted to resolve, but evidently didn't work.
Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/arm.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -19,8 +19,8 @@
from interface import controller
from interface import logPanel
-VERSION = "1.1.2"
-LAST_MODIFIED = "Sep 27, 2009"
+VERSION = "1.1.3"
+LAST_MODIFIED = "Sep 28, 2009"
DEFAULT_CONTROL_ADDR = "127.0.0.1"
DEFAULT_CONTROL_PORT = 9051
Modified: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/interface/bandwidthMonitor.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -99,7 +99,6 @@
def getHeaderLabel(self, width, isPrimary):
graphType = "Downloaded" if isPrimary else "Uploaded"
- total = self.primaryTotal if isPrimary else self.secondaryTotal
stats = [""]
# conditional is to avoid flickering as stats change size for tty terminals
Modified: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/interface/connCountMonitor.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -2,55 +2,42 @@
# connCountMonitor.py -- Tracks the number of connections made by Tor.
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-import os
-import time
from TorCtl import TorCtl
-import connPanel
import graphPanel
class ConnCountMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
"""
- Tracks number of connections, using cached values in connPanel if recent
- enough (otherwise retrieved independently). Client connections are counted
- as outbound.
+ Tracks number of connections, counting client and directory connections as
+ outbound.
"""
- def __init__(self, connectionPanel):
+ def __init__(self, conn, connResolver):
graphPanel.GraphStats.__init__(self)
TorCtl.PostEventListener.__init__(self)
graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
- self.connectionPanel = connectionPanel # connection panel, used to limit netstat calls
+ self.connResolver = connResolver # thread performing netstat queries
+ self.orPort = conn.get_option("ORPort")[0][1]
+ self.dirPort = conn.get_option("DirPort")[0][1]
+ self.controlPort = conn.get_option("ControlPort")[0][1]
def bandwidth_event(self, event):
# doesn't use events but this keeps it in sync with the bandwidth panel
# (and so it stops if Tor stops - used to use a separate thread but this
# is better)
- if self.connectionPanel.lastUpdate + 1 >= time.time():
- # reuses netstat results if recent enough
- counts = self.connectionPanel.connectionCount
- self._processEvent(counts[0], counts[1] + counts[2] + counts[3])
- else:
- # cached results stale - requery netstat
- inbound, outbound, control = 0, 0, 0
- netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.connectionPanel.pid)
- try:
- results = netstatCall.readlines()
-
- for line in results:
- if not line.startswith("tcp"): continue
- param = line.split()
- localPort = param[3][param[3].find(":") + 1:]
-
- if localPort in (self.connectionPanel.orPort, self.connectionPanel.dirPort): inbound += 1
- elif localPort == self.connectionPanel.controlPort: control += 1
- else: outbound += 1
- except IOError:
- # netstat call failed
- self.connectionPanel.monitor_event("WARN", "Unable to query netstat for connection counts")
+ inbound, outbound, control = 0, 0, 0
+ results = self.connResolver.getConnections()
+
+ for line in results:
+ if not line.startswith("tcp"): continue
+ param = line.split()
+ localPort = param[3][param[3].find(":") + 1:]
- netstatCall.close()
- self._processEvent(inbound, outbound)
+ if localPort in (self.orPort, self.dirPort): inbound += 1
+ elif localPort == self.controlPort: control += 1
+ else: outbound += 1
+
+ self._processEvent(inbound, outbound)
def getTitle(self, width):
return "Connection Count:"
Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/interface/connPanel.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -2,7 +2,6 @@
# connPanel.py -- Lists network connections used by tor.
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-import os
import time
import socket
import curses
@@ -98,19 +97,19 @@
Lists netstat provided network data of tor.
"""
- def __init__(self, lock, conn, torPid, logger):
+ def __init__(self, lock, conn, connResolver, logger):
TorCtl.PostEventListener.__init__(self)
util.Panel.__init__(self, lock, -1)
self.scroll = 0
- self.conn = conn # tor connection for querrying country codes
- self.logger = logger # notified in case of problems
- self.pid = torPid # tor process ID to make sure we've got the right instance
- self.listingType = LIST_IP # information used in listing entries
- self.allowDNS = True # permits hostname resolutions if true
- self.showLabel = True # shows top label if true, hides otherwise
- self.showingDetails = False # augments display to accomidate details window if true
- self.lastUpdate = -1 # time last stats was retrived
- self.localhostEntry = None # special connection - tuple with (entry for this node, fingerprint)
+ self.conn = conn # tor connection for querrying country codes
+ self.connResolver = connResolver # thread performing netstat queries
+ self.logger = logger # notified in case of problems
+ self.listingType = LIST_IP # information used in listing entries
+ self.allowDNS = True # permits hostname resolutions if true
+ self.showLabel = True # shows top label if true, hides otherwise
+ self.showingDetails = False # augments display to accomidate details window if true
+ self.lastUpdate = -1 # time last stats was retrived
+ self.localhostEntry = None # special connection - tuple with (entry for this node, fingerprint)
self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
self.resolver = hostnameResolver.HostnameResolver()
self.fingerprintLookupCache = {} # chache of (ip, port) -> fingerprint
@@ -123,6 +122,7 @@
self.clientConnectionCache = None # listing of nicknames for our client connections
self.clientConnectionLock = RLock() # lock for clientConnectionCache
self.isDisabled = False # prevent panel from updating entirely
+ self.lastNetstatResults = None # used to check if raw netstat results have changed
self.isCursorEnabled = True
self.cursorSelection = None
@@ -206,7 +206,6 @@
Reloads netstat results.
"""
- if not self.pid or self.isDisabled: return
self.connectionsLock.acquire()
self.clientConnectionLock.acquire()
@@ -223,63 +222,55 @@
for entry in (self.connections if not self.isPaused else self.connectionsBuffer):
connTimes[(entry[CONN_F_IP], entry[CONN_F_PORT])] = entry[CONN_TIME]
- # looks at netstat for tor with stderr redirected to /dev/null, options are:
- # n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
- netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.pid)
+ results = self.connResolver.getConnections()
+ if results == self.lastNetstatResults: return # contents haven't changed
- try:
- results = netstatCall.readlines()
+ for line in results:
+ if not line.startswith("tcp"): continue
+ param = line.split()
+ local, foreign = param[3], param[4]
+ localIP, foreignIP = local[:local.find(":")], foreign[:foreign.find(":")]
+ localPort, foreignPort = local[len(localIP) + 1:], foreign[len(foreignIP) + 1:]
- for line in results:
- if not line.startswith("tcp"): continue
- param = line.split()
- local, foreign = param[3], param[4]
- localIP, foreignIP = local[:local.find(":")], foreign[:foreign.find(":")]
- localPort, foreignPort = local[len(localIP) + 1:], foreign[len(foreignIP) + 1:]
+ if localPort in (self.orPort, self.dirPort):
+ type = "inbound"
+ connectionCountTmp[0] += 1
+ elif localPort == self.controlPort:
+ type = "control"
+ connectionCountTmp[4] += 1
+ else:
+ fingerprint = self.getFingerprint(foreignIP, foreignPort)
+ nickname = self.getNickname(foreignIP, foreignPort)
- if localPort in (self.orPort, self.dirPort):
- type = "inbound"
- connectionCountTmp[0] += 1
- elif localPort == self.controlPort:
- type = "control"
- connectionCountTmp[4] += 1
- else:
- fingerprint = self.getFingerprint(foreignIP, foreignPort)
- nickname = self.getNickname(foreignIP, foreignPort)
-
- isClient = False
- for clientName in self.clientConnectionCache:
- if nickname == clientName or (len(clientName) > 1 and clientName[0] == "$" and fingerprint == clientName[1:]):
- isClient = True
- break
-
- if isClient:
- type = "client"
- connectionCountTmp[2] += 1
- elif (foreignIP, foreignPort) in DIR_SERVERS:
- type = "directory"
- connectionCountTmp[3] += 1
- else:
- type = "outbound"
- connectionCountTmp[1] += 1
+ isClient = False
+ for clientName in self.clientConnectionCache:
+ if nickname == clientName or (len(clientName) > 1 and clientName[0] == "$" and fingerprint == clientName[1:]):
+ isClient = True
+ break
- try:
- countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
- countryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
- except (socket.error, TorCtl.ErrorReply):
- countryCode = "??"
- if not self.providedGeoipWarning:
- self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
- self.providedGeoipWarning = True
-
- if (foreignIP, foreignPort) in connTimes: connTime = connTimes[(foreignIP, foreignPort)]
- else: connTime = time.time()
-
- connectionsTmp.append((type, localIP, localPort, foreignIP, foreignPort, countryCode, connTime))
- except IOError:
- # netstat call failed
- self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
- return
+ if isClient:
+ type = "client"
+ connectionCountTmp[2] += 1
+ elif (foreignIP, foreignPort) in DIR_SERVERS:
+ type = "directory"
+ connectionCountTmp[3] += 1
+ else:
+ type = "outbound"
+ connectionCountTmp[1] += 1
+
+ try:
+ countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
+ countryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
+ except (socket.error, TorCtl.ErrorReply):
+ countryCode = "??"
+ if not self.providedGeoipWarning:
+ self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
+ self.providedGeoipWarning = True
+
+ if (foreignIP, foreignPort) in connTimes: connTime = connTimes[(foreignIP, foreignPort)]
+ else: connTime = time.time()
+
+ connectionsTmp.append((type, localIP, localPort, foreignIP, foreignPort, countryCode, connTime))
# appends localhost connection to allow user to look up their own consensus entry
selfAddress, selfPort, selfFingerprint = None, None, None
@@ -304,7 +295,6 @@
else:
self.localhostEntry = None
- netstatCall.close()
self.lastUpdate = time.time()
# assigns results
@@ -316,6 +306,7 @@
# hostnames are sorted at redraw - otherwise now's a good time
if self.listingType != LIST_HOSTNAME: self.sortConnections()
+ self.lastNetstatResults = results
finally:
self.connectionsLock.release()
self.clientConnectionLock.release()
Added: arm/trunk/interface/connResolver.py
===================================================================
--- arm/trunk/interface/connResolver.py (rev 0)
+++ arm/trunk/interface/connResolver.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+# connResolver.py -- Background thread for retrieving tor's connections.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import time
+from threading import Thread
+from threading import RLock
+
+MIN_LOOKUP_WAIT = 5 # minimum seconds between lookups
+SLEEP_INTERVAL = 1 # period to sleep when not making a netstat call
+FAILURE_TOLERANCE = 3 # number of subsiquent failures tolerated before pausing thread
+FAILURE_MSG = "Unable to query netstat for new connections"
+SERIAL_FAILURE_MSG = "Failing to query netstat (connection related portions of the monitor won't function)"
+
+class ConnResolver(Thread):
+ """
+ Service that periodically queries Tor's current connections to allow for a
+ best effort, non-blocking lookup. This is currently implemented via netstat.
+ In case of failure this gives an INFO level warning and provides the last
+ known results. This process provides an WARN level warning and pauses itself
+ if there's several subsiquent failures (probably indicating that netstat
+ isn't available).
+ """
+
+ def __init__(self, pid, logPanel):
+ Thread.__init__(self)
+ self.pid = pid # tor process ID to make sure we've got the right instance
+ self.logger = logPanel # used to notify of lookup failures
+
+ self.connections = [] # unprocessed lines from netstat results
+ self.connectionsLock = RLock() # limits concurrent access to connections
+ self.isPaused = False
+ self.halt = False # terminates thread if true
+ self.lastLookup = -1 # time of last lookup (reguardless of success)
+ self.subsiquentFailures = 0 # number of failed netstat calls in a row
+ self.setDaemon(True)
+
+ def getConnections(self):
+ """
+ Provides the last querried connection results.
+ """
+
+ connectionsTmp = None
+
+ self.connectionsLock.acquire()
+ try: connectionsTmp = list(self.connections)
+ finally: self.connectionsLock.release()
+
+ return connectionsTmp
+
+ def run(self):
+ if not self.pid: return
+
+ while not self.halt:
+ if self.isPaused or time.time() - MIN_LOOKUP_WAIT < self.lastLookup: time.sleep(SLEEP_INTERVAL)
+ else:
+ try:
+ # looks at netstat for tor with stderr redirected to /dev/null, options are:
+ # n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
+ netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.pid)
+ results = netstatCall.readlines()
+ if not results: raise IOError
+
+ # assign obtained results
+ self.connectionsLock.acquire()
+ try: self.connections = results
+ finally: self.connectionsLock.release()
+
+ self.subsiquentFailures = 0
+ except IOError:
+ # netstat call failed
+ self.subsiquentFailures += 1
+ self.logger.monitor_event("INFO", "%s (%i)" % (FAILURE_MSG, self.subsiquentFailures))
+
+ if self.subsiquentFailures >= FAILURE_TOLERANCE:
+ self.logger.monitor_event("WARN", SERIAL_FAILURE_MSG)
+ self.setPaused(True)
+ finally:
+ self.lastLookup = time.time()
+ netstatCall.close()
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents further netstat lookups.
+ """
+
+ if isPause == self.isPaused: return
+ self.isPaused = isPause
+
Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/interface/controller.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -10,6 +10,7 @@
import os
import time
import curses
+import socket
from threading import RLock
from TorCtl import TorCtl
@@ -21,6 +22,7 @@
import descriptorPopup
import util
+import connResolver
import bandwidthMonitor
import cpuMemMonitor
import connCountMonitor
@@ -262,13 +264,37 @@
except IOError: pass # netstat call failed
netstatCall.close()
+ if not torPid:
+ try:
+ # third try, use ps if there's only one possability
+ psCall = os.popen("ps -o pid -C tor 2> /dev/null")
+ results = psCall.readlines()
+ if len(results) == 2 and len(results[0].split()) == 1: torPid = results[1].strip()
+ except IOError: pass # ps call failed
+ psCall.close()
+
+ confLocation = conn.get_info("config-file")["config-file"]
+ if confLocation[0] != "/":
+ # relative path - attempt to add process pwd
+ try:
+ pwdxCall = os.popen("pwdx %s 2> /dev/null" % torPid)
+ results = pwdxCall.readlines()
+ if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
+ except IOError: pass # pwdx call failed
+ pwdxCall.close()
+
panels = {
"header": headerPanel.HeaderPanel(cursesLock, conn, torPid),
"popup": util.Panel(cursesLock, 9),
"graph": graphPanel.GraphPanel(cursesLock),
"log": logPanel.LogMonitor(cursesLock, loggedEvents),
- "torrc": confPanel.ConfPanel(cursesLock, conn.get_info("config-file")["config-file"])}
- panels["conn"] = connPanel.ConnPanel(cursesLock, conn, torPid, panels["log"])
+ "torrc": confPanel.ConfPanel(cursesLock, confLocation)}
+
+ # starts thread for processing netstat queries
+ connResolutionThread = connResolver.ConnResolver(torPid, panels["log"])
+ connResolutionThread.start()
+
+ panels["conn"] = connPanel.ConnPanel(cursesLock, conn, connResolutionThread, panels["log"])
panels["control"] = ControlPanel(cursesLock, panels["conn"].resolver)
# prevents netstat calls by connPanel if not being used
@@ -280,7 +306,7 @@
# statistical monitors for graph
panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
panels["graph"].addStats("cpu / memory", cpuMemMonitor.CpuMemMonitor(panels["header"]))
- panels["graph"].addStats("connection count", connCountMonitor.ConnCountMonitor(panels["conn"]))
+ panels["graph"].addStats("connection count", connCountMonitor.ConnCountMonitor(conn, connResolutionThread))
panels["graph"].setStats("bandwidth")
# listeners that update bandwidth and log panels with Tor status
@@ -299,7 +325,6 @@
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
page = 0
- netstatRefresh = time.time() # time of last netstat refresh
regexFilters = [] # previously used log regex filters
while True:
@@ -309,8 +334,6 @@
cursesLock.acquire()
try:
- y, x = stdscr.getmaxyx()
-
# if sighup received then reload related information
if sighupTracker.isReset:
panels["header"]._updateParams(True)
@@ -341,6 +364,7 @@
panels[panelKey].recreate(stdscr, tmpStartY)
tmpStartY += panels[panelKey].height
+
# if it's been at least ten seconds since the last BW event Tor's probably done
if not isUnresponsive and panels["log"].getHeartbeat() >= 10:
isUnresponsive = True
@@ -350,11 +374,7 @@
isUnresponsive = False
panels["log"].monitor_event("NOTICE", "Relay resumed")
- # if it's been at least five seconds since the last refresh of connection listing, update
- currentTime = time.time()
- if not panels["conn"].isPaused and (currentTime - netstatRefresh >= 5):
- panels["conn"].reset()
- netstatRefresh = currentTime
+ if not panels["conn"].isPaused: panels["conn"].reset()
# I haven't the foggiest why, but doesn't work if redrawn out of order...
for panelKey in (PAGE_S + PAGES[page]): panels[panelKey].redraw()
Modified: arm/trunk/interface/cpuMemMonitor.py
===================================================================
--- arm/trunk/interface/cpuMemMonitor.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/interface/cpuMemMonitor.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -29,7 +29,6 @@
self._processEvent(float(self.headerPanel.vals["%cpu"]), float(self.headerPanel.vals["rss"]) / 1024.0)
else:
# cached results stale - requery ps
- inbound, outbound, control = 0, 0, 0
psCall = os.popen('ps -p %s -o %s 2> /dev/null' % (self.headerPanel.vals["pid"], "%cpu,rss"))
try:
sampling = psCall.read().strip().split()[2:]
Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py 2009-09-28 00:51:28 UTC (rev 20677)
+++ arm/trunk/interface/logPanel.py 2009-09-28 06:56:21 UTC (rev 20678)
@@ -2,7 +2,6 @@
# logPanel.py -- Resources related to Tor event monitoring.
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-import re
import time
import curses
from curses.ascii import isprint
@@ -166,7 +165,7 @@
def monitor_event(self, level, msg):
# events provided by the arm monitor - types use the same as runlevel
- self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+ if level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
def registerEvent(self, type, msg, color):
"""
More information about the tor-commits
mailing list