From 8bdfb8df6897a81a0c0087c6f778fe07902dd8f7 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Wed, 17 Nov 2010 11:43:08 -0500 Subject: [PATCH] util: add Util.spawn and friends Add Util.spawn, Util.spawnCommandLine, and Util.spawnDesktop for spawning a command/argv/.desktop file in the background, automatically handling errors via MessageTray.SystemNotificationSource(), and Util.trySpawn, Util.trySpawnCommandLine, and Utils.trySpawnDesktop that don't do automatic error handling (but do at least clean up the error message in the exception a bit). Update various other bits of code around the shell to use the new methods. https://bugzilla.gnome.org/show_bug.cgi?id=635089 --- js/misc/util.js | 131 ++++++++++++++++++++++++++++++++++ js/ui/main.js | 8 +-- js/ui/messageTray.js | 3 +- js/ui/placeDisplay.js | 3 +- js/ui/runDialog.js | 14 ++-- js/ui/status/accessibility.js | 4 +- js/ui/status/power.js | 11 ++- js/ui/status/volume.js | 4 +- js/ui/statusMenu.js | 21 ++---- po/POTFILES.in | 1 + 10 files changed, 158 insertions(+), 42 deletions(-) diff --git a/js/misc/util.js b/js/misc/util.js index 0d1d5c464..e61f0dd94 100644 --- a/js/misc/util.js +++ b/js/misc/util.js @@ -1,5 +1,16 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; + +const Gettext = imports.gettext.domain('gnome-shell'); +const _ = Gettext.gettext; + /* http://daringfireball.net/2010/07/improved_regex_for_matching_urls */ const _urlRegexp = new RegExp('\b(([a-z][\w-]+:(/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)([^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))', 'gi'); @@ -17,3 +28,123 @@ function findUrls(str) { res.push({ url: match[0], pos: match.index }); return res; } + +// spawn: +// @argv: an argv array +// +// Runs @argv in the background, handling any errors that occur +// when trying to start the program. +function spawn(argv) { + try { + trySpawn(argv); + } catch (err) { + _handleSpawnError(argv[0], err); + } +} + +// spawnCommandLine: +// @command_line: a command line +// +// Runs @command_line in the background, handling any errors that +// occur when trying to parse or start the program. +function spawnCommandLine(command_line) { + try { + let [success, argc, argv] = GLib.shell_parse_argv(command_line); + trySpawn(argv); + } catch (err) { + _handleSpawnError(command_line, err); + } +} + +// spawnDesktop: +// @id: a desktop file ID +// +// Spawns the desktop file identified by @id using startup notification, +// etc, handling any errors that occur when trying to find or start +// the program. +function spawnDesktop(id) { + try { + trySpawnDesktop(id); + } catch (err) { + _handleSpawnError(id, err); + } +} + +// trySpawn: +// @argv: an argv array +// +// Runs @argv in the background. If launching @argv fails, +// this will throw an error. +function trySpawn(argv) +{ + try { + GLib.spawn_async(null, argv, null, + GLib.SpawnFlags.SEARCH_PATH, + null, null); + } catch (err) { + // The exception from gjs contains an error string like: + // Error invoking GLib.spawn_command_line_async: Failed to + // execute child process "foo" (No such file or directory) + // We are only interested in the part in the parentheses. (And + // we can't pattern match the text, since it gets localized.) + err.message = err.message.replace(/.*\((.+)\)/, '$1'); + throw err; + } +} + +// trySpawnCommandLine: +// @command_line: a command line +// +// Runs @command_line in the background. If launching @command_line +// fails, this will throw an error. +function trySpawnCommandLine(command_line) { + let success, argc, argv; + + try { + [success, argc, argv] = GLib.shell_parse_argv(command_line); + } catch (err) { + // Replace "Error invoking GLib.shell_parse_argv: " with + // something nicer + err.message = err.message.replace(/[^:]*: /, _("Could not parse command:") + "\n"); + throw err; + } + + trySpawn(argv); +} + +// trySpawnDesktop: +// @id: a desktop file ID +// +// Spawns the desktop file identified by @id using startup notification. +// On error, throws an exception. +function trySpawnDesktop(id) { + let app; + + // shell_app_system_load_from_desktop_file() will end up returning + // a stupid error message if the desktop file doesn't exist, but + // that's the only case it returns an error for, so we just + // substitute our own error in instead + try { + app = Shell.AppSystem.get_default().load_from_desktop_file(id + '.desktop'); + } catch (err) { + throw new Error(_("No such application")); + } + + try { + app.launch(); + } catch(err) { + // see trySpawn + err.message = err.message.replace(/.*\((.+)\)/, '$1'); + throw err; + } +} + +function _handleSpawnError(command, err) { + let title = _("Execution of '%s' failed:").format(command); + + let source = new MessageTray.SystemNotificationSource(); + Main.messageTray.add(source); + let notification = new MessageTray.Notification(source, title, err.message); + notification.setTransient(true); + source.notify(notification); +} diff --git a/js/ui/main.js b/js/ui/main.js index 8574fe701..1ee806516 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -39,6 +39,7 @@ const WindowManager = imports.ui.windowManager; const Magnifier = imports.ui.magnifier; const XdndHandler = imports.ui.xdndHandler; const StatusIconDispatcher = imports.ui.statusIconDispatcher; +const Util = imports.misc.util; const DEFAULT_BACKGROUND_COLOR = new Clutter.Color(); DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff); @@ -317,11 +318,8 @@ function _globalKeyPressHandler(actor, event) { if (action == Meta.KeyBindingAction.COMMAND_SCREENSHOT) { let gconf = GConf.Client.get_default(); let command = gconf.get_string('/apps/metacity/keybinding_commands/command_screenshot'); - if (command != null && command != '') { - let [ok, len, args] = GLib.shell_parse_argv(command); - let p = new Shell.Process({'args' : args}); - p.run(); - } + if (command != null && command != '') + Util.spawnCommandLine(command); return true; } diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index e085421ef..57f5acf02 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -106,8 +106,7 @@ URLHighlighter.prototype = { return true; } catch (e) { // TODO: remove this after gnome 3 release - let p = new Shell.Process({ 'args' : ['gvfs-open', url] }); - p.run(); + Util.spawn(['gvfs-open', url]); return true; } } diff --git a/js/ui/placeDisplay.js b/js/ui/placeDisplay.js index fb96149c9..9b7134b89 100644 --- a/js/ui/placeDisplay.js +++ b/js/ui/placeDisplay.js @@ -14,6 +14,7 @@ const _ = Gettext.gettext; const DND = imports.ui.dnd; const Main = imports.ui.main; const Search = imports.ui.search; +const Util = imports.misc.util; const NAUTILUS_PREFS_DIR = '/apps/nautilus/preferences'; const DESKTOP_IS_HOME_KEY = NAUTILUS_PREFS_DIR + '/desktop_is_home_dir'; @@ -163,7 +164,7 @@ PlacesManager.prototype = { icon_size: size }); }, function () { - new Shell.Process({ args: ['nautilus-connect-server'] }).run(); + Util.spawn(['nautilus-connect-server']); }); let networkApp = null; diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js index d09639ceb..955f1e777 100644 --- a/js/ui/runDialog.js +++ b/js/ui/runDialog.js @@ -14,6 +14,7 @@ const _ = Gettext.gettext; const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const Tweener = imports.ui.tweener; +const Util = imports.misc.util; const MAX_FILE_DELETED_BEFORE_INVALID = 10; @@ -354,9 +355,7 @@ RunDialog.prototype = { try { if (inTerminal) command = 'gnome-terminal -x ' + input; - let [ok, len, args] = GLib.shell_parse_argv(command); - let p = new Shell.Process({ 'args' : args }); - p.run(); + Util.trySpawnCommandLine(command); } catch (e) { // Mmmh, that failed - see if @input matches an existing file let path = null; @@ -374,13 +373,8 @@ RunDialog.prototype = { global.create_app_launch_context()); } else { this._commandError = true; - // The exception contains an error string like: - // Error invoking Shell.run: Failed to execute child - // process "foo" (No such file or directory) - // We are only interested in the actual error, so parse - //that out. - let m = /.+\((.+)\)/.exec(e); - let errorStr = _("Execution of '%s' failed:").format(command) + '\n' + m[1]; + + let errorStr = _("Execution of '%s' failed:").format(command) + '\n' + e.message; this._errorMessage.set_text(errorStr); this._errorBox.show(); diff --git a/js/ui/status/accessibility.js b/js/ui/status/accessibility.js index f96e193a8..11a95f373 100644 --- a/js/ui/status/accessibility.js +++ b/js/ui/status/accessibility.js @@ -12,6 +12,7 @@ const St = imports.gi.St; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; @@ -108,8 +109,7 @@ ATIndicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Universal Access Settings"), function() { - let p = new Shell.Process({ args: ['gnome-control-center','universal-access'] }); - p.run(); + Util.spawnDesktop('gnome-universal-access-panel'); }); }, diff --git a/js/ui/status/power.js b/js/ui/status/power.js index 3a909c4cd..fe787f790 100644 --- a/js/ui/status/power.js +++ b/js/ui/status/power.js @@ -10,6 +10,7 @@ const St = imports.gi.St; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; @@ -83,7 +84,7 @@ Indicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Power Settings"),function() { - GLib.spawn_command_line_async('gnome-control-center power'); + Util.spawnDesktop('gnome-power-panel'); }); this._proxy.connect('Changed', Lang.bind(this, this._devicesChanged)); @@ -134,8 +135,7 @@ Indicator.prototype = { this._batteryItem.actor.reactive = true; this._batteryItem.actor.can_focus = true; this._batteryItem.connect('activate', function(item) { - let p = new Shell.Process({ args: ['gnome-power-statistics', '--device', device_id] }); - p.run(); + Util.spawn(['gnome-power-statistics', '--device', device_id]); }); } else { // virtual device @@ -164,8 +164,7 @@ Indicator.prototype = { let item = new DeviceItem (devices[i]); item.connect('activate', function() { - let p = new Shell.Process({ args: ['gnome-power-statistics', '--device', device_id] }); - p.run(); + Util.spawn(['gnome-power-statistics', '--device', device_id]); }); this._deviceItems.push(item); this.menu.addMenuItem(item, this._otherDevicePosition + position); @@ -198,7 +197,7 @@ Indicator.prototype = { _checkError: function(error) { if (!this._restarted && error && error.message.match(/org\.freedesktop\.DBus\.Error\.(UnknownMethod|InvalidArgs)/)) { GLib.spawn_command_line_sync('pkill -f "^gnome-power-manager$"'); - GLib.spawn_command_line_async('gnome-power-manager'); + Util.spawn(['gnome-power-manager']); this._restarted = true; } } diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js index 63f76fe0d..9211968f9 100644 --- a/js/ui/status/volume.js +++ b/js/ui/status/volume.js @@ -11,6 +11,7 @@ const St = imports.gi.St; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; @@ -60,8 +61,7 @@ Indicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Sound Settings"), function() { - let p = new Shell.Process({ args: ['gnome-control-center', 'sound'] }); - p.run(); + Util.spawnDesktop('gnome-sound-panel'); }); this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); diff --git a/js/ui/statusMenu.js b/js/ui/statusMenu.js index e2d13c68d..f834aad49 100644 --- a/js/ui/statusMenu.js +++ b/js/ui/statusMenu.js @@ -12,6 +12,7 @@ const GnomeSession = imports.misc.gnomeSession; const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; // Adapted from gdm/gui/user-switch-applet/applet.c // @@ -31,7 +32,7 @@ StatusMenuButton.prototype = { this.actor.set_child(box); this._gdm = Gdm.UserManager.ref_default(); - this._gdm.queue_load() + this._gdm.queue_load(); this._user = this._gdm.get_user(GLib.get_user_name()); this._presence = new GnomeSession.Presence(); @@ -153,17 +154,17 @@ StatusMenuButton.prototype = { _onMyAccountActivate: function() { Main.overview.hide(); - this._spawn(['gnome-control-center', 'user-accounts']); + Util.spawnDesktop('gnome-user-accounts-panel'); }, _onPreferencesActivate: function() { Main.overview.hide(); - this._spawn(['gnome-control-center', '-o']); + Util.spawnDesktop('gnome-control-center'); }, _onLockScreenActivate: function() { Main.overview.hide(); - this._spawn(['gnome-screensaver-command', '--lock']); + Util.spawn(['gnome-screensaver-command', '--lock']); }, _onLoginScreenActivate: function() { @@ -174,19 +175,11 @@ StatusMenuButton.prototype = { _onQuitSessionActivate: function() { Main.overview.hide(); - this._spawn(['gnome-session-save', '--logout-dialog']); + Util.spawn(['gnome-session-save', '--logout-dialog']); }, _onShutDownActivate: function() { Main.overview.hide(); - this._spawn(['gnome-session-save', '--shutdown-dialog']); - }, - - _spawn: function(args) { - // FIXME: once Shell.Process gets support for signalling - // errors we should pop up an error dialog or something here - // on failure - let p = new Shell.Process({'args' : args}); - p.run(); + Util.spawn(['gnome-session-save', '--shutdown-dialog']); } }; diff --git a/po/POTFILES.in b/po/POTFILES.in index cb9c39f0b..255a3d651 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,6 +1,7 @@ data/gnome-shell.desktop.in.in data/org.gnome.shell.gschema.xml.in data/org.gnome.accessibility.magnifier.gschema.xml.in +js/misc/util.js js/ui/appDisplay.js js/ui/appFavorites.js js/ui/dash.js