root / tea4cups / trunk / tea4cups @ 3512

Revision 3512, 57.8 kB (checked in by jerome, 14 years ago)

Marks cancelled jobs as cancelled in CUPS.

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