root / pykota / trunk / bin / cupspykota @ 1280

Revision 1280, 17.2 kB (checked in by jalet, 20 years ago)

Denied jobs weren't stored into the history anymore, this is now fixed.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[1177]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#
[1257]8# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
[1177]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$
[1280]26# Revision 1.21  2004/01/12 18:17:36  jalet
27# Denied jobs weren't stored into the history anymore, this is now fixed.
28#
[1271]29# Revision 1.20  2004/01/11 23:22:42  jalet
30# Major code refactoring, it's way cleaner, and now allows automated addition
31# of printers on first print.
32#
[1257]33# Revision 1.19  2004/01/08 14:10:32  jalet
34# Copyright year changed.
35#
[1256]36# Revision 1.18  2004/01/07 16:16:32  jalet
37# Better debugging information
38#
[1240]39# Revision 1.17  2003/12/27 16:49:25  uid67467
40# Should be ok now.
41#
42# Revision 1.17  2003/12/06 08:54:29  jalet
43# Code simplifications.
44# Added many debugging messages.
45#
[1222]46# Revision 1.16  2003/11/26 20:43:29  jalet
47# Inadvertantly introduced a bug, which is fixed.
48#
[1221]49# Revision 1.15  2003/11/26 19:17:35  jalet
50# Printing on a printer not present in the Quota Storage now results
51# in the job being stopped or cancelled depending on the system.
52#
[1210]53# Revision 1.14  2003/11/25 13:25:45  jalet
54# Boolean problem with old Python, replaced with 0
55#
[1203]56# Revision 1.13  2003/11/23 19:01:35  jalet
57# Job price added to history
58#
[1200]59# Revision 1.12  2003/11/21 14:28:43  jalet
60# More complete job history.
61#
[1196]62# Revision 1.11  2003/11/19 23:19:35  jalet
63# Code refactoring work.
64# Explicit redirection to /dev/null has to be set in external policy now, just
65# like in external mailto.
66#
[1191]67# Revision 1.10  2003/11/18 17:54:24  jalet
68# SIGTERMs are now transmitted to original backends.
69#
[1190]70# Revision 1.9  2003/11/18 14:11:07  jalet
71# Small fix for bizarre urls
72#
[1186]73# Revision 1.8  2003/11/15 14:26:44  jalet
74# General improvements to the documentation.
75# Email address changed in sample configuration file, because
76# I receive low quota messages almost every day...
77#
[1185]78# Revision 1.7  2003/11/14 22:05:12  jalet
79# New CUPS backend fully functionnal.
80# Old CUPS configuration method is now officially deprecated.
81#
[1184]82# Revision 1.6  2003/11/14 20:13:11  jalet
83# We exit the loop too soon.
84#
[1183]85# Revision 1.5  2003/11/14 18:31:27  jalet
86# Not perfect, but seems to work with the poll() loop.
87#
[1182]88# Revision 1.4  2003/11/14 17:04:15  jalet
89# More (untested) work on the CUPS backend.
90#
[1180]91# Revision 1.3  2003/11/12 23:27:44  jalet
92# More work on new backend. This commit may be unstable.
93#
[1178]94# Revision 1.2  2003/11/12 09:33:34  jalet
95# New CUPS backend supports device enumeration
96#
[1177]97# Revision 1.1  2003/11/08 16:05:31  jalet
98# CUPS backend added for people to experiment.
99#
100#
101#
102
103import sys
104import os
[1182]105import popen2
[1178]106import cStringIO
107import shlex
[1182]108import select
109import signal
[1177]110
[1196]111from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError
[1177]112from pykota.config import PyKotaConfigError
113from pykota.storage import PyKotaStorageError
[1196]114from pykota.accounter import PyKotaAccounterError
115from pykota.requester import PyKotaRequesterError
[1177]116
[1271]117gotSigTerm = 0
118def sigterm_handler(signum, frame) :
119    """Sets a global variable whenever SIGTERM is received."""
120    # SIGTERM will be ignore most of the time, but during
121    # the call to the real backend, we have to pass it through.
122    global gotSigTerm
123    gotSigTerm = 1
124   
[1182]125class PyKotaPopen3(popen2.Popen3) :
126    """Our own class to execute real backends.
127   
128       Their first argument is different from their path so using
129       native popen2.Popen3 would not be feasible.
130    """
[1210]131    def __init__(self, cmd, capturestderr=0, bufsize=-1, arg0=None) :
[1182]132        self.arg0 = arg0
133        popen2.Popen3.__init__(self, cmd, capturestderr, bufsize)
134       
135    def _run_child(self, cmd):
[1183]136        for i in range(3, 256): # TODO : MAXFD in original popen2 module
[1182]137            try:
138                os.close(i)
139            except OSError:
140                pass
141        try:
142            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
143        finally:
144            os._exit(1)
145   
[1271]146class PyKotaBackend(PyKotaFilterOrBackend) :       
147    """A class for the pykota backend."""
148    def acceptJob(self) :       
149        """Returns the appropriate exit code to tell CUPS all is OK."""
150        return 0
151           
152    def removeJob(self) :           
153        """Returns the appropriate exit code to let CUPS think all is OK.
[1177]154       
[1271]155           Returning 0 (success) prevents CUPS from stopping the print queue.
156        """   
157        return 0
[1222]158       
[1271]159    def doWork(self, policy, printer, user, userpquota) :   
160        """Most of the work is done here."""
161        global gotSigTerm
162        # Two different values possible for policy here :
163        # ALLOW means : Either printer, user or user print quota doesn't exist,
164        #               but the job should be allowed anyway.
165        # OK means : Both printer, user and user print quota exist, job should
166        #            be allowed if current user is allowed to print on this printer
167        if policy == "OK" :
168            self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
169            action = self.warnUserPQuota(userpquota)
170            self.logdebug("Job accounting begins.")
171            self.accounter.beginJob(userpquota)
[1280]172        else :   
173            action = "ALLOW"
[1271]174           
175        # pass the job's data to the real backend   
[1280]176        if action in ["ALLOW", "WARN"] :
177            if gotSigTerm :
178                retcode = self.removeJob()
179            else :   
180                retcode = self.handleData()       
181        else :       
[1271]182            retcode = self.removeJob()
183       
184        if policy == "OK" :       
185            # stops accounting.
186            self.accounter.endJob(userpquota)
187            self.logdebug("Job accounting ends.")
188               
189            # retrieve the job size   
190            jobsize = self.accounter.getJobSize()
191            self.logdebug("Job size : %i" % jobsize)
192           
193            # update the quota for the current user on this printer
194            jobprice = (float(printer.PricePerPage or 0.0) * jobsize) + float(printer.PricePerJob or 0.0)
195            if jobsize :
196                self.logdebug("Updating user %s's quota on printer %s" % (user.Name, printer.Name))
197                userpquota.increasePagesUsage(jobsize)
198           
199            # adds the current job to history   
200            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), action, jobsize, jobprice, self.preserveinputfile, self.title, self.copies, self.options)
201            self.logdebug("Job added to history.")
202           
203        return retcode   
204                   
205    def handleData(self) :                   
206        """Pass the job's data to the real backend."""
[1222]207        # Now it becomes tricky...
[1271]208        # We must pass the unmodified job to the original backend
[1222]209       
[1271]210        global gotSigTerm
211       
[1222]212        # First ensure that we have a file object as input
213        mustclose = 0   
[1271]214        if self.inputfile is not None :   
215            if hasattr(self.inputfile, "read") :
216                infile = self.inputfile
[1182]217            else :   
[1271]218                infile = open(self.inputfile, "rb")
[1222]219            mustclose = 1
220        else :   
221            infile = sys.stdin
[1185]222           
[1222]223        # Find the real backend pathname   
[1271]224        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
[1222]225       
226        # And launch it
[1271]227        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + sys.argv[1:])])))
[1222]228        subprocess = PyKotaPopen3([realbackend] + sys.argv[1:], capturestderr=1, bufsize=0, arg0=os.environ["DEVICE_URI"])
229       
230        # Save file descriptors, we will need them later.
231        infno = infile.fileno()
232        stdoutfno = sys.stdout.fileno()
233        stderrfno = sys.stderr.fileno()
234        fromcfno = subprocess.fromchild.fileno()
235        tocfno = subprocess.tochild.fileno()
236        cerrfno = subprocess.childerr.fileno()
237       
238        # We will have to be careful when dealing with I/O
239        # So we use a poll object to know when to read or write
240        pollster = select.poll()
241        pollster.register(infno, select.POLLIN | select.POLLPRI)
242        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
243        pollster.register(cerrfno, select.POLLIN | select.POLLPRI)
244        pollster.register(stdoutfno, select.POLLOUT)
245        pollster.register(stderrfno, select.POLLOUT)
246        pollster.register(tocfno, select.POLLOUT)
247       
248        # Initialize our buffers
249        indata = ""
250        outdata = ""
251        errdata = ""
252        endinput = 0
253        status = -1
[1240]254        inputclosed = 0
[1271]255        self.logdebug("Entering streams polling loop...")
[1222]256        while status == -1 :
257            # First check if original backend is still alive
258            status = subprocess.poll()
[1185]259           
[1222]260            # Now if we got SIGTERM, we have
261            # to kill -TERM the original backend
262            if gotSigTerm :
263                try :
264                    os.kill(subprocess.pid, signal.SIGTERM)
265                except : # ignore if process was already killed.
266                    pass
[1191]267           
[1222]268            # In any case, deal with any remaining I/O
269            availablefds = pollster.poll()
270            for (fd, mask) in availablefds :
[1271]271                # self.logdebug("file: %i    mask: %04x" % (fd, mask))
[1222]272                if mask & select.POLLOUT :
273                    # We can write
274                    if fd == tocfno :
275                        if indata :
276                            os.write(fd, indata)   
277                            indata = ""
278                    elif fd == stdoutfno :
279                        if outdata :
280                            os.write(fd, outdata)
281                            outdata = ""
282                    elif fd == stderrfno :
283                        if errdata :
284                            os.write(fd, errdata)
285                            errdata = ""
286                if (mask & select.POLLIN) or (mask & select.POLLPRI) :     
287                    # We have something to read
288                    data = os.read(fd, 256 * 1024)
289                    if fd == infno :
290                        indata += data
291                        if not data :    # If yes, then no more input data
292                            endinput = 1 # this happens with real files.
293                    elif fd == fromcfno :
294                        outdata += data
295                    elif fd == cerrfno :   
296                        errdata += data
297                if (mask & select.POLLHUP) or (mask & select.POLLERR) :
298                    # I've never seen POLLERR myself, but this probably
299                    # can't hurt to treat an error condition just like
300                    # an EOF.
301                    #
302                    # Some standard I/O stream has no more datas
303                    if fd == infno :
304                        # Here we are in the case where the input file is stdin.
305                        # which has no more data to be read.
306                        endinput = 1
307                    elif fd == fromcfno :   
308                        # This should never happen, since
309                        # CUPS backends don't send anything on their
310                        # standard output
311                        if outdata :               
[1240]312                            os.write(stdoutfno, outdata)
313                            outdata = ""
314                        # We are no more interested in this file descriptor       
315                        pollster.unregister(fromcfno)       
316                        os.close(fromcfno)
[1271]317                        self.logdebug("Real backend's stdout ends.")
[1222]318                    elif fd == cerrfno :   
319                        # Original CUPS backend has finished
320                        # to write informations on its standard error
321                        if errdata :               
322                            # Try to write remaining info (normally "Ready to print.")
[1240]323                            os.write(stderrfno, errdata)
324                            errdata = ""
[1222]325                        # We are no more interested in this file descriptor       
[1240]326                        pollster.unregister(cerrfno)       
327                        os.close(cerrfno)
[1271]328                        self.logdebug("Real backend's stderr ends.")
[1222]329                       
[1240]330            if endinput and not inputclosed :           
[1222]331                # We deal with remaining input datas here
332                # because EOF can happen in two different
333                # situations and I don't want to duplicate
334                # code, nor making functions.
335                if indata :               
[1240]336                    os.write(tocfno, indata)
337                    indata = ""
338                # Again, we're not interested in this file descriptor anymore.
339                pollster.unregister(tocfno)       
340                os.close(tocfno)
341                inputclosed = 1
[1271]342                self.logdebug("Input data ends.")
[1191]343               
[1222]344        # Input file was a real file, we have to close it.   
345        if mustclose :
346            infile.close()
[1185]347           
[1271]348        self.logdebug("Exiting streams polling loop...")
[1240]349           
[1222]350        # Check exit code of original CUPS backend.   
351        if os.WIFEXITED(status) :
352            retcode = os.WEXITSTATUS(status)
353        else :   
[1271]354            self.logger.log_message(_("CUPS backend %s died abnormally.") % realbackend, "error")
[1222]355            retcode = -1
[1271]356        return retcode   
[1222]357   
[1177]358if __name__ == "__main__" :   
359    # This is a CUPS backend, we should act and die like a CUPS backend
[1271]360   
361    # first deal with signals
362    # CUPS backends ignore SIGPIPE and exit(1) on SIGTERM
363    # Fortunately SIGPIPE is already ignored by Python
364    # It's there just in case this changes in the future.
365    # Here we have to handle SIGTERM correctly, and pass
366    # it to the original backend if needed.
367    global gotSigTerm
368    gotSigTerm = 0
369    signal.signal(signal.SIGPIPE, signal.SIG_IGN)
370    signal.signal(signal.SIGTERM, sigterm_handler)
371   
[1177]372    if len(sys.argv) == 1 :
[1178]373        # we will execute each existing backend in device enumeration mode
374        # and generate their PyKota accounting counterpart
375        (directory, myname) = os.path.split(sys.argv[0])
376        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)] :
377            answer = os.popen(backend, "r")
378            try :
379                devices = [line.strip() for line in answer.readlines()]
380            except :   
381                devices = []
382            status = answer.close()
383            if status is None :
384                for d in devices :
[1180]385                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
386                    # so we have to decompose it carefully
[1178]387                    fdevice = cStringIO.StringIO("%s" % d)
388                    tokenizer = shlex.shlex(fdevice)
389                    tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
390                    arguments = []
391                    while 1 :
392                        token = tokenizer.get_token()
393                        if token :
394                            arguments.append(token)
395                        else :
396                            break
397                    fdevice.close()
[1180]398                    try :
399                        (devicetype, device, name, fullname) = arguments
400                    except ValueError :   
401                        pass    # ignore this 'bizarre' device
402                    else :   
403                        if name.startswith('"') and name.endswith('"') :
404                            name = name[1:-1]
405                        if fullname.startswith('"') and fullname.endswith('"') :
406                            fullname = fullname[1:-1]
[1191]407                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
[1177]408        retcode = 0
409    elif len(sys.argv) not in (6, 7) :   
410        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
411        retcode = 1
412    else :   
413        try :
414            # Initializes the backend
[1271]415            kotabackend = PyKotaBackend()   
416            retcode = kotabackend.mainWork()
[1196]417        except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, PyKotaRequesterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg :
[1177]418            sys.stderr.write("ERROR : cupspykota backend failed (%s)\n" % msg)
419            sys.stderr.flush()
420            retcode = 1
421       
422        try :
423            kotabackend.storage.close()
424        except (TypeError, NameError, AttributeError) :   
425            pass
426       
427    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.