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
RevLine 
[565]1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
[576]4# Tea4CUPS : Tee for CUPS
[565]5#
[674]6# (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
[639]7# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
[565]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.
[641]17#
[565]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
[644]20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
[565]21#
22# $Id$
23#
24#
25
26import sys
27import os
[676]28import time
[630]29import pwd
[568]30import errno
[577]31import md5
[565]32import cStringIO
33import shlex
[568]34import tempfile
[570]35import ConfigParser
[588]36import signal
[677]37import socket
38import urllib2
[653]39from struct import pack, unpack
[565]40
[677]41__version__ = "3.12alpha_unofficial"
[585]42
[570]43class TeeError(Exception):
[576]44    """Base exception for Tea4CUPS related stuff."""
[570]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__
[641]51
52class ConfigError(TeeError) :
[570]53    """Configuration related exceptions."""
[641]54    pass
55
56class IPPError(TeeError) :
[581]57    """IPP related exceptions."""
[641]58    pass
[677]59   
60IPP_VERSION = "1.1"     # default version number
[641]61
[677]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   
[646]329class IPPRequest :
[677]330    """A class for IPP requests."""
[646]331    attributes_types = ("operation", "job", "printer", "unsupported", \
332                                     "subscription", "event_notification")
[677]333    def __init__(self, data="", version=IPP_VERSION, 
334                                operation_id=None, \
335                                request_id=None, \
336                                debug=False) :
[646]337        """Initializes an IPP Message object.
338       
[635]339           Parameters :
[646]340           
341             data : the complete IPP Message's content.
[635]342             debug : a boolean value to output debug info on stderr.
343        """
[631]344        self.debug = debug
[646]345        self._data = data
[677]346        self.parsed = False
[646]347       
348        # Initializes message
[677]349        self.setVersion(version)               
350        self.setOperationId(operation_id)
351        self.setRequestId(request_id)
[646]352        self.data = ""
353       
[638]354        for attrtype in self.attributes_types :
[677]355            setattr(self, "_%s_attributes" % attrtype, [[]])
356       
[646]357        # Initialize tags   
358        self.tags = [ None ] * 256 # by default all tags reserved
359       
[581]360        # Delimiter tags
[646]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"
[676]367        self.tags[0x07] = "event_notification-attributes-tag"
[646]368       
[581]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"
[646]374        self.tags[0x15] = "not-settable"
375        self.tags[0x16] = "delete-attribute"
376        self.tags[0x17] = "admin-define"
377 
[581]378        # integer values
379        self.tags[0x20] = "generic-integer"
380        self.tags[0x21] = "integer"
381        self.tags[0x22] = "boolean"
382        self.tags[0x23] = "enum"
[646]383       
[581]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"
[646]389        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
[581]390        self.tags[0x35] = "textWithLanguage"
391        self.tags[0x36] = "nameWithLanguage"
[646]392        self.tags[0x37] = "endCollection"
393       
[581]394        # character strings
[646]395        self.tags[0x40] = "generic-character-string"
[581]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"
[646]404        self.tags[0x4a] = "memberAttrName"
405       
406        # Reverse mapping to generate IPP messages
[677]407        self.tagvalues = {}
[646]408        for i in range(len(self.tags)) :
409            value = self.tags[i]
410            if value is not None :
[677]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)
[646]437       
[655]438    def logDebug(self, msg) :   
[633]439        """Prints a debug message."""
440        if self.debug :
441            sys.stderr.write("%s\n" % msg)
442            sys.stderr.flush()
[646]443           
[677]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(".")]
[646]457       
[677]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       
[646]466    def dump(self) :   
467        """Generates an IPP Message.
468       
469           Returns the message as a string of text.
470        """   
[653]471        mybuffer = []
[677]472        if None not in (self.version, self.operation_id) :
[653]473            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
474            mybuffer.append(pack(">H", self.operation_id))
[677]475            mybuffer.append(pack(">I", self.request_id or 1))
[646]476            for attrtype in self.attributes_types :
[677]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"]))
[653]500        mybuffer.append(self.data)   
501        return "".join(mybuffer)
[646]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
[677]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])
[646]514        self.position = 8
[677]515        endofattributes = self.tagvalues["end-of-attributes-tag"]
516        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
517        nulloffset = lambda : 0
[646]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 :
[677]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       
[646]530                tag = ord(self._data[self.position])
[677]531                if tag == oldtag :
532                    self._curattributes.append([])
[646]533        except IndexError :
534            raise IPPError, "Unexpected end of IPP message."
535           
536        self.data = self._data[self.position+1:]           
[677]537        self.parsed = True
[646]538       
539    def parseTag(self) :   
[581]540        """Extracts information from an IPP tag."""
541        pos = self.position
[646]542        tagtype = self.tags[ord(self._data[pos])]
[581]543        pos += 1
544        posend = pos2 = pos + 2
[646]545        namelength = unpack(">H", self._data[pos:pos2])[0]
[581]546        if not namelength :
[631]547            name = self._curname
[646]548        else :   
[581]549            posend += namelength
[646]550            self._curname = name = self._data[pos2:posend]
[581]551        pos2 = posend + 2
[646]552        valuelength = unpack(">H", self._data[posend:pos2])[0]
[581]553        posend = pos2 + valuelength
[646]554        value = self._data[pos2:posend]
[633]555        if tagtype in ("integer", "enum") :
[634]556            value = unpack(">I", value)[0]
[646]557        elif tagtype == "boolean" :   
558            value = ord(value)
[677]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)]))
[655]567        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
[581]568        return posend - self.position
[646]569       
570    def operation_attributes_tag(self) : 
[581]571        """Indicates that the parser enters into an operation-attributes-tag group."""
[677]572        self._curattributes = self._operation_attributes
[581]573        return self.parseTag()
[646]574       
575    def job_attributes_tag(self) : 
[635]576        """Indicates that the parser enters into a job-attributes-tag group."""
[677]577        self._curattributes = self._job_attributes
[581]578        return self.parseTag()
[646]579       
580    def printer_attributes_tag(self) : 
[635]581        """Indicates that the parser enters into a printer-attributes-tag group."""
[677]582        self._curattributes = self._printer_attributes
[581]583        return self.parseTag()
[646]584       
585    def unsupported_attributes_tag(self) : 
[635]586        """Indicates that the parser enters into an unsupported-attributes-tag group."""
[677]587        self._curattributes = self._unsupported_attributes
[635]588        return self.parseTag()
[646]589       
590    def subscription_attributes_tag(self) : 
591        """Indicates that the parser enters into a subscription-attributes-tag group."""
[677]592        self._curattributes = self._subscription_attributes
[646]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."""
[677]597        self._curattributes = self._event_notification_attributes
[646]598        return self.parseTag()
[677]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       
[641]793
794class FakeConfig :
[570]795    """Fakes a configuration file parser."""
796    def get(self, section, option, raw=0) :
[626]797        """Fakes the retrieval of an option."""
[570]798        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
[641]799
[568]800class CupsBackend :
801    """Base class for tools with no database access."""
802    def __init__(self) :
803        """Initializes the CUPS backend wrapper."""
[588]804        signal.signal(signal.SIGTERM, signal.SIG_IGN)
805        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
[577]806        self.MyName = "Tea4CUPS"
807        self.myname = "tea4cups"
808        self.pid = os.getpid()
[641]809
810    def readConfig(self) :
[626]811        """Reads the configuration file."""
[641]812        confdir = os.environ.get("CUPS_SERVERROOT", ".")
[577]813        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
[570]814        if os.path.isfile(self.conffile) :
815            self.config = ConfigParser.ConfigParser()
816            self.config.read([self.conffile])
[573]817            self.debug = self.isTrue(self.getGlobalOption("debug", ignore=1))
[641]818        else :
[570]819            self.config = FakeConfig()
820            self.debug = 1      # no config, so force debug mode !
[641]821
822    def logInfo(self, message, level="info") :
[574]823        """Logs a message to CUPS' error_log file."""
[640]824        try :
[664]825            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
[640]826            sys.stderr.flush()
827        except IOError :
828            pass
[641]829
830    def logDebug(self, message) :
[585]831        """Logs something to debug output if debug is enabled."""
832        if self.debug :
833            self.logInfo(message, level="debug")
[641]834
835    def isTrue(self, option) :
[570]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
[641]839        else :
[570]840            return 0
[641]841
842    def getGlobalOption(self, option, ignore=0) :
[570]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)
[641]846        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[577]847            if not ignore :
[570]848                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
[641]849
850    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
[570]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 :
[574]854            return self.config.get(printqueuename, option, raw=1)
[641]855        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[570]856            if globaloption is not None :
857                return globaloption
[577]858            elif not ignore :
[574]859                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
[641]860
[597]861    def enumBranches(self, printqueuename, branchtype="tee") :
862        """Returns the list of branchtypes branches for a particular section's."""
863        branchbasename = "%s_" % branchtype.lower()
[573]864        try :
[627]865            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
[641]866        except ConfigParser.NoSectionError, msg :
[579]867            raise ConfigError, "Invalid configuration file : %s" % msg
[573]868        try :
[627]869            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
[641]870        except ConfigParser.NoSectionError, msg :
[583]871            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
[573]872            sectionbranches = []
873        branches = {}
874        for (k, v) in globalbranches :
875            value = v.strip()
876            if value :
877                branches[k] = value
[641]878        for (k, v) in sectionbranches :
[573]879            value = v.strip()
880            if value :
881                branches[k] = value # overwrite any global option or set a new value
[641]882            else :
[573]883                del branches[k] # empty value disables a global option
884        return branches
[641]885
886    def discoverOtherBackends(self) :
[568]887        """Discovers the other CUPS backends.
[641]888
[568]889           Executes each existing backend in turn in device enumeration mode.
890           Returns the list of available backends.
891        """
[569]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.
[568]895        available = []
[569]896        (directory, myname) = os.path.split(sys.argv[0])
[588]897        if not directory :
898            directory = "./"
[568]899        tmpdir = tempfile.gettempdir()
[569]900        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
[568]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)
[641]908            except OSError, e :
[568]909                if e.errno != errno.EPERM :
910                    # process doesn't exist anymore
911                    os.remove(lockfilename)
[641]912
[568]913        if not os.path.exists(lockfilename) :
914            lockfile = open(lockfilename, "w")
[577]915            lockfile.write("%i" % self.pid)
[568]916            lockfile.close()
[569]917            allbackends = [ os.path.join(directory, b) \
[641]918                                for b in os.listdir(directory)
[569]919                                    if os.access(os.path.join(directory, b), os.X_OK) \
[641]920                                        and (b != myname)]
921            for backend in allbackends :
[568]922                answer = os.popen(backend, "r")
923                try :
924                    devices = [line.strip() for line in answer.readlines()]
[641]925                except :
[568]926                    devices = []
927                status = answer.close()
928                if status is None :
929                    for d in devices :
[641]930                        # each line is of the form :
[568]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
[641]947                        except ValueError :
[568]948                            pass    # ignore this 'bizarre' device
[641]949                        else :
[568]950                            if name.startswith('"') and name.endswith('"') :
951                                name = name[1:-1]
952                            if fullname.startswith('"') and fullname.endswith('"') :
953                                fullname = fullname[1:-1]
[577]954                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
955                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
[568]956            os.remove(lockfilename)
[599]957        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
958                             % (self.myname, self.MyName, self.MyName))
[568]959        return available
[641]960
961    def initBackend(self) :
[577]962        """Initializes the backend's attributes."""
[677]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       
[641]976        # check that the DEVICE_URI environment variable's value is
[577]977        # prefixed with self.myname otherwise don't touch it.
[641]978        # If this is the case, we have to remove the prefix from
979        # the environment before launching the real backend
[577]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):]
[583]985            for i in range(2) :
[641]986                if device_uri.startswith("/") :
[583]987                    device_uri = device_uri[1:]
[577]988        try :
[641]989            (backend, destination) = device_uri.split(":", 1)
990        except ValueError :
[583]991            if not device_uri :
[600]992                self.logDebug("Not attached to an existing print queue.")
[583]993                backend = ""
[641]994            else :
[583]995                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
[641]996
[577]997        self.RealBackend = backend
998        self.DeviceURI = device_uri
[677]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)
[652]1015        if type(john) == type([]) :                         
1016            john = john[-1]
1017        (chtype, self.ClientHost) = john                         
[677]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
[641]1025
[583]1026    def getCupsConfigDirectives(self, directives=[]) :
1027        """Retrieves some CUPS directives from its configuration file.
[641]1028
1029           Returns a mapping with lowercased directives as keys and
[583]1030           their setting as values.
1031        """
[641]1032        dirvalues = {}
[583]1033        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
1034        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
1035        try :
1036            conffile = open(cupsdconf, "r")
[641]1037        except IOError :
[583]1038            raise TeeError, "Unable to open %s" % cupsdconf
[641]1039        else :
[583]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]
[641]1046                        except :
[583]1047                            pass # ignore errors, we take the last value in any case.
[641]1048                        else :
[583]1049                            dirvalues[di] = val
[641]1050            conffile.close()
1051        return dirvalues
1052
[646]1053    def parseIPPRequestFile(self) :
[624]1054        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
[677]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")
[582]1059        if (len(self.JobId) < 5) and self.JobId.isdigit() :
1060            ippmessagefile = "c%05i" % int(self.JobId)
[641]1061        else :
[582]1062            ippmessagefile = "c%s" % self.JobId
1063        ippmessagefile = os.path.join(requestroot, ippmessagefile)
1064        ippmessage = {}
1065        try :
1066            ippdatafile = open(ippmessagefile)
[641]1067        except :
[582]1068            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
[641]1069        else :
[582]1070            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
1071            try :
[646]1072                ippmessage = IPPRequest(ippdatafile.read())
1073                ippmessage.parse()
[641]1074            except IPPError, msg :
[582]1075                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
[641]1076            else :
[582]1077                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
1078            ippdatafile.close()
[615]1079        return (ippmessagefile, ippmessage)
[641]1080
1081    def exportAttributes(self) :
[577]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
[579]1087        os.environ["TEAJOBSIZE"] = str(self.JobSize)
[577]1088        os.environ["TEAMD5SUM"] = self.JobMD5Sum
[582]1089        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
[577]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 ""
[615]1096        os.environ["TEABILLING"] = self.JobBilling or ""
1097        os.environ["TEACONTROLFILE"] = self.ControlFile
[641]1098
[577]1099    def saveDatasAndCheckSum(self) :
1100        """Saves the input datas into a static file."""
[583]1101        self.logDebug("Duplicating data stream into %s" % self.DataFile)
[577]1102        mustclose = 0
1103        if self.InputFile is not None :
1104            infile = open(self.InputFile, "rb")
1105            mustclose = 1
[641]1106        else :
[577]1107            infile = sys.stdin
[659]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           
[577]1125        CHUNK = 64*1024         # read 64 Kb at a time
1126        dummy = 0
1127        sizeread = 0
1128        checksum = md5.new()
[641]1129        outfile = open(self.DataFile, "wb")
[577]1130        while 1 :
[641]1131            data = infile.read(CHUNK)
[577]1132            if not data :
1133                break
[641]1134            sizeread += len(data)
[577]1135            outfile.write(data)
[641]1136            checksum.update(data)
[577]1137            if not (dummy % 32) : # Only display every 2 Mb
1138                self.logDebug("%s bytes saved..." % sizeread)
[641]1139            dummy += 1
[577]1140        outfile.close()
[659]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               
[641]1149        if mustclose :
[579]1150            infile.close()
[659]1151           
1152        self.logDebug("%s bytes saved..." % sizeread)
[641]1153        self.JobSize = sizeread
[577]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))
[568]1157
[577]1158    def cleanUp(self) :
1159        """Cleans up the place."""
[676]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")
[641]1166
[588]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)
[641]1171
1172    def runBranches(self) :
[640]1173        """Launches each hook defined for the current print queue."""
[604]1174        self.isCancelled = 0    # did a prehook cancel the print job ?
1175        self.gotSigTerm = 0
[588]1176        signal.signal(signal.SIGTERM, self.sigtermHandler)
[598]1177        serialize = self.isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
[643]1178        self.pipes = { 0: (0, 1) }
[640]1179        branches = self.enumBranches(self.PrinterName, "prehook")
[662]1180        for b in branches.keys() :
[640]1181            self.pipes[b.split("_", 1)[1]] = os.pipe()
1182        retcode = self.runCommands("prehook", branches, serialize)
[643]1183        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]1184            os.close(p[1][1])
1185        if not self.isCancelled and not self.gotSigTerm :
1186            if self.RealBackend :
[676]1187                retcode = self.launchOriginalBackend()
[659]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)
[640]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) :
[625]1198                    self.logInfo("An error occured during the execution of posthooks.", "warn")
[643]1199        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]1200            os.close(p[1][0])
[598]1201        signal.signal(signal.SIGTERM, signal.SIG_IGN)
[640]1202        if not retcode :
[600]1203            self.logInfo("OK")
[641]1204        else :
[600]1205            self.logInfo("An error occured, please check CUPS' error_log file.")
[640]1206        return retcode
[641]1207
[639]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
[636]1211        pid = os.fork()
1212        if pid == 0 :
[639]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)
[640]1221            except OSError, msg :
[639]1222                self.logDebug("execl() failed: %s" % msg)
[640]1223            os._exit(-1)
1224        status = os.waitpid(pid, 0)[1]
1225        if os.WIFEXITED(status) :
1226            return os.WEXITSTATUS(status)
1227        return -1
[641]1228
[639]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)
[643]1234        if bname not in self.pipes.keys() :
[640]1235            bname = 0
1236        if btype == "prehook" :
[639]1237            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
1238        else :
1239            return self.stdioRedirSystem(command, self.pipes[bname][0])
1240
[641]1241    def runCommands(self, btype, branches, serialize) :
[598]1242        """Runs the commands for a particular branch type."""
[641]1243        exitcode = 0
[598]1244        btype = btype.lower()
1245        btypetitle = btype.title()
[641]1246        branchlist = branches.keys()
[598]1247        branchlist.sort()
1248        if serialize :
[600]1249            self.logDebug("Begin serialized %ss" % btypetitle)
[598]1250            for branch in branchlist :
[588]1251                if self.gotSigTerm :
1252                    break
[640]1253                retcode = self.runCommand(branch, branches[branch])
[598]1254                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
[641]1255                if retcode :
[601]1256                    if (btype == "prehook") and (retcode == 255) : # -1
1257                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]1258                        self.isCancelled = 1
[641]1259                    else :
[601]1260                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
1261                        exitcode = 1
[600]1262            self.logDebug("End serialized %ss" % btypetitle)
[641]1263        else :
[600]1264            self.logDebug("Begin forked %ss" % btypetitle)
[584]1265            pids = {}
[598]1266            for branch in branchlist :
[588]1267                if self.gotSigTerm :
1268                    break
[584]1269                pid = os.fork()
1270                if pid :
1271                    pids[branch] = pid
[641]1272                else :
[640]1273                    os._exit(self.runCommand(branch, branches[branch]))
[584]1274            for (branch, pid) in pids.items() :
[640]1275                retcode = os.waitpid(pid, 0)[1]
[584]1276                if os.WIFEXITED(retcode) :
1277                    retcode = os.WEXITSTATUS(retcode)
[640]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))
[641]1281                if retcode :
[601]1282                    if (btype == "prehook") and (retcode == 255) : # -1
1283                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]1284                        self.isCancelled = 1
[641]1285                    else :
[640]1286                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
[601]1287                        exitcode = 1
[600]1288            self.logDebug("End forked %ss" % btypetitle)
[584]1289        return exitcode
[641]1290
[676]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) :
[677]1311                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
[676]1312                    time.sleep(delay)
1313                    loopcount += 1
1314                else :   
1315                    break
1316        return retcode           
1317       
[641]1318    def runOriginalBackend(self) :
[587]1319        """Launches the original backend."""
1320        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
[640]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 :
[653]1327                f = open(self.DataFile, "rb")
[640]1328                os.dup2(f.fileno(), 0)
1329                f.close()
[676]1330            else :   
1331                arguments[6] = self.DataFile # in case a tea4cups filter was applied
[640]1332            try :
[643]1333                os.execve(originalbackend, arguments, os.environ)
[640]1334            except OSError, msg :
1335                self.logDebug("execve() failed: %s" % msg)
1336            os._exit(-1)
[588]1337        killed = 0
1338        status = -1
[640]1339        while status == -1 :
[588]1340            try :
[640]1341                status = os.waitpid(pid, 0)[1]
1342            except OSError, (err, msg) :
[648]1343                if (err == 4) and self.gotSigTerm :
[640]1344                    os.kill(pid, signal.SIGTERM)
1345                    killed = 1
[587]1346        if os.WIFEXITED(status) :
[640]1347            status = os.WEXITSTATUS(status)
1348            if status :
[676]1349                self.logInfo("CUPS backend %s returned %d." % (originalbackend,\
[643]1350                                                             status), "error")
[640]1351            return status
[641]1352        elif not killed :
[643]1353            self.logInfo("CUPS backend %s died abnormally." % originalbackend,\
1354                                                              "error")
[588]1355            return -1
[641]1356        else :
[587]1357            return 1
[641]1358
1359if __name__ == "__main__" :
[565]1360    # This is a CUPS backend, we should act and die like a CUPS backend
[568]1361    wrapper = CupsBackend()
[565]1362    if len(sys.argv) == 1 :
[568]1363        print "\n".join(wrapper.discoverOtherBackends())
[641]1364        sys.exit(0)
1365    elif len(sys.argv) not in (6, 7) :
[568]1366        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1367                              % sys.argv[0])
1368        sys.exit(1)
[641]1369    else :
[676]1370        retcode = 1
[585]1371        try :
[676]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 :       
[585]1392            wrapper.cleanUp()
[579]1393        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.