root / pykota / trunk / bin / cupspykota @ 1221

Revision 1221, 19.2 kB (checked in by jalet, 20 years ago)

Printing on a printer not present in the Quota Storage now results
in the job being stopped or cancelled depending on the system.

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