root / pykota / trunk / bin / cupspykota @ 1196

Revision 1196, 17.9 kB (checked in by jalet, 20 years ago)

Code refactoring work.
Explicit redirection to /dev/null has to be set in external policy now, just
like in external mailto.

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