Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/platform/pycups_printing.py : 39%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# This file is part of Xpra.
3# Copyright (C) 2014-2019 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7#default implementation using pycups
8import sys
9import os
10import time
11from subprocess import PIPE, Popen
12import shlex
13from threading import Lock
14import cups
16from xpra.os_util import OSX, bytestostr
17from xpra.util import engs, envint, envbool, parse_simple_dict
18from xpra.log import Logger
20log = Logger("printing")
22SIMULATE_PRINT_FAILURE = envint("XPRA_SIMULATE_PRINT_FAILURE")
24RAW_MODE = envbool("XPRA_PRINTER_RAW", False)
25GENERIC = envbool("XPRA_PRINTERS_GENERIC", True)
26FORWARDER_TMPDIR = os.environ.get("XPRA_FORWARDER_TMPDIR", os.environ.get("TMPDIR", "/tmp"))
27#the mimetype to use for clients that do not specify one
28#(older clients just assumed postscript)
29DEFAULT_MIMETYPE = os.environ.get("XPRA_PRINTER_DEFAULT_MIMETYPE", "application/postscript")
31LPADMIN = "lpadmin"
32LPINFO = "lpinfo"
33ADD_OPTIONS = ["-E", "-o printer-is-shared=false", "-u allow:$USER"]
35FORWARDER_BACKEND = "xpraforwarder"
37SKIPPED_PRINTERS = os.environ.get("XPRA_SKIPPED_PRINTERS", "Cups-PDF").split(",")
38CUPS_OPTIONS_WHITELIST = os.environ.get("XPRA_CUPS_OPTIONS_WHITELIST", "Resolution,PageSize").split(",")
40#PRINTER_PREFIX = "Xpra:"
41ADD_LOCAL_PRINTERS = envbool("XPRA_ADD_LOCAL_PRINTERS", False)
42PRINTER_PREFIX = ""
43if ADD_LOCAL_PRINTERS:
44 #this prevents problems where we end up deleting local printers!
45 PRINTER_PREFIX = "Xpra:"
46PRINTER_PREFIX = os.environ.get("XPRA_PRINTER_PREFIX", PRINTER_PREFIX)
48DEFAULT_CUPS_DBUS = int(not OSX)
49CUPS_DBUS = envint("XPRA_CUPS_DBUS", DEFAULT_CUPS_DBUS)
50POLLING_DELAY = envint("XPRA_CUPS_POLLING_DELAY", 60)
51log("pycups settings: DEFAULT_CUPS_DBUS=%s, CUPS_DBUS=%s, POLLING_DELAY=%s",
52 DEFAULT_CUPS_DBUS, CUPS_DBUS, POLLING_DELAY)
53log("pycups settings: PRINTER_PREFIX=%s, ADD_LOCAL_PRINTERS=%s",
54 PRINTER_PREFIX, ADD_LOCAL_PRINTERS)
55log("pycups settings: FORWARDER_TMPDIR=%s", FORWARDER_TMPDIR)
56log("pycups settings: SKIPPED_PRINTERS=%s", SKIPPED_PRINTERS)
58MIMETYPE_TO_PRINTER = {
59 "application/postscript" : "Generic PostScript Printer",
60 "application/pdf" : "Generic PDF Printer",
61 }
62MIMETYPE_TO_PPD = {
63 "application/postscript" : "CUPS-PDF.ppd",
64 "application/pdf" : "Generic-PDF_Printer-PDF.ppd",
65 }
68dco = os.environ.get("XPRA_DEFAULT_CUPS_OPTIONS", "fit-to-page=True")
69DEFAULT_CUPS_OPTIONS = parse_simple_dict(dco)
70log("DEFAULT_CUPS_OPTIONS=%s", DEFAULT_CUPS_OPTIONS)
73#allows us to inject the lpadmin and lpinfo commands from the config file
74def set_lpadmin_command(lpadmin):
75 global LPADMIN
76 LPADMIN = lpadmin
78def set_add_printer_options(options):
79 global ADD_OPTIONS
80 ADD_OPTIONS = options
82def set_lpinfo_command(lpinfo):
83 global LPINFO
84 LPINFO = lpinfo
87def find_ppd_file(short_name, filename):
88 ev = os.environ.get("XPRA_%s_PPD" % short_name)
89 if ev and os.path.exists(ev):
90 log("using environment override for %s ppd file: %s", short_name, ev)
91 return ev
92 paths = []
93 for p in os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":"):
94 if os.path.exists(p) and os.path.isdir(p):
95 paths.append(os.path.join(p, "cups", "model")) #used on Fedora and others
96 paths.append(os.path.join(p, "ppd", "cups-pdf")) #used on Fedora and others
97 paths.append(os.path.join(p, "ppd", "cupsfilters"))
98 log("find ppd file: paths=%s", paths)
99 for p in paths:
100 f = os.path.join(p, filename)
101 if os.path.exists(f) and os.path.isfile(f):
102 return f
103 log("cannot find %s in %s", filename, paths)
104 return None
107def get_lpinfo_drv(make_and_model):
108 if not LPINFO:
109 log.error("Error: lpinfo command is not defined")
110 return None
111 command = shlex.split(LPINFO)+["--make-and-model", make_and_model, "-m"]
112 log("get_lpinfo_drv(%s) command=%s", make_and_model, command)
113 try:
114 proc = Popen(command, stdout=PIPE, stderr=PIPE, start_new_session=True)
115 except Exception as e:
116 log("get_lp_info_drv(%s) lpinfo command %s failed", make_and_model, command, exc_info=True)
117 log.error("Error: lpinfo command failed to run")
118 log.error(" %s", e)
119 log.error(" command used: '%s'", " ".join(command))
120 return None
121 #use the global child reaper to make sure this doesn't end up as a zombie
122 from xpra.child_reaper import getChildReaper
123 cr = getChildReaper()
124 cr.add_process(proc, "lpinfo", command, ignore=True, forget=True)
125 from xpra.make_thread import start_thread
126 def watch_lpinfo():
127 #give it 15 seconds to run:
128 for _ in range(15):
129 if proc.poll() is not None:
130 return #finished already
131 time.sleep(1)
132 if proc.poll() is not None:
133 return
134 log.warn("Warning: lpinfo command is taking too long,")
135 log.warn(" is the cups server running?")
136 try:
137 proc.terminate()
138 except Exception as e:
139 log("%s.terminate()", proc, exc_info=True)
140 log.error("Error: failed to terminate lpinfo command")
141 log.error(" %s", e)
142 start_thread(watch_lpinfo, "lpinfo watcher", daemon=True)
143 out, err = proc.communicate()
144 if proc.wait()!=0:
145 log.warn("Warning: lpinfo command failed and returned %s", proc.returncode)
146 log.warn(" command used: '%s'", " ".join(command))
147 return None
148 try:
149 out = out.decode()
150 except Exception:
151 out = str(out)
152 log("lpinfo out=%r", out)
153 log("lpinfo err=%r", err)
154 if err:
155 log.warn("Warning: lpinfo command produced some warnings:")
156 log.warn(" %r", err)
157 for line in out.splitlines():
158 if line.startswith("drv://"):
159 return line.split(" ")[0]
160 return None
163UNPROBED_PRINTER_DEFS = {}
164def add_printer_def(mimetype, definition):
165 if definition.startswith("drv://"):
166 UNPROBED_PRINTER_DEFS[mimetype] = ["-m", definition]
167 elif definition.lower().endswith("ppd"):
168 if os.path.exists(definition):
169 UNPROBED_PRINTER_DEFS[mimetype] = ["-P", definition]
170 else:
171 log.warn("Warning: ppd file '%s' does not exist", definition)
172 else:
173 log.warn("Warning: invalid printer definition for %s:", mimetype)
174 log.warn(" '%s' is not a valid driver or ppd file", definition)
177PRINTER_DEF = None
178PRINTER_DEF_LOCK = Lock()
179def get_printer_definitions():
180 global PRINTER_DEF, PRINTER_DEF_LOCK, UNPROBED_PRINTER_DEFS, MIMETYPE_TO_PRINTER
181 with PRINTER_DEF_LOCK:
182 if PRINTER_DEF is not None:
183 return PRINTER_DEF
184 log("get_printer_definitions() UNPROBED_PRINTER_DEFS=%s, GENERIC=%s", UNPROBED_PRINTER_DEFS, GENERIC)
185 from xpra.platform.printing import get_mimetypes
186 mimetypes = get_mimetypes()
187 #first add the user-supplied definitions:
188 PRINTER_DEF = UNPROBED_PRINTER_DEFS.copy()
189 #raw mode if supported:
190 if RAW_MODE:
191 PRINTER_DEF["raw"] = ["-o", "raw"]
192 #now probe for generic printers via lpinfo:
193 if GENERIC:
194 for mt in mimetypes:
195 if mt in PRINTER_DEF:
196 continue #we have a pre-defined one already
197 x = MIMETYPE_TO_PRINTER.get(mt)
198 if not x:
199 log.warn("Warning: unknown mimetype '%s', cannot find printer definition", mt)
200 continue
201 drv = get_lpinfo_drv(x)
202 if drv:
203 #ie: ["-m", "drv:///sample.drv/generic.ppd"]
204 PRINTER_DEF[mt] = ["-m", drv]
205 #fallback to locating ppd files:
206 for mt in mimetypes:
207 if mt in PRINTER_DEF:
208 continue #we have a generic or pre-defined one already
209 x = MIMETYPE_TO_PPD.get(mt)
210 if not x:
211 log.warn("Warning: unknown mimetype '%s', cannot find corresponding PPD file", mt)
212 continue
213 f = find_ppd_file(mt.replace("application/", "").upper(), x)
214 if f:
215 #ie: ["-P", "/usr/share/cups/model/Generic-PDF_Printer-PDF.ppd"]
216 PRINTER_DEF[mt] = ["-P", f]
217 log("pycups settings: PRINTER_DEF=%s", PRINTER_DEF)
218 return PRINTER_DEF
220def get_printer_definition(mimetype):
221 v = get_printer_definitions().get("application/%s" % mimetype)
222 if not v:
223 return ""
224 if len(v)!=2:
225 return ""
226 if v[0] not in ("-m", "-P"):
227 return ""
228 return v[1] #ie: /usr/share/ppd/cupsfilters/Generic-PDF_Printer-PDF.ppd
231def validate_setup():
232 #very simple check: at least one ppd file exists
233 defs = get_printer_definitions()
234 if not defs:
235 return False
236 return defs
239def exec_lpadmin(args, success_cb=None):
240 command = shlex.split(LPADMIN)+args
241 log("exec_lpadmin(%s) command=%s", args, command)
242 proc = Popen(command, start_new_session=True)
243 #use the global child reaper to make sure this doesn't end up as a zombie
244 from xpra.child_reaper import getChildReaper
245 cr = getChildReaper()
246 def check_returncode(_proc_cb):
247 returncode = proc.poll()
248 log("returncode(%s)=%s", command, returncode)
249 if returncode!=0:
250 log.warn("lpadmin failed and returned error code: %s", returncode)
251 from xpra.platform import get_username
252 log.warn(" verify that user '%s' has all the required permissions", get_username())
253 log.warn(" for running: '%s'", LPADMIN)
254 log.warn(" full command: %s", " ".join("'%s'" % x for x in command))
255 elif success_cb:
256 success_cb()
257 cr.add_process(proc, "lpadmin", command, ignore=True, forget=True, callback=check_returncode)
258 if proc.poll() not in (None, 0):
259 raise Exception("lpadmin command '%s' failed and returned %s" % (command, proc.poll()))
262def sanitize_name(name):
263 import string
264 name = name.replace(" ", "-")
265 valid_chars = "-_.:%s%s" % (string.ascii_letters, string.digits)
266 return ''.join(c for c in name if c in valid_chars)
268def add_printer(name, options, info, location, attributes, success_cb=None):
269 log("add_printer%s", (name, options, info, location, attributes, success_cb))
270 mimetypes = options.strtupleget("mimetypes", (DEFAULT_MIMETYPE,))
271 if not mimetypes:
272 log.error("Error: no mimetypes specified for printer '%s'", name)
273 return
274 xpra_printer_name = PRINTER_PREFIX+sanitize_name(name)
275 if xpra_printer_name in get_all_printers():
276 log.warn("Warning: not adding duplicate printer '%s'", name)
277 return
278 #find a matching definition:
279 mimetype, printer_def = None, None
280 defs = get_printer_definitions()
281 for mt in mimetypes:
282 printer_def = defs.get(mt)
283 if printer_def:
284 log("using printer definition '%s' for %s", printer_def, mt)
285 #ie: printer_def = ["-P", "/path/to/CUPS-PDF.ppd"]
286 mimetype = mt
287 attributes["mimetype"] = mimetype
288 break
289 if not printer_def:
290 log.error("Error: cannot add printer '%s':", name)
291 log.error(" the printing system does not support %s", " or ".join(mimetypes))
292 return
293 from urllib.parse import urlencode #@UnresolvedImport @UnusedImport
294 command = [
295 "-p", xpra_printer_name,
296 "-v", FORWARDER_BACKEND+":"+FORWARDER_TMPDIR+"?"+urlencode(attributes),
297 "-D", info,
298 "-L", location,
299 ]
300 if ADD_OPTIONS:
301 #ie: ["-E", "-o printer-is-shared=false", "-u allow:$USER"]
302 for opt in ADD_OPTIONS:
303 parts = shlex.split(opt) #ie: "-u allow:$USER" -> ["-u", "allow:$USER"]
304 for part in parts: #ie: "allow:$USER"
305 command.append(os.path.expandvars(part))
306 command += printer_def
307 #add attributes:
308 log("pycups_printing adding printer: %s", command)
309 exec_lpadmin(command, success_cb=success_cb)
311def remove_printer(name):
312 log("remove_printer(%s)", name)
313 exec_lpadmin(["-x", PRINTER_PREFIX+sanitize_name(name)])
316dbus_init = None
317printers_modified_callback = None
318DBUS_PATH="/com/redhat/PrinterSpooler"
319DBUS_IFACE="com.redhat.PrinterSpooler"
320def handle_dbus_signal(*args):
321 global printers_modified_callback
322 log("handle_dbus_signal(%s) printers_modified_callback=%s", args, printers_modified_callback)
323 if printers_modified_callback:
324 printers_modified_callback()
326def init_dbus_listener():
327 if not CUPS_DBUS:
328 return False
329 global dbus_init
330 log("init_dbus_listener() dbus_init=%s", dbus_init)
331 if dbus_init is None:
332 try:
333 from xpra.dbus.common import init_system_bus
334 system_bus = init_system_bus()
335 log("system bus: %s", system_bus)
336 sig_match = system_bus.add_signal_receiver(handle_dbus_signal, path=DBUS_PATH, dbus_interface=DBUS_IFACE)
337 log("system_bus.add_signal_receiver(..)=%s", sig_match)
338 dbus_init = True
339 except ImportError as e:
340 log.warn("Warning: cannot watch for printer device changes,")
341 log.warn(" the dbus bindings seem to be missing:")
342 log.warn(" %s", e)
343 dbus_init = False
344 except Exception:
345 if OSX:
346 log("no dbus on osx")
347 else:
348 log.error("failed to initialize dbus cups event listener", exc_info=True)
349 dbus_init = False
350 return dbus_init
352def check_printers():
353 global printers_modified_callback
354 #we don't actually check anything here and just
355 #fire the callback every time, relying in client_base
356 #to notice that nothing has changed and avoid sending the same printers to the server
357 log("check_printers() printers_modified_callback=%s", printers_modified_callback)
358 if printers_modified_callback:
359 printers_modified_callback()
360 schedule_polling_timer()
362_polling_timer = None
363def schedule_polling_timer():
364 #fallback to polling:
365 cancel_polling_timer()
366 from threading import Timer
367 global _polling_timer
368 _polling_timer = Timer(POLLING_DELAY, check_printers)
369 _polling_timer.start()
370 log("schedule_polling_timer() timer=%s", _polling_timer)
372def cancel_polling_timer():
373 global _polling_timer
374 pt = _polling_timer
375 log("cancel_polling_timer() timer=%s", pt)
376 if pt:
377 try:
378 _polling_timer = None
379 pt.cancel()
380 except Exception:
381 log("error cancelling polling timer %s", pt, exc_info=True)
383def init_printing(callback=None):
384 global printers_modified_callback
385 log("init_printing(%s) printers_modified_callback=%s", callback, printers_modified_callback)
386 printers_modified_callback = callback
387 if not init_dbus_listener():
388 log("init_printing(%s) will use polling", callback)
389 schedule_polling_timer()
391def cleanup_printing():
392 cancel_polling_timer()
395def get_printers():
396 all_printers = get_all_printers()
397 return dict((k,v) for k,v in all_printers.items() if k not in SKIPPED_PRINTERS)
399def get_all_printers():
400 conn = cups.Connection()
401 printers = conn.getPrinters()
402 log("pycups.get_all_printers()=%s", printers)
403 return printers
405def get_default_printer():
406 conn = cups.Connection()
407 return conn.getDefault()
409def get_printer_attributes(name):
410 conn = cups.Connection()
411 return conn.getPrinterAttributes(name)
414def print_files(printer, filenames, title, options):
415 if printer not in get_printers():
416 raise Exception("invalid printer: '%s'" % printer)
417 log("pycups.print_files%s", (printer, filenames, title, options))
418 actual_options = DEFAULT_CUPS_OPTIONS.copy()
419 s = bytestostr
420 used_options = dict((s(k),s(v)) for k,v in options.items() if s(k) in CUPS_OPTIONS_WHITELIST)
421 unused_options = dict((s(k),s(v)) for k,v in options.items() if s(k) not in CUPS_OPTIONS_WHITELIST)
422 log("used options=%s", used_options)
423 log("unused options=%s", unused_options)
424 actual_options.update(used_options)
425 if SIMULATE_PRINT_FAILURE:
426 log.warn("Warning: simulating print failure")
427 conn = None
428 printpid = -1
429 else:
430 conn = cups.Connection()
431 log("calling printFiles on %s", conn)
432 printpid = conn.printFiles(printer, filenames, title, actual_options)
433 if printpid<=0:
434 log.error("Error: pycups printing on '%s' failed for file%s", printer, engs(filenames))
435 for f in filenames:
436 log.error(" %s", f)
437 log.error(" using cups server connection: %s", conn)
438 if actual_options:
439 log.error(" printer options:")
440 for k,v in actual_options.items():
441 log.error(" %-24s : %s", k, v)
442 else:
443 log("pycups %s.printFiles%s=%s", conn, (printer, filenames, title, actual_options), printpid)
444 return printpid
446def printing_finished(printpid):
447 conn = cups.Connection()
448 f = conn.getJobs().get(printpid, None) is None
449 log("pycups.printing_finished(%s)=%s", printpid, f)
450 return f
453PRINTER_STATE = {
454 3 : "idle",
455 4 : "printing",
456 5 : "stopped",
457 }
460def get_info():
461 from xpra.platform.printing import get_mimetypes, DEFAULT_MIMETYPES
462 return {"mimetypes" : {"" : get_mimetypes(),
463 "default" : DEFAULT_MIMETYPES,
464 "printers" : MIMETYPE_TO_PRINTER,
465 "ppd" : MIMETYPE_TO_PPD},
466 "mimetype" : {"default" : DEFAULT_MIMETYPE},
467 "simulate-failure" : SIMULATE_PRINT_FAILURE,
468 "raw-mode" : RAW_MODE,
469 "generic" : GENERIC,
470 "tmpdir" : FORWARDER_TMPDIR,
471 "lpadmin" : LPADMIN,
472 "lpinfo" : LPINFO,
473 "forwarder" : FORWARDER_BACKEND,
474 "skipped-printers" : SKIPPED_PRINTERS,
475 "add-local-printers": ADD_LOCAL_PRINTERS,
476 "printer-prefix" : PRINTER_PREFIX,
477 "cups-dbus" : {"" : CUPS_DBUS,
478 "default" : DEFAULT_CUPS_DBUS,
479 "poll-delay" : POLLING_DELAY},
480 "cups.default-options" : DEFAULT_CUPS_OPTIONS,
481 "printers" : {"" : get_printer_definitions(),
482 "predefined" : UNPROBED_PRINTER_DEFS},
483 }
486def main():
487 for arg in list(sys.argv):
488 if arg in ("-v", "--verbose"):
489 from xpra.log import add_debug_category, enable_debug_for
490 add_debug_category("printing")
491 enable_debug_for("printing")
492 sys.argv.remove(arg)
494 from xpra.platform import program_context
495 from xpra.log import enable_color
496 with program_context("PyCUPS Printing"):
497 enable_color()
498 validate_setup()
499 log.info("")
500 log.info("printer definitions:")
501 for k,v in get_printer_definitions().items():
502 log.info("* %-32s: %s", k, v)
503 log.info("")
504 log.info("local printers:")
505 try:
506 printers = get_printers()
507 except RuntimeError as e:
508 log.error("Error accessing the printing system")
509 log.error(" %s", e)
510 else:
511 for k,d in get_all_printers().items():
512 log.info("* %s%s", k, [" (NOT EXPORTED)", ""][int(k in printers)])
513 for pk, pv in d.items():
514 if pk=="printer-state" and pv in PRINTER_STATE:
515 pv = "%s (%s)" % (pv, PRINTER_STATE.get(pv))
516 log.info(" %-32s: %s", pk, pv)
519if __name__ == "__main__":
520 main()