root / pykota / trunk / bin / cupspykota @ 1513

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

Should correctly capture ALL errors now

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