root / pykota / trunk / bin / cupspykota @ 1313

Revision 1302, 17.9 kB (checked in by jalet, 21 years ago)

Fuck Fuck Fuck !!!

  • 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-2004 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.25  2004/01/16 17:51:46  jalet
27# Fuck Fuck Fuck !!!
28#
29# Revision 1.24  2004/01/14 15:52:01  jalet
30# Small fix for job cancelling code.
31#
32# Revision 1.23  2004/01/13 10:48:28  jalet
33# Small streams polling loop modification.
34#
35# Revision 1.22  2004/01/12 22:43:40  jalet
36# New formula to compute a job's price
37#
38# Revision 1.21  2004/01/12 18:17:36  jalet
39# Denied jobs weren't stored into the history anymore, this is now fixed.
40#
41# Revision 1.20  2004/01/11 23:22:42  jalet
42# Major code refactoring, it's way cleaner, and now allows automated addition
43# of printers on first print.
44#
45# Revision 1.19  2004/01/08 14:10:32  jalet
46# Copyright year changed.
47#
48# Revision 1.18  2004/01/07 16:16:32  jalet
49# Better debugging information
50#
51# Revision 1.17  2003/12/27 16:49:25  uid67467
52# Should be ok now.
53#
54# Revision 1.17  2003/12/06 08:54:29  jalet
55# Code simplifications.
56# Added many debugging messages.
57#
58# Revision 1.16  2003/11/26 20:43:29  jalet
59# Inadvertantly introduced a bug, which is fixed.
60#
61# Revision 1.15  2003/11/26 19:17:35  jalet
62# Printing on a printer not present in the Quota Storage now results
63# in the job being stopped or cancelled depending on the system.
64#
65# Revision 1.14  2003/11/25 13:25:45  jalet
66# Boolean problem with old Python, replaced with 0
67#
68# Revision 1.13  2003/11/23 19:01:35  jalet
69# Job price added to history
70#
71# Revision 1.12  2003/11/21 14:28:43  jalet
72# More complete job history.
73#
74# Revision 1.11  2003/11/19 23:19:35  jalet
75# Code refactoring work.
76# Explicit redirection to /dev/null has to be set in external policy now, just
77# like in external mailto.
78#
79# Revision 1.10  2003/11/18 17:54:24  jalet
80# SIGTERMs are now transmitted to original backends.
81#
82# Revision 1.9  2003/11/18 14:11:07  jalet
83# Small fix for bizarre urls
84#
85# Revision 1.8  2003/11/15 14:26:44  jalet
86# General improvements to the documentation.
87# Email address changed in sample configuration file, because
88# I receive low quota messages almost every day...
89#
90# Revision 1.7  2003/11/14 22:05:12  jalet
91# New CUPS backend fully functionnal.
92# Old CUPS configuration method is now officially deprecated.
93#
94# Revision 1.6  2003/11/14 20:13:11  jalet
95# We exit the loop too soon.
96#
97# Revision 1.5  2003/11/14 18:31:27  jalet
98# Not perfect, but seems to work with the poll() loop.
99#
100# Revision 1.4  2003/11/14 17:04:15  jalet
101# More (untested) work on the CUPS backend.
102#
103# Revision 1.3  2003/11/12 23:27:44  jalet
104# More work on new backend. This commit may be unstable.
105#
106# Revision 1.2  2003/11/12 09:33:34  jalet
107# New CUPS backend supports device enumeration
108#
109# Revision 1.1  2003/11/08 16:05:31  jalet
110# CUPS backend added for people to experiment.
111#
112#
113#
114
115import sys
116import os
117import popen2
118import cStringIO
119import shlex
120import select
121import signal
122import time
123
124from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError
125from pykota.config import PyKotaConfigError
126from pykota.storage import PyKotaStorageError
127from pykota.accounter import PyKotaAccounterError
128from pykota.requester import PyKotaRequesterError
129   
130class PyKotaPopen3(popen2.Popen3) :
131    """Our own class to execute real backends.
132   
133       Their first argument is different from their path so using
134       native popen2.Popen3 would not be feasible.
135    """
136    def __init__(self, cmd, capturestderr=0, bufsize=-1, arg0=None) :
137        self.arg0 = arg0
138        popen2.Popen3.__init__(self, cmd, capturestderr, bufsize)
139       
140    def _run_child(self, cmd):
141        for i in range(3, 256): # TODO : MAXFD in original popen2 module
142            try:
143                os.close(i)
144            except OSError:
145                pass
146        try:
147            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
148        finally:
149            os._exit(1)
150   
151class PyKotaBackend(PyKotaFilterOrBackend) :       
152    """A class for the pykota backend."""
153    def __init__(self) :
154        """Does normal initialization then installs signal handler."""
155        # Normal init
156        PyKotaFilterOrBackend.__init__(self)
157       
158        # then deal with signals
159        # CUPS backends ignore SIGPIPE and exit(1) on SIGTERM
160        # Fortunately SIGPIPE is already ignored by Python
161        # It's there just in case this changes in the future.
162        # Here we have to handle SIGTERM correctly, and pass
163        # it to the original backend if needed.
164        self.gotSigTerm = 0
165        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
166        signal.signal(signal.SIGTERM, self.sigterm_handler)
167       
168    def sigterm_handler(self, signum, frame) :
169        """Sets a global variable whenever SIGTERM is received."""
170        # SIGTERM will be ignore most of the time, but during
171        # the call to the real backend, we have to pass it through.
172        self.gotSigTerm = 1
173        self.logger.log_message(_("SIGTERM received, job %s cancelled.") % self.jobid, "info")
174       
175    def acceptJob(self) :       
176        """Returns the appropriate exit code to tell CUPS all is OK."""
177        return 0
178           
179    def removeJob(self) :           
180        """Returns the appropriate exit code to let CUPS think all is OK.
181       
182           Returning 0 (success) prevents CUPS from stopping the print queue.
183        """   
184        return 0
185       
186    def doWork(self, policy, printer, user, userpquota) :   
187        """Most of the work is done here."""
188        # Two different values possible for policy here :
189        # ALLOW means : Either printer, user or user print quota doesn't exist,
190        #               but the job should be allowed anyway.
191        # OK means : Both printer, user and user print quota exist, job should
192        #            be allowed if current user is allowed to print on this printer
193        if policy == "OK" :
194            self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
195            action = self.warnUserPQuota(userpquota)
196            self.logdebug("Job accounting begins.")
197            self.accounter.beginJob(userpquota)
198        else :   
199            action = "ALLOW"
200           
201        # pass the job's data to the real backend   
202        if action in ["ALLOW", "WARN"] :
203            if self.gotSigTerm :
204                retcode = self.removeJob()
205            else :   
206                retcode = self.handleData()       
207        else :       
208            retcode = self.removeJob()
209       
210        if policy == "OK" :       
211            # stops accounting.
212            self.accounter.endJob(userpquota)
213            self.logdebug("Job accounting ends.")
214               
215            # retrieve the job size   
216            jobsize = self.accounter.getJobSize()
217            self.logdebug("Job size : %i" % jobsize)
218           
219            # update the quota for the current user on this printer
220            self.logdebug("Updating user %s's quota on printer %s" % (user.Name, printer.Name))
221            jobprice = userpquota.increasePagesUsage(jobsize)
222           
223            # adds the current job to history   
224            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), action, jobsize, jobprice, self.preserveinputfile, self.title, self.copies, self.options)
225            self.logdebug("Job added to history.")
226           
227        return retcode   
228                   
229    def handleData(self) :                   
230        """Pass the job's data to the real backend."""
231        # Now it becomes tricky...
232        # We must pass the unmodified job to the original backend
233        # First ensure that we have a file object as input
234        mustclose = 0   
235        if self.inputfile is not None :   
236            if hasattr(self.inputfile, "read") :
237                infile = self.inputfile
238            else :   
239                infile = open(self.inputfile, "rb")
240            mustclose = 1
241        else :   
242            infile = sys.stdin
243           
244        # Find the real backend pathname   
245        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
246       
247        # And launch it
248        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + sys.argv[1:])])))
249        subprocess = PyKotaPopen3([realbackend] + sys.argv[1:], capturestderr=1, bufsize=0, arg0=os.environ["DEVICE_URI"])
250       
251        # Save file descriptors, we will need them later.
252        infno = infile.fileno()
253        stdoutfno = sys.stdout.fileno()
254        stderrfno = sys.stderr.fileno()
255        fromcfno = subprocess.fromchild.fileno()
256        tocfno = subprocess.tochild.fileno()
257        cerrfno = subprocess.childerr.fileno()
258       
259        # We will have to be careful when dealing with I/O
260        # So we use a poll object to know when to read or write
261        pollster = select.poll()
262        pollster.register(infno, select.POLLIN | select.POLLPRI)
263        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
264        pollster.register(cerrfno, select.POLLIN | select.POLLPRI)
265        pollster.register(stdoutfno, select.POLLOUT)
266        pollster.register(stderrfno, select.POLLOUT)
267        pollster.register(tocfno, select.POLLOUT)
268       
269        # Initialize our buffers
270        indata = ""
271        outdata = ""
272        errdata = ""
273        endinput = endoutput = enderr = 0
274        inputclosed = outputclosed = errclosed = 0
275        killed = 0
276        self.logdebug("Entering streams polling loop...")
277        status = -1
278        while status == -1 :
279            # First check if original backend is still alive
280            status = subprocess.poll()
281           
282            # Now if we got SIGTERM, we have
283            # to kill -TERM the original backend
284            if self.gotSigTerm and not killed :
285                try :
286                    os.kill(subprocess.pid, signal.SIGTERM)
287                    self.logger.log_message(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid), "info")
288                    killed = 1
289                except : # ignore if process was already killed.
290                    pass
291           
292            # In any case, deal with any remaining I/O
293            availablefds = pollster.poll(5000)
294            for (fd, mask) in availablefds :
295                # self.logdebug("file: %i    mask: %04x" % (fd, mask))
296                if mask & select.POLLOUT :
297                    # We can write
298                    if fd == tocfno :
299                        if indata :
300                            os.write(fd, indata)   
301                            indata = ""
302                        if endinput :   
303                            pollster.unregister(tocfno)       
304                            self.logdebug("Closing real backend's stdin.")
305                            os.close(tocfno)
306                            inputclosed = 1
307                    elif fd == stdoutfno :
308                        if outdata :
309                            os.write(fd, outdata)
310                            outdata = ""
311                        if endoutput :   
312                            pollster.unregister(stdoutfno)       
313                            outputclosed = 1
314                    elif fd == stderrfno :
315                        if errdata :
316                            os.write(fd, errdata)
317                            errdata = ""
318                        if enderr :   
319                            pollster.unregister(stderrfno)       
320                            errclosed = 1
321                if (mask & select.POLLIN) or (mask & select.POLLPRI) :     
322                    # We have something to read
323                    data = os.read(fd, 256 * 1024)
324                    if fd == infno :
325                        indata += data
326                        if not data :    # If yes, then no more input data
327                            pollster.unregister(infno)
328                            self.logdebug("Input data ends.")
329                            endinput = 1 # this happens with real files.
330                    elif fd == fromcfno :
331                        outdata += data
332                    elif fd == cerrfno :   
333                        errdata += data
334                if (mask & select.POLLHUP) or (mask & select.POLLERR) :
335                    # I've never seen POLLERR myself, but this probably
336                    # can't hurt to treat an error condition just like
337                    # an EOF.
338                    #
339                    # Some standard I/O stream has no more datas
340                    pollster.unregister(fd)
341                    if fd == infno :
342                        # Here we are in the case where the input file is stdin.
343                        # which has no more data to be read.
344                        self.logdebug("Input data ends.")
345                        endinput = 1
346                    elif fd == fromcfno :   
347                        # This should never happen, since
348                        # CUPS backends don't send anything on their
349                        # standard output.
350                        # We are no more interested in this file descriptor       
351                        self.logdebug("Closing real backend's stdout.")
352                        os.close(fromcfno)
353                        endoutput = 1
354                    elif fd == cerrfno :   
355                        # Original CUPS backend has finished
356                        # to write informations on its standard error.
357                        # We are no more interested in this file descriptor        .
358                        self.logdebug("Closing real backend's stderr.")
359                        os.close(cerrfno)
360                        enderr = 1
361            if killed or (inputclosed and outputclosed and errclosed) :
362                break
363               
364        # We must close the real backend's input stream
365        if killed and not inputclosed :
366            self.logdebug("Forcing close of real backend's stdin.")
367            os.close(tocfno)
368       
369        # Input file was a real file, we have to close it.   
370        if mustclose :
371            infile.close()
372           
373        self.logdebug("Exiting streams polling loop...")
374           
375        # Check exit code of original CUPS backend.   
376        if os.WIFEXITED(status) :
377            retcode = os.WEXITSTATUS(status)
378        elif not killed :   
379            self.logger.log_message(_("CUPS backend %s died abnormally.") % realbackend, "error")
380            retcode = -1
381        else :   
382            retcode = self.removeJob()
383        return retcode   
384   
385if __name__ == "__main__" :   
386    # This is a CUPS backend, we should act and die like a CUPS backend
387    if len(sys.argv) == 1 :
388        # we will execute each existing backend in device enumeration mode
389        # and generate their PyKota accounting counterpart
390        (directory, myname) = os.path.split(sys.argv[0])
391        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)] :
392            answer = os.popen(backend, "r")
393            try :
394                devices = [line.strip() for line in answer.readlines()]
395            except :   
396                devices = []
397            status = answer.close()
398            if status is None :
399                for d in devices :
400                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
401                    # so we have to decompose it carefully
402                    fdevice = cStringIO.StringIO("%s" % d)
403                    tokenizer = shlex.shlex(fdevice)
404                    tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
405                    arguments = []
406                    while 1 :
407                        token = tokenizer.get_token()
408                        if token :
409                            arguments.append(token)
410                        else :
411                            break
412                    fdevice.close()
413                    try :
414                        (devicetype, device, name, fullname) = arguments
415                    except ValueError :   
416                        pass    # ignore this 'bizarre' device
417                    else :   
418                        if name.startswith('"') and name.endswith('"') :
419                            name = name[1:-1]
420                        if fullname.startswith('"') and fullname.endswith('"') :
421                            fullname = fullname[1:-1]
422                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
423        retcode = 0
424    elif len(sys.argv) not in (6, 7) :   
425        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
426        retcode = 1
427    else :   
428        try :
429            # Initializes the backend
430            kotabackend = PyKotaBackend()   
431            retcode = kotabackend.mainWork()
432        except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, PyKotaRequesterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg :
433            sys.stderr.write("ERROR : cupspykota backend failed (%s)\n" % msg)
434            sys.stderr.flush()
435            retcode = 1
436       
437        try :
438            kotabackend.storage.close()
439        except (TypeError, NameError, AttributeError) :   
440            pass
441       
442    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.