root / tea4cups / trunk / tea4cups @ 3438

Revision 3438, 57.7 kB (checked in by jerome, 16 years ago)

Removed trailing spaces.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Rev
RevLine 
[565]1#! /usr/bin/env python
[3438]2# -*- coding: utf-8 -*-
[565]3
[576]4# Tea4CUPS : Tee for CUPS
[565]5#
[691]6# (c) 2005, 2006, 2007 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
[691]31  (c) 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
[679]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.
[3438]37
[679]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.
[3438]42
[679]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 :
[3438]51
[679]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/
[3438]56
[679]57  Now edit the configuration file to suit your needs :
[3438]58
[679]59    $ vi /etc/cupsd/tea4cups.conf
[3438]60
[679]61    NB : you can use emacs as well :-)
[3438]62
[679]63  Finally restart CUPS :
[3438]64
[679]65    $ /etc/init.d/cupsys restart
[3438]66
[679]67  You can now create "Tea4CUPS Managed" print queues from
68  CUPS' web interface, or using lpadmin.
69
[3438]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
[690]89__version__ = "3.13alpha_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
[3438]107
[677]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
[3438]316
[677]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
[3438]343
[677]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
[3438]350
[677]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
[3438]361            attribute.append((key, [value]))
362
[677]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
[3438]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")
[3438]381    def __init__(self, data="", version=IPP_VERSION,
[677]382                                operation_id=None, \
383                                request_id=None, \
384                                debug=False) :
[646]385        """Initializes an IPP Message object.
[3438]386
[635]387           Parameters :
[3438]388
[646]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
[3438]395
[646]396        # Initializes message
[3438]397        self.setVersion(version)
[677]398        self.setOperationId(operation_id)
399        self.setRequestId(request_id)
[646]400        self.data = ""
[3438]401
[638]402        for attrtype in self.attributes_types :
[677]403            setattr(self, "_%s_attributes" % attrtype, [[]])
[3438]404
405        # Initialize tags
[646]406        self.tags = [ None ] * 256 # by default all tags reserved
[3438]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"
[3438]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"
[3438]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"
[3438]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"
[3438]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"
[3438]453
[646]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
[3438]460
461    def __getattr__(self, name) :
[677]462        """Fakes attribute access."""
463        if name in self.attributes_types :
464            return FakeAttribute(self, name)
465        else :
466            raise AttributeError, name
[3438]467
468    def __str__(self) :
[677]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))
[3438]482        if self.data :
[677]483            mybuffer.append("IPP datas : %s" % repr(self.data))
484        return "\n".join(mybuffer)
[3438]485
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()
[3438]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
[3438]500                else :
[677]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(".")]
[3438]505
506    def setOperationId(self, opid) :
[677]507        """Sets the request's operation id."""
508        self.operation_id = opid
[3438]509
510    def setRequestId(self, reqid) :
[677]511        """Sets the request's request id."""
512        self.request_id = reqid
[3438]513
514    def dump(self) :
[646]515        """Generates an IPP Message.
[3438]516
[646]517           Returns the message as a string of text.
[3438]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
[3438]536                            else :
[677]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))
[3438]544                            else :
[677]545                                mybuffer.append(pack(">H", len(val)))
546                                mybuffer.append(val)
547            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
[3438]548        mybuffer.append(self.data)
[653]549        return "".join(mybuffer)
[3438]550
[646]551    def parse(self) :
552        """Parses an IPP Request.
[3438]553
[646]554           NB : Only a subset of RFC2910 is implemented.
555        """
556        self._curname = None
[677]557        self._curattributes = None
[3438]558
[677]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
[3438]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."
[3438]583
584        self.data = self._data[self.position+1:]
[677]585        self.parsed = True
[3438]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
[3438]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]
[3438]605        elif tagtype == "boolean" :
[646]606            value = ord(value)
[3438]607        try :
[677]608            (oldname, oldval) = self._curattributes[-1][-1]
609            if oldname == name :
610                oldval.append((tagtype, value))
[3438]611            else :
[677]612                raise IndexError
[3438]613        except IndexError :
[677]614            self._curattributes[-1].append((name, [(tagtype, value)]))
[655]615        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
[581]616        return posend - self.position
[3438]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()
[3438]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()
[3438]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()
[3438]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()
[3438]637
638    def subscription_attributes_tag(self) :
[646]639        """Indicates that the parser enters into a subscription-attributes-tag group."""
[677]640        self._curattributes = self._subscription_attributes
[646]641        return self.parseTag()
[3438]642
643    def event_notification_attributes_tag(self) :
[646]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()
[3438]647
648
[677]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]
[3438]657        else :
[677]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
[3438]667
668    def getDefaultURL(self) :
[677]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
[3438]677        else :
[677]678            return "http://%s:%s" % (server, port)
[3438]679
[677]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)
[3438]685
686    def nextRequestId(self) :
[677]687        """Increments the current request id and returns the new value."""
688        try :
689            self.requestId += 1
[3438]690        except TypeError :
[677]691            self.requestId = 1
692        return self.requestId
[3438]693
[677]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
[3438]703
[677]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.
[3438]707        """
[677]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 "")
[3438]717            authhandler = urllib2.HTTPBasicAuthHandler(pwmanager)
[677]718            opener = urllib2.build_opener(authhandler)
719            urllib2.install_opener(opener)
[3438]720        self.lastError = None
[677]721        self.lastErrorMessage = None
[3438]722        try :
[677]723            response = urllib2.urlopen(connexion)
[3438]724        except (urllib2.URLError, urllib2.HTTPError, socket.error), error :
[677]725            self.lastError = error
726            self.lastErrorMessage = str(error)
727            return None
[3438]728        else :
[677]729            datas = response.read()
730            ippresponse = IPPRequest(datas)
731            ippresponse.parse()
732            return ippresponse
[3438]733
734    def getPPD(self, queuename) :
[677]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
[3438]741
[677]742    def getDefault(self) :
743        """Retrieves CUPS' default printer."""
744        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))
[3438]745
746    def getJobAttributes(self, jobid) :
[677]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)
[3438]751
752    def getPrinters(self) :
[677]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"]]
[3438]759
760    def getDevices(self) :
[677]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"]])
[3438]767
768    def getPPDs(self) :
[677]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"]])
[3438]775
[677]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.
[3438]786
[677]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
[3438]796        """
[677]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)
[3438]807        if userdata is not None :
[677]808            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
[3438]809        if recipient is not None :
[677]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)
[3438]824
825    def cancelSubscription(self, uri, subscriptionid, jobid=None) :
[677]826        """Cancels a subscription.
[3438]827
[677]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)
[641]840
[3438]841
[641]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
[3438]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
[3438]914    def waitForLock(self) :
[688]915        """Waits until we can acquire the lock file."""
916        lockfilename = self.DeviceURI.replace("/", ".")
917        lockfilename = lockfilename.replace(":", ".")
918        lockfilename = lockfilename.replace("?", ".")
919        lockfilename = lockfilename.replace("&", ".")
920        lockfilename = lockfilename.replace("@", ".")
921        lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, lockfilename))
[690]922        self.logDebug("Waiting for lock %s to become available..." % lockfilename)
923        haslock = False
924        while not haslock :
925            try :
926                # open the lock file, optionally creating it if needed.
927                self.LockFile = open(lockfilename, "a+")
[3438]928
[690]929                # we wait indefinitely for the lock to become available.
930                # works over NFS too.
931                fcntl.lockf(self.LockFile, fcntl.LOCK_EX)
932                haslock = True
[3438]933
[690]934                self.logDebug("Lock %s acquired." % lockfilename)
[3438]935
[690]936                # Here we save the PID in the lock file, but we don't use
937                # it, because the lock file may be in a directory shared
938                # over NFS between two (or more) print servers, so the PID
939                # has no meaning in this case.
[688]940                self.LockFile.truncate(0)
941                self.LockFile.seek(0, 0)
[690]942                self.LockFile.write(str(self.pid))
[688]943                self.LockFile.flush()
[3438]944            except IOError :
[690]945                self.logDebug("I/O Error while waiting for lock %s" % lockfilename)
946                time.sleep(0.25)
[3438]947
[641]948    def readConfig(self) :
[626]949        """Reads the configuration file."""
[641]950        confdir = os.environ.get("CUPS_SERVERROOT", ".")
[577]951        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
[570]952        if os.path.isfile(self.conffile) :
953            self.config = ConfigParser.ConfigParser()
954            self.config.read([self.conffile])
[679]955            self.debug = isTrue(self.getGlobalOption("debug", ignore=1))
[641]956        else :
[570]957            self.config = FakeConfig()
958            self.debug = 1      # no config, so force debug mode !
[641]959
960    def logInfo(self, message, level="info") :
[574]961        """Logs a message to CUPS' error_log file."""
[640]962        try :
[664]963            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
[640]964            sys.stderr.flush()
965        except IOError :
966            pass
[641]967
968    def logDebug(self, message) :
[585]969        """Logs something to debug output if debug is enabled."""
970        if self.debug :
971            self.logInfo(message, level="debug")
[641]972
973    def getGlobalOption(self, option, ignore=0) :
[570]974        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
975        try :
976            return self.config.get("global", option, raw=1)
[641]977        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[577]978            if not ignore :
[570]979                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
[641]980
981    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
[570]982        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
983        globaloption = self.getGlobalOption(option, ignore=1)
984        try :
[574]985            return self.config.get(printqueuename, option, raw=1)
[641]986        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[570]987            if globaloption is not None :
988                return globaloption
[577]989            elif not ignore :
[574]990                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
[641]991
[597]992    def enumBranches(self, printqueuename, branchtype="tee") :
993        """Returns the list of branchtypes branches for a particular section's."""
994        branchbasename = "%s_" % branchtype.lower()
[573]995        try :
[627]996            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
[641]997        except ConfigParser.NoSectionError, msg :
[579]998            raise ConfigError, "Invalid configuration file : %s" % msg
[573]999        try :
[627]1000            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
[641]1001        except ConfigParser.NoSectionError, msg :
[583]1002            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
[573]1003            sectionbranches = []
1004        branches = {}
1005        for (k, v) in globalbranches :
1006            value = v.strip()
1007            if value :
1008                branches[k] = value
[641]1009        for (k, v) in sectionbranches :
[573]1010            value = v.strip()
1011            if value :
1012                branches[k] = value # overwrite any global option or set a new value
[641]1013            else :
[573]1014                del branches[k] # empty value disables a global option
1015        return branches
[641]1016
1017    def discoverOtherBackends(self) :
[568]1018        """Discovers the other CUPS backends.
[641]1019
[568]1020           Executes each existing backend in turn in device enumeration mode.
1021           Returns the list of available backends.
1022        """
[569]1023        # Unfortunately this method can't output any debug information
1024        # to stdout or stderr, else CUPS considers that the device is
1025        # not available.
[568]1026        available = []
[569]1027        (directory, myname) = os.path.split(sys.argv[0])
[588]1028        if not directory :
1029            directory = "./"
[568]1030        tmpdir = tempfile.gettempdir()
[569]1031        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
[568]1032        if os.path.exists(lockfilename) :
1033            lockfile = open(lockfilename, "r")
1034            pid = int(lockfile.read())
1035            lockfile.close()
1036            try :
1037                # see if the pid contained in the lock file is still running
1038                os.kill(pid, 0)
[679]1039            except OSError, error :
1040                if error.errno != errno.EPERM :
[568]1041                    # process doesn't exist anymore
1042                    os.remove(lockfilename)
[641]1043
[568]1044        if not os.path.exists(lockfilename) :
1045            lockfile = open(lockfilename, "w")
[577]1046            lockfile.write("%i" % self.pid)
[568]1047            lockfile.close()
[569]1048            allbackends = [ os.path.join(directory, b) \
[641]1049                                for b in os.listdir(directory)
[569]1050                                    if os.access(os.path.join(directory, b), os.X_OK) \
[641]1051                                        and (b != myname)]
1052            for backend in allbackends :
[568]1053                answer = os.popen(backend, "r")
1054                try :
[679]1055                    devices = [deviceline.strip() for deviceline in answer.readlines()]
[641]1056                except :
[568]1057                    devices = []
1058                status = answer.close()
1059                if status is None :
1060                    for d in devices :
[641]1061                        # each line is of the form :
[568]1062                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
1063                        # so we have to decompose it carefully
1064                        fdevice = cStringIO.StringIO(d)
1065                        tokenizer = shlex.shlex(fdevice)
1066                        tokenizer.wordchars = tokenizer.wordchars + \
1067                                                        r".:,?!~/\_$*-+={}[]()#"
1068                        arguments = []
1069                        while 1 :
1070                            token = tokenizer.get_token()
1071                            if token :
1072                                arguments.append(token)
1073                            else :
1074                                break
1075                        fdevice.close()
1076                        try :
1077                            (devicetype, device, name, fullname) = arguments
[641]1078                        except ValueError :
[568]1079                            pass    # ignore this 'bizarre' device
[641]1080                        else :
[568]1081                            if name.startswith('"') and name.endswith('"') :
1082                                name = name[1:-1]
1083                            if fullname.startswith('"') and fullname.endswith('"') :
1084                                fullname = fullname[1:-1]
[577]1085                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
1086                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
[568]1087            os.remove(lockfilename)
[599]1088        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
1089                             % (self.myname, self.MyName, self.MyName))
[568]1090        return available
[641]1091
1092    def initBackend(self) :
[577]1093        """Initializes the backend's attributes."""
[677]1094        self.JobId = sys.argv[1].strip()
1095        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
1096        self.Title = sys.argv[3].strip()
1097        self.Copies = int(sys.argv[4].strip())
1098        self.Options = sys.argv[5].strip()
1099        if len(sys.argv) == 7 :
1100            self.InputFile = sys.argv[6] # read job's datas from file
1101        else :
1102            self.InputFile = None        # read job's datas from stdin
1103        self.PrinterName = os.environ.get("PRINTER", "")
1104        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
1105        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
[3438]1106
[641]1107        # check that the DEVICE_URI environment variable's value is
[577]1108        # prefixed with self.myname otherwise don't touch it.
[641]1109        # If this is the case, we have to remove the prefix from
1110        # the environment before launching the real backend
[577]1111        muststartwith = "%s:" % self.myname
1112        device_uri = os.environ.get("DEVICE_URI", "")
1113        if device_uri.startswith(muststartwith) :
1114            fulldevice_uri = device_uri[:]
1115            device_uri = fulldevice_uri[len(muststartwith):]
[679]1116            for dummy in range(2) :
[641]1117                if device_uri.startswith("/") :
[583]1118                    device_uri = device_uri[1:]
[577]1119        try :
[679]1120            (backend, dummy) = device_uri.split(":", 1)
[641]1121        except ValueError :
[583]1122            if not device_uri :
[600]1123                self.logDebug("Not attached to an existing print queue.")
[583]1124                backend = ""
[641]1125            else :
[583]1126                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
[641]1127
[577]1128        self.RealBackend = backend
1129        self.DeviceURI = device_uri
[3438]1130
[677]1131        try :
1132            cupsserver = CUPS() # TODO : username and password and/or encryption
1133            answer = cupsserver.getJobAttributes(self.JobId)
[693]1134            if answer is None :  # probably connection refused because we
1135                raise ValueError # don't hande unix domain sockets yet.
[677]1136            self.ControlFile = "NotUsedAnymore"
[3438]1137        except :
[677]1138            (ippfilename, answer) = self.parseIPPRequestFile()
1139            self.ControlFile = ippfilename
[3438]1140
[677]1141        try :
1142            john = answer.job["job-originating-host-name"]
[3438]1143        except (KeyError, AttributeError) :
[677]1144            try :
1145                john = answer.operation["job-originating-host-name"]
[3438]1146            except (KeyError, AttributeError) :
[677]1147                john = (None, None)
[3438]1148        if type(john) == type([]) :
[652]1149            john = john[-1]
[3438]1150        (dummy, self.ClientHost) = john
1151        try :
[677]1152            jbing = answer.job["job-billing"]
[3438]1153        except (KeyError, AttributeError) :
[677]1154            jbing = (None, None)
[3438]1155        if type(jbing) == type([]) :
[677]1156            jbing = jbing[-1]
[679]1157        (dummy, self.JobBilling) = jbing
[3438]1158
[646]1159    def parseIPPRequestFile(self) :
[624]1160        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
[677]1161        requestroot = os.environ.get("CUPS_REQUESTROOT")
1162        if requestroot is None :
[679]1163            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
[677]1164            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
[582]1165        if (len(self.JobId) < 5) and self.JobId.isdigit() :
1166            ippmessagefile = "c%05i" % int(self.JobId)
[641]1167        else :
[582]1168            ippmessagefile = "c%s" % self.JobId
1169        ippmessagefile = os.path.join(requestroot, ippmessagefile)
1170        ippmessage = {}
1171        try :
1172            ippdatafile = open(ippmessagefile)
[641]1173        except :
[582]1174            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
[641]1175        else :
[582]1176            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
1177            try :
[646]1178                ippmessage = IPPRequest(ippdatafile.read())
1179                ippmessage.parse()
[641]1180            except IPPError, msg :
[582]1181                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
[641]1182            else :
[582]1183                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
1184            ippdatafile.close()
[615]1185        return (ippmessagefile, ippmessage)
[641]1186
1187    def exportAttributes(self) :
[577]1188        """Exports our backend's attributes to the environment."""
1189        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
1190        os.environ["TEAPRINTERNAME"] = self.PrinterName
1191        os.environ["TEADIRECTORY"] = self.Directory
1192        os.environ["TEADATAFILE"] = self.DataFile
[579]1193        os.environ["TEAJOBSIZE"] = str(self.JobSize)
[577]1194        os.environ["TEAMD5SUM"] = self.JobMD5Sum
[582]1195        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
[577]1196        os.environ["TEAJOBID"] = self.JobId
1197        os.environ["TEAUSERNAME"] = self.UserName
1198        os.environ["TEATITLE"] = self.Title
1199        os.environ["TEACOPIES"] = str(self.Copies)
1200        os.environ["TEAOPTIONS"] = self.Options
1201        os.environ["TEAINPUTFILE"] = self.InputFile or ""
[615]1202        os.environ["TEABILLING"] = self.JobBilling or ""
1203        os.environ["TEACONTROLFILE"] = self.ControlFile
[641]1204
[577]1205    def saveDatasAndCheckSum(self) :
1206        """Saves the input datas into a static file."""
[583]1207        self.logDebug("Duplicating data stream into %s" % self.DataFile)
[577]1208        mustclose = 0
1209        if self.InputFile is not None :
1210            infile = open(self.InputFile, "rb")
1211            mustclose = 1
[641]1212        else :
[577]1213            infile = sys.stdin
[3438]1214
[659]1215        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
1216                                                 ignore=1)
[3438]1217        if filtercommand :
[659]1218            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
1219            filteroutput = "%s.filteroutput" % self.DataFile
1220            outf = open(filteroutput, "wb")
1221            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
1222            outf.close()
1223            self.logDebug("Filter's output status : %s" % repr(filterstatus))
1224            if mustclose :
1225                infile.close()
1226            infile = open(filteroutput, "rb")
1227            mustclose = 1
[3438]1228        else :
[659]1229            self.logDebug("Data stream will be used as-is (no filter defined)")
[3438]1230
[577]1231        CHUNK = 64*1024         # read 64 Kb at a time
1232        dummy = 0
1233        sizeread = 0
1234        checksum = md5.new()
[641]1235        outfile = open(self.DataFile, "wb")
[577]1236        while 1 :
[641]1237            data = infile.read(CHUNK)
[577]1238            if not data :
1239                break
[641]1240            sizeread += len(data)
[577]1241            outfile.write(data)
[641]1242            checksum.update(data)
[577]1243            if not (dummy % 32) : # Only display every 2 Mb
1244                self.logDebug("%s bytes saved..." % sizeread)
[641]1245            dummy += 1
[577]1246        outfile.close()
[3438]1247
[659]1248        if filtercommand :
1249            self.logDebug("Removing filter's output file %s" % filteroutput)
1250            try :
1251                os.remove(filteroutput)
[3438]1252            except :
[659]1253                pass
[3438]1254
[641]1255        if mustclose :
[579]1256            infile.close()
[3438]1257
[659]1258        self.logDebug("%s bytes saved..." % sizeread)
[641]1259        self.JobSize = sizeread
[577]1260        self.JobMD5Sum = checksum.hexdigest()
1261        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
1262        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
[568]1263
[577]1264    def cleanUp(self) :
1265        """Cleans up the place."""
[688]1266        self.logDebug("Cleaning up...")
[679]1267        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
[676]1268            and os.path.exists(self.DataFile) :
1269            try :
1270                os.remove(self.DataFile)
[3438]1271            except OSError, msg :
[676]1272                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
[3438]1273
[690]1274        if self.LockFile is not None :
1275            self.logDebug("Removing lock...")
1276            try :
1277                fcntl.lockf(self.LockFile, fcntl.LOCK_UN)
1278                self.LockFile.close()
[3438]1279            except :
[690]1280                self.logInfo("Problem while unlocking.", "error")
[3438]1281            else :
[690]1282                self.logDebug("Lock removed.")
[688]1283        self.logDebug("Clean.")
[641]1284
1285    def runBranches(self) :
[640]1286        """Launches each hook defined for the current print queue."""
[604]1287        self.isCancelled = 0    # did a prehook cancel the print job ?
[679]1288        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
[643]1289        self.pipes = { 0: (0, 1) }
[640]1290        branches = self.enumBranches(self.PrinterName, "prehook")
[662]1291        for b in branches.keys() :
[640]1292            self.pipes[b.split("_", 1)[1]] = os.pipe()
1293        retcode = self.runCommands("prehook", branches, serialize)
[643]1294        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]1295            os.close(p[1][1])
[679]1296        if not self.isCancelled :
[640]1297            if self.RealBackend :
[676]1298                retcode = self.launchOriginalBackend()
[659]1299                if retcode :
1300                    onfail = self.getPrintQueueOption(self.PrinterName, \
1301                                                      "onfail", ignore=1)
1302                    if onfail :
1303                        self.logDebug("Launching onfail script %s" % onfail)
1304                        os.system(onfail)
[3438]1305
[679]1306            os.environ["TEASTATUS"] = str(retcode)
1307            branches = self.enumBranches(self.PrinterName, "posthook")
1308            if self.runCommands("posthook", branches, serialize) :
1309                self.logInfo("An error occured during the execution of posthooks.", "warn")
[3438]1310
[643]1311        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]1312            os.close(p[1][0])
1313        if not retcode :
[600]1314            self.logInfo("OK")
[641]1315        else :
[600]1316            self.logInfo("An error occured, please check CUPS' error_log file.")
[640]1317        return retcode
[641]1318
[639]1319    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
1320        """Launches a command with stdio redirected."""
1321        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
[636]1322        pid = os.fork()
1323        if pid == 0 :
[639]1324            if stdin != 0 :
1325                os.dup2(stdin, 0)
1326                os.close(stdin)
1327            if stdout != 1 :
1328                os.dup2(stdout, 1)
1329                os.close(stdout)
1330            try :
1331                os.execl("/bin/sh", "sh", "-c", cmd)
[640]1332            except OSError, msg :
[639]1333                self.logDebug("execl() failed: %s" % msg)
[640]1334            os._exit(-1)
1335        status = os.waitpid(pid, 0)[1]
1336        if os.WIFEXITED(status) :
1337            return os.WEXITSTATUS(status)
1338        return -1
[641]1339
[639]1340    def runCommand(self, branch, command) :
1341        """Runs a particular branch command."""
1342        # Code contributed by Peter Stuge on June 7th 2005
1343        self.logDebug("Launching %s : %s" % (branch, command))
1344        btype, bname = branch.split("_", 1)
[643]1345        if bname not in self.pipes.keys() :
[640]1346            bname = 0
1347        if btype == "prehook" :
[639]1348            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
1349        else :
1350            return self.stdioRedirSystem(command, self.pipes[bname][0])
1351
[641]1352    def runCommands(self, btype, branches, serialize) :
[598]1353        """Runs the commands for a particular branch type."""
[641]1354        exitcode = 0
[598]1355        btype = btype.lower()
1356        btypetitle = btype.title()
[641]1357        branchlist = branches.keys()
[598]1358        branchlist.sort()
1359        if serialize :
[600]1360            self.logDebug("Begin serialized %ss" % btypetitle)
[598]1361            for branch in branchlist :
[640]1362                retcode = self.runCommand(branch, branches[branch])
[598]1363                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
[641]1364                if retcode :
[601]1365                    if (btype == "prehook") and (retcode == 255) : # -1
1366                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]1367                        self.isCancelled = 1
[641]1368                    else :
[601]1369                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
1370                        exitcode = 1
[600]1371            self.logDebug("End serialized %ss" % btypetitle)
[641]1372        else :
[600]1373            self.logDebug("Begin forked %ss" % btypetitle)
[584]1374            pids = {}
[598]1375            for branch in branchlist :
[584]1376                pid = os.fork()
1377                if pid :
1378                    pids[branch] = pid
[641]1379                else :
[640]1380                    os._exit(self.runCommand(branch, branches[branch]))
[584]1381            for (branch, pid) in pids.items() :
[640]1382                retcode = os.waitpid(pid, 0)[1]
[584]1383                if os.WIFEXITED(retcode) :
1384                    retcode = os.WEXITSTATUS(retcode)
[640]1385                else :
1386                    retcode = -1
1387                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
[641]1388                if retcode :
[601]1389                    if (btype == "prehook") and (retcode == 255) : # -1
1390                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]1391                        self.isCancelled = 1
[641]1392                    else :
[640]1393                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
[601]1394                        exitcode = 1
[600]1395            self.logDebug("End forked %ss" % btypetitle)
[584]1396        return exitcode
[641]1397
[676]1398    def launchOriginalBackend(self) :
1399        """Launches the original backend, optionally retrying if needed."""
1400        number = 1
1401        delay = 0
1402        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
1403        if retry is not None :
1404            try :
1405                (number, delay) = [int(p) for p in retry.strip().split(",")]
[3438]1406            except (ValueError, AttributeError, TypeError) :
[676]1407                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
1408                number = 1
1409                delay = 0
[3438]1410
1411        loopcount = 1
1412        while 1 :
[676]1413            retcode = self.runOriginalBackend()
1414            if not retcode :
1415                break
1416            else :
1417                if (not number) or (loopcount < number) :
[677]1418                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
[676]1419                    time.sleep(delay)
1420                    loopcount += 1
[3438]1421                else :
[676]1422                    break
[3438]1423        return retcode
1424
[641]1425    def runOriginalBackend(self) :
[587]1426        """Launches the original backend."""
1427        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
[640]1428        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1429        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1430
1431        pid = os.fork()
1432        if pid == 0 :
1433            if self.InputFile is None :
[653]1434                f = open(self.DataFile, "rb")
[640]1435                os.dup2(f.fileno(), 0)
1436                f.close()
[3438]1437            else :
[676]1438                arguments[6] = self.DataFile # in case a tea4cups filter was applied
[640]1439            try :
[643]1440                os.execve(originalbackend, arguments, os.environ)
[640]1441            except OSError, msg :
1442                self.logDebug("execve() failed: %s" % msg)
1443            os._exit(-1)
[588]1444        killed = 0
1445        status = -1
[640]1446        while status == -1 :
[588]1447            try :
[640]1448                status = os.waitpid(pid, 0)[1]
1449            except OSError, (err, msg) :
[679]1450                if err == 4 :
[640]1451                    killed = 1
[587]1452        if os.WIFEXITED(status) :
[640]1453            status = os.WEXITSTATUS(status)
1454            if status :
[679]1455                self.logInfo("CUPS backend %s returned %d." % (originalbackend, \
[643]1456                                                             status), "error")
[640]1457            return status
[641]1458        elif not killed :
[679]1459            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
[643]1460                                                              "error")
[588]1461            return -1
[641]1462        else :
[587]1463            return 1
[641]1464
1465if __name__ == "__main__" :
[565]1466    # This is a CUPS backend, we should act and die like a CUPS backend
[568]1467    wrapper = CupsBackend()
[565]1468    if len(sys.argv) == 1 :
[568]1469        print "\n".join(wrapper.discoverOtherBackends())
[641]1470        sys.exit(0)
1471    elif len(sys.argv) not in (6, 7) :
[568]1472        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1473                              % sys.argv[0])
1474        sys.exit(1)
[641]1475    else :
[679]1476        returncode = 1
[585]1477        try :
[676]1478            try :
1479                wrapper.readConfig()
1480                wrapper.initBackend()
[688]1481                wrapper.waitForLock()
[676]1482                wrapper.saveDatasAndCheckSum()
1483                wrapper.exportAttributes()
[679]1484                returncode = wrapper.runBranches()
[676]1485            except SystemExit, e :
[679]1486                returncode = e.code
[3438]1487            except KeyboardInterrupt :
[683]1488                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
[676]1489            except :
1490                import traceback
1491                lines = []
[679]1492                for errline in traceback.format_exception(*sys.exc_info()) :
1493                    lines.extend([l for l in errline.split("\n") if l])
1494                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
[676]1495                                                              wrapper.pid, l) \
1496                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
[679]1497                sys.stderr.write(errormessage)
[676]1498                sys.stderr.flush()
[679]1499                returncode = 1
[3438]1500        finally :
[585]1501            wrapper.cleanUp()
[679]1502        sys.exit(returncode)
Note: See TracBrowser for help on using the browser.