From d775c181515c695bc607e2e27acab629fe2de7ff Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 29 May 2026 00:18:51 +0900 Subject: [PATCH 1/3] Recover compile requests when Unity misses finish callback Unity can leave the compile command waiting forever if EditorApplication.isCompiling drops before compilationFinished reaches the controller. Watch the compile lifecycle after start and complete the request as indeterminate so the execution slot is released and follow-up commands can run. - Preserve the existing start-timeout behavior in the watchdog. - Add focused EditMode tests for missed callback recovery and normal callback completion. --- .../Editor/CompileLifecycleWatchdogTests.cs | 125 ++++++++++++++++++ .../CompileLifecycleWatchdogTests.cs.meta | 11 ++ .../Compile/CompileController.cs | 93 +++++++++---- .../Compile/CompileLifecycleWatchdog.cs | 113 ++++++++++++++++ .../Compile/CompileLifecycleWatchdog.cs.meta | 11 ++ .../ToolContracts/UnityCliLoopConstants.cs | 1 + 6 files changed, 326 insertions(+), 28 deletions(-) create mode 100644 Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs create mode 100644 Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs create mode 100644 Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs.meta diff --git a/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs new file mode 100644 index 000000000..55c1ec32b --- /dev/null +++ b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Tests compile lifecycle recovery decisions without invoking Unity's real compiler. + /// + [TestFixture] + public sealed class CompileLifecycleWatchdogTests + { + [Test] + public async Task WatchAsync_WhenEditorStopsCompilingWithoutCallback_RequestsMissedCallbackRecovery() + { + // Verifies the watchdog recovers when Unity leaves compiling state without firing completion callback. + SequenceCompilationState compilationState = new SequenceCompilationState( + new bool[] { false, true, false, false, false, false, false }); + int missedCallbackStoppedMs = -1; + int startTimeoutCount = 0; + int cancellationCount = 0; + CompileLifecycleWatchdog watchdog = new CompileLifecycleWatchdog( + compilationState.IsCompiling, + () => false, + () => Task.CompletedTask, + _ => { }, + _ => startTimeoutCount++, + stoppedMs => missedCallbackStoppedMs = stoppedMs, + _ => cancellationCount++); + + await watchdog.WatchAsync(CancellationToken.None); + + Assert.That(missedCallbackStoppedMs, Is.EqualTo(UnityCliLoopConstants.COMPILE_FINISH_MISSED_CALLBACK_GRACE_MS)); + Assert.That(startTimeoutCount, Is.EqualTo(0)); + Assert.That(cancellationCount, Is.EqualTo(0)); + } + + [Test] + public async Task WatchAsync_WhenRequestCompletesAfterStart_StopsWithoutRecovery() + { + // Verifies the watchdog does not recover a request already completed by Unity's callback. + SequenceCompilationState compilationState = new SequenceCompilationState( + new bool[] { false, true, false, false, false }); + int waitCount = 0; + int missedCallbackCount = 0; + int startTimeoutCount = 0; + CompileLifecycleWatchdog watchdog = new CompileLifecycleWatchdog( + compilationState.IsCompiling, + () => waitCount >= 2, + () => + { + waitCount++; + return Task.CompletedTask; + }, + _ => { }, + _ => startTimeoutCount++, + _ => missedCallbackCount++, + _ => { }); + + await watchdog.WatchAsync(CancellationToken.None); + + Assert.That(missedCallbackCount, Is.EqualTo(0)); + Assert.That(startTimeoutCount, Is.EqualTo(0)); + } + + [Test] + public async Task WatchAsync_WhenCompileNeverStarts_RequestsStartTimeout() + { + // Verifies the watchdog keeps the existing start-timeout recovery path. + ConstantCompilationState compilationState = new ConstantCompilationState(false); + int startTimeoutMs = -1; + int missedCallbackCount = 0; + CompileLifecycleWatchdog watchdog = new CompileLifecycleWatchdog( + compilationState.IsCompiling, + () => false, + () => Task.CompletedTask, + _ => { }, + waitedMs => startTimeoutMs = waitedMs, + _ => missedCallbackCount++, + _ => { }); + + await watchdog.WatchAsync(CancellationToken.None); + + Assert.That(startTimeoutMs, Is.EqualTo(UnityCliLoopConstants.COMPILE_START_TIMEOUT_MS)); + Assert.That(missedCallbackCount, Is.EqualTo(0)); + } + + private sealed class SequenceCompilationState + { + private readonly bool[] _states; + private int _index; + + public SequenceCompilationState(bool[] states) + { + _states = states ?? throw new ArgumentNullException(nameof(states)); + } + + public bool IsCompiling() + { + int index = Math.Min(_index, _states.Length - 1); + _index++; + return _states[index]; + } + } + + private sealed class ConstantCompilationState + { + private readonly bool _isCompiling; + + public ConstantCompilationState(bool isCompiling) + { + _isCompiling = isCompiling; + } + + public bool IsCompiling() + { + return _isCompiling; + } + } + } +} diff --git a/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs.meta b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs.meta new file mode 100644 index 000000000..b7fbd1d2a --- /dev/null +++ b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8ff68a991de24b20aa0c8c76c5a3737 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs index 82d78f31b..3a3805fce 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs @@ -162,7 +162,7 @@ public async Task TryCompileAsync(bool forceRecompile, Cancellati "Unity script compilation request returned.", new { force_recompile = forceRecompile }); - _ = WatchCompileStartAsync(ct); + _ = WatchCompileLifecycleAsync(ct); VibeLogger.LogInfo( "compile_controller_waiting_for_finish", "Waiting for Unity compilationFinished callback.", @@ -189,38 +189,39 @@ public async Task TryCompileAsync(bool forceRecompile, Cancellati } } - private async Task WatchCompileStartAsync(CancellationToken ct) + private Task WatchCompileLifecycleAsync(CancellationToken ct) { - int waitedMs = 0; - - while (waitedMs < UnityCliLoopConstants.COMPILE_START_TIMEOUT_MS) - { - if (ct.IsCancellationRequested) - { - AbortCompile("Compilation request was cancelled before it started."); - return; - } + CompileLifecycleWatchdog watchdog = new CompileLifecycleWatchdog( + () => EditorApplication.isCompiling, + IsCompileRequestCompleted, + WaitForCompileWatchdogPollAsync, + HandleCompileStartedObserved, + HandleCompileStartTimeout, + HandleCompileStoppedWithoutFinishEvent, + AbortCompile); + return watchdog.WatchAsync(ct); + } - if (EditorApplication.isCompiling) - { - VibeLogger.LogInfo( - "compile_editor_compiling_observed", - "EditorApplication.isCompiling became true.", - new { waited_ms = waitedMs }); - return; - } + private bool IsCompileRequestCompleted() + { + return _currentCompileTask == null || _currentCompileTask.Task.IsCompleted; + } - if (_currentCompileTask == null || _currentCompileTask.Task.IsCompleted) - { - return; - } + private static Task WaitForCompileWatchdogPollAsync() + { + return TimerDelay.Wait(UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS); + } - // Do not pass CancellationToken here: this method is fire-and-forget and must not throw. - // Cancellation is handled by explicit checks above to avoid leaving _currentCompileTask pending. - await TimerDelay.Wait(UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS); - waitedMs += UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS; - } + private void HandleCompileStartedObserved(int waitedMs) + { + VibeLogger.LogInfo( + "compile_editor_compiling_observed", + "EditorApplication.isCompiling became true.", + new { waited_ms = waitedMs }); + } + private void HandleCompileStartTimeout(int waitedMs) + { AssemblyDefinitionConsoleErrorValidationService assemblyDefinitionValidationService = new(); AssemblyDefinitionConsoleErrorResult assemblyDefinitionErrors = assemblyDefinitionValidationService.FindCurrentErrors(); @@ -255,6 +256,23 @@ private async Task WatchCompileStartAsync(CancellationToken ct) ); } + private void HandleCompileStoppedWithoutFinishEvent(int stoppedMs) + { + string message = + "Unity stopped compiling before Unity CLI Loop received the compilationFinished callback. " + + "The compile result is indeterminate; use get-logs to inspect the compiler output."; + VibeLogger.LogWarning( + "compile_finish_callback_missing", + message, + new + { + force_recompile = _isForceCompile, + stopped_ms = stoppedMs, + message_count = _compileMessages.Count + }); + AbortCompileWithResult(CreateIndeterminateCompileResult(message)); + } + /// /// Completes an active compile request with a prepared failure result before Unity reports compilationFinished. /// @@ -447,6 +465,23 @@ private CompileResult CreateCompileResult() ); } + private CompileResult CreateIndeterminateCompileResult(string message) + { + CompilerMessage[] errors = _compileMessages.Where(m => m.type == CompilerMessageType.Error).ToArray(); + CompilerMessage[] warnings = _compileMessages.Where(m => m.type == CompilerMessageType.Warning).ToArray(); + return new CompileResult( + success: null, + errorCount: errors.Length, + warningCount: warnings.Length, + completedAt: DateTime.Now, + messages: _compileMessages.ToArray(), + errors: errors, + warnings: warnings, + isIndeterminate: true, + message: message + ); + } + /// /// Creates a failed compile result from Assembly Definition and Assembly Reference Console errors. /// @@ -503,6 +538,8 @@ public void Cleanup() _currentCompileTask.SetCanceled(); _currentCompileTask = null; } + _isCompiling = false; + _isForceCompile = false; } /// diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs new file mode 100644 index 000000000..b3fc9c3af --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Watches one Unity compile request so the controller can recover when Unity misses lifecycle callbacks. + /// + internal sealed class CompileLifecycleWatchdog + { + private readonly Func _isEditorCompiling; + private readonly Func _isRequestCompleted; + private readonly Func _waitForPollAsync; + private readonly Action _onCompileStartedObserved; + private readonly Action _onStartTimeout; + private readonly Action _onMissedCompletionCallback; + private readonly Action _onCancelled; + + internal CompileLifecycleWatchdog( + Func isEditorCompiling, + Func isRequestCompleted, + Func waitForPollAsync, + Action onCompileStartedObserved, + Action onStartTimeout, + Action onMissedCompletionCallback, + Action onCancelled) + { + Debug.Assert(isEditorCompiling != null, "isEditorCompiling must not be null"); + Debug.Assert(isRequestCompleted != null, "isRequestCompleted must not be null"); + Debug.Assert(waitForPollAsync != null, "waitForPollAsync must not be null"); + Debug.Assert(onCompileStartedObserved != null, "onCompileStartedObserved must not be null"); + Debug.Assert(onStartTimeout != null, "onStartTimeout must not be null"); + Debug.Assert(onMissedCompletionCallback != null, "onMissedCompletionCallback must not be null"); + Debug.Assert(onCancelled != null, "onCancelled must not be null"); + + _isEditorCompiling = isEditorCompiling ?? throw new ArgumentNullException(nameof(isEditorCompiling)); + _isRequestCompleted = isRequestCompleted ?? throw new ArgumentNullException(nameof(isRequestCompleted)); + _waitForPollAsync = waitForPollAsync ?? throw new ArgumentNullException(nameof(waitForPollAsync)); + _onCompileStartedObserved = onCompileStartedObserved ?? throw new ArgumentNullException(nameof(onCompileStartedObserved)); + _onStartTimeout = onStartTimeout ?? throw new ArgumentNullException(nameof(onStartTimeout)); + _onMissedCompletionCallback = onMissedCompletionCallback ?? throw new ArgumentNullException(nameof(onMissedCompletionCallback)); + _onCancelled = onCancelled ?? throw new ArgumentNullException(nameof(onCancelled)); + } + + /// + /// Observes compile start and finish transitions for a single compile request. + /// + internal async Task WatchAsync(CancellationToken ct) + { + bool observedStart = false; + int waitedForStartMs = 0; + int stoppedAfterStartMs = 0; + + while (!_isRequestCompleted()) + { + if (ct.IsCancellationRequested) + { + string reason = observedStart + ? "Compilation request was cancelled before Unity reported completion." + : "Compilation request was cancelled before it started."; + _onCancelled(reason); + return; + } + + bool isEditorCompiling = _isEditorCompiling(); + if (!observedStart) + { + if (isEditorCompiling) + { + observedStart = true; + stoppedAfterStartMs = 0; + _onCompileStartedObserved(waitedForStartMs); + } + else if (waitedForStartMs >= UnityCliLoopConstants.COMPILE_START_TIMEOUT_MS) + { + if (!_isRequestCompleted()) + { + _onStartTimeout(waitedForStartMs); + } + return; + } + } + else if (isEditorCompiling) + { + stoppedAfterStartMs = 0; + } + else + { + stoppedAfterStartMs += UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS; + if (stoppedAfterStartMs >= UnityCliLoopConstants.COMPILE_FINISH_MISSED_CALLBACK_GRACE_MS) + { + if (!_isRequestCompleted()) + { + _onMissedCompletionCallback(stoppedAfterStartMs); + } + return; + } + } + + // The delay is not cancelled directly because the watchdog must convert cancellation into cleanup. + await _waitForPollAsync(); + if (!observedStart) + { + waitedForStartMs += UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS; + } + } + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs.meta b/Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs.meta new file mode 100644 index 000000000..60c7f89e6 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileLifecycleWatchdog.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 203b5ab6d45ce4879a5389e422da4d3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs index 1ed44b567..b38c39f5e 100644 --- a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs @@ -94,6 +94,7 @@ public static UnityEditor.PackageManager.PackageInfo PackageInfo public const int COMPILE_START_TIMEOUT_MS = 5000; public const int COMPILE_START_POLL_INTERVAL_MS = 100; + public const int COMPILE_FINISH_MISSED_CALLBACK_GRACE_MS = 500; public const int MAX_SETTINGS_SIZE_BYTES = 1024 * 16; public const string SECURITY_LOG_PREFIX = "[UnityCliLoop Security]"; From cdc4c7f079c474183970d311af56859793872209 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 29 May 2026 01:01:08 +0900 Subject: [PATCH 2/3] Handle compile watchdog fault paths Keep the missed-callback recovery from leaving a pending compile request if the watchdog itself faults, and preserve the force-compile result contract for indeterminate recovery results. --- .../Editor/CompileLifecycleWatchdogTests.cs | 21 ++++++++++ .../Compile/CompileController.cs | 41 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs index 55c1ec32b..800e3e883 100644 --- a/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs +++ b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs @@ -89,6 +89,27 @@ public async Task WatchAsync_WhenCompileNeverStarts_RequestsStartTimeout() Assert.That(missedCallbackCount, Is.EqualTo(0)); } + [Test] + public void WatchAsync_WhenPollingFails_ExposesFaultForControllerRecovery() + { + // Verifies watchdog faults remain observable so the controller can abort the active compile request. + ConstantCompilationState compilationState = new ConstantCompilationState(false); + InvalidOperationException expectedException = new InvalidOperationException("poll failed"); + CompileLifecycleWatchdog watchdog = new CompileLifecycleWatchdog( + compilationState.IsCompiling, + () => false, + () => Task.FromException(expectedException), + _ => { }, + _ => { }, + _ => { }, + _ => { }); + + InvalidOperationException actualException = Assert.ThrowsAsync( + async () => await watchdog.WatchAsync(CancellationToken.None)); + + Assert.That(actualException, Is.SameAs(expectedException)); + } + private sealed class SequenceCompilationState { private readonly bool[] _states; diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs index 3a3805fce..2056fb051 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs @@ -162,7 +162,7 @@ public async Task TryCompileAsync(bool forceRecompile, Cancellati "Unity script compilation request returned.", new { force_recompile = forceRecompile }); - _ = WatchCompileLifecycleAsync(ct); + StartCompileLifecycleWatchdog(ct); VibeLogger.LogInfo( "compile_controller_waiting_for_finish", "Waiting for Unity compilationFinished callback.", @@ -189,6 +189,16 @@ public async Task TryCompileAsync(bool forceRecompile, Cancellati } } + private void StartCompileLifecycleWatchdog(CancellationToken ct) + { + Task watchdogTask = WatchCompileLifecycleAsync(ct); + _ = watchdogTask.ContinueWith( + HandleCompileLifecycleWatchdogFault, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + } + private Task WatchCompileLifecycleAsync(CancellationToken ct) { CompileLifecycleWatchdog watchdog = new CompileLifecycleWatchdog( @@ -212,6 +222,26 @@ private static Task WaitForCompileWatchdogPollAsync() return TimerDelay.Wait(UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS); } + private void HandleCompileLifecycleWatchdogFault(Task faultedTask) + { + UnityEngine.Debug.Assert(faultedTask != null, "faultedTask must not be null"); + UnityEngine.Debug.Assert(faultedTask.IsFaulted, "faultedTask must be faulted"); + + Exception exception = faultedTask.Exception; + UnityEngine.Debug.Assert(exception != null, "faultedTask exception must not be null"); + if (exception != null) + { + UnityEngine.Debug.LogException(exception); + } + + EditorApplication.delayCall += AbortCompileAfterWatchdogFault; + } + + private void AbortCompileAfterWatchdogFault() + { + AbortCompile("Compilation watchdog failed unexpectedly."); + } + private void HandleCompileStartedObserved(int waitedMs) { VibeLogger.LogInfo( @@ -469,14 +499,17 @@ private CompileResult CreateIndeterminateCompileResult(string message) { CompilerMessage[] errors = _compileMessages.Where(m => m.type == CompilerMessageType.Error).ToArray(); CompilerMessage[] warnings = _compileMessages.Where(m => m.type == CompilerMessageType.Warning).ToArray(); + CompilerMessage[] messages = _isForceCompile ? Array.Empty() : _compileMessages.ToArray(); + CompilerMessage[] resultErrors = _isForceCompile ? Array.Empty() : errors; + CompilerMessage[] resultWarnings = _isForceCompile ? Array.Empty() : warnings; return new CompileResult( success: null, errorCount: errors.Length, warningCount: warnings.Length, completedAt: DateTime.Now, - messages: _compileMessages.ToArray(), - errors: errors, - warnings: warnings, + messages: messages, + errors: resultErrors, + warnings: resultWarnings, isIndeterminate: true, message: message ); From 562d1b40f3c76f4367505248260fb49dcc43764a Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 29 May 2026 01:18:47 +0900 Subject: [PATCH 3/3] Bind compile watchdog faults to their request Prevent a delayed watchdog fault from aborting a newer compile by carrying the original compile task through fault handling and delayCall recovery. Add focused coverage for matching and stale compile request identity. --- .../Editor/CompileLifecycleWatchdogTests.cs | 25 +++++++++++++ .../Compile/CompileController.cs | 37 ++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs index 800e3e883..7968c161a 100644 --- a/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs +++ b/Assets/Tests/Editor/CompileLifecycleWatchdogTests.cs @@ -110,6 +110,31 @@ public void WatchAsync_WhenPollingFails_ExposesFaultForControllerRecovery() Assert.That(actualException, Is.SameAs(expectedException)); } + [Test] + public void IsCurrentCompileRequest_WhenTaskMatches_ReturnsTrue() + { + // Verifies watchdog fault recovery accepts the compile request it was created for. + TaskCompletionSource compileTask = new(); + + bool isCurrentCompileRequest = CompileController.IsCurrentCompileRequest(compileTask, compileTask); + + Assert.That(isCurrentCompileRequest, Is.True); + } + + [Test] + public void IsCurrentCompileRequest_WhenTaskDiffers_ReturnsFalse() + { + // Verifies late watchdog faults from older requests cannot abort a newer compile request. + TaskCompletionSource currentCompileTask = new(); + TaskCompletionSource staleCompileTask = new(); + + bool isCurrentCompileRequest = CompileController.IsCurrentCompileRequest( + currentCompileTask, + staleCompileTask); + + Assert.That(isCurrentCompileRequest, Is.False); + } + private sealed class SequenceCompilationState { private readonly bool[] _states; diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs index 2056fb051..0cfbbc775 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs @@ -162,7 +162,7 @@ public async Task TryCompileAsync(bool forceRecompile, Cancellati "Unity script compilation request returned.", new { force_recompile = forceRecompile }); - StartCompileLifecycleWatchdog(ct); + StartCompileLifecycleWatchdog(compileTask, ct); VibeLogger.LogInfo( "compile_controller_waiting_for_finish", "Waiting for Unity compilationFinished callback.", @@ -189,11 +189,13 @@ public async Task TryCompileAsync(bool forceRecompile, Cancellati } } - private void StartCompileLifecycleWatchdog(CancellationToken ct) + private void StartCompileLifecycleWatchdog(TaskCompletionSource compileTask, CancellationToken ct) { + UnityEngine.Debug.Assert(compileTask != null, "compileTask must not be null"); + Task watchdogTask = WatchCompileLifecycleAsync(ct); _ = watchdogTask.ContinueWith( - HandleCompileLifecycleWatchdogFault, + faultedTask => HandleCompileLifecycleWatchdogFault(compileTask, faultedTask), CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); @@ -222,11 +224,19 @@ private static Task WaitForCompileWatchdogPollAsync() return TimerDelay.Wait(UnityCliLoopConstants.COMPILE_START_POLL_INTERVAL_MS); } - private void HandleCompileLifecycleWatchdogFault(Task faultedTask) + private void HandleCompileLifecycleWatchdogFault( + TaskCompletionSource compileTask, + Task faultedTask) { + UnityEngine.Debug.Assert(compileTask != null, "compileTask must not be null"); UnityEngine.Debug.Assert(faultedTask != null, "faultedTask must not be null"); UnityEngine.Debug.Assert(faultedTask.IsFaulted, "faultedTask must be faulted"); + if (!IsCurrentCompileRequest(_currentCompileTask, compileTask)) + { + return; + } + Exception exception = faultedTask.Exception; UnityEngine.Debug.Assert(exception != null, "faultedTask exception must not be null"); if (exception != null) @@ -234,14 +244,29 @@ private void HandleCompileLifecycleWatchdogFault(Task faultedTask) UnityEngine.Debug.LogException(exception); } - EditorApplication.delayCall += AbortCompileAfterWatchdogFault; + EditorApplication.delayCall += () => AbortCompileAfterWatchdogFault(compileTask); } - private void AbortCompileAfterWatchdogFault() + private void AbortCompileAfterWatchdogFault(TaskCompletionSource compileTask) { + UnityEngine.Debug.Assert(compileTask != null, "compileTask must not be null"); + + if (!IsCurrentCompileRequest(_currentCompileTask, compileTask)) + { + return; + } + AbortCompile("Compilation watchdog failed unexpectedly."); } + internal static bool IsCurrentCompileRequest( + TaskCompletionSource currentCompileTask, + TaskCompletionSource compileTask) + { + UnityEngine.Debug.Assert(compileTask != null, "compileTask must not be null"); + return currentCompileTask != null && ReferenceEquals(currentCompileTask, compileTask); + } + private void HandleCompileStartedObserved(int waitedMs) { VibeLogger.LogInfo(