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

ui.py

# (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.

'''Abstract user interface, which provides all logic and strings.

Concrete implementations need to implement a set of abstract presentation
functions with an appropriate toolkit.
'''

import gettext, optparse, logging, textwrap, os.path, urllib2, tempfile, sys
import os, time

import detection, oslib, xorg_driver

00028 class AbstractUI:
    '''Abstract user interface.

    This encapsulates the entire program logic and all strings, but does not
    implement any concrete user interface.
    '''
00034     def __init__(self):
        '''Initialize system.
        
        This parses command line arguments, detects available hardware,
        and already installed drivers and handlers.
        '''
        self.gettext_domain = 'jockey'
        (self.argv_options, self.argv_args) = self.parse_argv()

        if self.argv_options.check:
            time.sleep(self.argv_options.check)

        gettext.textdomain(self.gettext_domain)

        # set up logging
        logging.raiseExceptions = False
        if self.argv_options.debug:
            logging.basicConfig(level=logging.DEBUG, 
                format='%(asctime)s %(levelname)s: %(message)s')
        else:
            logging.basicConfig(level=logging.WARNING, 
                format='%(levelname)s: %(message)s')

        self.init_strings()

        # TODO: use a locally caching driver db
        self.handlers = detection.get_handlers(self,
            driverdb=detection.LocalKernelModulesDriverDB(),
            handler_dir=self.argv_options.handler_dir,
            mode=self.argv_options.mode)

00065     def _(self, str, convert_keybindings=False):
        '''Keyboard accelerator aware gettext() wrapper.
        
        This optionally converts keyboard accelerators to the appropriate
        format for the frontend.

        All strings in the source code should use the '_' prefix for key
        accelerators (like in GTK). For inserting a real '_', use '__'.
        '''
        # KDE compatible conversion
        result = unicode(gettext.gettext(str), 'UTF-8')

        if convert_keybindings:
            result = self.convert_keybindings(result)

        return result

00082     def init_strings(self):
        '''Initialize all static strings which are used in UI implementations.'''

        self.string_handler = self._('Component')
        self.string_enabled = self._('Enabled')
        self.string_disabled = self._('Disabled')
        self.string_status = self._('Status')
        self.string_restart = self._('Needs computer restart')
        self.string_in_use = self._('In use')
        self.string_not_in_use = self._('Not in use')
        # this is used in --list to denote free/restricted drivers
        self.string_free = self._('free')
        # this is used in --list to denote free/restricted drivers
        self.string_restricted = self._('restricted')
        fw_append = self._('Your hardware will not work without the firmware.')
        self.string_free_aux_firmware = self._(
            'The driver itself is already installed, but it requires a piece '
            'of firmware which is not shipped with the operating system.') + \
            ' ' + fw_append
        self.string_nonfree_aux_firmware = self._(
            'While this driver itself is free software, it relies on '
            'proprietary firmware which cannot be legally shipped with the '
            'operating system.') + ' ' + fw_append
        self.string_download_progress_title = self._('Download in progress')
        self.string_unknown_driver = self._('Unknown driver')

00108     def main_window_title(self):
        '''Return an appropriate translated window title.

        This might depend on the mode the program is called (e. g. showing only
        free drivers, only restricted ones, etc.).
        '''
        if self.argv_options.mode == detection.MODE_NONFREE:
            return self._('Restricted Hardware Drivers')
        else:
            return self._('Hardware Drivers')

00119     def main_window_text(self):
        '''Return a tuple (heading, subtext) of main window texts.

        This changes depending on whether restricted or free drivers are
        used/available, thus the UI should update it whenever it changes a
        handler.
        '''
        proprietary_in_use = False
        proprietary_available = False

        for h in self.handlers:
            if not h.free():
                proprietary_available = True
                if h.used():
                    proprietary_in_use = True
                    break

        if proprietary_in_use:
            heading = self._('Proprietary drivers are being used to make '
                    'this computer work properly.')
        else:
            heading = self._('No proprietary drivers are in use on this system.')

        if proprietary_available:
            subtext = self._(
            # %(os)s stands for the OS name. Prefix it or suffix it,
            # but do not replace it.
            'Proprietary drivers do not have public source code that %(os)s '
            'developers are free to modify. They represent a risk to you '
            'because they are only available on the types of computer chosen by '
            'the manufacturer, and security updates to them depend solely on the '
            'responsiveness of the manufacturer. %(os)s cannot fix or improve '
            'these drivers.') % {'os': oslib.OSLib.inst.os_vendor}
        else:
            subtext = ''

        return (heading, subtext)

00157     def get_handler_tooltip(self, handler):
        '''Format handler rationale as a tooltip.

        Return None if the handler is None or does not have a rationale.
        '''
        try:
            tip = ''
            for par in handler.rationale().split('\n'):
                if tip:
                    tip += '\n'
                tip += '\n'.join(textwrap.wrap(par, 60))
            return tip
        except AttributeError:
            return None

00172     def parse_argv(self):
        '''Parse command line arguments, and return (options, args) pair.'''

        # --check can have an optional numeric argument which sleeps for the
        # given number of seconds; this is mostly useful for the XDG autostart
        # .desktop file, to not do expensive operations right at session start
        def check_option_callback(option, opt_str, value, parser):
            if len(parser.rargs) > 0 and parser.rargs[0].isdigit():
                setattr(parser.values, 'check', int(parser.rargs.pop(0)))
            else:
                setattr(parser.values, 'check', 0)

        parser = optparse.OptionParser()
        parser.set_defaults(check=None)
        parser.add_option ('-c', '--check', action='callback',
                callback=check_option_callback,
                help=self._('Check for newly used or usable drivers and notify the user.'))
        parser.add_option ('-u', '--update-db', action='store_true',
                dest='update_db', default=False,
                help=self._('Query driver databases for newly available or updated drivers.'))
        parser.add_option ('-l', '--list', action='store_true',
                dest='list', default=False,
                help=self._('List available drivers and their status.'))
        parser.add_option ('-e', '--enable', type='string',
                dest='enable', default=None, metavar='DRIVER',
                help=self._('Enable a driver'))
        parser.add_option ('-d', '--disable', type='string',
                dest='disable', default=None, metavar='DRIVER',
                help=self._('Disable a driver'))
        parser.add_option ('--confirm', action='store_true',
                dest='confirm', default=False,
                help=self._('Ask for confirmation for --enable/--disable'))
        parser.add_option ('-C', '--check-composite', action='store_true',
                dest='check_composite', default=False,
                help=self._('Check if there is a graphics driver available that supports composite and offer to enable it'))
        parser.add_option ('-H', '--handler-dir',
                type='string', dest='handler_dir', metavar='DIR', default=None,
                help=self._('Add a custom handler directory.'))
        parser.add_option ('-m', '--mode',
                type='choice', dest='mode', default='any',
                choices=['free', 'nonfree', 'any'],
                metavar='free|nonfree|any',
                help=self._('Only manage free/nonfree drivers. By default, all'
                ' available drivers with any license are presented.'))
        parser.add_option ('--debug', action='store_true',
                dest='debug', default=False,
                help=self._('Enable debugging messages.'))

        (opts, args) = parser.parse_args()

        # transform mode string into constant
        modes = {
            'free': detection.MODE_FREE,
            'nonfree': detection.MODE_NONFREE,
            'any': detection.MODE_ANY
        }
        opts.mode = modes[opts.mode]

        return (opts, args)

00232     def run(self):
        '''Evaluate command line arguments and do the appropriate action.

        If no argument was specified, this starts the interactive UI.
        
        This returns the exit code of the program.
        '''
        if self.argv_options.update_db:
            self.update_driverdb()

        if self.argv_options.list:
            self.list()
            return 0
        elif self.argv_options.enable:
            if self.change_enable(self.argv_options.enable, True):
                return 0
            else:
                return 1
        elif self.argv_options.disable:
            if self.change_enable(self.argv_options.disable, False):
                return 0
            else:
                return 1
        elif self.argv_options.check is not None:
            if self.check():
                return 0
            else:
                return 1
        elif self.argv_options.check_composite:
            if self.check_composite():
                return 0
            else:
                return 1
        # start the UI
        self.ui_init()
        return self.ui_main_loop()

00269     def list(self):
        '''Print a list of available handlers and their status to stdout.'''

        import sys
        for h in self.handlers:
            print '%s - %s (%s, %s, %s)' % (
                h.id(), h.name(), 
                h.free() and self.string_free or self.string_restricted,
                h.enabled() and self.string_enabled or self.string_disabled,
                h.used() and self.string_in_use or self.string_not_in_use)

00280     def change_enable(self, id, enable):
        '''Enable or disable a driver ID.'''

        for h in self.handlers:
            if h.id() == id:
                break
        else:
            print >> sys.stderr, '%s: %s' % (self.string_unknown_driver, id)
            print >> sys.stderr, self._('Use --list to see available drivers')
            return False

        ch = h.can_change()
        if ch:
            print >> sys.stderr, ch
            return False

        if h.enabled() == enable:
            return True

        if self.argv_options.confirm:
            self.toggle_handler(h)
        else:
            if enable:
                h.enable()
            else:
                h.disable()

        return enable == h.enabled()

00309     def check(self):
        '''Notify the user about newly used or available drivers since last check().
        
        Return True if any new driver is available which is not yet enabled.
        '''
        os.nice(10)

        if not oslib.OSLib.inst.is_admin():
            logging.error('Only administrators can use this function.')
            return False

        # read previously seen/used handlers
        seen = set()
        used = set()

        if os.path.exists(oslib.OSLib.inst.check_cache):
            f = open(oslib.OSLib.inst.check_cache)
            for line in f:
                try:
                    (flag, h) = line.split(None, 1)
                    h = unicode(h, 'UTF-8')
                except ValueError:
                    logging.error('invalid line in %s: %s',
                        oslib.OSLib.inst.check_cache, line)
                if flag == 'seen':
                    seen.add(h.strip())
                elif flag == 'used':
                    used.add(h.strip())
                else:
                    logging.error('invalid flag in %s: %s',
                        oslib.OSLib.inst.check_cache, line)
            f.close()

        # check for newly used/available handlers
        new_avail = {}
        new_used = {}
        for h in self.handlers:
            id = h.id()
            if id not in seen:
                new_avail[id] = h
                logging.debug('handler %s previously unseen', id)
            if id not in used and h.used():
                new_used[id] = h
                logging.debug('handler %s previously unused', id)

        # write back cache
        if new_avail or new_used:
            logging.debug('new available/used drivers, writing back check cache %s', 
                oslib.OSLib.inst.check_cache)
            seen.update(new_avail.keys())
            used.update(new_used.keys())
            f = open(oslib.OSLib.inst.check_cache, 'w')
            for s in seen:
                print >> f, 'seen', s
            for u in used:
                print >> f, 'used', u
            f.close()

        # throw out newly available handlers which are already enabled, no need
        # to bother people about them
        restricted_available = False
        for h in new_avail.keys(): # create a copy for iteration
            try:
                if new_avail[h].enabled():
                    logging.debug('%s is already enabled or not available, not announcing', id)
                    del new_avail[h]
                elif not new_avail[h].free():
                    restricted_available = True
            except ValueError:
                # thrown if package does not exist; might be a race condition
                # between jockey --check and a cron job fetching new package
                # indexes at session start, see LP #200089
                continue

        # throw out newly used free drivers; no need for education here
        for h in new_used.keys():
            if new_used[h].free():
                logging.debug('%s is a newly used free driver, not announcing', id)
                del new_used[h]

        notified = False

        # launch notifications if anything remains
        if new_avail:
            if restricted_available:
                self.ui_notification(self._('Restricted drivers available'),
                    self._('In order to use your hardware more efficiently, you'
                    ' can enable drivers which are not free software.'))
            else:
                self.ui_notification(self._('New drivers available'),
                    self._('There are new or updated drivers available for '
                    'your hardware.'))
            notified = True
        elif new_used:
            self.ui_notification(self._('New restricted drivers in use'),
                # %(os)s stands for the OS name. Prefix it or suffix it,
                # but do not replace it.
                self._('In order for this computer to function properly, %(os)s is '
                'using driver software that cannot be supported by %(os)s.') % 
                    {'os': oslib.OSLib.inst.os_vendor})
            notified = True

        if notified:
            # we need to stay in the main loop so that the tray icon stays
            # around
            self.ui_main_loop()

        return new_avail

00418     def check_composite(self):
        '''Check for a composite-providing X.org driver.

        If one is available and not installed, offer to install it and return
        True. Otherwise return False.
        '''

        for h in self.handlers:
            if isinstance(h, xorg_driver.XorgDriverHandler) and \
                h.supports_composite():
                if h.enabled():
                    print >> sys.stderr, self._('Driver "%s" is already enabled and supports the composite extension.') % h.name()
                    return False
                else:
                    oslib.OSLib.inst.open_app(self, ['--confirm', '--enable', h.id()])
                    return True

        print >> sys.stderr, self._('There is no available graphics driver for your system which supports the composite extension.')
        return False

00438     def update_driverdb(self):
        '''Query remote driver DB for updates.'''

        raise NotImplementedError, 'TODO'

00443     def toggle_handler(self, handler):
        '''Callback for toggling the handler enable/disable state in the UI.
        
        After this, you need to refresh the UI's handler tree view if this
        method returns True.
        '''
        # check if we can change at all
        ch = handler.can_change()
        if ch:
            self.error_message(self._('Cannot change driver'), ch)
            return False

        en = handler.enabled()

        # construct and ask confirmation question
        if en:
            title = self._('Disable driver?')
            action = self._('_Disable', True)
        else:
            title = self._('Enable driver?')
            action = self._('_Enable', True)

        d = handler.description() or ''
        r = handler.rationale() or ''
        if d and r:
            subtext = d.strip() + '\n\n' + r
        elif d:
            subtext = d
        elif r:
            subtext = r
        else:
            subtext = None
        if not self.confirm_action(title, handler.name(), subtext, action):
            return False

        # go
        if en:
            handler.disable()
        else:
            handler.enable()

        return True

00486     def install_package(self, package):
        '''Install software package.'''

        if oslib.OSLib.inst.package_installed(package):
            return

        oslib.OSLib.inst.install_package(package, self)
        if oslib.OSLib.inst.package_installed(package):
            self._update_installed_packages([package], [])

00496     def remove_package(self, package):
        '''Remove software package.'''

        if not oslib.OSLib.inst.package_installed(package):
            return

        oslib.OSLib.inst.remove_package(package, self)
        if not oslib.OSLib.inst.package_installed(package):
            self._update_installed_packages([], [package])

00506     def _update_installed_packages(self, add, remove):
        '''Update backup_dir/installed_packages list of driver packages.
        
        This keeps a log of all packages that jockey installed for supporting
        drivers, so that distribution installers on live CDs can push them into
        the installed system as well.

        add and remove are lists which package names to add/remove from it.
        '''
        # get current list
        current = set()
        path = os.path.join(oslib.OSLib.inst.backup_dir, 'installed_packages')
        if os.path.exists(path):
            for line in open(path):
                line = line.strip()
                if line:
                    current.add(line)
    
        current = current.union(add).difference(remove)
        
        if current:
            # write it back
            f = open(path, 'w')
            for p in current:
                print >> f, p
            f.close()
        else:
            # delete it if it is empty
            if os.path.exists(path):
                os.unlink(path)

00537     def download_url(self, url, filename=None, data=None):
        '''Download an URL into a local file, and display a progress dialog.
        
        If filename is not given, a temporary file will be created.

        Additional POST data can be submitted for HTTP requests in the data
        argument (see urllib2.urlopen).

        Return (filename, headers) tuple, or (None, headers) if the user
        cancelled the download.
        '''
        block_size = 8192
        current_size = 0
        try:
            f = urllib2.urlopen(url)
        except Exception, e:
            self.error_message(self._('Download error'), str(e))
            return (None, None)
        headers = f.info()

        if 'Content-Length' in headers:
            total_size = int(headers['Content-Length'])
        else:
            total_size = -1

        self.ui_download_start(url, total_size)

        if filename:
            tfp = open(filename, 'wb')
            result_filename = filename
        else:
            (fd, result_filename) = tempfile.mkstemp()
            tfp = os.fdopen(fd, 'wb')

        try:
            while current_size < total_size:
                block = f.read(block_size)
                tfp.write (block)
                current_size += len(block)
                # if True, user canceled download
                if self.ui_download_progress(current_size, total_size):
                    # if we created a temporary file, clean it up
                    if not filename:
                        os.unlink(result_filename)
                    result_filename = None
                    break
        finally:
            tfp.close()
            f.close()
            self.ui_download_finish()

        return (result_filename, headers)

    #
    # The following methods must be implemented in subclasses
    # 

00594     def convert_keybindings(self, str):
        '''Convert keyboard accelerators to the particular UI's format.

        The abstract UI and drivers use the '_' prefix to mark a keyboard
        accelerator.

        A double underscore ('__') is converted to a real '_'.'''

        raise NotImplementedError, 'subclasses need to implement this'

00604     def ui_init(self):
        '''Initialize UI.
        
        This should set up presentation of self.handlers and show the main
        window.
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00612     def ui_main_loop(self):
        '''Main loop for the user interface.
        
        This should return if the user wants to quit the program, and return
        the exit code.
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00620     def error_message(self, title, text):
        '''Present an error message box.'''

        raise NotImplementedError, 'subclasses need to implement this'

00625     def confirm_action(self, title, text, subtext=None, action=None):
        '''Present a confirmation dialog.

        If action is given, it is used as button label instead of the default
        'OK'.  Return True if the user confirms, False otherwise.
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00633     def ui_notification(self, title, text):
        '''Present a notification popup.

        This should preferably create a tray icon. Clicking on the tray icon or
        notification should run the GUI.
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00641     def ui_idle(self):
        '''Process pending UI events and return.

        This is called while waiting for external processes such as package
        installers.
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00649     def ui_download_start(self, url, total_size):
        '''Create a progress dialog for a download of given URL.

        total_size specifes the number of bytes to download, or -1 if it cannot
        be determined. In this case the dialog should display an indeterminated
        progress bar (bouncing back and forth).
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00658     def ui_download_progress(self, current_size, total_size):
        '''Update download progress of current download.
        
        This should return True to cancel the current download, and False
        otherwise.
        '''
        raise NotImplementedError, 'subclasses need to implement this'

00666     def ui_download_finish(self):
        '''Close the current download progress dialog.'''

        raise NotImplementedError, 'subclasses need to implement this'

if __name__ == '__main__':
    oslib.OSLib.inst = oslib.OSLib()
    u = AbstractUI()
    u.run()

Generated by  Doxygen 1.6.0   Back to index