root / pykota / trunk / bin / cupspykota @ 1503

Revision 1503, 28.2 kB (checked in by jalet, 20 years ago)

Now logs something when client hostname can't be extracted

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# CUPSPyKota accounting backend
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
26# Revision 1.54  2004/05/26 16:44:48  jalet
27# Now logs something when client hostname can't be extracted
28#
29# Revision 1.53  2004/05/26 14:49:35  jalet
30# First try at saving the job-originating-hostname in the database
31#
32# Revision 1.52  2004/05/25 09:15:13  jalet
33# accounter.py : old code deleted
34# the rest : now exports PYKOTAPRECOMPUTEDJOBSIZE and PYKOTAPRECOMPUTEDJOBPRICE
35#
36# Revision 1.51  2004/05/25 08:31:16  jalet
37# Heavy CPU usage seems to be fixed at least !
38#
39# Revision 1.50  2004/05/25 05:17:50  jalet
40# Now precomputes the job's size only if current printer's enforcement
41# is "STRICT"
42#
43# Revision 1.49  2004/05/24 22:45:48  jalet
44# New 'enforcement' directive added
45# Polling loop improvements
46#
47# Revision 1.48  2004/05/24 14:36:24  jalet
48# Revert to old polling loop. Will need optimisations
49#
50# Revision 1.47  2004/05/24 11:59:46  jalet
51# More robust (?) code
52#
53# Revision 1.46  2004/05/21 22:02:51  jalet
54# Preliminary work on pre-accounting
55#
56# Revision 1.45  2004/05/19 07:15:32  jalet
57# Could the 'misterious' bug in my loop be finally fixed ???
58#
59# Revision 1.44  2004/05/18 14:48:47  jalet
60# Big code changes to completely remove the need for "requester" directives,
61# jsut use "hardware(... your previous requester directive's content ...)"
62#
63# Revision 1.43  2004/05/17 11:46:05  jalet
64# First try at cupspykota's main loop rewrite
65#
66# Revision 1.42  2004/05/10 11:22:28  jalet
67# Typo
68#
69# Revision 1.41  2004/05/10 10:07:30  jalet
70# Catches OSError while reading
71#
72# Revision 1.40  2004/05/10 09:29:48  jalet
73# Should be more robust if we receive a SIGTERM during an I/O operation
74#
75# Revision 1.39  2004/05/07 14:44:53  jalet
76# Fix for file handles unregistered twice from the polling object
77#
78# Revision 1.38  2004/04/09 22:24:46  jalet
79# Began work on correct handling of child processes when jobs are cancelled by
80# the user. Especially important when an external requester is running for a
81# long time.
82#
83# Revision 1.37  2004/03/18 19:11:25  jalet
84# Fix for raw jobs in cupspykota
85#
86# Revision 1.36  2004/03/18 14:03:18  jalet
87# Added fsync() calls
88#
89# Revision 1.35  2004/03/16 12:05:01  jalet
90# Small fix for new waitprinter.sh : when job was denied, would wait forever
91# for printer being in printing mode.
92#
93# Revision 1.34  2004/03/15 10:47:56  jalet
94# This time the traceback formatting should be correct !
95#
96# Revision 1.33  2004/03/05 12:46:07  jalet
97# Improve tracebacks
98#
99# Revision 1.32  2004/03/05 12:31:35  jalet
100# Now should output full traceback when crashing
101#
102# Revision 1.31  2004/03/01 14:35:56  jalet
103# PYKOTAPHASE wasn't set soon enough at the start of the job
104#
105# Revision 1.30  2004/03/01 14:34:15  jalet
106# PYKOTAPHASE wasn't set at the right time at the end of data transmission
107# to underlying layer (real backend)
108#
109# Revision 1.29  2004/03/01 11:23:25  jalet
110# Pre and Post hooks to external commands are available in the cupspykota
111# backend. Forthe pykota filter they will be implemented real soon now.
112#
113# Revision 1.28  2004/02/26 14:18:07  jalet
114# Should fix the remaining bugs wrt printers groups and users groups.
115#
116# Revision 1.27  2004/02/04 23:41:27  jalet
117# Should fix the incorrect "backend died abnormally" problem.
118#
119# Revision 1.26  2004/01/30 16:35:03  jalet
120# Fixes stupid software accounting bug in CUPS backend
121#
122# Revision 1.25  2004/01/16 17:51:46  jalet
123# Fuck Fuck Fuck !!!
124#
125# Revision 1.24  2004/01/14 15:52:01  jalet
126# Small fix for job cancelling code.
127#
128# Revision 1.23  2004/01/13 10:48:28  jalet
129# Small streams polling loop modification.
130#
131# Revision 1.22  2004/01/12 22:43:40  jalet
132# New formula to compute a job's price
133#
134# Revision 1.21  2004/01/12 18:17:36  jalet
135# Denied jobs weren't stored into the history anymore, this is now fixed.
136#
137# Revision 1.20  2004/01/11 23:22:42  jalet
138# Major code refactoring, it's way cleaner, and now allows automated addition
139# of printers on first print.
140#
141# Revision 1.19  2004/01/08 14:10:32  jalet
142# Copyright year changed.
143#
144# Revision 1.18  2004/01/07 16:16:32  jalet
145# Better debugging information
146#
147# Revision 1.17  2003/12/27 16:49:25  uid67467
148# Should be ok now.
149#
150# Revision 1.17  2003/12/06 08:54:29  jalet
151# Code simplifications.
152# Added many debugging messages.
153#
154# Revision 1.16  2003/11/26 20:43:29  jalet
155# Inadvertantly introduced a bug, which is fixed.
156#
157# Revision 1.15  2003/11/26 19:17:35  jalet
158# Printing on a printer not present in the Quota Storage now results
159# in the job being stopped or cancelled depending on the system.
160#
161# Revision 1.14  2003/11/25 13:25:45  jalet
162# Boolean problem with old Python, replaced with 0
163#
164# Revision 1.13  2003/11/23 19:01:35  jalet
165# Job price added to history
166#
167# Revision 1.12  2003/11/21 14:28:43  jalet
168# More complete job history.
169#
170# Revision 1.11  2003/11/19 23:19:35  jalet
171# Code refactoring work.
172# Explicit redirection to /dev/null has to be set in external policy now, just
173# like in external mailto.
174#
175# Revision 1.10  2003/11/18 17:54:24  jalet
176# SIGTERMs are now transmitted to original backends.
177#
178# Revision 1.9  2003/11/18 14:11:07  jalet
179# Small fix for bizarre urls
180#
181# Revision 1.8  2003/11/15 14:26:44  jalet
182# General improvements to the documentation.
183# Email address changed in sample configuration file, because
184# I receive low quota messages almost every day...
185#
186# Revision 1.7  2003/11/14 22:05:12  jalet
187# New CUPS backend fully functionnal.
188# Old CUPS configuration method is now officially deprecated.
189#
190# Revision 1.6  2003/11/14 20:13:11  jalet
191# We exit the loop too soon.
192#
193# Revision 1.5  2003/11/14 18:31:27  jalet
194# Not perfect, but seems to work with the poll() loop.
195#
196# Revision 1.4  2003/11/14 17:04:15  jalet
197# More (untested) work on the CUPS backend.
198#
199# Revision 1.3  2003/11/12 23:27:44  jalet
200# More work on new backend. This commit may be unstable.
201#
202# Revision 1.2  2003/11/12 09:33:34  jalet
203# New CUPS backend supports device enumeration
204#
205# Revision 1.1  2003/11/08 16:05:31  jalet
206# CUPS backend added for people to experiment.
207#
208#
209#
210
211import sys
212import os
213import fcntl
214import popen2
215import cStringIO
216import shlex
217import select
218import signal
219import time
220
221from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError
222from pykota.config import PyKotaConfigError
223from pykota.storage import PyKotaStorageError
224from pykota.accounter import PyKotaAccounterError
225   
226class PyKotaPopen4(popen2.Popen4) :
227    """Our own class to execute real backends.
228   
229       Their first argument is different from their path so using
230       native popen2.Popen3 would not be feasible.
231    """
232    def __init__(self, cmd, bufsize=-1, arg0=None) :
233        self.arg0 = arg0
234        popen2.Popen4.__init__(self, cmd, bufsize)
235       
236    def _run_child(self, cmd):
237        for i in range(3, 256): # TODO : MAXFD in original popen2 module
238            try:
239                os.close(i)
240            except OSError:
241                pass
242        try:
243            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
244        finally:
245            os._exit(1)
246   
247class PyKotaBackend(PyKotaFilterOrBackend) :       
248    """A class for the pykota backend."""
249    def acceptJob(self) :       
250        """Returns the appropriate exit code to tell CUPS all is OK."""
251        return 0
252           
253    def removeJob(self) :           
254        """Returns the appropriate exit code to let CUPS think all is OK.
255       
256           Returning 0 (success) prevents CUPS from stopping the print queue.
257        """   
258        return 0
259       
260    def getPageLogLocation(self) :
261        """Retrieves CUPS' page_log file location."""
262        location = None
263        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
264        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
265        try :
266            conffile = open(cupsdconf, "r")
267        except IOError :   
268            self.logdebug("Unable to open %s" % cupsdconf)
269            return # file doesn't exist or can't be read
270        else :   
271            for line in conffile.readlines() :
272                linecopy = line.strip().lower()
273                if linecopy.startswith("pagelog ") :
274                    try :
275                        location = line.split()[1]
276                    except :   
277                        pass # ignore errors, we take the last value in any case.
278            conffile.close()           
279            return location           
280           
281    def getJobOriginatingHostname(self, printername, username, jobid) :
282        """Retrieves the job-originating-hostname from the CUPS page_log file if possible."""
283        pagelogpath = self.getPageLogLocation() or "/var/log/cups/page_log"
284        try :
285            pagelog = open(pagelogpath, "r")
286        except IOError :   
287            self.logdebug("Unable to open %s" % pagelogpath)
288            return # no page log or can't read it, originating hostname unknown yet
289        else :   
290            # TODO : read backward so we could take first value seen
291            # TODO : here we read forward so we must take the last value seen
292            prefix = "%s %s %s" % (printername, username, jobid)
293            matchingline = None
294            while 1 :
295                line = pagelog.readline()
296                if not line :
297                    break
298                else :
299                    line = line.strip()
300                    if line.startswith(prefix) :   
301                        matchingline = line # no break, because we read forward
302            pagelog.close()       
303            if matchingline is None :
304                self.logdebug("No matching line found in page_log")
305                return # correct line not found, job-originating-hostname unknown
306            else :   
307                return matchingline.split()[-1]
308               
309    def doWork(self, policy, printer, user, userpquota) :   
310        """Most of the work is done here."""
311        # Two different values possible for policy here :
312        # ALLOW means : Either printer, user or user print quota doesn't exist,
313        #               but the job should be allowed anyway.
314        # OK means : Both printer, user and user print quota exist, job should
315        #            be allowed if current user is allowed to print on this printer
316        if policy == "OK" :
317            # exports user information with initial values
318            self.exportUserInfo(userpquota)
319           
320            # tries to extract job-originating-hostname
321            clienthost = self.getJobOriginatingHostname(printer.Name, user.Name, self.jobid)
322            self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
323            os.putenv("PYKOTAJOBORIGINATINGHOSTNAME", str(clienthost or ""))
324           
325            # enters first phase
326            os.putenv("PYKOTAPHASE", "BEFORE")
327           
328            # checks the user's quota
329            if self.config.getPrinterEnforcement(printer.Name) == "STRICT" :
330                self.softwareJobSize = self.precomputeJobSize()
331                self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
332                self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
333            os.putenv("PYKOTAPRECOMPUTEDJOBSIZE", str(self.softwareJobSize))   
334            os.putenv("PYKOTAPRECOMPUTEDJOBPRICE", str(self.softwareJobPrice))   
335            action = self.warnUserPQuota(userpquota)
336           
337            # exports some new environment variables
338            os.putenv("PYKOTAACTION", action)
339           
340            # launches the pre hook
341            self.prehook(userpquota)
342           
343            self.logdebug("Job accounting begins.")
344            self.accounter.beginJob(userpquota)
345        else :   
346            action = "ALLOW"
347            os.putenv("PYKOTAACTION", action)
348           
349        # pass the job's data to the real backend   
350        if action in ["ALLOW", "WARN"] :
351            if self.gotSigTerm :
352                retcode = self.removeJob()
353            else :   
354                retcode = self.handleData()       
355        else :       
356            retcode = self.removeJob()
357       
358        if policy == "OK" :       
359            # indicate phase change
360            os.putenv("PYKOTAPHASE", "AFTER")
361           
362            # stops accounting.
363            self.accounter.endJob(userpquota)
364            self.logdebug("Job accounting ends.")
365               
366            # retrieve the job size   
367            if action == "DENY" :
368                jobsize = 0
369                self.logdebug("Job size forced to 0 because printing is denied.")
370            else :   
371                jobsize = self.accounter.getJobSize()
372            self.logdebug("Job size : %i" % jobsize)
373           
374            # update the quota for the current user on this printer
375            self.logdebug("Updating user %s's quota on printer %s" % (user.Name, printer.Name))
376            jobprice = userpquota.increasePagesUsage(jobsize)
377           
378            # adds the current job to history   
379            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), action, jobsize, jobprice, self.preserveinputfile, self.title, self.copies, self.options, clienthost)
380            self.logdebug("Job added to history.")
381           
382            # exports some new environment variables
383            os.putenv("PYKOTAJOBSIZE", str(jobsize))
384            os.putenv("PYKOTAJOBPRICE", str(jobprice))
385           
386            # then re-export user information with new values
387            self.exportUserInfo(userpquota)
388           
389            # Launches the post hook
390            self.posthook(userpquota)
391           
392        return retcode   
393               
394    def unregisterFileNo(self, pollobj, fileno) :               
395        """Removes a file handle from the polling object."""
396        try :
397            pollobj.unregister(fileno)
398        except KeyError :   
399            self.logger.log_message(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn")
400        except :   
401            self.logdebug("Error while unregistering file number %s from polling object." % fileno)
402        else :   
403            self.logdebug("File number %s unregistered from polling object." % fileno)
404           
405    def formatFileEvent(self, fd, mask) :       
406        """Formats file debug info."""
407        maskval = []
408        if mask & select.POLLIN :
409            maskval.append("POLLIN")
410        if mask & select.POLLOUT :
411            maskval.append("POLLOUT")
412        if mask & select.POLLPRI :
413            maskval.append("POLLPRI")
414        if mask & select.POLLERR :
415            maskval.append("POLLERR")
416        if mask & select.POLLHUP :
417            maskval.append("POLLHUP")
418        if mask & select.POLLNVAL :
419            maskval.append("POLLNVAL")
420        return "%s (%s)" % (fd, " | ".join(maskval))
421       
422    def handleData(self) :                   
423        """Pass the job's data to the real backend."""
424        # Find the real backend pathname   
425        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
426       
427        # And launch it
428        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + sys.argv[1:])])))
429        subprocess = PyKotaPopen4([realbackend] + sys.argv[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
430       
431        # Save file descriptors, we will need them later.
432        stderrfno = sys.stderr.fileno()
433        fromcfno = subprocess.fromchild.fileno()
434        tocfno = subprocess.tochild.fileno()
435       
436        # We will have to be careful when dealing with I/O
437        # So we use a poll object to know when to read or write
438        pollster = select.poll()
439        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
440        pollster.register(stderrfno, select.POLLOUT)
441        pollster.register(tocfno, select.POLLOUT)
442       
443        # Initialize our buffers
444        indata = ""
445        outdata = ""
446        endinput = endoutput = 0
447        inputclosed = outputclosed = 0
448       
449        if self.preserveinputfile is None :
450            # this is not a real file, we read the job's data
451            # from our temporary file which is a copy of stdin
452            infno = self.jobdatastream.fileno()
453            self.jobdatastream.seek(0)
454            pollster.register(infno, select.POLLIN | select.POLLPRI)
455        else :   
456            # job's data is in a file, no need to pass the data
457            # to the real backend
458            self.logdebug("Job's data is in %s" % self.preserveinputfile)
459            infno = None
460            endinput = 1
461       
462        self.logdebug("Capturing SIGTERM events.")
463        signal.signal(signal.SIGTERM, self.sigterm_handler)
464       
465        self.logdebug("Entering streams polling loop...")
466        MEGABYTE = 1024*1024
467        killed = 0
468        status = -1
469        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
470            # First check if original backend is still alive
471            status = subprocess.poll()
472           
473            # Now if we got SIGTERM, we have
474            # to kill -TERM the original backend
475            if self.gotSigTerm and not killed :
476                try :
477                    os.kill(subprocess.pid, signal.SIGTERM)
478                except OSError, msg : # ignore but logs if process was already killed.
479                    self.logdebug("Error while sending signal to pid %s" % subprocess.pid)
480                else :   
481                    self.logger.log_message(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid), "info")
482                    killed = 1
483           
484            # In any case, deal with any remaining I/O
485            try :
486                availablefds = pollster.poll(5000)
487            except select.error, msg :   
488                self.logdebug("Interrupted poll : %s" % msg)
489                availablefds = []
490            if not availablefds :
491                self.logdebug("Nothing to do, sleeping a bit...")
492                time.sleep(0.01) # give some time to the system
493            else :
494                for (fd, mask) in availablefds :
495                    # self.logdebug(self.formatFileEvent(fd, mask))
496                    try :
497                        if mask & select.POLLOUT :
498                            # We can write
499                            if fd == tocfno :
500                                if indata :
501                                    try :
502                                        os.write(fd, indata)   
503                                    except IOError, msg :   
504                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
505                                    else :   
506                                        indata = ""
507                                else :       
508                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
509                                    time.sleep(0.01)
510                                   
511                                if endinput :   
512                                    self.unregisterFileNo(pollster, tocfno)       
513                                    self.logdebug("Closing real backend's stdin.")
514                                    os.close(tocfno)
515                                    inputclosed = 1
516                            elif fd == stderrfno :
517                                if outdata :
518                                    try :
519                                        os.write(fd, outdata)
520                                    except IOError, msg :   
521                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
522                                    else :
523                                        outdata = ""
524                                else :       
525                                    # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
526                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
527                                   
528                                if endoutput :   
529                                    self.unregisterFileNo(pollster, stderrfno)       
530                                    outputclosed = 1
531                            else :   
532                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
533                                time.sleep(0.01)
534                               
535                        if mask & (select.POLLIN | select.POLLPRI) :     
536                            # We have something to read
537                            try :
538                                data = os.read(fd, MEGABYTE)
539                            except (IOError, OSError), msg :   
540                                self.logdebug("Error while reading file %s : %s" % (fd, msg))
541                            else :
542                                if fd == infno :
543                                    if not data :    # If yes, then no more input data
544                                        self.unregisterFileNo(pollster, infno)
545                                        self.logdebug("Input data ends.")
546                                        endinput = 1 # this happens with real files.
547                                    else :   
548                                        indata += data
549                                elif fd == fromcfno :
550                                    if not data :
551                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
552                                        time.sleep(0.01)
553                                    else :
554                                        outdata += data
555                                else :   
556                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
557                                    time.sleep(0.01)
558                                   
559                        if mask & (select.POLLHUP | select.POLLERR) :
560                            # Treat POLLERR as an EOF.
561                            # Some standard I/O stream has no more datas
562                            self.unregisterFileNo(pollster, fd)
563                            if fd == infno :
564                                # Here we are in the case where the input file is stdin.
565                                # which has no more data to be read.
566                                self.logdebug("Input data ends.")
567                                endinput = 1
568                            elif fd == fromcfno :   
569                                # We are no more interested in this file descriptor       
570                                self.logdebug("Closing real backend's stdout+stderr.")
571                                os.close(fromcfno)
572                                endoutput = 1
573                            else :   
574                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
575                                time.sleep(0.01)
576                               
577                        if mask & select.POLLNVAL :       
578                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
579                            self.unregisterFileNo(pollster, fd)
580                    except IOError, msg :           
581                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
582               
583        # We must close the real backend's input stream
584        if killed and not inputclosed :
585            self.logdebug("Forcing close of real backend's stdin.")
586            os.close(tocfno)
587       
588        self.logdebug("Exiting streams polling loop...")
589       
590        self.logdebug("Ignoring SIGTERM again.")
591        signal.signal(signal.SIGTERM, signal.SIG_IGN)
592           
593        # Check exit code of original CUPS backend.   
594        if status == -1 :
595            # we exited the loop before the real backend exited
596            # now we have to wait for it to finish and get its status
597            self.logdebug("Waiting for real backend to exit...")
598            try :
599                status = subprocess.wait()
600            except OSError : # already dead   
601                status = 0
602        if os.WIFEXITED(status) :
603            retcode = os.WEXITSTATUS(status)
604        elif not killed :   
605            self.logger.log_message(_("CUPS backend %s died abnormally.") % realbackend, "error")
606            retcode = -1
607        else :   
608            retcode = self.removeJob()
609        return retcode   
610   
611if __name__ == "__main__" :   
612    # This is a CUPS backend, we should act and die like a CUPS backend
613    if len(sys.argv) == 1 :
614        # we will execute each existing backend in device enumeration mode
615        # and generate their PyKota accounting counterpart
616        (directory, myname) = os.path.split(sys.argv[0])
617        for backend in [os.path.join(directory, b) for b in os.listdir(directory) if os.path.isfile(os.path.join(directory, b)) and (b != myname)] :
618            answer = os.popen(backend, "r")
619            try :
620                devices = [line.strip() for line in answer.readlines()]
621            except :   
622                devices = []
623            status = answer.close()
624            if status is None :
625                for d in devices :
626                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
627                    # so we have to decompose it carefully
628                    fdevice = cStringIO.StringIO("%s" % d)
629                    tokenizer = shlex.shlex(fdevice)
630                    tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
631                    arguments = []
632                    while 1 :
633                        token = tokenizer.get_token()
634                        if token :
635                            arguments.append(token)
636                        else :
637                            break
638                    fdevice.close()
639                    try :
640                        (devicetype, device, name, fullname) = arguments
641                    except ValueError :   
642                        pass    # ignore this 'bizarre' device
643                    else :   
644                        if name.startswith('"') and name.endswith('"') :
645                            name = name[1:-1]
646                        if fullname.startswith('"') and fullname.endswith('"') :
647                            fullname = fullname[1:-1]
648                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
649        retcode = 0
650    elif len(sys.argv) not in (6, 7) :   
651        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
652        retcode = 1
653    else :   
654        try :
655            # Initializes the backend
656            kotabackend = PyKotaBackend()   
657            retcode = kotabackend.mainWork()
658        except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg :
659            import traceback
660            mm = [((f.endswith('\n') and f) or (f + '\n')) for f in traceback.format_exception(*sys.exc_info())]
661            sys.stderr.write("ERROR : cupspykota backend failed (%s)\n%s" % (msg, "ERROR : ".join(mm)))
662            sys.stderr.flush()
663            retcode = 1
664       
665        try :
666            kotabackend.storage.close()
667        except (TypeError, NameError, AttributeError) :   
668            pass
669           
670        kotabackend.closeJobDataStream()   
671       
672    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.