From d12152b7d4ac364ac912c682babde7331404aeb7 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 00:53:49 +0900 Subject: [PATCH 1/2] Characterize shared worker response headers Cover valid result headers and both invalid-header classifications before moving protocol parsing out of the worker host. --- .../SharedRoslynCompilerWorkerHostTests.cs | 29 +++++++++++++++++++ .../SharedRoslynCompilerWorkerHost.cs | 4 +-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs index 1e23ec57bc..0afb13db89 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs @@ -47,6 +47,35 @@ public void CreateCompileRequestCommand_WhenPathIsWindowsAbsolutePath_ShouldEnco Assert.That(decodedPath, Is.EqualTo(Path.GetFullPath(requestFilePath))); } + [Test] + public void TryParseResponseHeader_WhenHeaderContainsExitCode_ShouldReturnParsedCode() + { + // Verifies the worker protocol accepts its result prefix followed by a numeric exit code. + bool parsed = SharedRoslynCompilerWorkerHost.TryParseResponseHeader("__ULOOP_RESULT__ 7", out int exitCode); + + Assert.That(parsed, Is.True); + Assert.That(exitCode, Is.EqualTo(7)); + } + + [Test] + public void GetResponseHeaderFailureReason_WhenPrefixIsInvalid_ShouldReportInvalidHeader() + { + // Verifies a response without the worker result prefix is classified as an invalid header. + string failureReason = SharedRoslynCompilerWorkerHost.GetResponseHeaderFailureReason("unexpected response"); + + Assert.That(failureReason, Is.EqualTo("worker_invalid_header")); + } + + [Test] + public void GetResponseHeaderFailureReason_WhenExitCodeIsInvalid_ShouldReportInvalidExitCode() + { + // Verifies a prefixed response with a non-numeric status is classified as an invalid exit code. + string failureReason = SharedRoslynCompilerWorkerHost.GetResponseHeaderFailureReason( + "__ULOOP_RESULT__ not-a-number"); + + Assert.That(failureReason, Is.EqualTo("worker_invalid_exit_code")); + } + [Test] public void CreateProgramSource_WhenRequestPathHasNoPrefix_ShouldRecoverRawPath() { diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs index ad68bfdf79..0981af4f42 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs @@ -450,7 +450,7 @@ private static WorkerAttemptResult ReadWorkerResponse( return WorkerAttemptResult.Successful(compilerMessages); } - private static bool TryParseResponseHeader(string responseHeader, out int exitCode) + internal static bool TryParseResponseHeader(string responseHeader, out int exitCode) { exitCode = 0; @@ -463,7 +463,7 @@ private static bool TryParseResponseHeader(string responseHeader, out int exitCo return int.TryParse(statusText, out exitCode); } - private static string GetResponseHeaderFailureReason(string responseHeader) + internal static string GetResponseHeaderFailureReason(string responseHeader) { if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal)) { From 48d819177eee9ce9c9846c5caa4fabb4f226e9a3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 00:57:20 +0900 Subject: [PATCH 2/2] Extract shared Roslyn worker protocol Move request framing, response parsing, protocol reads, and worker template rendering out of the process host. Pass the locked worker output reader explicitly so the protocol layer remains independent of host lifecycle state. --- .../SharedRoslynCompilerWorkerHostTests.cs | 32 ++-- .../SharedRoslynCompilerWorkerHost.cs | 143 ++---------------- .../SharedRoslynCompilerWorkerProtocol.cs | 114 ++++++++++++++ ...SharedRoslynCompilerWorkerProtocol.cs.meta | 11 ++ 4 files changed, 151 insertions(+), 149 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs.meta diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs index 0afb13db89..c886cc4367 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs @@ -33,16 +33,16 @@ public void CreateCompileRequestCommand_WhenPathIsWindowsAbsolutePath_ShouldEnco string requestFilePath = @"C:\Users\ExampleUser\Documents\unity\SampleWorkspace\SampleUnityProject\Temp\UnityCliLoopCompilation\DynamicCommand_1.worker"; - string command = SharedRoslynCompilerWorkerHost.CreateCompileRequestCommandForTests(requestFilePath); + string command = SharedRoslynCompilerWorkerProtocol.CreateCompileRequestCommand(requestFilePath); - Assert.That(command, Does.StartWith(SharedRoslynCompilerWorkerHost.CompileRequestPathPrefix)); + Assert.That(command, Does.StartWith(SharedRoslynCompilerWorkerProtocol.CompileRequestPathPrefix)); Assert.That(command, Does.Not.Contain(requestFilePath)); foreach (char character in command) { Assert.That(character, Is.LessThanOrEqualTo((char)127)); } - string encodedPath = command.Substring(SharedRoslynCompilerWorkerHost.CompileRequestPathPrefix.Length); + string encodedPath = command.Substring(SharedRoslynCompilerWorkerProtocol.CompileRequestPathPrefix.Length); string decodedPath = Encoding.UTF8.GetString(Convert.FromBase64String(encodedPath)); Assert.That(decodedPath, Is.EqualTo(Path.GetFullPath(requestFilePath))); } @@ -51,7 +51,7 @@ public void CreateCompileRequestCommand_WhenPathIsWindowsAbsolutePath_ShouldEnco public void TryParseResponseHeader_WhenHeaderContainsExitCode_ShouldReturnParsedCode() { // Verifies the worker protocol accepts its result prefix followed by a numeric exit code. - bool parsed = SharedRoslynCompilerWorkerHost.TryParseResponseHeader("__ULOOP_RESULT__ 7", out int exitCode); + bool parsed = SharedRoslynCompilerWorkerProtocol.TryParseResponseHeader("__ULOOP_RESULT__ 7", out int exitCode); Assert.That(parsed, Is.True); Assert.That(exitCode, Is.EqualTo(7)); @@ -61,7 +61,7 @@ public void TryParseResponseHeader_WhenHeaderContainsExitCode_ShouldReturnParsed public void GetResponseHeaderFailureReason_WhenPrefixIsInvalid_ShouldReportInvalidHeader() { // Verifies a response without the worker result prefix is classified as an invalid header. - string failureReason = SharedRoslynCompilerWorkerHost.GetResponseHeaderFailureReason("unexpected response"); + string failureReason = SharedRoslynCompilerWorkerProtocol.GetResponseHeaderFailureReason("unexpected response"); Assert.That(failureReason, Is.EqualTo("worker_invalid_header")); } @@ -70,7 +70,7 @@ public void GetResponseHeaderFailureReason_WhenPrefixIsInvalid_ShouldReportInval public void GetResponseHeaderFailureReason_WhenExitCodeIsInvalid_ShouldReportInvalidExitCode() { // Verifies a prefixed response with a non-numeric status is classified as an invalid exit code. - string failureReason = SharedRoslynCompilerWorkerHost.GetResponseHeaderFailureReason( + string failureReason = SharedRoslynCompilerWorkerProtocol.GetResponseHeaderFailureReason( "__ULOOP_RESULT__ not-a-number"); Assert.That(failureReason, Is.EqualTo("worker_invalid_exit_code")); @@ -79,7 +79,7 @@ public void GetResponseHeaderFailureReason_WhenExitCodeIsInvalid_ShouldReportInv [Test] public void CreateProgramSource_WhenRequestPathHasNoPrefix_ShouldRecoverRawPath() { - string programSource = SharedRoslynCompilerWorkerHost.CreateProgramSourceForTests(); + string programSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource(); Assert.That(programSource, Does.Contain("return RecoverRawRequestPath(requestPath);")); Assert.That(programSource, Does.Contain("FindWindowsDrivePathIndex")); @@ -89,24 +89,24 @@ public void CreateProgramSource_WhenRequestPathHasNoPrefix_ShouldRecoverRawPath( [Test] public void CreateProgramSource_WhenTemplateIsLoaded_ShouldReplaceTokens() { - string templatePath = SharedRoslynCompilerWorkerHost.GetWorkerProgramTemplatePathForTests(); - string programSource = SharedRoslynCompilerWorkerHost.CreateProgramSourceForTests(); + string templatePath = SharedRoslynCompilerWorkerProtocol.GetWorkerProgramTemplatePath(); + string programSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource(); Assert.That(File.Exists(templatePath), Is.True); - Assert.That(programSource, Does.Contain(SharedRoslynCompilerWorkerHost.CompileRequestPathPrefix)); + Assert.That(programSource, Does.Contain(SharedRoslynCompilerWorkerProtocol.CompileRequestPathPrefix)); Assert.That(programSource, Does.Contain( - SharedRoslynCompilerWorkerHost.GetSharedCompilerWorkerResultPrefixForTests())); + SharedRoslynCompilerWorkerProtocol.SharedCompilerWorkerResultPrefix)); Assert.That(programSource, Does.Contain( - SharedRoslynCompilerWorkerHost.GetSharedCompilerWorkerEndMarkerForTests())); + SharedRoslynCompilerWorkerProtocol.SharedCompilerWorkerEndMarker)); Assert.That(programSource, Does.Contain( - SharedRoslynCompilerWorkerHost.GetSharedCompilerWorkerQuitCommandForTests())); + SharedRoslynCompilerWorkerProtocol.SharedCompilerWorkerQuitCommand)); Assert.That(programSource, Does.Not.Contain("{{")); } [Test] public void CreateProgramSource_WhenRequestPathPrefixHasLeadingGarbage_ShouldDecodeEncodedPath() { - string programSource = SharedRoslynCompilerWorkerHost.CreateProgramSourceForTests(); + string programSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource(); Assert.That(programSource, Does.Contain("FindRequestPathPrefixIndex")); Assert.That(programSource, Does.Contain("IndexOf(RequestPathPrefix")); @@ -116,7 +116,7 @@ public void CreateProgramSource_WhenRequestPathPrefixHasLeadingGarbage_ShouldDec [Test] public void CreateProgramSource_WhenRawPathContainsPrefixAfterDirectorySeparator_ShouldRecoverRawPath() { - string programSource = SharedRoslynCompilerWorkerHost.CreateProgramSourceForTests(); + string programSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource(); Assert.That(programSource, Does.Contain("HasDirectorySeparatorBeforePrefix")); Assert.That(programSource, Does.Contain("return HasDirectorySeparatorBeforePrefix(requestPath, encodedPathIndex) ? -1 : encodedPathIndex;")); @@ -125,7 +125,7 @@ public void CreateProgramSource_WhenRawPathContainsPrefixAfterDirectorySeparator [Test] public void CreateProgramSource_WhenEncodedPayloadIsMalformed_ShouldRecoverRawPath() { - string programSource = SharedRoslynCompilerWorkerHost.CreateProgramSourceForTests(); + string programSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource(); Assert.That(programSource, Does.Contain("IsBase64Payload")); Assert.That(programSource, Does.Contain("HasValidBase64Padding")); diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs index 0981af4f42..a995e0fde1 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; using UnityEditor; @@ -19,20 +18,9 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools 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/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/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"; @@ -392,36 +380,18 @@ private static void SendCompileRequest(string requestFilePath) private static void SendCompileRequestCore(Process workerProcess, string requestFilePath) { - string requestCommand = CreateCompileRequestCommand(requestFilePath); + string requestCommand = SharedRoslynCompilerWorkerProtocol.CreateCompileRequestCommand(requestFilePath); workerProcess.StandardInput.WriteLine(requestCommand); workerProcess.StandardInput.Flush(); } - internal static string CreateCompileRequestCommandForTests(string requestFilePath) - { - return CreateCompileRequestCommand(requestFilePath); - } - - internal static string CreateProgramSourceForTests() - { - return CreateProgramSource(); - } - - 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, + StreamReader reader = _sharedCompilerWorkerProcess.StandardOutput; + string responseHeader = SharedRoslynCompilerWorkerProtocol.ReadProtocolLine( + reader, ct); if (string.IsNullOrEmpty(responseHeader)) { @@ -430,14 +400,14 @@ private static WorkerAttemptResult ReadWorkerResponse( new { request_file_path = requestFilePath }); } - if (!TryParseResponseHeader(responseHeader, out int exitCode)) + if (!SharedRoslynCompilerWorkerProtocol.TryParseResponseHeader(responseHeader, out int exitCode)) { return WorkerAttemptResult.RetryableFailure( - GetResponseHeaderFailureReason(responseHeader), + SharedRoslynCompilerWorkerProtocol.GetResponseHeaderFailureReason(responseHeader), new { header = responseHeader }); } - List outputLines = ReadDiagnosticLines(ct); + List outputLines = SharedRoslynCompilerWorkerProtocol.ReadDiagnosticLines(reader, ct); if (outputLines == null) { return WorkerAttemptResult.RetryableFailure( @@ -450,49 +420,6 @@ private static WorkerAttemptResult ReadWorkerResponse( return WorkerAttemptResult.Successful(compilerMessages); } - internal 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); - } - - internal static string GetResponseHeaderFailureReason(string responseHeader) - { - if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal)) - { - return "worker_invalid_header"; - } - - return "worker_invalid_exit_code"; - } - - private static List ReadDiagnosticLines(CancellationToken ct) - { - List outputLines = new(); - 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(); @@ -515,7 +442,7 @@ private static string GetWorkerDirectoryPath() private static void SynchronizeWorkerSource(WorkerPaths workerPaths) { - string workerSource = CreateProgramSource(); + string workerSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource(); if (File.Exists(workerPaths.SourcePath) && File.ReadAllText(workerPaths.SourcePath) == workerSource) { return; @@ -686,22 +613,6 @@ private static void WriteWorkerCompilerResponseFile( File.WriteAllLines(responseFilePath, lines); } - private static string ReadProtocolLine(StreamReader reader, CancellationToken ct) - { - Debug.Assert(reader != null, "reader must not be null"); - - Task 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; @@ -811,7 +722,8 @@ private static void ShutdownWorkerProcessLocked() { if (!workerProcess.HasExited) { - workerProcess.StandardInput.WriteLine(SharedCompilerWorkerQuitCommand); + workerProcess.StandardInput.WriteLine( + SharedRoslynCompilerWorkerProtocol.SharedCompilerWorkerQuitCommand); workerProcess.StandardInput.Flush(); workerProcess.WaitForExit(500); } @@ -949,40 +861,5 @@ private static void AddIfExists( 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); - } - - internal static string GetWorkerProgramTemplatePathForTests() - { - return GetWorkerProgramTemplatePath(); - } - - internal static string GetSharedCompilerWorkerResultPrefixForTests() - { - return SharedCompilerWorkerResultPrefix; - } - - internal static string GetSharedCompilerWorkerEndMarkerForTests() - { - return SharedCompilerWorkerEndMarker; - } - - internal static string GetSharedCompilerWorkerQuitCommandForTests() - { - return SharedCompilerWorkerQuitCommand; - } - - private static string GetWorkerProgramTemplatePath() - { - return Path.Combine(UnityCliLoopConstants.PackageResolvedPath, RoslynWorkerProgramTemplateRelativePath); - } } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs new file mode 100644 index 0000000000..8379a631aa --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Debug = UnityEngine.Debug; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Encodes and decodes the shared Roslyn compiler worker protocol. + /// + internal static class SharedRoslynCompilerWorkerProtocol + { + internal const string SharedCompilerWorkerResultPrefix = "__ULOOP_RESULT__"; + internal const string SharedCompilerWorkerEndMarker = "__ULOOP_END__"; + internal const string SharedCompilerWorkerQuitCommand = "__QUIT__"; + internal const string CompileRequestPathPrefix = "path-base64:"; + private const string RoslynWorkerProgramTemplateRelativePath = + "Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/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; + + internal 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); + } + + internal 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); + } + + internal static string GetResponseHeaderFailureReason(string responseHeader) + { + if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal)) + { + return "worker_invalid_header"; + } + + return "worker_invalid_exit_code"; + } + + internal static List ReadDiagnosticLines(StreamReader reader, CancellationToken ct) + { + List outputLines = new(); + while (true) + { + string outputLine = ReadProtocolLine(reader, ct); + if (outputLine == null) + { + return null; + } + + if (outputLine == SharedCompilerWorkerEndMarker) + { + return outputLines; + } + + outputLines.Add(outputLine); + } + } + + internal static string ReadProtocolLine(StreamReader reader, CancellationToken ct) + { + Debug.Assert(reader != null, "reader must not be null"); + + Task 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(); + } + + internal 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); + } + + internal static string GetWorkerProgramTemplatePath() + { + return Path.Combine(UnityCliLoopConstants.PackageResolvedPath, RoslynWorkerProgramTemplateRelativePath); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs.meta new file mode 100644 index 0000000000..2aeb126f2e --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerProtocol.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1207fcd146325443ea468ea8f98e7f63 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: