root / pykota / trunk / bin / cupspykota @ 1182

Revision 1182, 14.7 kB (checked in by jalet, 20 years ago)

More (untested) work on the CUPS backend.

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