root / tea4cups / trunk / tea4cups @ 3519

Revision 3514, 58.0 kB (checked in by jerome, 14 years ago)

Try to do something about deadlock, not sure it happens there though...

  • 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-2009 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, 2007 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.13alpha_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
345class FakeAttribute :
346    """Fakes an IPPRequest attribute to simplify usage syntax."""
347    def __init__(self, request, name) :
348        """Initializes the fake attribute."""
349        self.request = request
350        self.name = name
351
352    def __setitem__(self, key, value) :
353        """Appends the value to the real attribute."""
354        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
355        for i in range(len(attributeslist)) :
356            attribute = attributeslist[i]
357            for j in range(len(attribute)) :
358                (attrname, attrvalue) = attribute[j]
359                if attrname == key :
360                    attribute[j][1].append(value)
361                    return
362            attribute.append((key, [value]))
363
364    def __getitem__(self, key) :
365        """Returns an attribute's value."""
366        answer = []
367        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
368        for i in range(len(attributeslist)) :
369            attribute = attributeslist[i]
370            for j in range(len(attribute)) :
371                (attrname, attrvalue) = attribute[j]
372                if attrname == key :
373                    answer.extend(attrvalue)
374        if answer :
375            return answer
376        raise KeyError, key
377
378class IPPRequest :
379    """A class for IPP requests."""
380    attributes_types = ("operation", "job", "printer", "unsupported", \
381                                     "subscription", "event_notification")
382    def __init__(self, data="", version=IPP_VERSION,
383                                operation_id=None, \
384                                request_id=None, \
385                                debug=False) :
386        """Initializes an IPP Message object.
387
388           Parameters :
389
390             data : the complete IPP Message's content.
391             debug : a boolean value to output debug info on stderr.
392        """
393        self.debug = debug
394        self._data = data
395        self.parsed = False
396
397        # Initializes message
398        self.setVersion(version)
399        self.setOperationId(operation_id)
400        self.setRequestId(request_id)
401        self.data = ""
402
403        for attrtype in self.attributes_types :
404            setattr(self, "_%s_attributes" % attrtype, [[]])
405
406        # Initialize tags
407        self.tags = [ None ] * 256 # by default all tags reserved
408
409        # Delimiter tags
410        self.tags[0x01] = "operation-attributes-tag"
411        self.tags[0x02] = "job-attributes-tag"
412        self.tags[0x03] = "end-of-attributes-tag"
413        self.tags[0x04] = "printer-attributes-tag"
414        self.tags[0x05] = "unsupported-attributes-tag"
415        self.tags[0x06] = "subscription-attributes-tag"
416        self.tags[0x07] = "event_notification-attributes-tag"
417
418        # out of band values
419        self.tags[0x10] = "unsupported"
420        self.tags[0x11] = "reserved-for-future-default"
421        self.tags[0x12] = "unknown"
422        self.tags[0x13] = "no-value"
423        self.tags[0x15] = "not-settable"
424        self.tags[0x16] = "delete-attribute"
425        self.tags[0x17] = "admin-define"
426
427        # integer values
428        self.tags[0x20] = "generic-integer"
429        self.tags[0x21] = "integer"
430        self.tags[0x22] = "boolean"
431        self.tags[0x23] = "enum"
432
433        # octetString
434        self.tags[0x30] = "octetString-with-an-unspecified-format"
435        self.tags[0x31] = "dateTime"
436        self.tags[0x32] = "resolution"
437        self.tags[0x33] = "rangeOfInteger"
438        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
439        self.tags[0x35] = "textWithLanguage"
440        self.tags[0x36] = "nameWithLanguage"
441        self.tags[0x37] = "endCollection"
442
443        # character strings
444        self.tags[0x40] = "generic-character-string"
445        self.tags[0x41] = "textWithoutLanguage"
446        self.tags[0x42] = "nameWithoutLanguage"
447        self.tags[0x44] = "keyword"
448        self.tags[0x45] = "uri"
449        self.tags[0x46] = "uriScheme"
450        self.tags[0x47] = "charset"
451        self.tags[0x48] = "naturalLanguage"
452        self.tags[0x49] = "mimeMediaType"
453        self.tags[0x4a] = "memberAttrName"
454
455        # Reverse mapping to generate IPP messages
456        self.tagvalues = {}
457        for i in range(len(self.tags)) :
458            value = self.tags[i]
459            if value is not None :
460                self.tagvalues[value] = i
461
462    def __getattr__(self, name) :
463        """Fakes attribute access."""
464        if name in self.attributes_types :
465            return FakeAttribute(self, name)
466        else :
467            raise AttributeError, name
468
469    def __str__(self) :
470        """Returns the parsed IPP message in a readable form."""
471        if not self.parsed :
472            return ""
473        mybuffer = []
474        mybuffer.append("IPP version : %s.%s" % self.version)
475        mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
476        mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
477        for attrtype in self.attributes_types :
478            for attribute in getattr(self, "_%s_attributes" % attrtype) :
479                if attribute :
480                    mybuffer.append("%s attributes :" % attrtype.title())
481                for (name, value) in attribute :
482                    mybuffer.append("  %s : %s" % (name, value))
483        if self.data :
484            mybuffer.append("IPP datas : %s" % repr(self.data))
485        return "\n".join(mybuffer)
486
487    def logDebug(self, msg) :
488        """Prints a debug message."""
489        if self.debug :
490            sys.stderr.write("%s\n" % msg)
491            sys.stderr.flush()
492
493    def setVersion(self, version) :
494        """Sets the request's operation id."""
495        if version is not None :
496            try :
497                self.version = [int(p) for p in version.split(".")]
498            except AttributeError :
499                if len(version) == 2 : # 2-tuple
500                    self.version = version
501                else :
502                    try :
503                        self.version = [int(p) for p in str(float(version)).split(".")]
504                    except :
505                        self.version = [int(p) for p in IPP_VERSION.split(".")]
506
507    def setOperationId(self, opid) :
508        """Sets the request's operation id."""
509        self.operation_id = opid
510
511    def setRequestId(self, reqid) :
512        """Sets the request's request id."""
513        self.request_id = reqid
514
515    def dump(self) :
516        """Generates an IPP Message.
517
518           Returns the message as a string of text.
519        """
520        mybuffer = []
521        if None not in (self.version, self.operation_id) :
522            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
523            mybuffer.append(pack(">H", self.operation_id))
524            mybuffer.append(pack(">I", self.request_id or 1))
525            for attrtype in self.attributes_types :
526                for attribute in getattr(self, "_%s_attributes" % attrtype) :
527                    if attribute :
528                        mybuffer.append(chr(self.tagvalues["%s-attributes-tag" % attrtype]))
529                    for (attrname, value) in attribute :
530                        nameprinted = 0
531                        for (vtype, val) in value :
532                            mybuffer.append(chr(self.tagvalues[vtype]))
533                            if not nameprinted :
534                                mybuffer.append(pack(">H", len(attrname)))
535                                mybuffer.append(attrname)
536                                nameprinted = 1
537                            else :
538                                mybuffer.append(pack(">H", 0))
539                            if vtype in ("integer", "enum") :
540                                mybuffer.append(pack(">H", 4))
541                                mybuffer.append(pack(">I", val))
542                            elif vtype == "boolean" :
543                                mybuffer.append(pack(">H", 1))
544                                mybuffer.append(chr(val))
545                            else :
546                                mybuffer.append(pack(">H", len(val)))
547                                mybuffer.append(val)
548            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
549        mybuffer.append(self.data)
550        return "".join(mybuffer)
551
552    def parse(self) :
553        """Parses an IPP Request.
554
555           NB : Only a subset of RFC2910 is implemented.
556        """
557        self._curname = None
558        self._curattributes = None
559
560        self.setVersion((ord(self._data[0]), ord(self._data[1])))
561        self.setOperationId(unpack(">H", self._data[2:4])[0])
562        self.setRequestId(unpack(">I", self._data[4:8])[0])
563        self.position = 8
564        endofattributes = self.tagvalues["end-of-attributes-tag"]
565        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
566        nulloffset = lambda : 0
567        try :
568            tag = ord(self._data[self.position])
569            while tag != endofattributes :
570                self.position += 1
571                name = self.tags[tag]
572                if name is not None :
573                    func = getattr(self, name.replace("-", "_"), nulloffset)
574                    self.position += func()
575                    if ord(self._data[self.position]) > maxdelimiter :
576                        self.position -= 1
577                        continue
578                oldtag = tag
579                tag = ord(self._data[self.position])
580                if tag == oldtag :
581                    self._curattributes.append([])
582        except IndexError :
583            raise IPPError, "Unexpected end of IPP message."
584
585        self.data = self._data[self.position+1:]
586        self.parsed = True
587
588    def parseTag(self) :
589        """Extracts information from an IPP tag."""
590        pos = self.position
591        tagtype = self.tags[ord(self._data[pos])]
592        pos += 1
593        posend = pos2 = pos + 2
594        namelength = unpack(">H", self._data[pos:pos2])[0]
595        if not namelength :
596            name = self._curname
597        else :
598            posend += namelength
599            self._curname = name = self._data[pos2:posend]
600        pos2 = posend + 2
601        valuelength = unpack(">H", self._data[posend:pos2])[0]
602        posend = pos2 + valuelength
603        value = self._data[pos2:posend]
604        if tagtype in ("integer", "enum") :
605            value = unpack(">I", value)[0]
606        elif tagtype == "boolean" :
607            value = ord(value)
608        try :
609            (oldname, oldval) = self._curattributes[-1][-1]
610            if oldname == name :
611                oldval.append((tagtype, value))
612            else :
613                raise IndexError
614        except IndexError :
615            self._curattributes[-1].append((name, [(tagtype, value)]))
616        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
617        return posend - self.position
618
619    def operation_attributes_tag(self) :
620        """Indicates that the parser enters into an operation-attributes-tag group."""
621        self._curattributes = self._operation_attributes
622        return self.parseTag()
623
624    def job_attributes_tag(self) :
625        """Indicates that the parser enters into a job-attributes-tag group."""
626        self._curattributes = self._job_attributes
627        return self.parseTag()
628
629    def printer_attributes_tag(self) :
630        """Indicates that the parser enters into a printer-attributes-tag group."""
631        self._curattributes = self._printer_attributes
632        return self.parseTag()
633
634    def unsupported_attributes_tag(self) :
635        """Indicates that the parser enters into an unsupported-attributes-tag group."""
636        self._curattributes = self._unsupported_attributes
637        return self.parseTag()
638
639    def subscription_attributes_tag(self) :
640        """Indicates that the parser enters into a subscription-attributes-tag group."""
641        self._curattributes = self._subscription_attributes
642        return self.parseTag()
643
644    def event_notification_attributes_tag(self) :
645        """Indicates that the parser enters into an event-notification-attributes-tag group."""
646        self._curattributes = self._event_notification_attributes
647        return self.parseTag()
648
649
650class CUPS :
651    """A class for a CUPS instance."""
652    def __init__(self, url=None, username=None, password=None, charset="utf-8", language="en-us", debug=False) :
653        """Initializes the CUPS instance."""
654        if url is not None :
655            self.url = url.replace("ipp://", "http://")
656            if self.url.endswith("/") :
657                self.url = self.url[:-1]
658        else :
659            self.url = self.getDefaultURL()
660        self.username = username
661        self.password = password
662        self.charset = charset
663        self.language = language
664        self.debug = debug
665        self.lastError = None
666        self.lastErrorMessage = None
667        self.requestId = None
668
669    def getDefaultURL(self) :
670        """Builds a default URL."""
671        # TODO : encryption methods.
672        server = os.environ.get("CUPS_SERVER") or "localhost"
673        port = os.environ.get("IPP_PORT") or 631
674        if server.startswith("/") :
675            # it seems it's a unix domain socket.
676            # we can't handle this right now, so we use the default instead.
677            return "http://localhost:%s" % port
678        else :
679            return "http://%s:%s" % (server, port)
680
681    def identifierToURI(self, service, ident) :
682        """Transforms an identifier into a particular URI depending on requested service."""
683        return "%s/%s/%s" % (self.url.replace("http://", "ipp://"),
684                             service,
685                             ident)
686
687    def nextRequestId(self) :
688        """Increments the current request id and returns the new value."""
689        try :
690            self.requestId += 1
691        except TypeError :
692            self.requestId = 1
693        return self.requestId
694
695    def newRequest(self, operationid=None) :
696        """Generates a new empty request."""
697        if operationid is not None :
698            req = IPPRequest(operation_id=operationid, \
699                             request_id=self.nextRequestId(), \
700                             debug=self.debug)
701            req.operation["attributes-charset"] = ("charset", self.charset)
702            req.operation["attributes-natural-language"] = ("naturalLanguage", self.language)
703            return req
704
705    def doRequest(self, req, url=None) :
706        """Sends a request to the CUPS server.
707           returns a new IPPRequest object, containing the parsed answer.
708        """
709        connexion = urllib2.Request(url=url or self.url, \
710                             data=req.dump())
711        connexion.add_header("Content-Type", "application/ipp")
712        if self.username :
713            pwmanager = urllib2.HTTPPasswordMgrWithDefaultRealm()
714            pwmanager.add_password(None, \
715                                   "%s%s" % (connexion.get_host(), connexion.get_selector()), \
716                                   self.username, \
717                                   self.password or "")
718            authhandler = urllib2.HTTPBasicAuthHandler(pwmanager)
719            opener = urllib2.build_opener(authhandler)
720            urllib2.install_opener(opener)
721        self.lastError = None
722        self.lastErrorMessage = None
723        try :
724            response = urllib2.urlopen(connexion)
725        except (urllib2.URLError, urllib2.HTTPError, socket.error), error :
726            self.lastError = error
727            self.lastErrorMessage = str(error)
728            return None
729        else :
730            datas = response.read()
731            ippresponse = IPPRequest(datas)
732            ippresponse.parse()
733            return ippresponse
734
735    def getPPD(self, queuename) :
736        """Retrieves the PPD for a particular queuename."""
737        req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES)
738        req.operation["printer-uri"] = ("uri", self.identifierToURI("printers", queuename))
739        for attrib in ("printer-uri-supported", "printer-type", "member-uris") :
740            req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib)
741        return self.doRequest(req)  # TODO : get the PPD from the actual print server
742
743    def getDefault(self) :
744        """Retrieves CUPS' default printer."""
745        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))
746
747    def getJobAttributes(self, jobid) :
748        """Retrieves a print job's attributes."""
749        req = self.newRequest(IPP_GET_JOB_ATTRIBUTES)
750        req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid))
751        return self.doRequest(req)
752
753    def getPrinters(self) :
754        """Returns the list of print queues names."""
755        req = self.newRequest(CUPS_GET_PRINTERS)
756        req.operation["requested-attributes"] = ("keyword", "printer-name")
757        req.operation["printer-type"] = ("enum", 0)
758        req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS)
759        return [printer[1] for printer in self.doRequest(req).printer["printer-name"]]
760
761    def getDevices(self) :
762        """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples."""
763        answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES))
764        return zip([d[1] for d in answer.printer["device-class"]], \
765                   [d[1] for d in answer.printer["device-info"]], \
766                   [d[1] for d in answer.printer["device-make-and-model"]], \
767                   [d[1] for d in answer.printer["device-uri"]])
768
769    def getPPDs(self) :
770        """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples."""
771        answer = self.doRequest(self.newRequest(CUPS_GET_PPDS))
772        return zip([d[1] for d in answer.printer["ppd-natural-language"]], \
773                   [d[1] for d in answer.printer["ppd-make"]], \
774                   [d[1] for d in answer.printer["ppd-make-and-model"]], \
775                   [d[1] for d in answer.printer["ppd-name"]])
776
777    def createSubscription(self, uri, events=["all"],
778                                      userdata=None,
779                                      recipient=None,
780                                      pullmethod=None,
781                                      charset=None,
782                                      naturallanguage=None,
783                                      leaseduration=None,
784                                      timeinterval=None,
785                                      jobid=None) :
786        """Creates a job, printer or server subscription.
787
788           uri : the subscription's uri, e.g. ipp://server
789           events : a list of events to subscribe to, e.g. ["printer-added", "printer-deleted"]
790           recipient : the notifier's uri
791           pullmethod : the pull method to use
792           charset : the charset to use when sending notifications
793           naturallanguage : the language to use when sending notifications
794           leaseduration : the duration of the lease in seconds
795           timeinterval : the interval of time during notifications
796           jobid : the optional job id in case of a job subscription
797        """
798        if jobid is not None :
799            opid = IPP_CREATE_JOB_SUBSCRIPTION
800            uritype = "job-uri"
801        else :
802            opid = IPP_CREATE_PRINTER_SUBSCRIPTION
803            uritype = "printer-uri"
804        req = self.newRequest(opid)
805        req.operation[uritype] = ("uri", uri)
806        for event in events :
807            req.subscription["notify-events"] = ("keyword", event)
808        if userdata is not None :
809            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
810        if recipient is not None :
811            req.subscription["notify-recipient"] = ("uri", recipient)
812        if pullmethod is not None :
813            req.subscription["notify-pull-method"] = ("keyword", pullmethod)
814        if charset is not None :
815            req.subscription["notify-charset"] = ("charset", charset)
816        if naturallanguage is not None :
817            req.subscription["notify-natural-language"] = ("naturalLanguage", naturallanguage)
818        if leaseduration is not None :
819            req.subscription["notify-lease-duration"] = ("integer", leaseduration)
820        if timeinterval is not None :
821            req.subscription["notify-time-interval"] = ("integer", timeinterval)
822        if jobid is not None :
823            req.subscription["notify-job-id"] = ("integer", jobid)
824        return self.doRequest(req)
825
826    def cancelSubscription(self, uri, subscriptionid, jobid=None) :
827        """Cancels a subscription.
828
829           uri : the subscription's uri.
830           subscriptionid : the subscription's id.
831           jobid : the optional job's id.
832        """
833        req = self.newRequest(IPP_CANCEL_SUBSCRIPTION)
834        if jobid is not None :
835            uritype = "job-uri"
836        else :
837            uritype = "printer-uri"
838        req.operation[uritype] = ("uri", uri)
839        req.event_notification["notify-subscription-id"] = ("integer", subscriptionid)
840        return self.doRequest(req)
841
842
843class FakeConfig :
844    """Fakes a configuration file parser."""
845    def get(self, section, option, raw=0) :
846        """Fakes the retrieval of an option."""
847        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
848
849def isTrue(option) :
850    """Returns 1 if option is set to true, else 0."""
851    if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
852        return 1
853    else :
854        return 0
855
856def getCupsConfigDirectives(directives=[]) :
857    """Retrieves some CUPS directives from its configuration file.
858
859       Returns a mapping with lowercased directives as keys and
860       their setting as values.
861    """
862    dirvalues = {}
863    cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
864    cupsdconf = os.path.join(cupsroot, "cupsd.conf")
865    try :
866        conffile = open(cupsdconf, "r")
867    except IOError :
868        raise TeeError, "Unable to open %s" % cupsdconf
869    else :
870        for line in conffile.readlines() :
871            linecopy = line.strip().lower()
872            for di in [d.lower() for d in directives] :
873                if linecopy.startswith("%s " % di) :
874                    try :
875                        val = line.split()[1]
876                    except :
877                        pass # ignore errors, we take the last value in any case.
878                    else :
879                        dirvalues[di] = val
880        conffile.close()
881    return dirvalues
882
883class CupsBackend :
884    """Base class for tools with no database access."""
885    def __init__(self) :
886        """Initializes the CUPS backend wrapper."""
887        signal.signal(signal.SIGTERM, signal.SIG_IGN)
888        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
889        self.MyName = "Tea4CUPS"
890        self.myname = "tea4cups"
891        self.pid = os.getpid()
892        self.debug = True
893        self.isCancelled = False
894        self.Title = None
895        self.InputFile = None
896        self.RealBackend = None
897        self.ControlFile = None
898        self.ClientHost = None
899        self.PrinterName = None
900        self.JobBilling = None
901        self.UserName = None
902        self.Copies = None
903        self.Options = None
904        self.DataFile = None
905        self.JobMD5Sum = None
906        self.JobSize = None
907        self.DeviceURI = None
908        self.JobId = None
909        self.Directory = None
910        self.pipes = {}
911        self.config = None
912        self.conffile = None
913        self.LockFile = None
914
915    def waitForLock(self) :
916        """Waits until we can acquire the lock file."""
917        random.seed()
918        lockfilename = self.DeviceURI.replace("/", ".")
919        lockfilename = lockfilename.replace(":", ".")
920        lockfilename = lockfilename.replace("?", ".")
921        lockfilename = lockfilename.replace("&", ".")
922        lockfilename = lockfilename.replace("@", ".")
923        lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, lockfilename))
924        self.logDebug("Waiting for lock %s to become available..." % lockfilename)
925        haslock = False
926        while not haslock :
927            try :
928                # open the lock file, optionally creating it if needed.
929                self.LockFile = None
930                self.LockFile = open(lockfilename, "a+")
931
932                # we wait indefinitely for the lock to become available.
933                # works over NFS too.
934                fcntl.lockf(self.LockFile, fcntl.LOCK_EX)
935                haslock = True
936
937                self.logDebug("Lock %s acquired." % lockfilename)
938
939                # Here we save the PID in the lock file, but we don't use
940                # it, because the lock file may be in a directory shared
941                # over NFS between two (or more) print servers, so the PID
942                # has no meaning in this case.
943                self.LockFile.truncate(0)
944                self.LockFile.seek(0, 0)
945                self.LockFile.write(str(self.pid))
946                self.LockFile.flush()
947            except IOError :
948                self.logDebug("I/O Error while waiting for lock %s" % lockfilename)
949                if self.LockFile is not None :
950                    self.LockFile.close()
951                time.sleep(0.25 + random.random())
952
953    def readConfig(self) :
954        """Reads the configuration file."""
955        confdir = os.environ.get("CUPS_SERVERROOT", ".")
956        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
957        if os.path.isfile(self.conffile) :
958            self.config = ConfigParser.ConfigParser()
959            self.config.read([self.conffile])
960            self.debug = isTrue(self.getGlobalOption("debug", ignore=1))
961        else :
962            self.config = FakeConfig()
963            self.debug = 1      # no config, so force debug mode !
964
965    def logInfo(self, message, level="info") :
966        """Logs a message to CUPS' error_log file."""
967        try :
968            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
969            sys.stderr.flush()
970        except IOError :
971            pass
972
973    def logDebug(self, message) :
974        """Logs something to debug output if debug is enabled."""
975        if self.debug :
976            self.logInfo(message, level="debug")
977
978    def getGlobalOption(self, option, ignore=0) :
979        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
980        try :
981            return self.config.get("global", option, raw=1)
982        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
983            if not ignore :
984                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
985
986    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
987        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
988        globaloption = self.getGlobalOption(option, ignore=1)
989        try :
990            return self.config.get(printqueuename, option, raw=1)
991        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
992            if globaloption is not None :
993                return globaloption
994            elif not ignore :
995                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
996
997    def enumBranches(self, printqueuename, branchtype="tee") :
998        """Returns the list of branchtypes branches for a particular section's."""
999        branchbasename = "%s_" % branchtype.lower()
1000        try :
1001            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
1002        except ConfigParser.NoSectionError, msg :
1003            raise ConfigError, "Invalid configuration file : %s" % msg
1004        try :
1005            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
1006        except ConfigParser.NoSectionError, msg :
1007            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
1008            sectionbranches = []
1009        branches = {}
1010        for (k, v) in globalbranches :
1011            value = v.strip()
1012            if value :
1013                branches[k] = value
1014        for (k, v) in sectionbranches :
1015            value = v.strip()
1016            if value :
1017                branches[k] = value # overwrite any global option or set a new value
1018            else :
1019                del branches[k] # empty value disables a global option
1020        return branches
1021
1022    def discoverOtherBackends(self) :
1023        """Discovers the other CUPS backends.
1024
1025           Executes each existing backend in turn in device enumeration mode.
1026           Returns the list of available backends.
1027        """
1028        # Unfortunately this method can't output any debug information
1029        # to stdout or stderr, else CUPS considers that the device is
1030        # not available.
1031        available = []
1032        (directory, myname) = os.path.split(sys.argv[0])
1033        if not directory :
1034            directory = "./"
1035        tmpdir = tempfile.gettempdir()
1036        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
1037        if os.path.exists(lockfilename) :
1038            lockfile = open(lockfilename, "r")
1039            pid = int(lockfile.read())
1040            lockfile.close()
1041            try :
1042                # see if the pid contained in the lock file is still running
1043                os.kill(pid, 0)
1044            except OSError, error :
1045                if error.errno != errno.EPERM :
1046                    # process doesn't exist anymore
1047                    os.remove(lockfilename)
1048
1049        if not os.path.exists(lockfilename) :
1050            lockfile = open(lockfilename, "w")
1051            lockfile.write("%i" % self.pid)
1052            lockfile.close()
1053            allbackends = [ os.path.join(directory, b) \
1054                                for b in os.listdir(directory)
1055                                    if os.access(os.path.join(directory, b), os.X_OK) \
1056                                        and (b != myname)]
1057            for backend in allbackends :
1058                answer = os.popen(backend, "r")
1059                try :
1060                    devices = [deviceline.strip() for deviceline in answer.readlines()]
1061                except :
1062                    devices = []
1063                status = answer.close()
1064                if status is None :
1065                    for d in devices :
1066                        # each line is of the form :
1067                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
1068                        # so we have to decompose it carefully
1069                        fdevice = cStringIO.StringIO(d)
1070                        tokenizer = shlex.shlex(fdevice)
1071                        tokenizer.wordchars = tokenizer.wordchars + \
1072                                                        r".:,?!~/\_$*-+={}[]()#"
1073                        arguments = []
1074                        while 1 :
1075                            token = tokenizer.get_token()
1076                            if token :
1077                                arguments.append(token)
1078                            else :
1079                                break
1080                        fdevice.close()
1081                        try :
1082                            (devicetype, device, name, fullname) = arguments
1083                        except ValueError :
1084                            pass    # ignore this 'bizarre' device
1085                        else :
1086                            if name.startswith('"') and name.endswith('"') :
1087                                name = name[1:-1]
1088                            if fullname.startswith('"') and fullname.endswith('"') :
1089                                fullname = fullname[1:-1]
1090                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
1091                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
1092            os.remove(lockfilename)
1093        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
1094                             % (self.myname, self.MyName, self.MyName))
1095        return available
1096
1097    def initBackend(self) :
1098        """Initializes the backend's attributes."""
1099        self.JobId = sys.argv[1].strip()
1100        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
1101        self.Title = sys.argv[3].strip()
1102        self.Copies = int(sys.argv[4].strip())
1103        self.Options = sys.argv[5].strip()
1104        if len(sys.argv) == 7 :
1105            self.InputFile = sys.argv[6] # read job's datas from file
1106        else :
1107            self.InputFile = None        # read job's datas from stdin
1108        self.PrinterName = os.environ.get("PRINTER", "")
1109        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
1110        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
1111
1112        # check that the DEVICE_URI environment variable's value is
1113        # prefixed with self.myname otherwise don't touch it.
1114        # If this is the case, we have to remove the prefix from
1115        # the environment before launching the real backend
1116        muststartwith = "%s:" % self.myname
1117        device_uri = os.environ.get("DEVICE_URI", "")
1118        if device_uri.startswith(muststartwith) :
1119            fulldevice_uri = device_uri[:]
1120            device_uri = fulldevice_uri[len(muststartwith):]
1121            for dummy in range(2) :
1122                if device_uri.startswith("/") :
1123                    device_uri = device_uri[1:]
1124        try :
1125            (backend, dummy) = device_uri.split(":", 1)
1126        except ValueError :
1127            if not device_uri :
1128                self.logDebug("Not attached to an existing print queue.")
1129                backend = ""
1130            else :
1131                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
1132
1133        self.RealBackend = backend
1134        self.DeviceURI = device_uri
1135
1136        try :
1137            cupsserver = CUPS() # TODO : username and password and/or encryption
1138            answer = cupsserver.getJobAttributes(self.JobId)
1139            if answer is None :  # probably connection refused because we
1140                raise ValueError # don't hande unix domain sockets yet.
1141            self.ControlFile = "NotUsedAnymore"
1142        except :
1143            (ippfilename, answer) = self.parseIPPRequestFile()
1144            self.ControlFile = ippfilename
1145
1146        try :
1147            john = answer.job["job-originating-host-name"]
1148        except (KeyError, AttributeError) :
1149            try :
1150                john = answer.operation["job-originating-host-name"]
1151            except (KeyError, AttributeError) :
1152                john = (None, None)
1153        if type(john) == type([]) :
1154            john = john[-1]
1155        (dummy, self.ClientHost) = john
1156        try :
1157            jbing = answer.job["job-billing"]
1158        except (KeyError, AttributeError) :
1159            jbing = (None, None)
1160        if type(jbing) == type([]) :
1161            jbing = jbing[-1]
1162        (dummy, self.JobBilling) = jbing
1163
1164    def parseIPPRequestFile(self) :
1165        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
1166        requestroot = os.environ.get("CUPS_REQUESTROOT")
1167        if requestroot is None :
1168            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
1169            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
1170        if (len(self.JobId) < 5) and self.JobId.isdigit() :
1171            ippmessagefile = "c%05i" % int(self.JobId)
1172        else :
1173            ippmessagefile = "c%s" % self.JobId
1174        ippmessagefile = os.path.join(requestroot, ippmessagefile)
1175        ippmessage = {}
1176        try :
1177            ippdatafile = open(ippmessagefile)
1178        except :
1179            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
1180        else :
1181            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
1182            try :
1183                ippmessage = IPPRequest(ippdatafile.read())
1184                ippmessage.parse()
1185            except IPPError, msg :
1186                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
1187            else :
1188                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
1189            ippdatafile.close()
1190        return (ippmessagefile, ippmessage)
1191
1192    def exportAttributes(self) :
1193        """Exports our backend's attributes to the environment."""
1194        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
1195        os.environ["TEAPRINTERNAME"] = self.PrinterName
1196        os.environ["TEADIRECTORY"] = self.Directory
1197        os.environ["TEADATAFILE"] = self.DataFile
1198        os.environ["TEAJOBSIZE"] = str(self.JobSize)
1199        os.environ["TEAMD5SUM"] = self.JobMD5Sum
1200        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
1201        os.environ["TEAJOBID"] = self.JobId
1202        os.environ["TEAUSERNAME"] = self.UserName
1203        os.environ["TEATITLE"] = self.Title
1204        os.environ["TEACOPIES"] = str(self.Copies)
1205        os.environ["TEAOPTIONS"] = self.Options
1206        os.environ["TEAINPUTFILE"] = self.InputFile or ""
1207        os.environ["TEABILLING"] = self.JobBilling or ""
1208        os.environ["TEACONTROLFILE"] = self.ControlFile
1209
1210    def saveDatasAndCheckSum(self) :
1211        """Saves the input datas into a static file."""
1212        self.logDebug("Duplicating data stream into %s" % self.DataFile)
1213        mustclose = 0
1214        if self.InputFile is not None :
1215            infile = open(self.InputFile, "rb")
1216            mustclose = 1
1217        else :
1218            infile = sys.stdin
1219
1220        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
1221                                                 ignore=1)
1222        if filtercommand :
1223            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
1224            filteroutput = "%s.filteroutput" % self.DataFile
1225            outf = open(filteroutput, "wb")
1226            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
1227            outf.close()
1228            self.logDebug("Filter's output status : %s" % repr(filterstatus))
1229            if mustclose :
1230                infile.close()
1231            infile = open(filteroutput, "rb")
1232            mustclose = 1
1233        else :
1234            self.logDebug("Data stream will be used as-is (no filter defined)")
1235
1236        CHUNK = 64*1024         # read 64 Kb at a time
1237        dummy = 0
1238        sizeread = 0
1239        checksum = md5.new()
1240        outfile = open(self.DataFile, "wb")
1241        while 1 :
1242            data = infile.read(CHUNK)
1243            if not data :
1244                break
1245            sizeread += len(data)
1246            outfile.write(data)
1247            checksum.update(data)
1248            if not (dummy % 32) : # Only display every 2 Mb
1249                self.logDebug("%s bytes saved..." % sizeread)
1250            dummy += 1
1251        outfile.close()
1252
1253        if filtercommand :
1254            self.logDebug("Removing filter's output file %s" % filteroutput)
1255            try :
1256                os.remove(filteroutput)
1257            except :
1258                pass
1259
1260        if mustclose :
1261            infile.close()
1262
1263        self.logDebug("%s bytes saved..." % sizeread)
1264        self.JobSize = sizeread
1265        self.JobMD5Sum = checksum.hexdigest()
1266        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
1267        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
1268
1269    def cleanUp(self) :
1270        """Cleans up the place."""
1271        self.logDebug("Cleaning up...")
1272        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
1273            and os.path.exists(self.DataFile) :
1274            try :
1275                os.remove(self.DataFile)
1276            except OSError, msg :
1277                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
1278
1279        if self.LockFile is not None :
1280            self.logDebug("Removing lock...")
1281            try :
1282                fcntl.lockf(self.LockFile, fcntl.LOCK_UN)
1283                self.LockFile.close()
1284            except :
1285                self.logInfo("Problem while unlocking.", "error")
1286            else :
1287                self.logDebug("Lock removed.")
1288        self.logDebug("Clean.")
1289
1290    def runBranches(self) :
1291        """Launches each hook defined for the current print queue."""
1292        self.isCancelled = 0    # did a prehook cancel the print job ?
1293        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
1294        self.pipes = { 0: (0, 1) }
1295        branches = self.enumBranches(self.PrinterName, "prehook")
1296        for b in branches.keys() :
1297            self.pipes[b.split("_", 1)[1]] = os.pipe()
1298        retcode = self.runCommands("prehook", branches, serialize)
1299        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
1300            os.close(p[1][1])
1301        if not self.isCancelled :
1302            if self.RealBackend :
1303                retcode = self.launchOriginalBackend()
1304                if retcode :
1305                    onfail = self.getPrintQueueOption(self.PrinterName, \
1306                                                      "onfail", ignore=1)
1307                    if onfail :
1308                        self.logDebug("Launching onfail script %s" % onfail)
1309                        os.system(onfail)
1310
1311            os.environ["TEASTATUS"] = str(retcode)
1312            branches = self.enumBranches(self.PrinterName, "posthook")
1313            if self.runCommands("posthook", branches, serialize) :
1314                self.logInfo("An error occured during the execution of posthooks.", "warn")
1315        else :
1316            retcode = 5 # Job cancelled, for CUPS.
1317        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
1318            os.close(p[1][0])
1319        if not retcode :
1320            self.logInfo("OK")
1321        else :
1322            self.logInfo("An error occured, please check CUPS' error_log file.")
1323        return retcode
1324
1325    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
1326        """Launches a command with stdio redirected."""
1327        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
1328        pid = os.fork()
1329        if pid == 0 :
1330            if stdin != 0 :
1331                os.dup2(stdin, 0)
1332                os.close(stdin)
1333            if stdout != 1 :
1334                os.dup2(stdout, 1)
1335                os.close(stdout)
1336            try :
1337                os.execl("/bin/sh", "sh", "-c", cmd)
1338            except OSError, msg :
1339                self.logDebug("execl() failed: %s" % msg)
1340            os._exit(-1)
1341        status = os.waitpid(pid, 0)[1]
1342        if os.WIFEXITED(status) :
1343            return os.WEXITSTATUS(status)
1344        return -1
1345
1346    def runCommand(self, branch, command) :
1347        """Runs a particular branch command."""
1348        # Code contributed by Peter Stuge on June 7th 2005
1349        self.logDebug("Launching %s : %s" % (branch, command))
1350        btype, bname = branch.split("_", 1)
1351        if bname not in self.pipes.keys() :
1352            bname = 0
1353        if btype == "prehook" :
1354            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
1355        else :
1356            return self.stdioRedirSystem(command, self.pipes[bname][0])
1357
1358    def runCommands(self, btype, branches, serialize) :
1359        """Runs the commands for a particular branch type."""
1360        exitcode = 0
1361        btype = btype.lower()
1362        btypetitle = btype.title()
1363        branchlist = branches.keys()
1364        branchlist.sort()
1365        if serialize :
1366            self.logDebug("Begin serialized %ss" % btypetitle)
1367            for branch in branchlist :
1368                retcode = self.runCommand(branch, branches[branch])
1369                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
1370                if retcode :
1371                    if (btype == "prehook") and (retcode == 255) : # -1
1372                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
1373                        self.isCancelled = 1
1374                    else :
1375                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
1376                        exitcode = 1
1377            self.logDebug("End serialized %ss" % btypetitle)
1378        else :
1379            self.logDebug("Begin forked %ss" % btypetitle)
1380            pids = {}
1381            for branch in branchlist :
1382                pid = os.fork()
1383                if pid :
1384                    pids[branch] = pid
1385                else :
1386                    os._exit(self.runCommand(branch, branches[branch]))
1387            for (branch, pid) in pids.items() :
1388                retcode = os.waitpid(pid, 0)[1]
1389                if os.WIFEXITED(retcode) :
1390                    retcode = os.WEXITSTATUS(retcode)
1391                else :
1392                    retcode = -1
1393                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
1394                if retcode :
1395                    if (btype == "prehook") and (retcode == 255) : # -1
1396                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
1397                        self.isCancelled = 1
1398                    else :
1399                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
1400                        exitcode = 1
1401            self.logDebug("End forked %ss" % btypetitle)
1402        return exitcode
1403
1404    def launchOriginalBackend(self) :
1405        """Launches the original backend, optionally retrying if needed."""
1406        number = 1
1407        delay = 0
1408        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
1409        if retry is not None :
1410            try :
1411                (number, delay) = [int(p) for p in retry.strip().split(",")]
1412            except (ValueError, AttributeError, TypeError) :
1413                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
1414                number = 1
1415                delay = 0
1416
1417        loopcount = 1
1418        while 1 :
1419            retcode = self.runOriginalBackend()
1420            if not retcode :
1421                break
1422            else :
1423                if (not number) or (loopcount < number) :
1424                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
1425                    time.sleep(delay)
1426                    loopcount += 1
1427                else :
1428                    break
1429        return retcode
1430
1431    def runOriginalBackend(self) :
1432        """Launches the original backend."""
1433        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1434        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1435        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1436
1437        pid = os.fork()
1438        if pid == 0 :
1439            if self.InputFile is None :
1440                f = open(self.DataFile, "rb")
1441                os.dup2(f.fileno(), 0)
1442                f.close()
1443            else :
1444                arguments[6] = self.DataFile # in case a tea4cups filter was applied
1445            try :
1446                os.execve(originalbackend, arguments, os.environ)
1447            except OSError, msg :
1448                self.logDebug("execve() failed: %s" % msg)
1449            os._exit(-1)
1450        killed = 0
1451        status = -1
1452        while status == -1 :
1453            try :
1454                status = os.waitpid(pid, 0)[1]
1455            except OSError, (err, msg) :
1456                if err == 4 :
1457                    killed = 1
1458        if os.WIFEXITED(status) :
1459            status = os.WEXITSTATUS(status)
1460            if status :
1461                self.logInfo("CUPS backend %s returned %d." % (originalbackend, \
1462                                                             status), "error")
1463            return status
1464        elif not killed :
1465            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
1466                                                              "error")
1467            return -1
1468        else :
1469            return 1
1470
1471if __name__ == "__main__" :
1472    # This is a CUPS backend, we should act and die like a CUPS backend
1473    wrapper = CupsBackend()
1474    if len(sys.argv) == 1 :
1475        print "\n".join(wrapper.discoverOtherBackends())
1476        sys.exit(0)
1477    elif len(sys.argv) not in (6, 7) :
1478        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1479                              % sys.argv[0])
1480        sys.exit(1)
1481    else :
1482        returncode = 1
1483        try :
1484            try :
1485                wrapper.readConfig()
1486                wrapper.initBackend()
1487                wrapper.waitForLock()
1488                wrapper.saveDatasAndCheckSum()
1489                wrapper.exportAttributes()
1490                returncode = wrapper.runBranches()
1491            except SystemExit, e :
1492                returncode = e.code
1493            except KeyboardInterrupt :
1494                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
1495            except :
1496                import traceback
1497                lines = []
1498                for errline in traceback.format_exception(*sys.exc_info()) :
1499                    lines.extend([l for l in errline.split("\n") if l])
1500                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
1501                                                              wrapper.pid, l) \
1502                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
1503                sys.stderr.write(errormessage)
1504                sys.stderr.flush()
1505                returncode = 1
1506        finally :
1507            wrapper.cleanUp()
1508        sys.exit(returncode)
Note: See TracBrowser for help on using the browser.