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
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.21  2004/01/12 18:17:36  jalet
27# Denied jobs weren't stored into the history anymore, this is now fixed.
28#
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#
33# Revision 1.19  2004/01/08 14:10:32  jalet
34# Copyright year changed.
35#
36# Revision 1.18  2004/01/07 16:16:32  jalet
37# Better debugging information
38#
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#
46# Revision 1.16  2003/11/26 20:43:29  jalet
47# Inadvertantly introduced a bug, which is fixed.
48#
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#
53# Revision 1.14  2003/11/25 13:25:45  jalet
54# Boolean problem with old Python, replaced with 0
55#
56# Revision 1.13  2003/11/23 19:01:35  jalet
57# Job price added to history
58#
59# Revision 1.12  2003/11/21 14:28:43  jalet
60# More complete job history.
61#
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#
67# Revision 1.10  2003/11/18 17:54:24  jalet
68# SIGTERMs are now transmitted to original backends.
69#
70# Revision 1.9  2003/11/18 14:11:07  jalet
71# Small fix for bizarre urls
72#
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#
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#
82# Revision 1.6  2003/11/14 20:13:11  jalet
83# We exit the loop too soon.
84#
85# Revision 1.5  2003/11/14 18:31:27  jalet
86# Not perfect, but seems to work with the poll() loop.
87#
88# Revision 1.4  2003/11/14 17:04:15  jalet
89# More (untested) work on the CUPS backend.
90#
91# Revision 1.3  2003/11/12 23:27:44  jalet
92# More work on new backend. This commit may be unstable.
93#
94# Revision 1.2  2003/11/12 09:33:34  jalet
95# New CUPS backend supports device enumeration
96#
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
105import popen2
106import cStringIO
107import shlex
108import select
109import signal
110
111from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError
112from pykota.config import PyKotaConfigError
113from pykota.storage import PyKotaStorageError
114from pykota.accounter import PyKotaAccounterError
115from pykota.requester import PyKotaRequesterError
116
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   
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    """
131    def __init__(self, cmd, capturestderr=0, bufsize=-1, arg0=None) :
132        self.arg0 = arg0
133        popen2.Popen3.__init__(self, cmd, capturestderr, bufsize)
134       
135    def _run_child(self, cmd):
136        for i in range(3, 256): # TODO : MAXFD in original popen2 module
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   
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.
154       
155           Returning 0 (success) prevents CUPS from stopping the print queue.
156        """   
157        return 0
158       
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)
172        else :   
173            action = "ALLOW"
174           
175        # pass the job's data to the real backend   
176        if action in ["ALLOW", "WARN"] :
177            if gotSigTerm :
178                retcode = self.removeJob()
179            else :   
180                retcode = self.handleData()       
181        else :       
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."""
207        # Now it becomes tricky...
208        # We must pass the unmodified job to the original backend
209       
210        global gotSigTerm
211       
212        # First ensure that we have a file object as input
213        mustclose = 0   
214        if self.inputfile is not None :   
215            if hasattr(self.inputfile, "read") :
216                infile = self.inputfile
217            else :   
218                infile = open(self.inputfile, "rb")
219            mustclose = 1
220        else :   
221            infile = sys.stdin
222           
223        # Find the real backend pathname   
224        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
225       
226        # And launch it
227        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + sys.argv[1:])])))
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
254        inputclosed = 0
255        self.logdebug("Entering streams polling loop...")
256        while status == -1 :
257            # First check if original backend is still alive
258            status = subprocess.poll()
259           
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
267           
268            # In any case, deal with any remaining I/O
269            availablefds = pollster.poll()
270            for (fd, mask) in availablefds :
271                # self.logdebug("file: %i    mask: %04x" % (fd, mask))
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 :               
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)
317                        self.logdebug("Real backend's stdout ends.")
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.")
323                            os.write(stderrfno, errdata)
324                            errdata = ""
325                        # We are no more interested in this file descriptor       
326                        pollster.unregister(cerrfno)       
327                        os.close(cerrfno)
328                        self.logdebug("Real backend's stderr ends.")
329                       
330            if endinput and not inputclosed :           
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 :               
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
342                self.logdebug("Input data ends.")
343               
344        # Input file was a real file, we have to close it.   
345        if mustclose :
346            infile.close()
347           
348        self.logdebug("Exiting streams polling loop...")
349           
350        # Check exit code of original CUPS backend.   
351        if os.WIFEXITED(status) :
352            retcode = os.WEXITSTATUS(status)
353        else :   
354            self.logger.log_message(_("CUPS backend %s died abnormally.") % realbackend, "error")
355            retcode = -1
356        return retcode   
357   
358if __name__ == "__main__" :   
359    # This is a CUPS backend, we should act and die like a CUPS backend
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   
372    if len(sys.argv) == 1 :
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 :
385                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
386                    # so we have to decompose it carefully
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()
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]
407                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
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
415            kotabackend = PyKotaBackend()   
416            retcode = kotabackend.mainWork()
417        except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, PyKotaRequesterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg :
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.