1 /*
2  * plugin.c
3  *
4  * Copyright 2011 Matthew Brush <mbrush@codebrainz.ca>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #if defined(HAVE_CONFIG_H) && !defined(GEANYPY_WINDOWS)
23 # include "config.h"
24 #endif
25 
26 #define INCLUDE_PYGOBJECT_ONCE_FULL
27 
28 #include "geanypy.h"
29 #include "geanypy-keybindings.h"
30 
31 #include <glib.h>
32 #include <glib/gstdio.h>
33 
34 GeanyData *geany_data;
35 
36 /* Forward declarations to prevent compiler warnings. */
37 PyMODINIT_FUNC initapp(void);
38 PyMODINIT_FUNC initdialogs(void);
39 PyMODINIT_FUNC initdocument(void);
40 PyMODINIT_FUNC initeditor(void);
41 PyMODINIT_FUNC initencoding(void);
42 PyMODINIT_FUNC initfiletypes(void);
43 PyMODINIT_FUNC initglog(void);
44 PyMODINIT_FUNC inithighlighting(void);
45 PyMODINIT_FUNC initmain(void);
46 PyMODINIT_FUNC initmsgwin(void);
47 PyMODINIT_FUNC initnavqueue(void);
48 PyMODINIT_FUNC initprefs(void);
49 PyMODINIT_FUNC initproject(void);
50 PyMODINIT_FUNC initscintilla(void);
51 PyMODINIT_FUNC initsearch(void);
52 PyMODINIT_FUNC inittemplates(void);
53 PyMODINIT_FUNC initui_utils(void);
54 PyMODINIT_FUNC initkeybindings(void);
55 
56 
57 static void
GeanyPy_start_interpreter(void)58 GeanyPy_start_interpreter(void)
59 {
60     gchar *init_code;
61     gchar *py_dir = NULL;
62 
63 
64 #ifndef GEANYPY_WINDOWS
65 	{ /* Prevents a crash in the dynload thingy
66 		 TODO: is this or the old dlopen version even needed? */
67 		GModule *mod = g_module_open(GEANYPY_PYTHON_LIBRARY, G_MODULE_BIND_LAZY);
68 		if (!mod) {
69 			g_warning(_("Unable to pre-load Python library: %s."), g_module_error());
70 			return;
71 		}
72 		g_module_close(mod);
73 	}
74 #endif
75 
76     Py_Initialize();
77 
78     /* Import the C modules */
79     initapp();
80     initdialogs();
81     initdocument();
82     initeditor();
83     initencoding();
84     initfiletypes();
85     initglog();
86     inithighlighting();
87     initmain();
88     initmsgwin();
89     initnavqueue();
90     initprefs();
91     initproject();
92     initscintilla();
93     initsearch();
94     inittemplates();
95     initui_utils();
96     initkeybindings();
97 
98 #ifdef GEANYPY_WINDOWS
99 	{ /* On windows, get path at runtime since we don't really know where
100 	   * Geany is installed ahead of time. */
101 		gchar *geany_base_dir;
102 		geany_base_dir = g_win32_get_package_installation_directory_of_module(NULL);
103 		if (geany_base_dir)
104 		{
105 			py_dir = g_build_filename(geany_base_dir, "lib", "geanypy", NULL);
106 			g_free(geany_base_dir);
107 		}
108 		if (!g_file_test(py_dir, G_FILE_TEST_EXISTS))
109 		{
110 			g_critical("The path to the `geany' module was not found: %s", py_dir);
111 			g_free(py_dir);
112 			py_dir = g_strdup(""); /* will put current dir on path? */
113 		}
114 	}
115 #else
116 	py_dir = g_strdup(GEANYPY_PYTHON_DIR);
117 #endif
118 
119     /* Adjust Python path to find wrapper package (geany) */
120     init_code = g_strdup_printf(
121         "import os, sys\n"
122         "path = '%s'.replace('~', os.path.expanduser('~'))\n"
123         "sys.path.append(path)\n"
124         "path = '%s/plugins'.replace('~', os.path.expanduser('~'))\n"
125         "sys.path.append(path)\n"
126         "path = '%s'.replace('~', os.path.expanduser('~'))\n"
127         "sys.path.append(path)\n"
128         "import geany\n", py_dir, geany_data->app->configdir, GEANYPY_PLUGIN_DIR);
129     g_free(py_dir);
130 
131     PyRun_SimpleString(init_code);
132     g_free(init_code);
133 
134 }
135 
136 static void
GeanyPy_stop_interpreter(void)137 GeanyPy_stop_interpreter(void)
138 {
139     if (Py_IsInitialized())
140         Py_Finalize();
141 }
142 
143 typedef struct
144 {
145 	PyObject *base;
146 	SignalManager *signal_manager;
147 }
148 GeanyPyData;
149 
150 typedef struct
151 {
152 	PyObject *class;
153 	PyObject *module;
154 	PyObject *instance;
155 }
156 GeanyPyPluginData;
157 
has_error(void)158 static gboolean has_error(void)
159 {
160 	if (PyErr_Occurred())
161 	{
162 		PyErr_Print();
163 		return TRUE;
164 	}
165 	return FALSE;
166 }
167 
geanypy_proxy_init(GeanyPlugin * plugin,gpointer pdata)168 static gboolean geanypy_proxy_init(GeanyPlugin *plugin, gpointer pdata)
169 {
170 	GeanyPyPluginData *data = (GeanyPyPluginData *) pdata;
171 
172 	data->instance = PyObject_CallObject(data->class, NULL);
173 	if (has_error())
174 		return FALSE;
175 
176 	return TRUE;
177 }
178 
179 
geanypy_proxy_cleanup(GeanyPlugin * plugin,gpointer pdata)180 static void geanypy_proxy_cleanup(GeanyPlugin *plugin, gpointer pdata)
181 {
182 	GeanyPyPluginData *data = (GeanyPyPluginData *) pdata;
183 
184 	PyObject_CallMethod(data->instance, "cleanup", NULL);
185 	if (has_error())
186 		return;
187 }
188 
189 
geanypy_proxy_configure(GeanyPlugin * plugin,GtkDialog * parent,gpointer pdata)190 static GtkWidget *geanypy_proxy_configure(GeanyPlugin *plugin, GtkDialog *parent, gpointer pdata)
191 {
192 	GeanyPyPluginData *data = (GeanyPyPluginData *) pdata;
193 	PyObject *o, *oparent;
194 	GObject *widget;
195 
196 	oparent = pygobject_new(G_OBJECT(parent));
197 	o = PyObject_CallMethod(data->instance, "configure", "O", oparent, NULL);
198 	Py_DECREF(oparent);
199 
200 	if (!has_error() && o != Py_None)
201 	{
202 		/* Geany wants only the underlying GtkWidget, we must only ref that
203 		 * and free the pygobject wrapper */
204 		widget = g_object_ref(pygobject_get(o));
205 		Py_DECREF(o);
206 		return GTK_WIDGET(widget);
207 	}
208 
209 	Py_DECREF(o); /* Must unref even if it's Py_None */
210 	return NULL;
211 }
212 
213 
do_show_configure(GtkWidget * button,gpointer pdata)214 static void do_show_configure(GtkWidget *button, gpointer pdata)
215 {
216 	GeanyPyPluginData *data = (GeanyPyPluginData *) pdata;
217 	PyObject_CallMethod(data->instance, "show_configure", NULL);
218 }
219 
220 
geanypy_proxy_configure_legacy(GeanyPlugin * plugin,GtkDialog * parent,gpointer pdata)221 static GtkWidget *geanypy_proxy_configure_legacy(GeanyPlugin *plugin, GtkDialog *parent, gpointer pdata)
222 {
223 	GeanyPyPluginData *data = (GeanyPyPluginData *) pdata;
224 	PyObject *o, *oparent;
225 	GtkWidget *box, *label, *button, *align;
226 	gchar *text;
227 
228 	/* This creates a simple page that has only one button to show the plugin's legacy configure
229 	 * dialog. It is for older plugins that implement show_configure(). It's not pretty but
230 	 * it provides basic backwards compatibility. */
231 	box = gtk_vbox_new(FALSE, 2);
232 
233 	text = g_strdup_printf("The plugin \"%s\" is older and hasn't been updated\nto provide a configuration UI. However, it provides a dialog to\nallow you to change the plugin's preferences.", plugin->info->name);
234 	label = gtk_label_new(text);
235 
236 	align = gtk_alignment_new(0, 0, 1, 1);
237 	gtk_container_add(GTK_CONTAINER(align), label);
238 	gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 6, 2, 2);
239 	gtk_box_pack_start(GTK_BOX(box), align, FALSE, FALSE, 0);
240 
241 	button = gtk_button_new_with_label("Open dialog");
242 	align = gtk_alignment_new(0.5, 0, 0.3f, 1);
243 	gtk_container_add(GTK_CONTAINER(align), button);
244 	g_signal_connect(button, "clicked", (GCallback) do_show_configure, pdata);
245 	gtk_box_pack_start(GTK_BOX(box), align, FALSE, TRUE, 0);
246 
247 	gtk_widget_show_all(box);
248 	g_free(text);
249 	return box;
250 }
251 
geanypy_proxy_help(GeanyPlugin * plugin,gpointer pdata)252 static void geanypy_proxy_help(GeanyPlugin *plugin, gpointer pdata)
253 {
254 	GeanyPyPluginData *data = (GeanyPyPluginData *) pdata;
255 
256 	PyObject_CallMethod(data->instance, "help", NULL);
257 	if (has_error())
258 		return;
259 }
260 
261 static gint
geanypy_probe(GeanyPlugin * proxy,const gchar * filename,gpointer pdata)262 geanypy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata)
263 {
264 	gchar *file_plugin = g_strdup_printf("%.*s.plugin",
265 			(int)(strrchr(filename, '.') - filename), filename);
266 	gint ret = PROXY_IGNORED;
267 
268 	/* avoid clash with libpeas py plugins, those come with a corresponding <plugin>.plugin file */
269 	if (!g_file_test(file_plugin, G_FILE_TEST_EXISTS))
270 		ret = PROXY_MATCHED;
271 
272 	g_free(file_plugin);
273 	return ret;
274 }
275 
276 
string_from_attr(PyObject * o,const gchar * attr)277 static const gchar *string_from_attr(PyObject *o, const gchar *attr)
278 {
279 	PyObject *string = PyObject_GetAttrString(o, attr);
280 	const gchar *ret = PyString_AsString(string);
281 	Py_DECREF(string);
282 
283 	return ret;
284 }
285 
286 
287 static gpointer
geanypy_load(GeanyPlugin * proxy,GeanyPlugin * subplugin,const gchar * filename,gpointer pdata)288 geanypy_load(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata)
289 {
290 	GeanyPyData *data = pdata;
291 	PyObject *fromlist, *module, *dict, *key, *val, *found = NULL;
292 	Py_ssize_t pos = 0;
293 	gchar *modulename, *dot;
294 	gpointer ret = NULL;
295 
296 	modulename = g_path_get_basename(filename);
297 	/* We are guaranteed that filename has a .py extension
298 	 * because we did geany_plugin_register_proxy() for it */
299 	dot = strrchr(modulename, '.');
300 	*dot = '\0';
301 	/* we need a fromlist to be able to import modules with a '.' in the
302 	 * name. -- libpeas */
303 	fromlist = PyTuple_New (0);
304 
305 	module = PyImport_ImportModuleEx(modulename, NULL, NULL, fromlist);
306 	if (has_error() || !module)
307 		goto err;
308 
309 	dict = PyModule_GetDict(module);
310 
311 	while (PyDict_Next (dict, &pos, &key, &val) && found == NULL)
312 	{
313 		if (PyType_Check(val) && PyObject_IsSubclass(val, data->base))
314 			found = val;
315 	}
316 
317 	if (found)
318 	{
319 		GeanyPyPluginData *pdata = g_slice_new(GeanyPyPluginData);
320 		PluginInfo *info     = subplugin->info;
321 		GeanyPluginFuncs *funcs = subplugin->funcs;
322 		PyObject *caps = PyCapsule_New(subplugin, "GeanyPlugin", NULL);
323 		Py_INCREF(found);
324 		pdata->module        = module;
325 		pdata->class         = found;
326 		PyObject_SetAttrString(pdata->class, "__geany_plugin__", caps);
327 		pdata->instance      = NULL;
328 		info->name           = string_from_attr(pdata->class, "__plugin_name__");
329 		info->description    = string_from_attr(pdata->class, "__plugin_description__");
330 		info->version        = string_from_attr(pdata->class, "__plugin_version__");
331 		info->author         = string_from_attr(pdata->class, "__plugin_author__");
332 		funcs->init          = geanypy_proxy_init;
333 		funcs->cleanup       = geanypy_proxy_cleanup;
334 		if (PyObject_HasAttrString(found, "configure"))
335 			funcs->configure = geanypy_proxy_configure;
336 		else if (PyObject_HasAttrString(found, "show_configure"))
337 			funcs->configure = geanypy_proxy_configure_legacy;
338 		if (PyObject_HasAttrString(found, "help"))
339 			funcs->help      = geanypy_proxy_help;
340 		if (GEANY_PLUGIN_REGISTER_FULL(subplugin, 224, pdata, NULL))
341 			ret              = pdata;
342 	}
343 
344 err:
345 	g_free(modulename);
346 	Py_DECREF(fromlist);
347 	return ret;
348 }
349 
350 
351 static void
geanypy_unload(GeanyPlugin * plugin,GeanyPlugin * subplugin,gpointer load_data,gpointer pdata_)352 geanypy_unload(GeanyPlugin *plugin, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata_)
353 {
354 	GeanyPyPluginData *pdata = load_data;
355 
356 	Py_XDECREF(pdata->instance);
357 	Py_DECREF(pdata->class);
358 	Py_DECREF(pdata->module);
359 	while (PyGC_Collect());
360 	g_slice_free(GeanyPyPluginData, pdata);
361 }
362 
363 
geanypy_init(GeanyPlugin * plugin_,gpointer pdata)364 static gboolean geanypy_init(GeanyPlugin *plugin_, gpointer pdata)
365 {
366 	const gchar *exts[] = { "py", NULL };
367 	GeanyPyData *state = pdata;
368 	PyObject *module;
369 
370 	plugin_->proxy_funcs->probe   = geanypy_probe;
371 	plugin_->proxy_funcs->load    = geanypy_load;
372 	plugin_->proxy_funcs->unload  = geanypy_unload;
373 
374 	geany_data = plugin_->geany_data;
375 
376 	GeanyPy_start_interpreter();
377 	state->signal_manager = signal_manager_new(plugin_);
378 
379 	module = PyImport_ImportModule("geany.plugin");
380 	if (has_error() || !module)
381 		goto err;
382 
383 	state->base = PyObject_GetAttrString(module, "Plugin");
384 	Py_DECREF(module);
385 	if (has_error() || !state->base)
386 		goto err;
387 
388 	if (!geany_plugin_register_proxy(plugin_, exts)) {
389 		Py_DECREF(state->base);
390 		goto err;
391 	}
392 
393 	return TRUE;
394 
395 err:
396 	signal_manager_free(state->signal_manager);
397 	GeanyPy_stop_interpreter();
398 	return FALSE;
399 }
400 
401 
geanypy_cleanup(GeanyPlugin * plugin,gpointer pdata)402 static void geanypy_cleanup(GeanyPlugin *plugin, gpointer pdata)
403 {
404 	GeanyPyData *state = pdata;
405 	signal_manager_free(state->signal_manager);
406 	Py_DECREF(state->base);
407 	GeanyPy_stop_interpreter();
408 }
409 
410 G_MODULE_EXPORT void
geany_load_module(GeanyPlugin * plugin)411 geany_load_module(GeanyPlugin *plugin)
412 {
413 	GeanyPyData *state = g_new0(GeanyPyData, 1);
414 
415 	plugin->info->name        = _("GeanyPy");
416 	plugin->info->description = _("Python plugins support");
417 	plugin->info->version     = "1.0";
418 	plugin->info->author      = "Matthew Brush <mbrush@codebrainz.ca>";
419 	plugin->funcs->init       = geanypy_init;
420 	plugin->funcs->cleanup    = geanypy_cleanup;
421 
422 	GEANY_PLUGIN_REGISTER_FULL(plugin, 226, state, g_free);
423 }
424