root / tea4cups / trunk / tea4cups @ 688

Revision 688, 58.5 kB (checked in by jerome, 18 years ago)

Now serializes accesses to the same device by different print queues
or print servers, just like 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
[688]85import fcntl
[677]86import urllib2
[653]87from struct import pack, unpack
[565]88
[688]89__version__ = "3.12_unofficial"
[585]90
[570]91class TeeError(Exception):
[576]92    """Base exception for Tea4CUPS related stuff."""
[570]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__
[641]99
100class ConfigError(TeeError) :
[570]101    """Configuration related exceptions."""
[641]102    pass
103
104class IPPError(TeeError) :
[581]105    """IPP related exceptions."""
[641]106    pass
[677]107   
108IPP_VERSION = "1.1"     # default version number
[641]109
[677]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   
[646]377class IPPRequest :
[677]378    """A class for IPP requests."""
[646]379    attributes_types = ("operation", "job", "printer", "unsupported", \
380                                     "subscription", "event_notification")
[677]381    def __init__(self, data="", version=IPP_VERSION, 
382                                operation_id=None, \
383                                request_id=None, \
384                                debug=False) :
[646]385        """Initializes an IPP Message object.
386       
[635]387           Parameters :
[646]388           
389             data : the complete IPP Message's content.
[635]390             debug : a boolean value to output debug info on stderr.
391        """
[631]392        self.debug = debug
[646]393        self._data = data
[677]394        self.parsed = False
[646]395       
396        # Initializes message
[677]397        self.setVersion(version)               
398        self.setOperationId(operation_id)
399        self.setRequestId(request_id)
[646]400        self.data = ""
401       
[638]402        for attrtype in self.attributes_types :
[677]403            setattr(self, "_%s_attributes" % attrtype, [[]])
404       
[646]405        # Initialize tags   
406        self.tags = [ None ] * 256 # by default all tags reserved
407       
[581]408        # Delimiter tags
[646]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"
[676]415        self.tags[0x07] = "event_notification-attributes-tag"
[646]416       
[581]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"
[646]422        self.tags[0x15] = "not-settable"
423        self.tags[0x16] = "delete-attribute"
424        self.tags[0x17] = "admin-define"
425 
[581]426        # integer values
427        self.tags[0x20] = "generic-integer"
428        self.tags[0x21] = "integer"
429        self.tags[0x22] = "boolean"
430        self.tags[0x23] = "enum"
[646]431       
[581]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"
[646]437        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
[581]438        self.tags[0x35] = "textWithLanguage"
439        self.tags[0x36] = "nameWithLanguage"
[646]440        self.tags[0x37] = "endCollection"
441       
[581]442        # character strings
[646]443        self.tags[0x40] = "generic-character-string"
[581]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"
[646]452        self.tags[0x4a] = "memberAttrName"
453       
454        # Reverse mapping to generate IPP messages
[677]455        self.tagvalues = {}
[646]456        for i in range(len(self.tags)) :
457            value = self.tags[i]
458            if value is not None :
[677]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)
[646]485       
[655]486    def logDebug(self, msg) :   
[633]487        """Prints a debug message."""
488        if self.debug :
489            sys.stderr.write("%s\n" % msg)
490            sys.stderr.flush()
[646]491           
[677]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(".")]
[646]505       
[677]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       
[646]514    def dump(self) :   
515        """Generates an IPP Message.
516       
517           Returns the message as a string of text.
518        """   
[653]519        mybuffer = []
[677]520        if None not in (self.version, self.operation_id) :
[653]521            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
522            mybuffer.append(pack(">H", self.operation_id))
[677]523            mybuffer.append(pack(">I", self.request_id or 1))
[646]524            for attrtype in self.attributes_types :
[677]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"]))
[653]548        mybuffer.append(self.data)   
549        return "".join(mybuffer)
[646]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
[677]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])
[646]562        self.position = 8
[677]563        endofattributes = self.tagvalues["end-of-attributes-tag"]
564        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
565        nulloffset = lambda : 0
[646]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 :
[677]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       
[646]578                tag = ord(self._data[self.position])
[677]579                if tag == oldtag :
580                    self._curattributes.append([])
[646]581        except IndexError :
582            raise IPPError, "Unexpected end of IPP message."
583           
584        self.data = self._data[self.position+1:]           
[677]585        self.parsed = True
[646]586       
587    def parseTag(self) :   
[581]588        """Extracts information from an IPP tag."""
589        pos = self.position
[646]590        tagtype = self.tags[ord(self._data[pos])]
[581]591        pos += 1
592        posend = pos2 = pos + 2
[646]593        namelength = unpack(">H", self._data[pos:pos2])[0]
[581]594        if not namelength :
[631]595            name = self._curname
[646]596        else :   
[581]597            posend += namelength
[646]598            self._curname = name = self._data[pos2:posend]
[581]599        pos2 = posend + 2
[646]600        valuelength = unpack(">H", self._data[posend:pos2])[0]
[581]601        posend = pos2 + valuelength
[646]602        value = self._data[pos2:posend]
[633]603        if tagtype in ("integer", "enum") :
[634]604            value = unpack(">I", value)[0]
[646]605        elif tagtype == "boolean" :   
606            value = ord(value)
[677]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)]))
[655]615        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
[581]616        return posend - self.position
[646]617       
618    def operation_attributes_tag(self) : 
[581]619        """Indicates that the parser enters into an operation-attributes-tag group."""
[677]620        self._curattributes = self._operation_attributes
[581]621        return self.parseTag()
[646]622       
623    def job_attributes_tag(self) : 
[635]624        """Indicates that the parser enters into a job-attributes-tag group."""
[677]625        self._curattributes = self._job_attributes
[581]626        return self.parseTag()
[646]627       
628    def printer_attributes_tag(self) : 
[635]629        """Indicates that the parser enters into a printer-attributes-tag group."""
[677]630        self._curattributes = self._printer_attributes
[581]631        return self.parseTag()
[646]632       
633    def unsupported_attributes_tag(self) : 
[635]634        """Indicates that the parser enters into an unsupported-attributes-tag group."""
[677]635        self._curattributes = self._unsupported_attributes
[635]636        return self.parseTag()
[646]637       
638    def subscription_attributes_tag(self) : 
639        """Indicates that the parser enters into a subscription-attributes-tag group."""
[677]640        self._curattributes = self._subscription_attributes
[646]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."""
[677]645        self._curattributes = self._event_notification_attributes
[646]646        return self.parseTag()
[677]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       
[641]841
842class FakeConfig :
[570]843    """Fakes a configuration file parser."""
844    def get(self, section, option, raw=0) :
[626]845        """Fakes the retrieval of an option."""
[570]846        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
[641]847
[679]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   
[568]882class CupsBackend :
883    """Base class for tools with no database access."""
884    def __init__(self) :
885        """Initializes the CUPS backend wrapper."""
[588]886        signal.signal(signal.SIGTERM, signal.SIG_IGN)
887        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
[577]888        self.MyName = "Tea4CUPS"
889        self.myname = "tea4cups"
890        self.pid = os.getpid()
[679]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
[688]912        self.LockFile = None
[641]913
[688]914    def waitForLock(self) :   
915        """Waits until we can acquire the lock file."""
916        self.logDebug("Waiting for lock...")
917        lockfilename = self.DeviceURI.replace("/", ".")
918        lockfilename = lockfilename.replace(":", ".")
919        lockfilename = lockfilename.replace("?", ".")
920        lockfilename = lockfilename.replace("&", ".")
921        lockfilename = lockfilename.replace("@", ".")
922        lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, lockfilename))
923        stillrunning = True
924        while 1 :
925            self.LockFile = open(lockfilename, "a+")
926            fcntl.lockf(self.LockFile, fcntl.LOCK_EX) # Will wait until lock available
927            self.LockFile.seek(0, 0)     
928            try : 
929                oldpid = int(self.LockFile.readlines()[-1].strip())
930            except :   
931                stillrunning = False
932            else :   
933                try :
934                    os.kill(oldpid, 0)
935                except OSError :   
936                    stillrunning = False
937            if not stillrunning :       
938                self.LockFile.truncate(0)
939                self.LockFile.seek(0, 0)
940                self.LockFile.write("%s\n" % self.pid)
941                self.LockFile.flush()
942                break
943            else :   
944                time.sleep(0.1)
945        self.logDebug("Lock acquired.")
946       
[641]947    def readConfig(self) :
[626]948        """Reads the configuration file."""
[641]949        confdir = os.environ.get("CUPS_SERVERROOT", ".")
[577]950        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
[570]951        if os.path.isfile(self.conffile) :
952            self.config = ConfigParser.ConfigParser()
953            self.config.read([self.conffile])
[679]954            self.debug = isTrue(self.getGlobalOption("debug", ignore=1))
[641]955        else :
[570]956            self.config = FakeConfig()
957            self.debug = 1      # no config, so force debug mode !
[641]958
959    def logInfo(self, message, level="info") :
[574]960        """Logs a message to CUPS' error_log file."""
[640]961        try :
[664]962            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
[640]963            sys.stderr.flush()
964        except IOError :
965            pass
[641]966
967    def logDebug(self, message) :
[585]968        """Logs something to debug output if debug is enabled."""
969        if self.debug :
970            self.logInfo(message, level="debug")
[641]971
972    def getGlobalOption(self, option, ignore=0) :
[570]973        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
974        try :
975            return self.config.get("global", option, raw=1)
[641]976        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[577]977            if not ignore :
[570]978                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
[641]979
980    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
[570]981        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
982        globaloption = self.getGlobalOption(option, ignore=1)
983        try :
[574]984            return self.config.get(printqueuename, option, raw=1)
[641]985        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[570]986            if globaloption is not None :
987                return globaloption
[577]988            elif not ignore :
[574]989                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
[641]990
[597]991    def enumBranches(self, printqueuename, branchtype="tee") :
992        """Returns the list of branchtypes branches for a particular section's."""
993        branchbasename = "%s_" % branchtype.lower()
[573]994        try :
[627]995            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
[641]996        except ConfigParser.NoSectionError, msg :
[579]997            raise ConfigError, "Invalid configuration file : %s" % msg
[573]998        try :
[627]999            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
[641]1000        except ConfigParser.NoSectionError, msg :
[583]1001            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
[573]1002            sectionbranches = []
1003        branches = {}
1004        for (k, v) in globalbranches :
1005            value = v.strip()
1006            if value :
1007                branches[k] = value
[641]1008        for (k, v) in sectionbranches :
[573]1009            value = v.strip()
1010            if value :
1011                branches[k] = value # overwrite any global option or set a new value
[641]1012            else :
[573]1013                del branches[k] # empty value disables a global option
1014        return branches
[641]1015
1016    def discoverOtherBackends(self) :
[568]1017        """Discovers the other CUPS backends.
[641]1018
[568]1019           Executes each existing backend in turn in device enumeration mode.
1020           Returns the list of available backends.
1021        """
[569]1022        # Unfortunately this method can't output any debug information
1023        # to stdout or stderr, else CUPS considers that the device is
1024        # not available.
[568]1025        available = []
[569]1026        (directory, myname) = os.path.split(sys.argv[0])
[588]1027        if not directory :
1028            directory = "./"
[568]1029        tmpdir = tempfile.gettempdir()
[569]1030        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
[568]1031        if os.path.exists(lockfilename) :
1032            lockfile = open(lockfilename, "r")
1033            pid = int(lockfile.read())
1034            lockfile.close()
1035            try :
1036                # see if the pid contained in the lock file is still running
1037                os.kill(pid, 0)
[679]1038            except OSError, error :
1039                if error.errno != errno.EPERM :
[568]1040                    # process doesn't exist anymore
1041                    os.remove(lockfilename)
[641]1042
[568]1043        if not os.path.exists(lockfilename) :
1044            lockfile = open(lockfilename, "w")
[577]1045            lockfile.write("%i" % self.pid)
[568]1046            lockfile.close()
[569]1047            allbackends = [ os.path.join(directory, b) \
[641]1048                                for b in os.listdir(directory)
[569]1049                                    if os.access(os.path.join(directory, b), os.X_OK) \
[641]1050                                        and (b != myname)]
1051            for backend in allbackends :
[568]1052                answer = os.popen(backend, "r")
1053                try :
[679]1054                    devices = [deviceline.strip() for deviceline in answer.readlines()]
[641]1055                except :
[568]1056                    devices = []
1057                status = answer.close()
1058                if status is None :
1059                    for d in devices :
[641]1060                        # each line is of the form :
[568]1061                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
1062                        # so we have to decompose it carefully
1063                        fdevice = cStringIO.StringIO(d)
1064                        tokenizer = shlex.shlex(fdevice)
1065                        tokenizer.wordchars = tokenizer.wordchars + \
1066                                                        r".:,?!~/\_$*-+={}[]()#"
1067                        arguments = []
1068                        while 1 :
1069                            token = tokenizer.get_token()
1070                            if token :
1071                                arguments.append(token)
1072                            else :
1073                                break
1074                        fdevice.close()
1075                        try :
1076                            (devicetype, device, name, fullname) = arguments
[641]1077                        except ValueError :
[568]1078                            pass    # ignore this 'bizarre' device
[641]1079                        else :
[568]1080                            if name.startswith('"') and name.endswith('"') :
1081                                name = name[1:-1]
1082                            if fullname.startswith('"') and fullname.endswith('"') :
1083                                fullname = fullname[1:-1]
[577]1084                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
1085                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
[568]1086            os.remove(lockfilename)
[599]1087        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
1088                             % (self.myname, self.MyName, self.MyName))
[568]1089        return available
[641]1090
1091    def initBackend(self) :
[577]1092        """Initializes the backend's attributes."""
[677]1093        self.JobId = sys.argv[1].strip()
1094        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
1095        self.Title = sys.argv[3].strip()
1096        self.Copies = int(sys.argv[4].strip())
1097        self.Options = sys.argv[5].strip()
1098        if len(sys.argv) == 7 :
1099            self.InputFile = sys.argv[6] # read job's datas from file
1100        else :
1101            self.InputFile = None        # read job's datas from stdin
1102        self.PrinterName = os.environ.get("PRINTER", "")
1103        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
1104        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
1105       
[641]1106        # check that the DEVICE_URI environment variable's value is
[577]1107        # prefixed with self.myname otherwise don't touch it.
[641]1108        # If this is the case, we have to remove the prefix from
1109        # the environment before launching the real backend
[577]1110        muststartwith = "%s:" % self.myname
1111        device_uri = os.environ.get("DEVICE_URI", "")
1112        if device_uri.startswith(muststartwith) :
1113            fulldevice_uri = device_uri[:]
1114            device_uri = fulldevice_uri[len(muststartwith):]
[679]1115            for dummy in range(2) :
[641]1116                if device_uri.startswith("/") :
[583]1117                    device_uri = device_uri[1:]
[577]1118        try :
[679]1119            (backend, dummy) = device_uri.split(":", 1)
[641]1120        except ValueError :
[583]1121            if not device_uri :
[600]1122                self.logDebug("Not attached to an existing print queue.")
[583]1123                backend = ""
[641]1124            else :
[583]1125                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
[641]1126
[577]1127        self.RealBackend = backend
1128        self.DeviceURI = device_uri
[677]1129       
1130        try :
1131            cupsserver = CUPS() # TODO : username and password and/or encryption
1132            answer = cupsserver.getJobAttributes(self.JobId)
1133            self.ControlFile = "NotUsedAnymore"
1134        except :   
1135            (ippfilename, answer) = self.parseIPPRequestFile()
1136            self.ControlFile = ippfilename
1137       
1138        try :
1139            john = answer.job["job-originating-host-name"]
1140        except KeyError :   
1141            try :
1142                john = answer.operation["job-originating-host-name"]
1143            except KeyError :   
1144                john = (None, None)
[652]1145        if type(john) == type([]) :                         
1146            john = john[-1]
[679]1147        (dummy, self.ClientHost) = john                         
[677]1148        try :       
1149            jbing = answer.job["job-billing"]
1150        except KeyError :   
1151            jbing = (None, None)
1152        if type(jbing) == type([]) : 
1153            jbing = jbing[-1]
[679]1154        (dummy, self.JobBilling) = jbing
[688]1155       
[646]1156    def parseIPPRequestFile(self) :
[624]1157        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
[677]1158        requestroot = os.environ.get("CUPS_REQUESTROOT")
1159        if requestroot is None :
[679]1160            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
[677]1161            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
[582]1162        if (len(self.JobId) < 5) and self.JobId.isdigit() :
1163            ippmessagefile = "c%05i" % int(self.JobId)
[641]1164        else :
[582]1165            ippmessagefile = "c%s" % self.JobId
1166        ippmessagefile = os.path.join(requestroot, ippmessagefile)
1167        ippmessage = {}
1168        try :
1169            ippdatafile = open(ippmessagefile)
[641]1170        except :
[582]1171            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
[641]1172        else :
[582]1173            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
1174            try :
[646]1175                ippmessage = IPPRequest(ippdatafile.read())
1176                ippmessage.parse()
[641]1177            except IPPError, msg :
[582]1178                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
[641]1179            else :
[582]1180                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
1181            ippdatafile.close()
[615]1182        return (ippmessagefile, ippmessage)
[641]1183
1184    def exportAttributes(self) :
[577]1185        """Exports our backend's attributes to the environment."""
1186        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
1187        os.environ["TEAPRINTERNAME"] = self.PrinterName
1188        os.environ["TEADIRECTORY"] = self.Directory
1189        os.environ["TEADATAFILE"] = self.DataFile
[579]1190        os.environ["TEAJOBSIZE"] = str(self.JobSize)
[577]1191        os.environ["TEAMD5SUM"] = self.JobMD5Sum
[582]1192        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
[577]1193        os.environ["TEAJOBID"] = self.JobId
1194        os.environ["TEAUSERNAME"] = self.UserName
1195        os.environ["TEATITLE"] = self.Title
1196        os.environ["TEACOPIES"] = str(self.Copies)
1197        os.environ["TEAOPTIONS"] = self.Options
1198        os.environ["TEAINPUTFILE"] = self.InputFile or ""
[615]1199        os.environ["TEABILLING"] = self.JobBilling or ""
1200        os.environ["TEACONTROLFILE"] = self.ControlFile
[641]1201
[577]1202    def saveDatasAndCheckSum(self) :
1203        """Saves the input datas into a static file."""
[583]1204        self.logDebug("Duplicating data stream into %s" % self.DataFile)
[577]1205        mustclose = 0
1206        if self.InputFile is not None :
1207            infile = open(self.InputFile, "rb")
1208            mustclose = 1
[641]1209        else :
[577]1210            infile = sys.stdin
[659]1211           
1212        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
1213                                                 ignore=1)
1214        if filtercommand :                                                 
1215            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
1216            filteroutput = "%s.filteroutput" % self.DataFile
1217            outf = open(filteroutput, "wb")
1218            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
1219            outf.close()
1220            self.logDebug("Filter's output status : %s" % repr(filterstatus))
1221            if mustclose :
1222                infile.close()
1223            infile = open(filteroutput, "rb")
1224            mustclose = 1
1225        else :   
1226            self.logDebug("Data stream will be used as-is (no filter defined)")
1227           
[577]1228        CHUNK = 64*1024         # read 64 Kb at a time
1229        dummy = 0
1230        sizeread = 0
1231        checksum = md5.new()
[641]1232        outfile = open(self.DataFile, "wb")
[577]1233        while 1 :
[641]1234            data = infile.read(CHUNK)
[577]1235            if not data :
1236                break
[641]1237            sizeread += len(data)
[577]1238            outfile.write(data)
[641]1239            checksum.update(data)
[577]1240            if not (dummy % 32) : # Only display every 2 Mb
1241                self.logDebug("%s bytes saved..." % sizeread)
[641]1242            dummy += 1
[577]1243        outfile.close()
[659]1244       
1245        if filtercommand :
1246            self.logDebug("Removing filter's output file %s" % filteroutput)
1247            try :
1248                os.remove(filteroutput)
1249            except :   
1250                pass
1251               
[641]1252        if mustclose :
[579]1253            infile.close()
[659]1254           
1255        self.logDebug("%s bytes saved..." % sizeread)
[641]1256        self.JobSize = sizeread
[577]1257        self.JobMD5Sum = checksum.hexdigest()
1258        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
1259        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
[568]1260
[577]1261    def cleanUp(self) :
1262        """Cleans up the place."""
[688]1263        self.logDebug("Cleaning up...")
[679]1264        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
[676]1265            and os.path.exists(self.DataFile) :
1266            try :
1267                os.remove(self.DataFile)
1268            except OSError, msg :   
1269                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
[688]1270        self.logDebug("Removing lock...")
1271        try :
1272            fcntl.flock(self.LockFile, fcntl.LOCK_UN)
1273            self.LockFile.close()
1274            # NB : we don't remove the lock file, since it might already be opened by another process.
1275        except :   
1276            self.logInfo("Problem while removing lock.", "error")
1277        else :   
1278            self.logDebug("Lock removed.")
1279        self.logDebug("Clean.")
[641]1280
1281    def runBranches(self) :
[640]1282        """Launches each hook defined for the current print queue."""
[604]1283        self.isCancelled = 0    # did a prehook cancel the print job ?
[679]1284        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
[643]1285        self.pipes = { 0: (0, 1) }
[640]1286        branches = self.enumBranches(self.PrinterName, "prehook")
[662]1287        for b in branches.keys() :
[640]1288            self.pipes[b.split("_", 1)[1]] = os.pipe()
1289        retcode = self.runCommands("prehook", branches, serialize)
[643]1290        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]1291            os.close(p[1][1])
[679]1292        if not self.isCancelled :
[640]1293            if self.RealBackend :
[676]1294                retcode = self.launchOriginalBackend()
[659]1295                if retcode :
1296                    onfail = self.getPrintQueueOption(self.PrinterName, \
1297                                                      "onfail", ignore=1)
1298                    if onfail :
1299                        self.logDebug("Launching onfail script %s" % onfail)
1300                        os.system(onfail)
[679]1301                       
1302            os.environ["TEASTATUS"] = str(retcode)
1303            branches = self.enumBranches(self.PrinterName, "posthook")
1304            if self.runCommands("posthook", branches, serialize) :
1305                self.logInfo("An error occured during the execution of posthooks.", "warn")
1306               
[643]1307        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]1308            os.close(p[1][0])
1309        if not retcode :
[600]1310            self.logInfo("OK")
[641]1311        else :
[600]1312            self.logInfo("An error occured, please check CUPS' error_log file.")
[640]1313        return retcode
[641]1314
[639]1315    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
1316        """Launches a command with stdio redirected."""
1317        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
[636]1318        pid = os.fork()
1319        if pid == 0 :
[639]1320            if stdin != 0 :
1321                os.dup2(stdin, 0)
1322                os.close(stdin)
1323            if stdout != 1 :
1324                os.dup2(stdout, 1)
1325                os.close(stdout)
1326            try :
1327                os.execl("/bin/sh", "sh", "-c", cmd)
[640]1328            except OSError, msg :
[639]1329                self.logDebug("execl() failed: %s" % msg)
[640]1330            os._exit(-1)
1331        status = os.waitpid(pid, 0)[1]
1332        if os.WIFEXITED(status) :
1333            return os.WEXITSTATUS(status)
1334        return -1
[641]1335
[639]1336    def runCommand(self, branch, command) :
1337        """Runs a particular branch command."""
1338        # Code contributed by Peter Stuge on June 7th 2005
1339        self.logDebug("Launching %s : %s" % (branch, command))
1340        btype, bname = branch.split("_", 1)
[643]1341        if bname not in self.pipes.keys() :
[640]1342            bname = 0
1343        if btype == "prehook" :
[639]1344            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
1345        else :
1346            return self.stdioRedirSystem(command, self.pipes[bname][0])
1347
[641]1348    def runCommands(self, btype, branches, serialize) :
[598]1349        """Runs the commands for a particular branch type."""
[641]1350        exitcode = 0
[598]1351        btype = btype.lower()
1352        btypetitle = btype.title()
[641]1353        branchlist = branches.keys()
[598]1354        branchlist.sort()
1355        if serialize :
[600]1356            self.logDebug("Begin serialized %ss" % btypetitle)
[598]1357            for branch in branchlist :
[640]1358                retcode = self.runCommand(branch, branches[branch])
[598]1359                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
[641]1360                if retcode :
[601]1361                    if (btype == "prehook") and (retcode == 255) : # -1
1362                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]1363                        self.isCancelled = 1
[641]1364                    else :
[601]1365                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
1366                        exitcode = 1
[600]1367            self.logDebug("End serialized %ss" % btypetitle)
[641]1368        else :
[600]1369            self.logDebug("Begin forked %ss" % btypetitle)
[584]1370            pids = {}
[598]1371            for branch in branchlist :
[584]1372                pid = os.fork()
1373                if pid :
1374                    pids[branch] = pid
[641]1375                else :
[640]1376                    os._exit(self.runCommand(branch, branches[branch]))
[584]1377            for (branch, pid) in pids.items() :
[640]1378                retcode = os.waitpid(pid, 0)[1]
[584]1379                if os.WIFEXITED(retcode) :
1380                    retcode = os.WEXITSTATUS(retcode)
[640]1381                else :
1382                    retcode = -1
1383                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
[641]1384                if retcode :
[601]1385                    if (btype == "prehook") and (retcode == 255) : # -1
1386                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]1387                        self.isCancelled = 1
[641]1388                    else :
[640]1389                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
[601]1390                        exitcode = 1
[600]1391            self.logDebug("End forked %ss" % btypetitle)
[584]1392        return exitcode
[641]1393
[676]1394    def launchOriginalBackend(self) :
1395        """Launches the original backend, optionally retrying if needed."""
1396        number = 1
1397        delay = 0
1398        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
1399        if retry is not None :
1400            try :
1401                (number, delay) = [int(p) for p in retry.strip().split(",")]
1402            except (ValueError, AttributeError, TypeError) :   
1403                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
1404                number = 1
1405                delay = 0
1406               
1407        loopcount = 1 
1408        while 1 :           
1409            retcode = self.runOriginalBackend()
1410            if not retcode :
1411                break
1412            else :
1413                if (not number) or (loopcount < number) :
[677]1414                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
[676]1415                    time.sleep(delay)
1416                    loopcount += 1
1417                else :   
1418                    break
1419        return retcode           
1420       
[641]1421    def runOriginalBackend(self) :
[587]1422        """Launches the original backend."""
1423        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
[640]1424        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1425        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1426
1427        pid = os.fork()
1428        if pid == 0 :
1429            if self.InputFile is None :
[653]1430                f = open(self.DataFile, "rb")
[640]1431                os.dup2(f.fileno(), 0)
1432                f.close()
[676]1433            else :   
1434                arguments[6] = self.DataFile # in case a tea4cups filter was applied
[640]1435            try :
[643]1436                os.execve(originalbackend, arguments, os.environ)
[640]1437            except OSError, msg :
1438                self.logDebug("execve() failed: %s" % msg)
1439            os._exit(-1)
[588]1440        killed = 0
1441        status = -1
[640]1442        while status == -1 :
[588]1443            try :
[640]1444                status = os.waitpid(pid, 0)[1]
1445            except OSError, (err, msg) :
[679]1446                if err == 4 :
[640]1447                    killed = 1
[587]1448        if os.WIFEXITED(status) :
[640]1449            status = os.WEXITSTATUS(status)
1450            if status :
[679]1451                self.logInfo("CUPS backend %s returned %d." % (originalbackend, \
[643]1452                                                             status), "error")
[640]1453            return status
[641]1454        elif not killed :
[679]1455            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
[643]1456                                                              "error")
[588]1457            return -1
[641]1458        else :
[587]1459            return 1
[641]1460
1461if __name__ == "__main__" :
[565]1462    # This is a CUPS backend, we should act and die like a CUPS backend
[568]1463    wrapper = CupsBackend()
[565]1464    if len(sys.argv) == 1 :
[568]1465        print "\n".join(wrapper.discoverOtherBackends())
[641]1466        sys.exit(0)
1467    elif len(sys.argv) not in (6, 7) :
[568]1468        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1469                              % sys.argv[0])
1470        sys.exit(1)
[641]1471    else :
[679]1472        returncode = 1
[585]1473        try :
[676]1474            try :
1475                wrapper.readConfig()
1476                wrapper.initBackend()
[688]1477                wrapper.waitForLock()
[676]1478                wrapper.saveDatasAndCheckSum()
1479                wrapper.exportAttributes()
[679]1480                returncode = wrapper.runBranches()
[676]1481            except SystemExit, e :
[679]1482                returncode = e.code
[683]1483            except KeyboardInterrupt :   
1484                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
[676]1485            except :
1486                import traceback
1487                lines = []
[679]1488                for errline in traceback.format_exception(*sys.exc_info()) :
1489                    lines.extend([l for l in errline.split("\n") if l])
1490                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
[676]1491                                                              wrapper.pid, l) \
1492                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
[679]1493                sys.stderr.write(errormessage)
[676]1494                sys.stderr.flush()
[679]1495                returncode = 1
[676]1496        finally :       
[585]1497            wrapper.cleanUp()
[679]1498        sys.exit(returncode)
Note: See TracBrowser for help on using the browser.