root / pykota / trunk / bin / cupspykota @ 1285

Revision 1285, 17.1 kB (checked in by jalet, 20 years ago)

New formula to compute a job's price

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