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