root / pykota / trunk / bin / cupspykota @ 1183

Revision 1183, 15.3 kB (checked in by jalet, 20 years ago)

Not perfect, but seems to work with the poll() loop.

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