Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Compilation/SharedRoslynCompilerWorkerHost.cs

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);
}
}
}