Index: pykota/trunk/conf/pykota.conf.sample
===================================================================
--- pykota/trunk/conf/pykota.conf.sample (revision 1510)
+++ pykota/trunk/conf/pykota.conf.sample (revision 1517)
@@ -152,4 +152,20 @@
 # If the value is not set then localhost is used.
 smtpserver: localhost
+
+# Crash messages' recipient : in addition to the log files
+# each software crash can be sent to the author of PyKota
+# or any other person of your choice. By default this
+# is disabled. The recipient pykotacrashed@librelogiciel.com
+# reaches PyKota's author.
+# The 'adminmail' (defined a bit below) is CCed.
+#
+# Privacy concerns : what is sent is only :
+#
+#        - a copy of the software's traceback
+#        - a copy of the software's command line arguments
+#        - a copy of the software's environment variables
+# 
+# suggested value
+# crashrecipient: pykotacrashed@librelogiciel.com
 
 # Email domain
@@ -458,4 +474,5 @@
 # PYKOTASTATUS : contains "CANCELLED" when SIGTERM was received by PyKota
 #                else is not set.
+# PYKOTAJOBSIZEBYTES : contains the job's size in bytes. Always available.
 # PYKOTAPRECOMPUTEDJOBSIZE : contains the precomputed job's size
 # PYKOTAPRECOMPUTEDJOBPRICE : contains the precomputed job's price
Index: pykota/trunk/pykota/tool.py
===================================================================
--- pykota/trunk/pykota/tool.py (revision 1516)
+++ pykota/trunk/pykota/tool.py (revision 1517)
@@ -22,4 +22,9 @@
 #
 # $Log$
+# Revision 1.95  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.94  2004/06/03 08:51:03  jalet
 # logs job's size in bytes now
@@ -391,9 +396,9 @@
         self.documentation = doc
         self.config = config.PyKotaConfig("/etc/pykota")
-        self.logger = logger.openLogger(self.config.getLoggingBackend())
         self.debug = self.config.getDebug()
-        self.storage = storage.openConnection(self)
         self.smtpserver = self.config.getSMTPServer()
         self.maildomain = self.config.getMailDomain()
+        self.logger = logger.openLogger(self.config.getLoggingBackend())
+        self.storage = storage.openConnection(self)
         self.softwareJobSize = 0
         self.softwareJobPrice = 0.0
@@ -422,4 +427,27 @@
         print self.documentation
         sys.exit(0)
+        
+    def crashed(self, message) :    
+        """Outputs a crash message, and optionally sends it to software author."""
+        import traceback
+        lines = []
+        for line in traceback.format_exception(*sys.exc_info()) :
+            lines.extend([l for l in line.split("\n") if l])
+        msg = "ERROR : ".join(["%s\n" % l for l in ([message] + lines)])
+        sys.stderr.write(msg)
+        sys.stderr.flush()
+        crashrecipient = self.config.getCrashRecipient()
+        if crashrecipient :
+            try :
+                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
+                fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
+                                (msg, \
+                                 "\n".join(["    %s" % repr(a) for a in sys.argv]), \
+                                 "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
+                server = smtplib.SMTP(self.smtpserver)
+                server.sendmail(admin, [admin, crashrecipient], "From: %s\nTo: %s\nCc: %s\nSubject: PyKota crash traceback !\n\n%s" % (admin, crashrecipient, admin, fullmessage))
+                server.quit()
+            except :
+                pass
         
     def parseCommandline(self, argv, short, long, allownothing=0) :
@@ -816,6 +844,6 @@
         self.exportJobInfo()
         self.jobdatastream = self.openJobDataStream()
-        os.putenv("PYKOTAJOBSIZEBYTES", str(self.JobSizeBytes))
-        self.logdebug("Job size is %s bytes" % self.JobSizeBytes)
+        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes)
+        self.logdebug("Job size is %s bytes" % self.jobSizeBytes)
         self.logdebug("Capturing SIGTERM events.")
         signal.signal(signal.SIGTERM, self.sigterm_handler)
@@ -829,5 +857,5 @@
             self.logdebug("Duplicating data stream from stdin to temporary file")
             MEGABYTE = 1024*1024
-            self.JobSizeBytes = 0
+            self.jobSizeBytes = 0
             infile = tempfile.TemporaryFile()
             while 1 :
@@ -835,5 +863,5 @@
                 if not data :
                     break
-                self.JobSizeBytes += len(data)    
+                self.jobSizeBytes += len(data)    
                 infile.write(data)
             infile.flush()    
@@ -843,5 +871,5 @@
             # real file, just open it
             self.logdebug("Opening data stream %s" % self.preserveinputfile)
-            self.JobSizeBytes = os.stat(self.preserveinputfile)[6]
+            self.jobSizeBytes = os.stat(self.preserveinputfile)[6]
             return open(self.preserveinputfile, "rb")
         
@@ -879,33 +907,33 @@
         """Sets an attribute whenever SIGTERM is received."""
         self.gotSigTerm = 1
-        os.putenv("PYKOTASTATUS", "CANCELLED")
+        os.environ["PYKOTASTATUS"] = "CANCELLED"
         self.logger.log_message(_("SIGTERM received, job %s cancelled.") % self.jobid, "info")
         
     def exportJobInfo(self) :    
         """Exports job information to the environment."""
-        os.putenv("PYKOTAUSERNAME", str(self.username))
-        os.putenv("PYKOTAPRINTERNAME", str(self.printername))
-        os.putenv("PYKOTAJOBID", str(self.jobid))
-        os.putenv("PYKOTATITLE", self.title or "")
-        os.putenv("PYKOTAFILENAME", self.preserveinputfile or "")
-        os.putenv("PYKOTACOPIES", str(self.copies))
-        os.putenv("PYKOTAOPTIONS", self.options or "")
+        os.environ["PYKOTAUSERNAME"] = str(self.username)
+        os.environ["PYKOTAPRINTERNAME"] = str(self.printername)
+        os.environ["PYKOTAJOBID"] = str(self.jobid)
+        os.environ["PYKOTATITLE"] = self.title or ""
+        os.environ["PYKOTAFILENAME"] = self.preserveinputfile or ""
+        os.environ["PYKOTACOPIES"] = str(self.copies)
+        os.environ["PYKOTAOPTIONS"] = self.options or ""
     
     def exportUserInfo(self, userpquota) :
         """Exports user information to the environment."""
-        os.putenv("PYKOTALIMITBY", str(userpquota.User.LimitBy))
-        os.putenv("PYKOTABALANCE", str(userpquota.User.AccountBalance or 0.0))
-        os.putenv("PYKOTALIFETIMEPAID", str(userpquota.User.LifeTimePaid or 0.0))
-        os.putenv("PYKOTAPAGECOUNTER", str(userpquota.PageCounter or 0))
-        os.putenv("PYKOTALIFEPAGECOUNTER", str(userpquota.LifePageCounter or 0))
-        os.putenv("PYKOTASOFTLIMIT", str(userpquota.SoftLimit))
-        os.putenv("PYKOTAHARDLIMIT", str(userpquota.HardLimit))
-        os.putenv("PYKOTADATELIMIT", str(userpquota.DateLimit))
+        os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy)
+        os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0)
+        os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0)
+        os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0)
+        os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0)
+        os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit)
+        os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit)
+        os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit)
         
         # not really an user information, but anyway
         # exports the list of printers groups the current
         # printer is a member of
-        os.putenv("PYKOTAPGROUPS", ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)]))
-            
+        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)])
+        
     def prehook(self, userpquota) :
         """Allows plugging of an external hook before the job gets printed."""
Index: pykota/trunk/pykota/version.py
===================================================================
--- pykota/trunk/pykota/version.py (revision 1502)
+++ pykota/trunk/pykota/version.py (revision 1517)
@@ -22,5 +22,5 @@
 #
 
-__version__ = "1.19alpha16_unofficial"
+__version__ = "1.19alpha17_unofficial"
 
 __doc__ = """PyKota : a complete Printing Quota Solution for CUPS and LPRng."""
Index: pykota/trunk/bin/warnpykota
===================================================================
--- pykota/trunk/bin/warnpykota (revision 1257)
+++ pykota/trunk/bin/warnpykota (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.27  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.26  2004/01/08 14:10:32  jalet
 # Copyright year changed.
@@ -247,7 +252,9 @@
         else :
             retcode = sender.main(args, options)
-    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :            
-        sys.stderr.write("%s\n" % msg)
-        sys.stderr.flush()
+    except :
+        try :
+            sender.crashed("warnpykota failed")
+        except :    
+            pass
         retcode = -1
         
Index: pykota/trunk/bin/edpykota
===================================================================
--- pykota/trunk/bin/edpykota (revision 1436)
+++ pykota/trunk/bin/edpykota (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.73  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.72  2004/04/16 16:20:19  jalet
 # Note about not implemented limitby values
@@ -690,7 +695,9 @@
         else :
             retcode = editor.main(args, options)
-    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :            
-        sys.stderr.write("%s\n" % msg)
-        sys.stderr.flush()
+    except :
+        try :
+            editor.crashed("edpykota failed")
+        except :    
+            pass
         retcode = -1
 
Index: pykota/trunk/bin/pkprinters
===================================================================
--- pykota/trunk/bin/pkprinters (revision 1453)
+++ pykota/trunk/bin/pkprinters (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.9  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.8  2004/05/06 20:30:24  jalet
 # Added --skipexisting command line option to pkprinters
@@ -255,7 +260,9 @@
         else :
             retcode = manager.main(args, options)
-    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :            
-        sys.stderr.write("%s\n" % msg)
-        sys.stderr.flush()
+    except :
+        try :
+            manager.crashed("pkprinters failed")
+        except :    
+            pass
         retcode = -1
 
Index: pykota/trunk/bin/cupspykota
===================================================================
--- pykota/trunk/bin/cupspykota (revision 1515)
+++ pykota/trunk/bin/cupspykota (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.58  2004/06/03 21:50:33  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.57  2004/06/02 22:18:07  jalet
 # I think the bug when cancelling jobs should be fixed right now
@@ -330,8 +335,8 @@
             clienthost = self.getJobOriginatingHostname(printer.Name, user.Name, self.jobid)
             self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))    
-            os.putenv("PYKOTAJOBORIGINATINGHOSTNAME", str(clienthost or ""))
+            os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
             
             # enters first phase
-            os.putenv("PYKOTAPHASE", "BEFORE")
+            os.environ["PYKOTAPHASE"] = "BEFORE"
             
             # checks the user's quota
@@ -340,10 +345,10 @@
                 self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
                 self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
-            os.putenv("PYKOTAPRECOMPUTEDJOBSIZE", str(self.softwareJobSize))    
-            os.putenv("PYKOTAPRECOMPUTEDJOBPRICE", str(self.softwareJobPrice))    
+            os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize)
+            os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
             action = self.warnUserPQuota(userpquota)
             
             # exports some new environment variables
-            os.putenv("PYKOTAACTION", action)
+            os.environ["PYKOTAACTION"] = action
             
             # launches the pre hook
@@ -354,5 +359,5 @@
         else :    
             action = "ALLOW"
-            os.putenv("PYKOTAACTION", action)
+            os.environ["PYKOTAACTION"] = action
             
         # pass the job's data to the real backend    
@@ -367,5 +372,5 @@
         if policy == "OK" :        
             # indicate phase change
-            os.putenv("PYKOTAPHASE", "AFTER")
+            os.environ["PYKOTAPHASE"] = "AFTER"
             
             # stops accounting. 
@@ -390,8 +395,8 @@
             
             # exports some new environment variables
-            os.putenv("PYKOTAJOBSIZE", str(jobsize))
-            os.putenv("PYKOTAJOBPRICE", str(jobprice))
-            
-            # then re-export user information with new values
+            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
+            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
+            
+            # then re-export user information with new value
             self.exportUserInfo(userpquota)
             
@@ -660,10 +665,8 @@
             retcode = kotabackend.mainWork()
         except :
-            import traceback
-            lines = []
-            for line in traceback.format_exception(*sys.exc_info()) :
-                lines.extend([l for l in line.split("\n") if l])
-            sys.stderr.write("ERROR : cupspykota backend failed : \n%s" % (msg, "ERROR : ".join(["%s\n" % l for l in lines])))
-            sys.stderr.flush()
+            try :
+                kotabackend.crashed("cupspykota backend failed")
+            except :    
+                pass
             retcode = 1
         
Index: pykota/trunk/bin/pykotme
===================================================================
--- pykota/trunk/bin/pykotme (revision 1488)
+++ pykota/trunk/bin/pykotme (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.10  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.9  2004/05/21 20:53:34  jalet
 # Now pykotme doesn't spawn a new process anymore to compute job's size, but
@@ -186,7 +191,9 @@
         else :
             retcode = sender.main(args, options)
-    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :            
-        sys.stderr.write("%s\n" % msg)
-        sys.stderr.flush()
+    except :
+        try :
+            sender.crashed("pykotme failed")
+        except :    
+            pass
         retcode = -1
 
Index: pykota/trunk/bin/pkhint
===================================================================
--- pykota/trunk/bin/pkhint (revision 1483)
+++ pykota/trunk/bin/pkhint (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.9  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.8  2004/05/18 14:48:47  jalet
 # Big code changes to completely remove the need for "requester" directives,
@@ -266,7 +271,9 @@
                 args = [ "*" ]
             retcode = manager.main(args, options)
-    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :            
-        sys.stderr.write("%s\n" % msg)
-        sys.stderr.flush()
+    except :
+        try :
+            manager.crashed("pkhint failed")
+        except :    
+            pass
         retcode = -1
 
Index: pykota/trunk/bin/repykota
===================================================================
--- pykota/trunk/bin/repykota (revision 1257)
+++ pykota/trunk/bin/repykota (revision 1517)
@@ -24,4 +24,9 @@
 #
 # $Log$
+# Revision 1.47  2004/06/03 21:50:34  jalet
+# Improved error logging.
+# crashrecipient directive added.
+# Now exports the job's size in bytes too.
+#
 # Revision 1.46  2004/01/08 14:10:32  jalet
 # Copyright year changed.
@@ -307,8 +312,10 @@
         else :
             retcode = reportTool.main(args, options)
-    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaReporterError), msg :            
-        sys.stderr.write("%s\n" % msg)
-        sys.stderr.flush()
-        retcode = -1 
+    except :
+        try :
+            reportTool.crashed("repykota failed")
+        except :    
+            pass
+        retcode = -1
 
     try :
Index: pykota/trunk/NEWS
===================================================================
--- pykota/trunk/NEWS (revision 1510)
+++ pykota/trunk/NEWS (revision 1517)
@@ -23,4 +23,17 @@
 
 
+    - 1.19alpha17 :
+    
+        - Mysterious bug (program exited unexpectedly without leaving
+          any log message) which occured once in a while when
+          cancelling jobs should be fixed now.
+          
+        - New 'crashrecipient' directive. Disabled by  
+          default. See sample configuration file to see how
+          to activate it. It is suggested that you activate it.
+          
+        - New PYKOTAJOBSIZEBYTES environment variable exported,  
+          which contains the job's size in bytes.
+          
     - 1.19alpha16 :
     
