root / tea4cups / trunk / tea4cups @ 690

Revision 690, 58.8 kB (checked in by jerome, 17 years ago)

Backported PyKota's locking mechanism.

  • 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.13alpha_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        lockfilename = self.DeviceURI.replace("/", ".")
917        lockfilename = lockfilename.replace(":", ".")
918        lockfilename = lockfilename.replace("?", ".")
919        lockfilename = lockfilename.replace("&", ".")
920        lockfilename = lockfilename.replace("@", ".")
921        lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, lockfilename))
922        self.logDebug("Waiting for lock %s to become available..." % lockfilename)
923        haslock = False
924        while not haslock :
925            try :
926                # open the lock file, optionally creating it if needed.
927                self.LockFile = open(lockfilename, "a+")
928               
929                # we wait indefinitely for the lock to become available.
930                # works over NFS too.
931                fcntl.lockf(self.LockFile, fcntl.LOCK_EX)
932                haslock = True
933               
934                self.logDebug("Lock %s acquired." % lockfilename)
935               
936                # Here we save the PID in the lock file, but we don't use
937                # it, because the lock file may be in a directory shared
938                # over NFS between two (or more) print servers, so the PID
939                # has no meaning in this case.
940                self.LockFile.truncate(0)
941                self.LockFile.seek(0, 0)
942                self.LockFile.write(str(self.pid))
943                self.LockFile.flush()
944            except IOError :           
945                self.logDebug("I/O Error while waiting for lock %s" % lockfilename)
946                time.sleep(0.25)
947       
948    def readConfig(self) :
949        """Reads the configuration file."""
950        confdir = os.environ.get("CUPS_SERVERROOT", ".")
951        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
952        if os.path.isfile(self.conffile) :
953            self.config = ConfigParser.ConfigParser()
954            self.config.read([self.conffile])
955            self.debug = isTrue(self.getGlobalOption("debug", ignore=1))
956        else :
957            self.config = FakeConfig()
958            self.debug = 1      # no config, so force debug mode !
959
960    def logInfo(self, message, level="info") :
961        """Logs a message to CUPS' error_log file."""
962        try :
963            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
964            sys.stderr.flush()
965        except IOError :
966            pass
967
968    def logDebug(self, message) :
969        """Logs something to debug output if debug is enabled."""
970        if self.debug :
971            self.logInfo(message, level="debug")
972
973    def getGlobalOption(self, option, ignore=0) :
974        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
975        try :
976            return self.config.get("global", option, raw=1)
977        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
978            if not ignore :
979                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
980
981    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
982        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
983        globaloption = self.getGlobalOption(option, ignore=1)
984        try :
985            return self.config.get(printqueuename, option, raw=1)
986        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
987            if globaloption is not None :
988                return globaloption
989            elif not ignore :
990                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
991
992    def enumBranches(self, printqueuename, branchtype="tee") :
993        """Returns the list of branchtypes branches for a particular section's."""
994        branchbasename = "%s_" % branchtype.lower()
995        try :
996            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
997        except ConfigParser.NoSectionError, msg :
998            raise ConfigError, "Invalid configuration file : %s" % msg
999        try :
1000            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
1001        except ConfigParser.NoSectionError, msg :
1002            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
1003            sectionbranches = []
1004        branches = {}
1005        for (k, v) in globalbranches :
1006            value = v.strip()
1007            if value :
1008                branches[k] = value
1009        for (k, v) in sectionbranches :
1010            value = v.strip()
1011            if value :
1012                branches[k] = value # overwrite any global option or set a new value
1013            else :
1014                del branches[k] # empty value disables a global option
1015        return branches
1016
1017    def discoverOtherBackends(self) :
1018        """Discovers the other CUPS backends.
1019
1020           Executes each existing backend in turn in device enumeration mode.
1021           Returns the list of available backends.
1022        """
1023        # Unfortunately this method can't output any debug information
1024        # to stdout or stderr, else CUPS considers that the device is
1025        # not available.
1026        available = []
1027        (directory, myname) = os.path.split(sys.argv[0])
1028        if not directory :
1029            directory = "./"
1030        tmpdir = tempfile.gettempdir()
1031        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
1032        if os.path.exists(lockfilename) :
1033            lockfile = open(lockfilename, "r")
1034            pid = int(lockfile.read())
1035            lockfile.close()
1036            try :
1037                # see if the pid contained in the lock file is still running
1038                os.kill(pid, 0)
1039            except OSError, error :
1040                if error.errno != errno.EPERM :
1041                    # process doesn't exist anymore
1042                    os.remove(lockfilename)
1043
1044        if not os.path.exists(lockfilename) :
1045            lockfile = open(lockfilename, "w")
1046            lockfile.write("%i" % self.pid)
1047            lockfile.close()
1048            allbackends = [ os.path.join(directory, b) \
1049                                for b in os.listdir(directory)
1050                                    if os.access(os.path.join(directory, b), os.X_OK) \
1051                                        and (b != myname)]
1052            for backend in allbackends :
1053                answer = os.popen(backend, "r")
1054                try :
1055                    devices = [deviceline.strip() for deviceline in answer.readlines()]
1056                except :
1057                    devices = []
1058                status = answer.close()
1059                if status is None :
1060                    for d in devices :
1061                        # each line is of the form :
1062                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
1063                        # so we have to decompose it carefully
1064                        fdevice = cStringIO.StringIO(d)
1065                        tokenizer = shlex.shlex(fdevice)
1066                        tokenizer.wordchars = tokenizer.wordchars + \
1067                                                        r".:,?!~/\_$*-+={}[]()#"
1068                        arguments = []
1069                        while 1 :
1070                            token = tokenizer.get_token()
1071                            if token :
1072                                arguments.append(token)
1073                            else :
1074                                break
1075                        fdevice.close()
1076                        try :
1077                            (devicetype, device, name, fullname) = arguments
1078                        except ValueError :
1079                            pass    # ignore this 'bizarre' device
1080                        else :
1081                            if name.startswith('"') and name.endswith('"') :
1082                                name = name[1:-1]
1083                            if fullname.startswith('"') and fullname.endswith('"') :
1084                                fullname = fullname[1:-1]
1085                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
1086                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
1087            os.remove(lockfilename)
1088        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
1089                             % (self.myname, self.MyName, self.MyName))
1090        return available
1091
1092    def initBackend(self) :
1093        """Initializes the backend's attributes."""
1094        self.JobId = sys.argv[1].strip()
1095        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
1096        self.Title = sys.argv[3].strip()
1097        self.Copies = int(sys.argv[4].strip())
1098        self.Options = sys.argv[5].strip()
1099        if len(sys.argv) == 7 :
1100            self.InputFile = sys.argv[6] # read job's datas from file
1101        else :
1102            self.InputFile = None        # read job's datas from stdin
1103        self.PrinterName = os.environ.get("PRINTER", "")
1104        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
1105        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
1106       
1107        # check that the DEVICE_URI environment variable's value is
1108        # prefixed with self.myname otherwise don't touch it.
1109        # If this is the case, we have to remove the prefix from
1110        # the environment before launching the real backend
1111        muststartwith = "%s:" % self.myname
1112        device_uri = os.environ.get("DEVICE_URI", "")
1113        if device_uri.startswith(muststartwith) :
1114            fulldevice_uri = device_uri[:]
1115            device_uri = fulldevice_uri[len(muststartwith):]
1116            for dummy in range(2) :
1117                if device_uri.startswith("/") :
1118                    device_uri = device_uri[1:]
1119        try :
1120            (backend, dummy) = device_uri.split(":", 1)
1121        except ValueError :
1122            if not device_uri :
1123                self.logDebug("Not attached to an existing print queue.")
1124                backend = ""
1125            else :
1126                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
1127
1128        self.RealBackend = backend
1129        self.DeviceURI = device_uri
1130       
1131        try :
1132            cupsserver = CUPS() # TODO : username and password and/or encryption
1133            answer = cupsserver.getJobAttributes(self.JobId)
1134            self.ControlFile = "NotUsedAnymore"
1135        except :   
1136            (ippfilename, answer) = self.parseIPPRequestFile()
1137            self.ControlFile = ippfilename
1138       
1139        try :
1140            john = answer.job["job-originating-host-name"]
1141        except KeyError :   
1142            try :
1143                john = answer.operation["job-originating-host-name"]
1144            except KeyError :   
1145                john = (None, None)
1146        if type(john) == type([]) :                         
1147            john = john[-1]
1148        (dummy, self.ClientHost) = john                         
1149        try :       
1150            jbing = answer.job["job-billing"]
1151        except KeyError :   
1152            jbing = (None, None)
1153        if type(jbing) == type([]) : 
1154            jbing = jbing[-1]
1155        (dummy, self.JobBilling) = jbing
1156       
1157    def parseIPPRequestFile(self) :
1158        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
1159        requestroot = os.environ.get("CUPS_REQUESTROOT")
1160        if requestroot is None :
1161            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
1162            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
1163        if (len(self.JobId) < 5) and self.JobId.isdigit() :
1164            ippmessagefile = "c%05i" % int(self.JobId)
1165        else :
1166            ippmessagefile = "c%s" % self.JobId
1167        ippmessagefile = os.path.join(requestroot, ippmessagefile)
1168        ippmessage = {}
1169        try :
1170            ippdatafile = open(ippmessagefile)
1171        except :
1172            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
1173        else :
1174            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
1175            try :
1176                ippmessage = IPPRequest(ippdatafile.read())
1177                ippmessage.parse()
1178            except IPPError, msg :
1179                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
1180            else :
1181                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
1182            ippdatafile.close()
1183        return (ippmessagefile, ippmessage)
1184
1185    def exportAttributes(self) :
1186        """Exports our backend's attributes to the environment."""
1187        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
1188        os.environ["TEAPRINTERNAME"] = self.PrinterName
1189        os.environ["TEADIRECTORY"] = self.Directory
1190        os.environ["TEADATAFILE"] = self.DataFile
1191        os.environ["TEAJOBSIZE"] = str(self.JobSize)
1192        os.environ["TEAMD5SUM"] = self.JobMD5Sum
1193        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
1194        os.environ["TEAJOBID"] = self.JobId
1195        os.environ["TEAUSERNAME"] = self.UserName
1196        os.environ["TEATITLE"] = self.Title
1197        os.environ["TEACOPIES"] = str(self.Copies)
1198        os.environ["TEAOPTIONS"] = self.Options
1199        os.environ["TEAINPUTFILE"] = self.InputFile or ""
1200        os.environ["TEABILLING"] = self.JobBilling or ""
1201        os.environ["TEACONTROLFILE"] = self.ControlFile
1202
1203    def saveDatasAndCheckSum(self) :
1204        """Saves the input datas into a static file."""
1205        self.logDebug("Duplicating data stream into %s" % self.DataFile)
1206        mustclose = 0
1207        if self.InputFile is not None :
1208            infile = open(self.InputFile, "rb")
1209            mustclose = 1
1210        else :
1211            infile = sys.stdin
1212           
1213        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
1214                                                 ignore=1)
1215        if filtercommand :                                                 
1216            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
1217            filteroutput = "%s.filteroutput" % self.DataFile
1218            outf = open(filteroutput, "wb")
1219            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
1220            outf.close()
1221            self.logDebug("Filter's output status : %s" % repr(filterstatus))
1222            if mustclose :
1223                infile.close()
1224            infile = open(filteroutput, "rb")
1225            mustclose = 1
1226        else :   
1227            self.logDebug("Data stream will be used as-is (no filter defined)")
1228           
1229        CHUNK = 64*1024         # read 64 Kb at a time
1230        dummy = 0
1231        sizeread = 0
1232        checksum = md5.new()
1233        outfile = open(self.DataFile, "wb")
1234        while 1 :
1235            data = infile.read(CHUNK)
1236            if not data :
1237                break
1238            sizeread += len(data)
1239            outfile.write(data)
1240            checksum.update(data)
1241            if not (dummy % 32) : # Only display every 2 Mb
1242                self.logDebug("%s bytes saved..." % sizeread)
1243            dummy += 1
1244        outfile.close()
1245       
1246        if filtercommand :
1247            self.logDebug("Removing filter's output file %s" % filteroutput)
1248            try :
1249                os.remove(filteroutput)
1250            except :   
1251                pass
1252               
1253        if mustclose :
1254            infile.close()
1255           
1256        self.logDebug("%s bytes saved..." % sizeread)
1257        self.JobSize = sizeread
1258        self.JobMD5Sum = checksum.hexdigest()
1259        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
1260        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
1261
1262    def cleanUp(self) :
1263        """Cleans up the place."""
1264        self.logDebug("Cleaning up...")
1265        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
1266            and os.path.exists(self.DataFile) :
1267            try :
1268                os.remove(self.DataFile)
1269            except OSError, msg :   
1270                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
1271               
1272        if self.LockFile is not None :
1273            self.logDebug("Removing lock...")
1274            try :
1275                fcntl.lockf(self.LockFile, fcntl.LOCK_UN)
1276                self.LockFile.close()
1277            except :   
1278                self.logInfo("Problem while unlocking.", "error")
1279            else :   
1280                self.logDebug("Lock removed.")
1281        self.logDebug("Clean.")
1282
1283    def runBranches(self) :
1284        """Launches each hook defined for the current print queue."""
1285        self.isCancelled = 0    # did a prehook cancel the print job ?
1286        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
1287        self.pipes = { 0: (0, 1) }
1288        branches = self.enumBranches(self.PrinterName, "prehook")
1289        for b in branches.keys() :
1290            self.pipes[b.split("_", 1)[1]] = os.pipe()
1291        retcode = self.runCommands("prehook", branches, serialize)
1292        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
1293            os.close(p[1][1])
1294        if not self.isCancelled :
1295            if self.RealBackend :
1296                retcode = self.launchOriginalBackend()
1297                if retcode :
1298                    onfail = self.getPrintQueueOption(self.PrinterName, \
1299                                                      "onfail", ignore=1)
1300                    if onfail :
1301                        self.logDebug("Launching onfail script %s" % onfail)
1302                        os.system(onfail)
1303                       
1304            os.environ["TEASTATUS"] = str(retcode)
1305            branches = self.enumBranches(self.PrinterName, "posthook")
1306            if self.runCommands("posthook", branches, serialize) :
1307                self.logInfo("An error occured during the execution of posthooks.", "warn")
1308               
1309        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
1310            os.close(p[1][0])
1311        if not retcode :
1312            self.logInfo("OK")
1313        else :
1314            self.logInfo("An error occured, please check CUPS' error_log file.")
1315        return retcode
1316
1317    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
1318        """Launches a command with stdio redirected."""
1319        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
1320        pid = os.fork()
1321        if pid == 0 :
1322            if stdin != 0 :
1323                os.dup2(stdin, 0)
1324                os.close(stdin)
1325            if stdout != 1 :
1326                os.dup2(stdout, 1)
1327                os.close(stdout)
1328            try :
1329                os.execl("/bin/sh", "sh", "-c", cmd)
1330            except OSError, msg :
1331                self.logDebug("execl() failed: %s" % msg)
1332            os._exit(-1)
1333        status = os.waitpid(pid, 0)[1]
1334        if os.WIFEXITED(status) :
1335            return os.WEXITSTATUS(status)
1336        return -1
1337
1338    def runCommand(self, branch, command) :
1339        """Runs a particular branch command."""
1340        # Code contributed by Peter Stuge on June 7th 2005
1341        self.logDebug("Launching %s : %s" % (branch, command))
1342        btype, bname = branch.split("_", 1)
1343        if bname not in self.pipes.keys() :
1344            bname = 0
1345        if btype == "prehook" :
1346            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
1347        else :
1348            return self.stdioRedirSystem(command, self.pipes[bname][0])
1349
1350    def runCommands(self, btype, branches, serialize) :
1351        """Runs the commands for a particular branch type."""
1352        exitcode = 0
1353        btype = btype.lower()
1354        btypetitle = btype.title()
1355        branchlist = branches.keys()
1356        branchlist.sort()
1357        if serialize :
1358            self.logDebug("Begin serialized %ss" % btypetitle)
1359            for branch in branchlist :
1360                retcode = self.runCommand(branch, branches[branch])
1361                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
1362                if retcode :
1363                    if (btype == "prehook") and (retcode == 255) : # -1
1364                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
1365                        self.isCancelled = 1
1366                    else :
1367                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
1368                        exitcode = 1
1369            self.logDebug("End serialized %ss" % btypetitle)
1370        else :
1371            self.logDebug("Begin forked %ss" % btypetitle)
1372            pids = {}
1373            for branch in branchlist :
1374                pid = os.fork()
1375                if pid :
1376                    pids[branch] = pid
1377                else :
1378                    os._exit(self.runCommand(branch, branches[branch]))
1379            for (branch, pid) in pids.items() :
1380                retcode = os.waitpid(pid, 0)[1]
1381                if os.WIFEXITED(retcode) :
1382                    retcode = os.WEXITSTATUS(retcode)
1383                else :
1384                    retcode = -1
1385                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
1386                if retcode :
1387                    if (btype == "prehook") and (retcode == 255) : # -1
1388                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
1389                        self.isCancelled = 1
1390                    else :
1391                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
1392                        exitcode = 1
1393            self.logDebug("End forked %ss" % btypetitle)
1394        return exitcode
1395
1396    def launchOriginalBackend(self) :
1397        """Launches the original backend, optionally retrying if needed."""
1398        number = 1
1399        delay = 0
1400        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
1401        if retry is not None :
1402            try :
1403                (number, delay) = [int(p) for p in retry.strip().split(",")]
1404            except (ValueError, AttributeError, TypeError) :   
1405                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
1406                number = 1
1407                delay = 0
1408               
1409        loopcount = 1 
1410        while 1 :           
1411            retcode = self.runOriginalBackend()
1412            if not retcode :
1413                break
1414            else :
1415                if (not number) or (loopcount < number) :
1416                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
1417                    time.sleep(delay)
1418                    loopcount += 1
1419                else :   
1420                    break
1421        return retcode           
1422       
1423    def runOriginalBackend(self) :
1424        """Launches the original backend."""
1425        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1426        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1427        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1428
1429        pid = os.fork()
1430        if pid == 0 :
1431            if self.InputFile is None :
1432                f = open(self.DataFile, "rb")
1433                os.dup2(f.fileno(), 0)
1434                f.close()
1435            else :   
1436                arguments[6] = self.DataFile # in case a tea4cups filter was applied
1437            try :
1438                os.execve(originalbackend, arguments, os.environ)
1439            except OSError, msg :
1440                self.logDebug("execve() failed: %s" % msg)
1441            os._exit(-1)
1442        killed = 0
1443        status = -1
1444        while status == -1 :
1445            try :
1446                status = os.waitpid(pid, 0)[1]
1447            except OSError, (err, msg) :
1448                if err == 4 :
1449                    killed = 1
1450        if os.WIFEXITED(status) :
1451            status = os.WEXITSTATUS(status)
1452            if status :
1453                self.logInfo("CUPS backend %s returned %d." % (originalbackend, \
1454                                                             status), "error")
1455            return status
1456        elif not killed :
1457            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
1458                                                              "error")
1459            return -1
1460        else :
1461            return 1
1462
1463if __name__ == "__main__" :
1464    # This is a CUPS backend, we should act and die like a CUPS backend
1465    wrapper = CupsBackend()
1466    if len(sys.argv) == 1 :
1467        print "\n".join(wrapper.discoverOtherBackends())
1468        sys.exit(0)
1469    elif len(sys.argv) not in (6, 7) :
1470        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1471                              % sys.argv[0])
1472        sys.exit(1)
1473    else :
1474        returncode = 1
1475        try :
1476            try :
1477                wrapper.readConfig()
1478                wrapper.initBackend()
1479                wrapper.waitForLock()
1480                wrapper.saveDatasAndCheckSum()
1481                wrapper.exportAttributes()
1482                returncode = wrapper.runBranches()
1483            except SystemExit, e :
1484                returncode = e.code
1485            except KeyboardInterrupt :   
1486                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
1487            except :
1488                import traceback
1489                lines = []
1490                for errline in traceback.format_exception(*sys.exc_info()) :
1491                    lines.extend([l for l in errline.split("\n") if l])
1492                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
1493                                                              wrapper.pid, l) \
1494                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
1495                sys.stderr.write(errormessage)
1496                sys.stderr.flush()
1497                returncode = 1
1498        finally :       
1499            wrapper.cleanUp()
1500        sys.exit(returncode)
Note: See TracBrowser for help on using the browser.