947 lines
35 KiB
C#
947 lines
35 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UnityEditor;
|
|
using UnityEditor.Compilation;
|
|
using Debug = UnityEngine.Debug;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
internal static class SharedRoslynCompilerWorkerHost
|
|
{
|
|
private const int SharedCompilerWorkerMaxAttempts = 2;
|
|
private const string SharedCompilerWorkerResultPrefix = "__ULOOP_RESULT__";
|
|
private const string SharedCompilerWorkerEndMarker = "__ULOOP_END__";
|
|
private const string SharedCompilerWorkerQuitCommand = "__QUIT__";
|
|
internal const string CompileRequestPathPrefix = "path-base64:";
|
|
private const string RoslynWorkerSourceFileName = "RoslynCompilerWorker.cs";
|
|
private const string RoslynWorkerAssemblyFileName = "RoslynCompilerWorker.dll";
|
|
private const string RoslynWorkerCompileResponseFileName = "RoslynCompilerWorker.rsp";
|
|
private const string RoslynWorkerProgramTemplateRelativePath =
|
|
"Editor/Compilation/Templates/SharedRoslynCompilerWorkerProgram.cs.template";
|
|
private const string SharedCompilerWorkerResultPrefixToken = "{{SHARED_COMPILER_WORKER_RESULT_PREFIX}}";
|
|
private const string SharedCompilerWorkerEndMarkerToken = "{{SHARED_COMPILER_WORKER_END_MARKER}}";
|
|
private const string SharedCompilerWorkerQuitCommandToken = "{{SHARED_COMPILER_WORKER_QUIT_COMMAND}}";
|
|
private const string CompileRequestPathPrefixToken = "{{COMPILE_REQUEST_PATH_PREFIX}}";
|
|
private const int SharedCompilerWorkerResponseTimeoutMilliseconds = 30000;
|
|
private const int WorkerAssemblyBuildTimeoutMilliseconds = 30000;
|
|
internal const string DotnetMultilevelLookupEnvironmentVariableName = "DOTNET_MULTILEVEL_LOOKUP";
|
|
internal const string DotnetMultilevelLookupDisabledValue = "0";
|
|
|
|
private static readonly object SharedCompilerWorkerLock = new();
|
|
private static Action<string> s_deleteWorkerDirectory = path => Directory.Delete(path, true);
|
|
private static Action<Process, string> s_sendCompileRequest = SendCompileRequestCore;
|
|
private static Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]>
|
|
s_compileWorkerAssemblyForTests;
|
|
private static Process _sharedCompilerWorkerProcess;
|
|
private static string _workerDirectoryPath;
|
|
|
|
private sealed class WorkerAttemptResult
|
|
{
|
|
public CompilerMessage[] Messages { get; }
|
|
|
|
public bool ShouldRetry { get; }
|
|
|
|
public string FailureReason { get; }
|
|
|
|
public object FailureContext { get; }
|
|
|
|
private WorkerAttemptResult(
|
|
CompilerMessage[] messages,
|
|
bool shouldRetry,
|
|
string failureReason,
|
|
object failureContext)
|
|
{
|
|
Messages = messages;
|
|
ShouldRetry = shouldRetry;
|
|
FailureReason = failureReason;
|
|
FailureContext = failureContext;
|
|
}
|
|
|
|
public bool Succeeded => Messages != null;
|
|
|
|
public static WorkerAttemptResult Successful(CompilerMessage[] messages)
|
|
{
|
|
return new WorkerAttemptResult(messages, false, null, null);
|
|
}
|
|
|
|
public static WorkerAttemptResult RetryableFailure(string failureReason, object failureContext)
|
|
{
|
|
return new WorkerAttemptResult(null, true, failureReason, failureContext);
|
|
}
|
|
}
|
|
|
|
private sealed class WorkerStartupResult
|
|
{
|
|
public bool IsReady { get; }
|
|
|
|
public string FailureReason { get; }
|
|
|
|
public object FailureContext { get; }
|
|
|
|
private WorkerStartupResult(bool isReady, string failureReason, object failureContext)
|
|
{
|
|
IsReady = isReady;
|
|
FailureReason = failureReason;
|
|
FailureContext = failureContext;
|
|
}
|
|
|
|
public static WorkerStartupResult Ready()
|
|
{
|
|
return new WorkerStartupResult(true, null, null);
|
|
}
|
|
|
|
public static WorkerStartupResult Failure(string failureReason, object failureContext)
|
|
{
|
|
return new WorkerStartupResult(false, failureReason, failureContext);
|
|
}
|
|
}
|
|
|
|
private sealed class WorkerAssemblyBuildResult
|
|
{
|
|
public bool StartedSuccessfully { get; }
|
|
|
|
public CompilerMessage[] Messages { get; }
|
|
|
|
public string FailureReason { get; }
|
|
|
|
public object FailureContext { get; }
|
|
|
|
private WorkerAssemblyBuildResult(
|
|
bool startedSuccessfully,
|
|
CompilerMessage[] messages,
|
|
string failureReason,
|
|
object failureContext)
|
|
{
|
|
StartedSuccessfully = startedSuccessfully;
|
|
Messages = messages;
|
|
FailureReason = failureReason;
|
|
FailureContext = failureContext;
|
|
}
|
|
|
|
public static WorkerAssemblyBuildResult Started(CompilerMessage[] messages)
|
|
{
|
|
return new WorkerAssemblyBuildResult(true, messages, null, null);
|
|
}
|
|
|
|
public static WorkerAssemblyBuildResult StartFailure(string failureReason, object failureContext)
|
|
{
|
|
return new WorkerAssemblyBuildResult(false, null, failureReason, failureContext);
|
|
}
|
|
}
|
|
|
|
private sealed class WorkerPaths
|
|
{
|
|
public string DirectoryPath { get; }
|
|
|
|
public string SourcePath { get; }
|
|
|
|
public string AssemblyPath { get; }
|
|
|
|
public string CompileResponseFilePath { get; }
|
|
|
|
public WorkerPaths(
|
|
string directoryPath,
|
|
string sourcePath,
|
|
string assemblyPath,
|
|
string compileResponseFilePath)
|
|
{
|
|
DirectoryPath = directoryPath;
|
|
SourcePath = sourcePath;
|
|
AssemblyPath = assemblyPath;
|
|
CompileResponseFilePath = compileResponseFilePath;
|
|
}
|
|
}
|
|
|
|
[InitializeOnLoadMethod]
|
|
private static void RegisterLifecycle()
|
|
{
|
|
AssemblyReloadEvents.beforeAssemblyReload -= ShutdownForReload;
|
|
AssemblyReloadEvents.beforeAssemblyReload += ShutdownForReload;
|
|
EditorApplication.quitting -= ShutdownForQuit;
|
|
EditorApplication.quitting += ShutdownForQuit;
|
|
}
|
|
|
|
public static CompilerMessage[] TryCompile(
|
|
string requestFilePath,
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
CancellationToken ct,
|
|
Action markBuildStarted,
|
|
Action markBuildFinished,
|
|
Action incrementBuildCount)
|
|
{
|
|
lock (SharedCompilerWorkerLock)
|
|
{
|
|
return TryCompileWithRetries(
|
|
requestFilePath,
|
|
externalCompilerPaths,
|
|
ct,
|
|
markBuildStarted,
|
|
markBuildFinished,
|
|
incrementBuildCount);
|
|
}
|
|
}
|
|
|
|
private static CompilerMessage[] TryCompileWithRetries(
|
|
string requestFilePath,
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
CancellationToken ct,
|
|
Action markBuildStarted,
|
|
Action markBuildFinished,
|
|
Action incrementBuildCount)
|
|
{
|
|
for (int attempt = 1; attempt <= SharedCompilerWorkerMaxAttempts; attempt++)
|
|
{
|
|
WorkerAttemptResult attemptResult = TryCompileOnce(
|
|
requestFilePath,
|
|
externalCompilerPaths,
|
|
ct,
|
|
markBuildStarted,
|
|
markBuildFinished,
|
|
incrementBuildCount);
|
|
|
|
if (attemptResult.Succeeded)
|
|
{
|
|
return attemptResult.Messages;
|
|
}
|
|
|
|
ShutdownWorkerProcessLocked();
|
|
|
|
if (attemptResult.ShouldRetry && attempt < SharedCompilerWorkerMaxAttempts)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DynamicCompilationHealthMonitor.ReportSharedWorkerFailure(
|
|
attemptResult.FailureReason,
|
|
AppendAttempt(attemptResult.FailureContext, attempt));
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static WorkerAttemptResult TryCompileOnce(
|
|
string requestFilePath,
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
CancellationToken ct,
|
|
Action markBuildStarted,
|
|
Action markBuildFinished,
|
|
Action incrementBuildCount)
|
|
{
|
|
WorkerStartupResult startupResult = EnsureWorkerReady(externalCompilerPaths);
|
|
if (!startupResult.IsReady)
|
|
{
|
|
return WorkerAttemptResult.RetryableFailure(
|
|
startupResult.FailureReason,
|
|
startupResult.FailureContext);
|
|
}
|
|
|
|
return InvokeWorkerOnce(
|
|
requestFilePath,
|
|
ct,
|
|
markBuildStarted,
|
|
markBuildFinished,
|
|
incrementBuildCount);
|
|
}
|
|
|
|
private static WorkerStartupResult EnsureWorkerReady(ExternalCompilerPaths externalCompilerPaths)
|
|
{
|
|
if (HasLiveWorkerProcess())
|
|
{
|
|
return WorkerStartupResult.Ready();
|
|
}
|
|
|
|
WorkerPaths workerPaths = CreateWorkerPaths();
|
|
SynchronizeWorkerSource(workerPaths);
|
|
|
|
WorkerStartupResult workerAssemblyResult = EnsureWorkerAssemblyBuilt(
|
|
externalCompilerPaths,
|
|
workerPaths);
|
|
if (!workerAssemblyResult.IsReady)
|
|
{
|
|
return workerAssemblyResult;
|
|
}
|
|
|
|
return StartWorkerProcess(externalCompilerPaths, workerPaths);
|
|
}
|
|
|
|
private static WorkerStartupResult EnsureWorkerAssemblyBuilt(
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
WorkerPaths workerPaths)
|
|
{
|
|
if (File.Exists(workerPaths.AssemblyPath))
|
|
{
|
|
return WorkerStartupResult.Ready();
|
|
}
|
|
|
|
WorkerAssemblyBuildResult buildResult = CompileWorkerAssembly(
|
|
externalCompilerPaths,
|
|
workerPaths.SourcePath,
|
|
workerPaths.AssemblyPath,
|
|
workerPaths.CompileResponseFilePath);
|
|
if (!buildResult.StartedSuccessfully)
|
|
{
|
|
return WorkerStartupResult.Failure(
|
|
buildResult.FailureReason,
|
|
buildResult.FailureContext);
|
|
}
|
|
|
|
if (!HasErrors(buildResult.Messages))
|
|
{
|
|
return WorkerStartupResult.Ready();
|
|
}
|
|
|
|
DeleteWorkerAssemblyIfPresent(workerPaths.AssemblyPath);
|
|
return WorkerStartupResult.Failure(
|
|
"worker_build_failed",
|
|
new
|
|
{
|
|
first_error = FindFirstErrorMessage(buildResult.Messages),
|
|
worker_source_path = workerPaths.SourcePath
|
|
});
|
|
}
|
|
|
|
private static WorkerStartupResult StartWorkerProcess(
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
WorkerPaths workerPaths)
|
|
{
|
|
ProcessStartInfo startInfo = CreateWorkerStartInfo(externalCompilerPaths, workerPaths);
|
|
_sharedCompilerWorkerProcess = ProcessStartHelper.TryStart(startInfo);
|
|
if (_sharedCompilerWorkerProcess == null)
|
|
{
|
|
return WorkerStartupResult.Failure(
|
|
"worker_start_failed",
|
|
new
|
|
{
|
|
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
|
worker_assembly_path = workerPaths.AssemblyPath
|
|
});
|
|
}
|
|
|
|
return WorkerStartupResult.Ready();
|
|
}
|
|
|
|
private static WorkerAttemptResult InvokeWorkerOnce(
|
|
string requestFilePath,
|
|
CancellationToken ct,
|
|
Action markBuildStarted,
|
|
Action markBuildFinished,
|
|
Action incrementBuildCount)
|
|
{
|
|
if (!HasLiveWorkerProcess())
|
|
{
|
|
return WorkerAttemptResult.RetryableFailure(
|
|
"worker_process_missing",
|
|
new { request_file_path = requestFilePath });
|
|
}
|
|
|
|
ct.ThrowIfCancellationRequested();
|
|
incrementBuildCount();
|
|
markBuildStarted();
|
|
|
|
try
|
|
{
|
|
SendCompileRequest(requestFilePath);
|
|
return ReadWorkerResponse(requestFilePath, ct);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
return CreateRetryableWorkerCommunicationFailure(requestFilePath, ex);
|
|
}
|
|
catch (ObjectDisposedException ex)
|
|
{
|
|
return CreateRetryableWorkerCommunicationFailure(requestFilePath, ex);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
ShutdownWorkerProcessLocked();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
markBuildFinished();
|
|
}
|
|
}
|
|
|
|
private static void SendCompileRequest(string requestFilePath)
|
|
{
|
|
string absoluteRequestFilePath = Path.GetFullPath(requestFilePath);
|
|
s_sendCompileRequest(_sharedCompilerWorkerProcess, absoluteRequestFilePath);
|
|
}
|
|
|
|
private static void SendCompileRequestCore(Process workerProcess, string requestFilePath)
|
|
{
|
|
string requestCommand = CreateCompileRequestCommand(requestFilePath);
|
|
workerProcess.StandardInput.WriteLine(requestCommand);
|
|
workerProcess.StandardInput.Flush();
|
|
}
|
|
|
|
private static string CreateCompileRequestCommand(string requestFilePath)
|
|
{
|
|
Debug.Assert(!string.IsNullOrEmpty(requestFilePath), "requestFilePath must not be empty");
|
|
|
|
string fullRequestFilePath = Path.GetFullPath(requestFilePath);
|
|
byte[] requestPathBytes = Encoding.UTF8.GetBytes(fullRequestFilePath);
|
|
return CompileRequestPathPrefix + Convert.ToBase64String(requestPathBytes);
|
|
}
|
|
|
|
private static WorkerAttemptResult ReadWorkerResponse(
|
|
string requestFilePath,
|
|
CancellationToken ct)
|
|
{
|
|
string responseHeader = ReadProtocolLine(
|
|
_sharedCompilerWorkerProcess.StandardOutput,
|
|
ct);
|
|
if (string.IsNullOrEmpty(responseHeader))
|
|
{
|
|
return WorkerAttemptResult.RetryableFailure(
|
|
"worker_empty_header",
|
|
new { request_file_path = requestFilePath });
|
|
}
|
|
|
|
if (!TryParseResponseHeader(responseHeader, out int exitCode))
|
|
{
|
|
return WorkerAttemptResult.RetryableFailure(
|
|
GetResponseHeaderFailureReason(responseHeader),
|
|
new { header = responseHeader });
|
|
}
|
|
|
|
List<string> outputLines = ReadDiagnosticLines(ct);
|
|
if (outputLines == null)
|
|
{
|
|
return WorkerAttemptResult.RetryableFailure(
|
|
"worker_missing_end_marker",
|
|
new { request_file_path = requestFilePath });
|
|
}
|
|
|
|
string combinedOutput = string.Join("\n", outputLines);
|
|
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(combinedOutput, string.Empty, exitCode);
|
|
return WorkerAttemptResult.Successful(compilerMessages);
|
|
}
|
|
|
|
private static bool TryParseResponseHeader(string responseHeader, out int exitCode)
|
|
{
|
|
exitCode = 0;
|
|
|
|
if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string statusText = responseHeader.Substring(SharedCompilerWorkerResultPrefix.Length).Trim();
|
|
return int.TryParse(statusText, out exitCode);
|
|
}
|
|
|
|
private static string GetResponseHeaderFailureReason(string responseHeader)
|
|
{
|
|
if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal))
|
|
{
|
|
return "worker_invalid_header";
|
|
}
|
|
|
|
return "worker_invalid_exit_code";
|
|
}
|
|
|
|
private static List<string> ReadDiagnosticLines(CancellationToken ct)
|
|
{
|
|
List<string> outputLines = new List<string>();
|
|
while (true)
|
|
{
|
|
string outputLine = ReadProtocolLine(_sharedCompilerWorkerProcess.StandardOutput, ct);
|
|
if (outputLine == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (outputLine == SharedCompilerWorkerEndMarker)
|
|
{
|
|
return outputLines;
|
|
}
|
|
|
|
outputLines.Add(outputLine);
|
|
}
|
|
}
|
|
|
|
private static WorkerPaths CreateWorkerPaths()
|
|
{
|
|
string workerDirectoryPath = GetWorkerDirectoryPath();
|
|
Directory.CreateDirectory(workerDirectoryPath);
|
|
_workerDirectoryPath = workerDirectoryPath;
|
|
return new WorkerPaths(
|
|
workerDirectoryPath,
|
|
Path.Combine(workerDirectoryPath, RoslynWorkerSourceFileName),
|
|
Path.Combine(workerDirectoryPath, RoslynWorkerAssemblyFileName),
|
|
Path.Combine(workerDirectoryPath, RoslynWorkerCompileResponseFileName));
|
|
}
|
|
|
|
private static string GetWorkerDirectoryPath()
|
|
{
|
|
return Path.Combine(
|
|
Path.GetTempPath(),
|
|
"uLoopMCPCompilation",
|
|
$"RoslynWorker-{Process.GetCurrentProcess().Id}");
|
|
}
|
|
|
|
private static void SynchronizeWorkerSource(WorkerPaths workerPaths)
|
|
{
|
|
string workerSource = CreateProgramSource();
|
|
if (File.Exists(workerPaths.SourcePath) && File.ReadAllText(workerPaths.SourcePath) == workerSource)
|
|
{
|
|
return;
|
|
}
|
|
|
|
File.WriteAllText(workerPaths.SourcePath, workerSource);
|
|
if (File.Exists(workerPaths.AssemblyPath))
|
|
{
|
|
File.Delete(workerPaths.AssemblyPath);
|
|
}
|
|
}
|
|
|
|
private static ProcessStartInfo CreateWorkerStartInfo(
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
WorkerPaths workerPaths)
|
|
{
|
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = externalCompilerPaths.DotnetHostPath,
|
|
Arguments = "exec"
|
|
+ " --runtimeconfig " + QuoteCommandLineArgument(externalCompilerPaths.CompilerRuntimeConfigPath)
|
|
+ " --depsfile " + QuoteCommandLineArgument(externalCompilerPaths.CompilerDepsFilePath)
|
|
+ " " + QuoteCommandLineArgument(workerPaths.AssemblyPath),
|
|
WorkingDirectory = workerPaths.DirectoryPath,
|
|
UseShellExecute = false,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
|
|
return startInfo;
|
|
}
|
|
|
|
internal static void ConfigureWorkerDotnetRuntimeEnvironment(ProcessStartInfo startInfo)
|
|
{
|
|
Debug.Assert(startInfo != null, "startInfo must not be null");
|
|
|
|
// Why: global probing can select a system .NET 6 runtime while Unity 6000.4 worker
|
|
// references come from the bundled .NET 8 runtime, which breaks assembly binding.
|
|
startInfo.EnvironmentVariables[DotnetMultilevelLookupEnvironmentVariableName] =
|
|
DotnetMultilevelLookupDisabledValue;
|
|
}
|
|
|
|
private static WorkerAssemblyBuildResult CompileWorkerAssembly(
|
|
ExternalCompilerPaths externalCompilerPaths,
|
|
string workerSourcePath,
|
|
string workerAssemblyPath,
|
|
string workerCompileResponseFilePath)
|
|
{
|
|
if (s_compileWorkerAssemblyForTests != null)
|
|
{
|
|
return WorkerAssemblyBuildResult.Started(
|
|
s_compileWorkerAssemblyForTests(
|
|
externalCompilerPaths,
|
|
workerSourcePath,
|
|
workerAssemblyPath,
|
|
workerCompileResponseFilePath));
|
|
}
|
|
|
|
WriteWorkerCompilerResponseFile(
|
|
workerCompileResponseFilePath,
|
|
workerSourcePath,
|
|
workerAssemblyPath,
|
|
BuildWorkerReferenceSet(externalCompilerPaths));
|
|
|
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = externalCompilerPaths.DotnetHostPath,
|
|
Arguments = $"{QuoteCommandLineArgument(externalCompilerPaths.CompilerDllPath)} @{QuoteCommandLineArgument(workerCompileResponseFilePath)}",
|
|
WorkingDirectory = Path.GetDirectoryName(workerSourcePath),
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true
|
|
};
|
|
ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
|
|
|
|
using Process process = ProcessStartHelper.TryStart(startInfo);
|
|
if (process == null)
|
|
{
|
|
return WorkerAssemblyBuildResult.StartFailure(
|
|
"worker_compiler_start_failed",
|
|
new
|
|
{
|
|
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
|
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
|
});
|
|
}
|
|
|
|
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
|
|
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
|
|
if (!process.WaitForExit(WorkerAssemblyBuildTimeoutMilliseconds))
|
|
{
|
|
if (!process.HasExited)
|
|
{
|
|
process.Kill();
|
|
process.WaitForExit(500);
|
|
}
|
|
|
|
Task.WaitAll(stdoutTask, stderrTask);
|
|
return WorkerAssemblyBuildResult.StartFailure(
|
|
"worker_compiler_timeout",
|
|
new
|
|
{
|
|
timeout_ms = WorkerAssemblyBuildTimeoutMilliseconds,
|
|
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
|
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
|
});
|
|
}
|
|
|
|
Task.WaitAll(stdoutTask, stderrTask);
|
|
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
|
|
stdoutTask.GetAwaiter().GetResult(),
|
|
stderrTask.GetAwaiter().GetResult(),
|
|
process.ExitCode);
|
|
return WorkerAssemblyBuildResult.Started(compilerMessages);
|
|
}
|
|
|
|
private static List<string> BuildWorkerReferenceSet(ExternalCompilerPaths externalCompilerPaths)
|
|
{
|
|
string sharedRuntimeDirectoryPath = externalCompilerPaths.NetCoreRuntimeSharedDirectoryPath;
|
|
List<string> references = new List<string>
|
|
{
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Private.CoreLib.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Console.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Collections.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.IO.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.Tasks.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Text.Encoding.Extensions.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.Extensions.dll"),
|
|
Path.Combine(sharedRuntimeDirectoryPath, "netstandard.dll"),
|
|
externalCompilerPaths.CodeAnalysisDllPath,
|
|
externalCompilerPaths.CodeAnalysisCSharpDllPath
|
|
};
|
|
|
|
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Collections.Immutable.dll"));
|
|
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Reflection.Metadata.dll"));
|
|
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.CompilerServices.Unsafe.dll"));
|
|
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Memory.dll"));
|
|
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Buffers.dll"));
|
|
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.Tasks.Extensions.dll"));
|
|
|
|
return references;
|
|
}
|
|
|
|
private static void WriteWorkerCompilerResponseFile(
|
|
string responseFilePath,
|
|
string sourcePath,
|
|
string dllPath,
|
|
IReadOnlyCollection<string> references)
|
|
{
|
|
List<string> lines = new List<string>
|
|
{
|
|
"-nologo",
|
|
"-nostdlib+",
|
|
"-target:exe",
|
|
"-optimize+",
|
|
"-debug-",
|
|
QuoteResponseFileArgument("-out:", dllPath)
|
|
};
|
|
|
|
foreach (string reference in references)
|
|
{
|
|
lines.Add(QuoteResponseFileArgument("-r:", reference));
|
|
}
|
|
|
|
lines.Add(QuoteResponseFilePath(sourcePath));
|
|
File.WriteAllLines(responseFilePath, lines);
|
|
}
|
|
|
|
private static string ReadProtocolLine(StreamReader reader, CancellationToken ct)
|
|
{
|
|
Debug.Assert(reader != null, "reader must not be null");
|
|
|
|
Task<string> readTask = Task.Run(() => reader.ReadLine());
|
|
Task timeoutTask = Task.Delay(SharedCompilerWorkerResponseTimeoutMilliseconds, ct);
|
|
Task completedTask = Task.WhenAny(readTask, timeoutTask).GetAwaiter().GetResult();
|
|
if (!ReferenceEquals(completedTask, readTask))
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
return null;
|
|
}
|
|
|
|
return readTask.GetAwaiter().GetResult();
|
|
}
|
|
|
|
private static bool HasLiveWorkerProcess()
|
|
{
|
|
return _sharedCompilerWorkerProcess != null && !_sharedCompilerWorkerProcess.HasExited;
|
|
}
|
|
|
|
private static bool HasErrors(IReadOnlyCollection<CompilerMessage> messages)
|
|
{
|
|
foreach (CompilerMessage message in messages)
|
|
{
|
|
if (message.type == CompilerMessageType.Error)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string FindFirstErrorMessage(IReadOnlyCollection<CompilerMessage> messages)
|
|
{
|
|
foreach (CompilerMessage message in messages)
|
|
{
|
|
if (message.type == CompilerMessageType.Error)
|
|
{
|
|
return message.message;
|
|
}
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private static void DeleteWorkerAssemblyIfPresent(string assemblyPath)
|
|
{
|
|
if (File.Exists(assemblyPath))
|
|
{
|
|
File.Delete(assemblyPath);
|
|
}
|
|
}
|
|
|
|
private static void ShutdownForReload()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
private static void ShutdownForQuit()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
internal static void ShutdownForServerReset()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
internal static void ShutdownForTests()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
internal static Action<string> SwapWorkerDirectoryDeleterForTests(Action<string> deleter)
|
|
{
|
|
Debug.Assert(deleter != null, "deleter must not be null");
|
|
|
|
Action<string> previous = s_deleteWorkerDirectory;
|
|
s_deleteWorkerDirectory = deleter;
|
|
return previous;
|
|
}
|
|
|
|
internal static Action<Process, string> SwapCompileRequestSenderForTests(Action<Process, string> sender)
|
|
{
|
|
Debug.Assert(sender != null, "sender must not be null");
|
|
|
|
Action<Process, string> previous = s_sendCompileRequest;
|
|
s_sendCompileRequest = sender;
|
|
return previous;
|
|
}
|
|
|
|
internal static Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]>
|
|
SwapWorkerAssemblyCompilerForTests(
|
|
Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]> compiler)
|
|
{
|
|
Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]> previous =
|
|
s_compileWorkerAssemblyForTests;
|
|
s_compileWorkerAssemblyForTests = compiler;
|
|
return previous;
|
|
}
|
|
|
|
private static void Shutdown()
|
|
{
|
|
lock (SharedCompilerWorkerLock)
|
|
{
|
|
ShutdownWorkerProcessLocked();
|
|
CleanupWorkerDirectoryLocked();
|
|
}
|
|
}
|
|
|
|
private static void ShutdownWorkerProcessLocked()
|
|
{
|
|
Process workerProcess = _sharedCompilerWorkerProcess;
|
|
_sharedCompilerWorkerProcess = null;
|
|
if (workerProcess == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!workerProcess.HasExited)
|
|
{
|
|
workerProcess.StandardInput.WriteLine(SharedCompilerWorkerQuitCommand);
|
|
workerProcess.StandardInput.Flush();
|
|
workerProcess.WaitForExit(500);
|
|
}
|
|
|
|
if (!workerProcess.HasExited)
|
|
{
|
|
workerProcess.Kill();
|
|
workerProcess.WaitForExit(500);
|
|
}
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
LogWorkerShutdownFailure(ex);
|
|
}
|
|
catch (ObjectDisposedException ex)
|
|
{
|
|
LogWorkerShutdownFailure(ex);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
LogWorkerShutdownFailure(ex);
|
|
}
|
|
finally
|
|
{
|
|
workerProcess.Dispose();
|
|
}
|
|
}
|
|
|
|
private static void CleanupWorkerDirectoryLocked()
|
|
{
|
|
string workerDirectoryPath = _workerDirectoryPath ?? GetWorkerDirectoryPath();
|
|
if (!Directory.Exists(workerDirectoryPath))
|
|
{
|
|
_workerDirectoryPath = null;
|
|
return;
|
|
}
|
|
|
|
TryDeleteWorkerDirectory(workerDirectoryPath);
|
|
_workerDirectoryPath = null;
|
|
}
|
|
|
|
private static void TryDeleteWorkerDirectory(string workerDirectoryPath)
|
|
{
|
|
try
|
|
{
|
|
s_deleteWorkerDirectory(workerDirectoryPath);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
LogWorkerDirectoryCleanupFailure(workerDirectoryPath, ex);
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
LogWorkerDirectoryCleanupFailure(workerDirectoryPath, ex);
|
|
}
|
|
}
|
|
|
|
private static void LogWorkerDirectoryCleanupFailure(string workerDirectoryPath, Exception ex)
|
|
{
|
|
VibeLogger.LogWarning(
|
|
"dynamic_code_shared_worker_cleanup_failed",
|
|
"execute-dynamic-code shared Roslyn worker directory cleanup failed during shutdown",
|
|
new
|
|
{
|
|
worker_directory_path = workerDirectoryPath,
|
|
exception_type = ex.GetType().FullName,
|
|
exception_message = ex.Message
|
|
},
|
|
humanNote: "Shared Roslyn worker cleanup could not remove its temporary directory during shutdown.",
|
|
aiTodo: "Investigate file locks or permission issues if temporary worker directories continue to accumulate.");
|
|
Debug.LogWarning($"[{McpConstants.PROJECT_NAME}] Failed to delete shared Roslyn worker directory '{workerDirectoryPath}': {ex.Message}");
|
|
}
|
|
|
|
private static WorkerAttemptResult CreateRetryableWorkerCommunicationFailure(
|
|
string requestFilePath,
|
|
Exception ex)
|
|
{
|
|
return WorkerAttemptResult.RetryableFailure(
|
|
"worker_communication_failed",
|
|
new
|
|
{
|
|
request_file_path = requestFilePath,
|
|
exception_type = ex.GetType().FullName,
|
|
exception_message = ex.Message
|
|
});
|
|
}
|
|
|
|
private static void LogWorkerShutdownFailure(Exception ex)
|
|
{
|
|
VibeLogger.LogWarning(
|
|
"dynamic_code_shared_worker_shutdown_failed",
|
|
"execute-dynamic-code shared Roslyn worker shutdown observed a communication failure",
|
|
new
|
|
{
|
|
exception_type = ex.GetType().FullName,
|
|
exception_message = ex.Message
|
|
},
|
|
humanNote: "Shared Roslyn worker shutdown saw a broken communication channel while cleaning up a crashed worker.",
|
|
aiTodo: "Investigate repeated worker shutdown communication failures if shared compilation stops recovering cleanly.");
|
|
}
|
|
|
|
private static object AppendAttempt(object failureContext, int attempt)
|
|
{
|
|
return new
|
|
{
|
|
attempt,
|
|
details = failureContext
|
|
};
|
|
}
|
|
|
|
private static string QuoteResponseFileArgument(string prefix, string value)
|
|
{
|
|
return $"{prefix}{QuoteResponseFilePath(value)}";
|
|
}
|
|
|
|
private static string QuoteResponseFilePath(string path)
|
|
{
|
|
return $"\"{path}\"";
|
|
}
|
|
|
|
private static string QuoteCommandLineArgument(string value)
|
|
{
|
|
return $"\"{value}\"";
|
|
}
|
|
|
|
private static void AddIfExists(
|
|
List<string> destination,
|
|
string referencePath)
|
|
{
|
|
if (string.IsNullOrEmpty(referencePath) || !File.Exists(referencePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
destination.Add(referencePath);
|
|
}
|
|
|
|
private static string CreateProgramSource()
|
|
{
|
|
string templatePath = GetWorkerProgramTemplatePath();
|
|
string templateSource = File.ReadAllText(templatePath, Encoding.UTF8);
|
|
return templateSource
|
|
.Replace(SharedCompilerWorkerResultPrefixToken, SharedCompilerWorkerResultPrefix)
|
|
.Replace(SharedCompilerWorkerEndMarkerToken, SharedCompilerWorkerEndMarker)
|
|
.Replace(SharedCompilerWorkerQuitCommandToken, SharedCompilerWorkerQuitCommand)
|
|
.Replace(CompileRequestPathPrefixToken, CompileRequestPathPrefix);
|
|
}
|
|
|
|
private static string GetWorkerProgramTemplatePath()
|
|
{
|
|
return Path.Combine(McpConstants.PackageResolvedPath, RoslynWorkerProgramTemplateRelativePath);
|
|
}
|
|
}
|
|
}
|