root / tea4cups / trunk / tea4cups @ 577

Revision 577, 13.0 kB (checked in by jerome, 19 years ago)

Completed the backend initialization code.
Added the list of additionnal environment variables to the sample
configuration file.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Rev
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# Tea4CUPS : Tee for CUPS
5#
6# (c) 2005 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23#
24
25import sys
26import os
27import errno
28import md5
29import cStringIO
30import shlex
31import tempfile
32import ConfigParser
33
34class TeeError(Exception):
35    """Base exception for Tea4CUPS related stuff."""
36    def __init__(self, message = ""):
37        self.message = message
38        Exception.__init__(self, message)
39    def __repr__(self):
40        return self.message
41    __str__ = __repr__
42   
43class ConfigError(TeeError) :   
44    """Configuration related exceptions."""
45    pass 
46   
47class FakeConfig :   
48    """Fakes a configuration file parser."""
49    def get(self, section, option, raw=0) :
50        """Fakes the retrieval of a global option."""
51        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
52       
53class CupsBackend :
54    """Base class for tools with no database access."""
55    def __init__(self) :
56        """Initializes the CUPS backend wrapper."""
57        self.MyName = "Tea4CUPS"
58        self.myname = "tea4cups"
59        self.pid = os.getpid()
60        confdir = os.environ.get("CUPS_SERVERROOT", ".") 
61        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
62        if os.path.isfile(self.conffile) :
63            self.config = ConfigParser.ConfigParser()
64            self.config.read([self.conffile])
65            self.debug = self.isTrue(self.getGlobalOption("debug", ignore=1))
66        else :   
67            self.config = FakeConfig()
68            self.debug = 1      # no config, so force debug mode !
69       
70    def logDebug(self, message) :   
71        """Logs something to debug output if debug is enabled."""
72        if self.debug :
73            sys.stderr.write("DEBUG: %s (PID %i) : %s\n" % (self.MyName, self.pid, message))
74            sys.stderr.flush()
75           
76    def logInfo(self, message, level="info") :       
77        """Logs a message to CUPS' error_log file."""
78        sys.stderr.write("%s: %s (PID %i) : %s\n" % (level.upper(), self.MyName, self.pid, message))
79        sys.stderr.flush()
80       
81    def isTrue(self, option) :       
82        """Returns 1 if option is set to true, else 0."""
83        if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
84            return 1
85        else :   
86            return 0
87                       
88    def getGlobalOption(self, option, ignore=0) :   
89        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
90        try :
91            return self.config.get("global", option, raw=1)
92        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :   
93            if not ignore :
94                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
95               
96    def getPrintQueueOption(self, printqueuename, option, ignore=0) :   
97        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
98        globaloption = self.getGlobalOption(option, ignore=1)
99        try :
100            return self.config.get(printqueuename, option, raw=1)
101        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :   
102            if globaloption is not None :
103                return globaloption
104            elif not ignore :
105                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
106               
107    def enumTeeBranches(self, printqueuename) :
108        """Returns the list of branches for a particular section's Tee."""
109        try :
110            globalbranches = [ (k, v) for (k, v) in self.config.items("global") if k.startswith("tee_") ]
111        except ConfigParser.NoSectionError, msg :   
112            self.printInfo("Invalid configuration file : %s" % msg, "error")
113            globalbranches = []
114        try :
115            sectionbranches = [ (k, v) for (k, v) in self.config.items(printqueuename) if k.startswith("tee_") ]
116        except ConfigParser.NoSectionError, msg :   
117            self.printInfo("Invalid configuration file : %s" % msg, "error")
118            sectionbranches = []
119        branches = {}
120        for (k, v) in globalbranches :
121            value = v.strip()
122            if value :
123                branches[k] = value
124        for (k, v) in sectionbranches :   
125            value = v.strip()
126            if value :
127                branches[k] = value # overwrite any global option or set a new value
128            else :   
129                del branches[k] # empty value disables a global option
130        return branches
131       
132    def discoverOtherBackends(self) :   
133        """Discovers the other CUPS backends.
134       
135           Executes each existing backend in turn in device enumeration mode.
136           Returns the list of available backends.
137        """
138        # Unfortunately this method can't output any debug information
139        # to stdout or stderr, else CUPS considers that the device is
140        # not available.
141        available = []
142        (directory, myname) = os.path.split(sys.argv[0])
143        tmpdir = tempfile.gettempdir()
144        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
145        if os.path.exists(lockfilename) :
146            lockfile = open(lockfilename, "r")
147            pid = int(lockfile.read())
148            lockfile.close()
149            try :
150                # see if the pid contained in the lock file is still running
151                os.kill(pid, 0)
152            except OSError, e :   
153                if e.errno != errno.EPERM :
154                    # process doesn't exist anymore
155                    os.remove(lockfilename)
156           
157        if not os.path.exists(lockfilename) :
158            lockfile = open(lockfilename, "w")
159            lockfile.write("%i" % self.pid)
160            lockfile.close()
161            allbackends = [ os.path.join(directory, b) \
162                                for b in os.listdir(directory) 
163                                    if os.access(os.path.join(directory, b), os.X_OK) \
164                                        and (b != myname)] 
165            for backend in allbackends :                           
166                answer = os.popen(backend, "r")
167                try :
168                    devices = [line.strip() for line in answer.readlines()]
169                except :   
170                    devices = []
171                status = answer.close()
172                if status is None :
173                    for d in devices :
174                        # each line is of the form :
175                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
176                        # so we have to decompose it carefully
177                        fdevice = cStringIO.StringIO(d)
178                        tokenizer = shlex.shlex(fdevice)
179                        tokenizer.wordchars = tokenizer.wordchars + \
180                                                        r".:,?!~/\_$*-+={}[]()#"
181                        arguments = []
182                        while 1 :
183                            token = tokenizer.get_token()
184                            if token :
185                                arguments.append(token)
186                            else :
187                                break
188                        fdevice.close()
189                        try :
190                            (devicetype, device, name, fullname) = arguments
191                        except ValueError :   
192                            pass    # ignore this 'bizarre' device
193                        else :   
194                            if name.startswith('"') and name.endswith('"') :
195                                name = name[1:-1]
196                            if fullname.startswith('"') and fullname.endswith('"') :
197                                fullname = fullname[1:-1]
198                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
199                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
200            os.remove(lockfilename)
201        return available
202                       
203    def initBackend(self) :   
204        """Initializes the backend's attributes."""
205        # check that the DEVICE_URI environment variable's value is
206        # prefixed with self.myname otherwise don't touch it.
207        # If this is the case, we have to remove the prefix from
208        # the environment before launching the real backend
209        muststartwith = "%s:" % self.myname
210        device_uri = os.environ.get("DEVICE_URI", "")
211        if device_uri.startswith(muststartwith) :
212            fulldevice_uri = device_uri[:]
213            device_uri = fulldevice_uri[len(muststartwith):]
214            if device_uri.startswith("//") : 
215                device_uri = device_uri[2:]
216        try :
217            (backend, destination) = device_uri.split(":", 1) 
218        except ValueError :   
219            raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
220       
221        self.JobId = sys.argv[1].strip()
222        self.UserName = sys.argv[2].strip()
223        self.Title = sys.argv[3].strip()
224        self.Copies = int(sys.argv[4].strip())
225        self.Options = sys.argv[5].strip()
226        if len(sys.argv) == 7 :
227            self.InputFile = sys.argv[6] # read job's datas from file
228        else :   
229            self.InputFile = None        # read job's datas from stdin
230           
231        self.RealBackend = backend
232        self.DeviceURI = device_uri
233        self.PrinterName = os.environ.get("PRINTER", "")
234        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory")
235        self.DataFile = os.path.join(self.Directory, "%s-%s-%s" % (self.myname, self.PrinterName, self.JobId))
236           
237    def exportAttributes(self) :   
238        """Exports our backend's attributes to the environment."""
239        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
240        os.environ["TEAPRINTERNAME"] = self.PrinterName
241        os.environ["TEADIRECTORY"] = self.Directory
242        os.environ["TEADATAFILE"] = self.DataFile
243        os.environ["TEAJOBSIZE"] = self.JobSize
244        os.environ["TEAMD5SUM"] = self.JobMD5Sum
245        os.environ["TEAJOBID"] = self.JobId
246        os.environ["TEAUSERNAME"] = self.UserName
247        os.environ["TEATITLE"] = self.Title
248        os.environ["TEACOPIES"] = str(self.Copies)
249        os.environ["TEAOPTIONS"] = self.Options
250        os.environ["TEAINPUTFILE"] = self.InputFile or ""
251       
252    def saveDatasAndCheckSum(self) :
253        """Saves the input datas into a static file."""
254        self.logDebug("Duplicating data stream to %s" % self.DataFile)
255        mustclose = 0
256        if self.InputFile is not None :
257            infile = open(self.InputFile, "rb")
258            mustclose = 1
259        else :   
260            infile = sys.stdin
261        CHUNK = 64*1024         # read 64 Kb at a time
262        dummy = 0
263        sizeread = 0
264        checksum = md5.new()
265        outfile = open(self.DataFile, "wb")   
266        while 1 :
267            data = infile.read(CHUNK) 
268            if not data :
269                break
270            sizeread += len(data)   
271            outfile.write(data)
272            checksum.update(data)   
273            if not (dummy % 32) : # Only display every 2 Mb
274                self.logDebug("%s bytes saved..." % sizeread)
275            dummy += 1   
276        outfile.close()
277        if mustclose :   
278            self.infile.close()
279        self.JobSize = sizeread   
280        self.JobMD5Sum = checksum.hexdigest()
281        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
282        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
283
284    def cleanUp(self) :
285        """Cleans up the place."""
286        if not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1)) :
287            os.remove(self.DataFile)
288       
289if __name__ == "__main__" :   
290    # This is a CUPS backend, we should act and die like a CUPS backend
291    wrapper = CupsBackend()
292    if len(sys.argv) == 1 :
293        print "\n".join(wrapper.discoverOtherBackends())
294        sys.exit(0)               
295    elif len(sys.argv) not in (6, 7) :   
296        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
297                              % sys.argv[0])
298        sys.exit(1)
299    else :   
300        wrapper.initBackend()
301        wrapper.saveDatasAndCheckSum()
302        wrapper.exportAttributes()
303        wrapper.cleanUp()
304        sys.stderr.write("ERROR: Not Yet !\n")
305        sys.exit(1)
Note: See TracBrowser for help on using the browser.