From b06b0a4d1039659cdcd14c8d3644f1cfa0b2ff7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 07:46:07 +0000 Subject: [PATCH 1/2] fix: resolve threading issues in src/VisualStudio Agent-Logs-Url: https://github.com/X-Sharp/XSharpPublic/sessions/8c87e5e2-aca4-4fc7-9628-e98072a1a012 Co-authored-by: RobertvanderHulst <14240939+RobertvanderHulst@users.noreply.github.com> --- .../Debugger.Support/RTLink/RtLink.cs | 29 +++++++++------ .../Classifier/XSharpClassifier.cs | 4 +-- .../LanguageService/XMLDoc/XSharpXMLDoc.cs | 7 +++- .../Commands/CommandEditProjectFile.cs | 36 +++++++++++++------ .../List/ErrorList/ErrorListManager.cs | 14 +++++--- .../List/TaskList/TaskListManager.cs | 14 +++++--- .../ProjectPackage/Nodes/XSharpProjectNode.cs | 20 ++++++----- 7 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/VisualStudio/Debugger.Support/RTLink/RtLink.cs b/src/VisualStudio/Debugger.Support/RTLink/RtLink.cs index 2e68931d95..326f71525d 100644 --- a/src/VisualStudio/Debugger.Support/RTLink/RtLink.cs +++ b/src/VisualStudio/Debugger.Support/RTLink/RtLink.cs @@ -38,14 +38,16 @@ static RtLink() { AppDomain.CurrentDomain.AssemblyLoad += AssemblyLoadEventHandler; var loadedAssemblies = new HashSet(AppDomain.CurrentDomain.GetAssemblies()); - do + lock (LoadedAssemblies) { - LoadedAssemblies.UnionWith(loadedAssemblies); - loadedAssemblies = new HashSet(AppDomain.CurrentDomain.GetAssemblies()); - loadedAssemblies.RemoveWhere(a => a.IsDynamic); - loadedAssemblies.ExceptWith(LoadedAssemblies); - } while (loadedAssemblies.Count > 0); - + do + { + LoadedAssemblies.UnionWith(loadedAssemblies); + loadedAssemblies = new HashSet(AppDomain.CurrentDomain.GetAssemblies()); + loadedAssemblies.RemoveWhere(a => a.IsDynamic); + loadedAssemblies.ExceptWith(LoadedAssemblies); + } while (loadedAssemblies.Count > 0); + } } @@ -64,7 +66,11 @@ private static void AssemblyLoadEventHandler(object sender, AssemblyLoadEventArg static Type FindType(string name, string asmName, out string error) { error = null; - var asm = LoadedAssemblies.First(a => a.FullName.ToLower().Contains(asmName.ToLower())); + Assembly asm; + lock (LoadedAssemblies) + { + asm = LoadedAssemblies.FirstOrDefault(a => a.FullName.ToLower().Contains(asmName.ToLower())); + } if (asm == null) { error = string.Format(AssemblyNotFound, asmName); @@ -100,8 +106,11 @@ static PropertyInfo FindProperty(this Type type, string Name, BindingFlags flags #endregion public static bool IsRTLoaded() { - return LoadedAssemblies.Any(a => a.FullName.ToLower().Contains("xsharp.rt")) && - LoadedAssemblies.Any(a => a.FullName.ToLower().Contains("xsharp.core")); + lock (LoadedAssemblies) + { + return LoadedAssemblies.Any(a => a.FullName.ToLower().Contains("xsharp.rt")) && + LoadedAssemblies.Any(a => a.FullName.ToLower().Contains("xsharp.core")); + } } public static string GetGlobals() diff --git a/src/VisualStudio/LanguageService/Classifier/XSharpClassifier.cs b/src/VisualStudio/LanguageService/Classifier/XSharpClassifier.cs index cde2eb1dcf..89f6a407a7 100644 --- a/src/VisualStudio/LanguageService/Classifier/XSharpClassifier.cs +++ b/src/VisualStudio/LanguageService/Classifier/XSharpClassifier.cs @@ -62,8 +62,8 @@ public class XSharpClassifier : IClassifier private readonly List _xtraKeywords; private XSharpLineState _lineState; private XSharpLineKeywords _lineKeywords; - private bool IsLexing = false; - private bool IsStarted = false; + private volatile bool IsLexing = false; + private volatile bool IsStarted = false; private bool _needsKeywords = false; private bool forceRepaint = false; diff --git a/src/VisualStudio/LanguageService/XMLDoc/XSharpXMLDoc.cs b/src/VisualStudio/LanguageService/XMLDoc/XSharpXMLDoc.cs index c98eea5ada..f40b4cbe89 100644 --- a/src/VisualStudio/LanguageService/XMLDoc/XSharpXMLDoc.cs +++ b/src/VisualStudio/LanguageService/XMLDoc/XSharpXMLDoc.cs @@ -27,6 +27,7 @@ static public class XSharpXMLDocTools static IVsXMLMemberIndexService _XMLMemberIndexService ; static string coreLoc ; static IVsXMLMemberIndex coreIndex ; + static readonly object _initLock = new object(); static XSharpXMLDocTools() { _memberIndexes = new Dictionary(); @@ -46,8 +47,12 @@ static void init() static bool GetIndex() { - if (_XMLMemberIndexService == null) + if (_XMLMemberIndexService != null) + return true; + lock (_initLock) { + if (_XMLMemberIndexService != null) + return true; ThreadHelper.JoinableTaskFactory.Run(async ( )=> { _XMLMemberIndexService = await VS.GetServiceAsync(); diff --git a/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs b/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs index 535733b828..2f19bb95e9 100644 --- a/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs +++ b/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.Threading; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,13 +19,14 @@ namespace XSharp.Project [Command(PackageIds.idEditProjectFile)] internal sealed class CommandEditProjectFile : BaseCommand { - static Dictionary tempProjectFiles; // key is the project file, value is the temp file + static ConcurrentDictionary tempProjectFiles; // key is the project file, value is the temp file static string tempProjectDir; + static bool _eventsSubscribed = false; static CommandEditProjectFile() { tempProjectDir = Path.Combine(Path.GetTempPath(), "XSharp.Intellisense"); Directory.CreateDirectory(tempProjectDir); - tempProjectFiles = new Dictionary(); + tempProjectFiles = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); } protected override void BeforeQueryStatus(EventArgs e) { @@ -38,8 +40,14 @@ private async Task CheckAvailabilityAsync() protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) { var project = await VS.Solutions.GetActiveProjectAsync(); - VS.Events.DocumentEvents.Saved += DocumentEvents_Saved; - VS.Events.DocumentEvents.Closed += DocumentEvents_Closed; + // Unsubscribe before subscribing to prevent duplicate registrations + // if ExecuteAsync is called multiple times. + if (!_eventsSubscribed) + { + VS.Events.DocumentEvents.Saved += DocumentEvents_Saved; + VS.Events.DocumentEvents.Closed += DocumentEvents_Closed; + _eventsSubscribed = true; + } var projectNode = XSharpProjectNode.FindProject(project.FullPath); @@ -82,17 +90,25 @@ private void DocumentEvents_Saved(string obj) private void DocumentEvents_Closed(string obj) { + // Find the matching key without modifying the collection during iteration. + string keyToRemove = null; foreach (var item in tempProjectFiles) { - if ( item.Value == obj) + if (item.Value == obj) + { + keyToRemove = item.Key; + break; + } + } + if (keyToRemove != null && tempProjectFiles.TryRemove(keyToRemove, out _)) + { + File.Delete(obj); + // Unsubscribe event handlers when there are no more tracked files. + if (tempProjectFiles.IsEmpty) { - - tempProjectFiles.Remove(item.Key); VS.Events.DocumentEvents.Closed -= DocumentEvents_Closed; VS.Events.DocumentEvents.Saved -= DocumentEvents_Saved; - File.Delete(item.Value); - break; - + _eventsSubscribed = false; } } } diff --git a/src/VisualStudio/ProjectPackage/List/ErrorList/ErrorListManager.cs b/src/VisualStudio/ProjectPackage/List/ErrorList/ErrorListManager.cs index 59fe2152ec..0aace8ab56 100644 --- a/src/VisualStudio/ProjectPackage/List/ErrorList/ErrorListManager.cs +++ b/src/VisualStudio/ProjectPackage/List/ErrorList/ErrorListManager.cs @@ -27,6 +27,7 @@ internal class ErrorListManager : ListManager static ErrorListProvider _listprovider = null; static IErrorList _errorList = null; static ITableManager _manager = null; + static readonly object _initLock = new object(); static ErrorListManager() @@ -39,11 +40,16 @@ static void GetErrorList() { if (_errorList != null) return; - _errorList = XSharpProjectPackage.XInstance.ErrorList; - if (_errorList != null) + lock (_initLock) { - _manager = _errorList.TableControl.Manager; - _listprovider = new ErrorListProvider(_manager); + if (_errorList != null) + return; + _errorList = XSharpProjectPackage.XInstance.ErrorList; + if (_errorList != null) + { + _manager = _errorList.TableControl.Manager; + _listprovider = new ErrorListProvider(_manager); + } } } internal ErrorListManager(XSharpProjectNode node) : base(node) diff --git a/src/VisualStudio/ProjectPackage/List/TaskList/TaskListManager.cs b/src/VisualStudio/ProjectPackage/List/TaskList/TaskListManager.cs index 01fd923c9a..c3651e05c7 100644 --- a/src/VisualStudio/ProjectPackage/List/TaskList/TaskListManager.cs +++ b/src/VisualStudio/ProjectPackage/List/TaskList/TaskListManager.cs @@ -28,17 +28,23 @@ internal class TaskListManager : ListManager static ListProvider _listprovider = null; static ITaskList _taskList; static ITableManager _manager; + static readonly object _initLock = new object(); static void GetTaskList() { if (_taskList != null) return; - _taskList = XSharpProjectPackage.XInstance.TaskList; - if (_taskList != null) + lock (_initLock) { - _manager = _taskList.TableControl.Manager; - _listprovider = new TaskListProvider(_manager); + if (_taskList != null) + return; + _taskList = XSharpProjectPackage.XInstance.TaskList; + if (_taskList != null) + { + _manager = _taskList.TableControl.Manager; + _listprovider = new TaskListProvider(_manager); + } } } diff --git a/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs b/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs index 95cddc568d..67c5d5677f 100644 --- a/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs +++ b/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs @@ -51,16 +51,18 @@ public partial class XSharpProjectNode : XProjectNode, IVsSingleFileGeneratorFac static List nodes = new List(); internal static XSharpProjectNode FindProject(string url) { - var file = System.IO.Path.GetFileName(url); - foreach (var proj in nodes) + lock (nodes) { - if (string.Compare(proj.FileName, url, true) == 0) - return proj; - var pFile = System.IO.Path.GetFileName(proj.FileName); - if (string.Compare(pFile, url, true) == 0) - return proj; + var file = System.IO.Path.GetFileName(url); + foreach (var proj in nodes) + { + if (string.Compare(proj.FileName, url, true) == 0) + return proj; + var pFile = System.IO.Path.GetFileName(proj.FileName); + if (string.Compare(pFile, url, true) == 0) + return proj; + } } - return null; } @@ -86,7 +88,7 @@ static XSharpProjectNode() _changedProjectFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); } internal static IDictionary ChangedProjectFiles => _changedProjectFiles; - internal static XSharpProjectNode[] AllProjects => nodes.ToArray(); + internal static XSharpProjectNode[] AllProjects { get { lock (nodes) { return nodes.ToArray(); } } } #region Constants internal const string ProjectTypeName = XSharpConstants.LanguageName; From f8a5840979bf8ef2edd4163c5efecc90f86fd64b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 07:47:16 +0000 Subject: [PATCH 2/2] fix: address code review feedback on CommandEditProjectFile threading Agent-Logs-Url: https://github.com/X-Sharp/XSharpPublic/sessions/8c87e5e2-aca4-4fc7-9628-e98072a1a012 Co-authored-by: RobertvanderHulst <14240939+RobertvanderHulst@users.noreply.github.com> --- .../ProjectPackage/Commands/CommandEditProjectFile.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs b/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs index 2f19bb95e9..bf5730fc51 100644 --- a/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs +++ b/src/VisualStudio/ProjectPackage/Commands/CommandEditProjectFile.cs @@ -21,7 +21,7 @@ internal sealed class CommandEditProjectFile : BaseCommand tempProjectFiles; // key is the project file, value is the temp file static string tempProjectDir; - static bool _eventsSubscribed = false; + static volatile bool _eventsSubscribed = false; static CommandEditProjectFile() { tempProjectDir = Path.Combine(Path.GetTempPath(), "XSharp.Intellisense"); @@ -103,7 +103,8 @@ private void DocumentEvents_Closed(string obj) if (keyToRemove != null && tempProjectFiles.TryRemove(keyToRemove, out _)) { File.Delete(obj); - // Unsubscribe event handlers when there are no more tracked files. + // VS document events and commands all execute on the UI thread, so no other + // thread can insert a new entry between TryRemove and IsEmpty. if (tempProjectFiles.IsEmpty) { VS.Events.DocumentEvents.Closed -= DocumentEvents_Closed;