root / tea4cups / trunk / tea4cups @ 579

Revision 579, 13.3 kB (checked in by jerome, 19 years ago)

First working version

  • 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            raise ConfigError, "Invalid configuration file : %s" % msg
113        try :
114            sectionbranches = [ (k, v) for (k, v) in self.config.items(printqueuename) if k.startswith("tee_") ]
115        except ConfigParser.NoSectionError, msg :   
116            self.logInfo("No section for print queue %s : " % (printqueuename, msg), "info")
117            sectionbranches = []
118        branches = {}
119        for (k, v) in globalbranches :
120            value = v.strip()
121            if value :
122                branches[k] = value
123        for (k, v) in sectionbranches :   
124            value = v.strip()
125            if value :
126                branches[k] = value # overwrite any global option or set a new value
127            else :   
128                del branches[k] # empty value disables a global option
129        return branches
130       
131    def discoverOtherBackends(self) :   
132        """Discovers the other CUPS backends.
133       
134           Executes each existing backend in turn in device enumeration mode.
135           Returns the list of available backends.
136        """
137        # Unfortunately this method can't output any debug information
138        # to stdout or stderr, else CUPS considers that the device is
139        # not available.
140        available = []
141        (directory, myname) = os.path.split(sys.argv[0])
142        tmpdir = tempfile.gettempdir()
143        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
144        if os.path.exists(lockfilename) :
145            lockfile = open(lockfilename, "r")
146            pid = int(lockfile.read())
147            lockfile.close()
148            try :
149                # see if the pid contained in the lock file is still running
150                os.kill(pid, 0)
151            except OSError, e :   
152                if e.errno != errno.EPERM :
153                    # process doesn't exist anymore
154                    os.remove(lockfilename)
155           
156        if not os.path.exists(lockfilename) :
157            lockfile = open(lockfilename, "w")
158            lockfile.write("%i" % self.pid)
159            lockfile.close()
160            allbackends = [ os.path.join(directory, b) \
161                                for b in os.listdir(directory) 
162                                    if os.access(os.path.join(directory, b), os.X_OK) \
163                                        and (b != myname)] 
164            for backend in allbackends :                           
165                answer = os.popen(backend, "r")
166                try :
167                    devices = [line.strip() for line in answer.readlines()]
168                except :   
169                    devices = []
170                status = answer.close()
171                if status is None :
172                    for d in devices :
173                        # each line is of the form :
174                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
175                        # so we have to decompose it carefully
176                        fdevice = cStringIO.StringIO(d)
177                        tokenizer = shlex.shlex(fdevice)
178                        tokenizer.wordchars = tokenizer.wordchars + \
179                                                        r".:,?!~/\_$*-+={}[]()#"
180                        arguments = []
181                        while 1 :
182                            token = tokenizer.get_token()
183                            if token :
184                                arguments.append(token)
185                            else :
186                                break
187                        fdevice.close()
188                        try :
189                            (devicetype, device, name, fullname) = arguments
190                        except ValueError :   
191                            pass    # ignore this 'bizarre' device
192                        else :   
193                            if name.startswith('"') and name.endswith('"') :
194                                name = name[1:-1]
195                            if fullname.startswith('"') and fullname.endswith('"') :
196                                fullname = fullname[1:-1]
197                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
198                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
199            os.remove(lockfilename)
200        return available
201                       
202    def initBackend(self) :   
203        """Initializes the backend's attributes."""
204        # check that the DEVICE_URI environment variable's value is
205        # prefixed with self.myname otherwise don't touch it.
206        # If this is the case, we have to remove the prefix from
207        # the environment before launching the real backend
208        muststartwith = "%s:" % self.myname
209        device_uri = os.environ.get("DEVICE_URI", "")
210        if device_uri.startswith(muststartwith) :
211            fulldevice_uri = device_uri[:]
212            device_uri = fulldevice_uri[len(muststartwith):]
213            if device_uri.startswith("//") : 
214                device_uri = device_uri[2:]
215        try :
216            (backend, destination) = device_uri.split(":", 1) 
217        except ValueError :   
218            raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
219       
220        self.JobId = sys.argv[1].strip()
221        self.UserName = sys.argv[2].strip()
222        self.Title = sys.argv[3].strip()
223        self.Copies = int(sys.argv[4].strip())
224        self.Options = sys.argv[5].strip()
225        if len(sys.argv) == 7 :
226            self.InputFile = sys.argv[6] # read job's datas from file
227        else :   
228            self.InputFile = None        # read job's datas from stdin
229           
230        self.RealBackend = backend
231        self.DeviceURI = device_uri
232        self.PrinterName = os.environ.get("PRINTER", "")
233        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory")
234        self.DataFile = os.path.join(self.Directory, "%s-%s-%s" % (self.myname, self.PrinterName, self.JobId))
235           
236    def exportAttributes(self) :   
237        """Exports our backend's attributes to the environment."""
238        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
239        os.environ["TEAPRINTERNAME"] = self.PrinterName
240        os.environ["TEADIRECTORY"] = self.Directory
241        os.environ["TEADATAFILE"] = self.DataFile
242        os.environ["TEAJOBSIZE"] = str(self.JobSize)
243        os.environ["TEAMD5SUM"] = self.JobMD5Sum
244        os.environ["TEAJOBID"] = self.JobId
245        os.environ["TEAUSERNAME"] = self.UserName
246        os.environ["TEATITLE"] = self.Title
247        os.environ["TEACOPIES"] = str(self.Copies)
248        os.environ["TEAOPTIONS"] = self.Options
249        os.environ["TEAINPUTFILE"] = self.InputFile or ""
250       
251    def saveDatasAndCheckSum(self) :
252        """Saves the input datas into a static file."""
253        self.logDebug("Duplicating data stream to %s" % self.DataFile)
254        mustclose = 0
255        if self.InputFile is not None :
256            infile = open(self.InputFile, "rb")
257            mustclose = 1
258        else :   
259            infile = sys.stdin
260        CHUNK = 64*1024         # read 64 Kb at a time
261        dummy = 0
262        sizeread = 0
263        checksum = md5.new()
264        outfile = open(self.DataFile, "wb")   
265        while 1 :
266            data = infile.read(CHUNK) 
267            if not data :
268                break
269            sizeread += len(data)   
270            outfile.write(data)
271            checksum.update(data)   
272            if not (dummy % 32) : # Only display every 2 Mb
273                self.logDebug("%s bytes saved..." % sizeread)
274            dummy += 1   
275        outfile.close()
276        if mustclose :   
277            infile.close()
278        self.JobSize = sizeread   
279        self.JobMD5Sum = checksum.hexdigest()
280        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
281        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
282
283    def cleanUp(self) :
284        """Cleans up the place."""
285        if not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1)) :
286            os.remove(self.DataFile)
287           
288    def runBranches(self) :         
289        """Launches each tee defined for the current print queue."""
290        branches = self.enumTeeBranches(self.PrinterName)
291        for (branch, command) in branches.items() :
292            self.logDebug("Launching %s : %s" % (branch, command))
293            os.system(command)
294       
295if __name__ == "__main__" :   
296    # This is a CUPS backend, we should act and die like a CUPS backend
297    wrapper = CupsBackend()
298    if len(sys.argv) == 1 :
299        print "\n".join(wrapper.discoverOtherBackends())
300        sys.exit(0)               
301    elif len(sys.argv) not in (6, 7) :   
302        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
303                              % sys.argv[0])
304        sys.exit(1)
305    else :   
306        wrapper.initBackend()
307        wrapper.saveDatasAndCheckSum()
308        wrapper.exportAttributes()
309        retcode = wrapper.runBranches()
310        wrapper.cleanUp()
311        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.