root / pykota / trunk / bin / cupspykota @ 1210

Revision 1210, 18.3 kB (checked in by jalet, 20 years ago)

Boolean problem with old Python, replaced with 0

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