[or-cvs] r22616: {arm} Additional prep for release (updated change notes, spelling (in arm/trunk: . init interface/graphing util)
Damian Johnson
atagar1 at gmail.com
Wed Jul 7 16:44:55 UTC 2010
Author: atagar
Date: 2010-07-07 16:44:54 +0000 (Wed, 07 Jul 2010)
New Revision: 22616
Modified:
arm/trunk/ChangeLog
arm/trunk/README
arm/trunk/TODO
arm/trunk/init/starter.py
arm/trunk/interface/graphing/bandwidthStats.py
arm/trunk/interface/graphing/psStats.py
arm/trunk/util/conf.py
arm/trunk/util/connections.py
arm/trunk/util/log.py
arm/trunk/util/panel.py
arm/trunk/util/sysTools.py
arm/trunk/util/torTools.py
arm/trunk/util/uiTools.py
Log:
Additional prep for release (updated change notes, spelling corrections, and a few fixes thanks to pylint).
Modified: arm/trunk/ChangeLog
===================================================================
--- arm/trunk/ChangeLog 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/ChangeLog 2010-07-07 16:44:54 UTC (rev 22616)
@@ -1,5 +1,37 @@
CHANGE LOG
+6/7/10 - version 1.3.6
+Rewrite of the first third of the interface, providing vastly improved performance, maintainability, and a few very nice features.
+
+ * added: settings are fetched from an optional armrc (update rates, controller password, caching, runlevels, etc)
+ * added: system tools util providing simplified usage, suppression of leaks to stdout, logging, and optional caching
+ * added: wrapper for accessing TorCtl providing:
+ o client side caching for commonly fetched relay information (fingerprint, descriptor, etc)
+ o singleton accessor and convenience functions, simplifying interface code
+ o wrapper allowing reattachment to new controllers (ie, arm still works if tor's stopped then restarted - still in the works)
+ * change: full rewrite of the header panel, providing:
+ o notice for when tor's disconnected (with time-stamp)
+ o lightweight redrawing (smarter caching and moved updating into a daemon thread)
+ o more graceful handling of tiny displays
+ * change: rewrite of graph panel and related stats, providing:
+ o prepopulation of bandwidth information from the state file if possible
+ o observed and measured bandwidth stats (requested by arma)
+ o graph can be configured to display any numeric ps stat
+ o third option for graphing bounds (restricting to both local minima and maxima)
+ o substantially reduced redraw rate and making use of cached ps parameters (reducing call volume)
+ * fix: preventing 'command unavailable' error messages from going to stdout, which disrupts the display (caught by sid77)
+ * fix: removed -p option due to being a gaping security problem (caught by ioerror and nickm)
+ * fix: crashing issue if TorCtl reports TorCtlClosed before the first refresh (caught by Tas)
+ * fix: preventing the connection panel from initiating or resetting while in blind mode (caught by micah)
+ * fix: ss resolution wasn't specifying the use of numeric ports (caught by data)
+ * fix: parsing error when ExitPolicy is undefined (caught by Paul Menzel)
+ * fix: revised sleep pattern used for threads, greatly reducing the time it takes to quit
+ * fix: bug in defaulting the connection resolver to something predetermined to be available
+ * fix: stopping connection resolution (and related failover message) when tor's stopped
+ * fix: crashing issue when trying to resolve addresses without network connectivity
+ * fix: forgot to join on connection resolver when quitting
+ * fix: revised calculation for effective bandwidth rate to take MaxAdvertisedBandwidth into account
+
4/8/10 - version 1.3.5 (r22148)
Utility and service rewrite (refactored roughly a third of the codebase, including revised APIs and much better documentation).
Modified: arm/trunk/README
===================================================================
--- arm/trunk/README 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/README 2010-07-07 16:44:54 UTC (rev 22616)
@@ -88,10 +88,11 @@
./
arm - startup script
- ChangeLog - revision history
- LICENSE - copy of the gpl v3
- README - um... guess you figured this one out
- TODO - known issues, future plans, etc
+ armrc.sample - example arm configuration file with defaults
+ ChangeLog - revision history
+ LICENSE - copy of the gpl v3
+ README - um... guess you figured this one out
+ TODO - known issues, future plans, etc
screenshot_page1.png
screenshot_page2.png
@@ -102,16 +103,18 @@
prereq.py - checks python version and for required packages
interface/
+ graphing/
+ __init__.py
+ graphPanel.py - (page 1) presents graphs for data instances
+ bandwidthStats.py - tracks tor bandwidth usage
+ psStats.py - tracks system information (by default cpu and memory usage)
+ connStats.py - tracks number of tor connections
+
__init__.py
controller.py - main display loop, handling input and layout
headerPanel.py - top of all pages, providing general information
-
- graphPanel.py - (page 1) presents graphs for data instances
- bandwidthMonitor.py - (graph data) tracks tor bandwidth usage
- cpuMemMonitor.py - (graph data) tracks tor cpu and memory usage
- connCountMonitor.py - (graph data) tracks number of tor connections
- logPanel.py - displays tor, arm, and torctl events
+ logPanel.py - (page 1) displays tor, arm, and torctl events
fileDescriptorPopup.py - (popup) displays file descriptors used by tor
connPanel.py - (page 2) displays information on tor connections
@@ -121,9 +124,12 @@
util/
__init__.py
+ conf.py - loading and persistence for user configuration
connections.py - service providing periodic connection lookups
hostnames.py - service providing nonblocking reverse dns lookups
log.py - aggregator for application events
panel.py - wrapper for safely working with curses subwindows
- uiTools.py - helper functions for interface
+ sysTools.py - helper for system calls, providing client side caching
+ torTools.py - wrapper for TorCtl, providing caching and derived information
+ uiTools.py - helper functions for presenting the user interface
Modified: arm/trunk/TODO
===================================================================
--- arm/trunk/TODO 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/TODO 2010-07-07 16:44:54 UTC (rev 22616)
@@ -1,15 +1,13 @@
TODO
-- Roadmap for next release (1.3.6)
+- Roadmap for next release (1.3.7)
[ ] refactor panels
Currently the interface is a bit of a rat's nest (especially the
controller). The goal is to use better modularization to both simplify
the codebase and make it possible to use smarter caching to improve
performance (far too much is done in the ui logic). This work is in
- progress - /init and /util are done and /interface is in progress. Known
+ progress - /init and /util are done and /interface is partly done. Known
bugs are being fixed while refactoring.
- [X] header panel
- [X] graph panel
[ ] log panel
- option to clear log
- allow home/end keys to jump to start/end
@@ -22,7 +20,7 @@
Ie, make default "TOR/ARM NOTICE - ERR"
- fetch text via getinfo rather than reading directly?
conn.get_info("config-text")
- [-] conn panel (for version 1.3.7)
+ [-] conn panel (for version 1.3.8)
- check family connections to see if they're alive (VERSION cell
handshake?)
- fallback when pid or connection querying via pid is unavailable
@@ -32,11 +30,10 @@
- connection uptime to associate inbound/outbound connections?
- Identify controller connections (if it's arm, vidalia, etc) with
special detail page for them
- [-] controller (for version 1.3.7)
+ [-] controller (for version 1.3.8)
[ ] provide performance ARM-DEBUG events
Help with diagnosing performance bottlenecks. This is pending the
codebase revisions to figure out the low hanging fruit for caching.
- [X] user customizable armrc
[ ] tor util
[X] wrapper for accessing torctl
[ ] allow arm to resume after restarting tor (attaching to a new torctl
@@ -114,8 +111,7 @@
country, ISP, latency, exit policy for the circuit, traffic, etc
* attempt to clear controller password from memory
http://www.codexon.com/posts/clearing-passwords-in-memory-with-python
- * try/catch check when starting for curses support?
- * excaping function for uiTools' formatted strings
+ * escaping function for uiTools' formatted strings
* tor-weather like functionality (email notices)
* provide bridge / client country statistics
- Include bridge related data via GETINFO option (feature request by
@@ -131,11 +127,11 @@
* audit tor connections
Provide warnings if tor misbehaves, checks possibly including:
- ensuring ExitPolicyRejectPrivate is being obeyed
- - check that ExitPolicy violations don't occure (not possible yet since
+ - check that ExitPolicy violations don't occur (not possible yet since
not all relays aren't identified)
- check that all connections are properly related to a circuit, for
instance no outbound connections without a corresponding inbound (not
- possible yet due to being unable to correlate connections to circuts)
+ possible yet due to being unable to correlate connections to circuits)
* check file descriptors being accessed by tor to see if they're outside the
known pattern
* allow killing of circuits? Probably not useful...
@@ -173,7 +169,7 @@
* vnstat, nload, mrtg, and traceroute
- Ideas (low priority)
- * python 3 compatability
+ * python 3 compatibility
Currently blocked on TorCtl support.
* bundle script that dumps relay stats to stdout
Django has a small terminal coloring module that could be nice for
Modified: arm/trunk/init/starter.py
===================================================================
--- arm/trunk/init/starter.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/init/starter.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -25,8 +25,8 @@
import util.uiTools
import TorCtl.TorUtil
-VERSION = "1.3.5_dev"
-LAST_MODIFIED = "Apr 8, 2010"
+VERSION = "1.3.6_dev"
+LAST_MODIFIED = "July 7, 2010"
DEFAULT_CONFIG = os.path.expanduser("~/.armrc")
DEFAULTS = {"startup.controlPassword": None,
Modified: arm/trunk/interface/graphing/bandwidthStats.py
===================================================================
--- arm/trunk/interface/graphing/bandwidthStats.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/interface/graphing/bandwidthStats.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -4,7 +4,6 @@
"""
import time
-from TorCtl import TorCtl
import graphPanel
from util import log, sysTools, torTools, uiTools
@@ -23,14 +22,13 @@
DEFAULT_CONFIG = {"features.graph.bw.accounting.show": True, "features.graph.bw.accounting.rate": 10, "features.graph.bw.accounting.isTimeLong": False, "log.graph.bw.prepopulateSuccess": log.NOTICE, "log.graph.bw.prepopulateFailure": log.NOTICE}
-class BandwidthStats(graphPanel.GraphStats, TorCtl.PostEventListener):
+class BandwidthStats(graphPanel.GraphStats):
"""
Uses tor BW events to generate bandwidth usage graph.
"""
def __init__(self, config=None):
graphPanel.GraphStats.__init__(self)
- TorCtl.PostEventListener.__init__(self)
self._config = dict(DEFAULT_CONFIG)
if config:
@@ -271,16 +269,16 @@
bwMeasured = conn.getMyBandwidthMeasured()
if bwRate and bwBurst:
- bwRate = uiTools.getSizeLabel(bwRate, 1)
- bwBurst = uiTools.getSizeLabel(bwBurst, 1)
+ bwRateLabel = uiTools.getSizeLabel(bwRate, 1)
+ bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1)
# if both are using rounded values then strip off the ".0" decimal
- if ".0" in bwRate and ".0" in bwBurst:
- bwRate = bwRate.replace(".0", "")
- bwBurst = bwBurst.replace(".0", "")
+ if ".0" in bwRateLabel and ".0" in bwBurstLabel:
+ bwRateLabel = bwRateLabel.replace(".0", "")
+ bwBurstLabel = bwBurstLabel.replace(".0", "")
- stats.append("limit: %s" % bwRate)
- stats.append("burst: %s" % bwBurst)
+ stats.append("limit: %s" % bwRateLabel)
+ stats.append("burst: %s" % bwBurstLabel)
# Provide the observed bandwidth either if the measured bandwidth isn't
# available or if the measured bandwidth is the observed (this happens
Modified: arm/trunk/interface/graphing/psStats.py
===================================================================
--- arm/trunk/interface/graphing/psStats.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/interface/graphing/psStats.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -6,7 +6,7 @@
import graphPanel
from util import log, sysTools, torTools, uiTools
-# number of subsiquent failed queries before giving up
+# number of subsequent failed queries before giving up
FAILURE_THRESHOLD = 5
# attempts to use cached results from the header panel's ps calls
@@ -21,7 +21,7 @@
def __init__(self, config=None):
graphPanel.GraphStats.__init__(self)
- self.failedCount = 0 # number of subsiquent failed queries
+ self.failedCount = 0 # number of subsequent failed queries
self._config = dict(DEFAULT_CONFIG)
if config: config.update(self._config)
@@ -113,7 +113,7 @@
result = float(psResults[statName])
# The 'rss' and 'size' parameters provide memory usage in KB. This is
- # scaled up to MB so the graph's y-high is a resonable value.
+ # scaled up to MB so the graph's y-high is a reasonable value.
if statName in ("rss", "size"): result /= 1024.0
if isPrimary: primary = result
Modified: arm/trunk/util/conf.py
===================================================================
--- arm/trunk/util/conf.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/conf.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -40,7 +40,7 @@
class Config():
"""
- Handler for easily working with custom configurations, providing persistance
+ Handler for easily working with custom configurations, providing persistence
to and from files. All operations are thread safe.
Parameters:
@@ -54,7 +54,7 @@
Creates a new configuration instance.
"""
- self.path = None # path to the associated configuation file
+ self.path = None # path to the associated configuration file
self.contents = {} # configuration key/value pairs
self.contentsLock = threading.RLock()
self.requestedKeys = set()
@@ -146,7 +146,7 @@
Undefined values are left with their current values.
Arguments:
- confMappings - configuration key/value mappints to be revised
+ confMappings - configuration key/value mappings to be revised
"""
for entry in confMappings.keys():
@@ -229,12 +229,12 @@
already exists then merges as follows:
- comments and file contents not in this config are left unchanged
- lines with duplicate keys are stripped (first instance is kept)
- - existing enries are overwritten with their new values, preserving the
- positioning of inline comments if able
+ - existing entries are overwritten with their new values, preserving the
+ positioning of in-line comments if able
- config entries not in the file are appended to the end in alphabetical
order
- If problems arise in writting (such as an unset path or insufficient
+ If problems arise in writing (such as an unset path or insufficient
permissions) result in an IOError.
Arguments:
Modified: arm/trunk/util/connections.py
===================================================================
--- arm/trunk/util/connections.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/connections.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -303,7 +303,7 @@
# this logs in a couple of cases:
# - special failures noted by getConnections (most cases are already
# logged via sysTools)
- # - note failovers for default resolution methods
+ # - note fail-overs for default resolution methods
if str(exc).startswith("No results found using:"):
log.log(CONFIG["log.connLookupFailed"], str(exc))
@@ -323,7 +323,7 @@
break
if newResolver:
- # provide notice that failures have occured and resolver is changing
+ # provide notice that failures have occurred and resolver is changing
msg = RESOLVER_SERIAL_FAILURE_MSG % (CMD_STR[resolver], CMD_STR[newResolver])
log.log(CONFIG["log.connLookupFailover"], msg)
else:
Modified: arm/trunk/util/log.py
===================================================================
--- arm/trunk/util/log.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/log.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -69,9 +69,9 @@
the backlog. If the level is None then this is a no-op.
Arguments:
- level - runlevel coresponding to the message severity
+ level - runlevel corresponding to the message severity
msg - string associated with the message
- eventTime - unix time at which the event occured, current time if undefined
+ eventTime - unix time at which the event occurred, current time if undefined
"""
if not level: return
@@ -96,7 +96,7 @@
eventBacklog.insert(i + 1, newEvent)
break
- # turncates backlog if too long
+ # truncates backlog if too long
toDelete = len(eventBacklog) - CONFIG["cache.armLog.size"]
if toDelete >= 0: del eventBacklog[: toDelete + CONFIG["cache.armLog.trimSize"]]
Modified: arm/trunk/util/panel.py
===================================================================
--- arm/trunk/util/panel.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/panel.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -12,7 +12,7 @@
CURSES_LOCK = RLock()
# tags used by addfstr - this maps to functor/argument combinations since the
-# actual values (color attributes - grr...) might not yet be initialized
+# actual values (in the case of color attributes) might not yet be initialized
def _noOp(arg): return arg
FORMAT_TAGS = {"<b>": (_noOp, curses.A_BOLD),
"<u>": (_noOp, curses.A_UNDERLINE),
@@ -31,7 +31,7 @@
- locking when concurrently drawing to multiple windows
- gracefully handle terminal resizing
- clip text that falls outside the panel
- - convenience methods for word wrap, inline formatting, etc
+ - convenience methods for word wrap, in-line formatting, etc
This uses a design akin to Swing where panel instances provide their display
implementation by overwriting the draw() method, and are redrawn with
Modified: arm/trunk/util/sysTools.py
===================================================================
--- arm/trunk/util/sysTools.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/sysTools.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -54,7 +54,7 @@
"""
Convenience function for performing system calls, providing:
- suppression of any writing to stdout, both directing stderr to /dev/null
- and checking for the existance of commands before executing them
+ and checking for the existence of commands before executing them
- logging of results (command issued, runtime, success/failure, etc)
- optional exception suppression and caching (the max age for cached results
is a minute)
@@ -73,7 +73,7 @@
if cacheAge > 0:
global CALL_CACHE, CONFIG
- # keeps consistancy that we never use entries over a minute old (these
+ # keeps consistency that we never use entries over a minute old (these
# results are 'dirty' and might be trimmed at any time)
cacheAge = min(cacheAge, 60)
cacheSize = CONFIG["cache.sysCalls.size"]
Modified: arm/trunk/util/torTools.py
===================================================================
--- arm/trunk/util/torTools.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/torTools.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -54,7 +54,7 @@
def makeCtlConn(controlAddr="127.0.0.1", controlPort=9051):
"""
Opens a socket to the tor controller and queries its authentication type,
- raising an IOError if problems occure. The result of this function is a tuple
+ raising an IOError if problems occur. The result of this function is a tuple
of the TorCtl connection and the authentication type, where the later is one
of the following:
"NONE" - no authentication required
@@ -89,7 +89,7 @@
# password authentication
return (conn, "PASSWORD")
elif authInfo.startswith("AUTH METHODS=COOKIE"):
- # cookie authtication, parses authentication cookie path
+ # cookie authentication, parses authentication cookie path
start = authInfo.find("COOKIEFILE=\"") + 12
end = authInfo.find("\"", start)
return (conn, "COOKIE=%s" % authInfo[start:end])
@@ -183,10 +183,10 @@
return conn
except Exception, exc:
if passphrase and str(exc) == "Unable to authenticate: password incorrect":
- # provide a warining that the provided password didn't work, then try
+ # provide a warning that the provided password didn't work, then try
# again prompting for the user to enter it
print INCORRECT_PASSWORD_MSG
- return makeConn(controlAddr, controlPort)
+ return connect(controlAddr, controlPort)
else:
print exc
return None
@@ -199,9 +199,9 @@
2. "netstat -npl | grep 127.0.0.1:%s" % <tor control port>
3. "ps -o pid -C tor"
- If pidof or ps promide multiple tor instances then their results are discared
- (since only netstat can differentiate using the control port). This provdes
- None if either no running process exists or it can't be determined.
+ If pidof or ps provide multiple tor instances then their results are
+ discarded (since only netstat can differentiate using the control port). This
+ provides None if either no running process exists or it can't be determined.
Arguments:
controlPort - control port of the tor process if multiple exist
@@ -266,7 +266,7 @@
self.controllerEvents = {} # mapping of successfully set controller events to their failure level/msg
self._isReset = False # internal flag for tracking resets
self._status = TOR_CLOSED # current status of the attached control port
- self._statusTime = 0 # unix timestamp for the duration of the status
+ self._statusTime = 0 # unix time-stamp for the duration of the status
# cached getInfo parameters (None if unset or possibly changed)
self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
@@ -294,7 +294,7 @@
self.conn.add_event_listener(self)
for listener in self.eventListeners: self.conn.add_event_listener(listener)
- # sets the events listened for by the new controller (incompatable events
+ # sets the events listened for by the new controller (incompatible events
# are dropped with a logged warning)
self.setControllerEvents(self.controllerEvents)
@@ -357,13 +357,13 @@
"""
Queries the control port for the given GETINFO option, providing the
default if the response fails for any reason (error response, control port
- closed, ininitiated, etc).
+ closed, initiated, etc).
Arguments:
param - GETINFO option to be queried
default - result if the query fails and exception's suppressed
suppressExc - suppresses lookup errors (returning the default) if true,
- otherwises this raises the original exception
+ otherwise this raises the original exception
"""
self.connLock.acquire()
@@ -397,7 +397,7 @@
multiple - provides a list of results if true, otherwise this just
returns the first value
suppressExc - suppresses lookup errors (returning the default) if true,
- otherwises this raises the original exception
+ otherwise this raises the original exception
"""
self.connLock.acquire()
@@ -441,7 +441,7 @@
def getMyDescriptor(self, default = None):
"""
- Provides the descrptor entry for this relay if available.
+ Provides the descriptor entry for this relay if available.
Arguments:
default - result if the query fails
@@ -529,7 +529,7 @@
def getStatus(self):
"""
Provides a tuple consisting of the control port's current status and unix
- timestamp for when it became this way (zero if no status has yet to be
+ time-stamp for when it became this way (zero if no status has yet to be
set).
"""
@@ -604,7 +604,7 @@
unavailableEvents.update(events.intersection(FAILED_EVENTS))
events.difference_update(FAILED_EVENTS)
- # inital check for event availability
+ # initial check for event availability
validEvents = self.getInfo("events/names")
if validEvents:
@@ -863,7 +863,7 @@
def _notifyStatusListeners(self, eventType):
"""
Sends a notice to all current listeners that a given change in tor's
- controller status has occured.
+ controller status has occurred.
Arguments:
eventType - enum representing tor's new status
Modified: arm/trunk/util/uiTools.py
===================================================================
--- arm/trunk/util/uiTools.py 2010-07-07 15:11:54 UTC (rev 22615)
+++ arm/trunk/util/uiTools.py 2010-07-07 16:44:54 UTC (rev 22616)
@@ -55,7 +55,7 @@
Provides the msg constrained to the given length, truncating on word breaks.
If the last words is long this truncates mid-word with an ellipse. If there
isn't room for even a truncated single word (or one word plus the ellipse if
- inlcuding those) then this provides an empty string. Examples:
+ including those) then this provides an empty string. Examples:
cropStr("This is a looooong message", 17)
"This is a looo..."
@@ -107,7 +107,7 @@
"""
Converts byte count into label in its most significant units, for instance
7500 bytes would return "7 KB". If the isLong option is used this expands
- unit labels to be the properly pluralised full word (for instance 'Kilobytes'
+ unit labels to be the properly pluralized full word (for instance 'Kilobytes'
rather than 'KB'). Units go up through PB.
Example Usage:
@@ -130,7 +130,7 @@
This defaults to presenting single character labels, but if the isLong option
is used this expands labels to be the full word (space included and properly
- pluralised). For instance, "4h" would be "4 hours" and "1m" would become
+ pluralized). For instance, "4h" would be "4 hours" and "1m" would become
"1 minute".
Example Usage:
@@ -196,7 +196,7 @@
else:
# unfortunately the %f formatting has no method of rounding down, so
# reducing value to only concern the digits that are visible - note
- # that this doesn't work with miniscule values (starts breaking down at
+ # that this doesn't work with minuscule values (starts breaking down at
# around eight decimal places) or edge cases when working with powers
# of two
croppedCount = count - (count % (countPerUnit / (10 ** decimal)))
More information about the tor-commits
mailing list