root / tea4cups / trunk / tea4cups @ 677

Revision 677, 55.1 kB (checked in by jerome, 18 years ago)

Better support for CUPS v1.2.x and higher.
Fixed a Copy&Paste artifact of code coming from PyKota.
Now includes a copy of pkipplib v0.07 internally, and uses it by default,
only falling back to the old method if an error occurs.

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