diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs
index e26911308a..76477e6027 100644
--- a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs
@@ -293,6 +293,70 @@ public async Task CompileAsync_WhenSharedWorkerBuildFails_ShouldFallbackToOneSho
}
}
+ ///
+ /// Verifies a lifecycle transition during worker startup silently falls back to one-shot Roslyn.
+ ///
+ [Test]
+ public async Task CompileAsync_WhenWorkerLifecycleClosesDuringStartup_ShouldFallbackWithoutErrorLogs()
+ {
+ ExternalCompilerPaths externalCompilerPaths = ExternalCompilerPathResolver.Resolve();
+ Assert.That(externalCompilerPaths, Is.Not.Null, "Unity external compiler layout should be available.");
+
+ string sourcePath = Path.Combine(_tempDirectoryPath, "LifecycleFallbackSmoke.cs");
+ string dllPath = Path.Combine(_tempDirectoryPath, "LifecycleFallbackSmoke.dll");
+ File.WriteAllText(
+ sourcePath,
+ "public static class LifecycleFallbackSmoke { public static int Execute() { return 11; } }");
+ List references = new DynamicReferenceSetBuilderService().BuildReferenceSet(
+ new List(),
+ null,
+ externalCompilerPaths);
+ int buildCount = 0;
+ bool buildStarted = false;
+ bool buildFinished = false;
+
+ DynamicCompilationHealthMonitor.ResetForTests();
+ SharedRoslynCompilerWorkerHost.ShutdownForTests();
+ LogAssert.NoUnexpectedReceived();
+
+ Func previousWorkerCompiler =
+ SharedRoslynCompilerWorkerHost.SwapWorkerAssemblyCompilerForTests(
+ (ExternalCompilerPaths paths, string workerSourcePath, string workerAssemblyPath, string workerCompileResponseFilePath) =>
+ {
+ SharedRoslynCompilerWorkerHost.ShutdownForTests();
+ return Array.Empty();
+ });
+
+ try
+ {
+ using CancellationTokenSource compileCancellationTokenSource = new CancellationTokenSource();
+ compileCancellationTokenSource.CancelAfter(FallbackCompileTimeoutMilliseconds);
+ DynamicCompilationBackendResult result = await RoslynCompilerBackend.CompileAsync(
+ sourcePath,
+ dllPath,
+ references,
+ externalCompilerPaths,
+ new RoslynCompilerOptions(Array.Empty(), false),
+ compileCancellationTokenSource.Token,
+ () => buildStarted = true,
+ () => buildFinished = true,
+ () => buildCount++);
+
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result.BackendKind, Is.EqualTo(DynamicCompilationBackendKind.OneShotRoslyn));
+ Assert.That(File.Exists(dllPath), Is.True);
+ Assert.That(buildCount, Is.EqualTo(1));
+ Assert.That(buildStarted, Is.True);
+ Assert.That(buildFinished, Is.True);
+ LogAssert.NoUnexpectedReceived();
+ }
+ finally
+ {
+ SharedRoslynCompilerWorkerHost.SwapWorkerAssemblyCompilerForTests(previousWorkerCompiler);
+ SharedRoslynCompilerWorkerHost.ShutdownForTests();
+ }
+ }
+
[Test]
public void ReportInfrastructureFallback_WhenCompilerPathLayoutIsKnown_ShouldIncludeLayoutKind()
{
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs
index b13609d5a5..232dd9588e 100644
--- a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs
@@ -5,6 +5,7 @@
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
+using UnityEngine.TestTools;
using UnityEditor.Compilation;
using io.github.hatayama.UnityCliLoop.FirstPartyTools;
@@ -17,6 +18,35 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
[TestFixture]
public class SharedRoslynCompilerWorkerHostTests
{
+ ///
+ /// Verifies lifecycle closure is returned as a non-error compile outcome.
+ ///
+ [Test]
+ public void SharedWorkerCompileOutcome_WhenLifecycleCloses_ShouldCarryLifecycleClosedReason()
+ {
+ SharedWorkerCompileOutcome outcome = SharedWorkerCompileOutcome.Failed(
+ SharedWorkerFailureReasons.LifecycleClosed,
+ new { reason = "lifecycle_generation_advanced" });
+
+ Assert.That(outcome.Succeeded, Is.False);
+ Assert.That(outcome.FailureReason, Is.EqualTo(SharedWorkerFailureReasons.LifecycleClosed));
+ Assert.That(outcome.IsLifecycleClosed, Is.True);
+ }
+
+ ///
+ /// Verifies non-lifecycle worker failures retain their failure reason for error reporting.
+ ///
+ [Test]
+ public void SharedWorkerCompileOutcome_WhenWorkerStartFails_ShouldCarryFailureReason()
+ {
+ SharedWorkerCompileOutcome outcome = SharedWorkerCompileOutcome.Failed(
+ "worker_start_failed",
+ new { reason = "process_start_failed" });
+
+ Assert.That(outcome.Succeeded, Is.False);
+ Assert.That(outcome.FailureReason, Is.EqualTo("worker_start_failed"));
+ }
+
[Test]
public void ConfigureWorkerDotnetRuntimeEnvironment_WhenCalled_ShouldDisableMultilevelLookup()
{
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs
index d9ee44db3e..dc0d5979af 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/DynamicCompilationHealthMonitor.cs
@@ -63,6 +63,15 @@ public static void ReportSharedWorkerFailure(string reason, object context = nul
"Investigate worker startup, request handling, and Unity-version-specific runtime assumptions.");
}
+ public static void ReportSharedWorkerLifecycleClosed(object context = null)
+ {
+ LogInfoOnce(
+ SharedWorkerFailureReasons.LifecycleClosed,
+ "dynamic_code_shared_worker_lifecycle_closed",
+ "execute-dynamic-code shared Roslyn worker closed during an expected Unity lifecycle transition",
+ context ?? new { reason = SharedWorkerFailureReasons.LifecycleClosed });
+ }
+
public static void ReportOneShotCompilerStartFailure(object context)
{
LogErrorOnce(
@@ -101,6 +110,25 @@ private static void LogErrorOnce(
Debug.LogError(FormatConsoleErrorMessage(operation, message, context));
}
+ private static void LogInfoOnce(
+ string issueKey,
+ string operation,
+ string message,
+ object context)
+ {
+ string effectiveIssueKey = CreateEffectiveIssueKey(issueKey);
+
+ lock (ReportedIssueLock)
+ {
+ if (!ReportedIssues.Add(effectiveIssueKey))
+ {
+ return;
+ }
+ }
+
+ VibeLogger.LogInfo(operation, message, context);
+ }
+
private static string CreateEffectiveIssueKey(string issueKey)
{
Debug.Assert(!string.IsNullOrEmpty(issueKey), "issueKey must not be empty");
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs
index 050e331f09..56a32305bd 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs
@@ -71,29 +71,33 @@ public static async Task CompileAsync(
defineSymbols,
allowUnsafeCode);
- CompilerMessage[] workerMessages = await SharedRoslynCompilerWorkerHost.TryCompileAsync(
+ SharedWorkerCompileOutcome workerOutcome = await SharedRoslynCompilerWorkerHost.TryCompileAsync(
workerRequestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount).ConfigureAwait(false);
- if (workerMessages != null)
+ UnityEngine.Debug.Assert(workerOutcome != null, "Shared worker compile outcome must not be null");
+ if (workerOutcome.Succeeded)
{
return new DynamicCompilationBackendResult(
- workerMessages,
+ workerOutcome.Messages,
DynamicCompilationBackendKind.SharedRoslynWorker);
}
- DynamicCompilationHealthMonitor.ReportSharedWorkerFallback(
- "worker_unavailable",
- new
- {
- platform = UnityEngine.Application.platform.ToString(),
- dotnet_host_path = externalCompilerPaths.DotnetHostPath,
- compiler_dll_path = externalCompilerPaths.CompilerDllPath,
- layout_kind = externalCompilerPaths.LayoutKind.ToString()
- });
+ if (!workerOutcome.IsLifecycleClosed)
+ {
+ DynamicCompilationHealthMonitor.ReportSharedWorkerFallback(
+ "worker_unavailable",
+ new
+ {
+ platform = UnityEngine.Application.platform.ToString(),
+ dotnet_host_path = externalCompilerPaths.DotnetHostPath,
+ compiler_dll_path = externalCompilerPaths.CompilerDllPath,
+ layout_kind = externalCompilerPaths.LayoutKind.ToString()
+ });
+ }
ct.ThrowIfCancellationRequested();
OneShotCompileResult oneShotResult = await CompileWithOneShotAsync(
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs
index 757da064e1..79a9fd5823 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs
@@ -28,7 +28,7 @@ internal static void RegisterLifecycleForEditorStartup()
EditorApplication.quitting += ShutdownForQuit;
}
- public static Task TryCompileAsync(
+ public static Task TryCompileAsync(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
@@ -47,7 +47,7 @@ public static Task TryCompileAsync(
ct);
}
- private static async Task TryCompileWithRetriesAsync(
+ private static async Task TryCompileWithRetriesAsync(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
@@ -71,7 +71,7 @@ private static async Task TryCompileWithRetriesAsync(
if (attemptResult.Succeeded)
{
- return attemptResult.Messages;
+ return SharedWorkerCompileOutcome.SucceededWith(attemptResult.Messages);
}
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked);
@@ -81,13 +81,24 @@ private static async Task TryCompileWithRetriesAsync(
continue;
}
- DynamicCompilationHealthMonitor.ReportSharedWorkerFailure(
- attemptResult.FailureReason,
- AppendAttempt(attemptResult.FailureContext, attempt));
- return null;
+ object failureContext = AppendAttempt(attemptResult.FailureContext, attempt);
+ if (attemptResult.FailureReason == SharedWorkerFailureReasons.LifecycleClosed)
+ {
+ DynamicCompilationHealthMonitor.ReportSharedWorkerLifecycleClosed(failureContext);
+ }
+ else
+ {
+ DynamicCompilationHealthMonitor.ReportSharedWorkerFailure(
+ attemptResult.FailureReason,
+ failureContext);
+ }
+
+ return SharedWorkerCompileOutcome.Failed(attemptResult.FailureReason, failureContext);
}
- return null;
+ return SharedWorkerCompileOutcome.Failed(
+ "worker_unknown_failure",
+ new { reason = "retry_loop_exhausted" });
}
private static async Task TryCompileOnceAsync(
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs
index e6eadfe316..a4f2eba74d 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs
@@ -2,6 +2,47 @@
namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
+ internal static class SharedWorkerFailureReasons
+ {
+ internal const string LifecycleClosed = "shared_worker_lifecycle_closed";
+ }
+
+ ///
+ /// Carries shared worker compilation messages and the reason for a failed compilation.
+ ///
+ internal sealed class SharedWorkerCompileOutcome
+ {
+ public CompilerMessage[] Messages { get; }
+
+ public string FailureReason { get; }
+
+ public object FailureContext { get; }
+
+ private SharedWorkerCompileOutcome(
+ CompilerMessage[] messages,
+ string failureReason,
+ object failureContext)
+ {
+ Messages = messages;
+ FailureReason = failureReason;
+ FailureContext = failureContext;
+ }
+
+ public bool Succeeded => Messages != null;
+
+ public bool IsLifecycleClosed => FailureReason == SharedWorkerFailureReasons.LifecycleClosed;
+
+ public static SharedWorkerCompileOutcome SucceededWith(CompilerMessage[] messages)
+ {
+ return new SharedWorkerCompileOutcome(messages, null, null);
+ }
+
+ public static SharedWorkerCompileOutcome Failed(string failureReason, object failureContext)
+ {
+ return new SharedWorkerCompileOutcome(null, failureReason, failureContext);
+ }
+ }
+
///
/// Carries the result data produced by Worker Attempt behavior.
///
@@ -85,7 +126,7 @@ public static WorkerStartupResult ClosedLifecycleFailure()
return new WorkerStartupResult(
false,
false,
- "shared_worker_lifecycle_closed",
+ SharedWorkerFailureReasons.LifecycleClosed,
new { reason = "lifecycle_generation_advanced" });
}
}