Logo Search packages:      
Sourcecode: jockey version File versions  Download package

detection.py

# -*- coding: UTF-8 -*-

'''Check the hardware and software environment on the computer for available
devices, query the driver database, and generate a set of handlers.

The central function is get_handlers() which checks the system for available
hardware and, if given a driver database, queries that about updates and
unknown hardware.
'''

# (c) 2007 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import os, os.path, subprocess, sys, logging, re
from glob import glob

from oslib import OSLib
import handlers, xorg_driver

#--------------------------------------------------------------------#

00035 class HardwareID:
    '''A piece of hardware is denoted by an identification type and value.

    The most common identification type is a 'modalias', but in the future we
    might support other types (such as bus/vendorid/productid, printer
    manufacturer/model name, etc.).
    '''
    _recache = {}

    def __init__(self, type, id):
        self.type = type
        self.id = id

    def __repr__(self):
        return "HardwareID('%s', '%s')" % (self.type, self.id)

    def __eq__(self, other):
        if type(self) != type(other) or self.type != other.type:
            return False

        if self.type != 'modalias':
            return self.id == other.id

        # modalias pattern matching
        if '*' in self.id:
            # if used as dictionary keys we do need to compare two patterns; in
            # that case they should just be tested for string equality
            if '*' in other.id:
                return self.id == other.id
            else:
                return self.regex(self.id).match(other.id)
        else:
            if '*' in other.id:
                return self.regex(other.id).match(self.id)
            else:
                return self.id == other.id

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        # This is far from efficient, but we usually have a very small number
        # of handlers, so it doesn't matter.

        if self.type == 'modalias':
            # since we might have patterns, we cannot rely on hash identidy of
            # id
            return hash(self.type) ^ hash(self.id[:self.id.find(':')])
        else:
            return hash(self.type) ^ hash(self.id)

    @classmethod
00087     def regex(klass, pattern):
        '''Convert modalias pattern to a regular expression.'''

        r = klass._recache.get(pattern)
        if not r:
            r = re.compile(re.escape(pattern).replace('\\*', '.*') + '$')
            klass._recache[pattern] = r
        return r

#--------------------------------------------------------------------#

00098 class DriverID:
    '''Driver database entry describing a driver.
    
    This consists of a set of (type, value) pairs, the semantics of which can
    be defined and used freely for every distribution. A few conventional
    standard types exist:
    
    - handler: a handler class name or URL
    - url: A generic URL (e. g. where to download a piece of firmware)
    - sha1sum: SHA1 checksum (e. g. for above firmware)
    - package: a package name which contains the driver
    - repository: the location of a package repository (distributor dependent)
    '''
    def __init__(self, **properties):
        self.properties = properties

    def __getitem__(self, key):
        return self.properties.__getitem__(key)

    def __contains__(self, key):
        return self.properties.__contains__(key)

#--------------------------------------------------------------------#

00122 class DriverDB:
    '''Interface definition for a driver database.
    
       This maps a HWIdentifier to a list of DriverID instances (sorted by
       preference) which match the OS version.
       '''
00128     def query(self, hwid):
        '''Return a list of applicable DriverIDs for a HardwareID.'''

        raise NotImplementedError, 'subclasses need to implement this'

#--------------------------------------------------------------------#

00135 class LocalKernelModulesDriverDB(DriverDB):
    '''DriverDB implementation for kernel modules which are already available
    in the system.
    
    This evaluates modalias lists and overrides (such as /lib/modules/<kernel
    version>/modules.alias and other alias files/directories specified in
    OSLib.modaliases) to map modaliases in /sys to kernel modules and wrapping
    them into a KernelModuleHandler.
    
    As an addition to the 'alias' lines in modalias files, you can also specify
    lines "reset <module>" which will cause the current modalias mapping that
    was built up to that point to be discarded. Since modaliases are evaluated
    in the order they appear in OSLib.modaliases, this can be used to disable
    wrong upstream modaliases (like the ones from the proprietary NVIDIA
    graphics driver).
    '''
00151     def __init__(self):
        '''Initialize self.alias_cache.
        
        This maps bus → vendor → modalias → [module].
        '''
        # bus -> vendor -> alias -> [module]; vendor == None -> no vendor,
        # or vendor patterns, needs fnmatching
        self.alias_cache = {} 
        # patterns for which we can optimize lookup
        self.vendor_pattern_re = re.compile('(pci|usb):v([0-9A-F]{4,8})(?:d|p)')

        for alias_location in OSLib.inst.modaliases:
            if not os.path.exists(alias_location):
                continue
            if os.path.isdir(alias_location):
                alias_files = [os.path.join(alias_location, f) 
                    for f in sorted(os.listdir(alias_location))]
            else:
                alias_files = [alias_location]

            for alias_file in alias_files:
                logging.debug('reading modalias file ' + alias_file)
                for line in open(alias_file):
                    try:
                        (c, a, m) = line.split()
                    except ValueError:
                        try:
                            (c, m) = line.split()
                            a = None
                        except ValueError:
                            continue

                    if c == 'alias' and a:
                        vp = self.vendor_pattern_re.match(a)
                        if vp:
                            self.alias_cache.setdefault(vp.group(1), {}).setdefault(
                                vp.group(2), {}).setdefault(a, []).append(m)
                        else:
                            colon = a.find(':')
                            if colon > 0:
                                bus = a[:colon]
                            else:
                                bus = None
                            self.alias_cache.setdefault(bus, {}).setdefault(
                                None, {}).setdefault(a, []).append(m)
                    elif c == 'reset':
                        for map in self.alias_cache.itervalues():
                            for vmap in map.itervalues():
                                for k, mods in vmap.iteritems():
                                    try:
                                        mods.remove(m)
                                    except ValueError:
                                        pass

            #for bus, inf in self.alias_cache.iteritems():
            #    print '*********', bus, '*************'
            #    for vendor, alias in inf.iteritems():
            #        print '#', vendor, ':', alias

00210     def query(self, hwid):
        '''Return a list of applicable DriverIDs for a HardwareID.'''

        if hwid.type != 'modalias' or ':' not in hwid.id:
            return []

        # we can't build large dictionaries with HardwareID as keys, that's too
        # inefficient; thus we have to do some more clever matching and data
        # structure

        # TODO: we return all matching handlers here, which is
        # confusing; picking the first one is too arbitrary, though;
        # find a good heuristics for returning the best one

        result = []

        # look up vendor patterns
        m = self.vendor_pattern_re.match(hwid.id)
        if m:
            bus = m.group(1)
            for a, mods in self.alias_cache.get(bus, {}).get(m.group(2), {}).iteritems():
                if mods and HardwareID('modalias', a) == hwid:
                    for m in mods:
                        result.append(DriverID(handler='KernelModuleHandler', module=m))
        else:
            bus = hwid.id[:hwid.id.index(':')]

        # look up the remaining ones
        for a, mods in self.alias_cache.get(bus, {}).get(None, {}).iteritems():
            if mods and HardwareID('modalias', a) == hwid:
                for m in mods:
                    result.append(DriverID(handler='KernelModuleHandler', module=m))

        return result

#--------------------------------------------------------------------#
# internal helper functions

def _connected_modaliases():
    '''Return a mapping module → [modalias HardwareID] for available hardware.
    
    The None entry contains modaliases for hardware which is not connected to a
    driver.
    '''
    if _connected_modaliases.cache:
        return _connected_modaliases.cache

    mods = {}
    for path, dirs, files in os.walk(os.path.join(OSLib.inst.sys_dir, 'devices')):
        modalias = None

        # most devices have modalias files
        if 'modalias' in files:
            modalias = open(os.path.join(path, 'modalias')).read().strip()
        # devices on SSB bus only mention the modalias in the uevent file (as
        # of 2.6.24)
        elif 'ssb' in path and 'uevent' in files:
            info = {}
            for l in open(os.path.join(path, 'uevent')):
                if l.startswith('MODALIAS='):
                    modalias = l.split('=', 1)[1].strip()
                    break

        if not modalias:
            continue

        # check whether it already has a module loaded
        modlink = os.path.join(path, 'driver', 'module')
        if os.path.islink(modlink):
            # has driver, is a kernel module
            module = os.path.basename(os.readlink(modlink))
        elif os.path.islink(os.path.join(path, 'driver')):
            # has driver statically built into kernel
            continue
        else:
            # no module
            module = None

        mods.setdefault(module, []).append(HardwareID('modalias', modalias))

    _connected_modaliases.cache = mods
    return mods

_connected_modaliases.cache = None

def _handler_license_filter(handler, mode):
    '''Filter handlers by license.
    
    Return handler if the handler is compatible with mode (MODE_FREE,
    MODE_NONFREE, or MODE_ANY), else return None.
    '''
    if mode == MODE_FREE and handler and not handler.free():
        return None
    elif mode == MODE_NONFREE and handler and handler.free():
        return None
    return handler

def _driverid_to_handler(did, ui, hpool, mode):
    '''Find handler for a DriverID from a handler pool.

    The handler pool hpool is a (name → Handler object dictionary) mapping.
    
    mode is MODE_FREE, MODE_NONFREE, or MODE_ANY; see get_handlers() for
    details.
    '''
    if 'handler' not in did:
        # we do not support this ATM
        return None

    # instantiation of kernel modules handlers; try to find a custom handler
    # with this module, and fall back to creating a standard one
    if did['handler'] in ('KernelModuleHandler', 'FirmwareHandler') and \
            'module' in did:
        for h in hpool.itervalues():
            if isinstance(h, handlers.KernelModuleHandler) and \
                h.module == did['module']:
                return _handler_license_filter(h, mode)

        # lazily initialize and check ignored modules
        if _driverid_to_handler.ignored is None:
            _driverid_to_handler.ignored = OSLib.inst.ignored_modules()
        if did['module'] in _driverid_to_handler.ignored:
            return None

        if get_modinfo(did['module']):
            # only instantiate default handlers for modules which actually
            # exist
            args = did.properties.copy()
            del args['handler']
            h = getattr(handlers, did['handler']) (ui, **args)
            hpool['auto-' + did['module']] = h
            return _handler_license_filter(h, mode)

    # default: look up handler in the handler pool and return it
    try:
        return _handler_license_filter(hpool[did['handler']], mode)
    except KeyError:
        return None

_driverid_to_handler.ignored = None

#--------------------------------------------------------------------#
# public functions

def get_modinfo(module):
    '''Return information about a kernel module.
    
    This is delivered as a dictionary; keys are property names (strings),
    values are lists of strings (some properties might have multiple
    values, such as multi-line description fields or multiple PCI
    modaliases).
    '''
    try:
        return get_modinfo.cache[module]
    except KeyError:
        pass

    proc = subprocess.Popen((OSLib.inst.modinfo_path, module),
        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (stdout, stderr) = proc.communicate()
    if proc.returncode != 0:
        logging.warning('modinfo for module %s failed: %s' % (module, stderr))
        return None

    modinfo = {}
    for line in stdout.split('\n'):
        if ':' not in line:
            continue

        (key, value) = line.split(':', 1)
        modinfo.setdefault(key.strip(), []).append(value.strip())

    get_modinfo.cache[module] = modinfo
    return modinfo

get_modinfo.cache = {}

(MODE_FREE, MODE_NONFREE, MODE_ANY) = range(3)

def get_handlers(ui, driverdb=None, handler_dir=None, mode=MODE_ANY,
    available_only=True):
    '''Return a set of handlers which are applicable on this system.
    
    ui (an AbstractUI interface) is passed to the generated handlers. If a
    DriverDB instance is given, this will be queried for unknown detected
    devices and possible handlers for them. handler_dir specifies the
    directory where the custom handlers are stored (can be a list, too); if
    None, it defaults to OSLib.handler_dir.
    
    If mode is set to MODE_FREE, this will deliver only free handlers;
    MODE_NONFREE will only deliver nonfree handlers; by default (MODE_ANY), all
    available handlers are returned, regardless of their license.

    Usually this function only returns drivers that match the available
    hardware. With available_only=False, all handlers are returned (This is
    only useful for testing, though).
    '''
    available_handlers = set()
    handler_pool = {}

    # get all custom handlers which are available
    if handler_dir == None:
        handler_dir = OSLib.inst.handler_dir
    if hasattr(handler_dir, 'isspace'):
        handler_dir = [handler_dir]
    for dir in handler_dir:
        for mod in glob(os.path.join(dir, '*.py')):
            symb = {}
            logging.debug('loading custom handler %s', mod)
            try:
                execfile(mod, symb)
            except Exception:
                logging.warning('Invalid custom handler module %s', mod,
                    exc_info=True)
                continue

            for name, obj in symb.iteritems():
                try:
                    # ignore non-Handler things; also ignore imports of
                    # standard base handlers into the global namespace
                    if not issubclass(obj, handlers.Handler) or \
                        hasattr(handlers, name) or hasattr(xorg_driver, name):
                        continue
                except TypeError:
                    continue

                try:
                    inst = obj(ui)
                except:
                    logging.debug('Could not instantiate Handler subclass %s from name %s',
                        str(obj), name, exc_info=True)
                    continue

                logging.debug('Instantiated Handler subclass %s from name %s',
                    str(obj), name)
                inst = _handler_license_filter(inst, mode)
                if not inst:
                    logging.debug('%s does not match license mode %i', str(obj), mode)
                    continue

                avail = (not available_only) or inst.available()
                if avail:
                    logging.debug('%s is available', inst.name())
                    available_handlers.add(inst)
                    handler_pool[name] = inst
                elif avail == None:
                    logging.debug('%s availability undetermined, adding to pool', inst.name())
                    handler_pool[name] = inst
                else:
                    logging.debug('%s not available', inst.name())

    logging.debug('all custom handlers loaded')

    # ask the driver db about all hardware
    if driverdb:
        for module, hwids in _connected_modaliases().iteritems():
            for hwid in hwids:
                logging.debug('querying driver db about %s', hwid)
                for did in driverdb.query(hwid):
                    h = _driverid_to_handler(did, ui, handler_pool, mode)
                    if h:
                        logging.debug('got handler %s', h)
                        available_handlers.add(h)
                    else:
                        logging.debug('no corresponding handler available')

    return available_handlers


Generated by  Doxygen 1.6.0   Back to index