root / tea4cups / trunk / tea4cups @ 683

Revision 683, 56.6 kB (checked in by jerome, 16 years ago)

Added support for clean shutdown when tea4cups receives SIGINT,
similarly to what is done for PyKota.

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