#!/bin/env python
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" 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)
        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()
