diff --git a/MonacoRoslynCompletionProvider/Api/CodeRunner.cs b/MonacoRoslynCompletionProvider/Api/CodeRunner.cs index 35328db..c43173d 100644 --- a/MonacoRoslynCompletionProvider/Api/CodeRunner.cs +++ b/MonacoRoslynCompletionProvider/Api/CodeRunner.cs @@ -1,61 +1,61 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Emit; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using monacoEditorCSharp.DataHelpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Emit; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using monacoEditorCSharp.DataHelpers; using System.Threading; using System.Diagnostics; using System.IO.Compression; using System.Runtime.InteropServices; using System.Runtime.Loader; using MonacoRoslynCompletionProvider; - -namespace MonacoRoslynCompletionProvider.Api -{ - public class CodeRunner - { - private static readonly Dictionary ActiveProcessSessions = new(StringComparer.Ordinal); + +namespace MonacoRoslynCompletionProvider.Api +{ + public class CodeRunner + { + private static readonly Dictionary ActiveProcessSessions = new(StringComparer.Ordinal); private static readonly Dictionary LegacyReaders = new(); private static readonly object ProcessSessionLock = new(); private static readonly object ConsoleLock = new(); private const string WindowsFormsHostRequirementMessage = "WinForms can only run on Windows (System.Windows.Forms/System.Drawing are Windows-only)."; - private static readonly object RuntimeExtensionsLock = new(); - private static string _objectExtensionsSource = string.Empty; - - private static string NormalizeProjectType(string type) - { - if (string.IsNullOrWhiteSpace(type)) - { - return "console"; - } - - var filtered = new string(type.ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray()); - if (string.IsNullOrEmpty(filtered)) - { - return "console"; - } - - if (filtered.Contains("winform") || filtered.Contains("form") || filtered.Contains("windows")) - { - return "winforms"; - } - - if (filtered.Contains("aspnet") || filtered.Contains("webapi") || filtered == "web") - { - return "webapi"; - } - - return "console"; - } - + private const string ObjectExtensionsResourceName = "MonacoRoslynCompletionProvider.Extensions.ObjectExtension.cs"; + private static readonly Lazy _objectExtensionsSource = new(LoadObjectExtensionsFromEmbeddedResource, LazyThreadSafetyMode.ExecutionAndPublication); + + private static string NormalizeProjectType(string type) + { + if (string.IsNullOrWhiteSpace(type)) + { + return "console"; + } + + var filtered = new string(type.ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray()); + if (string.IsNullOrEmpty(filtered)) + { + return "console"; + } + + if (filtered.Contains("winform") || filtered.Contains("form") || filtered.Contains("windows")) + { + return "winforms"; + } + + if (filtered.Contains("aspnet") || filtered.Contains("webapi") || filtered == "web") + { + return "webapi"; + } + + return "console"; + } + private static (OutputKind OutputKind, bool RequiresStaThread) GetRunBehavior(string projectType) { return NormalizeProjectType(projectType) switch @@ -65,175 +65,149 @@ private static (OutputKind OutputKind, bool RequiresStaThread) GetRunBehavior(st }; } - private static string GetObjectExtensionsSource() + private static string GetObjectExtensionsSource() => _objectExtensionsSource.Value; + + private static string LoadObjectExtensionsFromEmbeddedResource() + { + var assembly = typeof(CodeRunner).Assembly; + + using var stream = assembly.GetManifestResourceStream(ObjectExtensionsResourceName); + if (stream == null) + { + throw new FileNotFoundException($"Unable to locate embedded resource: {ObjectExtensionsResourceName}. Available resources: {string.Join(", ", assembly.GetManifestResourceNames())}"); + } + + using var reader = new StreamReader(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + + private static Task RunEntryPointAsync(Func executeAsync, bool requiresStaThread) { - if (!string.IsNullOrEmpty(_objectExtensionsSource)) + if (executeAsync == null) throw new ArgumentNullException(nameof(executeAsync)); + + if (!requiresStaThread) + { + return executeAsync(); + } + + if (!OperatingSystem.IsWindows()) { - return _objectExtensionsSource; + return executeAsync(); } - lock (RuntimeExtensionsLock) + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var thread = new Thread(() => { - if (string.IsNullOrEmpty(_objectExtensionsSource)) + try { - var path = LocateObjectExtensionsFilePath(); - _objectExtensionsSource = File.ReadAllText(path, Encoding.UTF8); + executeAsync().GetAwaiter().GetResult(); + tcs.TrySetResult(null); } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }) + { + IsBackground = true, + Name = "CodeRunner-STA" + }; + + try + { + thread.SetApartmentState(ApartmentState.STA); + } + catch (PlatformNotSupportedException) + { + return executeAsync(); } - return _objectExtensionsSource; + thread.Start(); + return tcs.Task; } - private static string LocateObjectExtensionsFilePath() + public static bool ProvideInput(string sessionId, string input) { - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory != null) + if (string.IsNullOrEmpty(sessionId)) + { + return false; + } + + lock (LegacyReaders) { - var candidate = Path.Combine(directory.FullName, "MonacoRoslynCompletionProvider", "Extensions", "ObjectExtengsion.cs"); - if (File.Exists(candidate)) + if (LegacyReaders.TryGetValue(sessionId, out var reader)) { - return candidate; + reader.ProvideInput(input); + return true; } + } - var alternative = Path.Combine(directory.FullName, "Extensions", "ObjectExtengsion.cs"); - if (File.Exists(alternative)) + lock (ProcessSessionLock) + { + if (ActiveProcessSessions.TryGetValue(sessionId, out var session)) { - return alternative; + return session.ProvideInput(input); } - - directory = directory.Parent; } - throw new FileNotFoundException("Unable to locate MonacoRoslynCompletionProvider/Extensions/ObjectExtengsion.cs for publish runtime extensions."); + return false; } - private static Task RunEntryPointAsync(Func executeAsync, bool requiresStaThread) + public class RunResult { - if (executeAsync == null) throw new ArgumentNullException(nameof(executeAsync)); + public string Output { get; set; } + public string Error { get; set; } + } + + + public static Task RunMultiFileCodeAsync( + List files, + string nuget, + int languageVersion, + Func onOutput, + Func onError, + string sessionId = null, + string projectType = null, + CancellationToken cancellationToken = default) + { + return CompileAndExecuteAsync( + files ?? new List(), + nuget, + languageVersion, + onOutput, + onError, + sessionId, + projectType, + cancellationToken); + } - if (!requiresStaThread) - { - return executeAsync(); - } - - if (!OperatingSystem.IsWindows()) - { - return executeAsync(); - } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var thread = new Thread(() => - { - try - { - executeAsync().GetAwaiter().GetResult(); - tcs.TrySetResult(null); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }) - { - IsBackground = true, - Name = "CodeRunner-STA" - }; - - try - { - thread.SetApartmentState(ApartmentState.STA); - } - catch (PlatformNotSupportedException) - { - return executeAsync(); - } - - thread.Start(); - return tcs.Task; - } - - public static bool ProvideInput(string sessionId, string input) - { - if (string.IsNullOrEmpty(sessionId)) - { - return false; - } - - lock (LegacyReaders) - { - if (LegacyReaders.TryGetValue(sessionId, out var reader)) - { - reader.ProvideInput(input); - return true; - } - } - - lock (ProcessSessionLock) - { - if (ActiveProcessSessions.TryGetValue(sessionId, out var session)) - { - return session.ProvideInput(input); - } - } - - return false; - } - - public class RunResult - { - public string Output { get; set; } - public string Error { get; set; } - } - - - public static Task RunMultiFileCodeAsync( - List files, - string nuget, - int languageVersion, - Func onOutput, - Func onError, - string sessionId = null, - string projectType = null, - CancellationToken cancellationToken = default) - { - return CompileAndExecuteAsync( - files ?? new List(), - nuget, - languageVersion, - onOutput, - onError, - sessionId, - projectType, - cancellationToken); - } - public static Task RunProgramCodeAsync( string code, string nuget, int languageVersion, Func onOutput, Func onError, - string sessionId = null, - string projectType = null, - CancellationToken cancellationToken = default) - { + string sessionId = null, + string projectType = null, + CancellationToken cancellationToken = default) + { var files = new List { new FileContent { FileName = "Program.cs", - Content = code ?? string.Empty - } - }; - - return CompileAndExecuteAsync( - files, - nuget, - languageVersion, - onOutput, - onError, - sessionId, - projectType, + Content = code ?? string.Empty + } + }; + + return CompileAndExecuteAsync( + files, + nuget, + languageVersion, + onOutput, + onError, + sessionId, + projectType, cancellationToken); } @@ -256,123 +230,123 @@ private static async Task CompileAndExecuteAsync( string nuget, int languageVersion, Func onOutput, - Func onError, - string sessionId, - string projectType, - CancellationToken cancellationToken) - { - var result = new RunResult - { - Output = string.Empty, - Error = string.Empty - }; - - if (onOutput is null) - { - throw new ArgumentNullException(nameof(onOutput)); - } - - if (onError is null) - { - throw new ArgumentNullException(nameof(onError)); - } - - var files = (sourceFiles ?? Enumerable.Empty()) - .Where(f => f != null) - .Select(f => new FileContent - { - FileName = string.IsNullOrWhiteSpace(f.FileName) ? "Program.cs" : f.FileName, - Content = f.Content ?? string.Empty - }) - .ToList(); - - if (files.Count == 0) - { - files.Add(new FileContent - { - FileName = "Program.cs", - Content = string.Empty - }); - } - - var normalizedProjectType = NormalizeProjectType(projectType); - var runBehavior = GetRunBehavior(projectType); - - // 检测是否需要 WinForms 支持 - if (runBehavior.OutputKind != OutputKind.WindowsApplication && DetectWinFormsUsage(files?.Select(f => f?.Content))) - { - // 自动检测到 WinForms 代码时启用所需的运行时设置 - runBehavior = (OutputKind.WindowsApplication, true); - } - - if (runBehavior.OutputKind == OutputKind.WindowsApplication && !OperatingSystem.IsWindows()) - { - await onError(WindowsFormsHostRequirementMessage).ConfigureAwait(false); - result.Error = WindowsFormsHostRequirementMessage; - return result; - } - - // 根据项目类型选择执行方式 - if (runBehavior.OutputKind == OutputKind.WindowsApplication || - normalizedProjectType == "webapi" || - normalizedProjectType == "aspnetcore" || - normalizedProjectType == "aspnetcorewebapi" || - normalizedProjectType == "web") - { - // WinForms 和 Web API 应用使用进程外执行 - return await ExecuteInIsolatedProcessAsync(files, nuget, languageVersion, onOutput, onError, sessionId, projectType, cancellationToken); - } - else - { - // Console 应用使用进程内执行 - return await ExecuteInProcessAsync(files, nuget, languageVersion, onOutput, onError, sessionId, projectType, cancellationToken); - } - } - - // Console 应用进程内执行 - private static async Task ExecuteInProcessAsync( - List files, - string nuget, - int languageVersion, - Func onOutput, - Func onError, - string sessionId, - string projectType, - CancellationToken cancellationToken) - { - var result = new RunResult - { - Output = string.Empty, - Error = string.Empty - }; - - CustomAssemblyLoadContext loadContext = null; - Assembly assembly = null; - - try - { - var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget); - loadContext = new CustomAssemblyLoadContext(nugetAssemblies); - - var parseOptions = new CSharpParseOptions( - languageVersion: (LanguageVersion)languageVersion, - kind: SourceCodeKind.Regular, - documentationMode: DocumentationMode.Parse); - - string assemblyName = "DynamicCode"; - - // Parse all files into syntax trees - var syntaxTrees = new List(); - foreach (var file in files) - { - var syntaxTree = CSharpSyntaxTree.ParseText( - file.Content, - parseOptions, - path: file.FileName); - syntaxTrees.Add(syntaxTree); - } - - // Collect references + Func onError, + string sessionId, + string projectType, + CancellationToken cancellationToken) + { + var result = new RunResult + { + Output = string.Empty, + Error = string.Empty + }; + + if (onOutput is null) + { + throw new ArgumentNullException(nameof(onOutput)); + } + + if (onError is null) + { + throw new ArgumentNullException(nameof(onError)); + } + + var files = (sourceFiles ?? Enumerable.Empty()) + .Where(f => f != null) + .Select(f => new FileContent + { + FileName = string.IsNullOrWhiteSpace(f.FileName) ? "Program.cs" : f.FileName, + Content = f.Content ?? string.Empty + }) + .ToList(); + + if (files.Count == 0) + { + files.Add(new FileContent + { + FileName = "Program.cs", + Content = string.Empty + }); + } + + var normalizedProjectType = NormalizeProjectType(projectType); + var runBehavior = GetRunBehavior(projectType); + + // 检测是否需要 WinForms 支持 + if (runBehavior.OutputKind != OutputKind.WindowsApplication && DetectWinFormsUsage(files?.Select(f => f?.Content))) + { + // 自动检测到 WinForms 代码时启用所需的运行时设置 + runBehavior = (OutputKind.WindowsApplication, true); + } + + if (runBehavior.OutputKind == OutputKind.WindowsApplication && !OperatingSystem.IsWindows()) + { + await onError(WindowsFormsHostRequirementMessage).ConfigureAwait(false); + result.Error = WindowsFormsHostRequirementMessage; + return result; + } + + // 根据项目类型选择执行方式 + if (runBehavior.OutputKind == OutputKind.WindowsApplication || + normalizedProjectType == "webapi" || + normalizedProjectType == "aspnetcore" || + normalizedProjectType == "aspnetcorewebapi" || + normalizedProjectType == "web") + { + // WinForms 和 Web API 应用使用进程外执行 + return await ExecuteInIsolatedProcessAsync(files, nuget, languageVersion, onOutput, onError, sessionId, projectType, cancellationToken); + } + else + { + // Console 应用使用进程内执行 + return await ExecuteInProcessAsync(files, nuget, languageVersion, onOutput, onError, sessionId, projectType, cancellationToken); + } + } + + // Console 应用进程内执行 + private static async Task ExecuteInProcessAsync( + List files, + string nuget, + int languageVersion, + Func onOutput, + Func onError, + string sessionId, + string projectType, + CancellationToken cancellationToken) + { + var result = new RunResult + { + Output = string.Empty, + Error = string.Empty + }; + + CustomAssemblyLoadContext loadContext = null; + Assembly assembly = null; + + try + { + var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget); + loadContext = new CustomAssemblyLoadContext(nugetAssemblies); + + var parseOptions = new CSharpParseOptions( + languageVersion: (LanguageVersion)languageVersion, + kind: SourceCodeKind.Regular, + documentationMode: DocumentationMode.Parse); + + string assemblyName = "DynamicCode"; + + // Parse all files into syntax trees + var syntaxTrees = new List(); + foreach (var file in files) + { + var syntaxTree = CSharpSyntaxTree.ParseText( + file.Content, + parseOptions, + path: file.FileName); + syntaxTrees.Add(syntaxTree); + } + + // Collect references var references = new List(); foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { @@ -386,1480 +360,1480 @@ private static async Task ExecuteInProcessAsync( { references.Add(MetadataReference.CreateFromFile(pkg.Path)); } - - var compilation = CSharpCompilation.Create( - assemblyName, - syntaxTrees, - references, - new CSharpCompilationOptions(OutputKind.ConsoleApplication)); - - using (var peStream = new MemoryStream()) - { - var compileResult = compilation.Emit(peStream); - if (!compileResult.Success) - { - foreach (var diag in compileResult.Diagnostics) - { - if (diag.Severity == DiagnosticSeverity.Error) - { - var location = diag.Location.GetLineSpan(); - var fileName = location.Path ?? "unknown"; - var line = location.StartLinePosition.Line + 1; - await onError($"[{fileName}:{line}] {diag.GetMessage()}").ConfigureAwait(false); - } - } - result.Error = "Compilation error"; - return result; - } - peStream.Seek(0, SeekOrigin.Begin); - - assembly = loadContext.LoadFromStream(peStream); - - var entryPoint = assembly.EntryPoint; - if (entryPoint != null) - { - var parameters = entryPoint.GetParameters(); - - async Task WriteAction(string text) => await onOutput(text ?? string.Empty).ConfigureAwait(false); - await using var outputWriter = new ImmediateCallbackTextWriter(WriteAction); - - async Task ErrorAction(string text) => await onError(text ?? string.Empty).ConfigureAwait(false); - await using var errorWriter = new ImmediateCallbackTextWriter(ErrorAction); - - var interactiveReader = new InteractiveTextReader(async prompt => - { - await onOutput($"[INPUT REQUIRED] Please provide input: ").ConfigureAwait(false); - }); - - if (!string.IsNullOrEmpty(sessionId)) - { - lock (LegacyReaders) - { - LegacyReaders[sessionId] = interactiveReader; - } - } - - TextWriter originalOut = null, originalError = null; - TextReader originalIn = null; - lock (ConsoleLock) - { - originalOut = Console.Out; - originalError = Console.Error; - originalIn = Console.In; - Console.SetOut(outputWriter); - Console.SetError(errorWriter); - Console.SetIn(interactiveReader); - } - - try - { - cancellationToken.ThrowIfCancellationRequested(); - - if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string[])) - { - entryPoint.Invoke(null, new object[] { new string[] { "sharpPad" } }); - } - else - { - entryPoint.Invoke(null, null); - } - } - catch (OperationCanceledException) - { - await onError("代码执行已被取消").ConfigureAwait(false); - } - catch (Exception ex) - { - var errorMessage = "Execution error: " + (ex.InnerException?.Message ?? ex.Message); - await onError(errorMessage).ConfigureAwait(false); - } - finally - { - lock (ConsoleLock) - { - Console.SetOut(originalOut); - Console.SetError(originalError); - Console.SetIn(originalIn); - } - - if (!string.IsNullOrEmpty(sessionId)) - { - lock (LegacyReaders) - { - LegacyReaders.Remove(sessionId); - } - } - interactiveReader?.Dispose(); - } - } - else - { - await onError("No entry point found in the code. Please ensure one file contains a Main method.").ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - await onError("Runtime error: " + ex.Message).ConfigureAwait(false); - } - finally - { - assembly = null; - loadContext?.Unload(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - - return result; - } - - // WinForms 和 Web API 应用进程外执行 - private static async Task ExecuteInIsolatedProcessAsync( - List files, - string nuget, - int languageVersion, - Func onOutput, - Func onError, - string sessionId, - string projectType, - CancellationToken cancellationToken) - { - var result = new RunResult - { - Output = string.Empty, - Error = string.Empty - }; - - var normalizedProjectType = NormalizeProjectType(projectType); - var runBehavior = GetRunBehavior(projectType); - - // 检测是否需要 WinForms 支持 - if (runBehavior.OutputKind != OutputKind.WindowsApplication && DetectWinFormsUsage(files?.Select(f => f?.Content))) - { - runBehavior = (OutputKind.WindowsApplication, true); - } - - var parseOptions = new CSharpParseOptions( - languageVersion: (LanguageVersion)languageVersion, - kind: SourceCodeKind.Regular, - documentationMode: DocumentationMode.Parse); - - var syntaxTrees = new List(files.Count); - foreach (var file in files) - { - cancellationToken.ThrowIfCancellationRequested(); - var sourceText = SourceText.From(file.Content ?? string.Empty, Encoding.UTF8); - var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, parseOptions, path: file.FileName); - syntaxTrees.Add(syntaxTree); - } - - var references = new List(); - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - if (!asm.IsDynamic && !string.IsNullOrEmpty(asm.Location)) + + var compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees, + references, + new CSharpCompilationOptions(OutputKind.ConsoleApplication)); + + using (var peStream = new MemoryStream()) { - references.Add(MetadataReference.CreateFromFile(asm.Location)); - } - } + var compileResult = compilation.Emit(peStream); + if (!compileResult.Success) + { + foreach (var diag in compileResult.Diagnostics) + { + if (diag.Severity == DiagnosticSeverity.Error) + { + var location = diag.Location.GetLineSpan(); + var fileName = location.Path ?? "unknown"; + var line = location.StartLinePosition.Line + 1; + await onError($"[{fileName}:{line}] {diag.GetMessage()}").ConfigureAwait(false); + } + } + result.Error = "Compilation error"; + return result; + } + peStream.Seek(0, SeekOrigin.Begin); - EnsureStandardLibraryReferences(references); + assembly = loadContext.LoadFromStream(peStream); - if (runBehavior.OutputKind == OutputKind.WindowsApplication) + var entryPoint = assembly.EntryPoint; + if (entryPoint != null) + { + var parameters = entryPoint.GetParameters(); + + async Task WriteAction(string text) => await onOutput(text ?? string.Empty).ConfigureAwait(false); + await using var outputWriter = new ImmediateCallbackTextWriter(WriteAction); + + async Task ErrorAction(string text) => await onError(text ?? string.Empty).ConfigureAwait(false); + await using var errorWriter = new ImmediateCallbackTextWriter(ErrorAction); + + var interactiveReader = new InteractiveTextReader(async prompt => + { + await onOutput($"[INPUT REQUIRED] Please provide input: ").ConfigureAwait(false); + }); + + if (!string.IsNullOrEmpty(sessionId)) + { + lock (LegacyReaders) + { + LegacyReaders[sessionId] = interactiveReader; + } + } + + TextWriter originalOut = null, originalError = null; + TextReader originalIn = null; + lock (ConsoleLock) + { + originalOut = Console.Out; + originalError = Console.Error; + originalIn = Console.In; + Console.SetOut(outputWriter); + Console.SetError(errorWriter); + Console.SetIn(interactiveReader); + } + + try + { + cancellationToken.ThrowIfCancellationRequested(); + + if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string[])) + { + entryPoint.Invoke(null, new object[] { new string[] { "sharpPad" } }); + } + else + { + entryPoint.Invoke(null, null); + } + } + catch (OperationCanceledException) + { + await onError("代码执行已被取消").ConfigureAwait(false); + } + catch (Exception ex) + { + var errorMessage = "Execution error: " + (ex.InnerException?.Message ?? ex.Message); + await onError(errorMessage).ConfigureAwait(false); + } + finally + { + lock (ConsoleLock) + { + Console.SetOut(originalOut); + Console.SetError(originalError); + Console.SetIn(originalIn); + } + + if (!string.IsNullOrEmpty(sessionId)) + { + lock (LegacyReaders) + { + LegacyReaders.Remove(sessionId); + } + } + interactiveReader?.Dispose(); + } + } + else + { + await onError("No entry point found in the code. Please ensure one file contains a Main method.").ConfigureAwait(false); + } + } + } + catch (Exception ex) { - EnsureWinFormsAssembliesLoaded(); - TryAddWinFormsReferences(references); + await onError("Runtime error: " + ex.Message).ConfigureAwait(false); } - - var packageReferenceMap = BuildPackageReferenceMap(nuget, normalizedProjectType); - var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget); - foreach (var package in nugetAssemblies) - { - references.Add(MetadataReference.CreateFromFile(package.Path)); - } - - foreach (var pkg in packageReferenceMap.Keys) - { - TryEnsureAssemblyLoaded(pkg); - } - - var compilation = CSharpCompilation.Create( - "DynamicProgram", - syntaxTrees, - references, - new CSharpCompilationOptions(runBehavior.OutputKind)); - - var workingDirectory = CreateWorkingDirectory(); - var assemblyPath = Path.Combine(workingDirectory, "DynamicProgram.dll"); - var pdbPath = Path.Combine(workingDirectory, "DynamicProgram.pdb"); - - try - { - using (var peStream = File.Create(assemblyPath)) - using (var pdbStream = File.Create(pdbPath)) - { - cancellationToken.ThrowIfCancellationRequested(); - - var emitResult = compilation.Emit( - peStream, - pdbStream, - options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb)); - - if (!emitResult.Success) - { - foreach (var diagnostic in emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) - { - await onError(diagnostic.ToString()).ConfigureAwait(false); - } - - result.Error = "Compilation error"; - return result; - } - } - - var copiedFiles = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var package in nugetAssemblies) - { - cancellationToken.ThrowIfCancellationRequested(); - - var destination = Path.Combine(workingDirectory, Path.GetFileName(package.Path)); - if (string.Equals(package.Path, destination, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (!copiedFiles.Add(destination)) - { - continue; - } - - try - { - File.Copy(package.Path, destination, overwrite: true); - } - catch - { - // ignore copy failures; the execution host may still resolve assemblies from their original locations - } - } - - CopyHostAssembliesForExecution(workingDirectory, copiedFiles); - CopyAssembliesForPackages(packageReferenceMap.Keys, workingDirectory, copiedFiles); - - var hostAssemblyPath = LocateExecutionHost(); - cancellationToken.ThrowIfCancellationRequested(); - - await ExecuteInIsolatedProcessCoreAsync( - hostAssemblyPath, - workingDirectory, - assemblyPath, - runBehavior.RequiresStaThread, - onOutput, - onError, - sessionId, - cancellationToken).ConfigureAwait(false); - - result.Output = string.Empty; - result.Error = string.Empty; - return result; - } - catch (OperationCanceledException) - { - await onError("代码执行已被取消").ConfigureAwait(false); - result.Error = "代码执行已被取消"; - return result; - } - catch (Exception ex) - { - await onError("Runtime error: " + ex.Message).ConfigureAwait(false); - result.Error = ex.Message; - return result; - } - finally - { - TryDeleteDirectory(workingDirectory); - } - } - - // 自定义可卸载的AssemblyLoadContext - private static string CreateWorkingDirectory() - { - var root = Path.Combine(Path.GetTempPath(), "SharpPad", "sessions"); - Directory.CreateDirectory(root); - - var directory = Path.Combine(root, $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{Guid.NewGuid():N}"); - Directory.CreateDirectory(directory); - return directory; - } - - private static async Task ExecuteInIsolatedProcessCoreAsync( - string hostAssemblyPath, - string workingDirectory, - string compiledAssemblyPath, - bool requiresStaThread, - Func onOutput, - Func onError, - string sessionId, - CancellationToken cancellationToken) - { - var startInfo = new ProcessStartInfo - { - FileName = "dotnet", - WorkingDirectory = workingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 - }; - - startInfo.ArgumentList.Add(hostAssemblyPath); - startInfo.ArgumentList.Add("--assembly"); - startInfo.ArgumentList.Add(compiledAssemblyPath); - startInfo.ArgumentList.Add("--workingDirectory"); - startInfo.ArgumentList.Add(workingDirectory); - startInfo.ArgumentList.Add("--requiresSta"); - startInfo.ArgumentList.Add(requiresStaThread ? "true" : "false"); - - using var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }; - - if (!process.Start()) - { - throw new InvalidOperationException("Failed to start execution host process."); - } - - if (process.StandardInput == null) - { - throw new InvalidOperationException("Execution host stdin was not redirected."); - } - - ProcessSession session = null; - if (!string.IsNullOrEmpty(sessionId)) - { - session = new ProcessSession(process); - lock (ProcessSessionLock) - { - ActiveProcessSessions[sessionId] = session; - } - } - - var cancellationRequested = false; - using var registration = cancellationToken.Register(() => - { - cancellationRequested = true; - TryTerminateProcess(process); - }); - - var stdoutTask = PumpStreamAsync(process.StandardOutput, onOutput, cancellationToken); - var stderrTask = PumpStreamAsync(process.StandardError, onError, cancellationToken); - - try - { - await process.WaitForExitAsync().ConfigureAwait(false); - await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false); - } - finally - { - if (!string.IsNullOrEmpty(sessionId)) - { - lock (ProcessSessionLock) - { - ActiveProcessSessions.Remove(sessionId); - } - } - } - - if (cancellationRequested || cancellationToken.IsCancellationRequested) - { - throw new OperationCanceledException(cancellationToken); - } - - return process.ExitCode; - } - - private static async Task PumpStreamAsync(StreamReader reader, Func callback, CancellationToken cancellationToken) - { - if (reader == null) - { - return; - } - - var buffer = new char[1024]; - - try - { - while (true) - { - var read = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - if (read <= 0) - { - break; - } - - var text = new string(buffer, 0, read); - await callback(text).ConfigureAwait(false); - } - } - catch (ObjectDisposedException) - { - } - catch (IOException) - { - } - } - - private static string LocateExecutionHost() - { - var baseDirectory = AppContext.BaseDirectory; - var candidates = new[] - { - Path.Combine(baseDirectory, "ExecutionHost", "SharpPad.ExecutionHost.dll"), - Path.Combine(baseDirectory, "SharpPad.ExecutionHost.dll"), - Path.Combine(baseDirectory, "..", "ExecutionHost", "SharpPad.ExecutionHost.dll") - }; - - foreach (var candidate in candidates) - { - var fullPath = Path.GetFullPath(candidate); - if (File.Exists(fullPath)) - { - return fullPath; - } - } - - throw new FileNotFoundException("Unable to locate SharpPad.ExecutionHost.dll. Ensure the ExecutionHost project is built and copied to the output directory."); - } - - private static void TryDeleteDirectory(string path) - { - if (string.IsNullOrWhiteSpace(path)) - { - return; - } - - try - { - if (Directory.Exists(path)) - { - Directory.Delete(path, recursive: true); - } - } - catch - { - // ignore cleanup errors - } - } - - private static void TryTerminateProcess(Process process) - { - if (process == null) - { - return; - } - - try - { - if (!process.HasExited) - { - process.Kill(entireProcessTree: true); - } - } - catch - { - // ignore termination failures - } - } - - private sealed class ProcessSession - { - private readonly Process _process; - private readonly StreamWriter _inputWriter; - - public ProcessSession(Process process) - { - _process = process ?? throw new ArgumentNullException(nameof(process)); - _inputWriter = process.StandardInput ?? throw new InvalidOperationException("Process stdin not available."); - _inputWriter.AutoFlush = true; - } - - public bool ProvideInput(string input) - { - if (_process.HasExited) - { - return false; - } - - lock (_inputWriter) - { - try - { - _inputWriter.WriteLine(input ?? string.Empty); - _inputWriter.Flush(); // 确保数据立即发送到子进程 - return true; - } - catch - { - return false; - } - } - } - } - - private static bool DetectWinFormsUsage(IEnumerable sources) - { - if (sources == null) - { - return false; - } - - foreach (var source in sources) - { - if (DetectWinFormsUsage(source)) - { - return true; - } - } - - return false; - } - - private static bool DetectWinFormsUsage(string source) - { - if (string.IsNullOrWhiteSpace(source)) - { - return false; - } - - return source.IndexOf("System.Windows.Forms", StringComparison.OrdinalIgnoreCase) >= 0 - || source.IndexOf("Application.Run", StringComparison.OrdinalIgnoreCase) >= 0 - || source.IndexOf(": Form", StringComparison.OrdinalIgnoreCase) >= 0 - || source.IndexOf(" new Form", StringComparison.OrdinalIgnoreCase) >= 0; - } - - private static void EnsureWinFormsAssembliesLoaded() - { - if (!OperatingSystem.IsWindows()) - { - return; - } - - TryLoadType("System.Windows.Forms.Form, System.Windows.Forms"); - TryLoadType("System.Drawing.Point, System.Drawing"); - - static void TryLoadType(string typeName) - { - try - { - _ = Type.GetType(typeName, throwOnError: false); - } - catch - { - // ignore failures; the compilation step will surface missing assemblies - } + finally + { + assembly = null; + loadContext?.Unload(); + GC.Collect(); + GC.WaitForPendingFinalizers(); } - } - private static void TryAddWinFormsReferences(List references) - { - if (references == null) throw new ArgumentNullException(nameof(references)); - - if (!OperatingSystem.IsWindows()) - { - return; - } - - var requiredAssemblies = new[] - { - "System.Windows.Forms", - "System.Drawing", - "System.Drawing.Common", - "Microsoft.Win32.SystemEvents" - }; - - var missing = new List(); - - foreach (var assemblyName in requiredAssemblies) - { - if (!TryAddFromLoadedContext(assemblyName)) - { - missing.Add(assemblyName); - } - } - - if (missing.Count > 0) - { - foreach (var pathRef in GetWindowsDesktopReferencePaths(missing)) - { - AddReferenceFromFile(pathRef); - } - } - - bool TryAddFromLoadedContext(string assemblyName) - { - try - { - var assembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(asm => string.Equals(asm.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) - ?? Assembly.Load(assemblyName); - - if (assembly != null && !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) - { - AddReferenceFromFile(assembly.Location); - return true; - } - } - catch - { - // ignore load failures; we will try reference assemblies as a fallback - } - - return false; - } - - void AddReferenceFromFile(string pathRef) - { - if (string.IsNullOrWhiteSpace(pathRef) || !File.Exists(pathRef)) - { - return; - } - - if (references.OfType() - .Any(r => string.Equals(r.FilePath, pathRef, StringComparison.OrdinalIgnoreCase))) - { - return; - } - - references.Add(MetadataReference.CreateFromFile(pathRef)); - } + return result; } - private static void EnsureStandardLibraryReferences(List references) + // WinForms 和 Web API 应用进程外执行 + private static async Task ExecuteInIsolatedProcessAsync( + List files, + string nuget, + int languageVersion, + Func onOutput, + Func onError, + string sessionId, + string projectType, + CancellationToken cancellationToken) { - if (references == null) throw new ArgumentNullException(nameof(references)); - - var essentialAssemblies = new[] + var result = new RunResult { - "System.Net.Http", - "System.Net.WebSockets", - "System.Net.WebSockets.Client", - "System.Net.WebSockets.WebSocketProtocol" + Output = string.Empty, + Error = string.Empty }; - foreach (var assemblyName in essentialAssemblies) - { - TryAddReferenceByName(references, assemblyName); - } - } + var normalizedProjectType = NormalizeProjectType(projectType); + var runBehavior = GetRunBehavior(projectType); - private static void TryAddReferenceByName(List references, string assemblyName) - { - if (references == null) throw new ArgumentNullException(nameof(references)); - if (string.IsNullOrWhiteSpace(assemblyName)) + // 检测是否需要 WinForms 支持 + if (runBehavior.OutputKind != OutputKind.WindowsApplication && DetectWinFormsUsage(files?.Select(f => f?.Content))) { - return; + runBehavior = (OutputKind.WindowsApplication, true); } - bool ContainsReference(string path) => - references.OfType() - .Any(r => string.Equals(r.FilePath, path, StringComparison.OrdinalIgnoreCase)); + var parseOptions = new CSharpParseOptions( + languageVersion: (LanguageVersion)languageVersion, + kind: SourceCodeKind.Regular, + documentationMode: DocumentationMode.Parse); - try + var syntaxTrees = new List(files.Count); + foreach (var file in files) { - var assembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(asm => !asm.IsDynamic && string.Equals(asm.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) - ?? Assembly.Load(new AssemblyName(assemblyName)); + cancellationToken.ThrowIfCancellationRequested(); + var sourceText = SourceText.From(file.Content ?? string.Empty, Encoding.UTF8); + var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, parseOptions, path: file.FileName); + syntaxTrees.Add(syntaxTree); + } - if (assembly != null && !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) + var references = new List(); + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!asm.IsDynamic && !string.IsNullOrEmpty(asm.Location)) { - if (!ContainsReference(assembly.Location)) - { - references.Add(MetadataReference.CreateFromFile(assembly.Location)); - } - return; + references.Add(MetadataReference.CreateFromFile(asm.Location)); } } - catch - { - // ignore load failures; fall back to probing reference assemblies - } - foreach (var candidate in EnumerateReferenceAssemblyCandidates(assemblyName)) + EnsureStandardLibraryReferences(references); + + if (runBehavior.OutputKind == OutputKind.WindowsApplication) { - if (File.Exists(candidate) && !ContainsReference(candidate)) - { - references.Add(MetadataReference.CreateFromFile(candidate)); - return; - } + EnsureWinFormsAssembliesLoaded(); + TryAddWinFormsReferences(references); } - } - private static IEnumerable EnumerateReferenceAssemblyCandidates(string assemblyName) - { - if (string.IsNullOrWhiteSpace(assemblyName)) + var packageReferenceMap = BuildPackageReferenceMap(nuget, normalizedProjectType); + var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget); + foreach (var package in nugetAssemblies) { - yield break; + references.Add(MetadataReference.CreateFromFile(package.Path)); } - var runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location); - if (!string.IsNullOrEmpty(runtimeDir)) + foreach (var pkg in packageReferenceMap.Keys) { - yield return Path.Combine(runtimeDir, assemblyName + ".dll"); + TryEnsureAssemblyLoaded(pkg); } - foreach (var root in EnumerateDotnetRootCandidates()) + var compilation = CSharpCompilation.Create( + "DynamicProgram", + syntaxTrees, + references, + new CSharpCompilationOptions(runBehavior.OutputKind)); + + var workingDirectory = CreateWorkingDirectory(); + var assemblyPath = Path.Combine(workingDirectory, "DynamicProgram.dll"); + var pdbPath = Path.Combine(workingDirectory, "DynamicProgram.pdb"); + + try { - var packBase = Path.Combine(root, "packs", "Microsoft.NETCore.App.Ref"); - if (!Directory.Exists(packBase)) + using (var peStream = File.Create(assemblyPath)) + using (var pdbStream = File.Create(pdbPath)) { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); - var versionDirs = Directory.GetDirectories(packBase) - .Select(dir => new { dir, version = TryParseVersionFromDirectory(Path.GetFileName(dir)) }) - .OrderByDescending(item => item.version) - .ThenByDescending(item => item.dir, StringComparer.OrdinalIgnoreCase); + var emitResult = compilation.Emit( + peStream, + pdbStream, + options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb)); - foreach (var versionDir in versionDirs) + if (!emitResult.Success) + { + foreach (var diagnostic in emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) + { + await onError(diagnostic.ToString()).ConfigureAwait(false); + } + + result.Error = "Compilation error"; + return result; + } + } + + var copiedFiles = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var package in nugetAssemblies) { - var refRoot = Path.Combine(versionDir.dir, "ref"); - if (!Directory.Exists(refRoot)) + cancellationToken.ThrowIfCancellationRequested(); + + var destination = Path.Combine(workingDirectory, Path.GetFileName(package.Path)); + if (string.Equals(package.Path, destination, StringComparison.OrdinalIgnoreCase)) { continue; } - foreach (var tfmDir in EnumerateTfmDirectories(refRoot)) + if (!copiedFiles.Add(destination)) { - yield return Path.Combine(tfmDir, assemblyName + ".dll"); + continue; + } + + try + { + File.Copy(package.Path, destination, overwrite: true); + } + catch + { + // ignore copy failures; the execution host may still resolve assemblies from their original locations } } + + CopyHostAssembliesForExecution(workingDirectory, copiedFiles); + CopyAssembliesForPackages(packageReferenceMap.Keys, workingDirectory, copiedFiles); + + var hostAssemblyPath = LocateExecutionHost(); + cancellationToken.ThrowIfCancellationRequested(); + + await ExecuteInIsolatedProcessCoreAsync( + hostAssemblyPath, + workingDirectory, + assemblyPath, + runBehavior.RequiresStaThread, + onOutput, + onError, + sessionId, + cancellationToken).ConfigureAwait(false); + + result.Output = string.Empty; + result.Error = string.Empty; + return result; + } + catch (OperationCanceledException) + { + await onError("代码执行已被取消").ConfigureAwait(false); + result.Error = "代码执行已被取消"; + return result; + } + catch (Exception ex) + { + await onError("Runtime error: " + ex.Message).ConfigureAwait(false); + result.Error = ex.Message; + return result; + } + finally + { + TryDeleteDirectory(workingDirectory); } } - private static List BuildPackageReferenceLines(string nuget, string normalizedProjectType) + // 自定义可卸载的AssemblyLoadContext + private static string CreateWorkingDirectory() { - var map = BuildPackageReferenceMap(nuget, normalizedProjectType); - return map - .OrderBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase) - .Select(entry => string.IsNullOrWhiteSpace(entry.Value) - ? $" " - : $" ") - .ToList(); - } - - - private static Dictionary BuildPackageReferenceMap(string nuget, string normalizedProjectType) - { - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - - void Add(string? id, string? version) - { - if (string.IsNullOrWhiteSpace(id)) - { - return; - } - - var normalizedId = id.Trim(); - var normalizedVersion = NormalizePackageVersion(version); - - if (map.TryGetValue(normalizedId, out var existing)) - { - if (string.IsNullOrWhiteSpace(existing) && !string.IsNullOrWhiteSpace(normalizedVersion)) - { - map[normalizedId] = normalizedVersion; - } - - return; - } - - map[normalizedId] = normalizedVersion; - } - - if (!string.IsNullOrWhiteSpace(nuget)) - { - foreach (var part in nuget.Split(';', StringSplitOptions.RemoveEmptyEntries)) - { - var items = part.Split(',', StringSplitOptions.RemoveEmptyEntries); - var id = items.Length > 0 ? items[0].Trim() : null; - var version = items.Length > 1 ? items[1].Trim() : null; - Add(id, version); - } - } - - void AddDefaultPackages() - { - switch (normalizedProjectType) - { - case "aspnetcore": - case "aspnetcorewebapi": - case "webapi": - case "web": - Add("Microsoft.AspNetCore.Mvc.NewtonsoftJson", TryGetPackageVersionFromAssembly("Microsoft.AspNetCore.Mvc.NewtonsoftJson", "8.0.11")); - Add("Newtonsoft.Json", TryGetPackageVersionFromAssembly("Newtonsoft.Json", "13.0.3")); - Add("Swashbuckle.AspNetCore.Swagger", TryGetPackageVersionFromAssembly("Swashbuckle.AspNetCore.Swagger", "6.8.1")); - Add("Swashbuckle.AspNetCore.SwaggerGen", TryGetPackageVersionFromAssembly("Swashbuckle.AspNetCore.SwaggerGen", "6.8.1")); - Add("Swashbuckle.AspNetCore.SwaggerUI", TryGetPackageVersionFromAssembly("Swashbuckle.AspNetCore.SwaggerUI", "6.8.1")); - break; - } - } - - AddDefaultPackages(); - return map; - } - - private static string? NormalizePackageVersion(string? version) - { - if (string.IsNullOrWhiteSpace(version)) - { - return null; - } - - var trimmed = version.Trim(); - var plusIndex = trimmed.IndexOf('+'); - if (plusIndex >= 0) - { - trimmed = trimmed[..plusIndex]; - } - - return trimmed.Length == 0 ? null : trimmed; - } - - private static string? TryGetPackageVersionFromAssembly(string assemblyName, string? fallbackVersion = null) - { - if (string.IsNullOrWhiteSpace(assemblyName)) - { - return NormalizePackageVersion(fallbackVersion); - } - - try - { - var assembly = AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(a => !a.IsDynamic && string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) - ?? Assembly.Load(new AssemblyName(assemblyName)); - - if (assembly != null) - { - string? version = null; - - if (!string.IsNullOrWhiteSpace(assembly.Location)) - { - try - { - var info = FileVersionInfo.GetVersionInfo(assembly.Location); - version = NormalizePackageVersion(info.ProductVersion) ?? NormalizePackageVersion(info.FileVersion); - } - catch - { - // ignore version probing failures - } - } - - version ??= NormalizePackageVersion(assembly.GetName().Version?.ToString()); - - if (!string.IsNullOrWhiteSpace(version)) - { - return version; - } - } - } - catch - { - // ignore load failures - } - - return NormalizePackageVersion(fallbackVersion); - } - - - private static void TryEnsureAssemblyLoaded(string assemblyName) - { - if (string.IsNullOrWhiteSpace(assemblyName)) - { - return; - } - - try - { - var loaded = AppDomain.CurrentDomain - .GetAssemblies() - .Any(a => !a.IsDynamic && string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); - - if (!loaded) - { - Assembly.Load(new AssemblyName(assemblyName)); - } - } - catch - { - // ignore load failures; references may not be required at runtime - } - } - - private static void CopyAssembliesForPackages(IEnumerable packageIds, string workingDirectory, HashSet copiedFiles) - { - if (packageIds == null) - { - return; - } - - foreach (var packageId in packageIds) - { - CopyAssemblyByName(packageId, workingDirectory, copiedFiles); - } - } - - private static void CopyAssemblyByName(string assemblyName, string workingDirectory, HashSet copiedFiles) - { - if (string.IsNullOrWhiteSpace(assemblyName)) - { - return; - } - - Assembly? assembly = null; - try - { - assembly = AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(a => !a.IsDynamic && string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); - - assembly ??= Assembly.Load(new AssemblyName(assemblyName)); - } - catch - { - assembly = null; - } - - if (assembly == null || string.IsNullOrWhiteSpace(assembly.Location)) - { - return; - } - - try - { - CopyAssemblyFile(assembly.Location, workingDirectory, copiedFiles); - - var directory = Path.GetDirectoryName(assembly.Location); - if (!string.IsNullOrEmpty(directory)) - { - foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.TopDirectoryOnly)) - { - CopyAssemblyFile(file, workingDirectory, copiedFiles); - } - } - } - catch - { - // ignore copy failures - } - } - - private static void CopyHostAssembliesForExecution(string workingDirectory, HashSet copiedFiles) - { - var baseDirectory = AppContext.BaseDirectory; - if (string.IsNullOrWhiteSpace(baseDirectory)) - { - return; - } - - string? normalizedBase; - try - { - normalizedBase = Path.GetFullPath(baseDirectory); - } - catch - { - normalizedBase = null; - } - - if (string.IsNullOrEmpty(normalizedBase)) - { - return; - } - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (assembly.IsDynamic || string.IsNullOrWhiteSpace(assembly.Location)) - { - continue; - } - - string fullPath; - try - { - fullPath = Path.GetFullPath(assembly.Location); - } - catch - { - continue; - } - - if (!fullPath.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - CopyAssemblyFile(fullPath, workingDirectory, copiedFiles); - } - } - - private static void CopyAssemblyFile(string sourcePath, string workingDirectory, HashSet copiedFiles) - { - if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath)) - { - return; - } - - var fileName = Path.GetFileName(sourcePath); - if (string.IsNullOrWhiteSpace(fileName)) - { - return; - } - - var destination = Path.Combine(workingDirectory, fileName); - if (!copiedFiles.Add(destination)) - { - return; - } - - try - { - File.Copy(sourcePath, destination, overwrite: true); - } - catch - { - copiedFiles.Remove(destination); - } - } - + var root = Path.Combine(Path.GetTempPath(), "SharpPad", "sessions"); + Directory.CreateDirectory(root); + + var directory = Path.Combine(root, $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + return directory; + } + + private static async Task ExecuteInIsolatedProcessCoreAsync( + string hostAssemblyPath, + string workingDirectory, + string compiledAssemblyPath, + bool requiresStaThread, + Func onOutput, + Func onError, + string sessionId, + CancellationToken cancellationToken) + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + + startInfo.ArgumentList.Add(hostAssemblyPath); + startInfo.ArgumentList.Add("--assembly"); + startInfo.ArgumentList.Add(compiledAssemblyPath); + startInfo.ArgumentList.Add("--workingDirectory"); + startInfo.ArgumentList.Add(workingDirectory); + startInfo.ArgumentList.Add("--requiresSta"); + startInfo.ArgumentList.Add(requiresStaThread ? "true" : "false"); + + using var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }; + + if (!process.Start()) + { + throw new InvalidOperationException("Failed to start execution host process."); + } + + if (process.StandardInput == null) + { + throw new InvalidOperationException("Execution host stdin was not redirected."); + } + + ProcessSession session = null; + if (!string.IsNullOrEmpty(sessionId)) + { + session = new ProcessSession(process); + lock (ProcessSessionLock) + { + ActiveProcessSessions[sessionId] = session; + } + } + + var cancellationRequested = false; + using var registration = cancellationToken.Register(() => + { + cancellationRequested = true; + TryTerminateProcess(process); + }); + + var stdoutTask = PumpStreamAsync(process.StandardOutput, onOutput, cancellationToken); + var stderrTask = PumpStreamAsync(process.StandardError, onError, cancellationToken); + + try + { + await process.WaitForExitAsync().ConfigureAwait(false); + await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false); + } + finally + { + if (!string.IsNullOrEmpty(sessionId)) + { + lock (ProcessSessionLock) + { + ActiveProcessSessions.Remove(sessionId); + } + } + } + + if (cancellationRequested || cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + + return process.ExitCode; + } + + private static async Task PumpStreamAsync(StreamReader reader, Func callback, CancellationToken cancellationToken) + { + if (reader == null) + { + return; + } + + var buffer = new char[1024]; + + try + { + while (true) + { + var read = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + if (read <= 0) + { + break; + } + + var text = new string(buffer, 0, read); + await callback(text).ConfigureAwait(false); + } + } + catch (ObjectDisposedException) + { + } + catch (IOException) + { + } + } + + private static string LocateExecutionHost() + { + var baseDirectory = AppContext.BaseDirectory; + var candidates = new[] + { + Path.Combine(baseDirectory, "ExecutionHost", "SharpPad.ExecutionHost.dll"), + Path.Combine(baseDirectory, "SharpPad.ExecutionHost.dll"), + Path.Combine(baseDirectory, "..", "ExecutionHost", "SharpPad.ExecutionHost.dll") + }; + + foreach (var candidate in candidates) + { + var fullPath = Path.GetFullPath(candidate); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + throw new FileNotFoundException("Unable to locate SharpPad.ExecutionHost.dll. Ensure the ExecutionHost project is built and copied to the output directory."); + } + + private static void TryDeleteDirectory(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } + catch + { + // ignore cleanup errors + } + } + + private static void TryTerminateProcess(Process process) + { + if (process == null) + { + return; + } + + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + // ignore termination failures + } + } + + private sealed class ProcessSession + { + private readonly Process _process; + private readonly StreamWriter _inputWriter; + + public ProcessSession(Process process) + { + _process = process ?? throw new ArgumentNullException(nameof(process)); + _inputWriter = process.StandardInput ?? throw new InvalidOperationException("Process stdin not available."); + _inputWriter.AutoFlush = true; + } + + public bool ProvideInput(string input) + { + if (_process.HasExited) + { + return false; + } + + lock (_inputWriter) + { + try + { + _inputWriter.WriteLine(input ?? string.Empty); + _inputWriter.Flush(); // 确保数据立即发送到子进程 + return true; + } + catch + { + return false; + } + } + } + } + + private static bool DetectWinFormsUsage(IEnumerable sources) + { + if (sources == null) + { + return false; + } + + foreach (var source in sources) + { + if (DetectWinFormsUsage(source)) + { + return true; + } + } + + return false; + } + + private static bool DetectWinFormsUsage(string source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return false; + } + + return source.IndexOf("System.Windows.Forms", StringComparison.OrdinalIgnoreCase) >= 0 + || source.IndexOf("Application.Run", StringComparison.OrdinalIgnoreCase) >= 0 + || source.IndexOf(": Form", StringComparison.OrdinalIgnoreCase) >= 0 + || source.IndexOf(" new Form", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static void EnsureWinFormsAssembliesLoaded() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + TryLoadType("System.Windows.Forms.Form, System.Windows.Forms"); + TryLoadType("System.Drawing.Point, System.Drawing"); + + static void TryLoadType(string typeName) + { + try + { + _ = Type.GetType(typeName, throwOnError: false); + } + catch + { + // ignore failures; the compilation step will surface missing assemblies + } + } + } + + private static void TryAddWinFormsReferences(List references) + { + if (references == null) throw new ArgumentNullException(nameof(references)); + + if (!OperatingSystem.IsWindows()) + { + return; + } + + var requiredAssemblies = new[] + { + "System.Windows.Forms", + "System.Drawing", + "System.Drawing.Common", + "Microsoft.Win32.SystemEvents" + }; + + var missing = new List(); + + foreach (var assemblyName in requiredAssemblies) + { + if (!TryAddFromLoadedContext(assemblyName)) + { + missing.Add(assemblyName); + } + } + + if (missing.Count > 0) + { + foreach (var pathRef in GetWindowsDesktopReferencePaths(missing)) + { + AddReferenceFromFile(pathRef); + } + } + + bool TryAddFromLoadedContext(string assemblyName) + { + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(asm => string.Equals(asm.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) + ?? Assembly.Load(assemblyName); + + if (assembly != null && !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) + { + AddReferenceFromFile(assembly.Location); + return true; + } + } + catch + { + // ignore load failures; we will try reference assemblies as a fallback + } + + return false; + } + + void AddReferenceFromFile(string pathRef) + { + if (string.IsNullOrWhiteSpace(pathRef) || !File.Exists(pathRef)) + { + return; + } + + if (references.OfType() + .Any(r => string.Equals(r.FilePath, pathRef, StringComparison.OrdinalIgnoreCase))) + { + return; + } + + references.Add(MetadataReference.CreateFromFile(pathRef)); + } + } + + private static void EnsureStandardLibraryReferences(List references) + { + if (references == null) throw new ArgumentNullException(nameof(references)); + + var essentialAssemblies = new[] + { + "System.Net.Http", + "System.Net.WebSockets", + "System.Net.WebSockets.Client", + "System.Net.WebSockets.WebSocketProtocol" + }; + + foreach (var assemblyName in essentialAssemblies) + { + TryAddReferenceByName(references, assemblyName); + } + } + + private static void TryAddReferenceByName(List references, string assemblyName) + { + if (references == null) throw new ArgumentNullException(nameof(references)); + if (string.IsNullOrWhiteSpace(assemblyName)) + { + return; + } + + bool ContainsReference(string path) => + references.OfType() + .Any(r => string.Equals(r.FilePath, path, StringComparison.OrdinalIgnoreCase)); + + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(asm => !asm.IsDynamic && string.Equals(asm.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) + ?? Assembly.Load(new AssemblyName(assemblyName)); + + if (assembly != null && !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) + { + if (!ContainsReference(assembly.Location)) + { + references.Add(MetadataReference.CreateFromFile(assembly.Location)); + } + return; + } + } + catch + { + // ignore load failures; fall back to probing reference assemblies + } + + foreach (var candidate in EnumerateReferenceAssemblyCandidates(assemblyName)) + { + if (File.Exists(candidate) && !ContainsReference(candidate)) + { + references.Add(MetadataReference.CreateFromFile(candidate)); + return; + } + } + } + + private static IEnumerable EnumerateReferenceAssemblyCandidates(string assemblyName) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + yield break; + } + + var runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location); + if (!string.IsNullOrEmpty(runtimeDir)) + { + yield return Path.Combine(runtimeDir, assemblyName + ".dll"); + } + + foreach (var root in EnumerateDotnetRootCandidates()) + { + var packBase = Path.Combine(root, "packs", "Microsoft.NETCore.App.Ref"); + if (!Directory.Exists(packBase)) + { + continue; + } + + var versionDirs = Directory.GetDirectories(packBase) + .Select(dir => new { dir, version = TryParseVersionFromDirectory(Path.GetFileName(dir)) }) + .OrderByDescending(item => item.version) + .ThenByDescending(item => item.dir, StringComparer.OrdinalIgnoreCase); + + foreach (var versionDir in versionDirs) + { + var refRoot = Path.Combine(versionDir.dir, "ref"); + if (!Directory.Exists(refRoot)) + { + continue; + } + + foreach (var tfmDir in EnumerateTfmDirectories(refRoot)) + { + yield return Path.Combine(tfmDir, assemblyName + ".dll"); + } + } + } + } + + private static List BuildPackageReferenceLines(string nuget, string normalizedProjectType) + { + var map = BuildPackageReferenceMap(nuget, normalizedProjectType); + return map + .OrderBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase) + .Select(entry => string.IsNullOrWhiteSpace(entry.Value) + ? $" " + : $" ") + .ToList(); + } + + + private static Dictionary BuildPackageReferenceMap(string nuget, string normalizedProjectType) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + + void Add(string? id, string? version) + { + if (string.IsNullOrWhiteSpace(id)) + { + return; + } + + var normalizedId = id.Trim(); + var normalizedVersion = NormalizePackageVersion(version); + + if (map.TryGetValue(normalizedId, out var existing)) + { + if (string.IsNullOrWhiteSpace(existing) && !string.IsNullOrWhiteSpace(normalizedVersion)) + { + map[normalizedId] = normalizedVersion; + } + + return; + } + + map[normalizedId] = normalizedVersion; + } + + if (!string.IsNullOrWhiteSpace(nuget)) + { + foreach (var part in nuget.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + var items = part.Split(',', StringSplitOptions.RemoveEmptyEntries); + var id = items.Length > 0 ? items[0].Trim() : null; + var version = items.Length > 1 ? items[1].Trim() : null; + Add(id, version); + } + } + + void AddDefaultPackages() + { + switch (normalizedProjectType) + { + case "aspnetcore": + case "aspnetcorewebapi": + case "webapi": + case "web": + Add("Microsoft.AspNetCore.Mvc.NewtonsoftJson", TryGetPackageVersionFromAssembly("Microsoft.AspNetCore.Mvc.NewtonsoftJson", "8.0.11")); + Add("Newtonsoft.Json", TryGetPackageVersionFromAssembly("Newtonsoft.Json", "13.0.3")); + Add("Swashbuckle.AspNetCore.Swagger", TryGetPackageVersionFromAssembly("Swashbuckle.AspNetCore.Swagger", "6.8.1")); + Add("Swashbuckle.AspNetCore.SwaggerGen", TryGetPackageVersionFromAssembly("Swashbuckle.AspNetCore.SwaggerGen", "6.8.1")); + Add("Swashbuckle.AspNetCore.SwaggerUI", TryGetPackageVersionFromAssembly("Swashbuckle.AspNetCore.SwaggerUI", "6.8.1")); + break; + } + } + + AddDefaultPackages(); + return map; + } + + private static string? NormalizePackageVersion(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + { + return null; + } + + var trimmed = version.Trim(); + var plusIndex = trimmed.IndexOf('+'); + if (plusIndex >= 0) + { + trimmed = trimmed[..plusIndex]; + } + + return trimmed.Length == 0 ? null : trimmed; + } + + private static string? TryGetPackageVersionFromAssembly(string assemblyName, string? fallbackVersion = null) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + return NormalizePackageVersion(fallbackVersion); + } + + try + { + var assembly = AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(a => !a.IsDynamic && string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) + ?? Assembly.Load(new AssemblyName(assemblyName)); + + if (assembly != null) + { + string? version = null; + + if (!string.IsNullOrWhiteSpace(assembly.Location)) + { + try + { + var info = FileVersionInfo.GetVersionInfo(assembly.Location); + version = NormalizePackageVersion(info.ProductVersion) ?? NormalizePackageVersion(info.FileVersion); + } + catch + { + // ignore version probing failures + } + } + + version ??= NormalizePackageVersion(assembly.GetName().Version?.ToString()); + + if (!string.IsNullOrWhiteSpace(version)) + { + return version; + } + } + } + catch + { + // ignore load failures + } + + return NormalizePackageVersion(fallbackVersion); + } + + + private static void TryEnsureAssemblyLoaded(string assemblyName) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + return; + } + + try + { + var loaded = AppDomain.CurrentDomain + .GetAssemblies() + .Any(a => !a.IsDynamic && string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); + + if (!loaded) + { + Assembly.Load(new AssemblyName(assemblyName)); + } + } + catch + { + // ignore load failures; references may not be required at runtime + } + } + + private static void CopyAssembliesForPackages(IEnumerable packageIds, string workingDirectory, HashSet copiedFiles) + { + if (packageIds == null) + { + return; + } + + foreach (var packageId in packageIds) + { + CopyAssemblyByName(packageId, workingDirectory, copiedFiles); + } + } + + private static void CopyAssemblyByName(string assemblyName, string workingDirectory, HashSet copiedFiles) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + return; + } + + Assembly? assembly = null; + try + { + assembly = AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(a => !a.IsDynamic && string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); + + assembly ??= Assembly.Load(new AssemblyName(assemblyName)); + } + catch + { + assembly = null; + } + + if (assembly == null || string.IsNullOrWhiteSpace(assembly.Location)) + { + return; + } + + try + { + CopyAssemblyFile(assembly.Location, workingDirectory, copiedFiles); + + var directory = Path.GetDirectoryName(assembly.Location); + if (!string.IsNullOrEmpty(directory)) + { + foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.TopDirectoryOnly)) + { + CopyAssemblyFile(file, workingDirectory, copiedFiles); + } + } + } + catch + { + // ignore copy failures + } + } + + private static void CopyHostAssembliesForExecution(string workingDirectory, HashSet copiedFiles) + { + var baseDirectory = AppContext.BaseDirectory; + if (string.IsNullOrWhiteSpace(baseDirectory)) + { + return; + } + + string? normalizedBase; + try + { + normalizedBase = Path.GetFullPath(baseDirectory); + } + catch + { + normalizedBase = null; + } + + if (string.IsNullOrEmpty(normalizedBase)) + { + return; + } + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.IsDynamic || string.IsNullOrWhiteSpace(assembly.Location)) + { + continue; + } + + string fullPath; + try + { + fullPath = Path.GetFullPath(assembly.Location); + } + catch + { + continue; + } + + if (!fullPath.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + CopyAssemblyFile(fullPath, workingDirectory, copiedFiles); + } + } + + private static void CopyAssemblyFile(string sourcePath, string workingDirectory, HashSet copiedFiles) + { + if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath)) + { + return; + } + + var fileName = Path.GetFileName(sourcePath); + if (string.IsNullOrWhiteSpace(fileName)) + { + return; + } + + var destination = Path.Combine(workingDirectory, fileName); + if (!copiedFiles.Add(destination)) + { + return; + } + + try + { + File.Copy(sourcePath, destination, overwrite: true); + } + catch + { + copiedFiles.Remove(destination); + } + } + private static string GetHostTargetFramework(bool requireWindows) { - var version = Environment.Version; - var major = version.Major >= 5 ? version.Major : 6; - var minor = version.Major >= 5 ? Math.Max(0, version.Minor) : 0; - var moniker = $"net{major}.{minor}"; - if (requireWindows) + var version = Environment.Version; + var major = version.Major >= 5 ? version.Major : 6; + var minor = version.Major >= 5 ? Math.Max(0, version.Minor) : 0; + var moniker = $"net{major}.{minor}"; + if (requireWindows) + { + moniker += "-windows"; + } + return moniker; + } + + private static string GetRuntimeIdentifier() + { + var arch = RuntimeInformation.ProcessArchitecture; + + if (OperatingSystem.IsWindows()) + { + return arch switch + { + Architecture.Arm64 => "win-arm64", + Architecture.Arm => "win-arm", + Architecture.X86 => "win-x86", + _ => "win-x64" + }; + } + + if (OperatingSystem.IsMacOS()) + { + return arch switch + { + Architecture.Arm64 => "osx-arm64", + Architecture.X64 => "osx-x64", + _ => "osx-x64" + }; + } + + if (OperatingSystem.IsLinux()) + { + return arch switch + { + Architecture.Arm64 => "linux-arm64", + Architecture.Arm => "linux-arm", + Architecture.X86 => "linux-x86", + _ => "linux-x64" + }; + } + + return "linux-x64"; + } + + private static IEnumerable GetWindowsDesktopReferencePaths(IEnumerable assemblyNames) + { + if (assemblyNames == null) + { + return Array.Empty(); + } + + var names = assemblyNames + .Where(n => !string.IsNullOrWhiteSpace(n)) + .Select(n => n.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (names.Length == 0) + { + return Array.Empty(); + } + + foreach (var packDir in EnumerateWindowsDesktopPackDirectories()) + { + var refRoot = Path.Combine(packDir, "ref"); + if (!Directory.Exists(refRoot)) + { + continue; + } + + foreach (var tfmDir in EnumerateTfmDirectories(refRoot)) + { + var resolved = new List(); + foreach (var name in names) + { + var candidate = Path.Combine(tfmDir, name + ".dll"); + if (File.Exists(candidate)) + { + resolved.Add(candidate); + } + } + + if (resolved.Count > 0) + { + return resolved; + } + } + } + + return Array.Empty(); + } + + private static IEnumerable EnumerateWindowsDesktopPackDirectories() + { + foreach (var root in EnumerateDotnetRootCandidates()) + { + var packBase = Path.Combine(root, "packs", "Microsoft.WindowsDesktop.App.Ref"); + if (!Directory.Exists(packBase)) + { + continue; + } + + var versionDirs = Directory.GetDirectories(packBase) + .Select(dir => new { dir, version = TryParseVersionFromDirectory(Path.GetFileName(dir)) }) + .OrderByDescending(item => item.version) + .ThenByDescending(item => item.dir, StringComparer.OrdinalIgnoreCase); + + foreach (var item in versionDirs) + { + yield return item.dir; + } + } + } + + private static IEnumerable EnumerateDotnetRootCandidates() + { + var roots = new HashSet(StringComparer.OrdinalIgnoreCase); + + void AddIfExists(string pathCandidate) + { + if (string.IsNullOrWhiteSpace(pathCandidate)) + { + return; + } + + try + { + var fullPath = Path.GetFullPath(pathCandidate.Trim()); + if (Directory.Exists(fullPath)) + { + roots.Add(fullPath); + } + } + catch + { + // ignore invalid paths + } + } + + AddIfExists(Environment.GetEnvironmentVariable("DOTNET_ROOT")); + AddIfExists(Environment.GetEnvironmentVariable("DOTNET_ROOT(x86)")); + + try + { + var processPath = Environment.ProcessPath; + if (!string.IsNullOrEmpty(processPath)) + { + var dir = Path.GetDirectoryName(processPath); + AddIfExists(dir); + } + } + catch + { + // ignore access errors + } + + try + { + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + if (!string.IsNullOrEmpty(programFiles)) + { + AddIfExists(Path.Combine(programFiles, "dotnet")); + } + } + catch + { + // ignore folder resolution errors + } + + try + { + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + if (!string.IsNullOrEmpty(programFilesX86)) + { + AddIfExists(Path.Combine(programFilesX86, "dotnet")); + } + } + catch + { + // ignore folder resolution errors + } + + return roots; + } + + private static IEnumerable EnumerateTfmDirectories(string refRoot) + { + if (!Directory.Exists(refRoot)) { - moniker += "-windows"; + yield break; + } + + var runtimeVersion = Environment.Version; + + var candidates = Directory.GetDirectories(refRoot) + .Select(dir => + { + var name = Path.GetFileName(dir); + var version = ParseTfmVersion(name); + var isWindows = name.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0; + var scoreMajor = version.Major == runtimeVersion.Major ? 1 : 0; + var scoreMinor = version.Minor == runtimeVersion.Minor ? 1 : 0; + return new + { + Dir = dir, + Name = name, + Version = version, + IsWindows = isWindows, + ScoreMajor = scoreMajor, + ScoreMinor = scoreMinor + }; + }) + .OrderByDescending(c => c.IsWindows) + .ThenByDescending(c => c.ScoreMajor) + .ThenByDescending(c => c.ScoreMinor) + .ThenByDescending(c => c.Version) + .ThenByDescending(c => c.Name, StringComparer.OrdinalIgnoreCase); + + foreach (var candidate in candidates) + { + yield return candidate.Dir; } - return moniker; } - private static string GetRuntimeIdentifier() + private static Version ParseTfmVersion(string tfm) { - var arch = RuntimeInformation.ProcessArchitecture; + if (string.IsNullOrWhiteSpace(tfm) || !tfm.StartsWith("net", StringComparison.OrdinalIgnoreCase)) + { + return new Version(0, 0); + } - if (OperatingSystem.IsWindows()) + var span = tfm.AsSpan(3); + var dashIndex = span.IndexOf('-'); + if (dashIndex >= 0) { - return arch switch + span = span[..dashIndex]; + } + + return Version.TryParse(span.ToString(), out var version) ? version : new Version(0, 0); + } + + private static Version TryParseVersionFromDirectory(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return new Version(0, 0); + } + + return Version.TryParse(name, out var version) ? version : new Version(0, 0); + } + + private static string BuildProjectFileContent(string assemblyName, string nuget, int languageVersion, string projectType) + { + var normalizedType = NormalizeProjectType(projectType); + var sdk = "Microsoft.NET.Sdk"; + var targetFramework = GetHostTargetFramework(requireWindows: false); + var outputTypeValue = "Exe"; + var propertyExtraLines = new List(); + var frameworkReferences = new List(); + + switch (normalizedType) + { + case "winforms": + targetFramework = GetHostTargetFramework(requireWindows: true); + outputTypeValue = "WinExe"; + propertyExtraLines.Add(" true"); + propertyExtraLines.Add(" true"); + frameworkReferences.Add(" "); + break; + case "webapi": + sdk = "Microsoft.NET.Sdk.Web"; + break; + } + + var langVer = "latest"; + try + { + langVer = ((LanguageVersion)languageVersion).ToString(); + } + catch + { + // keep default when conversion fails + } + + var builder = new StringBuilder(); + builder.AppendLine($""); + builder.AppendLine(" "); + builder.AppendLine($" {outputTypeValue}"); + builder.AppendLine($" {targetFramework}"); + builder.AppendLine(" enable"); + builder.AppendLine(" enable"); + builder.AppendLine($" {assemblyName}"); + builder.AppendLine($" {assemblyName}"); + builder.AppendLine($" {langVer}"); + foreach (var line in propertyExtraLines) + { + builder.AppendLine(line); + } + builder.AppendLine(" "); + + var packages = BuildPackageReferenceLines(nuget, normalizedType); + if (packages.Count > 0) + { + builder.AppendLine(" "); + foreach (var line in packages) { - Architecture.Arm64 => "win-arm64", - Architecture.Arm => "win-arm", - Architecture.X86 => "win-x86", - _ => "win-x64" - }; + builder.AppendLine(line); + } + builder.AppendLine(" "); } - if (OperatingSystem.IsMacOS()) + if (frameworkReferences.Count > 0) { - return arch switch + builder.AppendLine(" "); + foreach (var line in frameworkReferences) { - Architecture.Arm64 => "osx-arm64", - Architecture.X64 => "osx-x64", - _ => "osx-x64" - }; + builder.AppendLine(line); + } + builder.AppendLine(" "); } - if (OperatingSystem.IsLinux()) + builder.AppendLine(""); + return builder.ToString(); + } + + private static string DeriveAssemblyNameForRun(List files) + { + var candidate = files?.FirstOrDefault(f => f?.IsEntry == true)?.FileName + ?? files?.FirstOrDefault()?.FileName + ?? "Program.cs"; + + var baseName = Path.GetFileNameWithoutExtension(candidate); + if (string.IsNullOrWhiteSpace(baseName)) { - return arch switch + baseName = "SharpPadProgram"; + } + + var sanitized = new string(baseName.Select(ch => char.IsLetterOrDigit(ch) || ch == '_' ? ch : '_').ToArray()); + return string.IsNullOrWhiteSpace(sanitized) ? "SharpPadProgram" : sanitized; + } + + private static async Task WriteSourceFilesAsync(List files, string destinationDir) + { + if (files == null) + { + return; + } + + foreach (var file in files) + { + var relative = string.IsNullOrWhiteSpace(file?.FileName) ? "Program.cs" : file.FileName; + relative = relative.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + var segments = relative.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) { - Architecture.Arm64 => "linux-arm64", - Architecture.Arm => "linux-arm", - Architecture.X86 => "linux-x86", - _ => "linux-x64" - }; + segments = new[] { "Program.cs" }; + } + + for (var i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + foreach (var invalid in Path.GetInvalidFileNameChars()) + { + segment = segment.Replace(invalid, '_'); + } + segments[i] = segment; + } + + var safeRelativePath = Path.Combine(segments); + var destinationPath = Path.Combine(destinationDir, safeRelativePath); + var directory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + await File.WriteAllTextAsync(destinationPath, file?.Content ?? string.Empty, Encoding.UTF8).ConfigureAwait(false); } + } - return "linux-x64"; + private static async Task<(int ExitCode, string StdOut, string StdErr)> RunProcessCaptureAsync(string fileName, string arguments, string workingDirectory, IDictionary environment = null) + { + var psi = new ProcessStartInfo(fileName, arguments) + { + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + + if (environment != null) + { + foreach (var kv in environment) + { + psi.Environment[kv.Key] = kv.Value; + } + } + + using var process = new Process { StartInfo = psi }; + var stdOut = new StringBuilder(); + var stdErr = new StringBuilder(); + process.OutputDataReceived += (_, e) => { if (e.Data != null) stdOut.AppendLine(e.Data); }; + process.ErrorDataReceived += (_, e) => { if (e.Data != null) stdErr.AppendLine(e.Data); }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + await process.WaitForExitAsync().ConfigureAwait(false); + + return (process.ExitCode, stdOut.ToString(), stdErr.ToString()); + } + + public static void DownloadPackage(string nuget, string preferredSourceKey = null) + { + DownloadNugetPackages.DownloadAllPackagesAsync(nuget, preferredSourceKey).GetAwaiter().GetResult(); } - - private static IEnumerable GetWindowsDesktopReferencePaths(IEnumerable assemblyNames) - { - if (assemblyNames == null) - { - return Array.Empty(); - } - - var names = assemblyNames - .Where(n => !string.IsNullOrWhiteSpace(n)) - .Select(n => n.Trim()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (names.Length == 0) - { - return Array.Empty(); - } - - foreach (var packDir in EnumerateWindowsDesktopPackDirectories()) - { - var refRoot = Path.Combine(packDir, "ref"); - if (!Directory.Exists(refRoot)) - { - continue; - } - - foreach (var tfmDir in EnumerateTfmDirectories(refRoot)) - { - var resolved = new List(); - foreach (var name in names) - { - var candidate = Path.Combine(tfmDir, name + ".dll"); - if (File.Exists(candidate)) - { - resolved.Add(candidate); - } - } - - if (resolved.Count > 0) - { - return resolved; - } - } - } - - return Array.Empty(); - } - - private static IEnumerable EnumerateWindowsDesktopPackDirectories() - { - foreach (var root in EnumerateDotnetRootCandidates()) - { - var packBase = Path.Combine(root, "packs", "Microsoft.WindowsDesktop.App.Ref"); - if (!Directory.Exists(packBase)) - { - continue; - } - - var versionDirs = Directory.GetDirectories(packBase) - .Select(dir => new { dir, version = TryParseVersionFromDirectory(Path.GetFileName(dir)) }) - .OrderByDescending(item => item.version) - .ThenByDescending(item => item.dir, StringComparer.OrdinalIgnoreCase); - - foreach (var item in versionDirs) - { - yield return item.dir; - } - } - } - - private static IEnumerable EnumerateDotnetRootCandidates() - { - var roots = new HashSet(StringComparer.OrdinalIgnoreCase); - - void AddIfExists(string pathCandidate) - { - if (string.IsNullOrWhiteSpace(pathCandidate)) - { - return; - } - - try - { - var fullPath = Path.GetFullPath(pathCandidate.Trim()); - if (Directory.Exists(fullPath)) - { - roots.Add(fullPath); - } - } - catch - { - // ignore invalid paths - } - } - - AddIfExists(Environment.GetEnvironmentVariable("DOTNET_ROOT")); - AddIfExists(Environment.GetEnvironmentVariable("DOTNET_ROOT(x86)")); - - try - { - var processPath = Environment.ProcessPath; - if (!string.IsNullOrEmpty(processPath)) - { - var dir = Path.GetDirectoryName(processPath); - AddIfExists(dir); - } - } - catch - { - // ignore access errors - } - - try - { - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - if (!string.IsNullOrEmpty(programFiles)) - { - AddIfExists(Path.Combine(programFiles, "dotnet")); - } - } - catch - { - // ignore folder resolution errors - } - - try - { - var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); - if (!string.IsNullOrEmpty(programFilesX86)) - { - AddIfExists(Path.Combine(programFilesX86, "dotnet")); - } - } - catch - { - // ignore folder resolution errors - } - - return roots; - } - - private static IEnumerable EnumerateTfmDirectories(string refRoot) - { - if (!Directory.Exists(refRoot)) - { - yield break; - } - - var runtimeVersion = Environment.Version; - - var candidates = Directory.GetDirectories(refRoot) - .Select(dir => - { - var name = Path.GetFileName(dir); - var version = ParseTfmVersion(name); - var isWindows = name.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0; - var scoreMajor = version.Major == runtimeVersion.Major ? 1 : 0; - var scoreMinor = version.Minor == runtimeVersion.Minor ? 1 : 0; - return new - { - Dir = dir, - Name = name, - Version = version, - IsWindows = isWindows, - ScoreMajor = scoreMajor, - ScoreMinor = scoreMinor - }; - }) - .OrderByDescending(c => c.IsWindows) - .ThenByDescending(c => c.ScoreMajor) - .ThenByDescending(c => c.ScoreMinor) - .ThenByDescending(c => c.Version) - .ThenByDescending(c => c.Name, StringComparer.OrdinalIgnoreCase); - - foreach (var candidate in candidates) - { - yield return candidate.Dir; - } - } - - private static Version ParseTfmVersion(string tfm) - { - if (string.IsNullOrWhiteSpace(tfm) || !tfm.StartsWith("net", StringComparison.OrdinalIgnoreCase)) - { - return new Version(0, 0); - } - - var span = tfm.AsSpan(3); - var dashIndex = span.IndexOf('-'); - if (dashIndex >= 0) - { - span = span[..dashIndex]; - } - - return Version.TryParse(span.ToString(), out var version) ? version : new Version(0, 0); - } - - private static Version TryParseVersionFromDirectory(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - return new Version(0, 0); - } - - return Version.TryParse(name, out var version) ? version : new Version(0, 0); - } - - private static string BuildProjectFileContent(string assemblyName, string nuget, int languageVersion, string projectType) - { - var normalizedType = NormalizeProjectType(projectType); - var sdk = "Microsoft.NET.Sdk"; - var targetFramework = GetHostTargetFramework(requireWindows: false); - var outputTypeValue = "Exe"; - var propertyExtraLines = new List(); - var frameworkReferences = new List(); - - switch (normalizedType) - { - case "winforms": - targetFramework = GetHostTargetFramework(requireWindows: true); - outputTypeValue = "WinExe"; - propertyExtraLines.Add(" true"); - propertyExtraLines.Add(" true"); - frameworkReferences.Add(" "); - break; - case "webapi": - sdk = "Microsoft.NET.Sdk.Web"; - break; - } - - var langVer = "latest"; - try - { - langVer = ((LanguageVersion)languageVersion).ToString(); - } - catch - { - // keep default when conversion fails - } - - var builder = new StringBuilder(); - builder.AppendLine($""); - builder.AppendLine(" "); - builder.AppendLine($" {outputTypeValue}"); - builder.AppendLine($" {targetFramework}"); - builder.AppendLine(" enable"); - builder.AppendLine(" enable"); - builder.AppendLine($" {assemblyName}"); - builder.AppendLine($" {assemblyName}"); - builder.AppendLine($" {langVer}"); - foreach (var line in propertyExtraLines) - { - builder.AppendLine(line); - } - builder.AppendLine(" "); - - var packages = BuildPackageReferenceLines(nuget, normalizedType); - if (packages.Count > 0) - { - builder.AppendLine(" "); - foreach (var line in packages) - { - builder.AppendLine(line); - } - builder.AppendLine(" "); - } - - if (frameworkReferences.Count > 0) - { - builder.AppendLine(" "); - foreach (var line in frameworkReferences) - { - builder.AppendLine(line); - } - builder.AppendLine(" "); - } - - builder.AppendLine(""); - return builder.ToString(); - } - - private static string DeriveAssemblyNameForRun(List files) - { - var candidate = files?.FirstOrDefault(f => f?.IsEntry == true)?.FileName - ?? files?.FirstOrDefault()?.FileName - ?? "Program.cs"; - - var baseName = Path.GetFileNameWithoutExtension(candidate); - if (string.IsNullOrWhiteSpace(baseName)) - { - baseName = "SharpPadProgram"; - } - - var sanitized = new string(baseName.Select(ch => char.IsLetterOrDigit(ch) || ch == '_' ? ch : '_').ToArray()); - return string.IsNullOrWhiteSpace(sanitized) ? "SharpPadProgram" : sanitized; - } - - private static async Task WriteSourceFilesAsync(List files, string destinationDir) - { - if (files == null) - { - return; - } - - foreach (var file in files) - { - var relative = string.IsNullOrWhiteSpace(file?.FileName) ? "Program.cs" : file.FileName; - relative = relative.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); - var segments = relative.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); - if (segments.Length == 0) - { - segments = new[] { "Program.cs" }; - } - - for (var i = 0; i < segments.Length; i++) - { - var segment = segments[i]; - foreach (var invalid in Path.GetInvalidFileNameChars()) - { - segment = segment.Replace(invalid, '_'); - } - segments[i] = segment; - } - - var safeRelativePath = Path.Combine(segments); - var destinationPath = Path.Combine(destinationDir, safeRelativePath); - var directory = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(directory)) - { - Directory.CreateDirectory(directory); - } - - await File.WriteAllTextAsync(destinationPath, file?.Content ?? string.Empty, Encoding.UTF8).ConfigureAwait(false); - } - } - - private static async Task<(int ExitCode, string StdOut, string StdErr)> RunProcessCaptureAsync(string fileName, string arguments, string workingDirectory, IDictionary environment = null) - { - var psi = new ProcessStartInfo(fileName, arguments) - { - WorkingDirectory = workingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 - }; - - if (environment != null) - { - foreach (var kv in environment) - { - psi.Environment[kv.Key] = kv.Value; - } - } - - using var process = new Process { StartInfo = psi }; - var stdOut = new StringBuilder(); - var stdErr = new StringBuilder(); - process.OutputDataReceived += (_, e) => { if (e.Data != null) stdOut.AppendLine(e.Data); }; - process.ErrorDataReceived += (_, e) => { if (e.Data != null) stdErr.AppendLine(e.Data); }; - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - await process.WaitForExitAsync().ConfigureAwait(false); - - return (process.ExitCode, stdOut.ToString(), stdErr.ToString()); - } - - public static void DownloadPackage(string nuget, string preferredSourceKey = null) - { - DownloadNugetPackages.DownloadAllPackagesAsync(nuget, preferredSourceKey).GetAwaiter().GetResult(); - } - + /// /// Removes NuGet packages from local cache /// @@ -1869,133 +1843,133 @@ public static void RemovePackages(string nuget) CompletionWorkspace.ClearReferenceCache(); DownloadNugetPackages.RemoveAllPackages(nuget); } - - public static async Task BuildMultiFileExecutableAsync( - List files, - string nuget, - int languageVersion, - string outputFileName, - string projectType, - Func onOutput = null) - { - return await BuildWithDotnetPublishAsync(files, nuget, languageVersion, outputFileName, projectType, onOutput); - } - - public static async Task BuildExecutableAsync( - string code, - string nuget, - int languageVersion, - string outputFileName, - string projectType, - Func onOutput = null) - { - var files = new List { new FileContent { FileName = "Program.cs", Content = code } }; - return await BuildWithDotnetPublishAsync(files, nuget, languageVersion, outputFileName, projectType, onOutput); - } - - private static async Task BuildWithDotnetPublishAsync( - List files, - string nuget, - int languageVersion, - string outputFileName, - string projectType, - Func onOutput = null) - { - var result = new ExeBuildResult(); - - try - { - var workingRoot = Path.Combine(Path.GetTempPath(), "SharpPadBuilds", Guid.NewGuid().ToString("N")); - var srcDir = Path.Combine(workingRoot, "src"); - var publishDir = Path.Combine(workingRoot, "publish"); - Directory.CreateDirectory(srcDir); - Directory.CreateDirectory(publishDir); - - var outName = string.IsNullOrWhiteSpace(outputFileName) ? "Program.exe" : outputFileName; - var asmName = Path.GetFileNameWithoutExtension(outName); - var artifactFileName = Path.ChangeExtension(outName, ".zip"); - - var normalizedProjectType = NormalizeProjectType(projectType); - var sdk = "Microsoft.NET.Sdk"; - var targetFramework = GetHostTargetFramework(requireWindows: false); - var outputTypeValue = "Exe"; - var propertyExtraLines = new List(); - var frameworkRefLines = new List(); - switch (normalizedProjectType) - { - case "winform": - case "winforms": - case "windowsforms": - targetFramework = GetHostTargetFramework(requireWindows: true); - outputTypeValue = "WinExe"; - propertyExtraLines.Add(" true"); - propertyExtraLines.Add(" true"); - frameworkRefLines.Add(" "); - break; - case "aspnetcore": - case "aspnetcorewebapi": - case "webapi": - case "web": - sdk = "Microsoft.NET.Sdk.Web"; - break; - default: - break; - } - - var csprojPath = Path.Combine(srcDir, $"{asmName}.csproj"); + + public static async Task BuildMultiFileExecutableAsync( + List files, + string nuget, + int languageVersion, + string outputFileName, + string projectType, + Func onOutput = null) + { + return await BuildWithDotnetPublishAsync(files, nuget, languageVersion, outputFileName, projectType, onOutput); + } + + public static async Task BuildExecutableAsync( + string code, + string nuget, + int languageVersion, + string outputFileName, + string projectType, + Func onOutput = null) + { + var files = new List { new FileContent { FileName = "Program.cs", Content = code } }; + return await BuildWithDotnetPublishAsync(files, nuget, languageVersion, outputFileName, projectType, onOutput); + } + + private static async Task BuildWithDotnetPublishAsync( + List files, + string nuget, + int languageVersion, + string outputFileName, + string projectType, + Func onOutput = null) + { + var result = new ExeBuildResult(); + + try + { + var workingRoot = Path.Combine(Path.GetTempPath(), "SharpPadBuilds", Guid.NewGuid().ToString("N")); + var srcDir = Path.Combine(workingRoot, "src"); + var publishDir = Path.Combine(workingRoot, "publish"); + Directory.CreateDirectory(srcDir); + Directory.CreateDirectory(publishDir); + + var outName = string.IsNullOrWhiteSpace(outputFileName) ? "Program.exe" : outputFileName; + var asmName = Path.GetFileNameWithoutExtension(outName); + var artifactFileName = Path.ChangeExtension(outName, ".zip"); + + var normalizedProjectType = NormalizeProjectType(projectType); + var sdk = "Microsoft.NET.Sdk"; + var targetFramework = GetHostTargetFramework(requireWindows: false); + var outputTypeValue = "Exe"; + var propertyExtraLines = new List(); + var frameworkRefLines = new List(); + switch (normalizedProjectType) + { + case "winform": + case "winforms": + case "windowsforms": + targetFramework = GetHostTargetFramework(requireWindows: true); + outputTypeValue = "WinExe"; + propertyExtraLines.Add(" true"); + propertyExtraLines.Add(" true"); + frameworkRefLines.Add(" "); + break; + case "aspnetcore": + case "aspnetcorewebapi": + case "webapi": + case "web": + sdk = "Microsoft.NET.Sdk.Web"; + break; + default: + break; + } + + var csprojPath = Path.Combine(srcDir, $"{asmName}.csproj"); var packageReferences = BuildPackageReferenceMap(nuget, normalizedProjectType); EnsureRuntimePackageReferences(packageReferences); var langVer = "latest"; try { langVer = ((LanguageVersion)languageVersion).ToString(); } catch { } - - var projectBuilder = new StringBuilder(); - projectBuilder.AppendLine($""); - projectBuilder.AppendLine(" "); - projectBuilder.AppendLine($" {outputTypeValue}"); - projectBuilder.AppendLine($" {targetFramework}"); - projectBuilder.AppendLine(" enable"); - projectBuilder.AppendLine(" enable"); - projectBuilder.AppendLine($" {asmName}"); - projectBuilder.AppendLine($" {asmName}"); - projectBuilder.AppendLine($" {langVer}"); - foreach (var line in propertyExtraLines) - { - projectBuilder.AppendLine(line); - } - projectBuilder.AppendLine(" "); - - if (packageReferences.Count > 0) - { - projectBuilder.AppendLine(" "); - foreach (var reference in packageReferences.OrderBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(reference.Value)) - { - projectBuilder.AppendLine($" "); - } - else - { - projectBuilder.AppendLine($" "); - } - } - projectBuilder.AppendLine(" "); - } - - if (frameworkRefLines.Count > 0) - { - projectBuilder.AppendLine(" "); - foreach (var line in frameworkRefLines) - { - projectBuilder.AppendLine(line); - } - projectBuilder.AppendLine(" "); - } - - projectBuilder.AppendLine(""); - var csproj = projectBuilder.ToString(); - await File.WriteAllTextAsync(csprojPath, csproj, Encoding.UTF8); + + var projectBuilder = new StringBuilder(); + projectBuilder.AppendLine($""); + projectBuilder.AppendLine(" "); + projectBuilder.AppendLine($" {outputTypeValue}"); + projectBuilder.AppendLine($" {targetFramework}"); + projectBuilder.AppendLine(" enable"); + projectBuilder.AppendLine(" enable"); + projectBuilder.AppendLine($" {asmName}"); + projectBuilder.AppendLine($" {asmName}"); + projectBuilder.AppendLine($" {langVer}"); + foreach (var line in propertyExtraLines) + { + projectBuilder.AppendLine(line); + } + projectBuilder.AppendLine(" "); + + if (packageReferences.Count > 0) + { + projectBuilder.AppendLine(" "); + foreach (var reference in packageReferences.OrderBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(reference.Value)) + { + projectBuilder.AppendLine($" "); + } + else + { + projectBuilder.AppendLine($" "); + } + } + projectBuilder.AppendLine(" "); + } + + if (frameworkRefLines.Count > 0) + { + projectBuilder.AppendLine(" "); + foreach (var line in frameworkRefLines) + { + projectBuilder.AppendLine(line); + } + projectBuilder.AppendLine(" "); + } + + projectBuilder.AppendLine(""); + var csproj = projectBuilder.ToString(); + await File.WriteAllTextAsync(csprojPath, csproj, Encoding.UTF8); foreach (var f in files) { @@ -2011,186 +1985,186 @@ private static async Task BuildWithDotnetPublishAsync( async Task<(int code, string stdout, string stderr)> RunAsync(string fileName, string args, string workingDir) { - var psi = new ProcessStartInfo(fileName, args) - { - WorkingDirectory = workingDir, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 - }; - var p = new Process { StartInfo = psi }; - var sbOut = new StringBuilder(); - var sbErr = new StringBuilder(); - p.OutputDataReceived += (_, e) => - { - if (e.Data != null) - { - sbOut.AppendLine(e.Data); - onOutput?.Invoke(e.Data + Environment.NewLine).GetAwaiter().GetResult(); - } - }; - p.ErrorDataReceived += (_, e) => - { - if (e.Data != null) - { - sbErr.AppendLine(e.Data); - onOutput?.Invoke(e.Data + Environment.NewLine).GetAwaiter().GetResult(); - } - }; - p.Start(); - p.BeginOutputReadLine(); - p.BeginErrorReadLine(); - await p.WaitForExitAsync(); - return (p.ExitCode, sbOut.ToString(), sbErr.ToString()); - } - - if (onOutput != null) - { - await onOutput($"开始还原 NuGet 包...\n"); - } - var (rc1, o1, e1) = await RunAsync("dotnet", "restore", srcDir); - if (rc1 != 0) - { - var msg = $"dotnet restore failed.\n{o1}\n{e1}"; - result.Success = false; - result.Error = msg; - return result; - } - - if (onOutput != null) - { - await onOutput($"开始发布应用...\n"); - } + var psi = new ProcessStartInfo(fileName, args) + { + WorkingDirectory = workingDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + var p = new Process { StartInfo = psi }; + var sbOut = new StringBuilder(); + var sbErr = new StringBuilder(); + p.OutputDataReceived += (_, e) => + { + if (e.Data != null) + { + sbOut.AppendLine(e.Data); + onOutput?.Invoke(e.Data + Environment.NewLine).GetAwaiter().GetResult(); + } + }; + p.ErrorDataReceived += (_, e) => + { + if (e.Data != null) + { + sbErr.AppendLine(e.Data); + onOutput?.Invoke(e.Data + Environment.NewLine).GetAwaiter().GetResult(); + } + }; + p.Start(); + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + await p.WaitForExitAsync(); + return (p.ExitCode, sbOut.ToString(), sbErr.ToString()); + } + + if (onOutput != null) + { + await onOutput($"开始还原 NuGet 包...\n"); + } + var (rc1, o1, e1) = await RunAsync("dotnet", "restore", srcDir); + if (rc1 != 0) + { + var msg = $"dotnet restore failed.\n{o1}\n{e1}"; + result.Success = false; + result.Error = msg; + return result; + } + + if (onOutput != null) + { + await onOutput($"开始发布应用...\n"); + } var rid = GetRuntimeIdentifier(); var publishArgs = $"publish -c Release -r {rid} --self-contained true -o \"{publishDir}\""; var (rc2, o2, e2) = await RunAsync("dotnet", publishArgs, srcDir); - if (rc2 != 0) - { - var msg = $"dotnet publish failed.\n{o2}\n{e2}"; - result.Success = false; - result.Error = msg; - return result; - } - - if (onOutput != null) - { - await onOutput($"创建发布包...\n"); - } - - // Find the actual executable file - string exePath; - if (OperatingSystem.IsWindows()) - { - var defaultExe = Path.Combine(publishDir, asmName + ".exe"); - var requestedExe = Path.Combine(publishDir, outName); - if (File.Exists(defaultExe) && !defaultExe.Equals(requestedExe, StringComparison.OrdinalIgnoreCase)) - { - if (File.Exists(requestedExe)) File.Delete(requestedExe); - File.Move(defaultExe, requestedExe); - exePath = requestedExe; - } - else - { - exePath = File.Exists(requestedExe) ? requestedExe : defaultExe; - } - } - else - { - // On Linux/macOS, executable doesn't have .exe extension - exePath = Path.Combine(publishDir, asmName); - if (!File.Exists(exePath)) - { - // Look for any executable file in the publish directory - var executableFiles = Directory.GetFiles(publishDir).Where(f => - { - var info = new FileInfo(f); - return info.Name == asmName || info.Name.StartsWith(asmName); - }); - exePath = executableFiles.FirstOrDefault() ?? exePath; - } - } - - if (!File.Exists(exePath)) - { - result.Success = false; - result.Error = $"Executable file not found at: {exePath}"; - return result; - } - - var artifactPath = Path.Combine(workingRoot, artifactFileName); - if (File.Exists(artifactPath)) - { - File.Delete(artifactPath); - } - - ZipFile.CreateFromDirectory(publishDir, artifactPath, CompressionLevel.Optimal, includeBaseDirectory: false); - - result.Success = true; - result.ExeFilePath = artifactPath; - result.FileSizeBytes = new FileInfo(artifactPath).Length; - result.CompilationMessages.Add($"Built package: {Path.GetFileName(artifactPath)}"); - } - catch (Exception ex) - { - result.Success = false; - result.Error = $"Build error: {ex.Message}"; - } - - return result; - } - - } - - // 立即回调的 TextWriter,用于进程内执行 - internal sealed class ImmediateCallbackTextWriter : TextWriter - { - private readonly Func _onWrite; - - public ImmediateCallbackTextWriter(Func onWrite) - { - _onWrite = onWrite ?? throw new ArgumentNullException(nameof(onWrite)); - } - - public override Encoding Encoding => Encoding.UTF8; - - public override void Write(char value) => _onWrite(value.ToString()).GetAwaiter().GetResult(); - - public override void Write(string value) => _onWrite(value ?? string.Empty).GetAwaiter().GetResult(); - - public override void WriteLine(string value) => _onWrite((value ?? string.Empty) + Environment.NewLine).GetAwaiter().GetResult(); - - public override ValueTask DisposeAsync() => ValueTask.CompletedTask; - } - - // 自定义可卸载的AssemblyLoadContext - internal sealed class CustomAssemblyLoadContext : AssemblyLoadContext - { - private readonly Dictionary _packageAssemblies; - - public CustomAssemblyLoadContext(IEnumerable packageAssemblies) : base(isCollectible: true) - { - _packageAssemblies = packageAssemblies? - .GroupBy(p => p.AssemblyName.Name ?? Path.GetFileNameWithoutExtension(p.Path), StringComparer.OrdinalIgnoreCase) - .ToDictionary(g => g.Key, g => g.OrderByDescending(p => p.AssemblyName.Version ?? new Version(0, 0, 0, 0)).First(), StringComparer.OrdinalIgnoreCase) - ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - protected override Assembly Load(AssemblyName assemblyName) - { - if (assemblyName?.Name != null && _packageAssemblies.TryGetValue(assemblyName.Name, out var package)) - { - return LoadFromAssemblyPath(package.Path); - } - - return null; - } - } -} - - - - - + if (rc2 != 0) + { + var msg = $"dotnet publish failed.\n{o2}\n{e2}"; + result.Success = false; + result.Error = msg; + return result; + } + + if (onOutput != null) + { + await onOutput($"创建发布包...\n"); + } + + // Find the actual executable file + string exePath; + if (OperatingSystem.IsWindows()) + { + var defaultExe = Path.Combine(publishDir, asmName + ".exe"); + var requestedExe = Path.Combine(publishDir, outName); + if (File.Exists(defaultExe) && !defaultExe.Equals(requestedExe, StringComparison.OrdinalIgnoreCase)) + { + if (File.Exists(requestedExe)) File.Delete(requestedExe); + File.Move(defaultExe, requestedExe); + exePath = requestedExe; + } + else + { + exePath = File.Exists(requestedExe) ? requestedExe : defaultExe; + } + } + else + { + // On Linux/macOS, executable doesn't have .exe extension + exePath = Path.Combine(publishDir, asmName); + if (!File.Exists(exePath)) + { + // Look for any executable file in the publish directory + var executableFiles = Directory.GetFiles(publishDir).Where(f => + { + var info = new FileInfo(f); + return info.Name == asmName || info.Name.StartsWith(asmName); + }); + exePath = executableFiles.FirstOrDefault() ?? exePath; + } + } + + if (!File.Exists(exePath)) + { + result.Success = false; + result.Error = $"Executable file not found at: {exePath}"; + return result; + } + + var artifactPath = Path.Combine(workingRoot, artifactFileName); + if (File.Exists(artifactPath)) + { + File.Delete(artifactPath); + } + + ZipFile.CreateFromDirectory(publishDir, artifactPath, CompressionLevel.Optimal, includeBaseDirectory: false); + + result.Success = true; + result.ExeFilePath = artifactPath; + result.FileSizeBytes = new FileInfo(artifactPath).Length; + result.CompilationMessages.Add($"Built package: {Path.GetFileName(artifactPath)}"); + } + catch (Exception ex) + { + result.Success = false; + result.Error = $"Build error: {ex.Message}"; + } + + return result; + } + + } + + // 立即回调的 TextWriter,用于进程内执行 + internal sealed class ImmediateCallbackTextWriter : TextWriter + { + private readonly Func _onWrite; + + public ImmediateCallbackTextWriter(Func onWrite) + { + _onWrite = onWrite ?? throw new ArgumentNullException(nameof(onWrite)); + } + + public override Encoding Encoding => Encoding.UTF8; + + public override void Write(char value) => _onWrite(value.ToString()).GetAwaiter().GetResult(); + + public override void Write(string value) => _onWrite(value ?? string.Empty).GetAwaiter().GetResult(); + + public override void WriteLine(string value) => _onWrite((value ?? string.Empty) + Environment.NewLine).GetAwaiter().GetResult(); + + public override ValueTask DisposeAsync() => ValueTask.CompletedTask; + } + + // 自定义可卸载的AssemblyLoadContext + internal sealed class CustomAssemblyLoadContext : AssemblyLoadContext + { + private readonly Dictionary _packageAssemblies; + + public CustomAssemblyLoadContext(IEnumerable packageAssemblies) : base(isCollectible: true) + { + _packageAssemblies = packageAssemblies? + .GroupBy(p => p.AssemblyName.Name ?? Path.GetFileNameWithoutExtension(p.Path), StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.OrderByDescending(p => p.AssemblyName.Version ?? new Version(0, 0, 0, 0)).First(), StringComparer.OrdinalIgnoreCase) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + if (assemblyName?.Name != null && _packageAssemblies.TryGetValue(assemblyName.Name, out var package)) + { + return LoadFromAssemblyPath(package.Path); + } + + return null; + } + } +} + + + + + diff --git a/MonacoRoslynCompletionProvider/CompletionWorkspace.cs b/MonacoRoslynCompletionProvider/CompletionWorkspace.cs index ad64e78..1ae35f0 100644 --- a/MonacoRoslynCompletionProvider/CompletionWorkspace.cs +++ b/MonacoRoslynCompletionProvider/CompletionWorkspace.cs @@ -67,7 +67,7 @@ private static string[] BuildDefaultAssemblyPaths() TryAddType(typeof(System.Security.Cryptography.HashAlgorithm)); TryAddType(typeof(System.Net.Mail.MailAddress)); TryAddType(typeof(Microsoft.Net.Http.Headers.MediaTypeHeaderValue)); - TryAddType(typeof(ObjectExtengsion)); + TryAddType(typeof(ObjectExtension)); TryAddType(typeof(System.Diagnostics.Process)); TryAddType(typeof(ParallelEnumerable)); TryAddType(typeof(Uri)); @@ -488,32 +488,32 @@ public void Dispose() } } - private static void DisposeReferenceCacheEntries() - { - foreach (var reference in ReferenceCache.Values) - { - if (reference is IDisposable disposable) - { - try - { - disposable.Dispose(); - } - catch - { - // Ignore disposal issues; cache will be rebuilt as needed. - } - } - } - } - - /// - /// 清理 MetadataReference 缓存,用于重新加载包含文档的引用 - /// - public static void ClearReferenceCache() - { - DisposeReferenceCacheEntries(); - ReferenceCache.Clear(); - } + private static void DisposeReferenceCacheEntries() + { + foreach (var reference in ReferenceCache.Values) + { + if (reference is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch + { + // Ignore disposal issues; cache will be rebuilt as needed. + } + } + } + } + + /// + /// 清理 MetadataReference 缓存,用于重新加载包含文档的引用 + /// + public static void ClearReferenceCache() + { + DisposeReferenceCacheEntries(); + ReferenceCache.Clear(); + } /// /// 测试方法:检查特定程序集的 XML 文档是否可以加载 @@ -572,11 +572,11 @@ public static string TestXmlDocumentationLoading(string assemblyName = "System.C /// public static void Shutdown() { - while (WorkspacePool.TryTake(out var workspace)) - { - workspace.Dispose(); - } - ClearReferenceCache(); - } + while (WorkspacePool.TryTake(out var workspace)) + { + workspace.Dispose(); + } + ClearReferenceCache(); + } } } diff --git a/MonacoRoslynCompletionProvider/Extensions/ObjectExtengsion.cs b/MonacoRoslynCompletionProvider/Extensions/ObjectExtension.cs similarity index 97% rename from MonacoRoslynCompletionProvider/Extensions/ObjectExtengsion.cs rename to MonacoRoslynCompletionProvider/Extensions/ObjectExtension.cs index 1fc706b..82f5697 100644 --- a/MonacoRoslynCompletionProvider/Extensions/ObjectExtengsion.cs +++ b/MonacoRoslynCompletionProvider/Extensions/ObjectExtension.cs @@ -9,7 +9,7 @@ namespace System { - public static class ObjectExtengsion + public static class ObjectExtension { public static string ToJson(this Object value, bool format = false) { diff --git a/MonacoRoslynCompletionProvider/MonacoRoslynCompletionProvider.csproj b/MonacoRoslynCompletionProvider/MonacoRoslynCompletionProvider.csproj index aca4773..f38b40e 100644 --- a/MonacoRoslynCompletionProvider/MonacoRoslynCompletionProvider.csproj +++ b/MonacoRoslynCompletionProvider/MonacoRoslynCompletionProvider.csproj @@ -56,4 +56,8 @@ PreserveNewest + + + +