Hide keyboard shortcuts

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. 

6 

7#default implementation using pycups 

8import sys 

9import os 

10import time 

11from subprocess import PIPE, Popen 

12import shlex 

13from threading import Lock 

14import cups 

15 

16from xpra.os_util import OSX, bytestostr 

17from xpra.util import engs, envint, envbool, parse_simple_dict 

18from xpra.log import Logger 

19 

20log = Logger("printing") 

21 

22SIMULATE_PRINT_FAILURE = envint("XPRA_SIMULATE_PRINT_FAILURE") 

23 

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") 

30 

31LPADMIN = "lpadmin" 

32LPINFO = "lpinfo" 

33ADD_OPTIONS = ["-E", "-o printer-is-shared=false", "-u allow:$USER"] 

34 

35FORWARDER_BACKEND = "xpraforwarder" 

36 

37SKIPPED_PRINTERS = os.environ.get("XPRA_SKIPPED_PRINTERS", "Cups-PDF").split(",") 

38CUPS_OPTIONS_WHITELIST = os.environ.get("XPRA_CUPS_OPTIONS_WHITELIST", "Resolution,PageSize").split(",") 

39 

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) 

47 

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) 

57 

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 } 

66 

67 

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) 

71 

72 

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 

77 

78def set_add_printer_options(options): 

79 global ADD_OPTIONS 

80 ADD_OPTIONS = options 

81 

82def set_lpinfo_command(lpinfo): 

83 global LPINFO 

84 LPINFO = lpinfo 

85 

86 

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 

105 

106 

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 

161 

162 

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) 

175 

176 

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 

219 

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 

229 

230 

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 

237 

238 

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())) 

260 

261 

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) 

267 

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) 

310 

311def remove_printer(name): 

312 log("remove_printer(%s)", name) 

313 exec_lpadmin(["-x", PRINTER_PREFIX+sanitize_name(name)]) 

314 

315 

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() 

325 

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 

351 

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() 

361 

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) 

371 

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) 

382 

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() 

390 

391def cleanup_printing(): 

392 cancel_polling_timer() 

393 

394 

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) 

398 

399def get_all_printers(): 

400 conn = cups.Connection() 

401 printers = conn.getPrinters() 

402 log("pycups.get_all_printers()=%s", printers) 

403 return printers 

404 

405def get_default_printer(): 

406 conn = cups.Connection() 

407 return conn.getDefault() 

408 

409def get_printer_attributes(name): 

410 conn = cups.Connection() 

411 return conn.getPrinterAttributes(name) 

412 

413 

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 

445 

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 

451 

452 

453PRINTER_STATE = { 

454 3 : "idle", 

455 4 : "printing", 

456 5 : "stopped", 

457 } 

458 

459 

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 } 

484 

485 

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) 

493 

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) 

517 

518 

519if __name__ == "__main__": 

520 main()