3
using System.Collections.Generic;
4
using System.Diagnostics;
5
using System.Reflection;
9
using Mono.Unix.Native;
13
using PluginTable = IDictionary<Type, NotePlugin>;
15
class PluginReference : WeakReference
17
readonly string description;
19
public PluginReference (object plugin, string description) :
22
this.description = description;
25
public string Description
27
get { return description; }
32
AttributeTargets.Class,
33
AllowMultiple = false, Inherited = false)]
34
public class PluginInfoAttribute : Attribute
42
Type preferencesWidget;
44
public const string OFFICIAL_AUTHOR = "official";
46
// The default constructor is, for some reason, needed or Tomboy will
47
// crash when attempting to read the plugin attributes.
48
public PluginInfoAttribute ()
50
// Intentionally blank
53
public PluginInfoAttribute (string name, string version,
54
string author, string description)
56
this.name = Catalog.GetString (name);
57
this.description = Catalog.GetString (description);
58
this.version = version;
59
if (author == OFFICIAL_AUTHOR)
60
this.author = Catalog.GetString ("Tomboy Project");
73
get { return version; }
74
set { version = value; }
77
public string Description
79
get { return description; }
80
set { description = value; }
85
get { return website; }
86
set { website = value; }
91
get { return author; }
92
set { author = value; }
95
public Type PreferencesWidget
97
get { return preferencesWidget; }
98
set { preferencesWidget = value; }
103
AttributeTargets.Class,
104
AllowMultiple = false, Inherited = true)]
105
public class RequiredPlugins: Attribute
107
readonly string[] pluginNames;
109
public RequiredPlugins(params string[] pluginNames)
111
this.pluginNames = pluginNames;
114
public string[] PluginNames
116
get { return pluginNames; }
121
AttributeTargets.Class,
122
AllowMultiple = false, Inherited = true)]
123
public class SuggestedPlugins: Attribute
125
readonly string[] pluginNames;
127
public SuggestedPlugins(params string[] pluginNames)
129
this.pluginNames = pluginNames;
132
public string[] PluginNames
134
get { return pluginNames; }
138
public interface IPlugin : IDisposable
142
public abstract class AbstractPlugin : IPlugin
144
bool disposing = false;
151
public void Dispose ()
156
GC.SuppressFinalize (this);
159
protected virtual void Dispose (bool disposing)
163
public bool IsDisposing
165
get { return disposing; }
169
public abstract class NotePlugin : AbstractPlugin
173
List<Gtk.MenuItem> plugin_menu_items;
174
List<Gtk.MenuItem> text_menu_items;
176
public void Initialize (Note note)
179
this.note.Opened += OnNoteOpenedEvent;
187
protected override void Dispose (bool disposing)
190
if (plugin_menu_items != null) {
191
foreach (Gtk.Widget item in plugin_menu_items)
195
if (text_menu_items != null) {
196
foreach (Gtk.Widget item in text_menu_items)
203
note.Opened -= OnNoteOpenedEvent;
206
protected abstract void Initialize ();
207
protected abstract void Shutdown ();
208
protected abstract void OnNoteOpened ();
215
public bool HasBuffer
217
get { return note.HasBuffer; }
220
public NoteBuffer Buffer
224
if (IsDisposing && !HasBuffer)
225
throw new InvalidOperationException ("Plugin is disposing already");
231
public bool HasWindow
233
get { return note.HasWindow; }
236
public NoteWindow Window
240
if (IsDisposing && !HasWindow)
241
throw new InvalidOperationException ("Plugin is disposing already");
247
public NoteManager Manager
249
get { return note.Manager; }
252
void OnNoteOpenedEvent (object sender, EventArgs args)
256
if (plugin_menu_items != null) {
257
foreach (Gtk.Widget item in plugin_menu_items) {
258
if (item.Parent == null ||
259
item.Parent != Window.PluginMenu)
260
Window.PluginMenu.Add (item);
264
if (text_menu_items != null) {
265
foreach (Gtk.Widget item in text_menu_items) {
266
if (item.Parent == null ||
267
item.Parent != Window.TextMenu) {
268
Window.TextMenu.Add (item);
269
Window.TextMenu.ReorderChild (item, 7);
275
public void AddPluginMenuItem (Gtk.MenuItem item)
278
throw new InvalidOperationException ("Plugin is disposing already");
280
if (plugin_menu_items == null)
281
plugin_menu_items = new List<Gtk.MenuItem> ();
283
plugin_menu_items.Add (item);
286
Window.PluginMenu.Add (item);
289
public void AddTextMenuItem (Gtk.MenuItem item)
292
throw new InvalidOperationException ("Plugin is disposing already");
294
if (text_menu_items == null)
295
text_menu_items = new List<Gtk.MenuItem> ();
297
text_menu_items.Add (item);
300
Window.TextMenu.Add (item);
301
Window.TextMenu.ReorderChild (item, 7);
306
public class PluginManager
308
readonly string plugins_dir;
310
readonly IList<Type> plugin_types;
311
readonly IDictionary<Note, PluginTable> attached_plugins;
313
readonly FileSystemWatcher dir_watcher;
314
readonly FileSystemWatcher sys_dir_watcher;
316
static bool check_plugin_unloading;
318
// Plugins in the tomboy.exe assembly, always loaded.
319
static Type[] stock_plugins = {
320
typeof (NoteRenameWatcher),
322
typeof (NoteSpellChecker),
324
typeof (NoteUrlWatcher),
325
typeof (NoteLinkWatcher),
326
typeof (NoteWikiWatcher),
327
typeof (MouseHandWatcher),
328
typeof (NoteTagsWatcher),
331
// typeof (NoteRelatedToWatcher),
332
// typeof (IndentWatcher),
335
public PluginManager (string plugins_dir)
337
this.plugins_dir = plugins_dir;
340
dir_watcher = new FileSystemWatcher (plugins_dir, "*.dll");
341
dir_watcher.Created += OnPluginCreated;
342
dir_watcher.Deleted += OnPluginDeleted;
343
dir_watcher.EnableRaisingEvents = true;
344
} catch (ArgumentException e) {
345
Logger.Log ("Error creating a FileSystemWatcher on \"{0}\": {1}",
346
plugins_dir, e.Message);
352
new FileSystemWatcher (Defines.SYS_PLUGINS_DIR, "*.dll");
353
sys_dir_watcher.Created += OnPluginCreated;
354
sys_dir_watcher.Deleted += OnPluginDeleted;
355
sys_dir_watcher.EnableRaisingEvents = true;
356
} catch (ArgumentException e) {
357
Logger.Log ("Error creating a FileSystemWatcher on \"{0}\": {1}",
358
Defines.SYS_PLUGINS_DIR, e.Message);
359
sys_dir_watcher = null;
362
plugin_types = FindPluginTypes ();
363
attached_plugins = new Dictionary<Note, PluginTable> ();
367
public static bool CheckPluginUnloading
369
get { return check_plugin_unloading; }
370
set { check_plugin_unloading = value; }
373
// Run file manager for ~/.tomboy/Plugins
374
public void ShowPluginsDirectory ()
376
string command, args;
378
// FIXME: There has to be a better way to check this...
379
if (Environment.GetEnvironmentVariable ("GNOME_DESKTOP_SESSION_ID") == null &&
380
(Environment.GetEnvironmentVariable ("KDE_FULL_SESSION") != null ||
381
Environment.GetEnvironmentVariable ("KDEHOME") != null ||
382
Environment.GetEnvironmentVariable ("KDEDIR") != null)) {
383
Logger.Log ("Starting Konqueror...");
385
command = "konqueror";
388
Logger.Log ("Starting Nautilus...");
390
command = "nautilus";
391
args = string.Format ("--no-desktop --no-default-window {0}",
396
Process.Start (command, args);
397
} catch (Exception e) {
398
Logger.Log ("Error opening file browser \"{0}\" to \"{1}\": {2}",
405
public void LoadPluginsForNote (Note note)
407
foreach (Type type in plugin_types as
408
System.Collections.Generic.IEnumerable<Type>) {
409
if (IsPluginEnabled (type))
410
AttachPlugin (type, note);
413
// Make sure we remove plugins when a note is deleted
414
note.Manager.NoteDeleted += OnNoteDeleted;
417
static void CleanupOldPlugins (string plugins_dir)
419
// NOTE: These might be symlinks to the system-installed
420
// versions, so use unlink() just in case.
422
// Remove old version 0.3.[12] "Uninstalled Plugins" symlink
423
string uninstalled_dir = Path.Combine (plugins_dir, "Uninstalled Plugins");
424
if (Directory.Exists (uninstalled_dir)) {
425
Logger.Log ("Removing old \"Uninstalled Plugins\" " +
428
Syscall.unlink (uninstalled_dir);
429
} catch (Exception e) {
430
Logger.Log ("Error removing: {0}", e);
434
// Remove old version 0.3.[12] "ExportToHTML.dll" file
435
string export_to_html_dll = Path.Combine (plugins_dir, "ExportToHTML.dll");
436
if (File.Exists (export_to_html_dll)) {
437
Logger.Log ("Removing old \"ExportToHTML.dll\" plugin...");
439
Syscall.unlink (export_to_html_dll);
440
} catch (Exception e) {
441
Logger.Log ("Error removing: {0}", e);
445
// Remove old version 0.3.[12] "PrintNotes.dll" file
446
string print_notes_dll = Path.Combine (plugins_dir, "PrintNotes.dll");
447
if (File.Exists (print_notes_dll)) {
448
Logger.Log ("Removing old \"PrintNotes.dll\" plugin...");
450
Syscall.unlink (print_notes_dll);
451
} catch (Exception e) {
452
Logger.Log ("Error removing: {0}", e);
457
public static void CreatePluginsDir (string plugins_dir)
459
// Create Plugins dir
460
if (!Directory.Exists (plugins_dir))
461
Directory.CreateDirectory (plugins_dir);
463
// Clean up old plugin remnants
464
CleanupOldPlugins (plugins_dir);
467
void OnNoteDeleted (object sender, Note deleted)
469
// Clean out the plugins for this deleted note.
470
PluginTable note_plugins = null;
472
if (attached_plugins.TryGetValue (deleted, out note_plugins)) {
473
foreach (NotePlugin plugin in note_plugins.Values as
474
System.Collections.Generic.IEnumerable<NotePlugin>) {
477
} catch (Exception e) {
479
"Cannot dispose {0}: {1}",
480
plugin.GetType(), e);
484
note_plugins.Clear ();
485
attached_plugins.Remove (deleted);
489
void AttachPlugin (Type type, Note note)
491
PluginTable note_plugins;
493
if (!attached_plugins.TryGetValue (note, out note_plugins)) {
494
note_plugins = new Dictionary<Type, NotePlugin> ();
495
attached_plugins [note] = note_plugins;
498
if (typeof (NotePlugin).IsAssignableFrom (type)) {
502
plugin = (NotePlugin)Activator.CreateInstance (type);
503
plugin.Initialize (note);
504
} catch (Exception e) {
506
"Cannot initialize {0} for note '{1}': {2}",
507
type, note.Title, e);
512
note_plugins[type] = plugin;
516
void AttachPlugin (Type type)
518
if (typeof (NotePlugin).IsAssignableFrom (type)) {
519
// A plugin may add or remove notes when being
520
// created. Therefore, it is best to iterate
521
// through a copy of the notes list to avoid
522
// "System.InvalidOperationException: out of sync"
523
List<Note> notes_copy =
524
new List<Note> (attached_plugins.Keys);
525
foreach (Note note in notes_copy)
526
AttachPlugin (type, note);
530
void DetachPlugin (Type type)
532
List<PluginReference> references;
534
if (CheckPluginUnloading) {
535
references = new List<PluginReference> ();
540
DetachPlugin (type, references);
542
Logger.Debug ("Starting garbage collection...");
545
GC.WaitForPendingFinalizers ();
547
Logger.Debug ("Garbage collection complete.");
549
if (!CheckPluginUnloading)
552
Logger.Debug ("Checking plugin references...");
554
int finalized = 0, leaking = 0;
556
foreach (PluginReference r in references) {
557
object plugin = r.Target;
559
if (null != plugin) {
561
"Leaking reference on {0}: '{1}'",
562
plugin, r.Description);
572
"http://svn.myrealbox.com/source/trunk/heap-shot";
573
string title = String.Format (
574
Catalog.GetString ("Cannot fully disable {0}."),
576
string message = String.Format (Catalog.GetString (
577
"Cannot fully disable {0} as there still are " +
578
"at least {1} reference to this plugin. This " +
579
"indicates a programming error. Contact the " +
580
"plugin's author and report this problem.\n\n" +
581
"<b>Developer Information:</b> This problem " +
582
"usually occurs when the plugin's Dispose " +
583
"method fails to disconnect all event handlers. " +
584
"The heap-shot profiler ({2}) can help to " +
585
"identify leaking references."),
586
type, leaking, heapshot);
588
HIGMessageDialog dialog = new HIGMessageDialog (
589
null, 0, Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
596
Logger.Debug ("finalized: {0}, leaking: {1}", finalized, leaking);
599
// Dispose loop has to happen on separate stack frame when debugging,
600
// as otherwise the local variable "plugin" will cause at least one
601
// plugin instance to not be garbage collected. Just assigning
602
// null to the variable will not help, as the JIT optimizes this
604
void DetachPlugin (Type type, List<PluginReference> references)
606
if (typeof (NotePlugin).IsAssignableFrom (type)) {
607
foreach (KeyValuePair<Note, PluginTable> pair
608
in attached_plugins as
609
System.Collections.Generic.IEnumerable<
610
System.Collections.Generic.KeyValuePair<
611
Note, PluginTable>>) {
612
NotePlugin plugin = null;
614
if (pair.Value.TryGetValue (type, out plugin)) {
615
pair.Value[type] = null;
616
pair.Value.Remove (type);
620
} catch (Exception e) {
622
"Cannot dispose {0} for note '{1}': {2}",
623
type, pair.Key.Title, e);
626
if (null != references)
627
references.Add (new PluginReference (
628
plugin, pair.Key.Title));
634
void OnPluginCreated (object sender, FileSystemEventArgs args)
636
Logger.Log ("Plugin '{0}' Created",
637
Path.GetFileName (args.FullPath));
639
IList<Type> asm_plugins = FindPluginTypesInFile (args.FullPath);
641
// Add the plugin to the list
642
// and load the added plugin for all existing plugged in notes
643
foreach (Type type in asm_plugins as
644
System.Collections.Generic.IEnumerable<Type>) {
645
if (IsPluginEnabled (type)) {
646
plugin_types.Add (type);
651
asm_plugins.Clear ();
654
void OnPluginDeleted (object sender, FileSystemEventArgs args)
656
Logger.Log ("Plugin '{0}' Deleted",
657
Path.GetFileName (args.FullPath));
659
List<Type> kill_list = new List<Type> ();
661
// Find the plugins in the deleted assembly
662
foreach (Type type in plugin_types as
663
System.Collections.Generic.IEnumerable<Type>) {
664
if (type.Assembly.Location == args.FullPath)
665
kill_list.Add (type);
668
foreach (Type type in kill_list) {
669
plugin_types.Remove (type);
676
public Type[] Plugins
680
List<Type> plugins = new List<Type> (plugin_types.Count);
682
foreach (Type type in plugin_types as
683
System.Collections.Generic.IEnumerable<Type>) {
684
if (!IsBuiltin (type))
688
return plugins.ToArray ();
692
static string[] GetActivePlugins ()
694
return (string[]) Preferences.Get (Preferences.ENABLED_PLUGINS);
697
public static bool IsBuiltin (Type plugin)
699
return plugin.Assembly == typeof (PluginManager).Assembly;
702
public bool IsPluginEnabled (Type plugin)
704
return IsBuiltin (plugin) ||
705
Array.IndexOf (GetActivePlugins (), plugin.Name) >= 0;
708
public void SetPluginEnabled (Type plugin, bool enabled)
710
List<string> active_plugins = new List<string> (GetActivePlugins ());
711
int index = active_plugins.IndexOf (plugin.Name);
713
if (enabled != (index >= 0)) {
715
active_plugins.Add (plugin.Name);
716
AttachPlugin (plugin);
718
active_plugins.RemoveAt (index);
719
DetachPlugin (plugin);
722
Preferences.Set (Preferences.ENABLED_PLUGINS,
723
active_plugins.ToArray ());
727
public static PluginInfoAttribute GetPluginInfo(Type plugin)
729
return Attribute.GetCustomAttribute (plugin,
730
typeof (PluginInfoAttribute)) as PluginInfoAttribute;
733
public static string GetPluginName (Type type, PluginInfoAttribute info)
745
public static string GetPluginVersion (Type type, PluginInfoAttribute info)
747
string version = null;
750
version = info.Version;
752
if (null == version) {
753
foreach (Attribute attribute
754
in type.Assembly.GetCustomAttributes (true)) {
755
AssemblyInformationalVersionAttribute productVersion =
756
attribute as AssemblyInformationalVersionAttribute;
758
if (null != productVersion) {
759
version = productVersion.InformationalVersion;
763
AssemblyFileVersionAttribute fileVersion =
764
attribute as AssemblyFileVersionAttribute;
766
if (null != fileVersion) {
767
version = fileVersion.Version;
774
version = type.Assembly.GetName().Version.ToString();
779
public static string GetPluginName (Type type)
781
return GetPluginName (type, GetPluginInfo (type));
784
public static Gtk.Widget CreatePreferencesWidget (PluginInfoAttribute info)
786
if (null != info && null != info.PreferencesWidget)
787
return (Gtk.Widget)Activator.CreateInstance (info.PreferencesWidget);
792
IList<Type> FindPluginTypes ()
794
List<Type> all_plugin_types = new List<Type> ();
796
all_plugin_types.AddRange (stock_plugins);
797
all_plugin_types.AddRange (
798
FindPluginTypesInDirectory (Defines.SYS_PLUGINS_DIR));
799
all_plugin_types.AddRange (FindPluginTypesInDirectory (plugins_dir));
801
return all_plugin_types;
804
static IList<Type> FindPluginTypesInDirectory (string dirpath)
806
IList<Type> dir_plugin_types = new List<Type> ();
810
files = Directory.GetFiles (dirpath, "*.dll");
811
} catch (Exception e) {
812
Logger.Log ("Error getting plugin types from {0}: {1}",
815
return dir_plugin_types;
818
foreach (string file in files) {
819
Console.Write ("Trying Plugin: {0} ... ", Path.GetFileName (file));
822
IList<Type> asm_plugins = FindPluginTypesInFile (file);
823
foreach (Type type in asm_plugins as
824
System.Collections.Generic.IEnumerable<Type>) {
825
dir_plugin_types.Add (type);
827
} catch (Exception e) {
828
Logger.Log ("Failed.\n{0}", e);
832
return dir_plugin_types;
835
static IList<Type> FindPluginTypesInFile (string filepath)
837
Assembly asm = Assembly.LoadFrom (filepath);
838
return FindPluginTypesInAssembly (asm);
841
static IList<Type> FindPluginTypesInAssembly (Assembly asm)
843
IList<Type> asm_plugins = new List<Type> ();
844
Type [] types = asm.GetTypes ();
845
bool found_one = false;
847
foreach (Type type in types) {
848
if (typeof (IPlugin).IsAssignableFrom (type)) {
849
Console.Write ("{0}. ", type.FullName);
850
asm_plugins.Add (type);
855
Logger.Log ("{0}", found_one ? "Done." : "Skipping.");