1 | #!/usr/bin/python |
---|
2 | |
---|
3 | # lpr.debathena |
---|
4 | # |
---|
5 | # also lp.debathena, lpq.debathena, and lprm.debathena |
---|
6 | # |
---|
7 | # Wrapper script that intelligently determines whether a command was |
---|
8 | # intended for should go to CUPS or LPRng and sends it off in the |
---|
9 | # right direction |
---|
10 | |
---|
11 | import cups |
---|
12 | import errno |
---|
13 | import hesiod |
---|
14 | import getopt |
---|
15 | import os |
---|
16 | import shlex |
---|
17 | import socket |
---|
18 | import sys |
---|
19 | from subprocess import call, PIPE |
---|
20 | import urllib |
---|
21 | |
---|
22 | try: |
---|
23 | cupsd = cups.Connection() |
---|
24 | except RuntimeError: # Is this actually safer than "except:"? |
---|
25 | cupsd = None |
---|
26 | |
---|
27 | def hesiod_lookup(hes_name, hes_type): |
---|
28 | """A wrapper with somewhat graceful error handling.""" |
---|
29 | try: |
---|
30 | h = hesiod.Lookup(hes_name, hes_type) |
---|
31 | if len(h.results) > 0: |
---|
32 | return h.results |
---|
33 | except IOError: |
---|
34 | return [] |
---|
35 | |
---|
36 | cups_frontends = ['cups.mit.edu', |
---|
37 | 'printers.mit.edu', |
---|
38 | 'cluster-printers.mit.edu'] |
---|
39 | cups_backends = [s.lower() for s in |
---|
40 | hesiod_lookup('cups-print', 'sloc') |
---|
41 | + hesiod_lookup('cups-cluster', 'sloc')] |
---|
42 | |
---|
43 | |
---|
44 | def lpropt_transform(args): |
---|
45 | if 'LPROPT' in os.environ: |
---|
46 | return shlex.split(os.environ['LPROPT']) + args |
---|
47 | return args |
---|
48 | |
---|
49 | def zephyr_transform(options): |
---|
50 | zephyr = True |
---|
51 | return_options = [] |
---|
52 | for o, a in options: |
---|
53 | if o == '-N': |
---|
54 | zephyr = False |
---|
55 | else: |
---|
56 | return_options.append((o, a)) |
---|
57 | |
---|
58 | if zephyr and os.environ.get('ATHENA_USER'): |
---|
59 | return_options.append(('-m', 'zephyr%' + os.environ['ATHENA_USER'])) |
---|
60 | |
---|
61 | return return_options |
---|
62 | |
---|
63 | opts = { |
---|
64 | 'cups': { |
---|
65 | 'lp': ('EU:cd:h:mn:o:q:st:H:P:i:', None, '-d'), |
---|
66 | 'lpq': ('EU:h:P:al', None, '-P'), |
---|
67 | 'lpr': ('EH:U:P:#:hlmo:pqrC:J:T:', |
---|
68 | (lpropt_transform, zephyr_transform), '-P'), |
---|
69 | 'lprm': ('EU:h:P:', None, '-P'), |
---|
70 | }, |
---|
71 | 'lprng': { |
---|
72 | 'lp': ('ckmprswBGYd:D:f:n:q:t:', None, '-d'), |
---|
73 | 'lpq': ('aAlLVcvP:st:D:', None, '-P'), |
---|
74 | 'lpr': ('ABblC:D:F:Ghi:kJ:K:#:m:NP:rR:sT:U:Vw:X:YZ:z1:2:3:4:', |
---|
75 | (lpropt_transform, zephyr_transform), '-P'), |
---|
76 | 'lprm': ('aAD:P:VU:', None, '-P'), |
---|
77 | } |
---|
78 | } |
---|
79 | |
---|
80 | preflist = { |
---|
81 | 'cups': ['cups', 'lprng'], |
---|
82 | 'lprng': ['lprng', 'cups'] |
---|
83 | } |
---|
84 | |
---|
85 | def error(code, message): |
---|
86 | """Exit out with an error |
---|
87 | """ |
---|
88 | sys.stderr.write(message) |
---|
89 | sys.exit(code) |
---|
90 | |
---|
91 | def execCups(command, queue, args): |
---|
92 | """Pass the command and arguments on to the CUPS versions of the command |
---|
93 | """ |
---|
94 | new_command = '/usr/bin/cups-%s' % command |
---|
95 | os.execv(new_command, [command] + args) |
---|
96 | |
---|
97 | def execLprng(command, queue, args): |
---|
98 | """Pass the command and arguments on to the LPRng versions of the command |
---|
99 | """ |
---|
100 | new_command = '/usr/bin/mit-%s' % command |
---|
101 | os.execv(new_command, [command] + args) |
---|
102 | |
---|
103 | def getPrintQueue(command, args): |
---|
104 | """Given argv, extract the printer name, using knowledge of the possible |
---|
105 | command line options for both the CUPS and LPRng versions of the command |
---|
106 | that this script was called as. Also, return whether the command line |
---|
107 | options imply a particular version of the command. |
---|
108 | """ |
---|
109 | |
---|
110 | preference = 'cups' |
---|
111 | for version in ['cups', 'lprng']: |
---|
112 | try: |
---|
113 | # Get the set of options that correspond to the command that this |
---|
114 | # script was invoked as |
---|
115 | (cmd_opts, transformers, destopt) = opts[version][command] |
---|
116 | if transformers: |
---|
117 | (transform_args, transform_opts) = transformers |
---|
118 | else: |
---|
119 | transform_args = transform_opts = lambda x: x |
---|
120 | except KeyError: |
---|
121 | error(1, """ |
---|
122 | Error: this script was called as %s, when it must be called as |
---|
123 | one of lpr, lpq, lprm, or lp |
---|
124 | |
---|
125 | """ % command) |
---|
126 | |
---|
127 | # Attempt to parse it with the current version of this command |
---|
128 | targs = transform_args(args) |
---|
129 | try: |
---|
130 | options, realargs = getopt.gnu_getopt(targs, cmd_opts) |
---|
131 | except getopt.GetoptError: |
---|
132 | # That's the wrong version, so try the next one. |
---|
133 | continue |
---|
134 | |
---|
135 | options = transform_opts(options) |
---|
136 | ttargs = [o + a for o, a in options] + realargs |
---|
137 | if destopt: |
---|
138 | for o, a in options: |
---|
139 | if o == destopt: |
---|
140 | return (version, a, ttargs) |
---|
141 | else: |
---|
142 | if len(realargs) > 0: |
---|
143 | return (version, realargs[0], ttargs) |
---|
144 | # Since we've successfully getopt'd, don't try any other versions, |
---|
145 | # but do note that we like this version. |
---|
146 | preference = version |
---|
147 | break |
---|
148 | |
---|
149 | # Either we failed to getopt, or nobody told us what printer to use, |
---|
150 | # so let's use the default printer |
---|
151 | default = os.getenv('PRINTER') |
---|
152 | if not default: |
---|
153 | if cupsd: |
---|
154 | default = cupsd.getDefault() |
---|
155 | if not default: |
---|
156 | h = hesiod_lookup(os.uname()[1], 'cluster') |
---|
157 | for result in h: |
---|
158 | (key, value) = result.split(None, 1) |
---|
159 | if key == 'lpr': |
---|
160 | default = value |
---|
161 | |
---|
162 | if default: |
---|
163 | return (preference, default, args) |
---|
164 | else: |
---|
165 | error(2, """ |
---|
166 | No default printer configured. Specify a -P option, or configure a |
---|
167 | default printer via e.g. System | Administration | Printing. |
---|
168 | |
---|
169 | """) |
---|
170 | |
---|
171 | def getCupsURI(printer): |
---|
172 | if not cupsd: |
---|
173 | return None |
---|
174 | try: |
---|
175 | attrs = cupsd.getPrinterAttributes(printer) |
---|
176 | return attrs.get('device-uri') |
---|
177 | except cups.IPPError: |
---|
178 | return None |
---|
179 | |
---|
180 | def translate_lprng_args_to_cups(command, args): |
---|
181 | # TODO yell at user if/when we decide that these args are deprecated |
---|
182 | |
---|
183 | # If getopt fails, something went very wrong -- _we_ generated this |
---|
184 | options, realargs = getopt.gnu_getopt(args, opts['lprng'][command][0]) |
---|
185 | cupsargs = [] |
---|
186 | for (o, a) in options: |
---|
187 | if o in ('-a') and command == 'lpq': |
---|
188 | cupsargs += [('-a', a)] |
---|
189 | elif o in ('-b', '-l') and command == 'lpr': |
---|
190 | cupsargs += [('-l', a)] |
---|
191 | elif o in ('-h') and command == 'lpr': |
---|
192 | cupsargs += [('-h', a)] |
---|
193 | elif o in ('-J') and command == 'lpr': |
---|
194 | cupsargs += [('-J', a)] |
---|
195 | elif o in ('-K', '-#') and command == 'lpr': |
---|
196 | cupsargs += [('-#', a)] |
---|
197 | elif o in ('-P'): |
---|
198 | cupsargs += [('-P', a)] |
---|
199 | elif o in ('-T') and command == 'lpr': |
---|
200 | cupsargs += [('-T', a)] |
---|
201 | elif o in ('-U') and command == 'lpr': |
---|
202 | cupsargs += [('-U', a)] |
---|
203 | elif o in ('-v') and command == 'lpq': |
---|
204 | cupsargs += [('-l', a)] |
---|
205 | elif o in ('-Z') and command == 'lpr': |
---|
206 | if a == 'simplex': |
---|
207 | cupsargs += [('-o', 'sides=one-sided')] |
---|
208 | elif a == 'duplex': |
---|
209 | cupsargs += [('-o', 'sides=two-sided-long-edge')] |
---|
210 | elif a == 'duplexshort': |
---|
211 | cupsargs += [('-o', 'sides=two-sided-short-edge')] |
---|
212 | # TODO attempt to deal banner=staff |
---|
213 | elif o in ('-m') and command == 'lpr': |
---|
214 | # TODO figure out if CUPS can do mail/zephyr |
---|
215 | pass # Don't warn about this, we probably generated it |
---|
216 | else: |
---|
217 | sys.stderr.write("Warning: option %s%s not converted to CUPS\n" |
---|
218 | % (o, a)) |
---|
219 | joincupsargs = [o + a for o, a in cupsargs] + realargs |
---|
220 | sys.stderr.write("Using cups-%s %s\n" % (command, ' '.join(joincupsargs))) |
---|
221 | return joincupsargs |
---|
222 | |
---|
223 | if __name__ == '__main__': |
---|
224 | # Remove the command name from the arguments when we extract it |
---|
225 | command = os.path.basename(sys.argv.pop(0)) |
---|
226 | |
---|
227 | # Determine if the arguments prefer one version of this command, |
---|
228 | # For instance, "lpr -Zduplex" wants mit-lpr. |
---|
229 | (preference, queue, args) = getPrintQueue(command, sys.argv) |
---|
230 | |
---|
231 | # Figure out if the queue exists on the local cupsd, and if so, if |
---|
232 | # it's an Athena queue. |
---|
233 | # Note that the "local" cupsd might not be on localhost; client.conf or |
---|
234 | # CUPS_SERVER might have it pointing to e.g. cups.mit.edu, but that's okay. |
---|
235 | url = getCupsURI(queue) |
---|
236 | if url: |
---|
237 | proto = rest = hostport = path = host = port = None |
---|
238 | |
---|
239 | (proto, rest) = urllib.splittype(url) |
---|
240 | if rest: |
---|
241 | (hostport, path) = urllib.splithost(rest) |
---|
242 | if hostport: |
---|
243 | (host, port) = urllib.splitport(hostport) |
---|
244 | if (proto and host and path and |
---|
245 | proto == 'ipp' and |
---|
246 | host.lower() in cups_frontends + cups_backends): |
---|
247 | # Canonicalize the queue name to Athena's, in case someone |
---|
248 | # has a local printer called something memorable like 'w20' |
---|
249 | # that points to 'ajax' or something |
---|
250 | if path[0:10] == '/printers/': |
---|
251 | queue = path[10:] |
---|
252 | elif path[0:9] == '/classes/': |
---|
253 | queue = path[9:] |
---|
254 | else: # we can't parse CUPS' URL, punt to CUPS |
---|
255 | execCups(command, queue, args) |
---|
256 | else: |
---|
257 | execCups(command, queue, args) |
---|
258 | |
---|
259 | # Get rid of any instance on the queue name |
---|
260 | # TODO The purpose of instances is to have different sets of default |
---|
261 | # options. Queues may also have default options on the null |
---|
262 | # instance. Figure out if we need to do anything about them |
---|
263 | queue = queue.split('/')[0] |
---|
264 | |
---|
265 | # If we're still here, the queue is definitely an Athena print |
---|
266 | # queue; it was either in the local cupsd pointing to Athena, or the |
---|
267 | # local cupsd didn't know about it. |
---|
268 | # Figure out what Athena thinks the backend server is, and whether |
---|
269 | # that server is running a cupsd; if not, fall back to LPRng |
---|
270 | |
---|
271 | pcap = hesiod_lookup(queue, 'pcap') |
---|
272 | rm = None |
---|
273 | if pcap: |
---|
274 | for field in pcap[0].split(':'): |
---|
275 | if field[0:3] == 'rm=': |
---|
276 | rm = field[3:] |
---|
277 | if not rm: |
---|
278 | # In the unlikely event we're wrong about it being an Athena |
---|
279 | # print queue, the local cupsd is good enough |
---|
280 | execCups(command, queue, args) |
---|
281 | |
---|
282 | try: |
---|
283 | # See if rm is running a cupsd. If not, assume it's an LPRng server. |
---|
284 | s = socket.socket() |
---|
285 | s.settimeout(0.3) |
---|
286 | s.connect((rm, 631)) |
---|
287 | s.close() |
---|
288 | except (socket.error, socket.timeout): |
---|
289 | execLprng(command, queue, args) |
---|
290 | |
---|
291 | os.environ['CUPS_SERVER'] = rm |
---|
292 | if preference == 'lprng': |
---|
293 | args = translate_lprng_args_to_cups(command, args) |
---|
294 | execCups(command, queue, args) |
---|