diff options
22 files changed, 2420 insertions, 1277 deletions
diff --git a/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs b/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs index 34f931f..842c12b 100644 --- a/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs +++ b/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs @@ -28,40 +28,43 @@ using System; -using System.Threading; using System.Collections; -using System.Collections.Specialized; -using System.IO; -using System.Xml; -using System.Reflection; -using Mono.Addins.Description; using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; using System.Linq; -using Mono.Addins.Serialization; +using System.Threading; +using Mono.Addins.Description; namespace Mono.Addins.Database { - class AddinDatabase + class AddinDatabase { public const string GlobalDomain = "global"; public const string UnknownDomain = "unknown"; public const string VersionTag = "004"; - List<Addin> allSetupInfos; - List<Addin> addinSetupInfos; - List<Addin> rootSetupInfos; + readonly AddinEngine addinEngine; + readonly AddinRegistry registry; + readonly string addinDbDir; + readonly FileDatabase fileDatabase; + readonly object localLock = new object (); + + bool allSetupInfosLoaded; + ImmutableArray<Addin> allSetupInfos; + ImmutableArray<Addin> addinSetupInfos; + ImmutableArray<Addin> rootSetupInfos; + + Dictionary<string, Addin> cachedAddinSetupInfos = new Dictionary<string, Addin> (); + ImmutableAddinHostIndex hostIndex; + internal static bool RunningSetupProcess; + bool fatalDatabseError; - Hashtable cachedAddinSetupInfos = new Hashtable (); - AddinHostIndex hostIndex; - FileDatabase fileDatabase; - string addinDbDir; DatabaseConfiguration config = null; - AddinRegistry registry; int lastDomainId; - AddinEngine addinEngine; - AddinFileSystemExtension fs = new AddinFileSystemExtension (); + AddinFileSystemExtension fileSystemExtension = new AddinFileSystemExtension (); List<object> extensions = new List<object> (); public AddinDatabase (AddinEngine addinEngine, AddinRegistry registry) @@ -71,13 +74,20 @@ namespace Mono.Addins.Database addinDbDir = Path.Combine (registry.AddinCachePath, "addin-db-" + VersionTag); fileDatabase = new FileDatabase (AddinDbDir); } - + + public AddinDatabaseTransaction BeginTransaction (AddinEngineTransaction addinEngineTransaction = null) + { + return new AddinDatabaseTransaction (this, localLock, addinEngineTransaction); + } + + internal AddinEngine AddinEngine => addinEngine; + string AddinDbDir { get { return addinDbDir; } } public AddinFileSystemExtension FileSystem { - get { return fs; } + get { return fileSystemExtension; } } public string AddinCachePath { @@ -116,36 +126,34 @@ namespace Mono.Addins.Database } } - public void Clear () - { - if (Directory.Exists (AddinCachePath)) - Directory.Delete (AddinCachePath, true); - if (Directory.Exists (AddinFolderCachePath)) - Directory.Delete (AddinFolderCachePath, true); - } - public void CopyExtensions (AddinDatabase other) { - foreach (object o in other.extensions) - RegisterExtension (o); + lock (extensions) { + foreach (object o in other.extensions) + RegisterExtension (o); + } } public void RegisterExtension (object extension) { - extensions.Add (extension); - if (extension is AddinFileSystemExtension) - fs = (AddinFileSystemExtension) extension; - else - throw new NotSupportedException (); + lock (extensions) { + extensions.Add (extension); + if (extension is AddinFileSystemExtension) + fileSystemExtension = (AddinFileSystemExtension)extension; + else + throw new NotSupportedException (); + } } public void UnregisterExtension (object extension) { - extensions.Remove (extension); - if ((extension as AddinFileSystemExtension) == fs) - fs = new AddinFileSystemExtension (); - else - throw new InvalidOperationException (); + lock (extensions) { + extensions.Remove (extension); + if ((extension as AddinFileSystemExtension) == fileSystemExtension) + fileSystemExtension = new AddinFileSystemExtension (); + else + throw new InvalidOperationException (); + } } public ExtensionNodeSet FindNodeSet (string domain, string addinId, string id) @@ -190,98 +198,69 @@ namespace Mono.Addins.Database // Get the cached list if the add-in list has already been loaded. // The domain doesn't have to be checked again, since it is always the same - IEnumerable<Addin> result = null; - - if ((flags & AddinSearchFlagsInternal.IncludeAll) == AddinSearchFlagsInternal.IncludeAll) { - if (allSetupInfos != null) - result = allSetupInfos; - } - else if ((flags & AddinSearchFlagsInternal.IncludeAddins) == AddinSearchFlagsInternal.IncludeAddins) { - if (addinSetupInfos != null) - result = addinSetupInfos; - } - else { - if (rootSetupInfos != null) - result = rootSetupInfos; - } - - if (result == null) { - InternalCheck (domain); - using (fileDatabase.LockRead ()) { - result = InternalGetInstalledAddins (domain, null, flags & ~AddinSearchFlagsInternal.LatestVersionsOnly); - } - } - - if ((flags & AddinSearchFlagsInternal.LatestVersionsOnly) == AddinSearchFlagsInternal.LatestVersionsOnly) - result = result.Where (a => a.IsLatestVersion); - - if ((flags & AddinSearchFlagsInternal.ExcludePendingUninstall) == AddinSearchFlagsInternal.ExcludePendingUninstall) - result = result.Where (a => !IsRegisteredForUninstall (a.Description.Domain, a.Id)); - - return result; + return InternalGetInstalledAddins (domain, null, flags & ~AddinSearchFlagsInternal.LatestVersionsOnly, false); } - IEnumerable<Addin> InternalGetInstalledAddins (string domain, AddinSearchFlagsInternal type) + IEnumerable<Addin> InternalGetInstalledAddins (string domain, AddinSearchFlagsInternal type, bool dbIsLockedForRead) { - return InternalGetInstalledAddins (domain, null, type); + return InternalGetInstalledAddins (domain, null, type, dbIsLockedForRead); } - IEnumerable<Addin> InternalGetInstalledAddins (string domain, string idFilter, AddinSearchFlagsInternal type) + IEnumerable<Addin> InternalGetInstalledAddins (string domain, string idFilter, AddinSearchFlagsInternal type, bool dbIsLockedForRead) { - if ((type & AddinSearchFlagsInternal.LatestVersionsOnly) != 0) - throw new InvalidOperationException ("LatestVersionsOnly flag not supported here"); - - if (allSetupInfos == null) { - Dictionary<string,Addin> adict = new Dictionary<string, Addin> (); + if (!allSetupInfosLoaded) { + lock (localLock) { + if (!allSetupInfosLoaded) { + Dictionary<string, Addin> adict = new Dictionary<string, Addin> (); - // Global add-ins are valid for any private domain - if (domain != AddinDatabase.GlobalDomain) - FindInstalledAddins (adict, AddinDatabase.GlobalDomain, idFilter); + using (!dbIsLockedForRead ? fileDatabase.LockRead() : null) { + // Global add-ins are valid for any private domain + if (domain != AddinDatabase.GlobalDomain) + FindInstalledAddins (adict, AddinDatabase.GlobalDomain); - FindInstalledAddins (adict, domain, idFilter); - List<Addin> alist = new List<Addin> (adict.Values); - UpdateLastVersionFlags (alist); - if (idFilter != null) - return alist; - allSetupInfos = alist; - } - if ((type & AddinSearchFlagsInternal.IncludeAll) == AddinSearchFlagsInternal.IncludeAll) - return FilterById (allSetupInfos, idFilter); - - if ((type & AddinSearchFlagsInternal.IncludeAddins) == AddinSearchFlagsInternal.IncludeAddins) { - if (addinSetupInfos == null) { - addinSetupInfos = new List<Addin> (); - foreach (Addin adn in allSetupInfos) - if (!adn.Description.IsRoot) - addinSetupInfos.Add (adn); + FindInstalledAddins (adict, domain); + } + List<Addin> alist = new List<Addin> (adict.Values); + UpdateLastVersionFlags (alist); + allSetupInfos = alist.ToImmutableArray (); + addinSetupInfos = alist.Where (addin => !addin.Description.IsRoot).ToImmutableArray (); + rootSetupInfos = alist.Where (addin => addin.Description.IsRoot).ToImmutableArray (); + allSetupInfosLoaded = true; + } } - return FilterById (addinSetupInfos, idFilter); } - else { - if (rootSetupInfos == null) { - rootSetupInfos = new List<Addin> (); - foreach (Addin adn in allSetupInfos) - if (adn.Description.IsRoot) - rootSetupInfos.Add (adn); - } - return FilterById (rootSetupInfos, idFilter); + IEnumerable<Addin> result; + + if ((type & AddinSearchFlagsInternal.IncludeAll) == AddinSearchFlagsInternal.IncludeAll) { + result = allSetupInfos; + } else if ((type & AddinSearchFlagsInternal.IncludeAddins) == AddinSearchFlagsInternal.IncludeAddins) { + result = addinSetupInfos; + } else { + result = rootSetupInfos; } + + result = FilterById (result, idFilter); + + if ((type & AddinSearchFlagsInternal.LatestVersionsOnly) == AddinSearchFlagsInternal.LatestVersionsOnly) + result = result.Where (a => a.IsLatestVersion); + + if ((type & AddinSearchFlagsInternal.ExcludePendingUninstall) == AddinSearchFlagsInternal.ExcludePendingUninstall) + result = result.Where (a => !IsRegisteredForUninstall (a.Description.Domain, a.Id)); + return result; } - - IEnumerable<Addin> FilterById (List<Addin> addins, string id) + + IEnumerable<Addin> FilterById (IEnumerable<Addin> addins, string id) { if (id == null) return addins; return addins.Where (a => Addin.GetIdName (a.Id) == id); } - void FindInstalledAddins (Dictionary<string,Addin> result, string domain, string idFilter) + void FindInstalledAddins (Dictionary<string,Addin> result, string domain) { - if (idFilter == null) - idFilter = "*"; string dir = Path.Combine (AddinCachePath, domain); if (Directory.Exists (dir)) { - foreach (string file in fileDatabase.GetDirectoryFiles (dir, idFilter + ",*.maddin")) { + foreach (string file in fileDatabase.GetDirectoryFiles (dir, "*,*.maddin")) { string id = Path.GetFileNameWithoutExtension (file); if (!result.ContainsKey (id)) { var adesc = GetInstalledDomainAddin (domain, id, true, false, false); @@ -330,23 +309,26 @@ namespace Mono.Addins.Database else return null; } - + Addin GetInstalledDomainAddin (string domain, string id, bool exactVersionMatch, bool enabledOnly, bool dbLockCheck) { - Addin sinfo = null; string idd = id + " " + domain; - object ob = cachedAddinSetupInfos [idd]; - if (ob != null) { - sinfo = ob as Addin; + Addin sinfo; + bool found; + lock (cachedAddinSetupInfos) { + found = cachedAddinSetupInfos.TryGetValue (idd, out sinfo); + } + + if (found) { if (sinfo != null) { if (!enabledOnly || sinfo.Enabled) return sinfo; if (exactVersionMatch) return null; - } - else if (enabledOnly) + } else if (enabledOnly) { // Ignore the 'not installed' flag when disabled add-ins are allowed return null; + } } if (dbLockCheck) @@ -376,13 +358,15 @@ namespace Mono.Addins.Database } if (foundDomain != null) { sinfo = new Addin (this, foundDomain, id); - cachedAddinSetupInfos [idd] = sinfo; - if (!enabledOnly || sinfo.Enabled) - return sinfo; - if (exactVersionMatch) { - // Cache lookups with negative result - cachedAddinSetupInfos [idd] = this; - return null; + lock (cachedAddinSetupInfos) { + cachedAddinSetupInfos [idd] = sinfo; + if (!enabledOnly || sinfo.Enabled) + return sinfo; + if (exactVersionMatch) { + // Cache lookups with negative result + cachedAddinSetupInfos [idd] = null; + return null; + } } } } @@ -393,7 +377,7 @@ namespace Mono.Addins.Database string bestVersion = null; Addin.GetIdParts (id, out name, out version); - foreach (Addin ia in InternalGetInstalledAddins (domain, name, AddinSearchFlagsInternal.IncludeAll)) + foreach (Addin ia in InternalGetInstalledAddins (domain, name, AddinSearchFlagsInternal.IncludeAll, true)) { if ((!enabledOnly || ia.Enabled) && (version.Length == 0 || ia.SupportsVersion (version)) && @@ -404,15 +388,20 @@ namespace Mono.Addins.Database } } if (sinfo != null) { - cachedAddinSetupInfos [idd] = sinfo; + lock (cachedAddinSetupInfos) { + cachedAddinSetupInfos [idd] = sinfo; + } return sinfo; } } - + // Cache lookups with negative result // Ignore the 'not installed' flag when disabled add-ins are allowed - if (enabledOnly) - cachedAddinSetupInfos [idd] = this; + if (enabledOnly) { + lock (cachedAddinSetupInfos) { + cachedAddinSetupInfos [idd] = null; + } + } return null; } } @@ -426,20 +415,22 @@ namespace Mono.Addins.Database { InternalCheck (domain); Addin ainfo = null; - - object ob = cachedAddinSetupInfos [assemblyLocation]; - if (ob != null) - return ob as Addin; // Don't use a cast here is ob may not be an Addin. - AddinHostIndex index = GetAddinHostIndex (); + lock (cachedAddinSetupInfos) { + if (cachedAddinSetupInfos.TryGetValue (assemblyLocation, out var ob)) + return ob; // this can be null, if the add-in is disabled + } + + var index = GetAddinHostIndex (); string addin, addinFile, rdomain; if (index.GetAddinForAssembly (assemblyLocation, out addin, out addinFile, out rdomain)) { string sid = addin + " " + rdomain; - ainfo = cachedAddinSetupInfos [sid] as Addin; - if (ainfo == null) - ainfo = new Addin (this, rdomain, addin); - cachedAddinSetupInfos [assemblyLocation] = ainfo; - cachedAddinSetupInfos [addin + " " + rdomain] = ainfo; + lock (cachedAddinSetupInfos) { + if (!cachedAddinSetupInfos.TryGetValue(sid, out ainfo)) + ainfo = new Addin (this, rdomain, addin); + cachedAddinSetupInfos [assemblyLocation] = ainfo; + cachedAddinSetupInfos [sid] = ainfo; + } } return ainfo; @@ -469,8 +460,14 @@ namespace Mono.Addins.Database { EnableAddin (domain, id, true); } - - internal void EnableAddin (string domain, string id, bool exactVersionMatch) + + public void EnableAddin (string domain, string id, bool exactVersionMatch) + { + using var transaction = BeginTransaction (); + EnableAddin (transaction, domain, id, exactVersionMatch); + } + + void EnableAddin (AddinDatabaseTransaction dbTransaction, string domain, string id, bool exactVersionMatch) { Addin ainfo = GetInstalledAddin (domain, id, exactVersionMatch, false); if (ainfo == null) @@ -486,19 +483,26 @@ namespace Mono.Addins.Database if (dep is AddinDependency) { AddinDependency adep = dep as AddinDependency; string adepid = Addin.GetFullId (ainfo.AddinInfo.Namespace, adep.AddinId, adep.Version); - EnableAddin (domain, adepid, false); + EnableAddin (dbTransaction, domain, adepid, false); } } - Configuration.SetEnabled (id, true, ainfo.AddinInfo.EnabledByDefault, true); - SaveConfiguration (); + Configuration.SetEnabled (dbTransaction, id, true, ainfo.AddinInfo.EnabledByDefault, true); + SaveConfiguration (dbTransaction); - if (addinEngine != null && addinEngine.IsInitialized) - addinEngine.ActivateAddin (id); + if (addinEngine != null && addinEngine.IsInitialized) { + addinEngine.ActivateAddin (dbTransaction.GetAddinEngineTransaction(), id); + } } - + public void DisableAddin (string domain, string id, bool exactVersionMatch = false, bool onlyForCurrentSession = false) { + using var transaction = BeginTransaction (); + DisableAddin (transaction, domain, id, exactVersionMatch, onlyForCurrentSession); + } + + void DisableAddin (AddinDatabaseTransaction dbTransaction, string domain, string id, bool exactVersionMatch = false, bool onlyForCurrentSession = false) + { Addin ai = GetInstalledAddin (domain, id, true); if (ai == null) throw new InvalidOperationException ("Add-in '" + id + "' not installed."); @@ -506,8 +510,8 @@ namespace Mono.Addins.Database if (!IsAddinEnabled (domain, id, exactVersionMatch)) return; - Configuration.SetEnabled (id, false, ai.AddinInfo.EnabledByDefault, exactVersionMatch, onlyForCurrentSession); - SaveConfiguration (); + Configuration.SetEnabled (dbTransaction, id, false, ai.AddinInfo.EnabledByDefault, exactVersionMatch, onlyForCurrentSession); + SaveConfiguration (dbTransaction); // Disable all add-ins which depend on it @@ -531,7 +535,7 @@ namespace Mono.Addins.Database Addin adepinfo = GetInstalledAddin (domain, adepid, false, true); if (adepinfo == null) { - DisableAddin (domain, ainfo.Id, onlyForCurrentSession: onlyForCurrentSession); + DisableAddin (dbTransaction, domain, ainfo.Id, onlyForCurrentSession: onlyForCurrentSession); break; } } @@ -539,25 +543,26 @@ namespace Mono.Addins.Database } catch { // If something goes wrong, enable the add-in again - Configuration.SetEnabled (id, true, ai.AddinInfo.EnabledByDefault, false, onlyForCurrentSession); - SaveConfiguration (); + Configuration.SetEnabled (dbTransaction, id, true, ai.AddinInfo.EnabledByDefault, false, onlyForCurrentSession); + SaveConfiguration (dbTransaction); throw; } - if (addinEngine != null && addinEngine.IsInitialized) - addinEngine.UnloadAddin (id); + if (addinEngine != null && addinEngine.IsInitialized) { + addinEngine.UnloadAddin (dbTransaction.GetAddinEngineTransaction(), id); + } } - public void UpdateEnabledStatus () + void UpdateEnabledStatus (AddinDatabaseTransaction transaction) { // Ensure that all enabled addins that have dependencies also have their dependencies enabled. HashSet<Addin> updatedAddins = new HashSet<Addin> (); var allAddins = GetInstalledAddins (registry.CurrentDomain, AddinSearchFlagsInternal.IncludeAddins | AddinSearchFlagsInternal.LatestVersionsOnly).ToList (); foreach (Addin addin in allAddins) - UpdateEnabledStatus (registry.CurrentDomain, addin, allAddins, updatedAddins); + UpdateEnabledStatus (transaction, registry.CurrentDomain, addin, allAddins, updatedAddins); } - void UpdateEnabledStatus (string domain, Addin addin, List<Addin> allAddins, HashSet<Addin> updatedAddins) + void UpdateEnabledStatus (AddinDatabaseTransaction transaction, string domain, Addin addin, List<Addin> allAddins, HashSet<Addin> updatedAddins) { if (!updatedAddins.Add (addin)) return; @@ -575,12 +580,12 @@ namespace Mono.Addins.Database string adepid = Addin.GetFullId (addin.AddinInfo.Namespace, adep.AddinId, null); var dependency = allAddins.FirstOrDefault (a => Addin.GetFullId (a.Namespace, a.LocalId, null) == adepid); if (dependency != null) { - UpdateEnabledStatus (domain, dependency, allAddins, updatedAddins); + UpdateEnabledStatus (transaction, domain, dependency, allAddins, updatedAddins); if (!dependency.Enabled) { // One of the dependencies is disabled, so this add-in also needs to be disabled. // However, we disabled only for the current configuration, we don't want to change // what the user configured. - DisableAddin (domain, addin.Id, onlyForCurrentSession: true); + DisableAddin (transaction, domain, addin.Id, onlyForCurrentSession: true); return; } } @@ -589,9 +594,10 @@ namespace Mono.Addins.Database public void RegisterForUninstall (string domain, string id, IEnumerable<string> files) { - DisableAddin (domain, id, true); - Configuration.RegisterForUninstall (id, files); - SaveConfiguration (); + using var transaction = BeginTransaction (); + DisableAddin (transaction, domain, id, true); + Configuration.RegisterForUninstall (transaction, id, files); + SaveConfiguration (transaction); } public bool IsRegisteredForUninstall (string domain, string addinId) @@ -630,7 +636,8 @@ namespace Mono.Addins.Database AddinUpdateData updateData = new AddinUpdateData (this, monitor); // Clear cached data - cachedAddinSetupInfos.Clear (); + lock(cachedAddinSetupInfos) + cachedAddinSetupInfos.Clear (); // Collect all information @@ -703,7 +710,7 @@ namespace Mono.Addins.Database } // If the original file does not exist, the description can be deleted - if (!fs.FileExists (conf.AddinFile)) { + if (!fileSystemExtension.FileExists (conf.AddinFile)) { SafeDelete (monitor, file); continue; } @@ -1012,19 +1019,19 @@ namespace Mono.Addins.Database internal void ResetBasicCachedData () { - allSetupInfos = null; - addinSetupInfos = null; - rootSetupInfos = null; + lock(localLock) + allSetupInfosLoaded = false; } - internal void ResetCachedData () + internal void ResetCachedData (AddinDatabaseTransaction dbTransaction = null) { ResetBasicCachedData (); hostIndex = null; - cachedAddinSetupInfos.Clear (); + lock(cachedAddinSetupInfos) + cachedAddinSetupInfos.Clear (); dependsOnCache.Clear (); if (addinEngine != null) - addinEngine.ResetCachedData (); + addinEngine.ResetCachedData (dbTransaction?.GetAddinEngineTransaction()); } Dictionary<string, HashSet<string>> dependsOnCache = new Dictionary<string, HashSet<string>> (); @@ -1088,79 +1095,87 @@ namespace Mono.Addins.Database Update (monitor, domain, context); } - - public void Update (IProgressStatus monitor, string domain, ScanOptions context = null) + + public void Update(IProgressStatus monitor, string domain, ScanOptions context = null, AddinEngineTransaction addinEngineTransaction = null) { if (monitor == null) - monitor = new ConsoleProgressStatus (false); + monitor = new ConsoleProgressStatus(false); if (RunningSetupProcess) return; - + fatalDatabseError = false; - + DateTime tim = DateTime.Now; - - RunPendingUninstalls (monitor); - - Hashtable installed = new Hashtable (); - bool changesFound = CheckFolders (monitor, domain); - + + using var dbTransaction = BeginTransaction(addinEngineTransaction); + + RunPendingUninstalls(dbTransaction, monitor); + + Hashtable installed = new Hashtable(); + bool changesFound = CheckFolders(monitor, domain); + if (monitor.IsCanceled) return; - + if (monitor.LogLevel > 1) - monitor.Log ("Folders checked (" + (int) (DateTime.Now - tim).TotalMilliseconds + " ms)"); - - if (changesFound) { + monitor.Log("Folders checked (" + (int)(DateTime.Now - tim).TotalMilliseconds + " ms)"); + + if (changesFound) + { // Something has changed, the add-ins need to be re-scanned, but it has // to be done in an external process - - if (domain != null) { - using (fileDatabase.LockRead ()) { - foreach (Addin ainfo in InternalGetInstalledAddins (domain, AddinSearchFlagsInternal.IncludeAddins)) { - installed [ainfo.Id] = ainfo.Id; - } + + if (domain != null) + { + foreach (Addin ainfo in InternalGetInstalledAddins(domain, AddinSearchFlagsInternal.IncludeAddins, false)) + { + installed[ainfo.Id] = ainfo.Id; } } - - RunScannerProcess (monitor, context); - - ResetCachedData (); - - registry.NotifyDatabaseUpdated (); + + RunScannerProcess(monitor, context); + + ResetCachedData(dbTransaction); + + registry.NotifyDatabaseUpdated(); } - + if (fatalDatabseError) - monitor.ReportError ("The add-in database could not be updated. It may be due to file corruption. Try running the setup repair utility", null); - + monitor.ReportError("The add-in database could not be updated. It may be due to file corruption. Try running the setup repair utility", null); + // Update the currently loaded add-ins - if (changesFound && domain != null && addinEngine != null && addinEngine.IsInitialized) { - Hashtable newInstalled = new Hashtable (); - foreach (Addin ainfo in GetInstalledAddins (domain, AddinSearchFlagsInternal.IncludeAddins)) { - newInstalled [ainfo.Id] = ainfo.Id; + if (changesFound && domain != null && addinEngine != null && addinEngine.IsInitialized) + { + Hashtable newInstalled = new Hashtable(); + foreach (Addin ainfo in GetInstalledAddins(domain, AddinSearchFlagsInternal.IncludeAddins)) + { + newInstalled[ainfo.Id] = ainfo.Id; } - - foreach (string aid in installed.Keys) { + + foreach (string aid in installed.Keys) + { // Always try to unload, event if the add-in was not currently loaded. // Required since the add-ins has to be marked as 'disabled', to avoid // extensions from this add-in to be loaded - if (!newInstalled.Contains (aid)) - addinEngine.UnloadAddin (aid); + if (!newInstalled.Contains(aid)) + addinEngine.UnloadAddin(dbTransaction.GetAddinEngineTransaction(), aid); } - - foreach (string aid in newInstalled.Keys) { - if (!installed.Contains (aid)) { - Addin addin = addinEngine.Registry.GetAddin (aid); + + foreach (string aid in newInstalled.Keys) + { + if (!installed.Contains(aid)) + { + Addin addin = addinEngine.Registry.GetAddin(aid); if (addin != null) - addinEngine.ActivateAddin (aid); + addinEngine.ActivateAddin(dbTransaction.GetAddinEngineTransaction(), aid); } } } - UpdateEnabledStatus (); + UpdateEnabledStatus(dbTransaction); } - void RunPendingUninstalls (IProgressStatus monitor) + void RunPendingUninstalls (AddinDatabaseTransaction dbTransaction, IProgressStatus monitor) { bool changesDone = false; @@ -1198,12 +1213,12 @@ namespace Mono.Addins.Database } if (canUninstall) { - Configuration.UnregisterForUninstall (adn.AddinId); + Configuration.UnregisterForUninstall (dbTransaction, adn.AddinId); changesDone = true; } } if (changesDone) - SaveConfiguration (); + SaveConfiguration (dbTransaction); } void RunScannerProcess (IProgressStatus monitor, ScanOptions context) @@ -1214,8 +1229,8 @@ namespace Mono.Addins.Database IProgressStatus scanMonitor = monitor; context = context ?? new ScanOptions (); - if (fs.GetType () != typeof (AddinFileSystemExtension)) - context.FileSystemExtension = fs; + if (fileSystemExtension.GetType () != typeof (AddinFileSystemExtension)) + context.FileSystemExtension = fileSystemExtension; bool retry = false; do { @@ -1350,10 +1365,10 @@ namespace Mono.Addins.Database void InternalScanFolders (IProgressStatus monitor, AddinScanResult scanResult) { try { - fs.ScanStarted (); + fileSystemExtension.ScanStarted (); InternalScanFolders2 (monitor, scanResult); } finally { - fs.ScanFinished (); + fileSystemExtension.ScanFinished (); } } @@ -1366,7 +1381,7 @@ namespace Mono.Addins.Database return; try { - scanResult.HostIndex = GetAddinHostIndex (); + scanResult.HostIndex = new AddinHostIndex(GetAddinHostIndex ()); } catch (Exception ex) { if (scanResult.CheckOnly) { @@ -1385,7 +1400,7 @@ namespace Mono.Addins.Database AddinScanFolderInfo folderInfo; bool res = ReadFolderInfo (monitor, file, out folderInfo); bool validForDomain = scanResult.Domain == null || folderInfo.Domain == GlobalDomain || folderInfo.Domain == scanResult.Domain; - if (!res || (validForDomain && !fs.DirectoryExists (folderInfo.Folder))) { + if (!res || (validForDomain && !fileSystemExtension.DirectoryExists (folderInfo.Folder))) { if (res) { // Folder has been deleted. Remove the add-ins it had. updater.UpdateDeletedAddins (monitor, folderInfo); @@ -1444,7 +1459,7 @@ namespace Mono.Addins.Database if (monitor.LogLevel > 1) monitor.Log ("Folders scan completed (" + (int) (DateTime.Now - tim).TotalMilliseconds + " ms)"); - SaveAddinHostIndex (); + SaveAddinHostIndex (scanResult); ResetCachedData (); if (!scanResult.ChangesFound) { @@ -1472,7 +1487,9 @@ namespace Mono.Addins.Database if (monitor.LogLevel > 1) monitor.Log ("Add-in relations analyzed (" + (int) (DateTime.Now - tim).TotalMilliseconds + " ms)"); - SaveAddinHostIndex (); + SaveAddinHostIndex (scanResult); + + hostIndex = scanResult.HostIndex.ToImmutableAddinHostIndex (); } public void ParseAddin (IProgressStatus progressStatus, string domain, string file, string outFile, bool inProcess) @@ -1768,25 +1785,25 @@ namespace Mono.Addins.Database return false; } } - - AddinHostIndex GetAddinHostIndex () + + ImmutableAddinHostIndex GetAddinHostIndex () { if (hostIndex != null) return hostIndex; using (fileDatabase.LockRead ()) { if (fileDatabase.Exists (HostIndexFile)) - hostIndex = AddinHostIndex.Read (fileDatabase, HostIndexFile); + hostIndex = AddinHostIndex.ReadAsImmutable (fileDatabase, HostIndexFile); else - hostIndex = new AddinHostIndex (); + hostIndex = new ImmutableAddinHostIndex (); } return hostIndex; } - void SaveAddinHostIndex () + void SaveAddinHostIndex (AddinScanResult scanResult) { - if (hostIndex != null) - hostIndex.Write (fileDatabase, HostIndexFile); + if (scanResult.HostIndex != null) + scanResult.HostIndex.Write (fileDatabase, HostIndexFile); } internal string GetUniqueAddinId (string file, string oldId, string ns, string version) @@ -1844,18 +1861,20 @@ namespace Mono.Addins.Database DatabaseConfiguration Configuration { get { if (config == null) { - using (fileDatabase.LockRead ()) { - if (fileDatabase.Exists (ConfigFile)) - config = DatabaseConfiguration.Read (ConfigFile); - else - config = DatabaseConfiguration.ReadAppConfig (); + lock (localLock) { + using (fileDatabase.LockRead ()) { + if (fileDatabase.Exists (ConfigFile)) + config = DatabaseConfiguration.Read (ConfigFile); + else + config = DatabaseConfiguration.ReadAppConfig (); + } } } return config; } } - void SaveConfiguration () + void SaveConfiguration (AddinDatabaseTransaction dbTransaction) { if (config != null) { using (fileDatabase.LockWrite ()) { @@ -1996,7 +2015,38 @@ namespace Mono.Addins.Database list.Add (desc); } } - + + class AddinDatabaseTransaction : IDisposable + { + readonly AddinDatabase addinDatabase; + readonly object localLock; + AddinEngineTransaction addinEngineTransaction; + bool addinEngineTransactionStarted; + + public AddinDatabaseTransaction (AddinDatabase addinDatabase, object localLock, AddinEngineTransaction addinEngineTransaction) + { + this.addinDatabase = addinDatabase; + this.localLock = localLock; + this.addinEngineTransaction = addinEngineTransaction; + Monitor.Enter (localLock); + } + + public AddinEngineTransaction GetAddinEngineTransaction() + { + if (addinEngineTransaction != null) + return addinEngineTransaction; + addinEngineTransactionStarted = true; + return addinEngineTransaction = addinDatabase.AddinEngine.BeginEngineTransaction(); + } + + public void Dispose () + { + Monitor.Exit(localLock); + if (addinEngineTransactionStarted) + addinEngineTransaction.Dispose(); + } + } + // Keep in sync with AddinSearchFlags [Flags] enum AddinSearchFlagsInternal diff --git a/Mono.Addins/Mono.Addins.Database/AddinHostIndex.cs b/Mono.Addins/Mono.Addins.Database/AddinHostIndex.cs index 7919497..571deda 100644 --- a/Mono.Addins/Mono.Addins.Database/AddinHostIndex.cs +++ b/Mono.Addins/Mono.Addins.Database/AddinHostIndex.cs @@ -31,26 +31,46 @@ using System; using System.Collections; using Mono.Addins.Serialization; using System.IO; +using System.Collections.Generic; namespace Mono.Addins.Database { class AddinHostIndex: IBinaryXmlElement { static BinaryXmlTypeMap typeMap = new BinaryXmlTypeMap (typeof(AddinHostIndex)); - - Hashtable index = new Hashtable (); - + + Dictionary<string, string> index; + + public AddinHostIndex () + { + index = new Dictionary<string, string> (); + } + + public AddinHostIndex (ImmutableAddinHostIndex immutableIndex) + { + this.index = immutableIndex.ToDictionary(); + } + + public ImmutableAddinHostIndex ToImmutableAddinHostIndex () + { + return new ImmutableAddinHostIndex (new Dictionary<string, string> (index)); + } + public void RegisterAssembly (string assemblyLocation, string addinId, string addinLocation, string domain) { assemblyLocation = NormalizeFileName (assemblyLocation); index [Path.GetFullPath (assemblyLocation)] = addinId + " " + addinLocation + " " + domain; } - + public bool GetAddinForAssembly (string assemblyLocation, out string addinId, out string addinLocation, out string domain) { + return LookupAddinForAssembly (index, assemblyLocation, out addinId, out addinLocation, out domain); + } + + internal static bool LookupAddinForAssembly (Dictionary<string, string> index, string assemblyLocation, out string addinId, out string addinLocation, out string domain) + { assemblyLocation = NormalizeFileName (assemblyLocation); - string s = index [Path.GetFullPath (assemblyLocation)] as string; - if (s == null) { + if (!index.TryGetValue(Path.GetFullPath (assemblyLocation), out var s)) { addinId = null; addinLocation = null; domain = null; @@ -70,7 +90,7 @@ namespace Mono.Addins.Database { string loc = addinId + " " + Path.GetFullPath (addinLocation) + " "; ArrayList todelete = new ArrayList (); - foreach (DictionaryEntry e in index) { + foreach (var e in index) { if (((string)e.Value).StartsWith (loc)) todelete.Add (e.Key); } @@ -82,7 +102,13 @@ namespace Mono.Addins.Database { return (AddinHostIndex) fileDatabase.ReadObject (file, typeMap); } - + + public static ImmutableAddinHostIndex ReadAsImmutable (FileDatabase fileDatabase, string file) + { + var hostIndex = (AddinHostIndex)fileDatabase.ReadObject (file, typeMap); + return new ImmutableAddinHostIndex (hostIndex.index); + } + public void Write (FileDatabase fileDatabase, string file) { fileDatabase.WriteObject (file, this, typeMap); @@ -98,7 +124,7 @@ namespace Mono.Addins.Database reader.ReadValue ("index", index); } - string NormalizeFileName (string name) + internal static string NormalizeFileName (string name) { if (Util.IsWindows) return name.ToLower (); @@ -106,4 +132,28 @@ namespace Mono.Addins.Database return name; } } + + class ImmutableAddinHostIndex + { + Dictionary<string, string> index; + + public ImmutableAddinHostIndex () : this (new Dictionary<string, string> ()) + { + } + + public ImmutableAddinHostIndex (Dictionary<string, string> index) + { + this.index = index; + } + + public bool GetAddinForAssembly (string assemblyLocation, out string addinId, out string addinLocation, out string domain) + { + return AddinHostIndex.LookupAddinForAssembly (index, assemblyLocation, out addinId, out addinLocation, out domain); + } + + public Dictionary<string, string> ToDictionary () + { + return new Dictionary<string, string> (index); + } + } } diff --git a/Mono.Addins/Mono.Addins.Database/DatabaseConfiguration.cs b/Mono.Addins/Mono.Addins.Database/DatabaseConfiguration.cs index 14d8840..fa208d0 100644 --- a/Mono.Addins/Mono.Addins.Database/DatabaseConfiguration.cs +++ b/Mono.Addins/Mono.Addins.Database/DatabaseConfiguration.cs @@ -33,25 +33,64 @@ using System.IO; using System.Collections; using System.Collections.Generic; using System.Xml; +using System.Collections.Immutable; namespace Mono.Addins.Database { internal class DatabaseConfiguration { - Dictionary<string,AddinStatus> addinStatus = new Dictionary<string,AddinStatus> (); + ImmutableDictionary<string, AddinStatus> addinStatus = ImmutableDictionary<string, AddinStatus>.Empty; internal class AddinStatus { - public AddinStatus (string addinId) + public AddinStatus (string addinId, bool configEnabled = false, bool? sessionEnabled = null, bool uninstalled = false, ImmutableArray<string> files = default) { this.AddinId = addinId; + ConfigEnabled = configEnabled; + SessionEnabled = sessionEnabled; + Uninstalled = uninstalled; + if (files.IsDefault) + Files = ImmutableArray<string>.Empty; + else + Files = files; } - public string AddinId; - public bool ConfigEnabled; - public bool? SessionEnabled; - public bool Uninstalled; - public List<string> Files; + AddinStatus Copy () + { + return new AddinStatus (AddinId, + configEnabled: ConfigEnabled, + sessionEnabled: SessionEnabled, + uninstalled: Uninstalled, + files: Files + ); + } + + public AddinStatus AsEnabled (bool enabled, bool sessionOnly) + { + var copy = Copy (); + if (sessionOnly) + copy.SessionEnabled = enabled; + else { + copy.ConfigEnabled = enabled; + copy.SessionEnabled = null; + } + return copy; + } + + public AddinStatus AsUninstalled () + { + var copy = Copy (); + copy.ConfigEnabled = false; + copy.Uninstalled = true; + return copy; + } + + public string AddinId { get; private set;} + public bool ConfigEnabled { get; private set; } + public bool? SessionEnabled { get; private set; } + public bool Uninstalled { get; private set; } + public ImmutableArray<string> Files { get; private set; } + public bool Enabled { get { return SessionEnabled ?? ConfigEnabled; } } @@ -63,56 +102,51 @@ namespace Mono.Addins.Database AddinStatus s; + var status = addinStatus; + // If the add-in is globaly disabled, it is disabled no matter what the version specific status is - if (addinStatus.TryGetValue (addinName, out s)) { + if (status.TryGetValue (addinName, out s)) { if (!s.Enabled) return false; } - if (addinStatus.TryGetValue (addinId, out s)) - return s.Enabled && !IsRegisteredForUninstall (addinId); + if (status.TryGetValue (addinId, out s)) + return s.Enabled && !s.Uninstalled; else return defaultValue; } - public void SetEnabled (string addinId, bool enabled, bool defaultValue, bool exactVersionMatch, bool onlyForTheSession = false) + public void SetEnabled (AddinDatabaseTransaction transaction, string addinId, bool enabled, bool defaultValue, bool exactVersionMatch, bool onlyForTheSession = false) { if (IsRegisteredForUninstall (addinId)) return; var addinName = exactVersionMatch ? addinId : Addin.GetIdName (addinId); - AddinStatus s; - addinStatus.TryGetValue (addinName, out s); - - if (s == null) - s = addinStatus [addinName] = new AddinStatus (addinName); - if (onlyForTheSession) - s.SessionEnabled = enabled; - else { - s.ConfigEnabled = enabled; - s.SessionEnabled = null; - } + if (!addinStatus.TryGetValue (addinName, out var s)) + s = new AddinStatus (addinName); + + s = s.AsEnabled (enabled, onlyForTheSession); + addinStatus = addinStatus.SetItem (addinName, s); // If enabling a specific version of an add-in, make sure the add-in is enabled as a whole if (enabled && exactVersionMatch) - SetEnabled (addinId, true, defaultValue, false, onlyForTheSession); + SetEnabled (transaction, addinId, true, defaultValue, false, onlyForTheSession); } - public void RegisterForUninstall (string addinId, IEnumerable<string> files) + public void RegisterForUninstall (AddinDatabaseTransaction transaction, string addinId, IEnumerable<string> files) { AddinStatus s; if (!addinStatus.TryGetValue (addinId, out s)) - s = addinStatus [addinId] = new AddinStatus (addinId); + s = new AddinStatus (addinId); - s.ConfigEnabled = false; - s.Uninstalled = true; - s.Files = new List<string> (files); + s = s.AsUninstalled (); + addinStatus = addinStatus.SetItem (addinId, s); } - - public void UnregisterForUninstall (string addinId) + + public void UnregisterForUninstall (AddinDatabaseTransaction transaction, string addinId) { - addinStatus.Remove (addinId); + addinStatus = addinStatus.Remove (addinId); } public bool IsRegisteredForUninstall (string addinId) @@ -143,8 +177,7 @@ namespace Mono.Addins.Database return config; // Overwrite app config values with user config values - foreach (var entry in config.addinStatus) - appConfig.addinStatus [entry.Key] = entry.Value; + appConfig.addinStatus = appConfig.addinStatus.SetItems (config.addinStatus); return appConfig; } @@ -170,25 +203,29 @@ namespace Mono.Addins.Database XmlElement disabledElem = (XmlElement) doc.DocumentElement.SelectSingleNode ("DisabledAddins"); if (disabledElem != null) { // For back compatibility - foreach (XmlElement elem in disabledElem.SelectNodes ("Addin")) - config.SetEnabled (elem.InnerText, false, true, false); + var dictionary = ImmutableDictionary.CreateBuilder<string, AddinStatus> (); + foreach (XmlElement elem in disabledElem.SelectNodes ("Addin")) { + AddinStatus status = new AddinStatus (Addin.GetIdName (elem.GetAttribute ("id")), configEnabled: false); + dictionary [status.AddinId] = status; + } + config.addinStatus = dictionary.ToImmutable (); return config; } - + XmlElement statusElem = (XmlElement) doc.DocumentElement.SelectSingleNode ("AddinStatus"); if (statusElem != null) { + var dictionary = ImmutableDictionary.CreateBuilder<string, AddinStatus> (); foreach (XmlElement elem in statusElem.SelectNodes ("Addin")) { - AddinStatus status = new AddinStatus (elem.GetAttribute ("id")); string senabled = elem.GetAttribute ("enabled"); - status.ConfigEnabled = senabled.Length == 0 || senabled == "True"; - status.Uninstalled = elem.GetAttribute ("uninstalled") == "True"; - config.addinStatus [status.AddinId] = status; - foreach (XmlElement fileElem in elem.SelectNodes ("File")) { - if (status.Files == null) - status.Files = new List<string> (); - status.Files.Add (fileElem.InnerText); - } + + AddinStatus status = new AddinStatus (elem.GetAttribute ("id"), + configEnabled: senabled.Length == 0 || senabled == "True", + uninstalled: elem.GetAttribute ("uninstalled") == "True", + files: elem.SelectNodes ("File").OfType<XmlElement> ().Select (fileElem => fileElem.InnerText).ToImmutableArray () + ); + dictionary [status.AddinId] = status; } + config.addinStatus = dictionary.ToImmutable (); } return config; } @@ -208,7 +245,7 @@ namespace Mono.Addins.Database tw.WriteAttributeString ("enabled", e.ConfigEnabled.ToString ()); if (e.Uninstalled) tw.WriteAttributeString ("uninstalled", "True"); - if (e.Files != null && e.Files.Count > 0) { + if (e.Files.Length > 0) { foreach (var f in e.Files) tw.WriteElementString ("File", f); } diff --git a/Mono.Addins/Mono.Addins.csproj b/Mono.Addins/Mono.Addins.csproj index 9bdd5d0..73bf194 100644 --- a/Mono.Addins/Mono.Addins.csproj +++ b/Mono.Addins/Mono.Addins.csproj @@ -20,6 +20,7 @@ <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/> <PackageReference Include="System.Reflection.Emit" Version="4.7.0" /> + <PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> </ItemGroup> <ItemGroup> <Compile Remove="CustomConditionAttribute.cs" /> diff --git a/Mono.Addins/Mono.Addins/Addin.cs b/Mono.Addins/Mono.Addins/Addin.cs index 47f9917..792eb84 100644 --- a/Mono.Addins/Mono.Addins/Addin.cs +++ b/Mono.Addins/Mono.Addins/Addin.cs @@ -136,14 +136,16 @@ namespace Mono.Addins internal AddinInfo AddinInfo { get { - if (addin == null) { + var addinInfo = addin; + + if (addinInfo == null) { try { - addin = AddinInfo.ReadFromDescription (Description); + addinInfo = addin = AddinInfo.ReadFromDescription (Description); } catch (Exception ex) { throw new InvalidOperationException ("Could not read add-in file: " + database.GetDescriptionPath (domain, id), ex); } } - return addin; + return addinInfo; } } @@ -235,17 +237,16 @@ namespace Mono.Addins /// </summary> public AddinDescription Description { get { - if (desc != null) { - AddinDescription d = desc.Target as AddinDescription; - if (d != null) - return d; + var addinDescription = (AddinDescription) desc?.Target; + if (addinDescription != null) { + return addinDescription; } var configFile = database.GetDescriptionPath (domain, id); - AddinDescription m; - database.ReadAddinDescription (new ConsoleProgressStatus (true), configFile, out m); - if (m == null) { + database.ReadAddinDescription (new ConsoleProgressStatus (true), configFile, out addinDescription); + + if (addinDescription == null) { try { if (File.Exists (configFile)) { // The file is corrupted. Remove it. @@ -257,14 +258,14 @@ namespace Mono.Addins throw new InvalidOperationException ("Could not read add-in description"); } if (addin == null) { - addin = AddinInfo.ReadFromDescription (m); - sourceFile = m.AddinFile; + addin = AddinInfo.ReadFromDescription (addinDescription); + sourceFile = addinDescription.AddinFile; } - SetIsUserAddin (m); + SetIsUserAddin (addinDescription); if (!isUserAddin.Value) - m.Flags |= AddinFlags.CantUninstall; - desc = new WeakReference (m); - return m; + addinDescription.Flags |= AddinFlags.CantUninstall; + desc = new WeakReference (addinDescription); + return addinDescription; } } diff --git a/Mono.Addins/Mono.Addins/AddinEngine.cs b/Mono.Addins/Mono.Addins/AddinEngine.cs index 9b361a0..3bfe794 100644 --- a/Mono.Addins/Mono.Addins/AddinEngine.cs +++ b/Mono.Addins/Mono.Addins/AddinEngine.cs @@ -37,7 +37,7 @@ using Mono.Addins.Description; using Mono.Addins.Database; using Mono.Addins.Localization; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Immutable; namespace Mono.Addins { @@ -60,10 +60,35 @@ namespace Mono.Addins IAddinInstaller installer; bool checkAssemblyLoadConflicts; - Dictionary<string,RuntimeAddin> loadedAddins = new Dictionary<string,RuntimeAddin> (); - Dictionary<string,ExtensionNodeSet> nodeSets = new Dictionary<string, ExtensionNodeSet> (); - Dictionary<string,string> autoExtensionTypes = new Dictionary<string, string> (); - Dictionary<string, RuntimeAddin> assemblyResolvePaths = new Dictionary<string, RuntimeAddin>(); + + // This collection is only used during a transaction, so it doesn't need to be immutable + Dictionary<string, ExtensionNodeSet> nodeSets = new Dictionary<string, ExtensionNodeSet>(); + + AddinEngineSnapshot currentSnapshot = new AddinEngineSnapshot(); + + internal class AddinEngineSnapshot: ExtensionContextSnapshot + { + public ImmutableDictionary<string, RuntimeAddin> LoadedAddins; + public ImmutableDictionary<string, string> AutoExtensionTypes; + public ImmutableDictionary<string, RuntimeAddin> AssemblyResolvePaths; + + public AddinEngineSnapshot() + { + LoadedAddins = ImmutableDictionary<string, RuntimeAddin>.Empty; + AutoExtensionTypes = ImmutableDictionary<string, string>.Empty; + AssemblyResolvePaths = ImmutableDictionary<string, RuntimeAddin>.Empty; + } + + public override void CopyFrom(ExtensionContextSnapshot other) + { + base.CopyFrom(other); + var context = (AddinEngineSnapshot)other; + LoadedAddins = context.LoadedAddins; + AutoExtensionTypes = context.AutoExtensionTypes; + AssemblyResolvePaths = context.AssemblyResolvePaths; + } + } + AddinLocalizer defaultLocalizer; IProgressStatus defaultProgressStatus = new ConsoleProgressStatus (false); @@ -93,7 +118,28 @@ namespace Mono.Addins public AddinEngine () { } - + + internal override ExtensionContextSnapshot CreateSnapshot () + { + return new AddinEngineSnapshot(); + } + + internal override void SetSnapshot (ExtensionContextSnapshot newSnapshot) + { + currentSnapshot = (AddinEngineSnapshot)newSnapshot; + base.SetSnapshot(newSnapshot); + } + + internal override ExtensionContextTransaction BeginTransaction() + { + return new AddinEngineTransaction(this); + } + + internal AddinEngineTransaction BeginEngineTransaction() + { + return (AddinEngineTransaction)BeginTransaction(); + } + /// <summary> /// Initializes the add-in engine /// </summary> @@ -246,14 +292,16 @@ namespace Mono.Addins if (string.IsNullOrEmpty (configDir)) registry = AddinRegistry.GetGlobalRegistry (this, startupDirectory); else - registry = new AddinRegistry (this, configDir, startupDirectory, addinsDir, databaseDir); + registry = new AddinRegistry (this, configDir, startupDirectory, addinsDir, databaseDir, null); + + using var transaction = BeginEngineTransaction(); if ((asmFile != null && registry.CreateHostAddinsFile (asmFile)) || registry.UnknownDomain) - registry.Update (new ConsoleProgressStatus (false)); + registry.Update (new ConsoleProgressStatus (false), transaction); initialized = true; - ActivateRoots (); + ActivateRoots (transaction); OnAssemblyLoaded (null, null); AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler (OnAssemblyLoaded); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve; @@ -262,25 +310,23 @@ namespace Mono.Addins Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args) { - lock (LocalLock) { - string assemblyName = args.Name; + string assemblyName = args.Name; - if (assemblyResolvePaths.TryGetValue (assemblyName, out var inAddin)) { - if (inAddin.TryGetAssembly (assemblyName, out var assembly)) - return assembly; + if (currentSnapshot.AssemblyResolvePaths.TryGetValue (assemblyName, out var inAddin)) { + if (inAddin.TryGetAssembly (assemblyName, out var assembly)) + return assembly; - int index = inAddin.Module.AssemblyNames.IndexOf (assemblyName); - if (index != -1) { - var path = inAddin.GetFilePath (inAddin.Module.Assemblies[index]); - assembly = Assembly.LoadFrom (path); - inAddin.RegisterAssemblyLoad (assemblyName, assembly); + int index = inAddin.Module.AssemblyNames.IndexOf (assemblyName); + if (index != -1) { + var path = inAddin.GetFilePath (inAddin.Module.Assemblies[index]); + assembly = Assembly.LoadFrom (path); + inAddin.RegisterAssemblyLoad (assemblyName, assembly); - return assembly; - } + return assembly; } - - return null; } + + return null; } /// <summary> @@ -292,7 +338,6 @@ namespace Mono.Addins initialized = false; AppDomain.CurrentDomain.AssemblyLoad -= new AssemblyLoadEventHandler (OnAssemblyLoaded); AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomainAssemblyResolve; - loadedAddins = new Dictionary<string, RuntimeAddin>(); registry.Dispose (); registry = null; startupDirectory = null; @@ -395,7 +440,7 @@ namespace Mono.Addins { ValidateAddinRoots (); - assemblyResolvePaths.TryGetValue(asm.FullName, out var ad); + currentSnapshot.AssemblyResolvePaths.TryGetValue(asm.FullName, out var ad); return ad; } @@ -460,44 +505,76 @@ namespace Mono.Addins /// </returns> public bool IsAddinLoaded (string id) { - CheckInitialized (); - ValidateAddinRoots (); - return loadedAddins.ContainsKey (Addin.GetIdName (id)); + return IsAddinLoaded(null, id); } - + + bool IsAddinLoaded(AddinEngineTransaction transaction, string id) + { + CheckInitialized(); + ValidateAddinRoots(); + if (transaction?.Context == this) + return transaction.AddinEngineSnapshot.LoadedAddins.ContainsKey(Addin.GetIdName(id)); + else + return currentSnapshot.LoadedAddins.ContainsKey(Addin.GetIdName(id)); + } + internal RuntimeAddin GetAddin (string id) { ValidateAddinRoots (); RuntimeAddin a; - loadedAddins.TryGetValue (Addin.GetIdName (id), out a); + currentSnapshot.LoadedAddins.TryGetValue (Addin.GetIdName (id), out a); return a; } - - internal void ActivateAddin (string id) + + internal RuntimeAddin GetAddin(AddinEngineTransaction transaction, string id) { - ActivateAddinExtensions (id); + ValidateAddinRoots(); + RuntimeAddin a; + + // If a transaction is provided, try using the snapshot from that transaction, + // but this is only possible if the transaction was created for this context + + if (transaction?.Context == this) + transaction.AddinEngineSnapshot.LoadedAddins.TryGetValue(Addin.GetIdName(id), out a); + else + currentSnapshot.LoadedAddins.TryGetValue(Addin.GetIdName(id), out a); + return a; } - - internal void UnloadAddin (string id) + + internal RuntimeAddin GetOrLoadAddin(string id, bool throwExceptions) { - RemoveAddinExtensions (id); + ValidateAddinRoots(); + RuntimeAddin a; + currentSnapshot.LoadedAddins.TryGetValue(Addin.GetIdName(id), out a); + if (a == null) + { + using var tr = BeginEngineTransaction(); + LoadAddin(tr, null, id, throwExceptions); + tr.AddinEngineSnapshot.LoadedAddins.TryGetValue(Addin.GetIdName(id), out a); + } + return a; + } + + internal void ActivateAddin (ExtensionContextTransaction transaction, string id) + { + ActivateAddinExtensions (transaction, id); + } + + internal void UnloadAddin (AddinEngineTransaction transaction, string id) + { + RemoveAddinExtensions (transaction, id); - RuntimeAddin addin = GetAddin (id); + RuntimeAddin addin = GetAddin (transaction, id); if (addin != null) { - addin.UnloadExtensions (); - lock (LocalLock) { - var loadedAddinsCopy = new Dictionary<string,RuntimeAddin> (loadedAddins); - loadedAddinsCopy.Remove (Addin.GetIdName (id)); - loadedAddins = loadedAddinsCopy; - - foreach (var asm in addin.Module.AssemblyNames) { - assemblyResolvePaths.Remove (asm); - } + addin.UnloadExtensions (transaction); + transaction.AddinEngineSnapshot.LoadedAddins = transaction.AddinEngineSnapshot.LoadedAddins.Remove (Addin.GetIdName (id)); + foreach (var asm in addin.Module.AssemblyNames) { + transaction.UnregisterAssemblyResolvePaths (asm); } - ReportAddinUnload (id); + transaction.ReportAddinUnload (id); } } - + /// <summary> /// Forces the loading of an add-in. /// </summary> @@ -516,52 +593,53 @@ namespace Mono.Addins public void LoadAddin (IProgressStatus statusMonitor, string id) { CheckInitialized (); - if (LoadAddin (statusMonitor, id, true)) { - var adn = GetAddin (id); + using var transaction = BeginEngineTransaction(); + if (LoadAddin (transaction, statusMonitor, id, true)) { + var adn = GetAddin (transaction, id); adn.EnsureAssembliesLoaded (); } } - internal bool LoadAddin (IProgressStatus statusMonitor, string id, bool throwExceptions) + internal bool LoadAddin (AddinEngineTransaction transaction, IProgressStatus statusMonitor, string id, bool throwExceptions) { try { - lock (LocalLock) { - if (IsAddinLoaded (id)) - return true; - - if (!Registry.IsAddinEnabled (id)) { - string msg = GettextCatalog.GetString ("Disabled add-ins can't be loaded."); - ReportError (msg, id, null, false); - if (throwExceptions) - throw new InvalidOperationException (msg); - return false; - } + if (IsAddinLoaded (transaction, id)) + return true; - var addins = new List<Addin> (); - Stack depCheck = new Stack (); - ResolveLoadDependencies (addins, depCheck, id, false); - addins.Reverse (); - - if (statusMonitor != null) - statusMonitor.SetMessage ("Loading Addins"); + if (!Registry.IsAddinEnabled (id)) { + string msg = GettextCatalog.GetString ("Disabled add-ins can't be loaded."); + ReportError (msg, id, null, false); + if (throwExceptions) + throw new InvalidOperationException (msg); + return false; + } + + var addins = new List<Addin> (); + Stack depCheck = new Stack (); + ResolveLoadDependencies (transaction, addins, depCheck, id, false); + addins.Reverse (); - for (int n=0; n<addins.Count; n++) { - + if (statusMonitor != null) + statusMonitor.SetMessage ("Loading Addins"); + + if (addins.Count > 0) { + for (int n = 0; n < addins.Count; n++) { + if (statusMonitor != null) - statusMonitor.SetProgress ((double) n / (double)addins.Count); - + statusMonitor.SetProgress ((double)n / (double)addins.Count); + Addin iad = addins [n]; - if (IsAddinLoaded (iad.Id)) + if (IsAddinLoaded (transaction, iad.Id)) continue; if (statusMonitor != null) - statusMonitor.SetMessage (string.Format(GettextCatalog.GetString("Loading {0} add-in"), iad.Id)); - - if (!InsertAddin (statusMonitor, iad)) + statusMonitor.SetMessage (string.Format (GettextCatalog.GetString ("Loading {0} add-in"), iad.Id)); + + if (!InsertAddin (transaction, statusMonitor, iad)) return false; } - return true; } + return true; } catch (Exception ex) { ReportError ("Add-in could not be loaded: " + ex.Message, id, ex, false); @@ -573,43 +651,38 @@ namespace Mono.Addins } } - internal override void ResetCachedData () + internal override void OnResetCachedData (ExtensionContextTransaction transaction) { - foreach (RuntimeAddin ad in loadedAddins.Values) + foreach (RuntimeAddin ad in transaction.AddinEngineSnapshot.LoadedAddins.Values) ad.Addin.ResetCachedData (); - base.ResetCachedData (); + base.OnResetCachedData (transaction); } - bool InsertAddin (IProgressStatus statusMonitor, Addin iad) + bool InsertAddin (AddinEngineTransaction transaction, IProgressStatus statusMonitor, Addin iad) { try { - RuntimeAddin runtimeAddin = new RuntimeAddin (this); + RuntimeAddin runtimeAddin = new RuntimeAddin (this, iad); + AddinDescription description = iad.Description; + + RegisterAssemblyResolvePaths (transaction, runtimeAddin, iad.Description.MainModule); - RegisterAssemblyResolvePaths (runtimeAddin, iad.Description.MainModule); - - // Read the config file and load the add-in assemblies - AddinDescription description = runtimeAddin.Load (iad); - // Register the add-in - var loadedAddinsCopy = new Dictionary<string,RuntimeAddin> (loadedAddins); - loadedAddinsCopy [Addin.GetIdName (runtimeAddin.Id)] = runtimeAddin; - loadedAddins = loadedAddinsCopy; + transaction.AddinEngineSnapshot.LoadedAddins = transaction.AddinEngineSnapshot.LoadedAddins.SetItem(Addin.GetIdName (runtimeAddin.Id), runtimeAddin); if (!AddinDatabase.RunningSetupProcess) { // Load the extension points and other addin data - RegisterNodeSets (iad.Id, description.ExtensionNodeSets); + RegisterNodeSets (transaction, iad.Id, description.ExtensionNodeSets); foreach (ConditionTypeDescription cond in description.ConditionTypes) - RegisterCondition (cond.Id, runtimeAddin, cond.TypeName); + RegisterCondition (transaction, cond.Id, runtimeAddin, cond.TypeName); } foreach (ExtensionPoint ep in description.ExtensionPoints) - InsertExtensionPoint (runtimeAddin, ep); + InsertExtensionPoint (transaction, runtimeAddin, ep); // Fire loaded event - NotifyAddinLoaded (runtimeAddin); - ReportAddinLoad (runtimeAddin.Id); + transaction.ReportAddinLoad (runtimeAddin); return true; } catch (Exception ex) { @@ -620,29 +693,27 @@ namespace Mono.Addins } } - void RegisterAssemblyResolvePaths (RuntimeAddin addin, ModuleDescription description) + void RegisterAssemblyResolvePaths (AddinEngineTransaction transaction, RuntimeAddin addin, ModuleDescription description) { - lock (LocalLock) { - foreach (var asm in description.AssemblyNames) { - //Debug.Assert(assemblyResolvePaths[asm] == addin); This does not look right. Not called in old project since DEBUG symbol is not defined. - assemblyResolvePaths [asm] = addin; - } + foreach (var asm in description.AssemblyNames) { + //Debug.Assert(assemblyResolvePaths[asm] == addin); This does not look right. Not called in old project since DEBUG symbol is not defined. + transaction.RegisterAssemblyResolvePaths (asm, addin); } } - - internal void InsertExtensionPoint (RuntimeAddin addin, ExtensionPoint ep) - { - CreateExtensionPoint (ep); + + internal void InsertExtensionPoint (AddinEngineTransaction transaction, RuntimeAddin addin, ExtensionPoint ep) + { + CreateExtensionPoint (transaction, ep); foreach (ExtensionNodeType nt in ep.NodeSet.NodeTypes) { if (nt.ObjectTypeName.Length > 0) { - RegisterAutoTypeExtensionPoint (nt.ObjectTypeName, ep.Path); + RegisterAutoTypeExtensionPoint (transaction, nt.ObjectTypeName, ep.Path); } } } - bool ResolveLoadDependencies (List<Addin> addins, Stack depCheck, string id, bool optional) + bool ResolveLoadDependencies (AddinEngineTransaction transaction, List<Addin> addins, Stack depCheck, string id, bool optional) { - if (IsAddinLoaded (id)) + if (IsAddinLoaded (transaction, id)) return true; if (depCheck.Contains (id)) @@ -671,7 +742,7 @@ namespace Mono.Addins if (adep != null) { try { string adepid = Addin.GetFullId (iad.AddinInfo.Namespace, adep.AddinId, adep.Version); - ResolveLoadDependencies (addins, depCheck, adepid, false); + ResolveLoadDependencies (transaction, addins, depCheck, adepid, false); } catch (MissingDependencyException) { if (optional) return false; @@ -686,7 +757,7 @@ namespace Mono.Addins AddinDependency adep = dep as AddinDependency; if (adep != null) { string adepid = Addin.GetFullId (iad.Namespace, adep.AddinId, adep.Version); - if (!ResolveLoadDependencies (addins, depCheck, adepid, true)) + if (!ResolveLoadDependencies (transaction, addins, depCheck, adepid, true)) return false; } } @@ -697,38 +768,27 @@ namespace Mono.Addins return true; } - void RegisterNodeSets (string addinId, ExtensionNodeSetCollection nsets) + void RegisterNodeSets (ExtensionContextTransaction transaction, string addinId, ExtensionNodeSetCollection nsets) { - lock (LocalLock) { - var nodeSetsCopy = new Dictionary<string,ExtensionNodeSet> (nodeSets); - foreach (ExtensionNodeSet nset in nsets) { - nset.SourceAddinId = addinId; - nodeSetsCopy [nset.Id] = nset; - } - nodeSets = nodeSetsCopy; + foreach (ExtensionNodeSet nset in nsets) + { + nset.SourceAddinId = addinId; + nodeSets[nset.Id] = nset; } } - internal void UnregisterAddinNodeSets (string addinId) + internal void UnregisterAddinNodeSets (ExtensionContextTransaction transaction, string addinId) { - lock (LocalLock) { - var nodeSetsCopy = new Dictionary<string,ExtensionNodeSet> (nodeSets); - foreach (var nset in nodeSetsCopy.Values.Where (n => n.SourceAddinId == addinId).ToArray ()) - nodeSetsCopy.Remove (nset.Id); - nodeSets = nodeSetsCopy; - } + foreach (var nset in nodeSets.Values.Where(n => n.SourceAddinId == addinId).ToList()) + nodeSets.Remove(nset.Id); } - internal string GetNodeTypeAddin (ExtensionNodeSet nset, string type, string callingAddinId) + internal ExtensionNodeType FindType (ExtensionContextTransaction transaction, ExtensionNodeSet nset, string name, string callingAddinId) { - ExtensionNodeType nt = FindType (nset, type, callingAddinId); - if (nt != null) - return nt.AddinId; - else - return null; + return FindType (nodeSets, nset, name, callingAddinId); } - - internal ExtensionNodeType FindType (ExtensionNodeSet nset, string name, string callingAddinId) + + ExtensionNodeType FindType (Dictionary<string, ExtensionNodeSet> sets, ExtensionNodeSet nset, string name, string callingAddinId) { if (nset == null) return null; @@ -740,34 +800,37 @@ namespace Mono.Addins foreach (string ns in nset.NodeSets) { ExtensionNodeSet regSet; - if (!nodeSets.TryGetValue (ns, out regSet)) { + if (!sets.TryGetValue (ns, out regSet)) { ReportError ("Unknown node set: " + ns, callingAddinId, null, false); return null; } - ExtensionNodeType nt = FindType (regSet, name, callingAddinId); + ExtensionNodeType nt = FindType (sets, regSet, name, callingAddinId); if (nt != null) return nt; } return null; } - internal void RegisterAutoTypeExtensionPoint (string typeName, string path) + internal void RegisterAutoTypeExtensionPoint (AddinEngineTransaction transaction, string typeName, string path) { if (Util.TryParseTypeName (typeName, out var t, out var _)) typeName = t; - autoExtensionTypes [typeName] = path; + transaction.RegisterAutoTypeExtensionPoint (typeName, path); } - internal void UnregisterAutoTypeExtensionPoint (string typeName, string path) + internal void UnregisterAutoTypeExtensionPoint (AddinEngineTransaction transaction, string typeName, string path) { if (Util.TryParseTypeName (typeName, out var t, out var _)) typeName = t; - autoExtensionTypes.Remove (typeName); + transaction.UnregisterAutoTypeExtensionPoint (typeName); } - + + /// <summary> + /// Returns an extension point identified by a type + /// </summary> internal string GetAutoTypeExtensionPoint (Type type) { - autoExtensionTypes.TryGetValue (type.FullName, out var path); + currentSnapshot.AutoExtensionTypes.TryGetValue (type.FullName, out var path); return path; } @@ -792,20 +855,22 @@ namespace Mono.Addins } } if (copy != null) { + using var tr = BeginEngineTransaction(); foreach (Assembly asm in copy) - CheckHostAssembly (asm); + CheckHostAssembly (tr, asm); } } - internal void ActivateRoots () + internal void ActivateRoots (AddinEngineTransaction transaction) { lock (pendingRootChecks) pendingRootChecks.Clear (); + foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ()) - CheckHostAssembly (asm); + CheckHostAssembly (transaction, asm); } - void CheckHostAssembly (Assembly asm) + void CheckHostAssembly (AddinEngineTransaction transaction, Assembly asm) { if (AddinDatabase.RunningSetupProcess || asm is System.Reflection.Emit.AssemblyBuilder || asm.IsDynamic) return; @@ -832,7 +897,7 @@ namespace Mono.Addins ainfo = Registry.GetAddinForHostAssembly (asmFile); } - if (ainfo != null && !IsAddinLoaded (ainfo.Id)) { + if (ainfo != null && !IsAddinLoaded (transaction, ainfo.Id)) { AddinDescription adesc = null; try { adesc = ainfo.Description; @@ -843,12 +908,12 @@ namespace Mono.Addins // If the add-in has changed, update the add-in database. // We do it here because once loaded, add-in roots can't be // reloaded like regular add-ins. - Registry.Update (null); + Registry.Update (null, transaction); ainfo = Registry.GetAddinForHostAssembly (asmFile); if (ainfo == null) return; } - LoadAddin (null, ainfo.Id, false); + LoadAddin (transaction, null, ainfo.Id, false); } } @@ -885,41 +950,35 @@ namespace Mono.Addins } } - internal void ReportAddinLoad (string id) + internal void ReportAddinLoad (RuntimeAddin addin) { - var handler = AddinLoaded; - if (handler != null) { - try { - handler (null, new AddinEventArgs (id)); - } catch { - // Ignore subscriber exceptions - } + NotifyAddinLoaded (addin); + if (AddinLoaded != null) { + InvokeCallback(() => + { + AddinLoaded?.Invoke(this, new AddinEventArgs(addin.Id)); + },null); } } internal void ReportAddinUnload (string id) { - var handler = AddinUnloaded; - if (handler != null) { - try { - handler (null, new AddinEventArgs (id)); - } catch { - // Ignore subscriber exceptions - } + if (AddinUnloaded != null) { + InvokeCallback(() => + { + AddinUnloaded?.Invoke(this, new AddinEventArgs(id)); + },null); } } internal void ReportAddinAssembliesLoad (string id) { - var handler = AddinAssembliesLoaded; - if (handler != null) { - try { - handler (null, new AddinEventArgs (id)); - } catch { - // Ignore subscriber exceptions - } + if (AddinAssembliesLoaded != null) { + InvokeCallback(() => + { + AddinAssembliesLoaded?.Invoke(this, new AddinEventArgs(id)); + },null); } } } - } diff --git a/Mono.Addins/Mono.Addins/AddinInfo.cs b/Mono.Addins/Mono.Addins/AddinInfo.cs index b1125f7..eb8f127 100644 --- a/Mono.Addins/Mono.Addins/AddinInfo.cs +++ b/Mono.Addins/Mono.Addins/AddinInfo.cs @@ -32,6 +32,8 @@ using System.Collections; using System.Xml; using System.Xml.Serialization; using Mono.Addins.Description; +using System.Collections.Immutable; +using System.Linq; namespace Mono.Addins { @@ -49,14 +51,14 @@ namespace Mono.Addins string category = ""; bool defaultEnabled = true; bool isroot; - DependencyCollection dependencies; - DependencyCollection optionalDependencies; + ImmutableArray<Dependency> dependencies; + ImmutableArray<Dependency> optionalDependencies; AddinPropertyCollection properties; private AddinInfo () { - dependencies = new DependencyCollection (); - optionalDependencies = new DependencyCollection (); + dependencies = ImmutableArray<Dependency>.Empty; + optionalDependencies = ImmutableArray<Dependency>.Empty; } public string Id { @@ -65,17 +67,14 @@ namespace Mono.Addins public string LocalId { get { return id; } - set { id = value; } } public string Namespace { get { return namspace; } - set { namspace = value; } } public bool IsRoot { get { return isroot; } - set { isroot = value; } } public string Name { @@ -90,17 +89,14 @@ namespace Mono.Addins sid = sid.Substring (2); return Addin.GetFullId (namspace, sid, null); } - set { name = value; } } public string Version { get { return version; } - set { version = value; } } public string BaseVersion { get { return baseVersion; } - set { baseVersion = value; } } public string Author { @@ -110,7 +106,6 @@ namespace Mono.Addins return s; return author; } - set { author = value; } } public string Copyright { @@ -120,7 +115,6 @@ namespace Mono.Addins return s; return copyright; } - set { copyright = value; } } public string Url { @@ -130,7 +124,6 @@ namespace Mono.Addins return s; return url; } - set { url = value; } } public string Description { @@ -140,7 +133,6 @@ namespace Mono.Addins return s; return description; } - set { description = value; } } public string Category { @@ -150,19 +142,17 @@ namespace Mono.Addins return s; return category; } - set { category = value; } } public bool EnabledByDefault { get { return defaultEnabled; } - set { defaultEnabled = value; } } - public DependencyCollection Dependencies { + public ImmutableArray<Dependency> Dependencies { get { return dependencies; } } - public DependencyCollection OptionalDependencies { + public ImmutableArray<Dependency> OptionalDependencies { get { return optionalDependencies; } } @@ -185,14 +175,9 @@ namespace Mono.Addins info.baseVersion = description.CompatVersion; info.isroot = description.IsRoot; info.defaultEnabled = description.EnabledByDefault; - - foreach (Dependency dep in description.MainModule.Dependencies) - info.Dependencies.Add (dep); - - foreach (ModuleDescription mod in description.OptionalModules) { - foreach (Dependency dep in mod.Dependencies) - info.OptionalDependencies.Add (dep); - } + + info.dependencies = ImmutableArray<Dependency>.Empty.AddRange (description.MainModule.Dependencies); + info.optionalDependencies = ImmutableArray<Dependency>.Empty.AddRange (description.OptionalModules.SelectMany(module => module.Dependencies)); info.properties = description.Properties; return info; diff --git a/Mono.Addins/Mono.Addins/AddinManager.cs b/Mono.Addins/Mono.Addins/AddinManager.cs index b5bef3f..663e8fa 100644 --- a/Mono.Addins/Mono.Addins/AddinManager.cs +++ b/Mono.Addins/Mono.Addins/AddinManager.cs @@ -155,7 +155,8 @@ namespace Mono.Addins /// </summary> public static void Shutdown () { - AddinEngine.Shutdown (); + sessionService?.Shutdown (); + sessionService = null; } /// <summary> @@ -688,7 +689,7 @@ namespace Mono.Addins AddinEngine.CheckInitialized (); return AddinEngine.GetExtensionObjects<T> (path, reuseCachedInstance); } - + /// <summary> /// Extension change event. /// </summary> @@ -698,6 +699,9 @@ namespace Mono.Addins /// it does not provide information about what changed. Hosts subscribing to /// this event should get the new list of nodes using a query method such as /// AddinManager.GetExtensionNodes() and then update whatever needs to be updated. + /// + /// Threading information: the thread on which the event is raised is undefined. + /// Events in this class are guaranteed to be raised sequentially, never concurrently. /// </remarks> public static event ExtensionEventHandler ExtensionChanged { add { AddinEngine.CheckInitialized(); AddinEngine.ExtensionChanged += value; } @@ -720,6 +724,9 @@ namespace Mono.Addins /// (Add or Remove) and the extension node added or removed. /// /// NOTE: The handler will be called for all nodes existing in the path at the moment of registration. + /// + /// Threading information: the thread on which the handler is executed is undefined. + /// Handlers and events are guaranteed to be executed sequentially, never concurrently. /// </remarks> public static void AddExtensionNodeHandler (string path, ExtensionNodeEventHandler handler) { @@ -761,6 +768,9 @@ namespace Mono.Addins /// (Add or Remove) and the extension node added or removed. /// /// NOTE: The handler will be called for all nodes existing in the path at the moment of registration. + /// + /// Threading information: the thread on which the handler is executed is undefined. + /// Handlers and events are guaranteed to be executed sequentially, never concurrently. /// </remarks> public static void AddExtensionNodeHandler (Type instanceType, ExtensionNodeEventHandler handler) { @@ -782,35 +792,44 @@ namespace Mono.Addins AddinEngine.CheckInitialized (); AddinEngine.RemoveExtensionNodeHandler (instanceType, handler); } - + /// <summary> /// Add-in loading error event. /// </summary> /// <remarks> /// This event is fired when there is an error when loading the extension /// of an add-in, or any other kind of error that may happen when querying extension points. + /// + /// Threading information: the thread on which the event is raised is undefined. + /// Events in this class are guaranteed to be raised sequentially, never concurrently. /// </remarks> public static event AddinErrorEventHandler AddinLoadError { add { AddinEngine.AddinLoadError += value; } remove { AddinEngine.AddinLoadError -= value; } } - + /// <summary> /// Add-in loaded event. /// </summary> /// <remarks> /// Fired after loading an add-in in memory. + /// + /// Threading information: the thread on which the event is raised is undefined. + /// Events in this class are guaranteed to be raised sequentially, never concurrently. /// </remarks> public static event AddinEventHandler AddinLoaded { add { AddinEngine.AddinLoaded += value; } remove { AddinEngine.AddinLoaded -= value; } } - + /// <summary> /// Add-in unload event. /// </summary> /// <remarks> /// Fired when an add-in is unloaded from memory. It may happen an add-in is disabled or uninstalled. + /// + /// Threading information: the thread on which the event is raised is undefined. + /// Events in this class are guaranteed to be raised sequentially, never concurrently. /// </remarks> public static event AddinEventHandler AddinUnloaded { add { AddinEngine.AddinUnloaded += value; } @@ -822,6 +841,9 @@ namespace Mono.Addins /// </summary> /// <remarks> /// Fired when the add-in assemblies are loaded. + /// + /// Threading information: the thread on which the event is raised is undefined. + /// Events in this class are guaranteed to be raised sequentially, never concurrently. /// </remarks> public static event AddinEventHandler AddinAssembliesLoaded { add { AddinEngine.AddinAssembliesLoaded += value; } diff --git a/Mono.Addins/Mono.Addins/AddinRegistry.cs b/Mono.Addins/Mono.Addins/AddinRegistry.cs index 0e1ef2a..944d96e 100644 --- a/Mono.Addins/Mono.Addins/AddinRegistry.cs +++ b/Mono.Addins/Mono.Addins/AddinRegistry.cs @@ -67,13 +67,13 @@ namespace Mono.Addins /// </remarks> public class AddinRegistry: IDisposable { - AddinDatabase database; - StringCollection addinDirs; - string basePath; + readonly AddinDatabase database; + readonly string [] addinDirs; + readonly string basePath; + readonly string startupDirectory; + readonly string addinsDir; + readonly string databaseDir; string currentDomain; - string startupDirectory; - string addinsDir; - string databaseDir; /// <summary> /// Initializes a new instance. @@ -92,7 +92,7 @@ namespace Mono.Addins /// of the Environment.SpecialFolder enumeration can be used (always between square /// brackets) /// </remarks> - public AddinRegistry (string registryPath): this (null, registryPath, null, null, null) + public AddinRegistry (string registryPath): this (null, registryPath, null, null, null, null) { } @@ -116,7 +116,7 @@ namespace Mono.Addins /// of the Environment.SpecialFolder enumeration can be used (always between square /// brackets) /// </remarks> - public AddinRegistry (string registryPath, string startupDirectory): this (null, registryPath, startupDirectory, null, null) + public AddinRegistry (string registryPath, string startupDirectory): this (null, registryPath, startupDirectory, null, null, null) { } @@ -145,7 +145,7 @@ namespace Mono.Addins /// of the Environment.SpecialFolder enumeration can be used (always between square /// brackets) /// </remarks> - public AddinRegistry (string registryPath, string startupDirectory, string addinsDir): this (null, registryPath, startupDirectory, addinsDir, null) + public AddinRegistry (string registryPath, string startupDirectory, string addinsDir): this (null, registryPath, startupDirectory, addinsDir, null, null) { } @@ -179,11 +179,11 @@ namespace Mono.Addins /// of the Environment.SpecialFolder enumeration can be used (always between square /// brackets) /// </remarks> - public AddinRegistry (string registryPath, string startupDirectory, string addinsDir, string databaseDir): this (null, registryPath, startupDirectory, addinsDir, databaseDir) + public AddinRegistry (string registryPath, string startupDirectory, string addinsDir, string databaseDir): this (null, registryPath, startupDirectory, addinsDir, databaseDir, null) { } - internal AddinRegistry (AddinEngine engine, string registryPath, string startupDirectory, string addinsDir, string databaseDir) + internal AddinRegistry (AddinEngine engine, string registryPath, string startupDirectory, string addinsDir, string databaseDir, string additionalGlobalAddinDirectory) { basePath = Path.GetFullPath (Util.NormalizePath (registryPath)); @@ -208,8 +208,10 @@ namespace Mono.Addins // Look for add-ins in the hosts directory and in the default // addins directory - addinDirs = new StringCollection (); - addinDirs.Add (DefaultAddinsFolder); + if (additionalGlobalAddinDirectory != null) + addinDirs = new [] { DefaultAddinsFolder, additionalGlobalAddinDirectory }; + else + addinDirs = new [] { DefaultAddinsFolder }; // Initialize the database after all paths have been set database = new AddinDatabase (engine, this); @@ -239,15 +241,15 @@ namespace Mono.Addins internal static AddinRegistry GetGlobalRegistry (AddinEngine engine, string startupDirectory) { - AddinRegistry reg = new AddinRegistry (engine, GlobalRegistryPath, startupDirectory, null, null); string baseDir; if (Util.IsWindows) baseDir = Environment.GetFolderPath (Environment.SpecialFolder.CommonProgramFiles); else baseDir = "/etc"; - - reg.GlobalAddinDirectories.Add (Path.Combine (baseDir, "mono.addins")); - return reg; + + var globalDir = Path.Combine (baseDir, "mono.addins"); + + return new AddinRegistry (engine, GlobalRegistryPath, startupDirectory, null, null, globalDir); } internal bool UnknownDomain { @@ -637,6 +639,11 @@ namespace Mono.Addins database.Update (monitor, currentDomain); } + internal void Update(IProgressStatus monitor, AddinEngineTransaction addinEngineTransaction) + { + database.Update(monitor, currentDomain, addinEngineTransaction:addinEngineTransaction); + } + /// <summary> /// Regenerates the cached data of the add-in registry. /// </summary> @@ -743,10 +750,15 @@ namespace Mono.Addins get { return databaseDir; } } - internal StringCollection GlobalAddinDirectories { + internal IEnumerable<string> GlobalAddinDirectories { get { return addinDirs; } } + internal void RegisterGlobalAddinDirectory (string dir) + { + + } + internal string StartupDirectory { get { return startupDirectory; diff --git a/Mono.Addins/Mono.Addins/ConditionType.cs b/Mono.Addins/Mono.Addins/ConditionType.cs index 4f7b0db..c5937f6 100644 --- a/Mono.Addins/Mono.Addins/ConditionType.cs +++ b/Mono.Addins/Mono.Addins/ConditionType.cs @@ -210,7 +210,8 @@ namespace Mono.Addins if (!string.IsNullOrEmpty (addin)) { // Make sure the add-in that implements the condition is loaded - addinEngine.LoadAddin (null, addin, true); + using var tr = addinEngine.BeginEngineTransaction(); + addinEngine.LoadAddin (tr, null, addin, true); addin = null; // Don't try again } diff --git a/Mono.Addins/Mono.Addins/ExtensionContext.cs b/Mono.Addins/Mono.Addins/ExtensionContext.cs index 7b510a3..7d03a1e 100644 --- a/Mono.Addins/Mono.Addins/ExtensionContext.cs +++ b/Mono.Addins/Mono.Addins/ExtensionContext.cs @@ -30,35 +30,58 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; using Mono.Addins.Description; namespace Mono.Addins { - /// <summary> - /// An extension context. - /// </summary> - /// <remarks> - /// Extension contexts can be used to query the extension tree - /// using particular condition values. Extension points which - /// declare the availability of a condition type can only be - /// queryed using an extension context which provides an - /// evaluator for that condition. - /// </remarks> - public class ExtensionContext + /// <summary> + /// An extension context. + /// </summary> + /// <remarks> + /// Extension contexts can be used to query the extension tree + /// using particular condition values. Extension points which + /// declare the availability of a condition type can only be + /// queryed using an extension context which provides an + /// evaluator for that condition. + /// </remarks> + public class ExtensionContext { internal object LocalLock = new object (); - Hashtable conditionTypes = new Hashtable (); - Hashtable conditionsToNodes = new Hashtable (); - List<WeakReference> childContexts; + ImmutableArray<WeakReference> childContexts = ImmutableArray<WeakReference>.Empty; ExtensionContext parentContext; ExtensionTree tree; - bool fireEvents = false; - - List<string> runTimeEnabledAddins; - List<string> runTimeDisabledAddins; - + + NotificationQueue notificationQueue; + + // runTimeEnabledAddins and runTimeDisabledAddins are modified only within a transaction, + // so they don't need to be immutable and don't need to be in the snapshot + HashSet<string> runTimeEnabledAddins = new HashSet<string>(); + HashSet<string> runTimeDisabledAddins = new HashSet<string>(); + + ExtensionContextSnapshot currentSnapshot = new ExtensionContextSnapshot(); + + internal class ExtensionContextSnapshot + { + public ImmutableDictionary<string, ConditionInfo> ConditionTypes; + public ImmutableDictionary<BaseCondition, ImmutableArray<TreeNode>> ConditionsToNodes; + + public ExtensionContextSnapshot() + { + ConditionTypes = ImmutableDictionary<string, ConditionInfo>.Empty; + ConditionsToNodes = ImmutableDictionary<BaseCondition, ImmutableArray<TreeNode>>.Empty; + } + + public virtual void CopyFrom(ExtensionContextSnapshot other) + { + ConditionTypes = other.ConditionTypes; + ConditionsToNodes = other.ConditionsToNodes; + } + } + /// <summary> /// Extension change event. /// </summary> @@ -68,15 +91,31 @@ namespace Mono.Addins /// it does not provide information about what changed. Hosts subscribing to /// this event should get the new list of nodes using a query method such as /// AddinManager.GetExtensionNodes() and then update whatever needs to be updated. + /// + /// Threading information: the thread on which the event is raised is undefined. Events are + /// guaranteed to be raised sequentially for a given extension context. /// </remarks> public event ExtensionEventHandler ExtensionChanged; internal void Initialize (AddinEngine addinEngine) { - fireEvents = false; + notificationQueue = new NotificationQueue(addinEngine); + SetSnapshot(CreateSnapshot()); tree = new ExtensionTree (addinEngine, this); } + internal virtual ExtensionContextSnapshot CreateSnapshot() + { + return new ExtensionContextSnapshot(); + } + + internal virtual void SetSnapshot(ExtensionContextSnapshot newSnapshot) + { + currentSnapshot = newSnapshot; + } + + internal ExtensionContextSnapshot CurrentSnapshot => currentSnapshot; + #pragma warning disable 1591 [ObsoleteAttribute] protected void Clear () @@ -84,52 +123,81 @@ namespace Mono.Addins } #pragma warning restore 1591 + internal void InvokeCallback(Action action, object source) + { + notificationQueue.Invoke(action, source); + } internal void ClearContext () { - conditionTypes.Clear (); - conditionsToNodes.Clear (); - childContexts = null; + SetSnapshot(CreateSnapshot()); + childContexts = ImmutableArray<WeakReference>.Empty; parentContext = null; tree = null; - runTimeEnabledAddins = null; - runTimeDisabledAddins = null; } - + internal AddinEngine AddinEngine { get { return tree.AddinEngine; } } void CleanDisposedChildContexts () { - if (childContexts != null) - childContexts.RemoveAll (w => w.Target == null); + var list = childContexts; + List<WeakReference> toRemove = null; + + for (int n = 0; n < list.Length; n++) { + if (list [n].Target == null) { + // Create the list only if there is something to remove + if (toRemove == null) + toRemove = new List<WeakReference> (); + toRemove.Add (list [n]); + } + } + if (toRemove != null) { + // Removing the stale contexts is not urgent, so if the lock can't be acquired now + // it is ok to just skip the clean up and try later + if (Monitor.TryEnter(LocalLock)) { + try { + childContexts = childContexts.RemoveRange (toRemove); + } finally { + Monitor.Exit (LocalLock); + } + } + } } - - internal virtual void ResetCachedData () + + internal void ResetCachedData(ExtensionContextTransaction transaction = null) { - tree.ResetCachedData (); - if (childContexts != null) { - foreach (WeakReference wref in childContexts) { - ExtensionContext ctx = wref.Target as ExtensionContext; - if (ctx != null) - ctx.ResetCachedData (); - } + var currentTransaction = transaction ?? BeginTransaction(); + try + { + OnResetCachedData(currentTransaction); } + finally + { + if (currentTransaction != transaction) + currentTransaction.Dispose(); + } + } + + internal virtual void OnResetCachedData (ExtensionContextTransaction transaction) + { + tree.ResetCachedData (transaction); + + foreach (var ctx in GetActiveChildContexes()) + ctx.ResetCachedData (); } internal ExtensionContext CreateChildContext () { - lock (conditionTypes) { - if (childContexts == null) - childContexts = new List<WeakReference> (); - else - CleanDisposedChildContexts (); - ExtensionContext ctx = new ExtensionContext (); - ctx.Initialize (AddinEngine); - ctx.parentContext = this; - WeakReference wref = new WeakReference (ctx); - childContexts.Add (wref); + ExtensionContext ctx = new ExtensionContext (); + ctx.Initialize (AddinEngine); + ctx.parentContext = this; + WeakReference wref = new WeakReference (ctx); + + lock (LocalLock) { + CleanDisposedChildContexts (); + childContexts = childContexts.Add (wref); return ctx; } } @@ -150,13 +218,9 @@ namespace Mono.Addins /// </remarks> public void RegisterCondition (string id, ConditionType type) { + using var transaction = BeginTransaction (); type.Id = id; - ConditionInfo info = CreateConditionInfo (id); - ConditionType ot = info.CondType as ConditionType; - if (ot != null) - ot.Changed -= new EventHandler (OnConditionChanged); - info.CondType = type; - type.Changed += new EventHandler (OnConditionChanged); + GetOrCreateConditionInfo (transaction, id, type); } /// <summary> @@ -174,63 +238,74 @@ namespace Mono.Addins /// </remarks> public void RegisterCondition (string id, Type type) { + using var transaction = BeginTransaction (); + // Allows delayed creation of condition types - ConditionInfo info = CreateConditionInfo (id); - ConditionType ot = info.CondType as ConditionType; - if (ot != null) - ot.Changed -= new EventHandler (OnConditionChanged); - info.CondType = type; + GetOrCreateConditionInfo (transaction, id, type); } - internal void RegisterCondition (string id, RuntimeAddin addin, string typeName) + internal void RegisterCondition (ExtensionContextTransaction transaction, string id, RuntimeAddin addin, string typeName) { // Allows delayed creation of condition types - ConditionInfo info = CreateConditionInfo (id); - ConditionType ot = info.CondType as ConditionType; - if (ot != null) - ot.Changed -= new EventHandler (OnConditionChanged); - info.CondType = new ConditionTypeData { + GetOrCreateConditionInfo (transaction, id, new ConditionTypeData { TypeName = typeName, Addin = addin - }; + }); } - ConditionInfo CreateConditionInfo (string id) + internal ConditionInfo GetOrCreateConditionInfo (ExtensionContextTransaction transaction, string id, object conditionTypeObject) { - ConditionInfo info = conditionTypes [id] as ConditionInfo; - if (info == null) { + if (!transaction.Snapshot.ConditionTypes.TryGetValue (id, out var info)) { info = new ConditionInfo (); - conditionTypes [id] = info; + info.CondType = conditionTypeObject; + transaction.Snapshot.ConditionTypes = transaction.Snapshot.ConditionTypes.Add (id, info); + } else { + // If CondType is not changing, nothing else to do + if (conditionTypeObject == null) + return info; + + // Replace the old CondType + var oldType = info.CondType as ConditionType; + info.CondType = conditionTypeObject; + if (oldType != null) + oldType.Changed -= new EventHandler (OnConditionChanged); } + if (conditionTypeObject is ConditionType conditionType) + conditionType.Changed += new EventHandler (OnConditionChanged); return info; } - internal bool FireEvents { - get { return fireEvents; } - } - internal ConditionType GetCondition (string id) { - ConditionInfo info = (ConditionInfo) conditionTypes [id]; - - if (info != null) { + if (currentSnapshot.ConditionTypes.TryGetValue(id, out var info)) { if (info.CondType is ConditionType condition) { return condition; } else { - // The condition was registered as a type, create an instance now - Type type; - if (info.CondType is ConditionTypeData data) { - type = data.Addin.GetType (data.TypeName, true); - } else - type = info.CondType as Type; - - if (type != null) { - var ct = (ConditionType)Activator.CreateInstance (type); - ct.Id = id; - ct.Changed += new EventHandler (OnConditionChanged); - info.CondType = ct; - return ct; + // The condition needs to be instantiated in a lock, to avoid duplicate + // creation if two threads are trying to get it + + lock (LocalLock) { + + // Check again from inside the lock (maybe another thread already created the condition) + if (info.CondType is ConditionType cond) + return cond; + + // The condition was registered as a type, create an instance now + + Type type; + if (info.CondType is ConditionTypeData data) { + type = data.Addin.GetType (data.TypeName, true); + } else + type = info.CondType as Type; + + if (type != null) { + var ct = (ConditionType)Activator.CreateInstance (type); + ct.Id = id; + ct.Changed += new EventHandler (OnConditionChanged); + info.CondType = ct; + return ct; + } } } } @@ -240,47 +315,8 @@ namespace Mono.Addins else return null; } - - internal void RegisterNodeCondition (TreeNode node, BaseCondition cond) - { - var list = (List<TreeNode>) conditionsToNodes [cond]; - if (list == null) { - list = new List<TreeNode> (); - conditionsToNodes [cond] = list; - var conditionTypeIds = new List<string> (); - cond.GetConditionTypes (conditionTypeIds); - - foreach (string cid in conditionTypeIds) { - - ConditionInfo info = CreateConditionInfo (cid); - if (info.BoundConditions == null) - info.BoundConditions = new List<BaseCondition> (); - - info.BoundConditions.Add (cond); - } - } - list.Add (node); - } - - internal void UnregisterNodeCondition (TreeNode node, BaseCondition cond) - { - var list = (List<TreeNode>) conditionsToNodes [cond]; - if (list == null) - return; - - list.Remove (node); - if (list.Count == 0) { - conditionsToNodes.Remove (cond); - var conditionTypeIds = new List<string> (); - cond.GetConditionTypes (conditionTypeIds); - foreach (string cid in conditionTypes.Keys) { - ConditionInfo info = conditionTypes [cid] as ConditionInfo; - if (info != null && info.BoundConditions != null) - info.BoundConditions.Remove (cond); - } - } - } - + + /// <summary> /// Returns the extension node in a path /// </summary> @@ -388,7 +424,7 @@ namespace Mono.Addins { string path = AddinEngine.GetAutoTypeExtensionPoint (instanceType); if (path == null) - return new ExtensionNodeList (null); + return ExtensionNodeList.Empty; return GetExtensionNodes (path, expectedNodeType); } @@ -434,10 +470,10 @@ namespace Mono.Addins public ExtensionNodeList GetExtensionNodes (string path, Type expectedNodeType) { TreeNode node = GetNode (path); - if (node == null || node.ExtensionNode == null) + if (node == null || !node.HasExtensionNode) return ExtensionNodeList.Empty; - ExtensionNodeList list = node.ExtensionNode.ChildNodes; + ExtensionNodeList list = node.ExtensionNode.GetChildNodes(); if (expectedNodeType != null) { bool foundError = false; @@ -772,251 +808,200 @@ namespace Mono.Addins throw new InvalidOperationException ("Type '" + instanceType + "' not bound to an extension point."); RemoveExtensionNodeHandler (path, handler); } + + internal virtual ExtensionContextTransaction BeginTransaction () + { + return new ExtensionContextTransaction (this); + } + + internal bool IsCurrentThreadInTransaction => Monitor.IsEntered (LocalLock); void OnConditionChanged (object s, EventArgs a) { ConditionType cond = (ConditionType) s; NotifyConditionChanged (cond); } - - internal void NotifyConditionChanged (ConditionType cond) + + void NotifyConditionChanged (ConditionType cond) { - try { - fireEvents = true; - - ConditionInfo info = (ConditionInfo) conditionTypes [cond.Id]; - if (info != null && info.BoundConditions != null) { - Hashtable parentsToNotify = new Hashtable (); - foreach (BaseCondition c in info.BoundConditions) { - var nodeList = (List<TreeNode>) conditionsToNodes [c]; - if (nodeList != null) { - foreach (TreeNode node in nodeList) - parentsToNotify [node.Parent] = null; - } - } - foreach (TreeNode node in parentsToNotify.Keys) { - if (node.NotifyChildrenChanged ()) - NotifyExtensionsChanged (new ExtensionEventArgs (node.GetPath ())); + HashSet<TreeNode> parentsToNotify = null; + + var snapshot = currentSnapshot; + + if (snapshot.ConditionTypes.TryGetValue (cond.Id, out var info) && info.BoundConditions != null) { + parentsToNotify = new HashSet<TreeNode> (); + foreach (BaseCondition c in info.BoundConditions) { + if (snapshot.ConditionsToNodes.TryGetValue(c, out var nodeList)) { + parentsToNotify.UnionWith (nodeList.Select (node => node.Parent)); } } } - finally { - fireEvents = false; - } - // Notify child contexts - lock (conditionTypes) { - if (childContexts != null) { - CleanDisposedChildContexts (); - foreach (WeakReference wref in childContexts) { - ExtensionContext ctx = wref.Target as ExtensionContext; - if (ctx != null) - ctx.NotifyConditionChanged (cond); - } + if (parentsToNotify != null) { + foreach (TreeNode node in parentsToNotify) { + if (node.NotifyChildrenChanged ()) + NotifyExtensionsChanged (new ExtensionEventArgs (node.GetPath ())); } } + + foreach (var ctx in GetActiveChildContexes ()) + ctx.NotifyConditionChanged (cond); } - - internal void NotifyExtensionsChanged (ExtensionEventArgs args) + IEnumerable<ExtensionContext> GetActiveChildContexes () { - if (!fireEvents) - return; + // Collect a list of child contexts that are still referenced + if (childContexts.Length > 0) { + CleanDisposedChildContexts (); + return childContexts.Select (t => (ExtensionContext)t.Target).Where (t => t != null); + } else + return Array.Empty<ExtensionContext> (); + } + + internal void NotifyExtensionsChanged (ExtensionEventArgs args) + { if (ExtensionChanged != null) - ExtensionChanged (this, args); + { + notificationQueue.Invoke(() => + { + ExtensionChanged?.Invoke(this, args); + }, null); + } } internal void NotifyAddinLoaded (RuntimeAddin ad) { tree.NotifyAddinLoaded (ad, true); - lock (conditionTypes) { - if (childContexts != null) { - CleanDisposedChildContexts (); - foreach (WeakReference wref in childContexts) { - ExtensionContext ctx = wref.Target as ExtensionContext; - if (ctx != null) - ctx.NotifyAddinLoaded (ad); - } - } - } + foreach (var ctx in GetActiveChildContexes()) + ctx.NotifyAddinLoaded (ad); } - internal void CreateExtensionPoint (ExtensionPoint ep) + internal void CreateExtensionPoint (ExtensionContextTransaction transaction, ExtensionPoint ep) { - TreeNode node = tree.GetNode (ep.Path, true); + TreeNode node = tree.GetNode (ep.Path, true, transaction); if (node.ExtensionPoint == null) { node.ExtensionPoint = ep; node.ExtensionNodeSet = ep.NodeSet; } } - internal void ActivateAddinExtensions (string id) + internal void ActivateAddinExtensions (ExtensionContextTransaction transaction, string id) { // Looks for loaded extension points which are extended by the provided // add-in, and adds the new nodes - try { - fireEvents = true; - - Addin addin = AddinEngine.Registry.GetAddin (id); - if (addin == null) { - AddinEngine.ReportError ("Required add-in not found", id, null, false); - return; - } - // Take note that this add-in has been enabled at run-time - // Needed because loaded add-in descriptions may not include this add-in. - RegisterRuntimeEnabledAddin (id); + Addin addin = AddinEngine.Registry.GetAddin (id); + if (addin == null) { + AddinEngine.ReportError ("Required add-in not found", id, null, false); + return; + } + // Take note that this add-in has been enabled at run-time + // Needed because loaded add-in descriptions may not include this add-in. + RegisterRuntimeEnabledAddin (transaction, id); - // Look for loaded extension points - Hashtable eps = new Hashtable (); - var newExtensions = new List<string> (); - foreach (ModuleDescription mod in addin.Description.AllModules) { - foreach (Extension ext in mod.Extensions) { - if (!newExtensions.Contains (ext.Path)) - newExtensions.Add (ext.Path); - ExtensionPoint ep = tree.FindLoadedExtensionPoint (ext.Path); - if (ep != null && !eps.Contains (ep)) - eps.Add (ep, ep); - } + // Look for loaded extension points + Hashtable eps = new Hashtable (); + foreach (ModuleDescription mod in addin.Description.AllModules) { + foreach (Extension ext in mod.Extensions) { + transaction.NotifyExtensionsChangedEvent (ext.Path); + ExtensionPoint ep = tree.FindLoadedExtensionPoint (ext.Path); + if (ep != null && !eps.Contains (ep)) + eps.Add (ep, ep); } + } - // Add the new nodes - var loadedNodes = new List<TreeNode> (); - foreach (ExtensionPoint ep in eps.Keys) { - ExtensionLoadData data = GetAddinExtensions (id, ep); - if (data != null) { - foreach (Extension ext in data.Extensions) { - TreeNode node = GetNode (ext.Path); - if (node != null && node.ExtensionNodeSet != null) { - if (node.ChildrenLoaded) - LoadModuleExtensionNodes (ext, data.AddinId, node.ExtensionNodeSet, loadedNodes); - } - else - AddinEngine.ReportError ("Extension node not found or not extensible: " + ext.Path, id, null, false); + // Add the new nodes + foreach (ExtensionPoint ep in eps.Keys) { + ExtensionLoadData data = GetAddinExtensions (transaction, id, ep); + if (data != null) { + foreach (Extension ext in data.Extensions) { + TreeNode node = GetNode (ext.Path); + if (node != null && node.ExtensionNodeSet != null) { + if (node.ChildrenFromExtensionsLoaded) + LoadModuleExtensionNodes (transaction, node, ext, data.AddinId); } + else + AddinEngine.ReportError ("Extension node not found or not extensible: " + ext.Path, id, null, false); } } - - // Call the OnAddinLoaded method on nodes, if the add-in is already loaded - foreach (TreeNode nod in loadedNodes) - nod.ExtensionNode.OnAddinLoaded (); - - // Global extension change event. Other events are fired by LoadModuleExtensionNodes. - // The event is called for all extensions, even for those not loaded. This is for coherence, - // although that something that it doesn't make much sense to do (subscribing the ExtensionChanged - // event without first getting the list of nodes that may change). - foreach (string newExt in newExtensions) - NotifyExtensionsChanged (new ExtensionEventArgs (newExt)); - } - finally { - fireEvents = false; } + // Do the same in child contexts - - lock (conditionTypes) { - if (childContexts != null) { - CleanDisposedChildContexts (); - foreach (WeakReference wref in childContexts) { - ExtensionContext ctx = wref.Target as ExtensionContext; - if (ctx != null) - ctx.ActivateAddinExtensions (id); - } - } - } + + foreach (var ctx in GetActiveChildContexes ()) + ctx.ActivateAddinExtensions (transaction, id); } - - internal void RemoveAddinExtensions (string id) + + internal void RemoveAddinExtensions (ExtensionContextTransaction transaction, string id) { - try { - // Registers this add-in as disabled, so from now on extension from this - // add-in will be ignored - RegisterRuntimeDisabledAddin (id); - - fireEvents = true; + // Registers this add-in as disabled, so from now on extension from this + // add-in will be ignored + RegisterRuntimeDisabledAddin (transaction, id); - // This method removes all extension nodes added by the add-in - // Get all nodes created by the addin - List<TreeNode> list = new List<TreeNode> (); - tree.FindAddinNodes (id, list); - - // Remove each node and notify the change - foreach (TreeNode node in list) { - if (node.ExtensionNode == null) { - // It's an extension point. Just remove it, no notifications are needed - node.Remove (); - } - else { - node.ExtensionNode.OnAddinUnloaded (); - node.Remove (); + // This method removes all extension nodes added by the add-in + // Get all nodes created by the addin + List<TreeNode> list = new List<TreeNode> (); + tree.FindAddinNodes (id, list); + + // Remove each node and notify the change + foreach (TreeNode node in list) { + node.NotifyAddinUnloaded (); + node.Parent?.RemoveChild (transaction, node); + } + + // Notify global extension point changes. + // The event is called for all extensions, even for those not loaded. This is for coherence, + // although that something that it doesn't make much sense to do (subscribing the ExtensionChanged + // event without first getting the list of nodes that may change). + + // We get the runtime add-in because the add-in may already have been deleted from the registry + RuntimeAddin addin = AddinEngine.GetAddin (transaction.GetAddinEngineTransaction(), id); + if (addin != null) { + var paths = new List<string> (); + // Using addin.Module.ParentAddinDescription here because addin.Addin.Description may not + // have a valid reference (the description is lazy loaded and may already have been removed from the registry) + foreach (ModuleDescription mod in addin.Module.ParentAddinDescription.AllModules) { + foreach (Extension ext in mod.Extensions) { + if (!paths.Contains (ext.Path)) + paths.Add (ext.Path); } } - - // Notify global extension point changes. - // The event is called for all extensions, even for those not loaded. This is for coherence, - // although that something that it doesn't make much sense to do (subscribing the ExtensionChanged - // event without first getting the list of nodes that may change). - - // We get the runtime add-in because the add-in may already have been deleted from the registry - RuntimeAddin addin = AddinEngine.GetAddin (id); - if (addin != null) { - var paths = new List<string> (); - // Using addin.Module.ParentAddinDescription here because addin.Addin.Description may not - // have a valid reference (the description is lazy loaded and may already have been removed from the registry) - foreach (ModuleDescription mod in addin.Module.ParentAddinDescription.AllModules) { - foreach (Extension ext in mod.Extensions) { - if (!paths.Contains (ext.Path)) - paths.Add (ext.Path); - } - } - foreach (string path in paths) - NotifyExtensionsChanged (new ExtensionEventArgs (path)); - } - } finally { - fireEvents = false; + foreach (string path in paths) + transaction.NotifyExtensionsChangedEvent (path); } } - void RegisterRuntimeDisabledAddin (string addinId) + void RegisterRuntimeDisabledAddin (ExtensionContextTransaction transaction, string addinId) { - if (runTimeDisabledAddins == null) - runTimeDisabledAddins = new List<string> (); - if (!runTimeDisabledAddins.Contains (addinId)) - runTimeDisabledAddins.Add (addinId); - - if (runTimeEnabledAddins != null) - runTimeEnabledAddins.Remove (addinId); + runTimeDisabledAddins.Add (addinId); + runTimeEnabledAddins.Remove (addinId); } - void RegisterRuntimeEnabledAddin (string addinId) + void RegisterRuntimeEnabledAddin (ExtensionContextTransaction transaction, string addinId) { - if (runTimeEnabledAddins == null) - runTimeEnabledAddins = new List<string> (); - if (!runTimeEnabledAddins.Contains (addinId)) - runTimeEnabledAddins.Add (addinId); - - if (runTimeDisabledAddins != null) - runTimeDisabledAddins.Remove (addinId); + runTimeEnabledAddins.Add (addinId); + runTimeDisabledAddins.Remove (addinId); } - internal List<string> GetAddinsForPath (List<string> col) + List<string> GetAddinsForPath (ExtensionContextTransaction transaction, List<string> col) { List<string> newlist = null; - + // Always consider add-ins which have been enabled at runtime since // they may contain extension for this path. // Ignore addins disabled at run-time. - - if (runTimeEnabledAddins != null && runTimeEnabledAddins.Count > 0) { + + if (runTimeEnabledAddins.Count > 0) { newlist = new List<string> (); newlist.AddRange (col); foreach (string s in runTimeEnabledAddins) if (!newlist.Contains (s)) newlist.Add (s); } - - if (runTimeDisabledAddins != null && runTimeDisabledAddins.Count > 0) { + + if (runTimeDisabledAddins.Count > 0) { if (newlist == null) { newlist = new List<string> (); newlist.AddRange (col); @@ -1027,14 +1012,13 @@ namespace Mono.Addins return newlist != null ? newlist : col; } - + // Load the extension nodes at the specified path. If the path // contains extension nodes implemented in an add-in which is // not loaded, the add-in will be automatically loaded - - internal void LoadExtensions (string requestedExtensionPath) + + internal void LoadExtensions (ExtensionContextTransaction transaction, string requestedExtensionPath, TreeNode node) { - TreeNode node = GetNode (requestedExtensionPath); if (node == null) throw new InvalidOperationException ("Extension point not defined: " + requestedExtensionPath); @@ -1045,16 +1029,16 @@ namespace Mono.Addins // Collect extensions to be loaded from add-ins. Before loading the extensions, // they must be sorted, that's why loading is split in two steps (collecting + loading). - var addins = GetAddinsForPath (ep.Addins); + var addins = GetAddinsForPath (transaction, ep.Addins); var loadData = new List<ExtensionLoadData> (addins.Count); foreach (string addin in addins) { - ExtensionLoadData ed = GetAddinExtensions (addin, ep); + ExtensionLoadData ed = GetAddinExtensions (transaction, addin, ep); if (ed != null) { // Insert the addin data taking into account dependencies. // An add-in must be processed after all its dependencies. bool added = false; - for (int n=0; n<loadData.Count; n++) { + for (int n = 0; n < loadData.Count; n++) { ExtensionLoadData other = loadData [n]; if (AddinEngine.Registry.AddinDependsOn (other.AddinName, ed.AddinName)) { loadData.Insert (n, ed); @@ -1066,33 +1050,33 @@ namespace Mono.Addins loadData.Add (ed); } } - + // Now load the extensions - + var loadedNodes = new List<TreeNode> (); foreach (ExtensionLoadData data in loadData) { foreach (Extension ext in data.Extensions) { TreeNode cnode = GetNode (ext.Path); if (cnode != null && cnode.ExtensionNodeSet != null) - LoadModuleExtensionNodes (ext, data.AddinId, cnode.ExtensionNodeSet, loadedNodes); + LoadModuleExtensionNodes (transaction, cnode, ext, data.AddinId); else AddinEngine.ReportError ("Extension node not found or not extensible: " + ext.Path, data.AddinId, null, false); } } // Call the OnAddinLoaded method on nodes, if the add-in is already loaded foreach (TreeNode nod in loadedNodes) - nod.ExtensionNode.OnAddinLoaded (); + nod.ExtensionNode.NotifyAddinLoaded(); - NotifyExtensionsChanged (new ExtensionEventArgs (requestedExtensionPath)); - } - } - - ExtensionLoadData GetAddinExtensions (string id, ExtensionPoint ep) + transaction.NotifyExtensionsChangedEvent(requestedExtensionPath); + } + } + + ExtensionLoadData GetAddinExtensions (ExtensionContextTransaction transaction, string id, ExtensionPoint ep) { Addin pinfo = null; // Root add-ins are not returned by GetInstalledAddin. - RuntimeAddin addin = AddinEngine.GetAddin (id); + RuntimeAddin addin = AddinEngine.GetAddin (transaction.GetAddinEngineTransaction(), id); if (addin != null) pinfo = addin.Addin; else @@ -1138,18 +1122,18 @@ namespace Mono.Addins } } - void LoadModuleExtensionNodes (Extension extension, string addinId, ExtensionNodeSet nset, List<TreeNode> loadedNodes) + void LoadModuleExtensionNodes (ExtensionContextTransaction transaction, TreeNode node, Extension extension, string addinId) { // Now load the extensions var addedNodes = new List<TreeNode> (); - tree.LoadExtension (addinId, extension, addedNodes); + tree.LoadExtension (transaction, node, addinId, extension, addedNodes); - RuntimeAddin ad = AddinEngine.GetAddin (addinId); + RuntimeAddin ad = AddinEngine.GetAddin (transaction.GetAddinEngineTransaction(), addinId); if (ad != null) { foreach (TreeNode nod in addedNodes) { // Don't call OnAddinLoaded here. Do it when the entire extension point has been loaded. - if (nod.ExtensionNode != null) - loadedNodes.Add (nod); + if (nod.HasExtensionNode) + transaction.ReportLoadedNode (nod); } } } @@ -1173,11 +1157,13 @@ namespace Mono.Addins TreeNode node = tree.GetNode (path); if (node != null || parentContext == null) return node; - + TreeNode supNode = parentContext.tree.GetNode (path); if (supNode == null) return null; - + + // Node not found and the context has a parent context which has the node + if (path.StartsWith ("/")) path = path.Substring (1); @@ -1185,51 +1171,57 @@ namespace Mono.Addins TreeNode srcNode = parentContext.tree; TreeNode dstNode = tree; - foreach (string part in parts) { - - // Look for the node in the source tree - - int i = srcNode.Children.IndexOfNode (part); - if (i != -1) - srcNode = srcNode.Children [i]; - else - return null; + ExtensionContextTransaction transaction = null; - // Now get the node in the target tree - - int j = dstNode.Children.IndexOfNode (part); - if (j != -1) { - dstNode = dstNode.Children [j]; - } - else { - // Create if not found - TreeNode newNode = new TreeNode (AddinEngine, part); - dstNode.AddChildNode (newNode); - dstNode = newNode; - - // Copy extension data - dstNode.ExtensionNodeSet = srcNode.ExtensionNodeSet; - dstNode.ExtensionPoint = srcNode.ExtensionPoint; - dstNode.Condition = srcNode.Condition; - - if (dstNode.Condition != null) - RegisterNodeCondition (dstNode, dstNode.Condition); + try { + foreach (string part in parts) { + + // Look for the node in the source tree (from parent context) + + srcNode = srcNode.GetChildNode (part); + if (srcNode == null) + return null; + + // Now get the node in the target tree + + var dstNodeChild = dstNode.GetChildNode (part); + if (dstNodeChild != null) { + dstNode = dstNodeChild; + } else { + if (transaction == null) + transaction = BeginTransaction (); + + // Check again just in case the node was created while taking the transaction + dstNodeChild = dstNode.GetChildNode (part); + if (dstNodeChild != null) + dstNode = dstNodeChild; + else { + + // Create if not found + TreeNode newNode = new TreeNode (AddinEngine, part); + + // Copy extension data + newNode.ExtensionNodeSet = srcNode.ExtensionNodeSet; + newNode.ExtensionPoint = srcNode.ExtensionPoint; + newNode.Condition = srcNode.Condition; + + dstNode.AddChildNode (transaction, newNode); + dstNode = newNode; + } + } } + } finally { + transaction?.Dispose (); } return dstNode; } - - internal bool FindExtensionPathByType (IProgressStatus monitor, Type type, string nodeName, out string path, out string pathNodeName) - { - return tree.FindExtensionPathByType (monitor, type, nodeName, out path, out pathNodeName); - } } class ConditionInfo { public object CondType; - public List<BaseCondition> BoundConditions; + public ImmutableArray<BaseCondition> BoundConditions = ImmutableArray<BaseCondition>.Empty; } @@ -1379,4 +1371,82 @@ namespace Mono.Addins public string TypeName { get; set; } public RuntimeAddin Addin { get; set; } } + + /// <summary> + /// A queue that can be used to dispatch callbacks sequentially + /// </summary> + class NotificationQueue + { + readonly AddinEngine addinEngine; + readonly Queue<(Action Action,object Source)> notificationQueue = new Queue<(Action,object)>(); + + bool sending; + + public NotificationQueue(AddinEngine addinEngine) + { + this.addinEngine = addinEngine; + } + + internal void Invoke(Action action, object source) + { + lock (notificationQueue) + { + if (sending) + { + // Already sending, enqueue the action so whoever is sending will take it + notificationQueue.Enqueue((action,source)); + return; + } + else + { + // Nobody is sending, do it now + sending = true; + } + } + + SafeInvoke(action, source); + + do + { + lock (notificationQueue) + { + if (notificationQueue.Count == 0) + { + sending = false; + return; + } + (action,source) = notificationQueue.Dequeue(); + } + SafeInvoke(action, source); + } + while (true); + } + + void SafeInvoke(Action action, object source) + { + try + { + action(); + } + catch (Exception ex) + { + RuntimeAddin addin = null; + + if (source is ExtensionNode node) + { + try + { + addin = node.Addin; + } + catch (Exception addinException) + { + addinEngine.ReportError(null, null, addinException, false); + addin = null; + } + } + + addinEngine.ReportError("Callback invocation failed", addin?.Id, ex, false); + } + } + } } diff --git a/Mono.Addins/Mono.Addins/ExtensionContextTransaction.cs b/Mono.Addins/Mono.Addins/ExtensionContextTransaction.cs new file mode 100644 index 0000000..ba2a245 --- /dev/null +++ b/Mono.Addins/Mono.Addins/ExtensionContextTransaction.cs @@ -0,0 +1,409 @@ +// +// ExtensionContext.cs +// +// Author: +// Lluis Sanchez Gual +// +// Copyright (C) 2007 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using static Mono.Addins.ExtensionContext; + +namespace Mono.Addins +{ + /// <summary> + /// Represents a mutation action of an extension context. It is necessary to acquire a transaction object + /// when doing changes in a context (methods that modify a context take a transaction as parameter). + /// Getting a transaction is a blocking operation: if a thread has started a transaction, other threads trying + /// to get one will be blocked until the current transaction is finished. + /// The transaction objects collects two kind of information: + /// 1) changes to be done in the context. For example, it collects conditions to be registered, and then all conditions + /// are added to the context at once when the transaction is completed (so the immutable collection that holds + /// the condition list can be updated with a single allocation). + /// 2) events to be fired once the transaction is completed. This is to avoid firing events while the context + /// lock is taken. + /// </summary> + class ExtensionContextTransaction : IDisposable + { + List<TreeNode> loadedNodes; + HashSet<TreeNode> childrenChanged; + HashSet<string> extensionsChanged; + List<(TreeNode Node, BaseCondition Condition)> nodeConditions; + List<(TreeNode Node, BaseCondition Condition)> nodeConditionUnregistrations; + List<TreeNode> treeNodeTransactions; + ExtensionContextSnapshot snapshot; + bool snaphotChanged; + AddinEngineTransaction nestedAddinEngineTransaction; + + public ExtensionContextTransaction (ExtensionContext context) + { + Context = context; + Monitor.Enter (Context.LocalLock); + snapshot = context.CurrentSnapshot; + } + + public ExtensionContext Context { get; } + + public bool DisableEvents { get; set; } + + public AddinEngine.AddinEngineSnapshot AddinEngineSnapshot => (AddinEngine.AddinEngineSnapshot)Snapshot; + + public ExtensionContextSnapshot Snapshot => snapshot; + + /// <summary> + /// Gets an add-in engine transaction, if there is one in progress. Returns null if there isn't one. + /// </summary> + public AddinEngineTransaction GetAddinEngineTransaction() + { + if (this is AddinEngineTransaction et) + return et; + if (nestedAddinEngineTransaction != null) + return nestedAddinEngineTransaction; + return null; + } + + /// <summary> + /// Gets or creates an add-in engine transaction, which will be committed together with this transaction + /// </summary> + public AddinEngineTransaction GetOrCreateAddinEngineTransaction() + { + if (this is AddinEngineTransaction et) + return et; + if (nestedAddinEngineTransaction != null) + return nestedAddinEngineTransaction; + + return nestedAddinEngineTransaction = Context.AddinEngine.BeginEngineTransaction(); + } + + protected void EnsureNewSnapshot() + { + if (!snaphotChanged) + { + snaphotChanged = true; + var newSnapshot = Context.CreateSnapshot(); + newSnapshot.CopyFrom(snapshot); + snapshot = newSnapshot; + } + } + + public void Dispose () + { + try + { + // Update the context + + UpdateSnapshot(); + + if (snaphotChanged) + Context.SetSnapshot(snapshot); + + } finally { + Monitor.Exit (Context.LocalLock); + } + + // If there is a nested engine transaction, make sure it is disposed + using var _ = nestedAddinEngineTransaction; + + // Do notifications outside the lock + + DispatchNotifications(); + } + + protected virtual void UpdateSnapshot() + { + if (nodeConditions != null) + { + BulkRegisterNodeConditions(nodeConditions); + } + + if (nodeConditionUnregistrations != null) + { + BulkUnregisterNodeConditions(nodeConditionUnregistrations); + } + + // Commit tree node transactions + if (treeNodeTransactions != null) + { + foreach (var node in treeNodeTransactions) + node.CommitChildrenUpdateTransaction(); + } + } + + protected virtual void DispatchNotifications() + { + if (loadedNodes != null) + { + foreach (var node in loadedNodes) + node.NotifyAddinLoaded(); + } + if (childrenChanged != null) + { + foreach (var node in childrenChanged) + { + if (node.NotifyChildrenChanged()) + NotifyExtensionsChangedEvent(node.GetPath()); + } + } + if (extensionsChanged != null) + { + foreach (var path in extensionsChanged) + Context.NotifyExtensionsChanged(new ExtensionEventArgs(path)); + } + } + + void BulkRegisterNodeConditions(IEnumerable<(TreeNode Node, BaseCondition Condition)> nodeConditions) + { + // We are going to do many changes, so create a builder for the dictionary + var dictBuilder = Snapshot.ConditionsToNodes.ToBuilder(); + List<(string ConditionId, BaseCondition BoundCondition)> bindings = new(); + + // Group nodes by the conditions, so that all nodes for a conditions can be processed together + + foreach (var group in nodeConditions.GroupBy(c => c.Condition)) + { + var condition = group.Key; + + if (!dictBuilder.TryGetValue(condition, out var list)) + { + + // Condition not yet registered, register it now + + // Get a list of conditions on which this one depends + var conditionTypeIds = new List<string>(); + condition.GetConditionTypes(conditionTypeIds); + + foreach (string cid in conditionTypeIds) + { + // For each condition on which 'condition' depends, register the dependency + // so that it if the condition changes, the dependencies are notified + bindings.Add((cid, condition)); + } + list = ImmutableArray<TreeNode>.Empty; + } + + dictBuilder[condition] = list.AddRange(group.Select(item => item.Node)); + } + + foreach (var binding in bindings.GroupBy(b => b.ConditionId, b => b.BoundCondition)) + { + ConditionInfo info = Context.GetOrCreateConditionInfo(this, binding.Key, null); + info.BoundConditions = info.BoundConditions.AddRange(binding); + } + + Snapshot.ConditionsToNodes = dictBuilder.ToImmutable(); + } + + void BulkUnregisterNodeConditions(IEnumerable<(TreeNode Node, BaseCondition Condition)> nodeConditions) + { + ImmutableDictionary<BaseCondition, ImmutableArray<TreeNode>>.Builder dictBuilder = null; + + foreach (var group in nodeConditions.GroupBy(c => c.Condition)) + { + var condition = group.Key; + if (!Snapshot.ConditionsToNodes.TryGetValue(condition, out var list)) + continue; + + var newList = list.RemoveRange(group.Select(item => item.Node)); + + // If there are no changes, continue, no need to create the dictionary builder + if (newList == list) + continue; + + if (dictBuilder == null) + dictBuilder = Snapshot.ConditionsToNodes.ToBuilder(); + + if (newList.Length == 0) + { + + // The condition is not used anymore. Remove it from the dictionary + // and unregister it from any condition it was bound to + + dictBuilder.Remove(condition); + var conditionTypeIds = new List<string>(); + condition.GetConditionTypes(conditionTypeIds); + foreach (string cid in conditionTypeIds) + { + var info = Snapshot.ConditionTypes[cid]; + if (info != null) + info.BoundConditions = info.BoundConditions.Remove(condition); + } + } + else + dictBuilder[condition] = newList; + } + if (dictBuilder != null) + Snapshot.ConditionsToNodes = dictBuilder.ToImmutable(); + } + + public void ReportLoadedNode (TreeNode node) + { + if (loadedNodes == null) + loadedNodes = new List<TreeNode> (); + loadedNodes.Add (node); + } + + public void ReportChildrenChanged (TreeNode node) + { + if (!DisableEvents) { + if (childrenChanged == null) + childrenChanged = new HashSet<TreeNode> (); + childrenChanged.Add (node); + } + } + + public void NotifyExtensionsChangedEvent (string path) + { + if (!DisableEvents) { + if (extensionsChanged == null) + extensionsChanged = new HashSet<string> (); + extensionsChanged.Add (path); + } + } + + public void RegisterNodeCondition (TreeNode node, BaseCondition cond) + { + EnsureNewSnapshot(); + if (nodeConditions == null) + nodeConditions = new List<(TreeNode Node, BaseCondition Condition)> (); + nodeConditions.Add ((node, cond)); + } + + public void UnregisterNodeCondition (TreeNode node, BaseCondition cond) + { + EnsureNewSnapshot(); + if (nodeConditionUnregistrations == null) + nodeConditionUnregistrations = new List<(TreeNode Node, BaseCondition Condition)> (); + nodeConditionUnregistrations.Add ((node, cond)); + if (nodeConditions != null) + nodeConditions.Remove ((node, cond)); + } + + public void RegisterChildrenUpdateTransaction (TreeNode node) + { + if (treeNodeTransactions == null) + treeNodeTransactions = new List<TreeNode> (); + treeNodeTransactions.Add (node); + } + + } + + class AddinEngineTransaction : ExtensionContextTransaction + { + List<KeyValuePair<string, string>> registeredAutoExtensionPoints; + List<string> unregisteredAutoExtensionPoints; + List<KeyValuePair<string, RuntimeAddin>> registeredAssemblyResolvePaths; + List<string> unregisteredAssemblyResolvePaths; + List<RuntimeAddin> addinLoadEvents; + List<string> addinUnloadEvents; + + public AddinEngineTransaction (AddinEngine engine) : base (engine) + { + } + + public void RegisterAssemblyResolvePaths(string assembly, RuntimeAddin addin) + { + EnsureNewSnapshot(); + if (registeredAssemblyResolvePaths == null) + registeredAssemblyResolvePaths = new List<KeyValuePair<string, RuntimeAddin>>(); + registeredAssemblyResolvePaths.Add(new KeyValuePair<string, RuntimeAddin>(assembly, addin)); + } + + public void UnregisterAssemblyResolvePaths(string assembly) + { + EnsureNewSnapshot(); + if (unregisteredAssemblyResolvePaths == null) + unregisteredAssemblyResolvePaths = new List<string>(); + unregisteredAssemblyResolvePaths.Add(assembly); + } + + public void ReportAddinLoad(RuntimeAddin addin) + { + if (addinLoadEvents == null) + addinLoadEvents = new List<RuntimeAddin>(); + addinLoadEvents.Add(addin); + } + + public void ReportAddinUnload(string id) + { + if (addinUnloadEvents == null) + addinUnloadEvents = new List<string>(); + addinUnloadEvents.Add(id); + } + + public void RegisterAutoTypeExtensionPoint(string typeName, string path) + { + EnsureNewSnapshot(); + if (registeredAutoExtensionPoints == null) + registeredAutoExtensionPoints = new List<KeyValuePair<string, string>>(); + registeredAutoExtensionPoints.Add(new KeyValuePair<string, string>(typeName, path)); + } + + public void UnregisterAutoTypeExtensionPoint(string typeName) + { + EnsureNewSnapshot(); + if (unregisteredAutoExtensionPoints == null) + unregisteredAutoExtensionPoints = new List<string>(); + unregisteredAutoExtensionPoints.Add(typeName); + } + + protected override void UpdateSnapshot() + { + base.UpdateSnapshot(); + + if (registeredAutoExtensionPoints != null) + AddinEngineSnapshot.AutoExtensionTypes = AddinEngineSnapshot.AutoExtensionTypes.SetItems(registeredAutoExtensionPoints); + + if (unregisteredAutoExtensionPoints != null) + AddinEngineSnapshot.AutoExtensionTypes = AddinEngineSnapshot.AutoExtensionTypes.RemoveRange(unregisteredAutoExtensionPoints); + + if (registeredAssemblyResolvePaths != null) + AddinEngineSnapshot.AssemblyResolvePaths = AddinEngineSnapshot.AssemblyResolvePaths.SetItems(registeredAssemblyResolvePaths); + + if (unregisteredAssemblyResolvePaths != null) + AddinEngineSnapshot.AssemblyResolvePaths = AddinEngineSnapshot.AssemblyResolvePaths.RemoveRange(unregisteredAssemblyResolvePaths); + } + + protected override void DispatchNotifications() + { + base.DispatchNotifications(); + + var engine = (AddinEngine)Context; + + if (addinLoadEvents != null) + { + foreach (var addin in addinLoadEvents) + engine.ReportAddinLoad(addin); + } + if (addinUnloadEvents != null) + { + foreach (var id in addinUnloadEvents) + engine.ReportAddinUnload(id); + } + } + } +} diff --git a/Mono.Addins/Mono.Addins/ExtensionNode.cs b/Mono.Addins/Mono.Addins/ExtensionNode.cs index 09dd0c9..36f227d 100644 --- a/Mono.Addins/Mono.Addins/ExtensionNode.cs +++ b/Mono.Addins/Mono.Addins/ExtensionNode.cs @@ -33,6 +33,8 @@ using System.Collections.Generic; using System.Xml; using System.Reflection; using Mono.Addins.Description; +using System.Threading; +using System.Linq; namespace Mono.Addins { @@ -59,7 +61,8 @@ namespace Mono.Addins ModuleDescription module; AddinEngine addinEngine; event ExtensionNodeEventHandler extensionNodeChanged; - + internal readonly object localLock = new object (); + /// <summary> /// Identifier of the node. /// </summary> @@ -118,11 +121,19 @@ namespace Mono.Addins internal void SetTreeNode (TreeNode node) { + // treeNode can only be set once (it's set during the node initialization) + if (treeNode != null) + throw new InvalidOperationException (); + treeNode = node; } internal void SetData (AddinEngine addinEngine, string plugid, ExtensionNodeType nodeType, ModuleDescription module) { + // SetData can only be called once (it's set during the node initialization) + if (this.addinEngine != null) + throw new InvalidOperationException (); + this.addinEngine = addinEngine; this.addinId = plugid; this.nodeType = nodeType; @@ -146,9 +157,7 @@ namespace Mono.Addins public RuntimeAddin Addin { get { if (addin == null && addinId != null) { - if (!addinEngine.IsAddinLoaded (addinId)) - addinEngine.LoadAddin (null, addinId, true); - addin = addinEngine.GetAddin (addinId); + addin = addinEngine.GetOrLoadAddin(addinId, true); if (addin != null) addin = addin.GetModule (module); } @@ -157,80 +166,87 @@ namespace Mono.Addins return addin; } } - + /// <summary> /// Notifies that a child node of this node has been added or removed. /// </summary> /// <remarks> /// The first time the event is subscribed, the handler will be called for each existing node. + /// + /// Threading information: the thread on which the event is raised is undefined. Events are + /// guaranteed to be raised sequentially for a given extension context. /// </remarks> public event ExtensionNodeEventHandler ExtensionNodeChanged { - add { - extensionNodeChanged += value; - foreach (ExtensionNode node in ChildNodes) { - try { - value (this, new ExtensionNodeEventArgs (ExtensionChange.Add, node)); - } catch (Exception ex) { - RuntimeAddin nodeAddin; - try { - nodeAddin = node.Addin; - } catch (Exception addinException) { - addinEngine.ReportError (null, null, addinException, false); - nodeAddin = null; - } - addinEngine.ReportError (null, nodeAddin != null ? nodeAddin.Id : null, ex, false); + add + { + ExtensionContext.InvokeCallback(() => + { + // The invocation here needs to be done right to make sure there are no duplicate or missing events. + + // 1) Get the list of current nodes and store it in a variable. This is the list of nodes for which + // notifications will initially be sent. + + var children = GetChildNodes(); + + // 2) Subscribe the real event. If nodes were added or removed since the above GetChildNodes + // call, callback invocations will now be queued (since we are inside InvokeCallback). + + extensionNodeChanged += value; + + // 3) Send the notifications for the events. Again, any change done after the above GetChildNodes + // call will have queued a notification. + + foreach (ExtensionNode node in children) + { + var theNode = node; + value(this, new ExtensionNodeEventArgs(ExtensionChange.Add, theNode)); } - } + + // 4) We are done notifying the pre-existing nodes. If there was any further change in the children + // list, the corresponding events will be raised after this callback ends. + + }, this); } remove { - extensionNodeChanged -= value; + ExtensionContext.InvokeCallback(() => + { + // This is done inside a InvokeCallback call for simetry with the 'add' block. Since the 'add' + // block runs on a callback that can be queued, doing the same here ensures that the unsubscription + // will always happen after the subscription + extensionNodeChanged -= value; + }, this); } } - + /// <summary> /// Child nodes of this extension node. /// </summary> - public ExtensionNodeList ChildNodes { - get { - if (childrenLoaded) - return childNodes; - - try { - if (treeNode.Children.Count == 0) { - childNodes = ExtensionNodeList.Empty; - return childNodes; - } - } - catch (Exception ex) { - addinEngine.ReportError (null, null, ex, false); - childNodes = ExtensionNodeList.Empty; - return childNodes; - } finally { - childrenLoaded = true; - } + [Obsolete("Use GetChildNodes()")] + public ExtensionNodeList ChildNodes => GetChildNodes(); - List<ExtensionNode> list = new List<ExtensionNode> (); - foreach (TreeNode cn in treeNode.Children) { - - // For each node check if it is visible for the current context. - // If something fails while evaluating the condition, just ignore the node. - - try { - if (cn.ExtensionNode != null && cn.IsEnabled) - list.Add (cn.ExtensionNode); - } catch (Exception ex) { - addinEngine.ReportError (null, null, ex, false); + /// <summary> + /// Child nodes of this extension node. + /// + /// Threading information: the returned list is an snapshot of the current children list. The returned + /// list will not change if this node's children change as result of add-ins loading or conditions changing. + /// Successive calls to this method can return different list instances. + /// </summary> + public ExtensionNodeList GetChildNodes() + { + if (!childrenLoaded) + { + lock (localLock) + { + if (!childrenLoaded) + { + childNodes = CreateChildrenList(); + childrenLoaded = true; } } - if (list.Count > 0) - childNodes = new ExtensionNodeList (list); - else - childNodes = ExtensionNodeList.Empty; - - return childNodes; } + return childNodes; } - + /// <summary> /// Returns the child objects of a node. /// </summary> @@ -349,10 +365,14 @@ namespace Mono.Addins Array GetChildObjectsInternal (Type arrayElementType, bool reuseCachedInstance) { - ArrayList list = new ArrayList (ChildNodes.Count); + // The ChildNodes collection can't change, but it can be replaced by another collection, + // so we keep a local reference, which won't change + var children = GetChildNodes(); + + ArrayList list = new ArrayList (children.Count); - for (int n=0; n<ChildNodes.Count; n++) { - InstanceExtensionNode node = ChildNodes [n] as InstanceExtensionNode; + for (int n=0; n<children.Count; n++) { + var node = children [n] as InstanceExtensionNode; if (node == null) { addinEngine.ReportError ("Error while getting object for node in path '" + Path + "'. Extension node is not a subclass of InstanceExtensionNode.", null, null, false); continue; @@ -471,79 +491,181 @@ namespace Mono.Addins return addinEngine.DefaultLocalizer; } - + + ExtensionNodeList CreateChildrenList () + { + var nodeChildren = treeNode.Children; + + try { + if (nodeChildren.Count == 0) { + return ExtensionNodeList.Empty; + } + } catch (Exception ex) { + addinEngine.ReportError (null, null, ex, false); + return ExtensionNodeList.Empty; + } + + List<ExtensionNode> list = new List<ExtensionNode> (nodeChildren.Count); + foreach (TreeNode cn in nodeChildren) { + + // For each node check if it is visible for the current context. + // If something fails while evaluating the condition, just ignore the node. + + try { + if (cn.IsEnabled && cn.HasExtensionNode) + list.Add (cn.ExtensionNode); + } catch (Exception ex) { + addinEngine.ReportError (null, null, ex, false); + } + } + + if (list.Count > 0) + return new ExtensionNodeList (list); + else + return ExtensionNodeList.Empty; + } + internal bool NotifyChildChanged () { + // If children are not loaded, there isn't anything to update if (!childrenLoaded) return false; - ExtensionNodeList oldList = childNodes; - childrenLoaded = false; - - bool changed = false; - - foreach (ExtensionNode nod in oldList) { - if (ChildNodes [nod.Id] == null) { - changed = true; - OnChildNodeRemoved (nod); + List<(ExtensionNode Node, bool Added)> changes = null; + + lock (localLock) { + var oldList = childNodes; + var newList = CreateChildrenList (); + childNodes = newList; + + // Compare old list and new list to find out which nodes have + // been added or removed, since the changes need to be notified. + + foreach (ExtensionNode nod in oldList) { + if (newList [nod.Id] == null) { + if (changes == null) + changes = new (); + changes.Add ((nod, false)); + } } - } - foreach (ExtensionNode nod in ChildNodes) { - if (oldList [nod.Id] == null) { - changed = true; - OnChildNodeAdded (nod); + foreach (ExtensionNode nod in newList) { + if (oldList [nod.Id] == null) { + if (changes == null) + changes = new (); + changes.Add ((nod, true)); + } } } - if (changed) - OnChildrenChanged (); - return changed; + + // Notify the changes outside the lock + + if (changes != null) { + foreach (var change in changes) { + var node = change.Node; + if (change.Added) + { + ExtensionContext.InvokeCallback(() => + { + OnChildNodeAdded(node); + }, this); + } + else + { + ExtensionContext.InvokeCallback(() => + { + OnChildNodeRemoved(node); + }, this); + } + } + ExtensionContext.InvokeCallback(OnChildrenChanged, this); + return true; + } else + return false; } - + + internal void NotifyAddinLoaded() + { + ExtensionContext.InvokeCallback(OnAddinLoaded, this); + } + + internal void NotifyAddinUnloaded() + { + ExtensionContext.InvokeCallback(OnAddinUnloaded, this); + } + /// <summary> /// Called when the add-in that defined this extension node is actually loaded in memory. /// </summary> - internal protected virtual void OnAddinLoaded () + /// <remarks> + /// Threading information: the thread on which the method is invoked is undefined. + /// Invocations to the virtual methods in this class are guaranteed to be done sequentially. + /// For example, OnAddinLoaded will always be called before OnAddinUnloaded, and never + /// concurrently. + /// </remarks> + protected virtual void OnAddinLoaded () { } - + /// <summary> /// Called when the add-in that defined this extension node is being /// unloaded from memory. /// </summary> - internal protected virtual void OnAddinUnloaded () + /// <remarks> + /// Threading information: the thread on which the method is invoked is undefined. + /// Invocations to the virtual methods in this class are guaranteed to be done sequentially. + /// For example, OnAddinLoaded will always be called before OnAddinUnloaded, and never + /// concurrently. + /// </remarks> + protected virtual void OnAddinUnloaded () { } - + /// <summary> /// Called when the children list of this node has changed. It may be due to add-ins /// being loaded/unloaded, or to conditions being changed. /// </summary> + /// <remarks> + /// Threading information: the thread on which the method is invoked is undefined. + /// Invocations to the virtual methods in this class are guaranteed to be done sequentially. + /// For example, OnChildNodeAdded will always be called before OnChildNodeRemoved for a given + /// child, and never concurrently. + /// </remarks> protected virtual void OnChildrenChanged () { } - + /// <summary> /// Called when a child node is added /// </summary> /// <param name="node"> /// Added node. /// </param> + /// <remarks> + /// Threading information: the thread on which the method is invoked is undefined. + /// Invocations to the virtual methods in this class are guaranteed to be done sequentially. + /// For example, OnChildNodeAdded will always be called before OnChildNodeRemoved for a given + /// child, and never concurrently. + /// </remarks> protected virtual void OnChildNodeAdded (ExtensionNode node) { - if (extensionNodeChanged != null) - extensionNodeChanged (this, new ExtensionNodeEventArgs (ExtensionChange.Add, node)); + extensionNodeChanged?.Invoke (this, new ExtensionNodeEventArgs (ExtensionChange.Add, node)); } - + /// <summary> /// Called when a child node is removed /// </summary> /// <param name="node"> /// Removed node. /// </param> + /// <remarks> + /// Threading information: the thread on which the method is invoked is undefined. + /// Invocations to the virtual methods in this class are guaranteed to be done sequentially. + /// For example, OnChildNodeAdded will always be called before OnChildNodeRemoved for a given + /// child, and never concurrently. + /// </remarks> protected virtual void OnChildNodeRemoved (ExtensionNode node) { - if (extensionNodeChanged != null) - extensionNodeChanged (this, new ExtensionNodeEventArgs (ExtensionChange.Remove, node)); + extensionNodeChanged?.Invoke (this, new ExtensionNodeEventArgs (ExtensionChange.Remove, node)); } } diff --git a/Mono.Addins/Mono.Addins/ExtensionNodeList.cs b/Mono.Addins/Mono.Addins/ExtensionNodeList.cs index 729b5ae..fc90fc8 100644 --- a/Mono.Addins/Mono.Addins/ExtensionNodeList.cs +++ b/Mono.Addins/Mono.Addins/ExtensionNodeList.cs @@ -36,7 +36,7 @@ namespace Mono.Addins /// <summary> /// A list of extension nodes. /// </summary> - public class ExtensionNodeList: IEnumerable + public class ExtensionNodeList: IEnumerable, IEnumerable<ExtensionNode> { internal List<ExtensionNode> list; @@ -55,10 +55,7 @@ namespace Mono.Addins /// </param> public ExtensionNode this [int n] { get { - if (list == null) - throw new System.IndexOutOfRangeException (); - else - return (ExtensionNode) list [n]; + return (ExtensionNode) list [n]; } } @@ -70,14 +67,10 @@ namespace Mono.Addins /// </param> public ExtensionNode this [string id] { get { - if (list == null) - return null; - else { - for (int n = list.Count - 1; n >= 0; n--) - if (((ExtensionNode) list [n]).Id == id) - return (ExtensionNode) list [n]; - return null; - } + for (int n = list.Count - 1; n >= 0; n--) + if (list [n].Id == id) + return list [n]; + return null; } } @@ -86,8 +79,6 @@ namespace Mono.Addins /// </summary> public IEnumerator GetEnumerator () { - if (list == null) - return ((IList)Type.EmptyTypes).GetEnumerator (); return list.GetEnumerator (); } @@ -95,7 +86,7 @@ namespace Mono.Addins /// Number of nodes of the collection. /// </summary> public int Count { - get { return list == null ? 0 : list.Count; } + get { return list.Count; } } /// <summary> @@ -109,8 +100,12 @@ namespace Mono.Addins /// </param> public void CopyTo (ExtensionNode[] array, int index) { - if (list != null) - list.CopyTo (array, index); + list.CopyTo (array, index); + } + + IEnumerator<ExtensionNode> IEnumerable<ExtensionNode>.GetEnumerator () + { + return list.GetEnumerator (); } } diff --git a/Mono.Addins/Mono.Addins/ExtensionTree.cs b/Mono.Addins/Mono.Addins/ExtensionTree.cs index ab49492..117eafe 100644 --- a/Mono.Addins/Mono.Addins/ExtensionTree.cs +++ b/Mono.Addins/Mono.Addins/ExtensionTree.cs @@ -41,7 +41,7 @@ namespace Mono.Addins { int internalId; internal const string AutoIdPrefix = "__nid_"; - ExtensionContext context; + readonly ExtensionContext context; public ExtensionTree (AddinEngine addinEngine, ExtensionContext context): base (addinEngine, "") { @@ -53,22 +53,21 @@ namespace Mono.Addins } - public void LoadExtension (string addin, Extension extension, List<TreeNode> addedNodes) + public void LoadExtension (ExtensionContextTransaction transaction, TreeNode tnode, string addin, Extension extension, List<TreeNode> addedNodes) { - TreeNode tnode = GetNode (extension.Path); if (tnode == null) { addinEngine.ReportError ("Can't load extensions for path '" + extension.Path + "'. Extension point not defined.", addin, null, false); return; } int curPos = -1; - LoadExtensionElement (tnode, addin, extension.ExtensionNodes, (ModuleDescription) extension.Parent, ref curPos, tnode.Condition, false, addedNodes); + LoadExtensionElement (transaction, tnode, addin, extension.ExtensionNodes, (ModuleDescription) extension.Parent, ref curPos, tnode.Condition, false, addedNodes); } - void LoadExtensionElement (TreeNode tnode, string addin, ExtensionNodeDescriptionCollection extension, ModuleDescription module, ref int curPos, BaseCondition parentCondition, bool inComplextCondition, List<TreeNode> addedNodes) + void LoadExtensionElement (ExtensionContextTransaction transaction, TreeNode parentNode, string addin, ExtensionNodeDescriptionCollection extension, ModuleDescription module, ref int curPos, BaseCondition parentCondition, bool inComplextCondition, List<TreeNode> addedNodes) { foreach (ExtensionNodeDescription elem in extension) { - + if (inComplextCondition) { parentCondition = ReadComplexCondition (elem, parentCondition); inComplextCondition = false; @@ -76,78 +75,76 @@ namespace Mono.Addins } if (elem.NodeName == "ComplexCondition") { - LoadExtensionElement (tnode, addin, elem.ChildNodes, module, ref curPos, parentCondition, true, addedNodes); + LoadExtensionElement (transaction, parentNode, addin, elem.ChildNodes, module, ref curPos, parentCondition, true, addedNodes); continue; } - + if (elem.NodeName == "Condition") { Condition cond = new Condition (AddinEngine, elem, parentCondition); - LoadExtensionElement (tnode, addin, elem.ChildNodes, module, ref curPos, cond, false, addedNodes); + LoadExtensionElement (transaction, parentNode, addin, elem.ChildNodes, module, ref curPos, cond, false, addedNodes); continue; } - var pnode = tnode; + var pnode = parentNode; ExtensionPoint extensionPoint = null; while (pnode != null && (extensionPoint = pnode.ExtensionPoint) == null) pnode = pnode.Parent; - + string after = elem.GetAttribute ("insertafter"); if (after.Length == 0 && extensionPoint != null && curPos == -1) after = extensionPoint.DefaultInsertAfter; if (after.Length > 0) { - int i = tnode.Children.IndexOfNode (after); + int i = parentNode.IndexOfChild (after); if (i != -1) - curPos = i+1; + curPos = i + 1; } string before = elem.GetAttribute ("insertbefore"); if (before.Length == 0 && extensionPoint != null && curPos == -1) before = extensionPoint.DefaultInsertBefore; if (before.Length > 0) { - int i = tnode.Children.IndexOfNode (before); + int i = parentNode.IndexOfChild (before); if (i != -1) curPos = i; } // If node position is not explicitly set, add the node at the end if (curPos == -1) - curPos = tnode.Children.Count; - + curPos = parentNode.Children.Count; + // Find the type of the node in this extension - ExtensionNodeType ntype = addinEngine.FindType (tnode.ExtensionNodeSet, elem.NodeName, addin); - + ExtensionNodeType ntype = addinEngine.FindType (transaction, parentNode.ExtensionNodeSet, elem.NodeName, addin); + if (ntype == null) { - addinEngine.ReportError ("Node '" + elem.NodeName + "' not allowed in extension: " + tnode.GetPath (), addin, null, false); + addinEngine.ReportError ("Node '" + elem.NodeName + "' not allowed in extension: " + parentNode.GetPath (), addin, null, false); continue; } - + string id = elem.GetAttribute ("id"); if (id.Length == 0) id = AutoIdPrefix + (++internalId); - TreeNode cnode = new TreeNode (addinEngine, id); - - ExtensionNode enode = ReadNode (cnode, addin, ntype, elem, module); + TreeNode childNode = new TreeNode (addinEngine, id); + + ExtensionNode enode = ReadNode (childNode, addin, ntype, elem, module, transaction); if (enode == null) continue; - cnode.Condition = parentCondition; - cnode.ExtensionNodeSet = ntype; - tnode.InsertChildNode (curPos, cnode); - addedNodes.Add (cnode); - - if (cnode.Condition != null) - Context.RegisterNodeCondition (cnode, cnode.Condition); + // Enables bulk update of children + parentNode.BeginChildrenUpdateTransaction (transaction); + + childNode.Condition = parentCondition; + childNode.ExtensionNodeSet = ntype; + parentNode.InsertChild (transaction, curPos, childNode); + addedNodes.Add (childNode); // Load children if (elem.ChildNodes.Count > 0) { int cp = 0; - LoadExtensionElement (cnode, addin, elem.ChildNodes, module, ref cp, parentCondition, false, addedNodes); + LoadExtensionElement (transaction, childNode, addin, elem.ChildNodes, module, ref cp, parentCondition, false, addedNodes); } curPos++; } - if (Context.FireEvents) - tnode.NotifyChildrenChanged (); } BaseCondition ReadComplexCondition (ExtensionNodeDescription elem, BaseCondition parentCondition) @@ -176,11 +173,11 @@ namespace Mono.Addins return new NullCondition (); } - public ExtensionNode ReadNode (TreeNode tnode, string addin, ExtensionNodeType ntype, ExtensionNodeDescription elem, ModuleDescription module) + public ExtensionNode ReadNode (TreeNode tnode, string addin, ExtensionNodeType ntype, ExtensionNodeDescription elem, ModuleDescription module, ExtensionContextTransaction transaction) { try { if (ntype.Type == null) { - if (!InitializeNodeType (ntype)) + if (!InitializeNodeType (ntype, transaction)) return null; } @@ -202,19 +199,18 @@ namespace Mono.Addins } } - bool InitializeNodeType (ExtensionNodeType ntype) + bool InitializeNodeType (ExtensionNodeType ntype, ExtensionContextTransaction transaction) { - RuntimeAddin p = addinEngine.GetAddin (ntype.AddinId); - if (p == null) { - if (!addinEngine.IsAddinLoaded (ntype.AddinId)) { - if (!addinEngine.LoadAddin (null, ntype.AddinId, false)) - return false; - p = addinEngine.GetAddin (ntype.AddinId); - if (p == null) { - addinEngine.ReportError ("Add-in not found", ntype.AddinId, null, false); - return false; - } + RuntimeAddin p = addinEngine.GetAddin(transaction.GetAddinEngineTransaction(), ntype.AddinId); + if (p == null) + { + var engineTransaction = transaction.GetOrCreateAddinEngineTransaction(); + if (!addinEngine.LoadAddin(engineTransaction, null, ntype.AddinId, false)) + { + addinEngine.ReportError("Add-in not found", ntype.AddinId, null, false); + return false; } + p = addinEngine.GetAddin(engineTransaction, ntype.AddinId); } // If no type name is provided, use TypeExtensionNode by default diff --git a/Mono.Addins/Mono.Addins/InstanceExtensionNode.cs b/Mono.Addins/Mono.Addins/InstanceExtensionNode.cs index 3d6640d..37e2791 100644 --- a/Mono.Addins/Mono.Addins/InstanceExtensionNode.cs +++ b/Mono.Addins/Mono.Addins/InstanceExtensionNode.cs @@ -69,8 +69,13 @@ namespace Mono.Addins /// </remarks> public object GetInstance () { - if (cachedInstance == null) - cachedInstance = CreateInstance (); + if (cachedInstance == null) { + lock (localLock) { + // Use locking here to avoid creating more than one instance per ExtensionNode + if (cachedInstance == null) + cachedInstance = CreateInstance (); + } + } return cachedInstance; } diff --git a/Mono.Addins/Mono.Addins/RuntimeAddin.cs b/Mono.Addins/Mono.Addins/RuntimeAddin.cs index 9b05ba2..abbbf1f 100644 --- a/Mono.Addins/Mono.Addins/RuntimeAddin.cs +++ b/Mono.Addins/Mono.Addins/RuntimeAddin.cs @@ -49,27 +49,37 @@ namespace Mono.Addins /// </summary> public class RuntimeAddin { - string id; - string baseDirectory; + readonly string id; + readonly string baseDirectory; + readonly Addin ainfo; + readonly RuntimeAddin parentAddin; + readonly AddinEngine addinEngine; + readonly ModuleDescription module; + string privatePath; - Addin ainfo; - RuntimeAddin parentAddin; Dictionary<string, Assembly> loadedAssemblies = new Dictionary<string, Assembly>(); bool fullyLoadedAssemblies; - - RuntimeAddin[] depAddins; - ResourceManager[] resourceManagers; + + RuntimeAddin [] depAddins; + ResourceManager [] resourceManagers; AddinLocalizer localizer; - ModuleDescription module; - AddinEngine addinEngine; ExtensionNodeDescription localizerDescription; - internal RuntimeAddin (AddinEngine addinEngine) + internal RuntimeAddin (AddinEngine addinEngine, Addin iad) { this.addinEngine = addinEngine; + + ainfo = iad; + + AddinDescription description = iad.Description; + id = description.AddinId; + baseDirectory = description.BasePath; + module = description.MainModule; + module.RuntimeAddin = this; + localizerDescription = description.Localizer; } - + internal RuntimeAddin (AddinEngine addinEngine, RuntimeAddin parentAddin, ModuleDescription module) { this.addinEngine = addinEngine; @@ -345,7 +355,7 @@ namespace Mono.Addins foreach (var kvp in loadedAssemblies) { var assembly = kvp.Value; - if (string.IsNullOrEmpty (assemblyName) || assembly.FullName == assemblyName) { + if (string.IsNullOrEmpty (assemblyName) || assembly.GetName().Name == assemblyName) { Type type = assembly.GetType (typeName, false); if (type != null) return type; @@ -619,19 +629,6 @@ namespace Mono.Addins return addin; } - internal AddinDescription Load (Addin iad) - { - ainfo = iad; - - AddinDescription description = iad.Description; - id = description.AddinId; - baseDirectory = description.BasePath; - module = description.MainModule; - module.RuntimeAddin = this; - localizerDescription = description.Localizer; - return description; - } - AddinLocalizer LoadLocalizer () { if (localizerDescription != null) { @@ -723,9 +720,9 @@ namespace Mono.Addins } } - internal void UnloadExtensions () + internal void UnloadExtensions (ExtensionContextTransaction transaction) { - addinEngine.UnregisterAddinNodeSets (id); + addinEngine.UnregisterAddinNodeSets (transaction, id); } bool CheckAddinDependencies (ModuleDescription module, bool forceLoadAssemblies) @@ -734,10 +731,11 @@ namespace Mono.Addins AddinDependency pdep = dep as AddinDependency; if (pdep == null) continue; - if (!addinEngine.IsAddinLoaded (pdep.FullAddinId)) + var addin = addinEngine.GetAddin(pdep.FullAddinId); + if (addin == null) return false; if (forceLoadAssemblies) - addinEngine.GetAddin (pdep.FullAddinId).EnsureAssembliesLoaded (); + addin.EnsureAssembliesLoaded (); } return true; } diff --git a/Mono.Addins/Mono.Addins/TreeNode.cs b/Mono.Addins/Mono.Addins/TreeNode.cs index e7fa478..6beab23 100644 --- a/Mono.Addins/Mono.Addins/TreeNode.cs +++ b/Mono.Addins/Mono.Addins/TreeNode.cs @@ -32,15 +32,20 @@ using System.Text; using System.Collections; using Mono.Addins.Description; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; namespace Mono.Addins { class TreeNode { - List<TreeNode> childrenList; - TreeNodeCollection children; + ImmutableArray<TreeNode> children; + + ImmutableArray<TreeNode>.Builder childrenBuilder; + ExtensionContextTransaction buildTransaction; + ExtensionNode extensionNode; - bool childrenLoaded; + bool childrenFromExtensionsLoaded; string id; TreeNode parent; ExtensionNodeSet nodeTypes; @@ -52,10 +57,12 @@ namespace Mono.Addins { this.id = id; this.addinEngine = addinEngine; - + + children = ImmutableArray<TreeNode>.Empty; + // Root node if (id.Length == 0) - childrenLoaded = true; + childrenFromExtensionsLoaded = true; } public AddinEngine AddinEngine { @@ -64,7 +71,11 @@ namespace Mono.Addins internal void AttachExtensionNode (ExtensionNode enode) { - this.extensionNode = enode; + if (Interlocked.CompareExchange (ref extensionNode, enode, null) != null) { + // Another thread already assigned the node + return; + } + if (extensionNode != null) extensionNode.SetTreeNode (this); } @@ -76,14 +87,36 @@ namespace Mono.Addins public ExtensionNode ExtensionNode { get { if (extensionNode == null && extensionPoint != null) { - extensionNode = new ExtensionNode (); - extensionNode.SetData (addinEngine, extensionPoint.RootAddin, null, null); - AttachExtensionNode (extensionNode); + var newNode = new ExtensionNode (); + newNode.SetData (addinEngine, extensionPoint.RootAddin, null, null); + AttachExtensionNode (newNode); } return extensionNode; } } - + + public void NotifyAddinUnloaded () + { + extensionNode?.NotifyAddinUnloaded (); + } + + public void NotifyAddinLoaded () + { + extensionNode?.NotifyAddinLoaded (); + } + + public bool HasExtensionNode { + get { + return extensionNode != null || extensionPoint != null; + } + } + + public string AddinId { + get { + return extensionNode != null ? extensionNode.AddinId : extensionPoint?.RootAddin; + } + } + public ExtensionPoint ExtensionPoint { get { return extensionPoint; } set { extensionPoint = value; } @@ -126,33 +159,91 @@ namespace Mono.Addins } } - public bool ChildrenLoaded { - get { return childrenLoaded; } + public bool ChildrenFromExtensionsLoaded { + get { return childrenFromExtensionsLoaded; } } - public void AddChildNode (TreeNode node) + public void AddChildNode (ExtensionContextTransaction transaction, TreeNode node) { - node.parent = this; - if (childrenList == null) - childrenList = new List<TreeNode> (); - childrenList.Add (node); + node.SetParent(transaction, this); + + if (childrenBuilder != null) + childrenBuilder.Add (node); + else + children = children.Add (node); + + transaction.ReportChildrenChanged (this); } - - public void InsertChildNode (int n, TreeNode node) + + public void InsertChild (ExtensionContextTransaction transaction, int n, TreeNode node) { - node.parent = this; - if (childrenList == null) - childrenList = new List<TreeNode> (); - childrenList.Insert (n, node); - - // Dont call NotifyChildrenChanged here. It is called by ExtensionTree, - // after inserting all children of the node. + node.SetParent (transaction, this); + + if (childrenBuilder != null) + childrenBuilder.Insert (n, node); + else + children = children.Insert (n, node); + + transaction.ReportChildrenChanged (this); } - - internal int ChildCount { - get { return childrenList == null ? 0 : childrenList.Count; } + + public void RemoveChild (ExtensionContextTransaction transaction, TreeNode node) + { + node.SetParent (transaction, null); + + if (childrenBuilder != null) + childrenBuilder.Remove (node); + else + children = children.Remove (node); + + transaction.ReportChildrenChanged (this); } - + + void SetParent (ExtensionContextTransaction transaction, TreeNode newParent) + { + if (parent == newParent) + return; + + if (this.parent != null && newParent != null) + throw new InvalidOperationException ("Node already has a parent"); + + var currentCtx = Context; + if (currentCtx != null && currentCtx != transaction.Context) + throw new InvalidOperationException ("Invalid context"); + + this.parent = newParent; + + if (newParent != null) { + if (newParent.Context != null) + OnAttachedToContext (transaction); + } else { + if (currentCtx != null) + OnDetachedFromContext (transaction); + } + } + + void OnAttachedToContext (ExtensionContextTransaction transaction) + { + // Once the node is part of a context, let's register the condition + if (Condition != null) + transaction.RegisterNodeCondition (this, Condition); + + // Propagate event to children + foreach (var child in GetLoadedChildren ()) + child.OnAttachedToContext (transaction); + } + + void OnDetachedFromContext (ExtensionContextTransaction transaction) + { + // Node being removed, unregister from context + if (Condition != null) + transaction.UnregisterNodeCondition (this, Condition); + + // Propagate event to children + foreach (var child in GetLoadedChildren ()) + child.OnDetachedFromContext (transaction); + } + public ExtensionNode GetExtensionNode (string path, string childId) { TreeNode node = GetNode (path, childId); @@ -177,8 +268,13 @@ namespace Mono.Addins { return GetNode (path, false); } - - public TreeNode GetNode (string path, bool buildPath) + + public TreeNode GetNode(string path, bool buildPath) + { + return GetNode(path, buildPath, null); + } + + public TreeNode GetNode (string path, bool buildPath, ExtensionContextTransaction transaction) { if (path.StartsWith ("/")) path = path.Substring (1); @@ -187,38 +283,153 @@ namespace Mono.Addins TreeNode curNode = this; foreach (string part in parts) { - int i = curNode.Children.IndexOfNode (part); - if (i != -1) { - curNode = curNode.Children [i]; + curNode.EnsureChildrenLoaded(transaction); + var node = curNode.GetChildNode (part); + if (node != null) { + curNode = node; continue; } if (buildPath) { - TreeNode newNode = new TreeNode (addinEngine, part); - curNode.AddChildNode (newNode); - curNode = newNode; + transaction = BeginContextTransaction (transaction, out var dispose); + try { + // Check again inside the lock, just in case + curNode.EnsureChildrenLoaded(transaction); + node = curNode.GetChildNode (part); + if (node != null) { + curNode = node; + continue; + } + + TreeNode newNode = new TreeNode (addinEngine, part); + curNode.AddChildNode (transaction, newNode); + curNode = newNode; + } finally { + if (dispose) + transaction.Dispose (); + } } else return null; } return curNode; } - - public TreeNodeCollection Children { + + public TreeNode GetChildNode (string id) + { + var childrenList = Children; + foreach (var node in childrenList) { + if (node.Id == id) + return node; + } + return null; + } + + public int IndexOfChild (string id) + { + var childrenList = Children; + for (int n = 0; n < childrenList.Count; n++) { + if (childrenList [n].Id == id) + return n; + } + return -1; + } + + public IReadOnlyList<TreeNode> Children { get { - if (!childrenLoaded) { - childrenLoaded = true; - if (extensionPoint != null) - Context.LoadExtensions (GetPath ()); - // We have to keep the relation info, since add-ins may be loaded/unloaded + if (IsInChildrenUpdateTransaction) { + return childrenBuilder; } - if (childrenList == null) - return TreeNodeCollection.Empty; - if (children == null) - children = new TreeNodeCollection (childrenList); + EnsureChildrenLoaded(null); + return (IReadOnlyList<TreeNode>)childrenBuilder ?? (IReadOnlyList<TreeNode>)children; + } + } + + void EnsureChildrenLoaded(ExtensionContextTransaction transaction) + { + if (IsInChildrenUpdateTransaction) + return; + + if (!childrenFromExtensionsLoaded) + { + transaction = BeginContextTransaction(transaction, out var disposeTransaction); + try + { + if (!childrenFromExtensionsLoaded) + { + if (extensionPoint != null) + { + BeginChildrenUpdateTransaction(transaction); + Context.LoadExtensions(transaction, GetPath(), this); + // We have to keep the reference to the extension point, since add-ins may be loaded/unloaded + } + } + } + finally + { + childrenFromExtensionsLoaded = true; + if (disposeTransaction) + transaction.Dispose(); + } + } + } + + IReadOnlyList<TreeNode> GetLoadedChildren () + { + if (IsInChildrenUpdateTransaction) + return childrenBuilder; + else return children; + } + + /// <summary> + /// Returns true if the tree node has a child update transaction in progress, + /// and the current thread is the one that created the transaction. + /// </summary> + bool IsInChildrenUpdateTransaction => childrenBuilder != null && Context.IsCurrentThreadInTransaction; + + ExtensionContextTransaction BeginContextTransaction (ExtensionContextTransaction currentTransaction, out bool dispose) + { + if (currentTransaction != null) + { + dispose = false; + return currentTransaction; + } + else if (IsInChildrenUpdateTransaction) { + dispose = false; + return buildTransaction; + } else { + dispose = true; + return Context.BeginTransaction (); } } - + + public void BeginChildrenUpdateTransaction (ExtensionContextTransaction transaction) + { + // If a transaction already started, just reuse it + if (buildTransaction != null) + return; + + childrenBuilder = children.ToBuilder (); + this.buildTransaction = transaction; + + transaction.RegisterChildrenUpdateTransaction (this); + } + + internal void CommitChildrenUpdateTransaction () + { + if (buildTransaction == null) + throw new InvalidOperationException ("No transaction started"); + + children = childrenBuilder.ToImmutable (); + + var transaction = buildTransaction; + + childrenBuilder = null; + buildTransaction = null; + + transaction.ReportChildrenChanged (this); + } + public string GetPath () { int num=0; @@ -241,9 +452,9 @@ namespace Mono.Addins public void NotifyAddinLoaded (RuntimeAddin ad, bool recursive) { if (extensionNode != null && extensionNode.AddinId == ad.Addin.Id) - extensionNode.OnAddinLoaded (); - if (recursive && childrenLoaded) { - foreach (TreeNode node in Children.Clone ()) + extensionNode.NotifyAddinLoaded (); + if (recursive && childrenFromExtensionsLoaded) { + foreach (TreeNode node in Children) node.NotifyAddinLoaded (ad, true); } } @@ -257,10 +468,10 @@ namespace Mono.Addins TreeNode curNode = this; foreach (string part in parts) { - int i = curNode.Children.IndexOfNode (part); - if (i != -1) { - curNode = curNode.Children [i]; - if (!curNode.ChildrenLoaded) + var node = curNode.GetChildNode (part); + if (node != null) { + curNode = node; + if (!curNode.ChildrenFromExtensionsLoaded) return null; if (curNode.ExtensionPoint != null) return curNode.ExtensionPoint; @@ -279,56 +490,16 @@ namespace Mono.Addins id = null; } - if (childrenLoaded) { + if (childrenFromExtensionsLoaded) { // Deep-first search, to make sure children are removed before the parent. foreach (TreeNode node in Children) node.FindAddinNodes (id, nodes); } - if (id == null || (ExtensionNode != null && ExtensionNode.AddinId == id)) + if (id == null || AddinId == id) nodes.Add (this); } - public bool FindExtensionPathByType (IProgressStatus monitor, Type type, string nodeName, out string path, out string pathNodeName) - { - if (extensionPoint != null) { - foreach (ExtensionNodeType nt in extensionPoint.NodeSet.NodeTypes) { - if (nt.ObjectTypeName.Length > 0 && (nodeName.Length == 0 || nodeName == nt.Id)) { - RuntimeAddin addin = addinEngine.GetAddin (extensionPoint.RootAddin); - Type ot = addin.GetType (nt.ObjectTypeName, true); - if (ot != null) { - if (ot.IsAssignableFrom (type)) { - path = extensionPoint.Path; - pathNodeName = nt.Id; - return true; - } - } - else - monitor.ReportError ("Type '" + nt.ObjectTypeName + "' not found in add-in '" + Id + "'", null); - } - } - } - else { - foreach (TreeNode node in Children) { - if (node.FindExtensionPathByType (monitor, type, nodeName, out path, out pathNodeName)) - return true; - } - } - path = null; - pathNodeName = null; - return false; - } - - public void Remove () - { - if (parent != null) { - if (Condition != null) - Context.UnregisterNodeCondition (this, Condition); - parent.childrenList.Remove (this); - parent.NotifyChildrenChanged (); - } - } - public bool NotifyChildrenChanged () { if (extensionNode != null) @@ -337,18 +508,21 @@ namespace Mono.Addins return false; } - public void ResetCachedData () + public void ResetCachedData (ExtensionContextTransaction transaction) { if (extensionPoint != null) { string aid = Addin.GetIdName (extensionPoint.ParentAddinDescription.AddinId); - RuntimeAddin ad = addinEngine.GetAddin (aid); + RuntimeAddin ad = addinEngine.GetAddin (transaction.GetAddinEngineTransaction(), aid); if (ad != null) extensionPoint = ad.Addin.Description.ExtensionPoints [GetPath ()]; } - if (childrenList != null) { - foreach (TreeNode cn in childrenList) - cn.ResetCachedData (); + if (childrenBuilder != null) { + foreach (TreeNode cn in childrenBuilder) + cn.ResetCachedData (transaction); } + + foreach (TreeNode cn in children) + cn.ResetCachedData (transaction); } } } diff --git a/Mono.Addins/Mono.Addins/TreeNodeCollection.cs b/Mono.Addins/Mono.Addins/TreeNodeCollection.cs deleted file mode 100644 index 9da13b7..0000000 --- a/Mono.Addins/Mono.Addins/TreeNodeCollection.cs +++ /dev/null @@ -1,86 +0,0 @@ -// -// TreeNodeCollection.cs -// -// Author: -// Lluis Sanchez Gual -// -// Copyright (C) 2007 Novell, Inc (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Mono.Addins -{ - class TreeNodeCollection: IEnumerable - { - List<TreeNode> list; - - internal static TreeNodeCollection Empty = new TreeNodeCollection (null); - - public TreeNodeCollection (List<TreeNode> list) - { - this.list = list; - } - - public IEnumerator GetEnumerator () - { - if (list != null) - return list.GetEnumerator (); - else - return Type.EmptyTypes.GetEnumerator (); - } - - public TreeNode this [int n] { - get { - if (list != null) - return list [n]; - else - throw new System.IndexOutOfRangeException (); - } - } - - public int IndexOfNode (string id) - { - for (int n=0; n<Count; n++) { - if (this [n].Id == id) - return n; - } - return -1; - } - - public int Count { - get { return list != null ? list.Count : 0; } - } - - public TreeNodeCollection Clone () - { - if (list != null) - return new TreeNodeCollection (list.ToList ()); - else - return Empty; - } - } -} diff --git a/Test/UnitTests/TestMultithreading.cs b/Test/UnitTests/TestMultithreading.cs new file mode 100644 index 0000000..646cdef --- /dev/null +++ b/Test/UnitTests/TestMultithreading.cs @@ -0,0 +1,243 @@ + +using System; +using System.IO; +using NUnit.Framework; +using Mono.Addins; +using SimpleApp; +using System.Threading; +using System.Diagnostics; +using System.Linq; + +namespace UnitTests +{ + [TestFixture ()] + public class TestMultiThreading: TestBase + { + public override void Setup () + { + base.Setup (); + GlobalInfoCondition.Value = "res"; + } + + [Test] + public void ChildConditionAttributeMultithread () + { + int threads = 50; + using var testData = new TestData(threads); + + GlobalInfoCondition.Value = ""; + + // Threads check the number of nodes in /SimpleApp/ConditionedWriters, which changes + // from 0 to 1 as the GlobalInfoCondition condition changes + + testData.StartThreads (RunChildConditionAttributeTest); + + for (int n = 0; n < 20; n++) { + Console.WriteLine ("Step " + n); + testData.CheckCounters (0, 10000); + + GlobalInfoCondition.Value = "foo"; + + testData.CheckCounters (1, 10000); + + GlobalInfoCondition.Value = ""; + } + } + + void RunChildConditionAttributeTest (int index, TestData data) + { + while (!data.Stopped) { + var writers = AddinManager.GetExtensionObjects<IWriter> ("/SimpleApp/ConditionedWriters"); + data.Counters [index] = writers.Length; + } + } + + [Test] + public void EnableDisableMultithread () + { + int threads = 50; + int steps = 20; + + using var testData = new TestData (threads); + + testData.StartThreads ((index, data) => { + while (!data.Stopped) { + var writers = AddinManager.GetExtensionObjects<IWriter> ("/SimpleApp/Writers"); + testData.Counters [index] = writers.Length; + } + }); + + for (int n = 0; n < steps; n++) { + Console.WriteLine ("Step " + n); + + testData.CheckCounters (4, 10000); + + var ainfo1 = AddinManager.Registry.GetAddin ("SimpleApp.HelloWorldExtension"); + ainfo1.Enabled = false; + + testData.CheckCounters (3, 10000); + + var ainfo2 = AddinManager.Registry.GetAddin ("SimpleApp.FileContentExtension"); + ainfo2.Enabled = false; + + testData.CheckCounters (2, 10000); + + ainfo1.Enabled = true; + ainfo2.Enabled = true; + } + } + + [Test] + public void EventsMultithread() + { + int threads = 50; + + int totalAdded = 0; + int totalRemoved = 0; + int nodesCount = 0; + int minCount = 0; + int maxCount = 0; + + var node = AddinManager.GetExtensionNode("/SimpleApp/Writers"); + + nodesCount = 0; + + node.ExtensionNodeChanged += (s, args) => + { + if (args.Change == ExtensionChange.Add) + { + nodesCount++; + totalAdded++; + } + else + { + nodesCount--; + totalRemoved++; + } + + if (nodesCount < minCount) + minCount = nodesCount; + if (nodesCount > maxCount) + maxCount = nodesCount; + }; + + Assert.AreEqual(4, nodesCount); + + minCount = 4; + maxCount = 4; + totalAdded = 0; + totalRemoved = 0; + + var ainfo1 = AddinManager.Registry.GetAddin("SimpleApp.HelloWorldExtension"); + var ainfo2 = AddinManager.Registry.GetAddin("SimpleApp.FileContentExtension"); + + using var enablers = new TestData(threads); + + enablers.StartThreads((index, data) => + { + var random = new Random(10000 + index); + int iterations = 100; + while (--iterations > 0) + { + var action = random.Next(4); + switch (action) + { + case 0: ainfo1.Enabled = false; break; + case 1: ainfo1.Enabled = true; break; + case 2: ainfo2.Enabled = false; break; + case 3: ainfo2.Enabled = true; break; + } + } + data.Counters[index] = 1; + }); + + // Wait for the threads to do the work. 5 seconds should be enough + enablers.CheckCounters(1, 20000); + + // Go back to the initial status + + ainfo1.Enabled = true; + ainfo2.Enabled = true; + + // If all events have been sent correctly, the node count should have never gone below 2 and over 4. + + Assert.That(nodesCount, Is.EqualTo(4)); + Assert.That(totalAdded, Is.AtLeast(100)); + Assert.That(totalAdded, Is.EqualTo(totalRemoved)); + Assert.That(minCount, Is.AtLeast(2)); + Assert.That(maxCount, Is.AtMost(4)); + } + + [Test] + public void ChildConditionWithContextMultithread () + { + int threads = 50; + using var testData = new TestData (threads); + + GlobalInfoCondition.Value = ""; + + testData.StartThreads (RunChildConditionWithContextMultithread); + + for (int n = 0; n < 20; n++) { + Console.WriteLine ("Step " + n); + testData.CheckCounters (0, 10000); + + GlobalInfoCondition.Value = "foo"; + + testData.CheckCounters (1, 10000); + + GlobalInfoCondition.Value = ""; + } + } + + void RunChildConditionWithContextMultithread (int index, TestData data) + { + var ctx = AddinManager.CreateExtensionContext (); + var writers = ctx.GetExtensionNode ("/SimpleApp/ConditionedWriters"); + while (!data.Stopped) { + data.Counters [index] = writers.GetChildNodes().Count; + } + } + + class TestData: IDisposable + { + int numThreads; + + public bool Stopped; + public int [] Counters; + + public TestData (int numThreads) + { + this.numThreads = numThreads; + Counters = new int [numThreads]; + } + + public void CheckCounters (int expectedResult, int timeout) + { + var sw = Stopwatch.StartNew (); + do { + if (Counters.All (c => c == expectedResult)) + return; + Thread.Sleep (10); + } while (sw.ElapsedMilliseconds < timeout); + + Assert.Fail ("Expected " + expectedResult); + } + + public void Dispose () + { + Stopped = true; + } + + public void StartThreads (Action<int, TestData> threadAction) + { + for (int n = 0; n < numThreads; n++) { + Counters [n] = -1; + var index = n; + var t = new Thread (() => threadAction(index, this)); + t.Start (); + } + } + } + } +} diff --git a/Test/UnitTests/UnitTests.csproj b/Test/UnitTests/UnitTests.csproj index 30badc8..a504119 100644 --- a/Test/UnitTests/UnitTests.csproj +++ b/Test/UnitTests/UnitTests.csproj @@ -9,7 +9,6 @@ <AssemblyOriginatorKeyFile>..\..\mono-addins.snk</AssemblyOriginatorKeyFile> <TargetFrameworks>net472;net6.0</TargetFrameworks> <LangVersion>9.0</LangVersion> - <DebugType>embedded</DebugType> </PropertyGroup> <ItemGroup> <Reference Include="System" /> diff --git a/Version.props b/Version.props index 6bbc3db..7eec2fb 100644 --- a/Version.props +++ b/Version.props @@ -1,6 +1,6 @@ <Project> <PropertyGroup> - <PackageVersion>1.3.14</PackageVersion> + <PackageVersion>1.3.15-alpha.3</PackageVersion> <Authors>Microsoft</Authors> <Owners>microsoft, xamarin</Owners> <PackageLicenseUrl>https://github.com/mono/mono-addins/blob/main/COPYING</PackageLicenseUrl> |