root / pykota / trunk / bin / cupspykota @ 1222

Revision 1222, 18.5 kB (checked in by jalet, 20 years ago)

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