diff options
author | Lluis Sanchez <llsan@microsoft.com> | 2022-09-13 20:34:02 +0300 |
---|---|---|
committer | Lluis Sanchez <llsan@microsoft.com> | 2022-09-13 20:38:42 +0300 |
commit | 377ecba0c4d6ce6c87c8b9fe748dcdbfaebd7ddd (patch) | |
tree | 076f169f9232422789ba6c4988d8b3ee1e3605f1 | |
parent | 3cf96414944ee8c5d1da11f97195a5163d610397 (diff) |
More thread safety
Store extension content data in a snapshot class, so that it is possible
to swap it all at once when committing a transaction.
Propagate transactions to more methods.
Events and virtual methods are now guaranteed to be executed sequentially,
and never concurrently.
Add threading docs.
-rw-r--r-- | Mono.Addins/Mono.Addins.Database/AddinDatabase.cs | 136 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/AddinEngine.cs | 231 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/AddinManager.cs | 29 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/AddinRegistry.cs | 5 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/ConditionType.cs | 3 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/ExtensionContext.cs | 337 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/ExtensionContextTransaction.cs | 173 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/ExtensionNode.cs | 171 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/ExtensionTree.cs | 28 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/RuntimeAddin.cs | 5 | ||||
-rw-r--r-- | Mono.Addins/Mono.Addins/TreeNode.cs | 40 | ||||
-rw-r--r-- | Version.props | 2 |
12 files changed, 722 insertions, 438 deletions
diff --git a/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs b/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs index 0043774..73cb55c 100644 --- a/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs +++ b/Mono.Addins/Mono.Addins.Database/AddinDatabase.cs @@ -75,11 +75,13 @@ namespace Mono.Addins.Database fileDatabase = new FileDatabase (AddinDbDir); } - public AddinDatabaseTransaction BeginTransaction () + public AddinDatabaseTransaction BeginTransaction (ExtensionContextTransaction addinEngineTransaction = null) { - return new AddinDatabaseTransaction (this, localLock); + return new AddinDatabaseTransaction (this, localLock, addinEngineTransaction); } - + + internal AddinEngine AddinEngine => addinEngine; + string AddinDbDir { get { return addinDbDir; } } @@ -489,8 +491,7 @@ namespace Mono.Addins.Database SaveConfiguration (dbTransaction); if (addinEngine != null && addinEngine.IsInitialized) { - using var transaction = addinEngine.BeginTransaction (); - addinEngine.ActivateAddin (transaction, id); + addinEngine.ActivateAddin (dbTransaction.GetAddinEngineTransaction(), id); } } @@ -548,8 +549,7 @@ namespace Mono.Addins.Database } if (addinEngine != null && addinEngine.IsInitialized) { - using var transaction = addinEngine.BeginTransaction (); - addinEngine.UnloadAddin (transaction, id); + addinEngine.UnloadAddin (dbTransaction.GetAddinEngineTransaction(), id); } } @@ -1023,7 +1023,7 @@ namespace Mono.Addins.Database allSetupInfosLoaded = false; } - internal void ResetCachedData () + internal void ResetCachedData (AddinDatabaseTransaction dbTransaction = null) { ResetBasicCachedData (); hostIndex = null; @@ -1031,7 +1031,7 @@ namespace Mono.Addins.Database cachedAddinSetupInfos.Clear (); dependsOnCache.Clear (); if (addinEngine != null) - addinEngine.ResetCachedData (); + addinEngine.ResetCachedData (dbTransaction?.GetAddinEngineTransaction()); } Dictionary<string, HashSet<string>> dependsOnCache = new Dictionary<string, HashSet<string>> (); @@ -1095,79 +1095,84 @@ 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, ExtensionContextTransaction addinEngineTransaction = null) { if (monitor == null) - monitor = new ConsoleProgressStatus (false); + monitor = new ConsoleProgressStatus(false); if (RunningSetupProcess) return; - + fatalDatabseError = false; - + DateTime tim = DateTime.Now; - using (var dbTransaction = BeginTransaction ()) { - RunPendingUninstalls (dbTransaction, monitor); - } - - Hashtable installed = new Hashtable (); - bool changesFound = CheckFolders (monitor, domain); - + 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) { - foreach (Addin ainfo in InternalGetInstalledAddins (domain, AddinSearchFlagsInternal.IncludeAddins, false)) { - 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; } - using (var transaction = addinEngine.BeginTransaction ()) { - 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 (transaction, aid); - } + 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(dbTransaction.GetAddinEngineTransaction(), aid); + } - foreach (string aid in newInstalled.Keys) { - if (!installed.Contains (aid)) { - Addin addin = addinEngine.Registry.GetAddin (aid); - if (addin != null) - addinEngine.ActivateAddin (transaction, aid); - } + foreach (string aid in newInstalled.Keys) + { + if (!installed.Contains(aid)) + { + Addin addin = addinEngine.Registry.GetAddin(aid); + if (addin != null) + addinEngine.ActivateAddin(dbTransaction.GetAddinEngineTransaction(), aid); } } } - using var dbTransation = BeginTransaction (); - UpdateEnabledStatus (dbTransation); + UpdateEnabledStatus(dbTransaction); } void RunPendingUninstalls (AddinDatabaseTransaction dbTransaction, IProgressStatus monitor) @@ -2015,16 +2020,29 @@ namespace Mono.Addins.Database { readonly AddinDatabase addinDatabase; readonly object localLock; + ExtensionContextTransaction addinEngineTransaction; + bool addinEngineTransactionStarted; - public AddinDatabaseTransaction (AddinDatabase addinDatabase, object localLock) + public AddinDatabaseTransaction (AddinDatabase addinDatabase, object localLock, ExtensionContextTransaction addinEngineTransaction) { this.addinDatabase = addinDatabase; this.localLock = localLock; + this.addinEngineTransaction = addinEngineTransaction; Monitor.Enter (localLock); } + public ExtensionContextTransaction GetAddinEngineTransaction() + { + if (addinEngineTransaction != null) + return addinEngineTransaction; + addinEngineTransactionStarted = true; + return addinEngineTransaction = addinDatabase.AddinEngine.BeginTransaction(); + } + public void Dispose () { + if (addinEngineTransactionStarted) + addinEngineTransaction.Dispose(); Monitor.Exit (localLock); } } diff --git a/Mono.Addins/Mono.Addins/AddinEngine.cs b/Mono.Addins/Mono.Addins/AddinEngine.cs index 1e2c39a..69a4112 100644 --- a/Mono.Addins/Mono.Addins/AddinEngine.cs +++ b/Mono.Addins/Mono.Addins/AddinEngine.cs @@ -37,7 +37,6 @@ 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 @@ -61,10 +60,35 @@ namespace Mono.Addins IAddinInstaller installer; bool checkAssemblyLoadConflicts; - ImmutableDictionary<string,RuntimeAddin> loadedAddins = ImmutableDictionary<string,RuntimeAddin>.Empty; - ImmutableDictionary<string,ExtensionNodeSet> nodeSets = ImmutableDictionary<string, ExtensionNodeSet>.Empty; - ImmutableDictionary<string,string> autoExtensionTypes = ImmutableDictionary<string, string>.Empty; - ImmutableDictionary<string, RuntimeAddin> assemblyResolvePaths = ImmutableDictionary<string, RuntimeAddin>.Empty; + + // 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); @@ -94,7 +118,18 @@ namespace Mono.Addins public AddinEngine () { } - + + internal override ExtensionContextSnapshot CreateSnapshot () + { + return new AddinEngineSnapshot(); + } + + internal override void SetSnapshot (ExtensionContextSnapshot newSnapshot) + { + currentSnapshot = (AddinEngineSnapshot)newSnapshot; + base.SetSnapshot(newSnapshot); + } + /// <summary> /// Initializes the add-in engine /// </summary> @@ -265,7 +300,7 @@ namespace Mono.Addins { string assemblyName = args.Name; - if (assemblyResolvePaths.TryGetValue (assemblyName, out var inAddin)) { + if (currentSnapshot.AssemblyResolvePaths.TryGetValue (assemblyName, out var inAddin)) { if (inAddin.TryGetAssembly (assemblyName, out var assembly)) return assembly; @@ -291,7 +326,6 @@ namespace Mono.Addins initialized = false; AppDomain.CurrentDomain.AssemblyLoad -= new AssemblyLoadEventHandler (OnAssemblyLoaded); AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomainAssemblyResolve; - loadedAddins = ImmutableDictionary<string, RuntimeAddin>.Empty; registry.Dispose (); registry = null; startupDirectory = null; @@ -394,7 +428,7 @@ namespace Mono.Addins { ValidateAddinRoots (); - assemblyResolvePaths.TryGetValue(asm.FullName, out var ad); + currentSnapshot.AssemblyResolvePaths.TryGetValue(asm.FullName, out var ad); return ad; } @@ -459,19 +493,46 @@ namespace Mono.Addins /// </returns> public bool IsAddinLoaded (string id) { - CheckInitialized (); - ValidateAddinRoots (); - return loadedAddins.ContainsKey (Addin.GetIdName (id)); + return IsAddinLoaded(currentSnapshot, id); } - + + bool IsAddinLoaded(AddinEngineSnapshot snapshot, string id) + { + CheckInitialized(); + ValidateAddinRoots(); + return snapshot.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 RuntimeAddin GetAddin(ExtensionContextTransaction transaction, string id) + { + ValidateAddinRoots(); + RuntimeAddin a; + transaction.AddinEngineSnapshot.LoadedAddins.TryGetValue(Addin.GetIdName(id), out a); + return a; + } + + internal RuntimeAddin GetOrLoadAddin(string id, bool throwExceptions) + { + ValidateAddinRoots(); + RuntimeAddin a; + currentSnapshot.LoadedAddins.TryGetValue(Addin.GetIdName(id), out a); + if (a == null) + { + using var tr = BeginTransaction(); + 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); @@ -481,10 +542,10 @@ namespace Mono.Addins { RemoveAddinExtensions (transaction, id); - RuntimeAddin addin = GetAddin (id); + RuntimeAddin addin = GetAddin (transaction, id); if (addin != null) { addin.UnloadExtensions (transaction); - loadedAddins = loadedAddins.Remove (Addin.GetIdName (id)); + transaction.AddinEngineSnapshot.LoadedAddins = transaction.AddinEngineSnapshot.LoadedAddins.Remove (Addin.GetIdName (id)); foreach (var asm in addin.Module.AssemblyNames) { transaction.UnregisterAssemblyResolvePaths (asm); } @@ -492,11 +553,6 @@ namespace Mono.Addins } } - internal void BulkUnregisterAssemblyResolvePaths (IEnumerable<string> assemblies) - { - assemblyResolvePaths = assemblyResolvePaths.RemoveRange (assemblies); - } - /// <summary> /// Forces the loading of an add-in. /// </summary> @@ -515,16 +571,17 @@ namespace Mono.Addins public void LoadAddin (IProgressStatus statusMonitor, string id) { CheckInitialized (); - if (LoadAddin (statusMonitor, id, true)) { - var adn = GetAddin (id); + using var transaction = BeginTransaction(); + 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 (ExtensionContextTransaction transaction, IProgressStatus statusMonitor, string id, bool throwExceptions) { try { - if (IsAddinLoaded (id)) + if (IsAddinLoaded (transaction.AddinEngineSnapshot, id)) return true; if (!Registry.IsAddinEnabled (id)) { @@ -537,14 +594,13 @@ namespace Mono.Addins var addins = new List<Addin> (); Stack depCheck = new Stack (); - ResolveLoadDependencies (addins, depCheck, id, false); + ResolveLoadDependencies (transaction, addins, depCheck, id, false); addins.Reverse (); if (statusMonitor != null) statusMonitor.SetMessage ("Loading Addins"); if (addins.Count > 0) { - ExtensionContextTransaction transaction = null; try { for (int n = 0; n < addins.Count; n++) { @@ -552,15 +608,12 @@ namespace Mono.Addins statusMonitor.SetProgress ((double)n / (double)addins.Count); Addin iad = addins [n]; - if (IsAddinLoaded (iad.Id)) + if (IsAddinLoaded (transaction.AddinEngineSnapshot, iad.Id)) continue; if (statusMonitor != null) statusMonitor.SetMessage (string.Format (GettextCatalog.GetString ("Loading {0} add-in"), iad.Id)); - if (transaction == null) - transaction = BeginTransaction (); - if (!InsertAddin (transaction, statusMonitor, iad)) return false; } @@ -580,11 +633,11 @@ namespace Mono.Addins } } - internal override void ResetCachedData () + internal override void ResetCachedData (ExtensionContextTransaction transaction) { - foreach (RuntimeAddin ad in loadedAddins.Values) + foreach (RuntimeAddin ad in transaction.AddinEngineSnapshot.LoadedAddins.Values) ad.Addin.ResetCachedData (); - base.ResetCachedData (); + base.ResetCachedData (transaction); } bool InsertAddin (ExtensionContextTransaction transaction, IProgressStatus statusMonitor, Addin iad) @@ -596,7 +649,7 @@ namespace Mono.Addins RegisterAssemblyResolvePaths (transaction, runtimeAddin, iad.Description.MainModule); // Register the add-in - loadedAddins = loadedAddins.SetItem(Addin.GetIdName (runtimeAddin.Id), runtimeAddin); + transaction.AddinEngineSnapshot.LoadedAddins = transaction.AddinEngineSnapshot.LoadedAddins.SetItem(Addin.GetIdName (runtimeAddin.Id), runtimeAddin); if (!AddinDatabase.RunningSetupProcess) { // Load the extension points and other addin data @@ -630,11 +683,6 @@ namespace Mono.Addins } } - internal void BulkRegisterAssemblyResolvePaths (IEnumerable<KeyValuePair<string,RuntimeAddin>> registrations) - { - assemblyResolvePaths = assemblyResolvePaths.SetItems (registrations); - } - internal void InsertExtensionPoint (ExtensionContextTransaction transaction, RuntimeAddin addin, ExtensionPoint ep) { CreateExtensionPoint (ep); @@ -645,9 +693,9 @@ namespace Mono.Addins } } - bool ResolveLoadDependencies (List<Addin> addins, Stack depCheck, string id, bool optional) + bool ResolveLoadDependencies (ExtensionContextTransaction transaction, List<Addin> addins, Stack depCheck, string id, bool optional) { - if (IsAddinLoaded (id)) + if (IsAddinLoaded (transaction.AddinEngineSnapshot, id)) return true; if (depCheck.Contains (id)) @@ -676,7 +724,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; @@ -691,7 +739,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; } } @@ -704,27 +752,25 @@ namespace Mono.Addins void RegisterNodeSets (ExtensionContextTransaction transaction, string addinId, ExtensionNodeSetCollection nsets) { - nodeSets = nodeSets.SetItems (nsets.Select (nset => { + foreach (ExtensionNodeSet nset in nsets) + { nset.SourceAddinId = addinId; - return new KeyValuePair<string, ExtensionNodeSet> (nset.Id, nset); - })); + nodeSets[nset.Id] = nset; + } } internal void UnregisterAddinNodeSets (ExtensionContextTransaction transaction, string addinId) { - nodeSets = nodeSets.RemoveRange (nodeSets.Where(n => n.Value.SourceAddinId == addinId).Select(n => n.Key)); + 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; @@ -736,11 +782,11 @@ 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; } @@ -754,11 +800,6 @@ namespace Mono.Addins transaction.RegisterAutoTypeExtensionPoint (typeName, path); } - internal void BulkRegisterAutoTypeExtensionPoint (List<KeyValuePair<string, string>> autoExtensionPoints) - { - autoExtensionTypes = autoExtensionTypes.AddRange (autoExtensionPoints); - } - internal void UnregisterAutoTypeExtensionPoint (ExtensionContextTransaction transaction, string typeName, string path) { if (Util.TryParseTypeName (typeName, out var t, out var _)) @@ -766,14 +807,12 @@ namespace Mono.Addins transaction.UnregisterAutoTypeExtensionPoint (typeName); } - internal void BulkUnregisterAutoTypeExtensionPoint (List<string> autoExtensionPointTypes) - { - autoExtensionTypes = autoExtensionTypes.RemoveRange (autoExtensionPointTypes); - } - + /// <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; } @@ -798,8 +837,9 @@ namespace Mono.Addins } } if (copy != null) { + using var tr = BeginTransaction(); foreach (Assembly asm in copy) - CheckHostAssembly (asm); + CheckHostAssembly (tr, asm); } } @@ -807,11 +847,13 @@ namespace Mono.Addins { lock (pendingRootChecks) pendingRootChecks.Clear (); + + using var tr = BeginTransaction(); foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ()) - CheckHostAssembly (asm); + CheckHostAssembly (tr, asm); } - void CheckHostAssembly (Assembly asm) + void CheckHostAssembly (ExtensionContextTransaction transaction, Assembly asm) { if (AddinDatabase.RunningSetupProcess || asm is System.Reflection.Emit.AssemblyBuilder || asm.IsDynamic) return; @@ -838,7 +880,7 @@ namespace Mono.Addins ainfo = Registry.GetAddinForHostAssembly (asmFile); } - if (ainfo != null && !IsAddinLoaded (ainfo.Id)) { + if (ainfo != null && !IsAddinLoaded (transaction.AddinEngineSnapshot, ainfo.Id)) { AddinDescription adesc = null; try { adesc = ainfo.Description; @@ -849,12 +891,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); } } @@ -894,39 +936,32 @@ namespace Mono.Addins internal void ReportAddinLoad (RuntimeAddin addin) { NotifyAddinLoaded (addin); - var handler = AddinLoaded; - if (handler != null) { - try { - handler (null, new AddinEventArgs (addin.Id)); - } catch { - // Ignore subscriber exceptions - } + 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/AddinManager.cs b/Mono.Addins/Mono.Addins/AddinManager.cs index b5bef3f..823bccc 100644 --- a/Mono.Addins/Mono.Addins/AddinManager.cs +++ b/Mono.Addins/Mono.Addins/AddinManager.cs @@ -688,7 +688,7 @@ namespace Mono.Addins AddinEngine.CheckInitialized (); return AddinEngine.GetExtensionObjects<T> (path, reuseCachedInstance); } - + /// <summary> /// Extension change event. /// </summary> @@ -698,6 +698,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 +723,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 +767,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 +791,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 +840,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 94e8123..1346883 100644 --- a/Mono.Addins/Mono.Addins/AddinRegistry.cs +++ b/Mono.Addins/Mono.Addins/AddinRegistry.cs @@ -639,6 +639,11 @@ namespace Mono.Addins database.Update (monitor, currentDomain); } + internal void Update(IProgressStatus monitor, ExtensionContextTransaction addinEngineTransaction) + { + database.Update(monitor, currentDomain, addinEngineTransaction:addinEngineTransaction); + } + /// <summary> /// Regenerates the cached data of the add-in registry. /// </summary> diff --git a/Mono.Addins/Mono.Addins/ConditionType.cs b/Mono.Addins/Mono.Addins/ConditionType.cs index 4f7b0db..d5bf6f6 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.BeginTransaction(); + 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 5e719d4..722b23f 100644 --- a/Mono.Addins/Mono.Addins/ExtensionContext.cs +++ b/Mono.Addins/Mono.Addins/ExtensionContext.cs @@ -37,29 +37,51 @@ 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 (); - ImmutableDictionary<string, ConditionInfo> conditionTypes = ImmutableDictionary<string, ConditionInfo>.Empty; - ImmutableDictionary<BaseCondition, ImmutableArray<TreeNode>> conditionsToNodes = ImmutableDictionary<BaseCondition, ImmutableArray<TreeNode>>.Empty; ImmutableArray<WeakReference> childContexts = ImmutableArray<WeakReference>.Empty; ExtensionContext parentContext; ExtensionTree tree; - ImmutableArray<string> runTimeEnabledAddins = ImmutableArray<string>.Empty; - ImmutableArray<string> runTimeDisabledAddins = ImmutableArray<string>.Empty; - + 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> @@ -69,14 +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) { + 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,18 +123,19 @@ namespace Mono.Addins } #pragma warning restore 1591 + internal void InvokeCallback(Action action, object source) + { + notificationQueue.Invoke(action, source); + } internal void ClearContext () { - conditionTypes = conditionTypes.Clear (); - conditionsToNodes.Clear (); + SetSnapshot(CreateSnapshot()); childContexts = ImmutableArray<WeakReference>.Empty; parentContext = null; tree = null; - runTimeEnabledAddins = ImmutableArray<string>.Empty; - runTimeDisabledAddins = ImmutableArray<string>.Empty; } - + internal AddinEngine AddinEngine { get { return tree.AddinEngine; } } @@ -125,11 +165,16 @@ namespace Mono.Addins } } } - - internal virtual void ResetCachedData () + + internal void ResetCachedData() + { + using (var transaction = BeginTransaction()) + ResetCachedData(transaction); + } + + internal virtual void ResetCachedData (ExtensionContextTransaction transaction) { - using (var transaction = BeginTransaction ()) - tree.ResetCachedData (transaction); + tree.ResetCachedData (transaction); foreach (var ctx in GetActiveChildContexes()) ctx.ResetCachedData (); @@ -191,12 +236,6 @@ namespace Mono.Addins GetOrCreateConditionInfo (transaction, id, type); } - void RegisterCondition (ExtensionContextTransaction transaction, string id, Type type) - { - // Allows delayed creation of condition types - GetOrCreateConditionInfo (transaction, id, type); - } - internal void RegisterCondition (ExtensionContextTransaction transaction, string id, RuntimeAddin addin, string typeName) { // Allows delayed creation of condition types @@ -206,12 +245,12 @@ namespace Mono.Addins }); } - ConditionInfo GetOrCreateConditionInfo (ExtensionContextTransaction transaction, string id, object conditionTypeObject) + internal ConditionInfo GetOrCreateConditionInfo (ExtensionContextTransaction transaction, string id, object conditionTypeObject) { - if (!conditionTypes.TryGetValue (id, out var info)) { + if (!transaction.Snapshot.ConditionTypes.TryGetValue (id, out var info)) { info = new ConditionInfo (); info.CondType = conditionTypeObject; - conditionTypes = conditionTypes.Add (id, info); + transaction.Snapshot.ConditionTypes = transaction.Snapshot.ConditionTypes.Add (id, info); } else { // If CondType is not changing, nothing else to do if (conditionTypeObject == null) @@ -230,7 +269,7 @@ namespace Mono.Addins internal ConditionType GetCondition (string id) { - if (conditionTypes.TryGetValue(id, out var info)) { + if (currentSnapshot.ConditionTypes.TryGetValue(id, out var info)) { if (info.CondType is ConditionType condition) { return condition; } @@ -269,87 +308,6 @@ namespace Mono.Addins return null; } - /// <summary> - /// Registers a set of node conditions - /// </summary> - internal void BulkRegisterNodeConditions (ExtensionContextTransaction transaction, IEnumerable<(TreeNode Node, BaseCondition Condition)> nodeConditions) - { - // We are going to do many changes, to create a builder for the dictionary - var dictBuilder = 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 = GetOrCreateConditionInfo (transaction, binding.Key, null); - info.BoundConditions = info.BoundConditions.AddRange (binding); - } - - conditionsToNodes = dictBuilder.ToImmutable (); - } - - /// <summary> - /// Unregisters a set of node conditions - /// </summary> - internal void BulkUnregisterNodeConditions (ExtensionContextTransaction transaction, 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 (!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 = 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 = conditionTypes [cid]; - if (info != null) - info.BoundConditions = info.BoundConditions.Remove (condition); - } - } else - dictBuilder [condition] = newList; - } - if (dictBuilder != null) - conditionsToNodes = dictBuilder.ToImmutable (); - } /// <summary> /// Returns the extension node in a path @@ -507,7 +465,7 @@ namespace Mono.Addins if (node == null || !node.HasExtensionNode) return ExtensionNodeList.Empty; - ExtensionNodeList list = node.ExtensionNode.ChildNodes; + ExtensionNodeList list = node.ExtensionNode.GetChildNodes(); if (expectedNodeType != null) { bool foundError = false; @@ -856,14 +814,16 @@ namespace Mono.Addins NotifyConditionChanged (cond); } - internal void NotifyConditionChanged (ConditionType cond) + void NotifyConditionChanged (ConditionType cond) { HashSet<TreeNode> parentsToNotify = null; - if (conditionTypes.TryGetValue (cond.Id, out var info) && info.BoundConditions != 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 (conditionsToNodes.TryGetValue(c, out var nodeList)) { + if (snapshot.ConditionsToNodes.TryGetValue(c, out var nodeList)) { parentsToNotify.UnionWith (nodeList.Select (node => node.Parent)); } } @@ -894,7 +854,12 @@ namespace Mono.Addins internal void NotifyExtensionsChanged (ExtensionEventArgs args) { if (ExtensionChanged != null) - ExtensionChanged (this, args); + { + notificationQueue.Invoke(() => + { + ExtensionChanged?.Invoke(this, args); + }, null); + } } internal void NotifyAddinLoaded (RuntimeAddin ad) @@ -941,7 +906,7 @@ namespace Mono.Addins // Add the new nodes foreach (ExtensionPoint ep in eps.Keys) { - ExtensionLoadData data = GetAddinExtensions (id, ep); + ExtensionLoadData data = GetAddinExtensions (transaction, id, ep); if (data != null) { foreach (Extension ext in data.Extensions) { TreeNode node = GetNode (ext.Path); @@ -984,7 +949,7 @@ namespace Mono.Addins // 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); + RuntimeAddin addin = AddinEngine.GetAddin (transaction, id); if (addin != null) { var paths = new List<string> (); // Using addin.Module.ParentAddinDescription here because addin.Addin.Description may not @@ -1002,21 +967,17 @@ namespace Mono.Addins void RegisterRuntimeDisabledAddin (ExtensionContextTransaction transaction, string addinId) { - if (!runTimeDisabledAddins.Contains (addinId)) - runTimeDisabledAddins = runTimeDisabledAddins.Add (addinId); - - runTimeEnabledAddins = runTimeEnabledAddins.Remove (addinId); + runTimeDisabledAddins.Add (addinId); + runTimeEnabledAddins.Remove (addinId); } void RegisterRuntimeEnabledAddin (ExtensionContextTransaction transaction, string addinId) { - if (!runTimeEnabledAddins.Contains (addinId)) - runTimeEnabledAddins = runTimeEnabledAddins.Add (addinId); - - runTimeDisabledAddins = 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; @@ -1024,23 +985,20 @@ namespace Mono.Addins // they may contain extension for this path. // Ignore addins disabled at run-time. - var enabledAddins = runTimeEnabledAddins; - - if (enabledAddins != null && enabledAddins.Length > 0) { + if (runTimeEnabledAddins.Count > 0) { newlist = new List<string> (); newlist.AddRange (col); - foreach (string s in enabledAddins) + foreach (string s in runTimeEnabledAddins) if (!newlist.Contains (s)) newlist.Add (s); } - var disabledAddins = runTimeDisabledAddins; - if (disabledAddins != null && disabledAddins.Length > 0) { + if (runTimeDisabledAddins.Count > 0) { if (newlist == null) { newlist = new List<string> (); newlist.AddRange (col); } - foreach (string s in disabledAddins) + foreach (string s in runTimeDisabledAddins) newlist.Remove (s); } @@ -1063,11 +1021,11 @@ 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. @@ -1099,18 +1057,18 @@ namespace Mono.Addins } // 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)); - } - } + transaction.NotifyExtensionsChangedEvent(requestedExtensionPath); + } + } - ExtensionLoadData GetAddinExtensions (string id, ExtensionPoint ep) + 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, id); if (addin != null) pinfo = addin.Addin; else @@ -1162,7 +1120,7 @@ namespace Mono.Addins var addedNodes = new List<TreeNode> (); tree.LoadExtension (transaction, node, addinId, extension, addedNodes); - RuntimeAddin ad = AddinEngine.GetAddin (addinId); + RuntimeAddin ad = AddinEngine.GetAddin (transaction, addinId); if (ad != null) { foreach (TreeNode nod in addedNodes) { // Don't call OnAddinLoaded here. Do it when the entire extension point has been loaded. @@ -1250,11 +1208,6 @@ namespace Mono.Addins 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 @@ -1410,4 +1363,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 index 18d0eb3..62b8d52 100644 --- a/Mono.Addins/Mono.Addins/ExtensionContextTransaction.cs +++ b/Mono.Addins/Mono.Addins/ExtensionContextTransaction.cs @@ -29,7 +29,10 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading; +using static Mono.Addins.ExtensionContext; namespace Mono.Addins { @@ -59,36 +62,76 @@ namespace Mono.Addins List<RuntimeAddin> addinLoadEvents; List<string> addinUnloadEvents; List<TreeNode> treeNodeTransactions; + ExtensionContextSnapshot snapshot; + bool snaphotChanged; 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; + + void EnsureNewSnapshot() + { + if (!snaphotChanged) + { + snaphotChanged = true; + var newSnapshot = Context.CreateSnapshot(); + newSnapshot.CopyFrom(snapshot); + snapshot = newSnapshot; + } + } + public void Dispose () { - try { + var engine = Context as AddinEngine; + + try + { // Update the context if (nodeConditions != null) { - Context.BulkRegisterNodeConditions (this, nodeConditions); + BulkRegisterNodeConditions (nodeConditions); } if (nodeConditionUnregistrations != null) { - Context.BulkUnregisterNodeConditions (this, nodeConditionUnregistrations); + BulkUnregisterNodeConditions (nodeConditionUnregistrations); + } + + if (engine != null) + { + 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); } // Commit tree node transactions - if (treeNodeTransactions != null) { + if (treeNodeTransactions != null) + { foreach (var node in treeNodeTransactions) - node.CommitChildrenUpdateTransaction (); + node.CommitChildrenUpdateTransaction(); } + if (snaphotChanged) + Context.SetSnapshot(snapshot); + } finally { Monitor.Exit (Context.LocalLock); } @@ -109,32 +152,101 @@ namespace Mono.Addins foreach (var path in extensionsChanged) Context.NotifyExtensionsChanged (new ExtensionEventArgs (path)); } - if (registeredAutoExtensionPoints != null) { - var engine = (AddinEngine)Context; - engine.BulkRegisterAutoTypeExtensionPoint (registeredAutoExtensionPoints); - } - if (unregisteredAutoExtensionPoints != null) { - var engine = (AddinEngine)Context; - engine.BulkUnregisterAutoTypeExtensionPoint (unregisteredAutoExtensionPoints); - } - if (registeredAssemblyResolvePaths != null) { - var engine = (AddinEngine)Context; - engine.BulkRegisterAssemblyResolvePaths (registeredAssemblyResolvePaths); + + if (engine != null) { + if (addinLoadEvents != null) { + foreach (var addin in addinLoadEvents) + engine.ReportAddinLoad (addin); + } + if (addinUnloadEvents != null) { + foreach (var id in addinUnloadEvents) + engine.ReportAddinUnload (id); + } } - if (unregisteredAssemblyResolvePaths != null) { - var engine = (AddinEngine)Context; - engine.BulkUnregisterAssemblyResolvePaths (unregisteredAssemblyResolvePaths); + } + + 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)); } - if (addinLoadEvents != null) { - var engine = (AddinEngine)Context; - foreach (var addin in addinLoadEvents) - engine.ReportAddinLoad (addin); + + 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); } - if (addinUnloadEvents != null) { - var engine = (AddinEngine)Context; - foreach (var id in addinUnloadEvents) - engine.ReportAddinUnload (id); + + 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) @@ -164,6 +276,7 @@ namespace Mono.Addins public void RegisterNodeCondition (TreeNode node, BaseCondition cond) { + EnsureNewSnapshot(); if (nodeConditions == null) nodeConditions = new List<(TreeNode Node, BaseCondition Condition)> (); nodeConditions.Add ((node, cond)); @@ -171,6 +284,7 @@ namespace Mono.Addins public void UnregisterNodeCondition (TreeNode node, BaseCondition cond) { + EnsureNewSnapshot(); if (nodeConditionUnregistrations == null) nodeConditionUnregistrations = new List<(TreeNode Node, BaseCondition Condition)> (); nodeConditionUnregistrations.Add ((node, cond)); @@ -180,6 +294,7 @@ namespace Mono.Addins 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)); @@ -187,6 +302,7 @@ namespace Mono.Addins public void UnregisterAutoTypeExtensionPoint (string typeName) { + EnsureNewSnapshot(); if (unregisteredAutoExtensionPoints == null) unregisteredAutoExtensionPoints = new List<string> (); unregisteredAutoExtensionPoints.Add (typeName); @@ -194,6 +310,7 @@ namespace Mono.Addins 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)); @@ -201,6 +318,7 @@ namespace Mono.Addins public void UnregisterAssemblyResolvePaths (string assembly) { + EnsureNewSnapshot(); if (unregisteredAssemblyResolvePaths == null) unregisteredAssemblyResolvePaths = new List<string> (); unregisteredAssemblyResolvePaths.Add (assembly); @@ -226,5 +344,6 @@ namespace Mono.Addins treeNodeTransactions = new List<TreeNode> (); treeNodeTransactions.Add (node); } + } } diff --git a/Mono.Addins/Mono.Addins/ExtensionNode.cs b/Mono.Addins/Mono.Addins/ExtensionNode.cs index ffb9243..36f227d 100644 --- a/Mono.Addins/Mono.Addins/ExtensionNode.cs +++ b/Mono.Addins/Mono.Addins/ExtensionNode.cs @@ -157,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); } @@ -168,53 +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) { - lock (localLock) { - if (!childrenLoaded) { - childNodes = CreateChildrenList (); - childrenLoaded = true; - } + [Obsolete("Use GetChildNodes()")] + public ExtensionNodeList ChildNodes => GetChildNodes(); + + /// <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; } - } - return childNodes; + } } + return childNodes; } - + /// <summary> /// Returns the child objects of a node. /// </summary> @@ -333,7 +365,9 @@ namespace Mono.Addins Array GetChildObjectsInternal (Type arrayElementType, bool reuseCachedInstance) { - var children = ChildNodes; + // 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); @@ -527,57 +561,108 @@ namespace Mono.Addins if (changes != null) { foreach (var change in changes) { + var node = change.Node; if (change.Added) - OnChildNodeAdded (change.Node); + { + ExtensionContext.InvokeCallback(() => + { + OnChildNodeAdded(node); + }, this); + } else - OnChildNodeRemoved (change.Node); + { + ExtensionContext.InvokeCallback(() => + { + OnChildNodeRemoved(node); + }, this); + } } - OnChildrenChanged (); + 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) { 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) { extensionNodeChanged?.Invoke (this, new ExtensionNodeEventArgs (ExtensionChange.Remove, node)); diff --git a/Mono.Addins/Mono.Addins/ExtensionTree.cs b/Mono.Addins/Mono.Addins/ExtensionTree.cs index 37a16ba..72f065d 100644 --- a/Mono.Addins/Mono.Addins/ExtensionTree.cs +++ b/Mono.Addins/Mono.Addins/ExtensionTree.cs @@ -112,7 +112,7 @@ namespace Mono.Addins curPos = parentNode.Children.Count; // Find the type of the node in this extension - ExtensionNodeType ntype = addinEngine.FindType (parentNode.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: " + parentNode.GetPath (), addin, null, false); @@ -125,7 +125,7 @@ namespace Mono.Addins TreeNode childNode = new TreeNode (addinEngine, id); - ExtensionNode enode = ReadNode (childNode, addin, ntype, elem, module); + ExtensionNode enode = ReadNode (childNode, addin, ntype, elem, module, transaction); if (enode == null) continue; @@ -173,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; } @@ -199,19 +199,17 @@ 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, ntype.AddinId); + if (p == null) + { + if (!addinEngine.LoadAddin(transaction, null, ntype.AddinId, false)) + { + addinEngine.ReportError("Add-in not found", ntype.AddinId, null, false); + return false; } + p = addinEngine.GetAddin(transaction, ntype.AddinId); } // If no type name is provided, use TypeExtensionNode by default diff --git a/Mono.Addins/Mono.Addins/RuntimeAddin.cs b/Mono.Addins/Mono.Addins/RuntimeAddin.cs index a23c9b8..abbbf1f 100644 --- a/Mono.Addins/Mono.Addins/RuntimeAddin.cs +++ b/Mono.Addins/Mono.Addins/RuntimeAddin.cs @@ -731,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 1c60176..533e092 100644 --- a/Mono.Addins/Mono.Addins/TreeNode.cs +++ b/Mono.Addins/Mono.Addins/TreeNode.cs @@ -97,12 +97,12 @@ namespace Mono.Addins public void NotifyAddinUnloaded () { - extensionNode?.OnAddinUnloaded (); + extensionNode?.NotifyAddinUnloaded (); } public void NotifyAddinLoaded () { - extensionNode?.OnAddinLoaded (); + extensionNode?.NotifyAddinLoaded (); } public bool HasExtensionNode { @@ -344,10 +344,10 @@ namespace Mono.Addins } } } finally { + childrenFromExtensionsLoaded = true; if (disposeTransaction) transaction.Dispose (); } - childrenFromExtensionsLoaded = true; } return (IReadOnlyList<TreeNode>)childrenBuilder ?? (IReadOnlyList<TreeNode>)children; } @@ -427,7 +427,7 @@ namespace Mono.Addins public void NotifyAddinLoaded (RuntimeAddin ad, bool recursive) { if (extensionNode != null && extensionNode.AddinId == ad.Addin.Id) - extensionNode.OnAddinLoaded (); + extensionNode.NotifyAddinLoaded (); if (recursive && childrenFromExtensionsLoaded) { foreach (TreeNode node in Children) node.NotifyAddinLoaded (ad, true); @@ -475,36 +475,6 @@ namespace Mono.Addins 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 bool NotifyChildrenChanged () { if (extensionNode != null) @@ -517,7 +487,7 @@ namespace Mono.Addins { if (extensionPoint != null) { string aid = Addin.GetIdName (extensionPoint.ParentAddinDescription.AddinId); - RuntimeAddin ad = addinEngine.GetAddin (aid); + RuntimeAddin ad = addinEngine.GetAddin (transaction, aid); if (ad != null) extensionPoint = ad.Addin.Description.ExtensionPoints [GetPath ()]; } diff --git a/Version.props b/Version.props index 90c3a01..8df8ed8 100644 --- a/Version.props +++ b/Version.props @@ -1,6 +1,6 @@ <Project> <PropertyGroup> - <PackageVersion>1.3.15-alpha</PackageVersion> + <PackageVersion>1.3.15-alpha.2</PackageVersion> <Authors>Microsoft</Authors> <Owners>microsoft, xamarin</Owners> <PackageLicenseUrl>https://github.com/mono/mono-addins/blob/main/COPYING</PackageLicenseUrl> |