root / tea4cups / trunk / tea4cups @ 688

Revision 688, 58.5 kB (checked in by jerome, 18 years ago)

Now serializes accesses to the same device by different print queues
or print servers, just like PyKota.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Rev
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# Tea4CUPS : Tee for CUPS
5#
6# (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
7# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21#
22# $Id$
23#
24#
25
26"""Tea4CUPS is the Swiss Army's knife of the CUPS Administrator.
27
28
29Licensing terms :
30
31  (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
32  (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
33  This program is free software; you can redistribute it and/or modify
34  it under the terms of the GNU General Public License as published by
35  the Free Software Foundation; either version 2 of the License, or
36  (at your option) any later version.
37 
38  This program is distributed in the hope that it will be useful,
39  but WITHOUT ANY WARRANTY; without even the implied warranty of
40  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
41  GNU General Public License for more details.
42 
43  You should have received a copy of the GNU General Public License
44  along with this program; if not, write to the Free Software
45  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
46
47
48Setup :
49
50  Copy the different files where they belong :
51 
52    $ cp tea4cups /usr/lib/cups/backend/
53    $ chown root.root /usr/lib/cups/backend/tea4cups
54    $ chmod 700 /usr/lib/cups/backend/tea4cups
55    $ cp tea4cups.conf /etc/cupsd/
56 
57  Now edit the configuration file to suit your needs :
58 
59    $ vi /etc/cupsd/tea4cups.conf
60   
61    NB : you can use emacs as well :-)
62   
63  Finally restart CUPS :
64 
65    $ /etc/init.d/cupsys restart
66 
67  You can now create "Tea4CUPS Managed" print queues from
68  CUPS' web interface, or using lpadmin.
69 
70Send bug reports to : alet@librelogiciel.com 
71"""       
72
73import sys
74import os
75import time
76import pwd
77import errno
78import md5
79import cStringIO
80import shlex
81import tempfile
82import ConfigParser
83import signal
84import socket
85import fcntl
86import urllib2
87from struct import pack, unpack
88
89__version__ = "3.12_unofficial"
90
91class TeeError(Exception):
92    """Base exception for Tea4CUPS related stuff."""
93    def __init__(self, message = ""):
94        self.message = message
95        Exception.__init__(self, message)
96    def __repr__(self):
97        return self.message
98    __str__ = __repr__
99
100class ConfigError(TeeError) :
101    """Configuration related exceptions."""
102    pass
103
104class IPPError(TeeError) :
105    """IPP related exceptions."""
106    pass
107   
108IPP_VERSION = "1.1"     # default version number
109
110IPP_PORT = 631
111
112IPP_MAX_NAME = 256
113IPP_MAX_VALUES = 8
114
115IPP_TAG_ZERO = 0x00
116IPP_TAG_OPERATION = 0x01
117IPP_TAG_JOB = 0x02
118IPP_TAG_END = 0x03
119IPP_TAG_PRINTER = 0x04
120IPP_TAG_UNSUPPORTED_GROUP = 0x05
121IPP_TAG_SUBSCRIPTION = 0x06
122IPP_TAG_EVENT_NOTIFICATION = 0x07
123IPP_TAG_UNSUPPORTED_VALUE = 0x10
124IPP_TAG_DEFAULT = 0x11
125IPP_TAG_UNKNOWN = 0x12
126IPP_TAG_NOVALUE = 0x13
127IPP_TAG_NOTSETTABLE = 0x15
128IPP_TAG_DELETEATTR = 0x16
129IPP_TAG_ADMINDEFINE = 0x17
130IPP_TAG_INTEGER = 0x21
131IPP_TAG_BOOLEAN = 0x22
132IPP_TAG_ENUM = 0x23
133IPP_TAG_STRING = 0x30
134IPP_TAG_DATE = 0x31
135IPP_TAG_RESOLUTION = 0x32
136IPP_TAG_RANGE = 0x33
137IPP_TAG_BEGIN_COLLECTION = 0x34
138IPP_TAG_TEXTLANG = 0x35
139IPP_TAG_NAMELANG = 0x36
140IPP_TAG_END_COLLECTION = 0x37
141IPP_TAG_TEXT = 0x41
142IPP_TAG_NAME = 0x42
143IPP_TAG_KEYWORD = 0x44
144IPP_TAG_URI = 0x45
145IPP_TAG_URISCHEME = 0x46
146IPP_TAG_CHARSET = 0x47
147IPP_TAG_LANGUAGE = 0x48
148IPP_TAG_MIMETYPE = 0x49
149IPP_TAG_MEMBERNAME = 0x4a
150IPP_TAG_MASK = 0x7fffffff
151IPP_TAG_COPY = -0x7fffffff-1
152
153IPP_RES_PER_INCH = 3
154IPP_RES_PER_CM = 4
155
156IPP_FINISHINGS_NONE = 3
157IPP_FINISHINGS_STAPLE = 4
158IPP_FINISHINGS_PUNCH = 5
159IPP_FINISHINGS_COVER = 6
160IPP_FINISHINGS_BIND = 7
161IPP_FINISHINGS_SADDLE_STITCH = 8
162IPP_FINISHINGS_EDGE_STITCH = 9
163IPP_FINISHINGS_FOLD = 10
164IPP_FINISHINGS_TRIM = 11
165IPP_FINISHINGS_BALE = 12
166IPP_FINISHINGS_BOOKLET_MAKER = 13
167IPP_FINISHINGS_JOB_OFFSET = 14
168IPP_FINISHINGS_STAPLE_TOP_LEFT = 20
169IPP_FINISHINGS_STAPLE_BOTTOM_LEFT = 21
170IPP_FINISHINGS_STAPLE_TOP_RIGHT = 22
171IPP_FINISHINGS_STAPLE_BOTTOM_RIGHT = 23
172IPP_FINISHINGS_EDGE_STITCH_LEFT = 24
173IPP_FINISHINGS_EDGE_STITCH_TOP = 25
174IPP_FINISHINGS_EDGE_STITCH_RIGHT = 26
175IPP_FINISHINGS_EDGE_STITCH_BOTTOM = 27
176IPP_FINISHINGS_STAPLE_DUAL_LEFT = 28
177IPP_FINISHINGS_STAPLE_DUAL_TOP = 29
178IPP_FINISHINGS_STAPLE_DUAL_RIGHT = 30
179IPP_FINISHINGS_STAPLE_DUAL_BOTTOM = 31
180IPP_FINISHINGS_BIND_LEFT = 50
181IPP_FINISHINGS_BIND_TOP = 51
182IPP_FINISHINGS_BIND_RIGHT = 52
183IPP_FINISHINGS_BIND_BOTTO = 53
184
185IPP_PORTRAIT = 3
186IPP_LANDSCAPE = 4
187IPP_REVERSE_LANDSCAPE = 5
188IPP_REVERSE_PORTRAIT = 6
189
190IPP_QUALITY_DRAFT = 3
191IPP_QUALITY_NORMAL = 4
192IPP_QUALITY_HIGH = 5
193
194IPP_JOB_PENDING = 3
195IPP_JOB_HELD = 4
196IPP_JOB_PROCESSING = 5
197IPP_JOB_STOPPED = 6
198IPP_JOB_CANCELLED = 7
199IPP_JOB_ABORTED = 8
200IPP_JOB_COMPLETE = 9
201
202IPP_PRINTER_IDLE = 3
203IPP_PRINTER_PROCESSING = 4
204IPP_PRINTER_STOPPED = 5
205
206IPP_ERROR = -1
207IPP_IDLE = 0
208IPP_HEADER = 1
209IPP_ATTRIBUTE = 2
210IPP_DATA = 3
211
212IPP_PRINT_JOB = 0x0002
213IPP_PRINT_URI = 0x0003
214IPP_VALIDATE_JOB = 0x0004
215IPP_CREATE_JOB = 0x0005
216IPP_SEND_DOCUMENT = 0x0006
217IPP_SEND_URI = 0x0007
218IPP_CANCEL_JOB = 0x0008
219IPP_GET_JOB_ATTRIBUTES = 0x0009
220IPP_GET_JOBS = 0x000a
221IPP_GET_PRINTER_ATTRIBUTES = 0x000b
222IPP_HOLD_JOB = 0x000c
223IPP_RELEASE_JOB = 0x000d
224IPP_RESTART_JOB = 0x000e
225IPP_PAUSE_PRINTER = 0x0010
226IPP_RESUME_PRINTER = 0x0011
227IPP_PURGE_JOBS = 0x0012
228IPP_SET_PRINTER_ATTRIBUTES = 0x0013
229IPP_SET_JOB_ATTRIBUTES = 0x0014
230IPP_GET_PRINTER_SUPPORTED_VALUES = 0x0015
231IPP_CREATE_PRINTER_SUBSCRIPTION = 0x0016
232IPP_CREATE_JOB_SUBSCRIPTION = 0x0017
233IPP_GET_SUBSCRIPTION_ATTRIBUTES = 0x0018
234IPP_GET_SUBSCRIPTIONS = 0x0019
235IPP_RENEW_SUBSCRIPTION = 0x001a
236IPP_CANCEL_SUBSCRIPTION = 0x001b
237IPP_GET_NOTIFICATIONS = 0x001c
238IPP_SEND_NOTIFICATIONS = 0x001d
239IPP_GET_PRINT_SUPPORT_FILES = 0x0021
240IPP_ENABLE_PRINTER = 0x0022
241IPP_DISABLE_PRINTER = 0x0023
242IPP_PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
243IPP_HOLD_NEW_JOBS = 0x0025
244IPP_RELEASE_HELD_NEW_JOBS = 0x0026
245IPP_DEACTIVATE_PRINTER = 0x0027
246IPP_ACTIVATE_PRINTER = 0x0028
247IPP_RESTART_PRINTER = 0x0029
248IPP_SHUTDOWN_PRINTER = 0x002a
249IPP_STARTUP_PRINTER = 0x002b
250IPP_REPROCESS_JOB = 0x002c
251IPP_CANCEL_CURRENT_JOB = 0x002d
252IPP_SUSPEND_CURRENT_JOB = 0x002e
253IPP_RESUME_JOB = 0x002f
254IPP_PROMOTE_JOB = 0x0030
255IPP_SCHEDULE_JOB_AFTER = 0x0031
256IPP_PRIVATE = 0x4000
257CUPS_GET_DEFAULT = 0x4001
258CUPS_GET_PRINTERS = 0x4002
259CUPS_ADD_PRINTER = 0x4003
260CUPS_DELETE_PRINTER = 0x4004
261CUPS_GET_CLASSES = 0x4005
262CUPS_ADD_CLASS = 0x4006
263CUPS_DELETE_CLASS = 0x4007
264CUPS_ACCEPT_JOBS = 0x4008
265CUPS_REJECT_JOBS = 0x4009
266CUPS_SET_DEFAULT = 0x400a
267CUPS_GET_DEVICES = 0x400b
268CUPS_GET_PPDS = 0x400c
269CUPS_MOVE_JOB = 0x400d
270CUPS_AUTHENTICATE_JOB = 0x400e
271
272IPP_OK = 0x0000
273IPP_OK_SUBST = 0x0001
274IPP_OK_CONFLICT = 0x0002
275IPP_OK_IGNORED_SUBSCRIPTIONS = 0x0003
276IPP_OK_IGNORED_NOTIFICATIONS = 0x0004
277IPP_OK_TOO_MANY_EVENTS = 0x0005
278IPP_OK_BUT_CANCEL_SUBSCRIPTION = 0x0006
279IPP_REDIRECTION_OTHER_SITE = 0x0300
280IPP_BAD_REQUEST = 0x0400
281IPP_FORBIDDEN = 0x0401
282IPP_NOT_AUTHENTICATED = 0x0402
283IPP_NOT_AUTHORIZED = 0x0403
284IPP_NOT_POSSIBLE = 0x0404
285IPP_TIMEOUT = 0x0405
286IPP_NOT_FOUND = 0x0406
287IPP_GONE = 0x0407
288IPP_REQUEST_ENTITY = 0x0408
289IPP_REQUEST_VALUE = 0x0409
290IPP_DOCUMENT_FORMAT = 0x040a
291IPP_ATTRIBUTES = 0x040b
292IPP_URI_SCHEME = 0x040c
293IPP_CHARSET = 0x040d
294IPP_CONFLICT = 0x040e
295IPP_COMPRESSION_NOT_SUPPORTED = 0x040f
296IPP_COMPRESSION_ERROR = 0x0410
297IPP_DOCUMENT_FORMAT_ERROR = 0x0411
298IPP_DOCUMENT_ACCESS_ERROR = 0x0412
299IPP_ATTRIBUTES_NOT_SETTABLE = 0x0413
300IPP_IGNORED_ALL_SUBSCRIPTIONS = 0x0414
301IPP_TOO_MANY_SUBSCRIPTIONS = 0x0415
302IPP_IGNORED_ALL_NOTIFICATIONS = 0x0416
303IPP_PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417
304
305IPP_INTERNAL_ERROR = 0x0500
306IPP_OPERATION_NOT_SUPPORTED = 0x0501
307IPP_SERVICE_UNAVAILABLE = 0x0502
308IPP_VERSION_NOT_SUPPORTED = 0x0503
309IPP_DEVICE_ERROR = 0x0504
310IPP_TEMPORARY_ERROR = 0x0505
311IPP_NOT_ACCEPTING = 0x0506
312IPP_PRINTER_BUSY = 0x0507
313IPP_ERROR_JOB_CANCELLED = 0x0508
314IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
315IPP_PRINTER_IS_DEACTIVATED = 0x50a
316 
317CUPS_PRINTER_LOCAL = 0x0000
318CUPS_PRINTER_CLASS = 0x0001
319CUPS_PRINTER_REMOTE = 0x0002
320CUPS_PRINTER_BW = 0x0004
321CUPS_PRINTER_COLOR = 0x0008
322CUPS_PRINTER_DUPLEX = 0x0010
323CUPS_PRINTER_STAPLE = 0x0020
324CUPS_PRINTER_COPIES = 0x0040
325CUPS_PRINTER_COLLATE = 0x0080
326CUPS_PRINTER_PUNCH = 0x0100
327CUPS_PRINTER_COVER = 0x0200
328CUPS_PRINTER_BIND = 0x0400
329CUPS_PRINTER_SORT = 0x0800
330CUPS_PRINTER_SMALL = 0x1000
331CUPS_PRINTER_MEDIUM = 0x2000
332CUPS_PRINTER_LARGE = 0x4000
333CUPS_PRINTER_VARIABLE = 0x8000
334CUPS_PRINTER_IMPLICIT = 0x1000
335CUPS_PRINTER_DEFAULT = 0x2000
336CUPS_PRINTER_FAX = 0x4000
337CUPS_PRINTER_REJECTING = 0x8000
338CUPS_PRINTER_DELETE = 0x1000
339CUPS_PRINTER_NOT_SHARED = 0x2000
340CUPS_PRINTER_AUTHENTICATED = 0x4000
341CUPS_PRINTER_COMMANDS = 0x8000
342CUPS_PRINTER_OPTIONS = 0xe6ff
343 
344class FakeAttribute :
345    """Fakes an IPPRequest attribute to simplify usage syntax."""
346    def __init__(self, request, name) :
347        """Initializes the fake attribute."""
348        self.request = request
349        self.name = name
350       
351    def __setitem__(self, key, value) :
352        """Appends the value to the real attribute."""
353        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
354        for i in range(len(attributeslist)) :
355            attribute = attributeslist[i]
356            for j in range(len(attribute)) :
357                (attrname, attrvalue) = attribute[j]
358                if attrname == key :
359                    attribute[j][1].append(value)
360                    return
361            attribute.append((key, [value]))       
362           
363    def __getitem__(self, key) :
364        """Returns an attribute's value."""
365        answer = []
366        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
367        for i in range(len(attributeslist)) :
368            attribute = attributeslist[i]
369            for j in range(len(attribute)) :
370                (attrname, attrvalue) = attribute[j]
371                if attrname == key :
372                    answer.extend(attrvalue)
373        if answer :
374            return answer
375        raise KeyError, key           
376   
377class IPPRequest :
378    """A class for IPP requests."""
379    attributes_types = ("operation", "job", "printer", "unsupported", \
380                                     "subscription", "event_notification")
381    def __init__(self, data="", version=IPP_VERSION, 
382                                operation_id=None, \
383                                request_id=None, \
384                                debug=False) :
385        """Initializes an IPP Message object.
386       
387           Parameters :
388           
389             data : the complete IPP Message's content.
390             debug : a boolean value to output debug info on stderr.
391        """
392        self.debug = debug
393        self._data = data
394        self.parsed = False
395       
396        # Initializes message
397        self.setVersion(version)               
398        self.setOperationId(operation_id)
399        self.setRequestId(request_id)
400        self.data = ""
401       
402        for attrtype in self.attributes_types :
403            setattr(self, "_%s_attributes" % attrtype, [[]])
404       
405        # Initialize tags   
406        self.tags = [ None ] * 256 # by default all tags reserved
407       
408        # Delimiter tags
409        self.tags[0x01] = "operation-attributes-tag"
410        self.tags[0x02] = "job-attributes-tag"
411        self.tags[0x03] = "end-of-attributes-tag"
412        self.tags[0x04] = "printer-attributes-tag"
413        self.tags[0x05] = "unsupported-attributes-tag"
414        self.tags[0x06] = "subscription-attributes-tag"
415        self.tags[0x07] = "event_notification-attributes-tag"
416       
417        # out of band values
418        self.tags[0x10] = "unsupported"
419        self.tags[0x11] = "reserved-for-future-default"
420        self.tags[0x12] = "unknown"
421        self.tags[0x13] = "no-value"
422        self.tags[0x15] = "not-settable"
423        self.tags[0x16] = "delete-attribute"
424        self.tags[0x17] = "admin-define"
425 
426        # integer values
427        self.tags[0x20] = "generic-integer"
428        self.tags[0x21] = "integer"
429        self.tags[0x22] = "boolean"
430        self.tags[0x23] = "enum"
431       
432        # octetString
433        self.tags[0x30] = "octetString-with-an-unspecified-format"
434        self.tags[0x31] = "dateTime"
435        self.tags[0x32] = "resolution"
436        self.tags[0x33] = "rangeOfInteger"
437        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
438        self.tags[0x35] = "textWithLanguage"
439        self.tags[0x36] = "nameWithLanguage"
440        self.tags[0x37] = "endCollection"
441       
442        # character strings
443        self.tags[0x40] = "generic-character-string"
444        self.tags[0x41] = "textWithoutLanguage"
445        self.tags[0x42] = "nameWithoutLanguage"
446        self.tags[0x44] = "keyword"
447        self.tags[0x45] = "uri"
448        self.tags[0x46] = "uriScheme"
449        self.tags[0x47] = "charset"
450        self.tags[0x48] = "naturalLanguage"
451        self.tags[0x49] = "mimeMediaType"
452        self.tags[0x4a] = "memberAttrName"
453       
454        # Reverse mapping to generate IPP messages
455        self.tagvalues = {}
456        for i in range(len(self.tags)) :
457            value = self.tags[i]
458            if value is not None :
459                self.tagvalues[value] = i
460                                     
461    def __getattr__(self, name) :                                 
462        """Fakes attribute access."""
463        if name in self.attributes_types :
464            return FakeAttribute(self, name)
465        else :
466            raise AttributeError, name
467           
468    def __str__(self) :       
469        """Returns the parsed IPP message in a readable form."""
470        if not self.parsed :
471            return ""
472        mybuffer = []
473        mybuffer.append("IPP version : %s.%s" % self.version)
474        mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
475        mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
476        for attrtype in self.attributes_types :
477            for attribute in getattr(self, "_%s_attributes" % attrtype) :
478                if attribute :
479                    mybuffer.append("%s attributes :" % attrtype.title())
480                for (name, value) in attribute :
481                    mybuffer.append("  %s : %s" % (name, value))
482        if self.data :           
483            mybuffer.append("IPP datas : %s" % repr(self.data))
484        return "\n".join(mybuffer)
485       
486    def logDebug(self, msg) :   
487        """Prints a debug message."""
488        if self.debug :
489            sys.stderr.write("%s\n" % msg)
490            sys.stderr.flush()
491           
492    def setVersion(self, version) :
493        """Sets the request's operation id."""
494        if version is not None :
495            try :
496                self.version = [int(p) for p in version.split(".")]
497            except AttributeError :
498                if len(version) == 2 : # 2-tuple
499                    self.version = version
500                else :   
501                    try :
502                        self.version = [int(p) for p in str(float(version)).split(".")]
503                    except :
504                        self.version = [int(p) for p in IPP_VERSION.split(".")]
505       
506    def setOperationId(self, opid) :       
507        """Sets the request's operation id."""
508        self.operation_id = opid
509       
510    def setRequestId(self, reqid) :       
511        """Sets the request's request id."""
512        self.request_id = reqid
513       
514    def dump(self) :   
515        """Generates an IPP Message.
516       
517           Returns the message as a string of text.
518        """   
519        mybuffer = []
520        if None not in (self.version, self.operation_id) :
521            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
522            mybuffer.append(pack(">H", self.operation_id))
523            mybuffer.append(pack(">I", self.request_id or 1))
524            for attrtype in self.attributes_types :
525                for attribute in getattr(self, "_%s_attributes" % attrtype) :
526                    if attribute :
527                        mybuffer.append(chr(self.tagvalues["%s-attributes-tag" % attrtype]))
528                    for (attrname, value) in attribute :
529                        nameprinted = 0
530                        for (vtype, val) in value :
531                            mybuffer.append(chr(self.tagvalues[vtype]))
532                            if not nameprinted :
533                                mybuffer.append(pack(">H", len(attrname)))
534                                mybuffer.append(attrname)
535                                nameprinted = 1
536                            else :     
537                                mybuffer.append(pack(">H", 0))
538                            if vtype in ("integer", "enum") :
539                                mybuffer.append(pack(">H", 4))
540                                mybuffer.append(pack(">I", val))
541                            elif vtype == "boolean" :
542                                mybuffer.append(pack(">H", 1))
543                                mybuffer.append(chr(val))
544                            else :   
545                                mybuffer.append(pack(">H", len(val)))
546                                mybuffer.append(val)
547            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
548        mybuffer.append(self.data)   
549        return "".join(mybuffer)
550           
551    def parse(self) :
552        """Parses an IPP Request.
553       
554           NB : Only a subset of RFC2910 is implemented.
555        """
556        self._curname = None
557        self._curattributes = None
558       
559        self.setVersion((ord(self._data[0]), ord(self._data[1])))
560        self.setOperationId(unpack(">H", self._data[2:4])[0])
561        self.setRequestId(unpack(">I", self._data[4:8])[0])
562        self.position = 8
563        endofattributes = self.tagvalues["end-of-attributes-tag"]
564        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
565        nulloffset = lambda : 0
566        try :
567            tag = ord(self._data[self.position])
568            while tag != endofattributes :
569                self.position += 1
570                name = self.tags[tag]
571                if name is not None :
572                    func = getattr(self, name.replace("-", "_"), nulloffset)
573                    self.position += func()
574                    if ord(self._data[self.position]) > maxdelimiter :
575                        self.position -= 1
576                        continue
577                oldtag = tag       
578                tag = ord(self._data[self.position])
579                if tag == oldtag :
580                    self._curattributes.append([])
581        except IndexError :
582            raise IPPError, "Unexpected end of IPP message."
583           
584        self.data = self._data[self.position+1:]           
585        self.parsed = True
586       
587    def parseTag(self) :   
588        """Extracts information from an IPP tag."""
589        pos = self.position
590        tagtype = self.tags[ord(self._data[pos])]
591        pos += 1
592        posend = pos2 = pos + 2
593        namelength = unpack(">H", self._data[pos:pos2])[0]
594        if not namelength :
595            name = self._curname
596        else :   
597            posend += namelength
598            self._curname = name = self._data[pos2:posend]
599        pos2 = posend + 2
600        valuelength = unpack(">H", self._data[posend:pos2])[0]
601        posend = pos2 + valuelength
602        value = self._data[pos2:posend]
603        if tagtype in ("integer", "enum") :
604            value = unpack(">I", value)[0]
605        elif tagtype == "boolean" :   
606            value = ord(value)
607        try :   
608            (oldname, oldval) = self._curattributes[-1][-1]
609            if oldname == name :
610                oldval.append((tagtype, value))
611            else :   
612                raise IndexError
613        except IndexError :   
614            self._curattributes[-1].append((name, [(tagtype, value)]))
615        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
616        return posend - self.position
617       
618    def operation_attributes_tag(self) : 
619        """Indicates that the parser enters into an operation-attributes-tag group."""
620        self._curattributes = self._operation_attributes
621        return self.parseTag()
622       
623    def job_attributes_tag(self) : 
624        """Indicates that the parser enters into a job-attributes-tag group."""
625        self._curattributes = self._job_attributes
626        return self.parseTag()
627       
628    def printer_attributes_tag(self) : 
629        """Indicates that the parser enters into a printer-attributes-tag group."""
630        self._curattributes = self._printer_attributes
631        return self.parseTag()
632       
633    def unsupported_attributes_tag(self) : 
634        """Indicates that the parser enters into an unsupported-attributes-tag group."""
635        self._curattributes = self._unsupported_attributes
636        return self.parseTag()
637       
638    def subscription_attributes_tag(self) : 
639        """Indicates that the parser enters into a subscription-attributes-tag group."""
640        self._curattributes = self._subscription_attributes
641        return self.parseTag()
642       
643    def event_notification_attributes_tag(self) : 
644        """Indicates that the parser enters into an event-notification-attributes-tag group."""
645        self._curattributes = self._event_notification_attributes
646        return self.parseTag()
647       
648           
649class CUPS :
650    """A class for a CUPS instance."""
651    def __init__(self, url=None, username=None, password=None, charset="utf-8", language="en-us", debug=False) :
652        """Initializes the CUPS instance."""
653        if url is not None :
654            self.url = url.replace("ipp://", "http://")
655            if self.url.endswith("/") :
656                self.url = self.url[:-1]
657        else :       
658            self.url = self.getDefaultURL()
659        self.username = username
660        self.password = password
661        self.charset = charset
662        self.language = language
663        self.debug = debug
664        self.lastError = None
665        self.lastErrorMessage = None
666        self.requestId = None
667       
668    def getDefaultURL(self) :   
669        """Builds a default URL."""
670        # TODO : encryption methods.
671        server = os.environ.get("CUPS_SERVER") or "localhost"
672        port = os.environ.get("IPP_PORT") or 631
673        if server.startswith("/") :
674            # it seems it's a unix domain socket.
675            # we can't handle this right now, so we use the default instead.
676            return "http://localhost:%s" % port
677        else :   
678            return "http://%s:%s" % (server, port)
679           
680    def identifierToURI(self, service, ident) :
681        """Transforms an identifier into a particular URI depending on requested service."""
682        return "%s/%s/%s" % (self.url.replace("http://", "ipp://"),
683                             service,
684                             ident)
685       
686    def nextRequestId(self) :       
687        """Increments the current request id and returns the new value."""
688        try :
689            self.requestId += 1
690        except TypeError :   
691            self.requestId = 1
692        return self.requestId
693           
694    def newRequest(self, operationid=None) :
695        """Generates a new empty request."""
696        if operationid is not None :
697            req = IPPRequest(operation_id=operationid, \
698                             request_id=self.nextRequestId(), \
699                             debug=self.debug)
700            req.operation["attributes-charset"] = ("charset", self.charset)
701            req.operation["attributes-natural-language"] = ("naturalLanguage", self.language)
702            return req
703   
704    def doRequest(self, req, url=None) :
705        """Sends a request to the CUPS server.
706           returns a new IPPRequest object, containing the parsed answer.
707        """   
708        connexion = urllib2.Request(url=url or self.url, \
709                             data=req.dump())
710        connexion.add_header("Content-Type", "application/ipp")
711        if self.username :
712            pwmanager = urllib2.HTTPPasswordMgrWithDefaultRealm()
713            pwmanager.add_password(None, \
714                                   "%s%s" % (connexion.get_host(), connexion.get_selector()), \
715                                   self.username, \
716                                   self.password or "")
717            authhandler = urllib2.HTTPBasicAuthHandler(pwmanager)                       
718            opener = urllib2.build_opener(authhandler)
719            urllib2.install_opener(opener)
720        self.lastError = None   
721        self.lastErrorMessage = None
722        try :   
723            response = urllib2.urlopen(connexion)
724        except (urllib2.URLError, urllib2.HTTPError, socket.error), error :   
725            self.lastError = error
726            self.lastErrorMessage = str(error)
727            return None
728        else :   
729            datas = response.read()
730            ippresponse = IPPRequest(datas)
731            ippresponse.parse()
732            return ippresponse
733   
734    def getPPD(self, queuename) :   
735        """Retrieves the PPD for a particular queuename."""
736        req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES)
737        req.operation["printer-uri"] = ("uri", self.identifierToURI("printers", queuename))
738        for attrib in ("printer-uri-supported", "printer-type", "member-uris") :
739            req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib)
740        return self.doRequest(req)  # TODO : get the PPD from the actual print server
741       
742    def getDefault(self) :
743        """Retrieves CUPS' default printer."""
744        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))
745   
746    def getJobAttributes(self, jobid) :   
747        """Retrieves a print job's attributes."""
748        req = self.newRequest(IPP_GET_JOB_ATTRIBUTES)
749        req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid))
750        return self.doRequest(req)
751       
752    def getPrinters(self) :   
753        """Returns the list of print queues names."""
754        req = self.newRequest(CUPS_GET_PRINTERS)
755        req.operation["requested-attributes"] = ("keyword", "printer-name")
756        req.operation["printer-type"] = ("enum", 0)
757        req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS)
758        return [printer[1] for printer in self.doRequest(req).printer["printer-name"]]
759       
760    def getDevices(self) :   
761        """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples."""
762        answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES))
763        return zip([d[1] for d in answer.printer["device-class"]], \
764                   [d[1] for d in answer.printer["device-info"]], \
765                   [d[1] for d in answer.printer["device-make-and-model"]], \
766                   [d[1] for d in answer.printer["device-uri"]])
767                   
768    def getPPDs(self) :   
769        """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples."""
770        answer = self.doRequest(self.newRequest(CUPS_GET_PPDS))
771        return zip([d[1] for d in answer.printer["ppd-natural-language"]], \
772                   [d[1] for d in answer.printer["ppd-make"]], \
773                   [d[1] for d in answer.printer["ppd-make-and-model"]], \
774                   [d[1] for d in answer.printer["ppd-name"]])
775                   
776    def createSubscription(self, uri, events=["all"],
777                                      userdata=None,
778                                      recipient=None,
779                                      pullmethod=None,
780                                      charset=None,
781                                      naturallanguage=None,
782                                      leaseduration=None,
783                                      timeinterval=None,
784                                      jobid=None) :
785        """Creates a job, printer or server subscription.
786         
787           uri : the subscription's uri, e.g. ipp://server
788           events : a list of events to subscribe to, e.g. ["printer-added", "printer-deleted"]
789           recipient : the notifier's uri
790           pullmethod : the pull method to use
791           charset : the charset to use when sending notifications
792           naturallanguage : the language to use when sending notifications
793           leaseduration : the duration of the lease in seconds
794           timeinterval : the interval of time during notifications
795           jobid : the optional job id in case of a job subscription
796        """   
797        if jobid is not None :
798            opid = IPP_CREATE_JOB_SUBSCRIPTION
799            uritype = "job-uri"
800        else :
801            opid = IPP_CREATE_PRINTER_SUBSCRIPTION
802            uritype = "printer-uri"
803        req = self.newRequest(opid)
804        req.operation[uritype] = ("uri", uri)
805        for event in events :
806            req.subscription["notify-events"] = ("keyword", event)
807        if userdata is not None :   
808            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
809        if recipient is not None :   
810            req.subscription["notify-recipient"] = ("uri", recipient)
811        if pullmethod is not None :
812            req.subscription["notify-pull-method"] = ("keyword", pullmethod)
813        if charset is not None :
814            req.subscription["notify-charset"] = ("charset", charset)
815        if naturallanguage is not None :
816            req.subscription["notify-natural-language"] = ("naturalLanguage", naturallanguage)
817        if leaseduration is not None :
818            req.subscription["notify-lease-duration"] = ("integer", leaseduration)
819        if timeinterval is not None :
820            req.subscription["notify-time-interval"] = ("integer", timeinterval)
821        if jobid is not None :
822            req.subscription["notify-job-id"] = ("integer", jobid)
823        return self.doRequest(req)
824           
825    def cancelSubscription(self, uri, subscriptionid, jobid=None) :   
826        """Cancels a subscription.
827       
828           uri : the subscription's uri.
829           subscriptionid : the subscription's id.
830           jobid : the optional job's id.
831        """
832        req = self.newRequest(IPP_CANCEL_SUBSCRIPTION)
833        if jobid is not None :
834            uritype = "job-uri"
835        else :
836            uritype = "printer-uri"
837        req.operation[uritype] = ("uri", uri)
838        req.event_notification["notify-subscription-id"] = ("integer", subscriptionid)
839        return self.doRequest(req)
840       
841
842class FakeConfig :
843    """Fakes a configuration file parser."""
844    def get(self, section, option, raw=0) :
845        """Fakes the retrieval of an option."""
846        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
847
848def isTrue(option) :
849    """Returns 1 if option is set to true, else 0."""
850    if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
851        return 1
852    else :
853        return 0
854
855def getCupsConfigDirectives(directives=[]) :
856    """Retrieves some CUPS directives from its configuration file.
857
858       Returns a mapping with lowercased directives as keys and
859       their setting as values.
860    """
861    dirvalues = {}
862    cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
863    cupsdconf = os.path.join(cupsroot, "cupsd.conf")
864    try :
865        conffile = open(cupsdconf, "r")
866    except IOError :
867        raise TeeError, "Unable to open %s" % cupsdconf
868    else :
869        for line in conffile.readlines() :
870            linecopy = line.strip().lower()
871            for di in [d.lower() for d in directives] :
872                if linecopy.startswith("%s " % di) :
873                    try :
874                        val = line.split()[1]
875                    except :
876                        pass # ignore errors, we take the last value in any case.
877                    else :
878                        dirvalues[di] = val
879        conffile.close()
880    return dirvalues
881   
882class CupsBackend :
883    """Base class for tools with no database access."""
884    def __init__(self) :
885        """Initializes the CUPS backend wrapper."""
886        signal.signal(signal.SIGTERM, signal.SIG_IGN)
887        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
888        self.MyName = "Tea4CUPS"
889        self.myname = "tea4cups"
890        self.pid = os.getpid()
891        self.debug = True
892        self.isCancelled = False
893        self.Title = None
894        self.InputFile = None
895        self.RealBackend = None
896        self.ControlFile = None
897        self.ClientHost = None
898        self.PrinterName = None
899        self.JobBilling = None
900        self.UserName = None
901        self.Copies = None
902        self.Options = None
903        self.DataFile = None
904        self.JobMD5Sum = None
905        self.JobSize = None
906        self.DeviceURI = None
907        self.JobId = None
908        self.Directory = None
909        self.pipes = {}
910        self.config = None
911        self.conffile = None
912        self.LockFile = None
913
914    def waitForLock(self) :   
915        """Waits until we can acquire the lock file."""
916        self.logDebug("Waiting for lock...")
917        lockfilename = self.DeviceURI.replace("/", ".")
918        lockfilename = lockfilename.replace(":", ".")
919        lockfilename = lockfilename.replace("?", ".")
920        lockfilename = lockfilename.replace("&", ".")
921        lockfilename = lockfilename.replace("@", ".")
922        lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, lockfilename))
923        stillrunning = True
924        while 1 :
925            self.LockFile = open(lockfilename, "a+")
926            fcntl.lockf(self.LockFile, fcntl.LOCK_EX) # Will wait until lock available
927            self.LockFile.seek(0, 0)     
928            try : 
929                oldpid = int(self.LockFile.readlines()[-1].strip())
930            except :   
931                stillrunning = False
932            else :   
933                try :
934                    os.kill(oldpid, 0)
935                except OSError :   
936                    stillrunning = False
937            if not stillrunning :       
938                self.LockFile.truncate(0)
939                self.LockFile.seek(0, 0)
940                self.LockFile.write("%s\n" % self.pid)
941                self.LockFile.flush()
942                break
943            else :   
944                time.sleep(0.1)
945        self.logDebug("Lock acquired.")
946       
947    def readConfig(self) :
948        """Reads the configuration file."""
949        confdir = os.environ.get("CUPS_SERVERROOT", ".")
950        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
951        if os.path.isfile(self.conffile) :
952            self.config = ConfigParser.ConfigParser()
953            self.config.read([self.conffile])
954            self.debug = isTrue(self.getGlobalOption("debug", ignore=1))
955        else :
956            self.config = FakeConfig()
957            self.debug = 1      # no config, so force debug mode !
958
959    def logInfo(self, message, level="info") :
960        """Logs a message to CUPS' error_log file."""
961        try :
962            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
963            sys.stderr.flush()
964        except IOError :
965            pass
966
967    def logDebug(self, message) :
968        """Logs something to debug output if debug is enabled."""
969        if self.debug :
970            self.logInfo(message, level="debug")
971
972    def getGlobalOption(self, option, ignore=0) :
973        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
974        try :
975            return self.config.get("global", option, raw=1)
976        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
977            if not ignore :
978                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
979
980    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
981        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
982        globaloption = self.getGlobalOption(option, ignore=1)
983        try :
984            return self.config.get(printqueuename, option, raw=1)
985        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
986            if globaloption is not None :
987                return globaloption
988            elif not ignore :
989                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
990
991    def enumBranches(self, printqueuename, branchtype="tee") :
992        """Returns the list of branchtypes branches for a particular section's."""
993        branchbasename = "%s_" % branchtype.lower()
994        try :
995            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
996        except ConfigParser.NoSectionError, msg :
997            raise ConfigError, "Invalid configuration file : %s" % msg
998        try :
999            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
1000        except ConfigParser.NoSectionError, msg :
1001            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
1002            sectionbranches = []
1003        branches = {}
1004        for (k, v) in globalbranches :
1005            value = v.strip()
1006            if value :
1007                branches[k] = value
1008        for (k, v) in sectionbranches :
1009            value = v.strip()
1010            if value :
1011                branches[k] = value # overwrite any global option or set a new value
1012            else :
1013                del branches[k] # empty value disables a global option
1014        return branches
1015
1016    def discoverOtherBackends(self) :
1017        """Discovers the other CUPS backends.
1018
1019           Executes each existing backend in turn in device enumeration mode.
1020           Returns the list of available backends.
1021        """
1022        # Unfortunately this method can't output any debug information
1023        # to stdout or stderr, else CUPS considers that the device is
1024        # not available.
1025        available = []
1026        (directory, myname) = os.path.split(sys.argv[0])
1027        if not directory :
1028            directory = "./"
1029        tmpdir = tempfile.gettempdir()
1030        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
1031        if os.path.exists(lockfilename) :
1032            lockfile = open(lockfilename, "r")
1033            pid = int(lockfile.read())
1034            lockfile.close()
1035            try :
1036                # see if the pid contained in the lock file is still running
1037                os.kill(pid, 0)
1038            except OSError, error :
1039                if error.errno != errno.EPERM :
1040                    # process doesn't exist anymore
1041                    os.remove(lockfilename)
1042
1043        if not os.path.exists(lockfilename) :
1044            lockfile = open(lockfilename, "w")
1045            lockfile.write("%i" % self.pid)
1046            lockfile.close()
1047            allbackends = [ os.path.join(directory, b) \
1048                                for b in os.listdir(directory)
1049                                    if os.access(os.path.join(directory, b), os.X_OK) \
1050                                        and (b != myname)]
1051            for backend in allbackends :
1052                answer = os.popen(backend, "r")
1053                try :
1054                    devices = [deviceline.strip() for deviceline in answer.readlines()]
1055                except :
1056                    devices = []
1057                status = answer.close()
1058                if status is None :
1059                    for d in devices :
1060                        # each line is of the form :
1061                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
1062                        # so we have to decompose it carefully
1063                        fdevice = cStringIO.StringIO(d)
1064                        tokenizer = shlex.shlex(fdevice)
1065                        tokenizer.wordchars = tokenizer.wordchars + \
1066                                                        r".:,?!~/\_$*-+={}[]()#"
1067                        arguments = []
1068                        while 1 :
1069                            token = tokenizer.get_token()
1070                            if token :
1071                                arguments.append(token)
1072                            else :
1073                                break
1074                        fdevice.close()
1075                        try :
1076                            (devicetype, device, name, fullname) = arguments
1077                        except ValueError :
1078                            pass    # ignore this 'bizarre' device
1079                        else :
1080                            if name.startswith('"') and name.endswith('"') :
1081                                name = name[1:-1]
1082                            if fullname.startswith('"') and fullname.endswith('"') :
1083                                fullname = fullname[1:-1]
1084                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
1085                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
1086            os.remove(lockfilename)
1087        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
1088                             % (self.myname, self.MyName, self.MyName))
1089        return available
1090
1091    def initBackend(self) :
1092        """Initializes the backend's attributes."""
1093        self.JobId = sys.argv[1].strip()
1094        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
1095        self.Title = sys.argv[3].strip()
1096        self.Copies = int(sys.argv[4].strip())
1097        self.Options = sys.argv[5].strip()
1098        if len(sys.argv) == 7 :
1099            self.InputFile = sys.argv[6] # read job's datas from file
1100        else :
1101            self.InputFile = None        # read job's datas from stdin
1102        self.PrinterName = os.environ.get("PRINTER", "")
1103        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
1104        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
1105       
1106        # check that the DEVICE_URI environment variable's value is
1107        # prefixed with self.myname otherwise don't touch it.
1108        # If this is the case, we have to remove the prefix from
1109        # the environment before launching the real backend
1110        muststartwith = "%s:" % self.myname
1111        device_uri = os.environ.get("DEVICE_URI", "")
1112        if device_uri.startswith(muststartwith) :
1113            fulldevice_uri = device_uri[:]
1114            device_uri = fulldevice_uri[len(muststartwith):]
1115            for dummy in range(2) :
1116                if device_uri.startswith("/") :
1117                    device_uri = device_uri[1:]
1118        try :
1119            (backend, dummy) = device_uri.split(":", 1)
1120        except ValueError :
1121            if not device_uri :
1122                self.logDebug("Not attached to an existing print queue.")
1123                backend = ""
1124            else :
1125                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
1126
1127        self.RealBackend = backend
1128        self.DeviceURI = device_uri
1129       
1130        try :
1131            cupsserver = CUPS() # TODO : username and password and/or encryption
1132            answer = cupsserver.getJobAttributes(self.JobId)
1133            self.ControlFile = "NotUsedAnymore"
1134        except :   
1135            (ippfilename, answer) = self.parseIPPRequestFile()
1136            self.ControlFile = ippfilename
1137       
1138        try :
1139            john = answer.job["job-originating-host-name"]
1140        except KeyError :   
1141            try :
1142                john = answer.operation["job-originating-host-name"]
1143            except KeyError :   
1144                john = (None, None)
1145        if type(john) == type([]) :                         
1146            john = john[-1]
1147        (dummy, self.ClientHost) = john                         
1148        try :       
1149            jbing = answer.job["job-billing"]
1150        except KeyError :   
1151            jbing = (None, None)
1152        if type(jbing) == type([]) : 
1153            jbing = jbing[-1]
1154        (dummy, self.JobBilling) = jbing
1155       
1156    def parseIPPRequestFile(self) :
1157        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
1158        requestroot = os.environ.get("CUPS_REQUESTROOT")
1159        if requestroot is None :
1160            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
1161            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
1162        if (len(self.JobId) < 5) and self.JobId.isdigit() :
1163            ippmessagefile = "c%05i" % int(self.JobId)
1164        else :
1165            ippmessagefile = "c%s" % self.JobId
1166        ippmessagefile = os.path.join(requestroot, ippmessagefile)
1167        ippmessage = {}
1168        try :
1169            ippdatafile = open(ippmessagefile)
1170        except :
1171            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
1172        else :
1173            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
1174            try :
1175                ippmessage = IPPRequest(ippdatafile.read())
1176                ippmessage.parse()
1177            except IPPError, msg :
1178                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
1179            else :
1180                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
1181            ippdatafile.close()
1182        return (ippmessagefile, ippmessage)
1183
1184    def exportAttributes(self) :
1185        """Exports our backend's attributes to the environment."""
1186        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
1187        os.environ["TEAPRINTERNAME"] = self.PrinterName
1188        os.environ["TEADIRECTORY"] = self.Directory
1189        os.environ["TEADATAFILE"] = self.DataFile
1190        os.environ["TEAJOBSIZE"] = str(self.JobSize)
1191        os.environ["TEAMD5SUM"] = self.JobMD5Sum
1192        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
1193        os.environ["TEAJOBID"] = self.JobId
1194        os.environ["TEAUSERNAME"] = self.UserName
1195        os.environ["TEATITLE"] = self.Title
1196        os.environ["TEACOPIES"] = str(self.Copies)
1197        os.environ["TEAOPTIONS"] = self.Options
1198        os.environ["TEAINPUTFILE"] = self.InputFile or ""
1199        os.environ["TEABILLING"] = self.JobBilling or ""
1200        os.environ["TEACONTROLFILE"] = self.ControlFile
1201
1202    def saveDatasAndCheckSum(self) :
1203        """Saves the input datas into a static file."""
1204        self.logDebug("Duplicating data stream into %s" % self.DataFile)
1205        mustclose = 0
1206        if self.InputFile is not None :
1207            infile = open(self.InputFile, "rb")
1208            mustclose = 1
1209        else :
1210            infile = sys.stdin
1211           
1212        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
1213                                                 ignore=1)
1214        if filtercommand :                                                 
1215            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
1216            filteroutput = "%s.filteroutput" % self.DataFile
1217            outf = open(filteroutput, "wb")
1218            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
1219            outf.close()
1220            self.logDebug("Filter's output status : %s" % repr(filterstatus))
1221            if mustclose :
1222                infile.close()
1223            infile = open(filteroutput, "rb")
1224            mustclose = 1
1225        else :   
1226            self.logDebug("Data stream will be used as-is (no filter defined)")
1227           
1228        CHUNK = 64*1024         # read 64 Kb at a time
1229        dummy = 0
1230        sizeread = 0
1231        checksum = md5.new()
1232        outfile = open(self.DataFile, "wb")
1233        while 1 :
1234            data = infile.read(CHUNK)
1235            if not data :
1236                break
1237            sizeread += len(data)
1238            outfile.write(data)
1239            checksum.update(data)
1240            if not (dummy % 32) : # Only display every 2 Mb
1241                self.logDebug("%s bytes saved..." % sizeread)
1242            dummy += 1
1243        outfile.close()
1244       
1245        if filtercommand :
1246            self.logDebug("Removing filter's output file %s" % filteroutput)
1247            try :
1248                os.remove(filteroutput)
1249            except :   
1250                pass
1251               
1252        if mustclose :
1253            infile.close()
1254           
1255        self.logDebug("%s bytes saved..." % sizeread)
1256        self.JobSize = sizeread
1257        self.JobMD5Sum = checksum.hexdigest()
1258        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
1259        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
1260
1261    def cleanUp(self) :
1262        """Cleans up the place."""
1263        self.logDebug("Cleaning up...")
1264        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
1265            and os.path.exists(self.DataFile) :
1266            try :
1267                os.remove(self.DataFile)
1268            except OSError, msg :   
1269                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
1270        self.logDebug("Removing lock...")
1271        try :
1272            fcntl.flock(self.LockFile, fcntl.LOCK_UN)
1273            self.LockFile.close()
1274            # NB : we don't remove the lock file, since it might already be opened by another process.
1275        except :   
1276            self.logInfo("Problem while removing lock.", "error")
1277        else :   
1278            self.logDebug("Lock removed.")
1279        self.logDebug("Clean.")
1280
1281    def runBranches(self) :
1282        """Launches each hook defined for the current print queue."""
1283        self.isCancelled = 0    # did a prehook cancel the print job ?
1284        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
1285        self.pipes = { 0: (0, 1) }
1286        branches = self.enumBranches(self.PrinterName, "prehook")
1287        for b in branches.keys() :
1288            self.pipes[b.split("_", 1)[1]] = os.pipe()
1289        retcode = self.runCommands("prehook", branches, serialize)
1290        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
1291            os.close(p[1][1])
1292        if not self.isCancelled :
1293            if self.RealBackend :
1294                retcode = self.launchOriginalBackend()
1295                if retcode :
1296                    onfail = self.getPrintQueueOption(self.PrinterName, \
1297                                                      "onfail", ignore=1)
1298                    if onfail :
1299                        self.logDebug("Launching onfail script %s" % onfail)
1300                        os.system(onfail)
1301                       
1302            os.environ["TEASTATUS"] = str(retcode)
1303            branches = self.enumBranches(self.PrinterName, "posthook")
1304            if self.runCommands("posthook", branches, serialize) :
1305                self.logInfo("An error occured during the execution of posthooks.", "warn")
1306               
1307        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
1308            os.close(p[1][0])
1309        if not retcode :
1310            self.logInfo("OK")
1311        else :
1312            self.logInfo("An error occured, please check CUPS' error_log file.")
1313        return retcode
1314
1315    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
1316        """Launches a command with stdio redirected."""
1317        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
1318        pid = os.fork()
1319        if pid == 0 :
1320            if stdin != 0 :
1321                os.dup2(stdin, 0)
1322                os.close(stdin)
1323            if stdout != 1 :
1324                os.dup2(stdout, 1)
1325                os.close(stdout)
1326            try :
1327                os.execl("/bin/sh", "sh", "-c", cmd)
1328            except OSError, msg :
1329                self.logDebug("execl() failed: %s" % msg)
1330            os._exit(-1)
1331        status = os.waitpid(pid, 0)[1]
1332        if os.WIFEXITED(status) :
1333            return os.WEXITSTATUS(status)
1334        return -1
1335
1336    def runCommand(self, branch, command) :
1337        """Runs a particular branch command."""
1338        # Code contributed by Peter Stuge on June 7th 2005
1339        self.logDebug("Launching %s : %s" % (branch, command))
1340        btype, bname = branch.split("_", 1)
1341        if bname not in self.pipes.keys() :
1342            bname = 0
1343        if btype == "prehook" :
1344            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
1345        else :
1346            return self.stdioRedirSystem(command, self.pipes[bname][0])
1347
1348    def runCommands(self, btype, branches, serialize) :
1349        """Runs the commands for a particular branch type."""
1350        exitcode = 0
1351        btype = btype.lower()
1352        btypetitle = btype.title()
1353        branchlist = branches.keys()
1354        branchlist.sort()
1355        if serialize :
1356            self.logDebug("Begin serialized %ss" % btypetitle)
1357            for branch in branchlist :
1358                retcode = self.runCommand(branch, branches[branch])
1359                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
1360                if retcode :
1361                    if (btype == "prehook") and (retcode == 255) : # -1
1362                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
1363                        self.isCancelled = 1
1364                    else :
1365                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
1366                        exitcode = 1
1367            self.logDebug("End serialized %ss" % btypetitle)
1368        else :
1369            self.logDebug("Begin forked %ss" % btypetitle)
1370            pids = {}
1371            for branch in branchlist :
1372                pid = os.fork()
1373                if pid :
1374                    pids[branch] = pid
1375                else :
1376                    os._exit(self.runCommand(branch, branches[branch]))
1377            for (branch, pid) in pids.items() :
1378                retcode = os.waitpid(pid, 0)[1]
1379                if os.WIFEXITED(retcode) :
1380                    retcode = os.WEXITSTATUS(retcode)
1381                else :
1382                    retcode = -1
1383                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
1384                if retcode :
1385                    if (btype == "prehook") and (retcode == 255) : # -1
1386                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
1387                        self.isCancelled = 1
1388                    else :
1389                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
1390                        exitcode = 1
1391            self.logDebug("End forked %ss" % btypetitle)
1392        return exitcode
1393
1394    def launchOriginalBackend(self) :
1395        """Launches the original backend, optionally retrying if needed."""
1396        number = 1
1397        delay = 0
1398        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
1399        if retry is not None :
1400            try :
1401                (number, delay) = [int(p) for p in retry.strip().split(",")]
1402            except (ValueError, AttributeError, TypeError) :   
1403                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
1404                number = 1
1405                delay = 0
1406               
1407        loopcount = 1 
1408        while 1 :           
1409            retcode = self.runOriginalBackend()
1410            if not retcode :
1411                break
1412            else :
1413                if (not number) or (loopcount < number) :
1414                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
1415                    time.sleep(delay)
1416                    loopcount += 1
1417                else :   
1418                    break
1419        return retcode           
1420       
1421    def runOriginalBackend(self) :
1422        """Launches the original backend."""
1423        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1424        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1425        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1426
1427        pid = os.fork()
1428        if pid == 0 :
1429            if self.InputFile is None :
1430                f = open(self.DataFile, "rb")
1431                os.dup2(f.fileno(), 0)
1432                f.close()
1433            else :   
1434                arguments[6] = self.DataFile # in case a tea4cups filter was applied
1435            try :
1436                os.execve(originalbackend, arguments, os.environ)
1437            except OSError, msg :
1438                self.logDebug("execve() failed: %s" % msg)
1439            os._exit(-1)
1440        killed = 0
1441        status = -1
1442        while status == -1 :
1443            try :
1444                status = os.waitpid(pid, 0)[1]
1445            except OSError, (err, msg) :
1446                if err == 4 :
1447                    killed = 1
1448        if os.WIFEXITED(status) :
1449            status = os.WEXITSTATUS(status)
1450            if status :
1451                self.logInfo("CUPS backend %s returned %d." % (originalbackend, \
1452                                                             status), "error")
1453            return status
1454        elif not killed :
1455            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
1456                                                              "error")
1457            return -1
1458        else :
1459            return 1
1460
1461if __name__ == "__main__" :
1462    # This is a CUPS backend, we should act and die like a CUPS backend
1463    wrapper = CupsBackend()
1464    if len(sys.argv) == 1 :
1465        print "\n".join(wrapper.discoverOtherBackends())
1466        sys.exit(0)
1467    elif len(sys.argv) not in (6, 7) :
1468        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1469                              % sys.argv[0])
1470        sys.exit(1)
1471    else :
1472        returncode = 1
1473        try :
1474            try :
1475                wrapper.readConfig()
1476                wrapper.initBackend()
1477                wrapper.waitForLock()
1478                wrapper.saveDatasAndCheckSum()
1479                wrapper.exportAttributes()
1480                returncode = wrapper.runBranches()
1481            except SystemExit, e :
1482                returncode = e.code
1483            except KeyboardInterrupt :   
1484                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
1485            except :
1486                import traceback
1487                lines = []
1488                for errline in traceback.format_exception(*sys.exc_info()) :
1489                    lines.extend([l for l in errline.split("\n") if l])
1490                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
1491                                                              wrapper.pid, l) \
1492                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
1493                sys.stderr.write(errormessage)
1494                sys.stderr.flush()
1495                returncode = 1
1496        finally :       
1497            wrapper.cleanUp()
1498        sys.exit(returncode)
Note: See TracBrowser for help on using the browser.