root / tea4cups / trunk / tea4cups @ 3565

Revision 3565, 58.5 kB (checked in by jerome, 11 years ago)

Changed copyright years

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