Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,53 @@ 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)));
}

[Test]
public void TryParseResponseHeader_WhenHeaderContainsExitCode_ShouldReturnParsedCode()
{
// Verifies the worker protocol accepts its result prefix followed by a numeric exit code.
bool parsed = SharedRoslynCompilerWorkerProtocol.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 = SharedRoslynCompilerWorkerProtocol.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 = SharedRoslynCompilerWorkerProtocol.GetResponseHeaderFailureReason(
"__ULOOP_RESULT__ not-a-number");

Assert.That(failureReason, Is.EqualTo("worker_invalid_exit_code"));
}

[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"));
Expand All @@ -60,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"));
Expand All @@ -87,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;"));
Expand All @@ -96,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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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))
{
Expand All @@ -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<string> outputLines = ReadDiagnosticLines(ct);
List<string> outputLines = SharedRoslynCompilerWorkerProtocol.ReadDiagnosticLines(reader, ct);
if (outputLines == null)
{
return WorkerAttemptResult.RetryableFailure(
Expand All @@ -450,49 +420,6 @@ private static WorkerAttemptResult ReadWorkerResponse(
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();
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();
Expand All @@ -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;
Expand Down Expand Up @@ -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<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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
}
Loading
Loading