summaryrefslogtreecommitdiff
path: root/src/plugins/Plugins.vala
blob: 9a8860b41bb91017a8e35ea3332566316c460b7e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
/* Copyright 2011-2015 Yorba Foundation
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

namespace Plugins {

// GModule doesn't have a truly generic way to determine if a file is a shared library by extension,
// so these are hard-coded
private const string[] SHARED_LIB_EXTS = { "so", "la" };

// Although not expecting this system to last very long, these ranges declare what versions of this
// interface are supported by the current implementation.
private const int MIN_SPIT_INTERFACE = 0;
private const int MAX_SPIT_INTERFACE = 0;

public class ExtensionPoint {
    public GLib.Type pluggable_type { get; private set; }
    // name is user-visible
    public string name { get; private set; }
    public string? icon_name { get; private set; }
    public string[]? core_ids { get; private set; }
    
    public ExtensionPoint(Type pluggable_type, string name, string? icon_name, string[]? core_ids) {
        this.pluggable_type = pluggable_type;
        this.name = name;
        this.icon_name = icon_name;
        this.core_ids = core_ids;
    }
}

private class ModuleRep {
    public File file;
    public Module? module;
    public Spit.Module? spit_module = null;
    public int spit_interface = Spit.UNSUPPORTED_INTERFACE;
    public string? id = null;
    
    private ModuleRep(File file) {
        this.file = file;
        
        module = Module.open(file.get_path(), ModuleFlags.BIND_LAZY);
    }
    
    ~ModuleRep() {
        // ensure that the Spit.Module is destroyed before the GLib.Module
        spit_module = null;
    }
    
    // Have to use this funky static factory because GModule is a compact class and has no copy
    // constructor.  The handle must be kept open for the lifetime of the application (or until
    // the module is ready to be discarded), as dropping the reference will unload the binary.
    public static ModuleRep? open(File file) {
        ModuleRep module_rep = new ModuleRep(file);
        
        return (module_rep.module != null) ? module_rep : null;
    }
}

private class PluggableRep {
    public Spit.Pluggable pluggable { get; private set; }
    public string id { get; private set; }
    public bool is_core { get; private set; default = false; }
    public bool activated { get; private set; default = false; }
    
    private bool enabled = false;
    
    // Note that creating a PluggableRep does not activate it.
    public PluggableRep(Spit.Pluggable pluggable) {
        this.pluggable = pluggable;
        id = pluggable.get_id();
    }
    
    public void activate() {
        // determine if a core pluggable (which is only known after all the extension points
        // register themselves)
        is_core = is_core_pluggable(pluggable);
        
       FuzzyPropertyState saved_state = Config.Facade.get_instance().is_plugin_enabled(id);
        enabled = ((is_core && (saved_state != FuzzyPropertyState.DISABLED)) ||
            (!is_core && (saved_state == FuzzyPropertyState.ENABLED)));
        
        // inform the plugin of its activation state
        pluggable.activation(enabled);
        
        activated = true;
    }
    
    public bool is_enabled() {
        return enabled;
    }
    
    // Returns true if value changed, false otherwise
    public bool set_enabled(bool enabled) {
        if (enabled == this.enabled)
            return false;
        
        this.enabled = enabled;
        Config.Facade.get_instance().set_plugin_enabled(id, enabled);
        pluggable.activation(enabled);
        
        return true;
    }
}

private File[] search_dirs;
private Gee.HashMap<string, ModuleRep> module_table;
private Gee.HashMap<string, PluggableRep> pluggable_table;
private Gee.HashMap<Type, ExtensionPoint> extension_points;
private Gee.HashSet<string> core_ids;

public void init() throws Error {
    search_dirs = new File[0];
    search_dirs += AppDirs.get_user_plugins_dir();
    search_dirs += AppDirs.get_system_plugins_dir();
    
    module_table = new Gee.HashMap<string, ModuleRep>();
    pluggable_table = new Gee.HashMap<string, PluggableRep>();
    extension_points = new Gee.HashMap<Type, ExtensionPoint>();
    core_ids = new Gee.HashSet<string>();
    
    // do this after constructing member variables so accessors don't blow up if GModule isn't
    // supported
    if (!Module.supported()) {
        warning("Plugins not support: GModule not supported on this platform.");
        
        return;
    }
    
    foreach (File dir in search_dirs) {
        try {
            search_for_plugins(dir);
        } catch (Error err) {
            debug("Unable to search directory %s for plugins: %s", dir.get_path(), err.message);
        }
    }
}

public void terminate() {
    search_dirs = null;
    pluggable_table = null;
    module_table = null;
    extension_points = null;
    core_ids = null;
}

public class Notifier {
    private static Notifier? instance = null;
    
    public signal void pluggable_activation(Spit.Pluggable pluggable, bool enabled);
    
    private Notifier() {
    }
    
    public static Notifier get_instance() {
        if (instance == null)
            instance = new Notifier();
        
        return instance;
    }
}

public void register_extension_point(Type type, string name, string? icon_name, string[]? core_ids) {
    // if this assertion triggers, it means this extension point has already registered
    assert(!extension_points.has_key(type));
    
    extension_points.set(type, new ExtensionPoint(type, name, icon_name, core_ids));
    
    // add core IDs to master list
    if (core_ids != null) {
        foreach (string core_id in core_ids)
            Plugins.core_ids.add(core_id);
    }
    
    // activate all the pluggables for this extension point
    foreach (PluggableRep pluggable_rep in pluggable_table.values) {
        if (!pluggable_rep.pluggable.get_type().is_a(type))
            continue;
        
        pluggable_rep.activate();
        Notifier.get_instance().pluggable_activation(pluggable_rep.pluggable, pluggable_rep.is_enabled());
    }
}

public Gee.Collection<Spit.Pluggable> get_pluggables(bool include_disabled = false) {
    Gee.Collection<Spit.Pluggable> all = new Gee.HashSet<Spit.Pluggable>();
    foreach (PluggableRep pluggable_rep in pluggable_table.values) {
        if (pluggable_rep.activated && (include_disabled || pluggable_rep.is_enabled()))
            all.add(pluggable_rep.pluggable);
    }
    
    return all;
}

public bool is_core_pluggable(Spit.Pluggable pluggable) {
    return core_ids.contains(pluggable.get_id());
}

private ModuleRep? get_module_for_pluggable(Spit.Pluggable needle) {
    foreach (ModuleRep module_rep in module_table.values) {
        Spit.Pluggable[]? pluggables = module_rep.spit_module.get_pluggables();
        if (pluggables != null) {
            foreach (Spit.Pluggable pluggable in pluggables) {
                if (pluggable == needle)
                    return module_rep;
            }
        }
    }
    
    return null;
}

public string? get_pluggable_module_id(Spit.Pluggable needle) {
    ModuleRep? module_rep = get_module_for_pluggable(needle);
    
    return (module_rep != null) ? module_rep.spit_module.get_id() : null;
}

public Gee.Collection<ExtensionPoint> get_extension_points(owned CompareDataFunc? compare_func = null) {
    Gee.Collection<ExtensionPoint> sorted = new Gee.TreeSet<ExtensionPoint>((owned) compare_func);
    sorted.add_all(extension_points.values);
    
    return sorted;
}

public Gee.Collection<Spit.Pluggable> get_pluggables_for_type(Type type,
    owned CompareDataFunc? compare_func = null, bool include_disabled = false) {
    // if this triggers it means the extension point didn't register itself at init() time
    assert(extension_points.has_key(type));
    
    Gee.Collection<Spit.Pluggable> for_type = new Gee.TreeSet<Spit.Pluggable>((owned) compare_func);
    foreach (PluggableRep pluggable_rep in pluggable_table.values) {
        if (pluggable_rep.activated 
            && pluggable_rep.pluggable.get_type().is_a(type) 
            && (include_disabled || pluggable_rep.is_enabled())) {
            for_type.add(pluggable_rep.pluggable);
        }
    }
    
    return for_type;
}

public string? get_pluggable_name(string id) {
    PluggableRep? pluggable_rep = pluggable_table.get(id);
    
    return (pluggable_rep != null && pluggable_rep.activated) 
        ? pluggable_rep.pluggable.get_pluggable_name() : null;
}

public bool get_pluggable_info(string id, ref Spit.PluggableInfo info) {
    PluggableRep? pluggable_rep = pluggable_table.get(id);
    if (pluggable_rep == null || !pluggable_rep.activated)
        return false;
    
    pluggable_rep.pluggable.get_info(ref info);
    
    return true;
}

public bool get_pluggable_enabled(string id, out bool enabled) {
    PluggableRep? pluggable_rep = pluggable_table.get(id);
    if (pluggable_rep == null || !pluggable_rep.activated) {
        enabled = false;
        
        return false;
    }
    
    enabled = pluggable_rep.is_enabled();
    
    return true;
}

public void set_pluggable_enabled(string id, bool enabled) {
    PluggableRep? pluggable_rep = pluggable_table.get(id);
    if (pluggable_rep == null || !pluggable_rep.activated)
        return;
    
    if (pluggable_rep.set_enabled(enabled))
        Notifier.get_instance().pluggable_activation(pluggable_rep.pluggable, enabled);
}

public File get_pluggable_module_file(Spit.Pluggable pluggable) {
    ModuleRep? module_rep = get_module_for_pluggable(pluggable);
    
    return (module_rep != null) ? module_rep.file : null;
}

public int compare_pluggable_names(void *a, void *b) {
    Spit.Pluggable *apluggable = (Spit.Pluggable *) a;
    Spit.Pluggable *bpluggable = (Spit.Pluggable *) b;
    
    return apluggable->get_pluggable_name().collate(bpluggable->get_pluggable_name());
}

public int compare_extension_point_names(void *a, void *b) {
    ExtensionPoint *apoint = (ExtensionPoint *) a;
    ExtensionPoint *bpoint = (ExtensionPoint *) b;
    
    return apoint->name.collate(bpoint->name);
}

private bool is_shared_library(File file) {
    string name, ext;
    disassemble_filename(file.get_basename(), out name, out ext);
    
    foreach (string shared_ext in SHARED_LIB_EXTS) {
        if (ext == shared_ext)
            return true;
    }
    
    return false;
}

private void search_for_plugins(File dir) throws Error {
    debug("Searching %s for plugins ...", dir.get_path());
    
    // build a set of module names sans file extension ... this is to deal with the question of
    // .so vs. .la existing in the same directory (and letting GModule deal with the problem)
    FileEnumerator enumerator = dir.enumerate_children(Util.FILE_ATTRIBUTES,
        FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
    for (;;) {
        FileInfo? info = enumerator.next_file(null);
        if (info == null)
            break;
        
        if (info.get_is_hidden())
            continue;
        
        File file = dir.get_child(info.get_name());
        
        switch (info.get_file_type()) {
            case FileType.DIRECTORY:
                try {
                    search_for_plugins(file);
                } catch (Error err) {
                    warning("Unable to search directory %s for plugins: %s", file.get_path(), err.message);
                }
            break;
            
            case FileType.REGULAR:
                if (is_shared_library(file))
                    load_module(file);
            break;
            
            default:
                // ignored
            break;
        }
    }
}

private void load_module(File file) {
    ModuleRep? module_rep = ModuleRep.open(file);
    if (module_rep == null) {
        critical("Unable to load module %s: %s", file.get_path(), Module.error());
        
        return;
    }
    
    // look for the well-known entry point
    void *entry;
    if (!module_rep.module.symbol(Spit.ENTRY_POINT_NAME, out entry)) {
        critical("Unable to load module %s: well-known entry point %s not found", file.get_path(),
            Spit.ENTRY_POINT_NAME);
        
        return;
    }
    
    Spit.EntryPoint spit_entry_point = (Spit.EntryPoint) entry;
    
    assert(MIN_SPIT_INTERFACE <= Spit.CURRENT_INTERFACE && Spit.CURRENT_INTERFACE <= MAX_SPIT_INTERFACE);
    Spit.EntryPointParams params = Spit.EntryPointParams();
    params.host_min_spit_interface = MIN_SPIT_INTERFACE;
    params.host_max_spit_interface = MAX_SPIT_INTERFACE;
    params.module_spit_interface = Spit.UNSUPPORTED_INTERFACE;
    params.module_file = file;
    
    module_rep.spit_module = spit_entry_point(&params);
    if (params.module_spit_interface == Spit.UNSUPPORTED_INTERFACE) {
        critical("Unable to load module %s: module reports no support for SPIT interfaces %d to %d",
            file.get_path(), MIN_SPIT_INTERFACE, MAX_SPIT_INTERFACE);
        
        return;
    }
    
    if (params.module_spit_interface < MIN_SPIT_INTERFACE || params.module_spit_interface > MAX_SPIT_INTERFACE) {
        critical("Unable to load module %s: module reports unsupported SPIT version %d (out of range %d to %d)",
            file.get_path(), module_rep.spit_interface, MIN_SPIT_INTERFACE, MAX_SPIT_INTERFACE);
        
        return;
    }
    
    module_rep.spit_interface = params.module_spit_interface;
    
    // verify type (as best as possible; still potential to segfault inside GType here)
    if (!(module_rep.spit_module is Spit.Module))
        module_rep.spit_module = null;
    
    if (module_rep.spit_module == null) {
        critical("Unable to load module %s (SPIT %d): no spit module returned", file.get_path(),
            module_rep.spit_interface);
        
        return;
    }
    
    // if module has already been loaded, drop this one (search path is set up to load user-installed
    // binaries prior to system binaries)
    module_rep.id = prepare_input_text(module_rep.spit_module.get_id(), PrepareInputTextOptions.DEFAULT, -1);
    if (module_rep.id == null) {
        critical("Unable to load module %s (SPIT %d): invalid or empty module name",
            file.get_path(), module_rep.spit_interface);
        
        return;
    }
    
    if (module_table.has_key(module_rep.id)) {
        critical("Not loading module %s (SPIT %d): module with name \"%s\" already loaded",
            file.get_path(), module_rep.spit_interface, module_rep.id);
        
        return;
    }
    
    debug("Loaded SPIT module \"%s %s\" (%s) [%s]", module_rep.spit_module.get_module_name(),
        module_rep.spit_module.get_version(), module_rep.id, file.get_path());
    
    // stash in module table by their ID
    module_table.set(module_rep.id, module_rep);
    
    // stash pluggables in pluggable table by their ID
    foreach (Spit.Pluggable pluggable in module_rep.spit_module.get_pluggables())
        pluggable_table.set(pluggable.get_id(), new PluggableRep(pluggable));
}

}