root / tea4cups / trunk / tea4cups @ 683

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

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

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