#!/usr/bin/python2.6 import dbus, sys, os, time, signal, re import traceback import cups, cupshelpers import subprocess from syslog import * try: import usb except: pass def get_hplip_uris_for_usb (fax=False, checkuri=None): hpuris = [] env = dict() env.update (os.environ) env['LC_ALL'] = 'C' devnull = file ("/dev/null", "r+") try: p = subprocess.Popen (['/sbin/lsusb'], env=env, stdin=devnull, stdout=subprocess.PIPE, stderr=devnull) (stdout, stderr) = p.communicate () lsusboutput = stdout.split ('\n') except: if checkuri: return False else: return hpuris for line in lsusboutput: if (line.find ("ID 03f0:") < 0): continue bus = line[4:7] device = line[15:18] if fax: type="-f" else: type="-c" try: p = subprocess.Popen (["hp-makeuri", "-lnone", type, "%s:%s" % (bus, device)], env=env, stdin=devnull, stdout=subprocess.PIPE, stderr=devnull) (stdout, stderr) = p.communicate () uri = stdout.split ('\n')[0].strip () except: continue if (not uri): continue if checkuri and checkuri == uri: return True hpuris.append (uri) if checkuri: return False else: return hpuris class HalPrinter: def __init__(self): self.get_properties() self.uris = None self.hp_fax_uris = None try: self.cups_connection = cups.Connection() except RuntimeError, e: syslog (LOG_ERR, "Unable to connect to CUPS: '%s'. Is CUPS running?" % e) sys.exit (1) def get_properties(self): self.properties = {} for key, value in os.environ.iteritems(): if key.startswith("HAL_PROP_"): name = key[9:].lower().replace("_", '.') self.properties[name] = value self.uid = os.getenv("UDI", "") if re.search ("_if\d+$", self.uid): syslog (LOG_DEBUG, "hal_lpadmin triggered by low-level USB device") else: syslog (LOG_DEBUG, "hal_lpadmin triggered by usblp kernel module") self.read() def fetch_device_id(self): p = self.properties devidstr = '' buses = usb.busses() usb_configuration_value = p.get ("usb.configuration.value") usb_interface_number = p.get ("usb.interface.number") for bus in buses: if int (bus.dirname) != int (p.get ("usb.bus.number")): continue for dev in bus.devices: if int (dev.filename) != int (p.get("usb.linux.device.number")): continue for config in dev.configurations: if (usb_configuration_value != None and (int (config.value) != int (usb_configuration_value))): continue for intf in config.interfaces: interfaceNumber = int (intf[0].interfaceNumber) if (usb_interface_number != None and interfaceNumber != usb_interface_number): continue syslog (LOG_DEBUG, "Device %s:%s: %s" % (bus.dirname, dev.filename, p.get ("info.udi"))) handle = dev.open () #handle.setConfiguration (config) handle.claimInterface (intf[0]) handle.setAltInterface (intf[0]) intfno = intf[0].interfaceNumber alt = intf[0].alternateSetting devidarr = handle.controlMsg (requestType = 0xa1, request = 0, value = config.value - 1, index = (alt + (intfno << 8)), buffer = 4096, timeout = 100) siz = len(devidarr) len0 = devidarr[0] len1 = devidarr[1] devidlen = (((len0 & 255) << 8) + (len1 & 255)) devidlen2 = (((len1 & 255) << 8) + (len0 & 255)) if devidlen > siz: if devidlen2 > siz: devidlen = siz else: devidlen = devidlen2 devidstr = devidarr[2:] syslog (LOG_DEBUG, "Device ID for %s:%s: %s" % (bus.dirname, dev.filename, devidstr)) break if len (devidstr) > 0: break if usb_configuration_value != None: break break break return devidstr def read(self): p = self.properties fetch_id = False if p.get ("printer.vendor") == None: fetch_id = True # Printer 1284 device URI not supplied by HAL, so we poll # it from the printer and add it to the HAL database # entry. This happens usually if the printer is detected # via low-level USB. if p.get ("usb.bus.number") == None or \ p.get ("usb.linux.device.number") == None: fetch_id = False if fetch_id: syslog (LOG_DEBUG, "Polling device ID from the printer") try: devidstr = self.fetch_device_id () except Exception, e: # We cannot read out the device ID from the printer, probably # because the low-level device access is clamed by the usblp # kernel module. So stop this instance of hal_lpadmin so that # the instance triggered by the kernel module does its job for # us. syslog (LOG_DEBUG, "Failed to fetch device ID: %s" % str (e)) fetch_id = False if fetch_id: # Write it into the HAL database. try: self.device_id = devidstr id_dict = cupshelpers.parseDeviceID (devidstr) self.make = (id_dict["MFG"] or p.get("usb.vendor", "Unknown")) p["printer.vendor"] = self.make self.model = (id_dict["MDL"] or p.get("usb.product", "Unknown")) p["printer.product"] = self.model self.description = id_dict["DES"] p["printer.description"] = self.description self.name = self.get_name() self.faxname = self.name + "_fax" self.commandsets = id_dict["CMD"] p["printer.commandset"] = '\t'.join (self.commandsets) self.serial = (id_dict["SN"] or p.get("usb.serial", None)) if self.serial != None: p["printer.serial"] = self.serial devnull = file ("/dev/null", "r+") env = dict() env.update (os.environ) env["LC_ALL"] = "C" for key in ("printer.vendor", "printer.product", "printer.description", "printer.serial"): if p.get (key) == None: continue try: pr = subprocess.Popen (["hal-set-property", "--udi=%s" % self.uid, "--key=%s" % key, "--string=%s" % p[key]], env=env, stdin=devnull, stdout=devnull, stderr=subprocess.STDOUT) pr.wait () except: pass for cs in self.commandsets: try: pr = subprocess.Popen (["hal-set-property", "--udi=%s" % self.uid, "--key=printer.commandset", "--strlist-post=%s" % cs], env=env, stdin=devnull, stdout=devnull, stderr=subprocess.STDOUT) pr.wait () except: pass syslog (LOG_DEBUG, "Written device ID into HAL database entry") except Exception, e: syslog (LOG_DEBUG, "Failed to write device ID into HAL database entry: %s" % str (e)) fetch_id = False if not fetch_id: # Printer device ID is already available in the HAL database. # Either HAL has already put it there (triggered by the usblp # kernel module) or hal_lpadmin has read it from the printer # when it was turned on. syslog (LOG_DEBUG, "Using device ID from HAL database entry") self.make = (p.get("printer.vendor", "") or p.get("usb.vendor", "Unknown")) self.model = (p.get("printer.product", "") or p.get("usb.product", "Unknown")) self.description = p.get("printer.description", "") self.name = self.get_name() self.faxname = self.name + "_fax" self.commandsets = p.get('printer.commandset', '').split('\t') self.serial = p.get("printer.serial", "") # Reconstruct Device ID ready to put it into the PPD file. devidstr = '' for (field, value) in [("MFG:", self.make), ("MDL:", self.model), ("DES:", self.description), ("CMD:", reduce (lambda x, y: x + ',' + y, self.commandsets)), ("SN:", self.serial)]: if len (value) > 0: devidstr += field + value + ";" self.device_id = devidstr def get_name(self): # XXX check for unallowed chars if self.properties.has_key("usb.port_number"): name = "%s-%s" % (self.model, self.properties["usb.port_number"]) else: name = self.model name = name.replace(" ", "-") name = name.replace("/", "-") return name.replace("#", "-") def get_cups_uris(self, removed=False): if self.uris != None: return self.uris uris=["hal://%s" % self.uid] if self.properties.has_key("printer.vendor"): vendor = self.properties["printer.vendor"].lower () if ((self.properties.get("linux.subsystem","") == "usb" or self.properties.get("info.solaris.driver","") == "usbprn") and self.properties.has_key("printer.product")): # Use a 'usb:...' URI. Use the same method the CUPS # usb backend uses to construct it. make = self.properties["printer.vendor"] model = self.properties["printer.product"] serial = self.properties.get ("printer.serial", None) if vendor == "hewlett-packard": make = "HP" elif vendor == "lexmark international": make = "Lexmark" if model.lower ().startswith (make.lower ()): model = model[len (make):] model = model.lstrip () model = model.rstrip () uri = "usb://%s/%s" % (make, model) uri = uri.replace (" ", "%20") if serial: uri += "?serial=%s" % serial uris.insert (0, uri) if (not removed and (vendor == "hewlett-packard" or vendor == "hp")): # Perhaps HPLIP can drive this device. If so, we # should use an 'hp:...' URI for CUPS. try: # Try to match serial number (support for having more # than one printer of the same model on one machine) hpuris = get_hplip_uris_for_usb () matchfound = 0 if self.properties.has_key("printer.serial"): for uri in hpuris: s = uri.find ("?serial=") if s == -1: continue s += 8 e = uri[s:].find ("?") if e == -1: e = len (uri) serial = uri[s:s+e] if serial == self.properties["printer.serial"]: uris.insert (0, uri) matchfound = 1 break # Fall back to printer model if matchfound == 0: for uri in hpuris: s = uri.find ("/usb/") if s == -1: s = uri.find ("/par/") if s == -1: s = uri.find ("/net/") if s == -1: continue s += 5 e = uri[s:].find ("?") if e == -1: e = len (uri) prod = uri[s:s+e].lower ().strip () if prod.startswith ("hp_"): prod = prod[3:] halprod = self.properties["printer.product"].lower ().strip () halprod = halprod.replace (" ", "_") if halprod.startswith ("hp_"): halprod = halprod[3:] if prod == halprod: matchfound = 1 uris.insert (0, uri) break except: pass self.uris = uris return uris def get_cups_uri(self): return self.get_cups_uris()[0] def get_cups_hp_fax_uris(self, removed=False): if self.hp_fax_uris != None: return self.hp_fax_uris faxurisfound = 0 if self.properties.has_key("printer.vendor"): vendor = self.properties["printer.vendor"].lower () if (not removed and (vendor == "hewlett-packard" or vendor == "hp")): # We only can have a fax URI if we have an HP printer # supported by HPLIP try: # Try to find a matching HPLIP fax URI for the HPLIP # print URI for this device hpfaxuris = get_hplip_uris_for_usb (True) uris = self.get_cups_uris () faxuris = [] for uri in uris: if not uri.startswith ("hp:"): continue faxuri = uri.replace("hp:/", "hpfax:/") for furi in hpfaxuris: if faxuri == furi: faxurisfound = 1 faxuris.append (faxuri) except: pass if faxurisfound == 1: self.hp_fax_uris = faxuris return faxuris else: return None def get_cups_hp_fax_uri(self): faxuris = self.get_cups_hp_fax_uris() if faxuris: return faxuris[0] else: return None def store_device_id_in_ppd(self): fname = self.cups_connection.getPPD(self.name) lines = file (fname).readlines () attr = "*1284DeviceID:" has_1284_attr = reduce (lambda x, y: x or y, map (lambda x: x.startswith (attr))) outf = file (fname, 'w') written = False attrline = attr + ' ' + self.device_id + '\n' for line in lines: if not written: if has_1284_attr: if line.startswith (attr): # Replace existing attribute. line = attrline written = True else: if line == '\n': # Write attribute before first blank line. line = attrline + line written = True outf.write (line) if not written: outf.write (attrline) outf.close () self.cups_connection.addPrinter(self.name, filename=fname) os.unlink (fname) def add(self): syslog (LOG_DEBUG, "add") printers = cupshelpers.getPrinters(self.cups_connection) printers_extra_info = None uris = self.get_cups_uris () syslog (LOG_DEBUG, "URIs: %s" % uris) faxuris = self.get_cups_hp_fax_uris () syslog (LOG_DEBUG, "HPLIP Fax URIs: %s" % faxuris) printer_exists = 0 fax_exists = 0 p = None for name, printer in printers.iteritems(): if printer.is_class: continue if printer.device_uri in uris: printer_exists = 1 syslog (LOG_DEBUG, "Not adding printer: %s already exists" % name) printer_exists = 1 if not printer.enabled: if printers_extra_info == None: printers_extra_info = self.cups_connection.getPrinters() statemsg = printers_extra_info[name]["printer-state-message"] if statemsg.lower ().startswith ("unplugged"): syslog (LOG_INFO, "Re-enabling printer %s" % name) self.cups_connection.enablePrinter(name) else: syslog (LOG_INFO, "Printer %s exists but is disabled, reason: %s; " "use 'cupsenable %s' to enable it" % (name, statemsg, name)) if faxuris and printer.device_uri in faxuris: syslog (LOG_DEBUG, "Not adding fax printer: %s already exists" % name) fax_exists = 1 if not printer.enabled: if printers_extra_info == None: printers_extra_info = self.cups_connection.getPrinters() statemsg = printers_extra_info[name]["printer-state-message"] if statemsg.lower ().startswith ("unplugged"): syslog (LOG_INFO, "Re-enabling fax printer %s" % name) self.cups_connection.enablePrinter(name) else: syslog (LOG_INFO, "Fax printer %s exists but is disabled, reason: %s; " "use 'cupsenable %s' to enable it" % (name, statemsg, name)) # Make the name unique. if self.name in printers.keys (): suffix = 2 while (self.name + str (suffix)) in printers.keys (): suffix += 1 if suffix == 100: break self.name += str (suffix) # Make the faxname unique if self.faxname in printers.keys (): suffix = 2 while (self.faxname + str (suffix)) in printers.keys (): suffix += 1 if suffix == 100: break self.faxname += str (suffix) def wait_child (sig, stack): (pid, status) = os.wait () signal.signal (signal.SIGCHLD, wait_child) pid = os.fork () if pid == 0: # Child. if fax_exists == 0: # really new fax printer faxuri = self.get_cups_hp_fax_uri() else: faxuri = None if printer_exists == 0 or faxuri: # really new printer or fax - show tray icon with magnifier bus = dbus.SystemBus() try: syslog (LOG_DEBUG, "Calling GetReady") obj = bus.get_object("com.redhat.NewPrinterNotification", "/com/redhat/NewPrinterNotification") notification = dbus.Interface(obj, "com.redhat.NewPrinterNotification") notification.GetReady () except dbus.DBusException, e: syslog (LOG_DEBUG, "D-Bus method call failed: %s" % e) notification = None else: notification = None if printer_exists == 0: # really new printer - try autodetection if p == None: cupsppds = self.cups_connection.getPPDs () p = cupshelpers.ppds.PPDs (cupsppds) syslog (LOG_DEBUG, "Device ID: MFG:%s;MDL:%s;DES:%s;CMD:%s; URI:%s" % (self.make, self.model, self.description, reduce(lambda x, y: x + ',' + y, self.commandsets), self.get_cups_uri())) (status, ppdname) = p.getPPDNameFromDeviceID (self.make, self.model, self.description, self.commandsets, self.get_cups_uri()) syslog (LOG_DEBUG, "PPD: %s; Status: %d" % (ppdname, status)) info = "%s %s" % (self.make, self.model) self.cups_connection.addPrinter(self.name, device=self.get_cups_uri(), ppdname=ppdname, info=info, location=os.uname ()[1]) try: self.store_device_id_in_ppd () except: pass cupshelpers.activateNewPrinter (self.cups_connection, self.name) syslog (LOG_INFO, "Added printer %s" % self.name) if faxuri: faxname = self.faxname if p == None: cupsppds = self.cups_connection.getPPDs () p = cupshelpers.ppds.PPDs (cupsppds) (status, faxppd) = p.getPPDNameFromDeviceID ("HP", "Fax", "HP Fax", [], faxuri) info = "Fax queue for %s %s" % (self.make, self.model) self.cups_connection.addPrinter(faxname, device=faxuri, ppdname=faxppd, info=info, location=os.uname()[1]) self.cups_connection.enablePrinter(faxname) self.cups_connection.acceptJobs(faxname) syslog (LOG_INFO, "Added fax printer %s" % faxname) if notification: if faxuri and printer_exists != 0: # Only fax queue n = faxname m = self.model + " (Fax)" else: n = self.name m = self.model try: notification.NewPrinter (status, n, self.make, m, self.description, reduce(lambda x, y: x + ',' + y, self.commandsets)) except dbus.DBusException: pass elif pid == -1: pass # should handle error def remove(self): syslog (LOG_DEBUG, "remove") # Disable all print queues which print to the device which # we detected as having been removed. This prevents from # jobs being retried every 30 seconds. The jobs wait in the # queue until the device is reconnected and turned on. # # We cannot ask CUPS for the HPLIP URIs after having unplugged or # turned off the printer. So we take model name and serial number # provided by HAL and search the print queues whose URIs contain # this model name and derial number. These are then the queues # which we will disable. printers = cupshelpers.getPrinters(self.cups_connection) printers_extra_info = None make = self.properties.get ("printer.vendor", None) model = self.properties.get ("printer.product", None) serial = self.properties.get ("printer.serial", None) if not serial: serial = self.properties.get ("info.udi", None) if not serial: serial = self.properties.get ("info.parent", None) if serial: res = re.search ("usb_device_[0-9a-fA-F]+_[0-9a-fA-F]+_([0-9a-zA-Z]+)", serial) if res: resg = res.groups() serial = resg[0] bus = self.properties.get ("linux.subsystem", None) if ((bus == None) and (self.properties.get("info.solaris.driver", None) == 'usbprn')): bus = 'usb' udi = self.properties.get ("info.udi", None) if make: makel = make.lower () if makel == "hewlett-packard": make = "HP" elif makel == "lexmark international": make = "Lexmark" if model: if model.startswith (make): model = model[len (make):] model = model.lstrip () for name, printer in printers.iteritems(): if printer.is_class: continue if (((model and (printer.device_uri.find (model.replace (" ", "%20")) \ != -1 or printer.device_uri.find (model.replace (" ", "_")) \ != -1)) and (not serial or printer.device_uri.find ("serial=") == -1 or (serial and printer.device_uri.find ("serial=" + serial) != -1)) and (not bus or printer.device_uri.find (bus) != -1)) or (udi and printer.device_uri.find (udi) != -1) or (serial and printer.device_uri.find ("serial=" + serial) != -1)): syslog (LOG_DEBUG, "Found configured printer: %s" % name) if printer.enabled: if (udi and re.search ("_if\d+$", udi)) or \ ((not printer.device_uri.startswith ("hp:") and not printer.device_uri.startswith ("hpfax:")) or (bus == "usb" and printer.device_uri.find ("/usb/") != -1 and not get_hplip_uris_for_usb (False, printer.device_uri) and not get_hplip_uris_for_usb (True, printer.device_uri))): self.cups_connection.disablePrinter(name, "Unplugged or turned off") syslog (LOG_INFO, "Disabled printer %s, as the corresponding device was unplugged or turned off" % (name)) def configure(self): syslog (LOG_DEBUG, "configure") make, model = sys.stdin.readlines() if make[-1]=="\n": make = make[:-1] if model[-1]=="\n": model = model[:-1] cupsppds = self.cups_connection.getPPDs () p = cupshelpers.ppds.PPDs (cupsppds) (status, ppdname) = p.getPPDNameFromDeviceID (make, model, "", "") if not ppdname: syslog (LOG_ERR, "User-selected make/model \"%s\" \"%s\" not found" % (make, model)) return # add printer self.cups_connection.addPrinter( self.name, device=self.get_cups_uri(), ppdname=ppdname, info="Added by HAL", location=os.uname()[1]) self.cups_connection.enablePrinter(self.name) self.cups_connection.acceptJobs(self.name) syslog (LOG_INFO, "Added printer %s with user-selected make/model" % self.name) class HalLpAdmin: def __init__(self): if len(sys.argv)!=2: return self.usage() if sys.argv[1]=="--add": self.addPrinter() elif sys.argv[1]=="--remove": self.removePrinter() elif sys.argv[1]=="--configure": self.configurePrinter() else: return self.usage() def usage(self): print "Usage: hal_lpadmin (--add|--remove|--configure)" def addPrinter(self): printer = HalPrinter() if printer.make: printer.add() def removePrinter(self): printer = HalPrinter() if printer.make: printer.remove() def configurePrinter(self): printer = HalPrinter() if printer.make: printer.configure() def main(): openlog ("hal_lpadmin", 0, LOG_DAEMON) syslog (LOG_DEBUG, "Running hal_lpadmin") time.sleep (1) # Give HPLIP a chance to reconnect try: h = HalLpAdmin() except SystemExit, e: sys.exit (e) except: (type, value, tb) = sys.exc_info () tblast = traceback.extract_tb (tb, limit=None) if len (tblast): tblast = tblast[:len (tblast) - 1] for line in traceback.format_tb (tb): syslog (LOG_ERR, line.strip ()) extxt = traceback.format_exception_only (type, value) syslog (LOG_ERR, extxt[0].strip ()) main()