root / tea4cups / trunk / tea4cups @ 3575

Revision 3575, 58.7 kB (checked in by jerome, 6 years ago)

Fixes a problem where the job billing attribute is an integer.

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