[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Add heavily modified patch from Brian Warner for status...



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv7396/lib/mixminion

Modified Files:
	BuildMessage.py ClientMain.py Common.py Fragments.py Main.py 
	Packet.py test.py 
Log Message:
Add heavily modified patch from Brian Warner for status-fd and packet-counting; refactor argument parsing; add unit tests

Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.73
retrieving revision 1.74
diff -u -d -r1.73 -r1.74
--- BuildMessage.py	23 Mar 2004 00:07:02 -0000	1.73
+++ BuildMessage.py	14 May 2004 23:44:08 -0000	1.74
@@ -13,7 +13,8 @@
 import mixminion.Crypto as Crypto
 import mixminion.Fragments
 from mixminion.Packet import *
-from mixminion.Common import MixError, MixFatalError, LOG, UIError
+from mixminion.Common import MixError, MixFatalError, LOG, STATUS, UIError, \
+     formatBase64
 import mixminion.Packet
 import mixminion._minionlib
 
@@ -22,7 +23,25 @@
 
 __all__ = ['buildForwardPacket', 'buildEncryptedForwardPacket',
            'buildReplyPacket', 'buildReplyBlock', 'checkPathLength',
-           'encodeMessage', 'decodePayload' ]
+           'encodeMessage', 'decodePayload', 'getNPacketsToEncode' ]
+
+def getNPacketsToEncode(message, overhead, uncompressedFragmentPrefix=""):
+    """Return the number of packets that would be needed to encode 'message'.
+       Arguments are as for encodeMessage.
+    """
+    assert overhead in (0, ENC_FWD_OVERHEAD)
+    compressedLen = len(compressData(message))
+
+    paddingLen = PAYLOAD_LEN - SINGLETON_PAYLOAD_OVERHEAD - overhead - compressedLen
+    if paddingLen >= 0:
+        return 1
+
+    if uncompressedFragmentPrefix:
+        compressedLen += len(uncompressedFragmentPrefix)
+
+    p = mixminion.Fragments.FragmentationParams(compressedLen, overhead)
+
+    return p.n * p.nChunks
 
 def encodeMessage(message, overhead, uncompressedFragmentPrefix="",
                   paddingPRNG=None):
@@ -31,7 +50,7 @@
        of strings, each of which is a message payload suitable for use in
        build*Message.
 
-              payload: the initial payload
+              message: the initial message
               overhead: number of bytes to omit from each payload,
                         given the type ofthe message encoding.
                         (0 or ENC_FWD_OVERHEAD)
@@ -64,7 +83,7 @@
         p.computeHash()
         return [ p.pack() ]
 
-    # Okay, we need fo fragment the message.  First, add the prefix if needed.
+    # Okay, we need to fragment the message.  First, add the prefix if needed.
     if uncompressedFragmentPrefix:
         payload = uncompressedFragmentPrefix+payload
     # Now generate a message ID
@@ -296,8 +315,10 @@
 
     prng = Crypto.AESCounterPRNG(Crypto.sha1(seed+userKey+"Generate")[:16])
 
-    return _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime, prng,
-                                seed)[0]
+    replyBlock, secrets, tag = _buildReplyBlockImpl(path, exitType, exitInfo,
+                                                    expiryTime, prng, seed)
+    STATUS.log("GENERATED_SURB", formatBase64(tag))
+    return replyBlock
 
 def checkPathLength(path1, path2, exitType, exitInfo, explicitSwap=0,
                     suppressTag=0):

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.178
retrieving revision 1.179
diff -u -d -r1.178 -r1.179
--- ClientMain.py	2 May 2004 18:45:15 -0000	1.178
+++ ClientMain.py	14 May 2004 23:44:09 -0000	1.179
@@ -23,8 +23,9 @@
 import mixminion.MMTPClient
 
 from mixminion.Common import LOG, Lockfile, LockfileLocked, MixError, \
-     MixFatalError, MixProtocolBadAuth, MixProtocolError, UIError, \
-     UsageError, createPrivateDir, englishSequence, floorDiv, isPrintingAscii,\
+     MixFatalError, MixProtocolBadAuth, MixProtocolError, STATUS, UIError, \
+     UsageError, createPrivateDir, englishSequence, floorDiv, formatTime, \
+     isPrintingAscii,\
      isSMTPMailbox, readFile, stringContains, succeedingMidnight, writeFile, \
      previousMidnight
 from mixminion.Packet import encodeMailHeaders, ParseError, parseMBOXInfo, \
@@ -768,6 +769,8 @@
          The .init() method initializes a config file, logging, a
            MixminionClient object, or the ClientDirectory object as requested.
          The parsePath method parses the path as given.
+
+         DOCDOC --status-fd
     """
     ##Fields:
     #  want*: as given as arguments to __init__
@@ -871,7 +874,8 @@
                         (v, o))
                 self.download = dl
             elif o in ('-t', '--to'):
-                assert wantForwardPath or wantReplyPath
+                #assert wantForwardPath or wantReplyPath
+                #XXXX008 reenable, sanely.
                 if self.exitAddress is not None:
                     raise UIError("Multiple addresses specified.")
                 try:
@@ -879,7 +883,7 @@
                 except ParseError, e:
                     raise UsageError(str(e))
             elif o in ('-R', '--reply-block'):
-                assert wantForwardPath
+                #assert wantForwardPath #XXXX008 re-enable, sanely
                 self.replyBlockSources.append(v)
             elif o == '--reply-block-fd':
                 try:
@@ -911,6 +915,11 @@
                 self.forceQueue = 1
             elif o in ('--no-queue',):
                 self.forceNoQueue = 1
+            elif o in ('--status-fd',):
+                try:
+                    STATUS.setFD(int(v))
+                except ValueError:
+                    raise UsageError("%s expects an integer"%o)
 
         if self.quiet and self.verbose:
             raise UsageError("I can't be quiet and verbose at the same time.")
@@ -1048,9 +1057,42 @@
         return self.directory.generatePaths(n,self.pathSpec,self.exitAddress,
                                             self.startAt,self.endAt)
 
+# DOCDOC
+def getOptions(args, shortOpts="", longOpts=(), dir=0, reply=0, path=0,
+               headers=0, argsOK=0, dest=0, input=0, output=0, passphrase=0):
+    longOpts = list(longOpts)
+    shortOpts += "hvQf:"
+    longOpts += ["help", "verbose", "quiet", "config=", "status-fd="]
+    if dir:
+        shortOpts += "D:"
+        longOpts += ["download-directory="]
+    if reply:
+        shortOpts += "R:"
+        longOpts += ["reply-block=", "reply-block-fd="]
+    if path:
+        shortOpts += "P:H:"
+        longOpts += ["path=","hops="]
+    if headers:
+        longOpts += ["subject=", "from=", "in-reply-to=", "references="]
+    if dest:
+        shortOpts += "t:"
+        longOpts += ["to="]
+    if input:
+        shortOpts += "i:"
+        longOpts += ["input="]
+    if output:
+        shortOpts += "o:"
+        longOpts += ["output="]
+    if passphrase:
+        longOpts += ["passphrase-fd="]
+    o, a = getopt.getopt(args, shortOpts, longOpts)
+    if a and not argsOK:
+        raise UsageError("No arguments expected")
+    return o, a
+
 _SEND_USAGE = """\
 Usage: %(cmd)s [options] <-t address>|<--to=address>|
-                          <-R reply-block>|--reply-block=reply-block>
+                          <-R reply-block>|<--reply-block=reply-block>
 Options:
   -h, --help                 Print this usage message and exit.
   -v, --verbose              Display extra debugging messages.
@@ -1071,7 +1113,7 @@
                              packet, then deliver multiple fragmented packets
                              to the recipient instead of having the server
                              reassemble the message.
-  --reply-block-fd=<N>       Read reply blcoks from file descriptor <N>.
+  --reply-block-fd=<N>       Read reply blocks from file descriptor <N>.
 %(extra)s
 
 EXAMPLES:
@@ -1138,12 +1180,15 @@
 
     ###
     # Parse and validate our options.
-    options, args = getopt.getopt(args, "hvQf:D:t:H:P:R:i:",
-             ["help", "verbose", "quiet", "config=", "download-directory=",
-              "to=", "hops=", "path=", "reply-block=", "reply-block-fd=",
-              "input=", "queue", "no-queue",
-              "subject=", "from=", "in-reply-to=", "references=",
-              "deliver-fragments", ])
+    options, args = getOptions(args, "",
+                               ["queue", "no-queue", "deliver-fragments"],
+                               dir=1,reply=1,path=1,headers=1,dest=1,input=1)
+##     options, args = getopt.getopt(args, "hvQf:D:t:H:P:R:i:",
+##              ["help", "verbose", "quiet", "config=", "download-directory=",
+##               "to=", "hops=", "path=", "reply-block=", "reply-block-fd=",
+##               "input=", "queue", "no-queue",
+##               "subject=", "from=", "in-reply-to=", "references=",
+##               "deliver-fragments", "status-fd=" ])
 
     if not options:
         sendUsageAndExit(cmd)
@@ -1165,8 +1210,8 @@
         elif opt == '--deliver-fragments':
             no_ss_fragment = 1
 
-    if args:
-        sendUsageAndExit(cmd,"Unexpected arguments")
+##     if args:
+##         sendUsageAndExit(cmd,"Unexpected arguments")
 
     try:
         parser = CLIArgumentParser(options, wantConfig=1,wantClientDirectory=1,
@@ -1258,6 +1303,116 @@
             message, parser.startAt, parser.endAt, forceQueue, forceNoQueue,
             forceNoServerSideFragments=no_ss_fragment)
 
+_COUNT_PACKETS_USAGE = """\
+Usage: mixminion count-packets [options] <-t address>|<--to=address>|
+                              <-R reply-block>|<--reply-block=reply-block>
+Options:
+  -h, --help                 Print this usage message and exit.
+  -v, --verbose              Display extra debugging messages.
+  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
+                               (You can also use MIXMINIONRC=FILE)
+  -i <file>, --input=<file>  Read the message from <file>. (Defaults to stdin.)
+  --subject=<str>, --from=<str>, --in-reply-to=<str>, --references=<str>
+                             Specify an email header for the exiting message.
+  --deliver-fragments        If the message is too long to fit in a single
+                             packet, then deliver multiple fragmented packets
+                             to the recipient instead of having the server
+                             reassemble the message.
+  --reply-block-fd=<N>       Read reply blocks from file descriptor <N>.
+"""
+def countPackets(cmd,args):
+    options, args = getOptions(args, dir=1, dest=1, path=1, reply=1,
+                               input=1, headers=1)
+
+##     options,args = getopt.getopt(args, "hvQf:D:t:R:i:",
+##              ["help", "verbose", "quiet", "config=", "download-directory=",
+##               "to=", "path=", "reply-block=", "reply-block-fd=",
+##               "input=",
+##               "subject=", "from=", "in-reply-to=", "references=",
+##               "deliver-fragments", "status-fd=" ])
+
+##     if args:
+##         print >>sys.stderr, "Unexpected arguments"
+##         print _COUNTPACKET_USAGE
+##         sys.exit(1)
+
+    inFile = '-'
+    h_subject = h_from = h_irt = h_references = None
+    no_ss_fragment = 0
+    for opt,val in options:
+        if opt in ('-i', '--input'):
+            inFile = val
+        elif opt == '--subject':
+            h_subject = val
+        elif opt == '--from':
+            h_from = val
+        elif opt == '--in-reply-to':
+            h_irt = val
+        elif opt == '--references':
+            h_references = val
+        elif opt == '--deliver-fragments':
+            no_ss_fragment = 1
+
+    try:
+        parser = CLIArgumentParser(options, wantConfig=1,wantClientDirectory=1,
+                                   wantLog=1)
+    except UsageError, e:
+        e.dump()
+        print _COUNT_PACKETS_USAGE
+        sys.exit()
+
+    # Encode the headers early so that we die before reading the message if
+    # they won't work.
+    try:
+        headerStr = encodeMailHeaders(subject=h_subject, fromAddr=h_from,
+                                      inReplyTo=h_irt, references=h_references)
+    except MixError, e:
+        raise UIError("Invalid headers: %s"%e)
+    if no_ss_fragment:
+        if headerStr != '\n':
+            raise UIError("Can't use --deliver-fragments with message headers")
+        else:
+            # suppress intial newline.
+            headerStr = ""
+
+    if inFile == '-' and '-' in parser.replyBlockSources:
+        raise UIError(
+            "Can't read both message and reply block from stdin")
+
+    parser.init()
+    address = parser.exitAddress
+    address.setHeaders(parseMessageAndHeaders(headerStr+"\n")[1])
+    if address and inFile == '-' and not address.hasPayload():
+        print "1 packet needed"
+        STATUS.log("COUNT_PACKETS", 1)
+        return
+    else:
+        if address and not address.hasPayload():
+            raise UIError("Cannot send a message in a DROP packet")
+        try:
+            if inFile == '-':
+                if os.isatty(sys.stdin.fileno()):
+                    print "Enter your message.  Type %s when you are done."%(
+                        EOF_STR)
+                message = sys.stdin.read()
+            else:
+                message = readFile(inFile)
+        except KeyboardInterrupt:
+            print "Interrupted."
+            return
+
+        message = "%s%s"%(headerStr,message)
+        address.setExitSize(len(message))
+
+        if no_ss_fragment:
+            prefix=""
+        else:
+            prefix=address.getFragmentedMessagePrefix()
+
+        n = mixminion.BuildMessage.getNPacketsToEncode(message, 0, prefix)
+        print "%d packets needed" % n
+        STATUS.log("COUNT_PACKETS", n)
+
 _PING_USAGE = """\
 Usage: mixminion ping [options] serverName
 Options
@@ -1274,11 +1429,13 @@
         print _PING_USAGE
         sys.exit(0)
 
-    options, args = getopt.getopt(args, "hvQf:D:",
-             ["help", "verbose", "quiet", "config=", "download-directory=", ])
+    options, args = getOptions(args, dir=1, argsOK=1)
+##     options, args = getopt.getopt(args, "hvQf:D:",
+##              ["help", "verbose", "quiet", "config=", "download-directory=",
+##               "status-fd=" ])
 
-    if len(args) == 0:
-        raise UsageError("(No servers provided)")
+##     if len(args) == 0:
+##         raise UsageError("(No servers provided)")
 
     print "==========================================================="
     print "WARNING: Pinging a server is potentially dangerous, since"
@@ -1341,8 +1498,9 @@
 
 def importServer(cmd, args):
     """[Entry point] Manually add a server to the client directory."""
-    options, args = getopt.getopt(args, "hf:vQ",
-                     ['help', 'config=', 'verbose', 'quiet'])
+    options, args = getOptions(args, dir=1, argsOK=1)
+##     options, args = getopt.getopt(args, "hf:vQ",
+##                      ['help', 'config=', 'verbose', 'quiet', "status-fd="])
 
     try:
         parser = CLIArgumentParser(options, wantConfig=1,wantClientDirectory=1,
@@ -1398,12 +1556,18 @@
 def listServers(cmd, args):
     """[Entry point] Print info about servers in the directory, or on
        the command line."""
-    options, args = getopt.getopt(args, "hf:D:vQF:JTrRs:cC",
-                                  ['help', 'config=', "download-directory=",
-                                   'verbose', 'quiet', 'feature=', 'justify',
-                                   'with-time', "no-collapse", "recommended",
-                                   "separator=", "cascade","cascade-features",
-                                   'list-features' ])
+    options, args = getOptions(args, "F:JTrRs:cC",
+                               ['feature=', 'justify',
+                                'with-time', "no-collapse", "recommended",
+                                "separator=", "cascade","cascade-features",
+                                'list-features'],
+                               dir=1, argsOK=1)
+##     options, args = getopt.getopt(args, "hf:D:vQF:JTrRs:cC",
+##                                 ['help', 'config=', "download-directory=",
+##                                  'verbose', 'quiet', 'feature=', 'justify',
+##                                  'with-time', "no-collapse", "recommended",
+##                                  "separator=", "cascade","cascade-features",
+##                                  'list-features', "status-fd=" ])
     try:
         parser = CLIArgumentParser(options, wantConfig=1,
                                    wantClientDirectory=1,
@@ -1519,8 +1683,9 @@
 """.strip()
 
 def updateServers(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:",
-               ['help', 'quiet', 'verbose', 'config='])
+    options, args = getOptions(args)
+##     options, args = getopt.getopt(args, "hvQf:",
+##                ['help', 'quiet', 'verbose', 'config=', "status-fd="])
 
     try:
         parser = CLIArgumentParser(options, wantConfig=1, wantClientDirectory=1,
@@ -1564,9 +1729,11 @@
 
 def clientDecode(cmd, args):
     """[Entry point] Decode a message."""
-    options, args = getopt.getopt(args, "hvQf:o:Fi:",
-          ['help', 'verbose', 'quiet', 'config=',
-           'output=', 'force', 'input=', 'passphrase-fd=',])
+    options, args = getOptions(args, input=1, output=1, passphrase=1)
+
+##     options, args = getopt.getopt(args, "hvQf:o:Fi:",
+##           ['help', 'verbose', 'quiet', 'config=',
+##            'output=', 'force', 'input=', 'passphrase-fd=', "status-fd="])
 
     outputFile = '-'
     inputFile = '-'
@@ -1666,10 +1833,14 @@
 """.strip()
 
 def generateSURB(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:D:t:H:P:o:bn:",
-          ['help', 'verbose', 'quiet', 'config=', 'download-directory=',
-           'to=', 'hops=', 'path=', 'lifetime=', 'passphrase-fd=',
-           'output=', 'binary', 'count=', 'identity='])
+    options, args = getOptions(args,
+                               "bn:", ["binary", "count=", "identity="],
+                               dir=1, dest=1, path=1, passphrase=1, output=1)
+
+##     options, args = getopt.getopt(args, "hvQf:D:t:H:P:o:bn:",
+##           ['help', 'verbose', 'quiet', 'config=', 'download-directory=',
+##            'to=', 'hops=', 'path=', 'lifetime=', 'passphrase-fd=',
+##            'output=', 'binary', 'count=', 'identity=', "status-fd="])
 
     outputFile = '-'
     binary = 0
@@ -1741,8 +1912,9 @@
 """.strip()
 
 def inspectSURBs(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:",
-             ["help", "verbose", "quiet", "config=", ])
+    options, args = getOptions(args, argsOK=1)
+##     options, args = getopt.getopt(args, "hvQf:",
+##              ["help", "verbose", "quiet", "config=", "status-fd="])
 
     try:
         parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
@@ -1770,6 +1942,9 @@
                     used = surblog.isSURBUsed(surb) and "yes" or "no"
                     print surb.format()
                     print "Used:", used
+                    STATUS.log("INSPECT_SURB", surb.getHexDigest(),
+                               surb.timestamp,
+                               surblog.isSURBUsed(surb) and "1" or "0")
             except ParseError, e:
                 print "Error while parsing: %s"%e
     finally:
@@ -1795,8 +1970,9 @@
 """.strip()
 
 def flushQueue(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:n:",
-             ["help", "verbose", "quiet", "config=", "count="])
+    options, args = getOptions(args, "", ["count="], argsOK=1)
+##    options, args = getopt.getopt(args, "hvQf:n:",
+##             ["help", "verbose", "quiet", "config=", "count=", "status-fd="])
     count=None
     for o,v in options:
         if o in ('-n','--count'):
@@ -1841,8 +2017,9 @@
 """.strip()
 
 def cleanQueue(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:d:",
-             ["help", "verbose", "quiet", "config=", "days=",])
+    options, args = getOptions(args, "d:" ["days"])
+##     options, args = getopt.getopt(args, "hvQf:d:",
+##              ["help", "verbose", "quiet", "config=", "days=", "status-fd="])
     days = 60
     for o,v in options:
         if o in ('-d','--days'):
@@ -1884,9 +2061,10 @@
 """.strip()
 
 def listQueue(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:D:",
-                                  ["help", "verbose", "quiet", "config=",
-                                   'download-directory=',])
+    options, args = getOptions(args, dir=1)
+##     options, args = getopt.getopt(args, "hvQf:D:",
+##                                   ["help", "verbose", "quiet", "config=",
+##                                    'download-directory=', "status-fd="])
     try:
         parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                    wantClient=1, wantClientDirectory=1)
@@ -1932,9 +2110,10 @@
 """.strip()
 
 def listFragments(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:D:",
-                                  ["help", "verbose", "quiet", "config=",
-                                   'download-directory=',])
+    options, args = getOptions(args, dir=1)
+##     options, args = getopt.getopt(args, "hvQf:D:",
+##                                   ["help", "verbose", "quiet", "config=",
+##                                    'download-directory=', "status-fd="])
     try:
         parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                    wantClient=1, wantClientDirectory=1)
@@ -1969,10 +2148,12 @@
 """.strip()
 
 def reassemble(cmd, args):
-    options, args = getopt.getopt(args, "hvQf:D:Po:F",
-                                  ["help", "verbose", "quiet", "config=",
-                                   'download-directory=','--purge',
-                                   '--output', '--force'])
+    options, args = getOptions(args, "PF", ["purge", "force"],
+                               output=1, argsOK=1)
+##     options, args = getopt.getopt(args, "hvQf:D:Po:F",
+##                                   ["help", "verbose", "quiet", "config=",
+##                                    'download-directory=','--purge',
+##                                    '--output', '--force',"status-fd="])
     reassemble = 1
     if cmd.endswith("purge-fragments") or cmd.endswith("purge-fragment"):
         reassemble = 0
@@ -1998,7 +2179,7 @@
             outfilename = v
         elif o in ("-P", "--purge"):
             purge = 1
-        elif o ("-F", "--force"):
+        elif o in ("-F", "--force"):
             force = 1
 
     if not args:

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.138
retrieving revision 1.139
diff -u -d -r1.138 -r1.139
--- Common.py	2 May 2004 18:45:15 -0000	1.138
+++ Common.py	14 May 2004 23:44:09 -0000	1.139
@@ -11,7 +11,7 @@
             'armorText', 'ceilDiv', 'checkPrivateDir', 'checkPrivateFile',
             'createPrivateDir', 'disp64',
             'encodeBase64', 'englishSequence', 'floorDiv', 'formatBase64',
-            'formatDate', 'formatFnameTime', 'formatTime',
+            'formatDate', 'formatFnameDate', 'formatFnameTime', 'formatTime',
             'installSIGCHLDHandler', 'isSMTPMailbox', 'openUnique',
             'previousMidnight', 'readFile', 'readPickled',
             'readPossiblyGzippedFile', 'secureDelete', 'stringContains',
@@ -1088,6 +1088,103 @@
     def close(self): pass
 
 #----------------------------------------------------------------------
+# StatusLog
+
+class StatusLog:
+    """Used to emit machine-parseable status messages to the file
+       descriptor specifed by the --status-fd argument. The set of valid
+       messages and their format is described in doc/statusfd.txt .
+
+       Each message consists of:
+            The string '[MIXMINION:]',
+            A space,
+            A 'message type' consisting only of capital letters, digits,
+                and underscores,
+            Optionally, a space, and a space-separated list of arguments,
+            A newline.
+
+       If an argument contains a space, a newline, or a backslash,
+       these characters are escaped with a backslash.
+
+       To maintain compatibility, arguments must only be added, never
+       removed, moved, or changed.  Messages should be emitted by
+       importing the global STATUS instance from this module and calling:
+
+       STATUS.log('SURB_GENERATED', surbid)
+
+       Each message type should be independent (it seems safer to not
+       assume any particular ordering between messages).
+
+       Keep machine-parseability in mind when adding these message types:
+       make sure the name is unique, and separate arguments with colons.
+    """
+    _ESC_PAT = re.compile(r'([ \n\\])')
+    def __init__(self):
+        """DOCDOC"""
+        self.fd = None
+        self.__lock = threading.Lock()
+
+    def setFD(self, fdnum):
+        """DOCDOC"""
+        # this will fail if the invoking user did not actually open the file
+        # descriptor they ask us to use, for example if they did:
+        #  mixminion send --status-fd=3
+        # instead of:
+        #  mixminion send --status-fd=3 3>somewhere.txt
+        #
+        # remember, this is not meant for use by a shell, it is for
+        # front-end programs that are running mixminion in a child process.
+        # There will be a pipe connected to this fd which the parent process
+        # will be reading from.
+
+        if fdnum is None:
+            self.fd = None
+        else:
+            self.fd = fdnum
+
+    def msg(self, name, args):
+        """DOCDOC"""
+        r = [ "[MIXMINION:]", name ]
+        for arg in args:
+            r.append(self._ESC_PAT.sub(r"\\\1",str(arg)))
+        return "%s\n"%(" ".join(r))
+
+    def log(self, name, *args):
+        """DOCDOC"""
+        if self.fd is None:
+            return
+
+        s = self.msg(name, args)
+        self.__lock.acquire()
+        try:
+            os.write(self.fd, s)
+        finally:
+            self.__lock.release()
+
+# The global Status instance for the mixminion client. Status messages
+# should be emitted with STATUS.log()
+STATUS = StatusLog()
+
+_STATUS_LINE_RE = re.compile(r'^\[MIXMINION:\] ([A-Z_]+)(?: (.*))?', re.S)
+_ARG_RE = re.compile(r'^((?:[^ \n\\]+|\\[ \n\\])+)[ \n]?')
+_UNESC_ARG_RE = re.compile(r'\\([ \n\\])')
+def parseStatusLogLine(s):
+    """DOCDOC"""
+    m = _STATUS_LINE_RE.match(s)
+    if not m:
+        return None,None
+    name = m.group(1)
+    s = m.group(2)
+    res = []
+    while s:
+        m = _ARG_RE.match(s)
+        if not m:
+            return None,None
+        res.append(_UNESC_ARG_RE.sub(r'\1', m.group(1)))
+        s = s[m.end():]
+    return name, res
+
+#----------------------------------------------------------------------
 # Time processing
 
 def previousMidnight(when):
@@ -1127,11 +1224,19 @@
     gmt = time.gmtime(when+1) # Add 1 to make sure we round down.
     return "%04d-%02d-%02d" % (gmt[0],gmt[1],gmt[2])
 
-def formatFnameTime(when=None):
+def formatFnameDate(when=None):
     """Given a time in seconds since the epoch, returns a date value suitable
        for use as part of a filename.  Defaults to the current time."""
     if when is None:
         when = time.time()
+    return time.strftime("%Y%m%d", time.localtime(when))
+
+def formatFnameTime(when=None):
+    """Given a time in seconds since the epoch, returns a date-time
+       value suitable for use as part of a filename.  Defaults to the
+       current time."""
+    if when is None:
+        when = time.time()
     return time.strftime("%Y%m%d%H%M%S", time.localtime(when))
 
 #----------------------------------------------------------------------

Index: Fragments.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Fragments.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -u -d -r1.15 -r1.16
--- Fragments.py	23 Mar 2004 00:05:32 -0000	1.15
+++ Fragments.py	14 May 2004 23:44:09 -0000	1.16
@@ -58,7 +58,7 @@
         self.nChunks = ceilDiv(minFragments, self.k)
         # Number of total fragments per chunk.
         self.n = int(math.ceil(EXP_FACTOR * self.k))
-        # Data in  a single chunk
+        # Data in a single chunk
         self.chunkSize = self.fragCapacity * self.k
         # Length of data to fill chunks
         self.paddedLen = self.nChunks * self.fragCapacity * self.k

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.72
retrieving revision 1.73
diff -u -d -r1.72 -r1.73
--- Main.py	6 Mar 2004 00:04:38 -0000	1.72
+++ Main.py	14 May 2004 23:44:09 -0000	1.73
@@ -127,6 +127,7 @@
     "generate-surbs" : ( 'mixminion.ClientMain', 'generateSURB' ),
     "inspect-surb" :   ( 'mixminion.ClientMain', 'inspectSURBs' ),
     "inspect-surbs" :  ( 'mixminion.ClientMain', 'inspectSURBs' ),
+    "count-packets":   ( 'mixminion.ClientMain', 'countPackets' ),
     "flush" :          ( 'mixminion.ClientMain', 'flushQueue' ),
     "inspect-queue" :  ( 'mixminion.ClientMain', 'listQueue' ),
     "clean-queue" :    ( 'mixminion.ClientMain', 'cleanQueue' ),
@@ -164,6 +165,7 @@
   "       decode         [Decode or decrypt a received message]\n"+
   "       generate-surb  [Generate a single-use reply block]\n"+
   "       inspect-surbs  [Describe a single-use reply block]\n"+
+  "       count-packets  [DOCDOC]\n"
   "       ping           [Quick and dirty check whether a server is running]\n"
   "                               (For Servers)\n"+
   "       server-start   [Begin running a Mixminion server]\n"+

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.75
retrieving revision 1.76
diff -u -d -r1.75 -r1.76
--- Packet.py	6 Mar 2004 00:04:38 -0000	1.75
+++ Packet.py	14 May 2004 23:44:09 -0000	1.76
@@ -39,8 +39,8 @@
 import zlib
 from socket import inet_ntoa, inet_aton
 from mixminion.Common import MixError, MixFatalError, encodeBase64, \
-     floorDiv, formatTime, isSMTPMailbox, LOG, armorText, unarmorText, \
-     isPlausibleHostname
+     floorDiv, formatBase64, formatTime, isSMTPMailbox, LOG, armorText, \
+     unarmorText, isPlausibleHostname
 from mixminion.Crypto import sha1
 
 if sys.version_info[:3] < (2,2,0):
@@ -524,8 +524,9 @@
         self.encryptionKey = key
 
     def format(self):
+        """DOCDOC"""
         import mixminion.ServerInfo
-        digest = binascii.b2a_hex(sha1(self.pack()))
+        digest = self.getHexDigest()
         expiry = formatTime(self.timestamp)
         if self.routingType == SWAP_FWD_IPV4_TYPE:
             routing = parseIPV4Info(self.routingInfo)
@@ -538,6 +539,10 @@
 Expires at: %s GMT
 First server is: %s""" % (digest, expiry, server)
 
+    def getHexDigest(self):
+        """DOCDOC"""
+        return binascii.b2a_hex(sha1(self.pack()))
+
     def pack(self):
         """Returns the external representation of this reply block"""
         return struct.pack(RB_UNPACK_PATTERN,
@@ -612,6 +617,9 @@
         return struct.pack(IPV4_PAT, inet_aton(self.ip),
                            self.port, self.keyinfo)
 
+    def __str__(self):
+        return "IP:%s:%s:%s"(self.ip,self.port,formatBase64(self.keyinfo))
+
     def __repr__(self):
         return "IPV4Info(%r, %r, %r)"%(self.ip, self.port, self.keyinfo)
 
@@ -664,6 +672,9 @@
         assert len(self.keyinfo) == DIGEST_LEN
         return struct.pack(MMTP_HOST_PAT,self.port,self.keyinfo)+self.hostname
 
+    def __str__(self):
+        return "%s:%s:%s"(self.hostname,self.port,formatBase64(self.keyinfo))
+
     def __repr__(self):
         return "MMTPHostInfo(%r, %r, %r)"%(
             self.hostname,self.port,self.keyinfo)

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.196
retrieving revision 1.197
diff -u -d -r1.196 -r1.197
--- test.py	5 May 2004 02:04:48 -0000	1.196
+++ test.py	14 May 2004 23:44:09 -0000	1.197
@@ -343,8 +343,10 @@
 
         now = time.time()
         ft = formatFnameTime(now)
+        fd = formatFnameDate(now)
         tm = time.localtime(now)
         self.assertEquals(ft, "%04d%02d%02d%02d%02d%02d" % tm[:6])
+        self.assertEquals(fd, "%04d%02d%02d" % tm[:3])
 
     def test_isSMTPMailbox(self):
         # Do we accept good addresses?
@@ -1884,6 +1886,7 @@
             for ov in 0, 42-20+16: # encrypted forward overhead
                 plds = BuildMessage.encodeMessage(m,ov,"",p)
                 assert len(plds) == 1
+                assert BuildMessage.getNPacketsToEncode(m,ov,"") == 1
                 pld = plds[0]
                 self.assertEquals(28*1024, len(pld)+ov)
                 comp = compressData(m)
@@ -2226,6 +2229,8 @@
         rsa1, rsa2 = self.pk1, self.pk512
         payloadE = BuildMessage.encodeMessage(
             "<<<<Hello>>>>"*100,ENC_FWD_OVERHEAD)[0]
+        assert BuildMessage.getNPacketsToEncode("<<<<Hello>>>>"*100,
+                                                ENC_FWD_OVERHEAD) == 1
         for rsakey in rsa1,rsa2:
             m = befm(payloadE, 500, "Phello",
                      [self.server1, self.server2],
@@ -2430,6 +2435,7 @@
         prefix = "Prefix!"
         payloads = BuildMessage.encodeMessage(msg, 0, prefix)
         self.assertEquals(len(payloads), 3)
+        self.assertEquals(BuildMessage.getNPacketsToEncode(msg, 0, prefix), 3)
         p1 = BuildMessage.decodePayload(payloads[0], "")
         p2 = BuildMessage.decodePayload(payloads[1], "")
         p3 = BuildMessage.decodePayload(payloads[2], "")
@@ -3218,6 +3224,40 @@
         for fn in os.listdir(d_parent):
             os.unlink(os.path.join(d_parent,fn))
 
+        # WritethroughDict
+        loc = os.path.join(d_parent, "db4")
+        WD = mixminion.Filestore.WritethroughDict
+        d = WD(loc, "testing")
+        d["xyzzy"] = 91
+        d["bliznert"] = (4,22,11,8)
+        self.assertEquals(d["xyzzy"], 91)
+        self.assertEquals(d["bliznert"], (4,22,11,8))
+        d.close()
+
+        d = WD(loc, "testing")
+        self.assertEquals(d["xyzzy"], 91)
+        self.assert_(d.has_key("bliznert"))
+        d["bliznert"] = [ 12, "aeaeae" ]
+        d["mulligan"] = ( "stately, plump", )
+        self.assertEquals(d.get("mulligan"), ("stately, plump",))
+        self.assertEquals(d.get("mulliganX"), None)
+        self.assertEquals(d["bliznert"], [12, "aeaeae"])
+        del d['xyzzy']
+        self.assert_(not d.has_key("xyzzy"))
+        d.close()
+
+        d = WD(loc, "testing")
+        try:
+            d["xyzzy"]
+        except KeyError:
+            pass
+        else:
+            self.fail()
+        self.assertEquals(d.get("mulligan"), ("stately, plump",))
+        self.assertEquals(d["bliznert"], [12, "aeaeae"])
+        self.assertUnorderedEq(d.keys(), ["mulligan","bliznert"])
+        d.close()
+
 class QueueTests(FStoreTestBase):
     def setUp(self):
         mixminion.Common.installSIGCHLDHandler()
@@ -3303,6 +3343,10 @@
         queue.removeAll(self.unlink)
         queue.cleanQueue(self.unlink)
 
+    def testAddressBasedQueue(self):
+        #XXXX008
+        pass
+
     def testMixPools(self):
         d_m = mix_mktemp("qm")
 
@@ -3428,6 +3472,37 @@
         self.assertEquals(lines[4], "[WARN] ->STREAM: C")
         self.assertEquals(lines[5], "[WARN] ->STREAM: X Y")
 
+    def testStatusLog(self):
+        SL = mixminion.Common.StatusLog()
+        self.assertEquals("[MIXMINION:] ABC\n",
+                          SL.msg("ABC", []))
+        self.assertEquals("[MIXMINION:] ABC 42 HELLO\n",
+                          SL.msg("ABC", [42, "HELLO"]))
+        self.assertEquals("[MIXMINION:] ABC A A\\ BC\\ D\n",
+                          SL.msg("ABC", ["A", "A BC D"]))
+        self.assertEquals("[MIXMINION:] A_BCD A\\\nB\\\\X\n",
+                          SL.msg("A_BCD", ["A\nB\\X"]))
+        tmpfile = mix_mktemp()
+        f = open(tmpfile, 'w')
+        SL.setFD(f.fileno())
+        SL.log("REVOLUTION_9", "number nine", "number nine")
+        SL.log("REVOLUTION_9", "take this brother")
+        SL.setFD(None)
+        f.close()
+        self.assertEquals(readFile(tmpfile),
+                   "[MIXMINION:] REVOLUTION_9 number\\ nine number\\ nine\n"
+                   "[MIXMINION:] REVOLUTION_9 take\\ this\\ brother\n")
+
+        PSL = mixminion.Common.parseStatusLogLine
+        self.assertEquals(PSL("[MIXMINION:] ABC 42 HELLO\n"),
+                          ("ABC",["42", "HELLO"]))
+        self.assertEquals(PSL("[MIXMINION:] ABC 42 HELLO\\ \n"),
+                          ("ABC",["42", "HELLO "]))
+        self.assertEquals(PSL("[MIXMINION:] ABC\n"),
+                          ("ABC",[]))
+        self.assertEquals(PSL("[MIXMINION:] ABC X\\\nYZ 03.1\\\\\n"),
+                          ("ABC",["X\nYZ", "03.1\\"]))
+
 #----------------------------------------------------------------------
 # File paranoia
 
@@ -7526,7 +7601,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(ClientMainTests))
+        suite.addTest(tc(FilestoreTests))
         return suite
     testClasses = [MiscTests,
                    MinionlibCryptoTests,