root / pykota / trunk / bin / cupspykota @ 1202

Revision 1200, 18.0 kB (checked in by jalet, 21 years ago)

More complete job history.

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