1 /* ide-application-plugins.c
2 *
3 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21 #define G_LOG_DOMAIN "ide-application-plugins"
22
23 #include "config.h"
24
25 #include <libide-plugins.h>
26
27 #include "ide-application.h"
28 #include "ide-application-addin.h"
29 #include "ide-application-private.h"
30
31 static void
ide_application_changed_plugin_cb(GSettings * settings,const gchar * key,PeasPluginInfo * plugin_info)32 ide_application_changed_plugin_cb (GSettings *settings,
33 const gchar *key,
34 PeasPluginInfo *plugin_info)
35 {
36 PeasEngine *engine;
37
38 IDE_ENTRY;
39
40 g_assert (G_IS_SETTINGS (settings));
41 g_assert (key != NULL);
42 g_assert (plugin_info != NULL);
43
44 engine = peas_engine_get_default ();
45
46 if (!g_settings_get_boolean (settings, key))
47 peas_engine_unload_plugin (engine, plugin_info);
48 else
49 peas_engine_load_plugin (engine, plugin_info);
50
51 IDE_EXIT;
52 }
53
54 static GSettings *
_ide_application_plugin_get_settings(IdeApplication * self,PeasPluginInfo * plugin_info)55 _ide_application_plugin_get_settings (IdeApplication *self,
56 PeasPluginInfo *plugin_info)
57 {
58 GSettings *settings;
59 const gchar *module_name;
60
61 g_assert (IDE_IS_MAIN_THREAD ());
62 g_assert (IDE_IS_APPLICATION (self));
63 g_assert (plugin_info != NULL);
64
65 module_name = peas_plugin_info_get_module_name (plugin_info);
66
67 if G_UNLIKELY (self->plugin_settings == NULL)
68 self->plugin_settings =
69 g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
70
71 if (!(settings = g_hash_table_lookup (self->plugin_settings, module_name)))
72 {
73 g_autofree gchar *path = NULL;
74
75 path = g_strdup_printf ("/org/gnome/builder/plugins/%s/", module_name);
76 settings = g_settings_new_with_path ("org.gnome.builder.plugin", path);
77 g_hash_table_insert (self->plugin_settings, g_strdup (module_name), settings);
78
79 g_signal_connect (settings,
80 "changed::enabled",
81 G_CALLBACK (ide_application_changed_plugin_cb),
82 plugin_info);
83 }
84
85 return settings;
86 }
87
88 static gboolean
ide_application_can_load_plugin(IdeApplication * self,PeasPluginInfo * plugin_info,GHashTable * circular)89 ide_application_can_load_plugin (IdeApplication *self,
90 PeasPluginInfo *plugin_info,
91 GHashTable *circular)
92 {
93 PeasEngine *engine = peas_engine_get_default ();
94 const gchar *module_name;
95 const gchar *module_dir;
96 const gchar **deps;
97 GSettings *settings;
98
99 g_assert (IDE_IS_MAIN_THREAD ());
100 g_assert (IDE_IS_APPLICATION (self));
101 g_assert (circular != NULL);
102
103 if (plugin_info == NULL)
104 return FALSE;
105
106 module_dir = peas_plugin_info_get_module_dir (plugin_info);
107 module_name = peas_plugin_info_get_module_name (plugin_info);
108
109 /* Short-circuit for single-plugin mode */
110 if (self->plugin != NULL)
111 return ide_str_equal0 (module_name, self->plugin);
112
113 if (g_hash_table_contains (circular, module_name))
114 {
115 g_warning ("Circular dependency found in module %s", module_name);
116 return FALSE;
117 }
118
119 g_hash_table_add (circular, (gpointer)module_name);
120
121 /* Make sure the plugin has not been disabled in settings. */
122 settings = _ide_application_plugin_get_settings (self, plugin_info);
123 if (!g_settings_get_boolean (settings, "enabled"))
124 return FALSE;
125
126 #if 0
127 if (self->mode == IDE_APPLICATION_MODE_WORKER)
128 {
129 if (self->worker != plugin_info)
130 return FALSE;
131 }
132 #endif
133
134 /*
135 * If the plugin is not bundled within the Builder executable, then we
136 * require that an X-Builder-ABI=major.minor style extended data be
137 * provided to ensure we have proper ABI.
138 *
139 * You could get around this by loading a plugin that then loads resouces
140 * containing external data, but this is good enough for now.
141 */
142
143 if (!g_str_has_prefix (module_dir, "resource:///plugins/"))
144 {
145 const gchar *abi;
146
147 if (!(abi = peas_plugin_info_get_external_data (plugin_info, "Builder-ABI")))
148 {
149 g_critical ("Refusing to load plugin %s because X-Builder-ABI is missing",
150 module_name);
151 return FALSE;
152 }
153
154 if (g_strcmp0 (PACKAGE_ABI_S, abi) != 0)
155 {
156 g_critical ("Refusing to load plugin %s, expected ABI %d.%d and got %s",
157 module_name, IDE_MAJOR_VERSION, 0, abi);
158 return FALSE;
159 }
160 }
161
162 /*
163 * If this plugin has dependencies, we need to check that the dependencies
164 * can also be loaded.
165 */
166 if ((deps = peas_plugin_info_get_dependencies (plugin_info)))
167 {
168 for (guint i = 0; deps[i]; i++)
169 {
170 PeasPluginInfo *dep = peas_engine_get_plugin_info (engine, deps[i]);
171
172 if (!ide_application_can_load_plugin (self, dep, circular))
173 return FALSE;
174 }
175 }
176
177 g_hash_table_remove (circular, (gpointer)module_name);
178
179 return TRUE;
180 }
181
182 static void
ide_application_load_plugin_resources(IdeApplication * self,PeasEngine * engine,PeasPluginInfo * plugin_info)183 ide_application_load_plugin_resources (IdeApplication *self,
184 PeasEngine *engine,
185 PeasPluginInfo *plugin_info)
186 {
187 g_autofree gchar *gresources_path = NULL;
188 g_autofree gchar *gresources_basename = NULL;
189 const gchar *module_dir;
190 const gchar *module_name;
191
192 g_assert (IDE_IS_APPLICATION (self));
193 g_assert (plugin_info != NULL);
194 g_assert (PEAS_IS_ENGINE (engine));
195
196 module_dir = peas_plugin_info_get_module_dir (plugin_info);
197 module_name = peas_plugin_info_get_module_name (plugin_info);
198 gresources_basename = g_strdup_printf ("%s.gresource", module_name);
199 gresources_path = g_build_filename (module_dir, gresources_basename, NULL);
200
201 if (g_file_test (gresources_path, G_FILE_TEST_IS_REGULAR))
202 {
203 g_autofree gchar *resource_path = NULL;
204 g_autoptr(GError) error = NULL;
205 GResource *resource;
206
207 resource = g_resource_load (gresources_path, &error);
208
209 if (resource == NULL)
210 {
211 g_warning ("Failed to load gresources: %s", error->message);
212 return;
213 }
214
215 g_hash_table_insert (self->plugin_gresources, g_strdup (module_name), resource);
216 g_resources_register (resource);
217
218 resource_path = g_strdup_printf ("resource:///plugins/%s", module_name);
219 dzl_application_add_resources (DZL_APPLICATION (self), resource_path);
220 }
221 }
222
223 void
_ide_application_load_plugin(IdeApplication * self,PeasPluginInfo * plugin_info)224 _ide_application_load_plugin (IdeApplication *self,
225 PeasPluginInfo *plugin_info)
226 {
227 PeasEngine *engine = peas_engine_get_default ();
228 g_autoptr(GHashTable) circular = NULL;
229
230 g_assert (IDE_IS_MAIN_THREAD ());
231 g_assert (IDE_IS_APPLICATION (self));
232 g_assert (plugin_info != NULL);
233
234 circular = g_hash_table_new (g_str_hash, g_str_equal);
235
236 if (ide_application_can_load_plugin (self, plugin_info, circular))
237 peas_engine_load_plugin (engine, plugin_info);
238 }
239
240 static void
ide_application_plugins_load_plugin_cb(IdeApplication * self,PeasPluginInfo * plugin_info,PeasEngine * engine)241 ide_application_plugins_load_plugin_cb (IdeApplication *self,
242 PeasPluginInfo *plugin_info,
243 PeasEngine *engine)
244 {
245 const gchar *data_dir;
246 const gchar *module_dir;
247 const gchar *module_name;
248
249 g_assert (IDE_IS_MAIN_THREAD ());
250 g_assert (IDE_IS_APPLICATION (self));
251 g_assert (plugin_info != NULL);
252 g_assert (PEAS_IS_ENGINE (engine));
253
254 data_dir = peas_plugin_info_get_data_dir (plugin_info);
255 module_dir = peas_plugin_info_get_module_dir (plugin_info);
256 module_name = peas_plugin_info_get_module_name (plugin_info);
257
258 g_debug ("Loaded plugin \"%s\" with module-dir \"%s\"",
259 module_name, module_dir);
260
261 if (peas_plugin_info_get_external_data (plugin_info, "Has-Resources"))
262 {
263 /* Possibly load bundled .gresource files if the plugin is not
264 * embedded into the application (such as python3 modules).
265 */
266 ide_application_load_plugin_resources (self, engine, plugin_info);
267 }
268
269 /*
270 * Only register resources if the path is to an embedded resource
271 * or if it's not builtin (and therefore maybe doesn't use .gresource
272 * files). That helps reduce the number IOPS we do.
273 */
274 if (g_str_has_prefix (data_dir, "resource://") ||
275 !peas_plugin_info_is_builtin (plugin_info))
276 dzl_application_add_resources (DZL_APPLICATION (self), data_dir);
277 }
278
279 static void
ide_application_plugins_unload_plugin_cb(IdeApplication * self,PeasPluginInfo * plugin_info,PeasEngine * engine)280 ide_application_plugins_unload_plugin_cb (IdeApplication *self,
281 PeasPluginInfo *plugin_info,
282 PeasEngine *engine)
283 {
284 g_assert (IDE_IS_MAIN_THREAD ());
285 g_assert (IDE_IS_APPLICATION (self));
286 g_assert (plugin_info != NULL);
287 g_assert (PEAS_IS_ENGINE (engine));
288
289 }
290
291 /**
292 * _ide_application_load_plugins_for_startup:
293 *
294 * This function will load all of the plugins that are candidates for
295 * early-stage initialization. Usually, that is any plugin that has a
296 * command-line handler and uses "X-At-Startup=true" in their .plugin
297 * manifest.
298 *
299 * Since: 3.32
300 */
301 void
_ide_application_load_plugins_for_startup(IdeApplication * self)302 _ide_application_load_plugins_for_startup (IdeApplication *self)
303 {
304 PeasEngine *engine = peas_engine_get_default ();
305 const GList *plugins;
306
307 g_assert (IDE_IS_APPLICATION (self));
308
309 g_signal_connect_object (engine,
310 "load-plugin",
311 G_CALLBACK (ide_application_plugins_load_plugin_cb),
312 self,
313 G_CONNECT_SWAPPED);
314
315 g_signal_connect_object (engine,
316 "unload-plugin",
317 G_CALLBACK (ide_application_plugins_unload_plugin_cb),
318 self,
319 G_CONNECT_SWAPPED);
320
321 /* Ensure that our embedded plugins are allowed early access to
322 * start loading (before we ever look at anything on disk). This
323 * ensures that only embedded plugins can be used at startup,
324 * saving us some precious disk I/O.
325 */
326 peas_engine_prepend_search_path (engine, "resource:///plugins", "resource:///plugins");
327
328 /* If we are within the Flatpak, then load any extensions we've
329 * found merged into the extensions directory.
330 */
331 if (ide_is_flatpak ())
332 peas_engine_add_search_path (engine,
333 "/app/extensions/lib/gnome-builder/plugins",
334 "/app/extensions/lib/gnome-builder/plugins");
335
336 /* Our first step is to load our "At-Startup" plugins, which may
337 * contain things like command-line handlers. For example, the
338 * greeter may handle command-line options and then show the
339 * greeter workspace.
340 */
341 plugins = peas_engine_get_plugin_list (engine);
342 for (const GList *iter = plugins; iter; iter = iter->next)
343 {
344 PeasPluginInfo *plugin_info = iter->data;
345
346 if (!peas_plugin_info_is_loaded (plugin_info) &&
347 peas_plugin_info_get_external_data (plugin_info, "At-Startup"))
348 _ide_application_load_plugin (self, plugin_info);
349 }
350 }
351
352 /**
353 * _ide_application_load_plugins:
354 * @self: a #IdeApplication
355 *
356 * This function loads any additional plugins that have not yet been
357 * loaded during early startup.
358 *
359 * Since: 3.32
360 */
361 void
_ide_application_load_plugins(IdeApplication * self)362 _ide_application_load_plugins (IdeApplication *self)
363 {
364 g_autofree gchar *user_plugins_dir = NULL;
365 g_autoptr(GError) error = NULL;
366 const GList *plugins;
367 PeasEngine *engine;
368
369 g_assert (IDE_IS_APPLICATION (self));
370
371 engine = peas_engine_get_default ();
372
373 /* Now that we have gotten past our startup plugins (which must be
374 * embedded into the gnome-builder executable, we can enable the
375 * system plugins that are loaded from disk.
376 */
377 peas_engine_prepend_search_path (engine,
378 PACKAGE_LIBDIR"/gnome-builder/plugins",
379 PACKAGE_DATADIR"/gnome-builder/plugins");
380
381 if (ide_is_flatpak ())
382 {
383 g_autofree gchar *extensions_plugins_dir = NULL;
384 g_autofree gchar *plugins_dir = NULL;
385
386 plugins_dir = g_build_filename (g_get_home_dir (),
387 ".local",
388 "share",
389 "gnome-builder",
390 "plugins",
391 NULL);
392 peas_engine_prepend_search_path (engine, plugins_dir, plugins_dir);
393
394 extensions_plugins_dir = g_build_filename ("/app",
395 "extensions",
396 "lib",
397 "gnome-builder",
398 "plugins",
399 NULL);
400 peas_engine_prepend_search_path (engine, extensions_plugins_dir, extensions_plugins_dir);
401 }
402
403 user_plugins_dir = g_build_filename (g_get_user_data_dir (),
404 "gnome-builder",
405 "plugins",
406 NULL);
407 peas_engine_prepend_search_path (engine, user_plugins_dir, NULL);
408
409 /* Ensure that we have all our required GObject Introspection packages
410 * loaded so that plugins don't need to require_version() as that is
411 * tedious and annoying to keep up to date.
412 *
413 * If we can't load any of our dependent packages, then fail to load
414 * python3 plugins altogether to avoid loading anything improper into
415 * the process space.
416 */
417 g_irepository_prepend_search_path (PACKAGE_LIBDIR"/gnome-builder/girepository-1.0");
418 if (!g_irepository_require (NULL, "GtkSource", "4", 0, &error) ||
419 !g_irepository_require (NULL, "Gio", "2.0", 0, &error) ||
420 !g_irepository_require (NULL, "GLib", "2.0", 0, &error) ||
421 !g_irepository_require (NULL, "Gtk", "3.0", 0, &error) ||
422 !g_irepository_require (NULL, "Dazzle", "1.0", 0, &error) ||
423 !g_irepository_require (NULL, "Jsonrpc", "1.0", 0, &error) ||
424 !g_irepository_require (NULL, "Template", "1.0", 0, &error) ||
425 #ifdef HAVE_WEBKIT
426 !g_irepository_require (NULL, "WebKit2", "4.0", 0, &error) ||
427 #endif
428 !g_irepository_require (NULL, "Ide", PACKAGE_ABI_S, 0, &error))
429 g_critical ("Cannot enable Python 3 plugins: %s", error->message);
430 else
431 peas_engine_enable_loader (engine, "python3");
432
433 peas_engine_rescan_plugins (engine);
434
435 plugins = peas_engine_get_plugin_list (engine);
436
437 for (const GList *iter = plugins; iter; iter = iter->next)
438 {
439 PeasPluginInfo *plugin_info = iter->data;
440
441 if (!peas_plugin_info_is_loaded (plugin_info))
442 _ide_application_load_plugin (self, plugin_info);
443 }
444 }
445
446 static void
ide_application_addin_added_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)447 ide_application_addin_added_cb (PeasExtensionSet *set,
448 PeasPluginInfo *plugin_info,
449 PeasExtension *exten,
450 gpointer user_data)
451 {
452 IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
453 IdeApplication *self = user_data;
454
455 g_assert (IDE_IS_MAIN_THREAD ());
456 g_assert (PEAS_IS_EXTENSION_SET (set));
457 g_assert (plugin_info != NULL);
458 g_assert (IDE_IS_APPLICATION_ADDIN (addin));
459 g_assert (IDE_IS_APPLICATION (self));
460
461 ide_application_addin_load (addin, self);
462 }
463
464 static void
ide_application_addin_removed_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)465 ide_application_addin_removed_cb (PeasExtensionSet *set,
466 PeasPluginInfo *plugin_info,
467 PeasExtension *exten,
468 gpointer user_data)
469 {
470 IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
471 IdeApplication *self = user_data;
472
473 g_assert (IDE_IS_MAIN_THREAD ());
474 g_assert (PEAS_IS_EXTENSION_SET (set));
475 g_assert (plugin_info != NULL);
476 g_assert (IDE_IS_APPLICATION_ADDIN (addin));
477 g_assert (IDE_IS_APPLICATION (self));
478
479 ide_application_addin_unload (addin, self);
480 }
481
482 /**
483 * _ide_application_load_addins:
484 * @self: a #IdeApplication
485 *
486 * Loads the #IdeApplicationAddin's for this application.
487 *
488 * Since: 3.32
489 */
490 void
_ide_application_load_addins(IdeApplication * self)491 _ide_application_load_addins (IdeApplication *self)
492 {
493 g_assert (IDE_IS_MAIN_THREAD ());
494 g_assert (IDE_IS_APPLICATION (self));
495 g_assert (self->addins == NULL);
496
497 self->addins = peas_extension_set_new (peas_engine_get_default (),
498 IDE_TYPE_APPLICATION_ADDIN,
499 NULL);
500
501 g_signal_connect (self->addins,
502 "extension-added",
503 G_CALLBACK (ide_application_addin_added_cb),
504 self);
505
506 g_signal_connect (self->addins,
507 "extension-removed",
508 G_CALLBACK (ide_application_addin_removed_cb),
509 self);
510
511 peas_extension_set_foreach (self->addins,
512 ide_application_addin_added_cb,
513 self);
514 }
515
516 /**
517 * _ide_application_unload_addins:
518 * @self: a #IdeApplication
519 *
520 * Unloads all of the previously loaded #IdeApplicationAddin.
521 *
522 * Since: 3.32
523 */
524 void
_ide_application_unload_addins(IdeApplication * self)525 _ide_application_unload_addins (IdeApplication *self)
526 {
527 g_assert (IDE_IS_MAIN_THREAD ());
528 g_assert (IDE_IS_APPLICATION (self));
529 g_assert (self->addins != NULL);
530
531 g_clear_object (&self->addins);
532 }
533