root / pykota / trunk / bin / cupspykota @ 1184

Revision 1184, 16.4 kB (checked in by jalet, 20 years ago)

We exit the loop too soon.

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