From f0322f45e251ceb226a2b34acac34f0d78efea84 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 6 Jul 2026 20:30:46 +0900 Subject: [PATCH] Extract server recovery tracking service Move recovery task tracking and retry backoff out of the server controller while preserving the public RecoveryTask surface and retry ordering. --- ...CliLoopServerControllerStartupLockTests.cs | 27 ++- ...nityCliLoopServerStartupProtectionTests.cs | 2 + .../UnityCliLoopApplicationRegistration.cs | 2 + .../Server/UnityCliLoopServerController.cs | 162 +--------------- ...ityCliLoopServerRecoveryTrackingService.cs | 179 ++++++++++++++++++ ...iLoopServerRecoveryTrackingService.cs.meta | 11 ++ 6 files changed, 220 insertions(+), 163 deletions(-) create mode 100644 Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs create mode 100644 Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs.meta diff --git a/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs b/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs index 7ab502f587..79636e2c3d 100644 --- a/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs @@ -39,7 +39,7 @@ public void ScheduleStartupRecovery_WhenCalled_ExposesRecoveryTaskBeforeDeferred // Tests that deferred startup recovery exposes its pending task before execution. System.Action scheduledAction = null; bool recoveryExecuted = false; - UnityCliLoopServerControllerService service = CreateControllerService(); + UnityCliLoopServerRecoveryTrackingService service = CreateRecoveryTrackingService(); Task recoveryTask = service.ScheduleStartupRecovery( action => scheduledAction = action, @@ -66,7 +66,7 @@ public void ScheduleStartupRecovery_WhenRecoveryThrowsSynchronously_FaultsTaskAn { // Tests that synchronous startup recovery failures fault and clear the tracked task. System.Action scheduledAction = null; - UnityCliLoopServerControllerService service = CreateControllerService(); + UnityCliLoopServerRecoveryTrackingService service = CreateRecoveryTrackingService(); Task recoveryTask = service.ScheduleStartupRecovery( action => scheduledAction = action, @@ -85,7 +85,7 @@ public async Task ScheduleStartupRecovery_WhenRecoveryIsAsync_KeepsTaskIncomplet // Tests that asynchronous startup recovery remains pending until its restore task completes. System.Action scheduledAction = null; TaskCompletionSource recoveryCompletionSource = new(); - UnityCliLoopServerControllerService service = CreateControllerService(); + UnityCliLoopServerRecoveryTrackingService service = CreateRecoveryTrackingService(); Task recoveryTask = service.ScheduleStartupRecovery( action => scheduledAction = action, @@ -225,8 +225,7 @@ public async Task ExecuteTrackedRecoveryAsync_WhenRecoveryFailsOnce_RetriesAfter // is retried with backoff instead of leaving the server down until the next domain reload. System.Collections.Generic.List recordedWaits = new(); int recoveryAttempts = 0; - UnityCliLoopServerControllerService service = CreateControllerService( - new TestReadinessProbe(), + UnityCliLoopServerRecoveryTrackingService service = CreateRecoveryTrackingService( (delayMilliseconds, ct) => { recordedWaits.Add(delayMilliseconds); @@ -259,8 +258,7 @@ public void ExecuteTrackedRecoveryAsync_WhenRecoveryKeepsFailing_GivesUpAfterAll _sessionFlagsRepository.MarkServerStarted(); System.Collections.Generic.List recordedWaits = new(); int recoveryAttempts = 0; - UnityCliLoopServerControllerService service = CreateControllerService( - new TestReadinessProbe(), + UnityCliLoopServerRecoveryTrackingService service = CreateRecoveryTrackingService( (delayMilliseconds, ct) => { recordedWaits.Add(delayMilliseconds); @@ -291,8 +289,7 @@ public async Task ExecuteTrackedRecoveryAsync_WhenServerManuallyStoppedDuringBac { // Tests that an explicit Stop Server during the retry backoff wins over automatic recovery. int recoveryAttempts = 0; - UnityCliLoopServerControllerService service = CreateControllerService( - new TestReadinessProbe(), + UnityCliLoopServerRecoveryTrackingService service = CreateRecoveryTrackingService( (delayMilliseconds, ct) => { _sessionFlagsRepository.MarkServerManuallyStopped(); @@ -424,6 +421,8 @@ private UnityCliLoopServerControllerService CreateControllerService( waitBeforeReadinessRetryAsync, readinessIdleTimeoutMilliseconds); UnityCliLoopServerStartupProtectionService startupProtectionService = new(); + UnityCliLoopServerRecoveryTrackingService recoveryTrackingService = CreateRecoveryTrackingService( + waitBeforeRecoveryRetryAsync); return new UnityCliLoopServerControllerService( effectiveServerInstanceFactory, effectiveLifecycleRegistry, @@ -434,7 +433,15 @@ private UnityCliLoopServerControllerService CreateControllerService( domainReloadRecoveryUseCase, readinessService, startupProtectionService, - new TestDomainReloadLifecycle(), + recoveryTrackingService, + new TestDomainReloadLifecycle()); + } + + private UnityCliLoopServerRecoveryTrackingService CreateRecoveryTrackingService( + System.Func waitBeforeRecoveryRetryAsync = null) + { + return new UnityCliLoopServerRecoveryTrackingService( + _sessionFlagsRepository, waitBeforeRecoveryRetryAsync); } diff --git a/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs b/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs index 8ca95514e9..a88d45ef75 100644 --- a/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs @@ -127,6 +127,7 @@ private UnityCliLoopServerControllerService CreateControllerService( UnityCliLoopServerReadinessService readinessService = new( lifecycleRegistry, new TestReadinessProbe()); + UnityCliLoopServerRecoveryTrackingService recoveryTrackingService = new(_sessionFlagsRepository); return new UnityCliLoopServerControllerService( serverInstanceFactory, lifecycleRegistry, @@ -137,6 +138,7 @@ private UnityCliLoopServerControllerService CreateControllerService( domainReloadRecoveryUseCase, readinessService, startupProtectionService ?? new UnityCliLoopServerStartupProtectionService(), + recoveryTrackingService, domainReloadLifecycle ?? new TestDomainReloadLifecycle()); } diff --git a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs index c63d81c244..f6ff3c759a 100644 --- a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs +++ b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs @@ -79,6 +79,7 @@ internal UnityCliLoopApplicationServices Register() lifecycleRegistry, firstPartyServerLifecycle); UnityCliLoopServerStartupProtectionService startupProtectionService = new(); + UnityCliLoopServerRecoveryTrackingService recoveryTrackingService = new(sessionFlagsRepository); UnityCliLoopServerControllerService controllerService = new( serverFactory, lifecycleRegistry, @@ -89,6 +90,7 @@ internal UnityCliLoopApplicationServices Register() domainReloadRecoveryUseCase, serverReadinessService, startupProtectionService, + recoveryTrackingService, firstPartyServerLifecycle); UnityCliLoopServerApplicationService applicationService = new(controllerService); UnityCliLoopServerApplicationFacade.RegisterService(applicationService); diff --git a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs index 770c31cd0a..dca28d55ab 100644 --- a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs +++ b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs @@ -34,11 +34,10 @@ private enum ServerStopIntent private readonly DomainReloadRecoveryUseCase _domainReloadRecoveryUseCase; private readonly UnityCliLoopServerReadinessService _readinessService; private readonly UnityCliLoopServerStartupProtectionService _startupProtectionService; + private readonly UnityCliLoopServerRecoveryTrackingService _recoveryTrackingService; private readonly IUnityCliLoopServerDomainReloadLifecycle _domainReloadLifecycle; - private readonly Func _waitBeforeRecoveryRetryAsync; private IUnityCliLoopServerInstance _bridgeServer; private readonly SemaphoreSlim _startupSemaphore = new SemaphoreSlim(1, 1); - private Task _currentRecoveryTask; internal UnityCliLoopServerControllerService( IUnityCliLoopServerInstanceFactory serverInstanceFactory, @@ -50,8 +49,8 @@ internal UnityCliLoopServerControllerService( DomainReloadRecoveryUseCase domainReloadRecoveryUseCase, UnityCliLoopServerReadinessService readinessService, UnityCliLoopServerStartupProtectionService startupProtectionService, - IUnityCliLoopServerDomainReloadLifecycle domainReloadLifecycle, - Func waitBeforeRecoveryRetryAsync = null) + UnityCliLoopServerRecoveryTrackingService recoveryTrackingService, + IUnityCliLoopServerDomainReloadLifecycle domainReloadLifecycle) { System.Diagnostics.Debug.Assert(serverInstanceFactory != null, "serverInstanceFactory must not be null"); System.Diagnostics.Debug.Assert(serverLifecycleRegistry != null, "serverLifecycleRegistry must not be null"); @@ -62,6 +61,7 @@ internal UnityCliLoopServerControllerService( System.Diagnostics.Debug.Assert(domainReloadRecoveryUseCase != null, "domainReloadRecoveryUseCase must not be null"); System.Diagnostics.Debug.Assert(readinessService != null, "readinessService must not be null"); System.Diagnostics.Debug.Assert(startupProtectionService != null, "startupProtectionService must not be null"); + System.Diagnostics.Debug.Assert(recoveryTrackingService != null, "recoveryTrackingService must not be null"); System.Diagnostics.Debug.Assert(domainReloadLifecycle != null, "domainReloadLifecycle must not be null"); _serverInstanceFactory = serverInstanceFactory ?? throw new ArgumentNullException(nameof(serverInstanceFactory)); @@ -73,8 +73,8 @@ internal UnityCliLoopServerControllerService( _domainReloadRecoveryUseCase = domainReloadRecoveryUseCase ?? throw new ArgumentNullException(nameof(domainReloadRecoveryUseCase)); _readinessService = readinessService ?? throw new ArgumentNullException(nameof(readinessService)); _startupProtectionService = startupProtectionService ?? throw new ArgumentNullException(nameof(startupProtectionService)); + _recoveryTrackingService = recoveryTrackingService ?? throw new ArgumentNullException(nameof(recoveryTrackingService)); _domainReloadLifecycle = domainReloadLifecycle ?? throw new ArgumentNullException(nameof(domainReloadLifecycle)); - _waitBeforeRecoveryRetryAsync = waitBeforeRecoveryRetryAsync ?? TimerDelay.Wait; } private bool IsBackgroundUnityProcess() @@ -104,7 +104,7 @@ internal void RegisterRecoveredServer(IUnityCliLoopServerInstance server) /// /// Current recovery task. Can be awaited by other components to ensure recovery completes first. /// - public Task RecoveryTask => _currentRecoveryTask; + public Task RecoveryTask => _recoveryTrackingService.RecoveryTask; public void InitializeForEditorStartup() { @@ -132,75 +132,11 @@ public void InitializeForEditorStartup() // Recovery binds the project IPC endpoint and may touch config files, so keep it off the // synchronous Editor startup path while preserving automatic startup. - ScheduleStartupRecovery( + _recoveryTrackingService.ScheduleStartupRecovery( action => EditorApplication.delayCall += () => action(), RestoreServerStateIfNeeded); } - internal Task ScheduleStartupRecovery( - Action scheduleDelayCall, - Func restoreServerState) - { - Debug.Assert(scheduleDelayCall != null, "scheduleDelayCall must not be null"); - Debug.Assert(restoreServerState != null, "restoreServerState must not be null"); - - TaskCompletionSource scheduledRecoveryCompletionSource = new(); - _currentRecoveryTask = scheduledRecoveryCompletionSource.Task; - - scheduleDelayCall(() => - { - Task restoreTask; - try - { - restoreTask = restoreServerState(); - } - catch (Exception ex) - { - CompleteScheduledStartupRecovery(Task.FromException(ex), scheduledRecoveryCompletionSource); - return; - } - - if (restoreTask.IsCompleted) - { - CompleteScheduledStartupRecovery(restoreTask, scheduledRecoveryCompletionSource); - return; - } - - _ = restoreTask.ContinueWith(task => - { - CompleteScheduledStartupRecovery(task, scheduledRecoveryCompletionSource); - }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()); - }); - - return scheduledRecoveryCompletionSource.Task; - } - - private void CompleteScheduledStartupRecovery( - Task restoreTask, - TaskCompletionSource scheduledRecoveryCompletionSource) - { - if (ReferenceEquals(_currentRecoveryTask, scheduledRecoveryCompletionSource.Task)) - { - _currentRecoveryTask = null; - } - - if (restoreTask.IsCanceled) - { - scheduledRecoveryCompletionSource.SetCanceled(); - return; - } - - if (restoreTask.IsFaulted) - { - VibeLogger.LogError("server_startup_restore_failed", - $"Failed to restore server: {restoreTask.Exception?.GetBaseException().Message}"); - scheduledRecoveryCompletionSource.SetException(restoreTask.Exception.GetBaseException()); - return; - } - - scheduledRecoveryCompletionSource.SetResult(true); - } - public void StartServer() { // Why: an async void body lets exceptions (e.g. a readiness probe failure thrown by @@ -335,7 +271,7 @@ internal void OnBeforeAssemblyReload() /// private void OnAfterAssemblyReload() { - ScheduleTrackedRecovery(() => ExecuteAfterDomainReloadRecoveryAsync(CancellationToken.None)); + _recoveryTrackingService.ScheduleTrackedRecovery(() => ExecuteAfterDomainReloadRecoveryAsync(CancellationToken.None)); } private async Task ExecuteAfterDomainReloadRecoveryAsync(CancellationToken cancellationToken) @@ -430,90 +366,10 @@ private void OnServerLoopUnexpectedlyExited() // within the 5-second protection window after a successful start _startupProtectionService.ClearStartupProtection(); - ScheduleTrackedRecovery(() => StartRecoveryIfNeededAsync(false, CancellationToken.None)); + _recoveryTrackingService.ScheduleTrackedRecovery(() => StartRecoveryIfNeededAsync(false, CancellationToken.None)); }; } - private Task ScheduleTrackedRecovery(Func recoveryAction) - { - Debug.Assert(recoveryAction != null, "recoveryAction must not be null"); - - Task recoveryTask = ExecuteTrackedRecoveryAsync(recoveryAction); - _currentRecoveryTask = recoveryTask; - _ = ClearTrackedRecoveryWhenCompleteAsync(recoveryTask); - return recoveryTask; - } - - /// - /// Runs a recovery action, retrying with backoff so one transient failure - /// (e.g. readiness timeout during a heavy import) does not leave the server - /// down until the next domain reload. - /// - internal async Task ExecuteTrackedRecoveryAsync(Func recoveryAction) - { - Debug.Assert(recoveryAction != null, "recoveryAction must not be null"); - - int failedAttemptCount = 0; - while (true) - { - try - { - await recoveryAction(); - return; - } - catch (Exception ex) - { - if (failedAttemptCount >= UnityCliLoopServerConfig.RECOVERY_RETRY_DELAYS_MS.Length) - { - string message = $"Unity CLI Loop server recovery failed before the bridge became ready. {ex.GetBaseException().Message}"; - // Why: the thrown exception ends in an unobserved task and VibeLogger is - // compiled out without ULOOP_DEBUG, so without this console entry an - // unrecoverable server (uloop unreachable) would be completely silent. - Debug.LogError($"[{UnityCliLoopConstants.PROJECT_NAME}] {message}"); - VibeLogger.LogError( - "server_recovery_failed", - message); - _sessionFlagsRepository.ClearServerSession(); - throw new InvalidOperationException(message, ex); - } - - int delayMilliseconds = UnityCliLoopServerConfig.RECOVERY_RETRY_DELAYS_MS[failedAttemptCount]; - failedAttemptCount++; - VibeLogger.LogWarning( - "server_recovery_retry_scheduled", - $"Recovery attempt {failedAttemptCount} failed; retrying in {delayMilliseconds}ms. {ex.GetBaseException().Message}"); - await _waitBeforeRecoveryRetryAsync(delayMilliseconds, CancellationToken.None); - - // Why: an explicit Stop Server issued during the backoff must win over - // automatic recovery, otherwise the retry would silently restart the server. - if (_sessionFlagsRepository.GetIsServerManuallyStopped()) - { - VibeLogger.LogInfo( - "server_recovery_retry_abandoned", - "Recovery retry abandoned because the server was manually stopped."); - return; - } - } - } - } - - private async Task ClearTrackedRecoveryWhenCompleteAsync(Task recoveryTask) - { - Debug.Assert(recoveryTask != null, "recoveryTask must not be null"); - - try - { - await recoveryTask; - } - finally - { - if (ReferenceEquals(_currentRecoveryTask, recoveryTask)) - { - _currentRecoveryTask = null; - } - } - } - /// /// Centralized, coalesced recovery start. /// Attempts recovery on the project IPC endpoint for up to 5 seconds. diff --git a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs new file mode 100644 index 0000000000..d763b817ef --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs @@ -0,0 +1,179 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Tracks active server recovery tasks and applies retry backoff for transient recovery failures. + /// + internal sealed class UnityCliLoopServerRecoveryTrackingService + { + private readonly ISessionFlagsRepository _sessionFlagsRepository; + private readonly Func _waitBeforeRecoveryRetryAsync; + private Task _currentRecoveryTask; + + internal UnityCliLoopServerRecoveryTrackingService( + ISessionFlagsRepository sessionFlagsRepository, + Func waitBeforeRecoveryRetryAsync = null) + { + Debug.Assert(sessionFlagsRepository != null, "sessionFlagsRepository must not be null"); + + _sessionFlagsRepository = sessionFlagsRepository ?? throw new ArgumentNullException(nameof(sessionFlagsRepository)); + _waitBeforeRecoveryRetryAsync = waitBeforeRecoveryRetryAsync ?? TimerDelay.Wait; + } + + /// + /// Current recovery task. Can be awaited by other components to ensure recovery completes first. + /// + internal Task RecoveryTask => _currentRecoveryTask; + + internal Task ScheduleStartupRecovery( + Action scheduleDelayCall, + Func restoreServerState) + { + Debug.Assert(scheduleDelayCall != null, "scheduleDelayCall must not be null"); + Debug.Assert(restoreServerState != null, "restoreServerState must not be null"); + + TaskCompletionSource scheduledRecoveryCompletionSource = new(); + _currentRecoveryTask = scheduledRecoveryCompletionSource.Task; + + scheduleDelayCall(() => + { + Task restoreTask; + try + { + restoreTask = restoreServerState(); + } + catch (Exception ex) + { + CompleteScheduledStartupRecovery(Task.FromException(ex), scheduledRecoveryCompletionSource); + return; + } + + if (restoreTask.IsCompleted) + { + CompleteScheduledStartupRecovery(restoreTask, scheduledRecoveryCompletionSource); + return; + } + + _ = restoreTask.ContinueWith(task => + { + CompleteScheduledStartupRecovery(task, scheduledRecoveryCompletionSource); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()); + }); + + return scheduledRecoveryCompletionSource.Task; + } + + private void CompleteScheduledStartupRecovery( + Task restoreTask, + TaskCompletionSource scheduledRecoveryCompletionSource) + { + if (ReferenceEquals(_currentRecoveryTask, scheduledRecoveryCompletionSource.Task)) + { + _currentRecoveryTask = null; + } + + if (restoreTask.IsCanceled) + { + scheduledRecoveryCompletionSource.SetCanceled(); + return; + } + + if (restoreTask.IsFaulted) + { + VibeLogger.LogError("server_startup_restore_failed", + $"Failed to restore server: {restoreTask.Exception?.GetBaseException().Message}"); + scheduledRecoveryCompletionSource.SetException(restoreTask.Exception.GetBaseException()); + return; + } + + scheduledRecoveryCompletionSource.SetResult(true); + } + + internal Task ScheduleTrackedRecovery(Func recoveryAction) + { + Debug.Assert(recoveryAction != null, "recoveryAction must not be null"); + + Task recoveryTask = ExecuteTrackedRecoveryAsync(recoveryAction); + _currentRecoveryTask = recoveryTask; + _ = ClearTrackedRecoveryWhenCompleteAsync(recoveryTask); + return recoveryTask; + } + + /// + /// Runs a recovery action, retrying with backoff so one transient failure + /// (e.g. readiness timeout during a heavy import) does not leave the server + /// down until the next domain reload. + /// + internal async Task ExecuteTrackedRecoveryAsync(Func recoveryAction) + { + Debug.Assert(recoveryAction != null, "recoveryAction must not be null"); + + int failedAttemptCount = 0; + while (true) + { + try + { + await recoveryAction(); + return; + } + catch (Exception ex) + { + if (failedAttemptCount >= UnityCliLoopServerConfig.RECOVERY_RETRY_DELAYS_MS.Length) + { + string message = $"Unity CLI Loop server recovery failed before the bridge became ready. {ex.GetBaseException().Message}"; + // Why: the thrown exception ends in an unobserved task and VibeLogger is + // compiled out without ULOOP_DEBUG, so without this console entry an + // unrecoverable server (uloop unreachable) would be completely silent. + Debug.LogError($"[{UnityCliLoopConstants.PROJECT_NAME}] {message}"); + VibeLogger.LogError( + "server_recovery_failed", + message); + _sessionFlagsRepository.ClearServerSession(); + throw new InvalidOperationException(message, ex); + } + + int delayMilliseconds = UnityCliLoopServerConfig.RECOVERY_RETRY_DELAYS_MS[failedAttemptCount]; + failedAttemptCount++; + VibeLogger.LogWarning( + "server_recovery_retry_scheduled", + $"Recovery attempt {failedAttemptCount} failed; retrying in {delayMilliseconds}ms. {ex.GetBaseException().Message}"); + await _waitBeforeRecoveryRetryAsync(delayMilliseconds, CancellationToken.None); + + // Why: an explicit Stop Server issued during the backoff must win over + // automatic recovery, otherwise the retry would silently restart the server. + if (_sessionFlagsRepository.GetIsServerManuallyStopped()) + { + VibeLogger.LogInfo( + "server_recovery_retry_abandoned", + "Recovery retry abandoned because the server was manually stopped."); + return; + } + } + } + } + + private async Task ClearTrackedRecoveryWhenCompleteAsync(Task recoveryTask) + { + Debug.Assert(recoveryTask != null, "recoveryTask must not be null"); + + try + { + await recoveryTask; + } + finally + { + if (ReferenceEquals(_currentRecoveryTask, recoveryTask)) + { + _currentRecoveryTask = null; + } + } + } + } +} diff --git a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs.meta b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs.meta new file mode 100644 index 0000000000..2c1e5bcc00 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerRecoveryTrackingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88018a714b234101b66f17dc78ed67e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: