root / pykota / trunk / bin / cupspykota @ 1256

Revision 1256, 19.1 kB (checked in by jalet, 20 years ago)

Better debugging information

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