Audacious  $Id:Doxyfile42802007-03-2104:39:00Znenolod$
plugin-registry.c
Go to the documentation of this file.
1 /*
2  * plugin-registry.c
3  * Copyright 2009-2011 John Lindgren
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice,
9  * this list of conditions, and the following disclaimer.
10  *
11  * 2. Redistributions in binary form must reproduce the above copyright notice,
12  * this list of conditions, and the following disclaimer in the documentation
13  * provided with the distribution.
14  *
15  * This software is provided "as is" and without any warranty, express or
16  * implied. In no event shall the authors be liable for any damages arising from
17  * the use of this software.
18  */
19 
20 /* While the registry is being built (during early startup) or destroyed (during
21  * late shutdown), the registry_locked flag will be set. Once this flag is
22  * cleared, the registry will not be modified and can be read by concurrent
23  * threads. The one change that can happen during this time is that a plugin is
24  * loaded; hence the mutex must be locked before checking that a plugin is
25  * loaded and while loading it. */
26 
27 #include <glib.h>
28 #include <pthread.h>
29 #include <stdio.h>
30 #include <string.h>
31 
32 #include <libaudcore/audstrings.h>
33 
34 #include "debug.h"
35 #include "i18n.h"
36 #include "interface.h"
37 #include "misc.h"
38 #include "plugin.h"
39 #include "plugins.h"
40 #include "util.h"
41 
42 #define FILENAME "plugin-registry"
43 #define FORMAT 8
44 
45 typedef struct {
46  GList * schemes;
48 
49 typedef struct {
50  GList * exts;
52 
53 typedef struct {
54  GList * keys[INPUT_KEYS];
55  bool_t has_images, has_subtunes, can_write_tuple, has_infowin;
57 
58 struct PluginHandle {
59  char * path;
62  Plugin * header;
63  char * name, * domain;
64  int priority;
66  GList * watches;
67  PluginMiscData misc;
68 
69  union {
73  } u;
74 };
75 
76 typedef struct {
78  void * data;
79 } PluginWatch;
80 
81 static const char * plugin_type_names[] = {
82  [PLUGIN_TYPE_TRANSPORT] = "transport",
83  [PLUGIN_TYPE_PLAYLIST] = "playlist",
84  [PLUGIN_TYPE_INPUT] = "input",
85  [PLUGIN_TYPE_EFFECT] = "effect",
86  [PLUGIN_TYPE_OUTPUT] = "output",
87  [PLUGIN_TYPE_VIS] = "vis",
88  [PLUGIN_TYPE_GENERAL] = "general",
89  [PLUGIN_TYPE_IFACE] = "iface"};
90 
91 static const char * input_key_names[] = {
92  [INPUT_KEY_SCHEME] = "scheme",
93  [INPUT_KEY_EXTENSION] = "ext",
94  [INPUT_KEY_MIME] = "mime"};
95 
96 static GList * plugin_list = NULL;
98 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
99 
100 static PluginHandle * plugin_new (char * path, bool_t confirmed, bool_t
101  loaded, int timestamp, int type, Plugin * header)
102 {
103  PluginHandle * plugin = g_malloc (sizeof (PluginHandle));
104 
105  plugin->path = path;
106  plugin->confirmed = confirmed;
107  plugin->loaded = loaded;
108  plugin->timestamp = timestamp;
109  plugin->type = type;
110  plugin->header = header;
111  plugin->name = NULL;
112  plugin->domain = NULL;
113  plugin->priority = 0;
114  plugin->has_about = FALSE;
115  plugin->has_configure = FALSE;
116  plugin->enabled = FALSE;
117  plugin->watches = NULL;
118 
119  memset (& plugin->misc, 0, sizeof (PluginMiscData));
120 
121  if (type == PLUGIN_TYPE_TRANSPORT)
122  {
123  plugin->enabled = TRUE;
124  plugin->u.t.schemes = NULL;
125  }
126  else if (type == PLUGIN_TYPE_PLAYLIST)
127  {
128  plugin->enabled = TRUE;
129  plugin->u.p.exts = NULL;
130  }
131  else if (type == PLUGIN_TYPE_INPUT)
132  {
133  plugin->enabled = TRUE;
134  memset (plugin->u.i.keys, 0, sizeof plugin->u.i.keys);
135  plugin->u.i.has_images = FALSE;
136  plugin->u.i.has_subtunes = FALSE;
137  plugin->u.i.can_write_tuple = FALSE;
138  plugin->u.i.has_infowin = FALSE;
139  }
140 
141  plugin_list = g_list_prepend (plugin_list, plugin);
142  return plugin;
143 }
144 
146 {
147  plugin_list = g_list_remove (plugin_list, plugin);
148 
149  g_list_foreach (plugin->watches, (GFunc) g_free, NULL);
150  g_list_free (plugin->watches);
151 
152  if (plugin->type == PLUGIN_TYPE_TRANSPORT)
153  {
154  g_list_foreach (plugin->u.t.schemes, (GFunc) g_free, NULL);
155  g_list_free (plugin->u.t.schemes);
156  }
157  else if (plugin->type == PLUGIN_TYPE_PLAYLIST)
158  {
159  g_list_foreach (plugin->u.p.exts, (GFunc) g_free, NULL);
160  g_list_free (plugin->u.p.exts);
161  }
162  else if (plugin->type == PLUGIN_TYPE_INPUT)
163  {
164  for (int key = 0; key < INPUT_KEYS; key ++)
165  {
166  g_list_foreach (plugin->u.i.keys[key], (GFunc) g_free, NULL);
167  g_list_free (plugin->u.i.keys[key]);
168  }
169  }
170 
171  g_free (plugin->path);
172  g_free (plugin->name);
173  g_free (plugin->domain);
174  g_free (plugin);
175 }
176 
177 static FILE * open_registry_file (const char * mode)
178 {
179  char * path = g_strdup_printf ("%s/" FILENAME, get_path (AUD_PATH_USER_DIR));
180  FILE * file = fopen (path, mode);
181  g_free (path);
182  return file;
183 }
184 
185 static void transport_plugin_save (PluginHandle * plugin, FILE * handle)
186 {
187  for (GList * node = plugin->u.t.schemes; node; node = node->next)
188  fprintf (handle, "scheme %s\n", (const char *) node->data);
189 }
190 
191 static void playlist_plugin_save (PluginHandle * plugin, FILE * handle)
192 {
193  for (GList * node = plugin->u.p.exts; node; node = node->next)
194  fprintf (handle, "ext %s\n", (const char *) node->data);
195 }
196 
197 static void input_plugin_save (PluginHandle * plugin, FILE * handle)
198 {
199  for (int key = 0; key < INPUT_KEYS; key ++)
200  {
201  for (GList * node = plugin->u.i.keys[key]; node; node = node->next)
202  fprintf (handle, "%s %s\n", input_key_names[key], (const char *)
203  node->data);
204  }
205 
206  fprintf (handle, "images %d\n", plugin->u.i.has_images);
207  fprintf (handle, "subtunes %d\n", plugin->u.i.has_subtunes);
208  fprintf (handle, "writes %d\n", plugin->u.i.can_write_tuple);
209  fprintf (handle, "infowin %d\n", plugin->u.i.has_infowin);
210 }
211 
212 static void plugin_save (PluginHandle * plugin, FILE * handle)
213 {
214  fprintf (handle, "%s %s\n", plugin_type_names[plugin->type], plugin->path);
215  fprintf (handle, "stamp %d\n", plugin->timestamp);
216  fprintf (handle, "name %s\n", plugin->name);
217 
218  if (plugin->domain)
219  fprintf (handle, "domain %s\n", plugin->domain);
220 
221  fprintf (handle, "priority %d\n", plugin->priority);
222  fprintf (handle, "about %d\n", plugin->has_about);
223  fprintf (handle, "config %d\n", plugin->has_configure);
224  fprintf (handle, "enabled %d\n", plugin->enabled);
225 
226  if (plugin->type == PLUGIN_TYPE_TRANSPORT)
227  transport_plugin_save (plugin, handle);
228  else if (plugin->type == PLUGIN_TYPE_PLAYLIST)
229  playlist_plugin_save (plugin, handle);
230  else if (plugin->type == PLUGIN_TYPE_INPUT)
231  input_plugin_save (plugin, handle);
232 }
233 
235 {
236  FILE * handle = open_registry_file ("w");
237  g_return_if_fail (handle);
238 
239  fprintf (handle, "format %d\n", FORMAT);
240 
241  g_list_foreach (plugin_list, (GFunc) plugin_save, handle);
242  fclose (handle);
243 
244  g_list_foreach (plugin_list, (GFunc) plugin_free, NULL);
246 }
247 
248 static char parse_key[512];
249 static char * parse_value;
250 
251 static void parse_next (FILE * handle)
252 {
253  parse_value = NULL;
254 
255  if (! fgets (parse_key, sizeof parse_key, handle))
256  return;
257 
258  char * space = strchr (parse_key, ' ');
259  if (! space)
260  return;
261 
262  * space = 0;
263  parse_value = space + 1;
264 
265  char * newline = strchr (parse_value, '\n');
266  if (newline)
267  * newline = 0;
268 }
269 
270 static bool_t parse_integer (const char * key, int * value)
271 {
272  return (parse_value && ! strcmp (parse_key, key) && sscanf (parse_value,
273  "%d", value) == 1);
274 }
275 
276 static char * parse_string (const char * key)
277 {
278  return (parse_value && ! strcmp (parse_key, key)) ? g_strdup (parse_value) :
279  NULL;
280 }
281 
282 static void transport_plugin_parse (PluginHandle * plugin, FILE * handle)
283 {
284  char * value;
285  while ((value = parse_string ("scheme")))
286  {
287  plugin->u.t.schemes = g_list_prepend (plugin->u.t.schemes, value);
288  parse_next (handle);
289  }
290 }
291 
292 static void playlist_plugin_parse (PluginHandle * plugin, FILE * handle)
293 {
294  char * value;
295  while ((value = parse_string ("ext")))
296  {
297  plugin->u.p.exts = g_list_prepend (plugin->u.p.exts, value);
298  parse_next (handle);
299  }
300 }
301 
302 static void input_plugin_parse (PluginHandle * plugin, FILE * handle)
303 {
304  for (int key = 0; key < INPUT_KEYS; key ++)
305  {
306  char * value;
307  while ((value = parse_string (input_key_names[key])))
308  {
309  plugin->u.i.keys[key] = g_list_prepend (plugin->u.i.keys[key],
310  value);
311  parse_next (handle);
312  }
313  }
314 
315  if (parse_integer ("images", & plugin->u.i.has_images))
316  parse_next (handle);
317  if (parse_integer ("subtunes", & plugin->u.i.has_subtunes))
318  parse_next (handle);
319  if (parse_integer ("writes", & plugin->u.i.can_write_tuple))
320  parse_next (handle);
321  if (parse_integer ("infowin", & plugin->u.i.has_infowin))
322  parse_next (handle);
323 }
324 
325 static bool_t plugin_parse (FILE * handle)
326 {
327  char * path = NULL;
328 
329  int type;
330  for (type = 0; type < PLUGIN_TYPES; type ++)
331  {
332  if ((path = parse_string (plugin_type_names[type])))
333  goto FOUND;
334  }
335 
336  return FALSE;
337 
338 FOUND:
339  parse_next (handle);
340 
341  int timestamp;
342  if (! parse_integer ("stamp", & timestamp))
343  {
344  g_free (path);
345  return FALSE;
346  }
347 
348  PluginHandle * plugin = plugin_new (path, FALSE, FALSE, timestamp, type,
349  NULL);
350  parse_next (handle);
351 
352  if ((plugin->name = parse_string ("name")))
353  parse_next (handle);
354  if ((plugin->domain = parse_string ("domain")))
355  parse_next (handle);
356  if (parse_integer ("priority", & plugin->priority))
357  parse_next (handle);
358  if (parse_integer ("about", & plugin->has_about))
359  parse_next (handle);
360  if (parse_integer ("config", & plugin->has_configure))
361  parse_next (handle);
362  if (parse_integer ("enabled", & plugin->enabled))
363  parse_next (handle);
364 
365  if (type == PLUGIN_TYPE_TRANSPORT)
366  transport_plugin_parse (plugin, handle);
367  else if (type == PLUGIN_TYPE_PLAYLIST)
368  playlist_plugin_parse (plugin, handle);
369  else if (type == PLUGIN_TYPE_INPUT)
370  input_plugin_parse (plugin, handle);
371 
372  return TRUE;
373 }
374 
376 {
377  FILE * handle = open_registry_file ("r");
378  if (! handle)
379  goto UNLOCK;
380 
381  parse_next (handle);
382 
383  int format;
384  if (! parse_integer ("format", & format) || format != FORMAT)
385  goto ERR;
386 
387  parse_next (handle);
388 
389  while (plugin_parse (handle))
390  ;
391 
392 ERR:
393  fclose (handle);
394 UNLOCK:
396 }
397 
399 {
400  if (plugin->confirmed)
401  return;
402 
403  AUDDBG ("Plugin not found: %s\n", plugin->path);
404  plugin_free (plugin);
405 }
406 
408 {
409  if (a->type < b->type)
410  return -1;
411  if (a->type > b->type)
412  return 1;
413  if (a->priority < b->priority)
414  return -1;
415  if (a->priority > b->priority)
416  return 1;
417 
418  int diff;
419  if ((diff = string_compare (dgettext (a->domain, a->name), dgettext (b->domain, b->name))))
420  return diff;
421 
422  return string_compare (a->path, b->path);
423 }
424 
426 {
427  g_list_foreach (plugin_list, (GFunc) plugin_prune, NULL);
428  plugin_list = g_list_sort (plugin_list, (GCompareFunc) plugin_compare);
430 }
431 
432 static int plugin_lookup_cb (PluginHandle * plugin, const char * path)
433 {
434  return strcmp (plugin->path, path);
435 }
436 
437 PluginHandle * plugin_lookup (const char * path)
438 {
439  GList * node = g_list_find_custom (plugin_list, path, (GCompareFunc)
441  return node ? node->data : NULL;
442 }
443 
444 static int plugin_lookup_basename_cb (PluginHandle * plugin, const char * basename)
445 {
446  char * test = g_path_get_basename (plugin->path);
447 
448  char * dot = strrchr (test, '.');
449  if (dot)
450  * dot = 0;
451 
452  int ret = strcmp (test, basename);
453 
454  g_free (test);
455  return ret;
456 }
457 
458 /* Note: If there are multiple plugins with the same basename, this returns only
459  * one of them. So give different plugins different basenames. --jlindgren */
460 PluginHandle * plugin_lookup_basename (const char * basename)
461 {
462  GList * node = g_list_find_custom (plugin_list, basename, (GCompareFunc)
464  return node ? node->data : NULL;
465 }
466 
467 void plugin_register (const char * path)
468 {
469  PluginHandle * plugin = plugin_lookup (path);
470  if (! plugin)
471  {
472  AUDDBG ("New plugin: %s\n", path);
473  plugin_load (path);
474  return;
475  }
476 
477  int timestamp = file_get_mtime (path);
478  g_return_if_fail (timestamp >= 0);
479 
480  AUDDBG ("Register plugin: %s\n", path);
481  plugin->confirmed = TRUE;
482 
483  if (plugin->timestamp == timestamp)
484  return;
485 
486  AUDDBG ("Rescan plugin: %s\n", path);
487  plugin->timestamp = timestamp;
488  plugin_load (path);
489 }
490 
491 void plugin_register_loaded (const char * path, Plugin * header)
492 {
493  AUDDBG ("Loaded plugin: %s\n", path);
494  PluginHandle * plugin = plugin_lookup (path);
495  bool_t new = FALSE;
496 
497  if (plugin)
498  {
499  g_return_if_fail (plugin->type == header->type);
500 
501  plugin->loaded = TRUE;
502  plugin->header = header;
503 
504  if (registry_locked)
505  return;
506  }
507  else
508  {
509  g_return_if_fail (! registry_locked);
510 
511  int timestamp = file_get_mtime (path);
512  g_return_if_fail (timestamp >= 0);
513 
514  plugin = plugin_new (g_strdup (path), TRUE, TRUE, timestamp,
515  header->type, header);
516  new = TRUE;
517  }
518 
519  g_free (plugin->name);
520  g_free (plugin->domain);
521  plugin->name = g_strdup (header->name);
522  plugin->domain = PLUGIN_HAS_FUNC (header, domain) ? g_strdup (header->domain) : NULL;
523  plugin->has_about = PLUGIN_HAS_FUNC (header, about) || PLUGIN_HAS_FUNC (header, about_text);
524  plugin->has_configure = PLUGIN_HAS_FUNC (header, configure) || PLUGIN_HAS_FUNC (header, prefs);
525 
526  if (header->type == PLUGIN_TYPE_TRANSPORT)
527  {
528  TransportPlugin * tp = (TransportPlugin *) header;
529  for (int i = 0; tp->schemes[i]; i ++)
530  plugin->u.t.schemes = g_list_prepend (plugin->u.t.schemes, g_strdup
531  (tp->schemes[i]));
532  }
533  else if (header->type == PLUGIN_TYPE_PLAYLIST)
534  {
535  PlaylistPlugin * pp = (PlaylistPlugin *) header;
536  for (int i = 0; pp->extensions[i]; i ++)
537  plugin->u.p.exts = g_list_prepend (plugin->u.p.exts, g_strdup
538  (pp->extensions[i]));
539  }
540  else if (header->type == PLUGIN_TYPE_INPUT)
541  {
542  InputPlugin * ip = (InputPlugin *) header;
543  plugin->priority = ip->priority;
544 
545  for (int key = 0; key < INPUT_KEYS; key ++)
546  {
547  g_list_foreach (plugin->u.i.keys[key], (GFunc) g_free, NULL);
548  g_list_free (plugin->u.i.keys[key]);
549  plugin->u.i.keys[key] = NULL;
550  }
551 
552  if (PLUGIN_HAS_FUNC (ip, extensions))
553  {
554  for (int i = 0; ip->extensions[i]; i ++)
555  plugin->u.i.keys[INPUT_KEY_EXTENSION] = g_list_prepend
556  (plugin->u.i.keys[INPUT_KEY_EXTENSION], g_strdup
557  (ip->extensions[i]));
558  }
559 
560  if (PLUGIN_HAS_FUNC (ip, mimes))
561  {
562  for (int i = 0; ip->mimes[i]; i ++)
563  plugin->u.i.keys[INPUT_KEY_MIME] = g_list_prepend
564  (plugin->u.i.keys[INPUT_KEY_MIME], g_strdup (ip->mimes[i]));
565  }
566 
567  if (PLUGIN_HAS_FUNC (ip, schemes))
568  {
569  for (int i = 0; ip->schemes[i]; i ++)
570  plugin->u.i.keys[INPUT_KEY_SCHEME] = g_list_prepend
571  (plugin->u.i.keys[INPUT_KEY_SCHEME], g_strdup (ip->schemes[i]));
572  }
573 
574  plugin->u.i.has_images = PLUGIN_HAS_FUNC (ip, get_song_image);
575  plugin->u.i.has_subtunes = ip->have_subtune;
576  plugin->u.i.can_write_tuple = PLUGIN_HAS_FUNC (ip, update_song_tuple);
577  plugin->u.i.has_infowin = PLUGIN_HAS_FUNC (ip, file_info_box);
578  }
579  else if (header->type == PLUGIN_TYPE_OUTPUT)
580  {
581  OutputPlugin * op = (OutputPlugin *) header;
582  plugin->priority = 10 - op->probe_priority;
583  }
584  else if (header->type == PLUGIN_TYPE_EFFECT)
585  {
586  EffectPlugin * ep = (EffectPlugin *) header;
587  plugin->priority = ep->order;
588  }
589  else if (header->type == PLUGIN_TYPE_GENERAL)
590  {
591  GeneralPlugin * gp = (GeneralPlugin *) header;
592  if (new)
593  plugin->enabled = gp->enabled_by_default;
594  }
595 }
596 
598 {
599  return plugin->type;
600 }
601 
603 {
604  return plugin->path;
605 }
606 
608 {
609  pthread_mutex_lock (& mutex);
610 
611  if (! plugin->loaded)
612  {
613  plugin_load (plugin->path);
614  plugin->loaded = TRUE;
615  }
616 
617  pthread_mutex_unlock (& mutex);
618  return plugin->header;
619 }
620 
622 {
623  return plugin->header;
624 }
625 
626 static int plugin_by_header_cb (PluginHandle * plugin, const void * header)
627 {
628  return (plugin->header == header) ? 0 : -1;
629 }
630 
631 PluginHandle * plugin_by_header (const void * header)
632 {
633  GList * node = g_list_find_custom (plugin_list, header, (GCompareFunc)
635  return node ? node->data : NULL;
636 }
637 
638 void plugin_for_each (int type, PluginForEachFunc func, void * data)
639 {
640  for (GList * node = plugin_list; node; node = node->next)
641  {
642  if (((PluginHandle *) node->data)->type != type)
643  continue;
644  if (! func (node->data, data))
645  break;
646  }
647 }
648 
650 {
651  return dgettext (plugin->domain, plugin->name);
652 }
653 
655 {
656  return plugin->has_about;
657 }
658 
660 {
661  return plugin->has_configure;
662 }
663 
665 {
666  return plugin->enabled;
667 }
668 
670 {
671  for (GList * node = plugin->watches; node; )
672  {
673  GList * next = node->next;
674  PluginWatch * watch = node->data;
675 
676  if (! watch->func (plugin, watch->data))
677  {
678  g_free (watch);
679  plugin->watches = g_list_delete_link (plugin->watches, node);
680  }
681 
682  node = next;
683  }
684 }
685 
687 {
688  plugin->enabled = enabled;
689  plugin_call_watches (plugin);
690 }
691 
692 typedef struct {
694  void * data;
696 
698  PluginForEnabledState * state)
699 {
700  if (! plugin->enabled)
701  return TRUE;
702  return state->func (plugin, state->data);
703 }
704 
706 {
707  PluginForEnabledState state = {func, data};
709 }
710 
712  data)
713 {
714  PluginWatch * watch = g_malloc (sizeof (PluginWatch));
715  watch->func = func;
716  watch->data = data;
717  plugin->watches = g_list_prepend (plugin->watches, watch);
718 }
719 
721  data)
722 {
723  for (GList * node = plugin->watches; node; )
724  {
725  GList * next = node->next;
726  PluginWatch * watch = node->data;
727 
728  if (watch->func == func && watch->data == data)
729  {
730  g_free (watch);
731  plugin->watches = g_list_delete_link (plugin->watches, node);
732  }
733 
734  node = next;
735  }
736 }
737 
739 {
740  return & plugin->misc;
741 }
742 
743 typedef struct {
744  const char * scheme;
747 
750 {
751  if (! g_list_find_custom (plugin->u.t.schemes, state->scheme,
752  (GCompareFunc) g_ascii_strcasecmp))
753  return TRUE;
754 
755  state->plugin = plugin;
756  return FALSE;
757 }
758 
760 {
761  TransportPluginForSchemeState state = {scheme, NULL};
764  return state.plugin;
765 }
766 
767 typedef struct {
768  const char * ext;
771 
774 {
775  if (! g_list_find_custom (plugin->u.p.exts, state->ext,
776  (GCompareFunc) g_ascii_strcasecmp))
777  return TRUE;
778 
779  state->plugin = plugin;
780  return FALSE;
781 }
782 
783 PluginHandle * playlist_plugin_for_extension (const char * extension)
784 {
785  PlaylistPluginForExtState state = {extension, NULL};
787  playlist_plugin_for_ext_cb, & state);
788  return state.plugin;
789 }
790 
791 typedef struct {
792  int key;
793  const char * value;
795  void * data;
797 
799  InputPluginForKeyState * state)
800 {
801  if (! g_list_find_custom (plugin->u.i.keys[state->key], state->value,
802  (GCompareFunc) g_ascii_strcasecmp))
803  return TRUE;
804 
805  return state->func (plugin, state->data);
806 }
807 
808 void input_plugin_for_key (int key, const char * value, PluginForEachFunc
809  func, void * data)
810 {
811  InputPluginForKeyState state = {key, value, func, data};
813  input_plugin_for_key_cb, & state);
814 }
815 
817 {
818  g_return_val_if_fail (plugin->type == PLUGIN_TYPE_INPUT, FALSE);
819  return plugin->u.i.has_images;
820 }
821 
823 {
824  g_return_val_if_fail (plugin->type == PLUGIN_TYPE_INPUT, FALSE);
825  return plugin->u.i.has_subtunes;
826 }
827 
829 {
830  g_return_val_if_fail (plugin->type == PLUGIN_TYPE_INPUT, FALSE);
831  return plugin->u.i.can_write_tuple;
832 }
833 
835 {
836  g_return_val_if_fail (plugin->type == PLUGIN_TYPE_INPUT, FALSE);
837  return plugin->u.i.has_infowin;
838 }