From 1e6cc434df61f18c2e77bf47b5ec518a18b6936a Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Fri, 10 Aug 2012 19:23:35 +0100 Subject: [PATCH] Cope better if no GUI is available We used to assume that a GUI was available whenever $DISPLAY was set. However, there could be other reasons why the GUI can't be used, such as missing Python modules. We now detect this and fall back to console mode gracefully. If --gui is specified (to force use of the GUI) and no GUI is available, we display an error message and abort, as before. --- tests/testdownload.py | 11 +++++++-- tests/testlaunch.py | 6 ++--- zeroinstall/0launch-gui/0launch-gui | 3 --- zeroinstall/0launch-gui/main.py | 49 ++++++++++++++++++++++++++++--------- zeroinstall/cmd/config.py | 9 +++---- zeroinstall/cmd/select.py | 22 +++++++++-------- zeroinstall/helpers.py | 34 +++++++++++++++++++------ zeroinstall/injector/background.py | 15 ++++++------ 8 files changed, 98 insertions(+), 51 deletions(-) diff --git a/tests/testdownload.py b/tests/testdownload.py index 3ae5259..ccad3b4 100755 --- a/tests/testdownload.py +++ b/tests/testdownload.py @@ -24,9 +24,16 @@ import my_dbus import server ran_gui = False -def raise_gui(*args): +def raise_gui(*args, **kwargs): global ran_gui - ran_gui = True + use_gui = kwargs.get('use_gui', True) + assert use_gui != False + if 'DISPLAY' in os.environ: + ran_gui = True + else: + assert use_gui is None + return helpers.DontUseGUI + background._detach = lambda: False local_hello = """ diff --git a/tests/testlaunch.py b/tests/testlaunch.py index 58205fa..21e300d 100755 --- a/tests/testlaunch.py +++ b/tests/testlaunch.py @@ -146,9 +146,9 @@ class TestLaunch(BaseTest): self.assertEqual("", err) del os.environ['DISPLAY'] - out, err = self.run_0launch(['--gui', '--dry-run']) - self.assertEqual("", err) - self.assertEqual("Finished\n", out) + out, err = self.run_0launch(['--gui']) + self.assertEqual("Can't use GUI because $DISPLAY is not set\n", err) + self.assertEqual("", out) def testRefreshDisplay(self): os.environ['DISPLAY'] = ':foo' diff --git a/zeroinstall/0launch-gui/0launch-gui b/zeroinstall/0launch-gui/0launch-gui index 0aec0df..f72714d 100755 --- a/zeroinstall/0launch-gui/0launch-gui +++ b/zeroinstall/0launch-gui/0launch-gui @@ -32,9 +32,6 @@ if localedir: if hasattr(locale, 'bindtextdomain'): locale.bindtextdomain('zero-install', localedir) -if sys.version_info[0] < 3: - import pygtk; pygtk.require('2.0') - import main try: diff --git a/zeroinstall/0launch-gui/main.py b/zeroinstall/0launch-gui/main.py index 92833b7..6062f27 100644 --- a/zeroinstall/0launch-gui/main.py +++ b/zeroinstall/0launch-gui/main.py @@ -4,10 +4,12 @@ from __future__ import print_function import os, sys +import logging +import warnings from optparse import OptionParser -from zeroinstall import _ +from zeroinstall import _, SafeException from zeroinstall.injector import requirements from zeroinstall.injector.driver import Driver from zeroinstall.injector.config import load_config @@ -28,6 +30,7 @@ def run_gui(args): parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU') parser.add_option("", "--command", help=_("command to select"), metavar='COMMAND') parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true') + parser.add_option("-g", "--force-gui", help=_("display an error if there's no GUI"), action='store_true') parser.add_option("", "--message", help=_("message to display when interacting with user")) parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION') parser.add_option("", "--os", help=_("target operation system type"), metavar='OS') @@ -44,15 +47,12 @@ def run_gui(args): (options, args) = parser.parse_args(args) if options.verbose: - import logging logger = logging.getLogger() if options.verbose == 1: logger.setLevel(logging.INFO) else: logger.setLevel(logging.DEBUG) - import gui - if options.version: print("0launch-gui (zero-install) " + gui.version) print("Copyright (C) 2010 Thomas Leonard") @@ -63,14 +63,39 @@ def run_gui(args): "\nFor more information about these matters, see the file named COPYING.")) sys.exit(0) - if sys.version_info[0] > 2: - from zeroinstall.gtkui import pygtkcompat - pygtkcompat.enable() - pygtkcompat.enable_gtk(version = '3.0') - import gtk - if gtk.gdk.get_display() is None: - print("Failed to connect to display. Aborting.", file=sys.stderr) - sys.exit(1) + def nogui(ex): + if options.force_gui: + fn = logging.warn + else: + fn = logging.info + fn("No GUI available", exc_info = ex) + sys.exit(100) + + with warnings.catch_warnings(): + if not options.force_gui: + warnings.filterwarnings("ignore") + if sys.version_info[0] < 3: + try: + import pygtk; pygtk.require('2.0') + except ImportError as ex: + nogui(ex) + + import gui + + try: + if sys.version_info[0] > 2: + from zeroinstall.gtkui import pygtkcompat + pygtkcompat.enable() + pygtkcompat.enable_gtk(version = '3.0') + import gtk + except (ImportError, ValueError) as ex: + nogui(ex) + + if gtk.gdk.get_display() is None: + try: + raise SafeException("Failed to connect to display.") + except SafeException as ex: + nogui(ex) # logging needs this as a raised exception handler = gui.GUIHandler() diff --git a/zeroinstall/cmd/config.py b/zeroinstall/cmd/config.py index e0e3f0f..40ab988 100644 --- a/zeroinstall/cmd/config.py +++ b/zeroinstall/cmd/config.py @@ -87,15 +87,12 @@ settings = { def handle(config, options, args): if len(args) == 0: - if options.gui is None and os.environ.get('DISPLAY', None): - options.gui = True - if options.gui: - from zeroinstall import helpers - return helpers.get_selections_gui(None, []) - else: + from zeroinstall import helpers + if helpers.get_selections_gui(None, [], use_gui = options.gui) == helpers.DontUseGUI: for key, setting_type in settings.items(): value = getattr(config, key) print(key, "=", setting_type.format(value)) + # (else we displayed the preferences dialog in the GUI) return elif len(args) > 2: raise UsageError() diff --git a/zeroinstall/cmd/select.py b/zeroinstall/cmd/select.py index 0715990..7726b1b 100644 --- a/zeroinstall/cmd/select.py +++ b/zeroinstall/cmd/select.py @@ -106,15 +106,12 @@ def get_selections_for(requirements, config, options, select_only, download_only background.spawn_background_update(driver, options.verbose) return driver.solver.selections - # If the user didn't say whether to use the GUI, choose for them. - if options.gui is None and os.environ.get('DISPLAY', None): - options.gui = True - # If we need to download anything, we might as well - # refresh all the feeds first. - options.refresh = True - logging.info(_("Switching to GUI mode... (use --console to disable)")) - - if options.gui: + # If we need to download anything, we might as well + # refresh all the feeds first. + options.refresh = True + + if options.gui != False: + # If the user didn't say whether to use the GUI, choose for them. gui_args = driver.requirements.get_as_options() if download_only: # Just changes the button's label @@ -132,11 +129,16 @@ def get_selections_for(requirements, config, options, select_only, download_only gui_args.append('--select-only') from zeroinstall import helpers - sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, test_callback) + sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, test_callback, use_gui = options.gui) if not sels: return None # Aborted + elif sels is helpers.DontUseGUI: + sels = None else: + sels = None + + if sels is None: # Note: --download-only also makes us stop and download stale feeds first. downloaded = driver.solve_and_download_impls(refresh = options.refresh or download_only or False, select_only = select_only) diff --git a/zeroinstall/helpers.py b/zeroinstall/helpers.py index 8af4025..cb46a8f 100644 --- a/zeroinstall/helpers.py +++ b/zeroinstall/helpers.py @@ -7,10 +7,12 @@ Convenience routines for performing common operations. # See the README file for details, or visit http://0install.net. import os, sys, logging -from zeroinstall import support +from zeroinstall import support, SafeException from zeroinstall.support import tasks -def get_selections_gui(iface_uri, gui_args, test_callback = None): +DontUseGUI = object() + +def get_selections_gui(iface_uri, gui_args, test_callback = None, use_gui = True): """Run the GUI to choose and download a set of implementations. The user may ask the GUI to submit a bug report about the program. In that case, the GUI may ask us to test it. test_callback is called in that case with the implementations @@ -21,10 +23,21 @@ def get_selections_gui(iface_uri, gui_args, test_callback = None): @type gui_args: [str] @param test_callback: function to use to try running the program @type test_callback: L{zeroinstall.injector.selections.Selections} -> str + @param use_gui: if True, raise a SafeException if the GUI is not available. If None, returns DontUseGUI if the GUI cannot be started. If False, returns DontUseGUI always. (since 1.11) + @param use_gui: bool | None @return: the selected implementations @rtype: L{zeroinstall.injector.selections.Selections} @since: 0.28 """ + if use_gui is False: + return DontUseGUI + + if 'DISPLAY' not in os.environ: + if use_gui is None: + return DontUseGUI + else: + raise SafeException("Can't use GUI because $DISPLAY is not set") + from zeroinstall.injector import selections, qdom from io import BytesIO @@ -44,6 +57,8 @@ def get_selections_gui(iface_uri, gui_args, test_callback = None): # We used to use pipes to support Python2.3... os.dup2(gui.fileno(), 1) os.dup2(gui.fileno(), 0) + if use_gui is True: + gui_args = ['-g'] + gui_args if iface_uri is not None: gui_args = gui_args + ['--', iface_uri] os.execvp(sys.executable, [sys.executable, gui_exe] + gui_args) @@ -91,6 +106,11 @@ def get_selections_gui(iface_uri, gui_args, test_callback = None): if status == 1 << 8: logging.info("User cancelled the GUI; aborting") return None # Aborted + elif status == 100 << 8: + if use_gui is None: + return DontUseGUI + else: + raise SafeException("No GUI available") if status != 0: raise Exception("Error from GUI: code = %d" % status) break @@ -122,10 +142,10 @@ def ensure_cached(uri, command = 'run', config = None): d = Driver(config, requirements) if d.need_download() or not d.solver.ready: - if os.environ.get('DISPLAY', None): - return get_selections_gui(uri, ['--command', command]) - else: - done = d.solve_and_download_impls() - tasks.wait_for_blocker(done) + sels = get_selections_gui(uri, ['--command', command], use_gui = None) + if sels != DontUseGUI: + return sels + done = d.solve_and_download_impls() + tasks.wait_for_blocker(done) return d.solver.selections diff --git a/zeroinstall/injector/background.py b/zeroinstall/injector/background.py index bc69e18..6ccef17 100644 --- a/zeroinstall/injector/background.py +++ b/zeroinstall/injector/background.py @@ -226,14 +226,13 @@ def _check_for_updates(requirements, verbose, app): _("Updates ready to download for '%s'.") % root_iface, timeout = 1) - if os.environ.get('DISPLAY', None): - # Run the GUI... - from zeroinstall import helpers - gui_args = ['--refresh', '--systray', '--download'] + requirements.get_as_options() - new_sels = helpers.get_selections_gui(requirements.interface_uri, gui_args) - if new_sels is None: - sys.exit(0) # Cancelled by user - else: + # Run the GUI if possible... + from zeroinstall import helpers + gui_args = ['--refresh', '--systray', '--download'] + requirements.get_as_options() + new_sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, use_gui = None) + if new_sels is None: + sys.exit(0) # Cancelled by user + elif new_sels is helpers.DontUseGUI: tasks.wait_for_blocker(driver.download_uncached_implementations()) new_sels = driver.solver.selections -- 2.11.4.GIT