root / pykota / trunk / bin / cupspykota @ 1289

Revision 1289, 17.1 kB (checked in by jalet, 20 years ago)

Small streams polling loop modification.

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