root / pykota / trunk / bin / cupspykota @ 1190

Revision 1190, 19.7 kB (checked in by jalet, 20 years ago)

Small fix for bizarre urls

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