~jbicha/ubuntu/oneiric/gnome-shell/oneiric-3.2.2.1

« back to all changes in this revision

Viewing changes to js/ui/extensionSystem.js

  • Committer: Package Import Robot
  • Author(s): Jeremy Bicha
  • Date: 2011-09-07 09:09:05 UTC
  • mfrom: (1.1.29 upstream)
  • Revision ID: package-import@ubuntu.com-20110907090905-kbo4fewcg12zt99u
Tags: 3.1.90.1-0ubuntu1
* New upstream release.
* debian/control: Bump build-depends on new mutter
* debian/patches/01_favorite_apps.patch: Updated
* debian/patches/03_remove-glx-dependency-on-armel.patch: Refreshed
* debian/patches/04_build-without-caribou.patch
  - Build without caribou since Ubuntu uses onboard and our System 
    Settings doesn't support choosing a different screen keyboard yet

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
2
2
 
 
3
const Lang = imports.lang;
 
4
const Signals = imports.signals;
 
5
 
3
6
const GLib = imports.gi.GLib;
4
7
const Gio = imports.gi.Gio;
5
8
const St = imports.gi.St;
6
9
const Shell = imports.gi.Shell;
 
10
const Soup = imports.gi.Soup;
7
11
 
8
12
const Config = imports.misc.config;
9
13
 
11
15
    ENABLED: 1,
12
16
    DISABLED: 2,
13
17
    ERROR: 3,
14
 
    OUT_OF_DATE: 4
 
18
    OUT_OF_DATE: 4,
 
19
    DOWNLOADING: 5,
 
20
 
 
21
    // Used as an error state for operations on unknown extensions,
 
22
    // should never be in a real extensionMeta object.
 
23
    UNINSTALLED: 99
15
24
};
16
25
 
17
26
const ExtensionType = {
19
28
    PER_USER: 2
20
29
};
21
30
 
 
31
const _httpSession = new Soup.SessionAsync();
 
32
 
 
33
// The unfortunate state of gjs, gobject-introspection and libsoup
 
34
// means that I have to do a hack to add a feature.
 
35
// See: https://bugzilla.gnome.org/show_bug.cgi?id=655189 for context.
 
36
 
 
37
if (Soup.Session.prototype.add_feature != null)
 
38
    Soup.Session.prototype.add_feature.call(_httpSession, new Soup.ProxyResolverDefault());
 
39
 
22
40
// Maps uuid -> metadata object
23
41
const extensionMeta = {};
24
42
// Maps uuid -> importer object (extension directory tree)
25
43
const extensions = {};
 
44
// Maps uuid -> extension state object (returned from init())
 
45
const extensionStateObjs = {};
26
46
// Arrays of uuids
27
 
var disabledExtensions;
28
47
var enabledExtensions;
29
48
// GFile for user extensions
30
49
var userExtensionsDir = null;
31
50
 
 
51
// We don't really have a class to add signals on. So, create
 
52
// a simple dummy object, add the signal methods, and export those
 
53
// publically.
 
54
var _signals = {};
 
55
Signals.addSignalMethods(_signals);
 
56
 
 
57
const connect = Lang.bind(_signals, _signals.connect);
 
58
const disconnect = Lang.bind(_signals, _signals.disconnect);
 
59
 
 
60
// UUID => Array of error messages
 
61
var errors = {};
 
62
 
 
63
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
 
64
 
32
65
/**
33
66
 * versionCheck:
34
67
 * @required: an array of versions we're compatible with
59
92
    return false;
60
93
}
61
94
 
 
95
function installExtensionFromManifestURL(uuid, url) {
 
96
    _httpSession.queue_message(
 
97
        Soup.Message.new('GET', url),
 
98
        function(session, message) {
 
99
            if (message.status_code != Soup.KnownStatusCode.OK) {
 
100
                logExtensionError(uuid, 'downloading manifest: ' + message.status_code.toString());
 
101
                return;
 
102
            }
 
103
 
 
104
            let manifest;
 
105
            try {
 
106
                manifest = JSON.parse(message.response_body.data);
 
107
            } catch (e) {
 
108
                logExtensionError(uuid, 'parsing: ' + e.toString());
 
109
                return;
 
110
            }
 
111
 
 
112
            if (uuid != manifest['uuid']) {
 
113
                logExtensionError(uuid, 'manifest: manifest uuids do not match');
 
114
                return;
 
115
            }
 
116
 
 
117
            let meta = extensionMeta[uuid] = { uuid: uuid,
 
118
                                               state: ExtensionState.DOWNLOADING,
 
119
                                               error: '' };
 
120
 
 
121
            _signals.emit('extension-state-changed', meta);
 
122
 
 
123
            installExtensionFromManifest(manifest, meta);
 
124
        });
 
125
}
 
126
 
 
127
function installExtensionFromManifest(manifest, meta) {
 
128
    let uuid = manifest['uuid'];
 
129
    let name = manifest['name'];
 
130
 
 
131
    if (!versionCheck(manifest['shell-version'], Config.PACKAGE_VERSION)) {
 
132
        meta.state = ExtensionState.OUT_OF_DATE;
 
133
        logExtensionError(uuid, 'version: ' + name + ' is not compatible with current GNOME Shell version', meta.state);
 
134
        return;
 
135
    }
 
136
 
 
137
    let url = manifest['__installer'];
 
138
    _httpSession.queue_message(Soup.Message.new('GET', url),
 
139
                               function(session, message) {
 
140
                                   gotExtensionZipFile(session, message, uuid);
 
141
                               });
 
142
}
 
143
 
 
144
function gotExtensionZipFile(session, message, uuid) {
 
145
    if (message.status_code != Soup.KnownStatusCode.OK) {
 
146
        logExtensionError(uuid, 'downloading extension: ' + message.status_code);
 
147
        return;
 
148
    }
 
149
 
 
150
    // FIXME: use a GFile mkstemp-type method once one exists
 
151
    let fd, tmpzip;
 
152
    try {
 
153
        [fd, tmpzip] = GLib.file_open_tmp('XXXXXX.shell-extension.zip');
 
154
    } catch (e) {
 
155
        logExtensionError(uuid, 'tempfile: ' + e.toString());
 
156
        return;
 
157
    }
 
158
 
 
159
    let stream = new Gio.UnixOutputStream({ fd: fd });
 
160
    let dir = userExtensionsDir.get_child(uuid);
 
161
    Shell.write_soup_message_to_stream(stream, message);
 
162
    stream.close(null);
 
163
    let [success, pid] = GLib.spawn_async(null,
 
164
                                          ['unzip', '-uod', dir.get_path(), '--', tmpzip],
 
165
                                          null,
 
166
                                          GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
 
167
                                          null);
 
168
 
 
169
    if (!success) {
 
170
        logExtensionError(uuid, 'extract: could not extract');
 
171
        return;
 
172
    }
 
173
 
 
174
    GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, function(pid, status) {
 
175
        GLib.spawn_close_pid(pid);
 
176
        loadExtension(dir, true, ExtensionType.PER_USER);
 
177
    });
 
178
}
 
179
 
 
180
function disableExtension(uuid) {
 
181
    let meta = extensionMeta[uuid];
 
182
    if (!meta)
 
183
        return;
 
184
 
 
185
    if (meta.state != ExtensionState.ENABLED)
 
186
        return;
 
187
 
 
188
    let extensionState = extensionStateObjs[uuid];
 
189
 
 
190
    try {
 
191
        extensionState.disable();
 
192
    } catch(e) {
 
193
        logExtensionError(uuid, e.toString());
 
194
        return;
 
195
    }
 
196
 
 
197
    meta.state = ExtensionState.DISABLED;
 
198
    _signals.emit('extension-state-changed', meta);
 
199
}
 
200
 
 
201
function enableExtension(uuid) {
 
202
    let meta = extensionMeta[uuid];
 
203
    if (!meta)
 
204
        return;
 
205
 
 
206
    if (meta.state != ExtensionState.DISABLED)
 
207
        return;
 
208
 
 
209
    let extensionState = extensionStateObjs[uuid];
 
210
 
 
211
    try {
 
212
        extensionState.enable();
 
213
    } catch(e) {
 
214
        logExtensionError(uuid, e.toString());
 
215
        return;
 
216
    }
 
217
 
 
218
    meta.state = ExtensionState.ENABLED;
 
219
    _signals.emit('extension-state-changed', meta);
 
220
}
 
221
 
 
222
function logExtensionError(uuid, message, state) {
 
223
    if (!errors[uuid]) errors[uuid] = [];
 
224
    errors[uuid].push(message);
 
225
    global.logError('Extension "%s" had error: %s'.format(uuid, message));
 
226
    state = state || ExtensionState.ERROR;
 
227
    _signals.emit('extension-state-changed', { uuid: uuid,
 
228
                                               error: message,
 
229
                                               state: state });
 
230
}
 
231
 
62
232
function loadExtension(dir, enabled, type) {
63
233
    let info;
64
 
    let baseErrorString = 'While loading extension from "' + dir.get_parse_name() + '": ';
 
234
    let uuid = dir.get_basename();
65
235
 
66
236
    let metadataFile = dir.get_child('metadata.json');
67
237
    if (!metadataFile.query_exists(null)) {
68
 
        global.logError(baseErrorString + 'Missing metadata.json');
 
238
        logExtensionError(uuid, 'Missing metadata.json');
69
239
        return;
70
240
    }
71
241
 
73
243
    try {
74
244
        metadataContents = Shell.get_file_contents_utf8_sync(metadataFile.get_path());
75
245
    } catch (e) {
76
 
        global.logError(baseErrorString + 'Failed to load metadata.json: ' + e);
 
246
        logExtensionError(uuid, 'Failed to load metadata.json: ' + e);
77
247
        return;
78
248
    }
79
249
    let meta;
80
250
    try {
81
251
        meta = JSON.parse(metadataContents);
82
252
    } catch (e) {
83
 
        global.logError(baseErrorString + 'Failed to parse metadata.json: ' + e);
 
253
        logExtensionError(uuid, 'Failed to parse metadata.json: ' + e);
84
254
        return;
85
255
    }
 
256
 
86
257
    let requiredProperties = ['uuid', 'name', 'description', 'shell-version'];
87
258
    for (let i = 0; i < requiredProperties.length; i++) {
88
259
        let prop = requiredProperties[i];
89
260
        if (!meta[prop]) {
90
 
            global.logError(baseErrorString + 'missing "' + prop + '" property in metadata.json');
 
261
            logExtensionError(uuid, 'missing "' + prop + '" property in metadata.json');
91
262
            return;
92
263
        }
93
264
    }
94
265
 
95
 
    if (extensions[meta.uuid] != undefined) {
96
 
        global.logError(baseErrorString + "extension already loaded");
 
266
    if (extensions[uuid] != undefined) {
 
267
        logExtensionError(uuid, "extension already loaded");
97
268
        return;
98
269
    }
99
270
 
100
271
    // Encourage people to add this
101
272
    if (!meta['url']) {
102
 
        global.log(baseErrorString + 'Warning: Missing "url" property in metadata.json');
 
273
        global.log('Warning: Missing "url" property in metadata.json');
103
274
    }
104
275
 
105
 
    let base = dir.get_basename();
106
 
    if (base != meta.uuid) {
107
 
        global.logError(baseErrorString + 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + base + '"');
 
276
    if (uuid != meta.uuid) {
 
277
        logExtensionError(uuid, 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + uuid + '"');
108
278
        return;
109
279
    }
110
280
 
111
281
    if (!versionCheck(meta['shell-version'], Config.PACKAGE_VERSION) ||
112
282
        (meta['js-version'] && !versionCheck(meta['js-version'], Config.GJS_VERSION))) {
113
 
        global.logError(baseErrorString + 'extension is not compatible with current GNOME Shell and/or GJS version');
 
283
        logExtensionError(uuid, 'extension is not compatible with current GNOME Shell and/or GJS version');
114
284
        return;
115
285
    }
116
286
 
117
 
    extensionMeta[meta.uuid] = meta;
118
 
    extensionMeta[meta.uuid].type = type;
119
 
    extensionMeta[meta.uuid].path = dir.get_path();
120
 
    if (!enabled) {
121
 
        extensionMeta[meta.uuid].state = ExtensionState.DISABLED;
122
 
        return;
123
 
    }
 
287
    extensionMeta[uuid] = meta;
 
288
    meta.type = type;
 
289
    meta.path = dir.get_path();
 
290
    meta.error = '';
124
291
 
125
292
    // Default to error, we set success as the last step
126
 
    extensionMeta[meta.uuid].state = ExtensionState.ERROR;
 
293
    meta.state = ExtensionState.ERROR;
 
294
 
 
295
    if (!versionCheck(meta['shell-version'], Config.PACKAGE_VERSION) ||
 
296
        (meta['js-version'] && !versionCheck(meta['js-version'], Config.GJS_VERSION))) {
 
297
        logExtensionError(uuid, 'extension is not compatible with current GNOME Shell and/or GJS version', ExtensionState.OUT_OF_DATE);
 
298
        meta.state = ExtensionState.OUT_OF_DATE;
 
299
        return;
 
300
    }
127
301
 
128
302
    let extensionJs = dir.get_child('extension.js');
129
303
    if (!extensionJs.query_exists(null)) {
130
 
        global.logError(baseErrorString + 'Missing extension.js');
 
304
        logExtensionError(uuid, 'Missing extension.js');
131
305
        return;
132
306
    }
133
307
    let stylesheetPath = null;
138
312
        try {
139
313
            theme.load_stylesheet(stylesheetFile.get_path());
140
314
        } catch (e) {
141
 
            global.logError(baseErrorString + 'Stylesheet parse error: ' + e);
 
315
            logExtensionError(uuid, 'Stylesheet parse error: ' + e);
142
316
            return;
143
317
        }
144
318
    }
145
319
 
146
320
    let extensionModule;
 
321
    let extensionState = null;
147
322
    try {
148
323
        global.add_extension_importer('imports.ui.extensionSystem.extensions', meta.uuid, dir.get_path());
149
324
        extensionModule = extensions[meta.uuid].extension;
150
325
    } catch (e) {
151
326
        if (stylesheetPath != null)
152
327
            theme.unload_stylesheet(stylesheetPath);
153
 
        global.logError(baseErrorString + e);
154
 
        return;
155
 
    }
156
 
    if (!extensionModule.main) {
157
 
        global.logError(baseErrorString + 'missing \'main\' function');
158
 
        return;
159
 
    }
 
328
        logExtensionError(uuid, e);
 
329
        return;
 
330
    }
 
331
 
 
332
    if (!extensionModule.init) {
 
333
        logExtensionError(uuid, 'missing \'init\' function');
 
334
        return;
 
335
    }
 
336
 
160
337
    try {
161
 
        extensionModule.main(meta);
 
338
        extensionState = extensionModule.init(meta);
162
339
    } catch (e) {
163
340
        if (stylesheetPath != null)
164
341
            theme.unload_stylesheet(stylesheetPath);
165
 
        global.logError(baseErrorString + 'Failed to evaluate main function:' + e);
166
 
        return;
167
 
    }
168
 
    extensionMeta[meta.uuid].state = ExtensionState.ENABLED;
 
342
        logExtensionError(uuid, 'Failed to evaluate init function:' + e);
 
343
        return;
 
344
    }
 
345
 
 
346
    if (!extensionState)
 
347
        extensionState = extensionModule;
 
348
    extensionStateObjs[uuid] = extensionState;
 
349
 
 
350
    if (!extensionState.enable) {
 
351
        logExtensionError(uuid, 'missing \'enable\' function');
 
352
        return;
 
353
    }
 
354
    if (!extensionState.disable) {
 
355
        logExtensionError(uuid, 'missing \'disable\' function');
 
356
        return;
 
357
    }
 
358
 
 
359
    meta.state = ExtensionState.DISABLED;
 
360
 
 
361
    if (enabled)
 
362
        enableExtension(uuid);
 
363
 
 
364
    _signals.emit('extension-loaded', meta.uuid);
 
365
    _signals.emit('extension-state-changed', meta);
169
366
    global.log('Loaded extension ' + meta.uuid);
170
367
}
171
368
 
 
369
function onEnabledExtensionsChanged() {
 
370
    let newEnabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
 
371
 
 
372
    // Find and enable all the newly enabled extensions: UUIDs found in the
 
373
    // new setting, but not in the old one.
 
374
    newEnabledExtensions.filter(function(uuid) {
 
375
        return enabledExtensions.indexOf(uuid) == -1;
 
376
    }).forEach(function(uuid) {
 
377
        enableExtension(uuid);
 
378
    });
 
379
 
 
380
    // Find and disable all the newly disabled extensions: UUIDs found in the
 
381
    // old setting, but not in the new one.
 
382
    enabledExtensions.filter(function(item) {
 
383
        return newEnabledExtensions.indexOf(item) == -1;
 
384
    }).forEach(function(uuid) {
 
385
        disableExtension(uuid);
 
386
    });
 
387
 
 
388
    enabledExtensions = newEnabledExtensions;
 
389
}
 
390
 
172
391
function init() {
173
392
    let userExtensionsPath = GLib.build_filenamev([global.userdatadir, 'extensions']);
174
393
    userExtensionsDir = Gio.file_new_for_path(userExtensionsPath);
175
394
    try {
176
 
        userExtensionsDir.make_directory_with_parents(null);
 
395
        if (!userExtensionsDir.query_exists(null))
 
396
            userExtensionsDir.make_directory_with_parents(null);
177
397
    } catch (e) {
178
398
        global.logError('' + e);
179
399
    }
180
400
 
181
 
    disabledExtensions = global.settings.get_strv('disabled-extensions', -1);
182
 
    enabledExtensions = global.settings.get_strv('enabled-extensions', -1);
 
401
    global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged);
 
402
    enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
183
403
}
184
404
 
185
405
function _loadExtensionsIn(dir, type) {
197
417
        if (fileType != Gio.FileType.DIRECTORY)
198
418
            continue;
199
419
        let name = info.get_name();
200
 
        // Enable all but disabled extensions if enabledExtensions is not set.
201
 
        // If it is set, enable one those, except they are disabled as well.
202
 
        let enabled = (enabledExtensions.length == 0 || enabledExtensions.indexOf(name) >= 0)
203
 
            && disabledExtensions.indexOf(name) < 0;
204
420
        let child = dir.get_child(name);
 
421
        let enabled = enabledExtensions.indexOf(name) != -1;
205
422
        loadExtension(child, enabled, type);
206
423
    }
207
424
    fileEnum.close(null);