root / pykota / trunk / bin / cupspykota @ 1240

Revision 1240, 19.0 kB (checked in by uid67467, 20 years ago)

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