root / pykota / trunk / bin / cupspykota @ 1185

Revision 1185, 19.3 kB (checked in by jalet, 20 years ago)

New CUPS backend fully functionnal.
Old CUPS configuration method is now officially deprecated.

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