From a7c6daa5b58b49346cb5ef00ff7fe738aedd0fea Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 19 May 2026 09:19:29 +0900 Subject: [PATCH 1/2] Move editor runtime flags to SessionState Store reload and recovery flags in Unity SessionState so stale runtime state does not survive Editor restarts. Keep UserSettings JSON focused on persistent preferences, strip legacy transient keys, and preserve the first reload that crosses the migration. --- .../DomainReloadDetectionServiceTests.cs | 105 +++++++++--- .../DomainReloadRecoveryUseCaseTests.cs | 56 +++---- .../Editor/StaticFacadeStateGuardTests.cs | 1 + ...liLoopEditorSessionStateRepositoryTests.cs | 75 +++++++++ ...pEditorSessionStateRepositoryTests.cs.meta | 11 ++ ...ityCliLoopEditorSessionStateTestFactory.cs | 61 +++++++ ...iLoopEditorSessionStateTestFactory.cs.meta | 11 ++ ...UnityCliLoopEditorSettingsRecoveryTests.cs | 24 ++- ...LoopPackageRemovalSettingsResetterTests.cs | 10 +- ...CliLoopServerControllerStartupLockTests.cs | 33 ++-- ...nityCliLoopServerStartupProtectionTests.cs | 62 ++++--- .../Application/SessionRecoveryService.cs | 22 +-- .../UnityCliLoopServerStartupService.cs | 12 +- .../UseCases/DomainReloadRecoveryUseCase.cs | 12 +- .../UnityCliLoopApplicationRegistration.cs | 18 ++- .../UnityCliLoopEditorBootstrapper.cs | 4 +- .../UnityCliLoopEditorSessionStateService.cs | 152 ++++++++++++++++++ ...tyCliLoopEditorSessionStateService.cs.meta | 11 ++ .../Domain/UnityCliLoopEditorSettingsData.cs | 6 - .../UnityCliLoopEditorSettingsService.cs | 78 --------- .../DomainReloadDetectionFileService.cs | 96 ++++++----- .../Server/UnityCliLoopServerController.cs | 40 ++--- ...tyCliLoopEditorLegacySessionStateReader.cs | 101 ++++++++++++ ...LoopEditorLegacySessionStateReader.cs.meta | 11 ++ ...nityCliLoopEditorSessionStateRepository.cs | 90 +++++++++++ ...liLoopEditorSessionStateRepository.cs.meta | 11 ++ .../UnityCliLoopEditorSettingsRepository.cs | 127 +-------------- .../UnityCliLoopBridgeServer.cs | 46 +----- .../Presentation/PresentationEditorStartup.cs | 6 +- .../UnityCliLoopSettingsWindow.cs | 26 ++- 30 files changed, 868 insertions(+), 450 deletions(-) create mode 100644 Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs create mode 100644 Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs.meta create mode 100644 Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs create mode 100644 Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs.meta create mode 100644 Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs create mode 100644 Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs create mode 100644 Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs create mode 100644 Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs.meta diff --git a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs index 977ed3a86..1843250ae 100644 --- a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs +++ b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs @@ -11,25 +11,26 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class DomainReloadDetectionServiceTests { - private UnityCliLoopEditorSettingsData _originalSettings; - private UnityCliLoopEditorSettingsService _editorSettingsService; + private UnityCliLoopEditorSessionStateService _sessionStateService; + private UnityCliLoopEditorSessionStateSnapshot _originalSessionState; private IDomainReloadDetectionService _domainReloadDetectionService; private ServerReadinessStateStore _stateStore; [SetUp] public void SetUp() { - _editorSettingsService = UnityCliLoopEditorSettingsTestFactory.CreateService(); - _originalSettings = CloneSettings(_editorSettingsService.GetSettings()); + _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); + _originalSessionState = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); + _sessionStateService.ClearAll(); _stateStore = CreateTestStateStore(); - _domainReloadDetectionService = new DomainReloadDetectionFileService(_editorSettingsService, _stateStore); + _domainReloadDetectionService = new DomainReloadDetectionFileService(_sessionStateService, _stateStore); UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); } [TearDown] public void TearDown() { - _editorSettingsService.SaveSettings(_originalSettings); + _originalSessionState.Restore(_sessionStateService); UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); _stateStore.Delete(); } @@ -43,34 +44,77 @@ public void RollbackDomainReloadStart_ClearsTemporaryFlagsProviderStateAndPublis _domainReloadDetectionService.StartDomainReload(correlationId, true); - UnityCliLoopEditorSettingsData startedSettings = _editorSettingsService.GetSettings(); - Assert.That(startedSettings.isServerRunning, Is.True); - Assert.That(startedSettings.isAfterCompile, Is.True); - Assert.That(startedSettings.isDomainReloadInProgress, Is.True); - Assert.That(startedSettings.isReconnecting, Is.True); - Assert.That(startedSettings.showReconnectingUI, Is.True); - Assert.That(startedSettings.showPostCompileReconnectingUI, Is.True); + Assert.That(_sessionStateService.GetIsServerRunning(), Is.True); + Assert.That(_sessionStateService.GetIsAfterCompile(), Is.True); + Assert.That(_sessionStateService.GetIsDomainReloadInProgress(), Is.True); + Assert.That(_sessionStateService.GetIsReconnecting(), Is.True); + Assert.That(_sessionStateService.GetShowReconnectingUI(), Is.True); + Assert.That(_sessionStateService.GetShowPostCompileReconnectingUI(), Is.True); Assert.That(provider.IsDomainReloadInProgress(), Is.True); _domainReloadDetectionService.RollbackDomainReloadStart(correlationId); - UnityCliLoopEditorSettingsData rolledBackSettings = _editorSettingsService.GetSettings(); - Assert.That(rolledBackSettings.isServerRunning, Is.True); - Assert.That(rolledBackSettings.isAfterCompile, Is.False); - Assert.That(rolledBackSettings.isDomainReloadInProgress, Is.False); - Assert.That(rolledBackSettings.isReconnecting, Is.False); - Assert.That(rolledBackSettings.showReconnectingUI, Is.False); - Assert.That(rolledBackSettings.showPostCompileReconnectingUI, Is.False); + Assert.That(_sessionStateService.GetIsServerRunning(), Is.True); + Assert.That(_sessionStateService.GetIsAfterCompile(), Is.False); + Assert.That(_sessionStateService.GetIsDomainReloadInProgress(), Is.False); + Assert.That(_sessionStateService.GetIsReconnecting(), Is.False); + Assert.That(_sessionStateService.GetShowReconnectingUI(), Is.False); + Assert.That(_sessionStateService.GetShowPostCompileReconnectingUI(), Is.False); Assert.That(provider.IsDomainReloadInProgress(), Is.False); ServerReadinessState state = _stateStore.Read(); Assert.That(state.Phase, Is.EqualTo("failed")); Assert.That(state.LastError, Is.Not.Empty); } - private static UnityCliLoopEditorSettingsData CloneSettings(UnityCliLoopEditorSettingsData settings) + [Test] + public void CompleteDomainReload_WhenLegacyReloadStateExists_MigratesRecoveryFlagsToSessionState() + { + // Verifies that the first reload after migration preserves old JSON recovery state. + UnityCliLoopEditorLegacySessionState legacySessionState = new( + isServerRunning: true, + isAfterCompile: true, + isDomainReloadInProgress: true, + isReconnecting: true, + showReconnectingUI: true, + showPostCompileReconnectingUI: true); + _domainReloadDetectionService = new DomainReloadDetectionFileService( + _sessionStateService, + _stateStore, + new TestLegacySessionStateReader(legacySessionState)); + + _domainReloadDetectionService.CompleteDomainReload("test-correlation"); + + Assert.That(_sessionStateService.GetIsServerRunning(), Is.True); + Assert.That(_sessionStateService.GetIsAfterCompile(), Is.True); + Assert.That(_sessionStateService.GetIsDomainReloadInProgress(), Is.False); + Assert.That(_sessionStateService.GetIsReconnecting(), Is.True); + Assert.That(_sessionStateService.GetShowReconnectingUI(), Is.True); + Assert.That(_sessionStateService.GetShowPostCompileReconnectingUI(), Is.True); + ServerReadinessState state = _stateStore.Read(); + Assert.That(state.Phase, Is.EqualTo("recovering")); + } + + [Test] + public void CompleteDomainReload_WhenLegacyStateOnlySaysRunning_IgnoresStaleRunningFlag() { - string json = UnityEngine.JsonUtility.ToJson(settings); - return UnityEngine.JsonUtility.FromJson(json); + // Verifies that stale running-only JSON does not opt into recovery after the migration. + UnityCliLoopEditorLegacySessionState legacySessionState = new( + isServerRunning: true, + isAfterCompile: false, + isDomainReloadInProgress: false, + isReconnecting: false, + showReconnectingUI: false, + showPostCompileReconnectingUI: false); + _domainReloadDetectionService = new DomainReloadDetectionFileService( + _sessionStateService, + _stateStore, + new TestLegacySessionStateReader(legacySessionState)); + + _domainReloadDetectionService.CompleteDomainReload("test-correlation"); + + Assert.That(_sessionStateService.GetIsServerRunning(), Is.False); + ServerReadinessState state = _stateStore.Read(); + Assert.That(state.Phase, Is.EqualTo("stopped")); } private static ServerReadinessStateStore CreateTestStateStore() @@ -81,5 +125,20 @@ private static ServerReadinessStateStore CreateTestStateStore() System.Guid.NewGuid().ToString("N")); return new ServerReadinessStateStore(projectRoot); } + + private sealed class TestLegacySessionStateReader : IUnityCliLoopEditorLegacySessionStateReader + { + private readonly UnityCliLoopEditorLegacySessionState _legacySessionState; + + internal TestLegacySessionStateReader(UnityCliLoopEditorLegacySessionState legacySessionState) + { + _legacySessionState = legacySessionState; + } + + public UnityCliLoopEditorLegacySessionState Read() + { + return _legacySessionState; + } + } } } diff --git a/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs b/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs index 190728d9e..0c1380657 100644 --- a/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs +++ b/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs @@ -15,35 +15,25 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class DomainReloadRecoveryUseCaseTests { - private bool _originalIsServerRunning; - private UnityCliLoopEditorSettingsService _editorSettingsService; + private UnityCliLoopEditorSessionStateService _sessionStateService; + private UnityCliLoopEditorSessionStateSnapshot _originalSessionState; private IDomainReloadDetectionService _domainReloadDetectionService; private ServerReadinessStateStore _stateStore; [SetUp] public void SetUp() { - // Save original session state - _editorSettingsService = UnityCliLoopEditorSettingsTestFactory.CreateService(); - _originalIsServerRunning = _editorSettingsService.GetIsServerRunning(); + _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); + _originalSessionState = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); + _sessionStateService.ClearAll(); _stateStore = CreateTestStateStore(); - _domainReloadDetectionService = new DomainReloadDetectionFileService(_editorSettingsService, _stateStore); + _domainReloadDetectionService = new DomainReloadDetectionFileService(_sessionStateService, _stateStore); } [TearDown] public void TearDown() { - // Restore original session state - _editorSettingsService.UpdateSettings(s => s with - { - isServerRunning = _originalIsServerRunning, - isAfterCompile = false, - isDomainReloadInProgress = false, - isReconnecting = false, - showReconnectingUI = false, - showPostCompileReconnectingUI = false - }); - + _originalSessionState.Restore(_sessionStateService); _stateStore.Delete(); } @@ -51,51 +41,51 @@ public void TearDown() public void ExecuteBeforeDomainReload_ShouldUseSessionState_WhenServerInstanceIsNull() { // Arrange - _editorSettingsService.SetIsServerRunning(true); + _sessionStateService.SetIsServerRunning(true); DomainReloadRecoveryUseCase useCase = CreateUseCase( _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); // Act ServiceResult result = useCase.ExecuteBeforeDomainReload(null); // Assert Assert.IsTrue(result.Success, "ExecuteBeforeDomainReload should succeed"); - Assert.IsTrue(_editorSettingsService.GetIsAfterCompile(), "IsAfterCompile should be set to true"); + Assert.IsTrue(_sessionStateService.GetIsAfterCompile(), "IsAfterCompile should be set to true"); } [Test] public void ExecuteBeforeDomainReload_ShouldNotSaveState_WhenBothInstanceAndSessionAreNotRunning() { // Arrange - _editorSettingsService.SetIsServerRunning(false); - _editorSettingsService.UpdateSettings(s => s with { isAfterCompile = false }); + _sessionStateService.SetIsServerRunning(false); + _sessionStateService.SetIsAfterCompile(false); DomainReloadRecoveryUseCase useCase = CreateUseCase( _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); // Act ServiceResult result = useCase.ExecuteBeforeDomainReload(null); // Assert Assert.IsTrue(result.Success, "ExecuteBeforeDomainReload should succeed"); - Assert.IsFalse(_editorSettingsService.GetIsAfterCompile(), "IsAfterCompile should remain false when server was not running"); + Assert.IsFalse(_sessionStateService.GetIsAfterCompile(), "IsAfterCompile should remain false when server was not running"); } [Test] public void ExecuteBeforeDomainReload_ShouldPreferInstanceState_WhenInstanceIsRunning() { // Arrange - _editorSettingsService.SetIsServerRunning(true); + _sessionStateService.SetIsServerRunning(true); TestServerInstance server = new TestServerInstance(); server.StartServer(); DomainReloadRecoveryUseCase useCase = CreateUseCase( _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); // Act ServiceResult result = useCase.ExecuteBeforeDomainReload(server); @@ -111,7 +101,7 @@ public void ExecuteBeforeDomainReload_ShouldPreferInstanceState_WhenInstanceIsRu public void CompleteDomainReload_WhenServerWasNotRunning_ShouldPublishStoppedState() { // Verifies that a domain reload with no server to recover does not leave CLI waiters in recovering state. - _editorSettingsService.SetIsServerRunning(false); + _sessionStateService.SetIsServerRunning(false); _domainReloadDetectionService.StartDomainReload("test-correlation", serverIsRunning: false); _domainReloadDetectionService.CompleteDomainReload("test-correlation"); @@ -124,13 +114,13 @@ public void CompleteDomainReload_WhenServerWasNotRunning_ShouldPublishStoppedSta public async Task RestoreServerStateIfNeededAsync_WhenRecoveryDoesNotStartServer_ShouldFail() { // Verifies that recovery is only reported as successful after a running server instance exists. - _editorSettingsService.SetIsServerRunning(true); - _editorSettingsService.UpdateSettings(s => s with { isAfterCompile = false }); + _sessionStateService.SetIsServerRunning(true); + _sessionStateService.SetIsAfterCompile(false); TestRecoveryCoordinator recoveryCoordinator = new(); SessionRecoveryService service = new( recoveryCoordinator, _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); ValidationResult result = await service.RestoreServerStateIfNeededAsync(CancellationToken.None); @@ -151,18 +141,18 @@ private static ServerReadinessStateStore CreateTestStateStore() private static DomainReloadRecoveryUseCase CreateUseCase( IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService) + UnityCliLoopEditorSessionStateService sessionStateService) { TestRecoveryCoordinator recoveryCoordinator = new(); SessionRecoveryService sessionRecoveryService = new SessionRecoveryService( recoveryCoordinator, domainReloadDetectionService, - editorSettingsService); + sessionStateService); return new DomainReloadRecoveryUseCase( sessionRecoveryService, domainReloadDetectionService, - editorSettingsService); + sessionStateService); } /// diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs index 250e999e4..fe07ab7e4 100644 --- a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs @@ -50,6 +50,7 @@ public sealed class StaticFacadeStateGuardTests "Packages/src/Editor/Application/SessionRecoveryService.cs", "Packages/src/Editor/Application/UseCases/SkillSetupUseCase.cs", "Packages/src/Editor/Domain/UnityCliLoopEditorSettingsService.cs", + "Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs", "Packages/src/Editor/Domain/ToolSettingsService.cs", "Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs" }; diff --git a/Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs new file mode 100644 index 000000000..7d95f7488 --- /dev/null +++ b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs @@ -0,0 +1,75 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Test fixture that verifies Unity CLI Loop Editor SessionState behavior. + /// + public sealed class UnityCliLoopEditorSessionStateRepositoryTests + { + private UnityCliLoopEditorSessionStateSnapshot _originalSnapshot; + private UnityCliLoopEditorSessionStateService _sessionStateService; + + [SetUp] + public void SetUp() + { + _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); + _originalSnapshot = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); + _sessionStateService.ClearAll(); + } + + [TearDown] + public void TearDown() + { + _originalSnapshot.Restore(_sessionStateService); + } + + [Test] + public void GetFlags_WhenSessionStateIsEmpty_ReturnsFalseDefaults() + { + // Verifies that transient runtime flags do not opt into stale recovery by default. + Assert.That(_sessionStateService.GetIsServerRunning(), Is.False); + Assert.That(_sessionStateService.GetIsAfterCompile(), Is.False); + Assert.That(_sessionStateService.GetIsDomainReloadInProgress(), Is.False); + Assert.That(_sessionStateService.GetIsReconnecting(), Is.False); + Assert.That(_sessionStateService.GetShowReconnectingUI(), Is.False); + Assert.That(_sessionStateService.GetShowPostCompileReconnectingUI(), Is.False); + } + + [Test] + public void GetFlags_WhenServiceAndRepositoryAreRecreated_ReadsExistingSessionValues() + { + // Verifies that SessionState survives service/repository recreation within the same Editor session. + _sessionStateService.MarkDomainReloadStarted(serverIsRunning: true); + + UnityCliLoopEditorSessionStateService recreatedService = + UnityCliLoopEditorSessionStateTestFactory.CreateService(); + + Assert.That(recreatedService.GetIsServerRunning(), Is.True); + Assert.That(recreatedService.GetIsAfterCompile(), Is.True); + Assert.That(recreatedService.GetIsDomainReloadInProgress(), Is.True); + Assert.That(recreatedService.GetIsReconnecting(), Is.True); + Assert.That(recreatedService.GetShowReconnectingUI(), Is.True); + Assert.That(recreatedService.GetShowPostCompileReconnectingUI(), Is.True); + } + + [Test] + public void ClearAll_WhenFlagsAreSet_ClearsEveryTransientFlag() + { + // Verifies that test and shutdown cleanup can reset all runtime SessionState flags together. + _sessionStateService.MarkDomainReloadStarted(serverIsRunning: true); + + _sessionStateService.ClearAll(); + + Assert.That(_sessionStateService.GetIsServerRunning(), Is.False); + Assert.That(_sessionStateService.GetIsAfterCompile(), Is.False); + Assert.That(_sessionStateService.GetIsDomainReloadInProgress(), Is.False); + Assert.That(_sessionStateService.GetIsReconnecting(), Is.False); + Assert.That(_sessionStateService.GetShowReconnectingUI(), Is.False); + Assert.That(_sessionStateService.GetShowPostCompileReconnectingUI(), Is.False); + } + } +} diff --git a/Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs.meta b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs.meta new file mode 100644 index 000000000..e083ce730 --- /dev/null +++ b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateRepositoryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fd0d352681f742ff81a7178fa510cef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs new file mode 100644 index 000000000..b7b20ee24 --- /dev/null +++ b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs @@ -0,0 +1,61 @@ +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Creates editor session-state services and snapshots for tests that touch live Unity SessionState. + /// + internal static class UnityCliLoopEditorSessionStateTestFactory + { + internal static UnityCliLoopEditorSessionStateService CreateService() + { + return new UnityCliLoopEditorSessionStateService(new UnityCliLoopEditorSessionStateRepository()); + } + + internal static UnityCliLoopEditorSessionStateSnapshot CaptureSnapshot( + UnityCliLoopEditorSessionStateService service) + { + return UnityCliLoopEditorSessionStateSnapshot.Capture(service); + } + } + + /// + /// Captures production SessionState values so tests can restore the live Editor session. + /// + internal readonly struct UnityCliLoopEditorSessionStateSnapshot + { + private readonly bool _isServerRunning; + private readonly bool _isAfterCompile; + private readonly bool _isDomainReloadInProgress; + private readonly bool _isReconnecting; + private readonly bool _showReconnectingUI; + private readonly bool _showPostCompileReconnectingUI; + + private UnityCliLoopEditorSessionStateSnapshot(UnityCliLoopEditorSessionStateService service) + { + _isServerRunning = service.GetIsServerRunning(); + _isAfterCompile = service.GetIsAfterCompile(); + _isDomainReloadInProgress = service.GetIsDomainReloadInProgress(); + _isReconnecting = service.GetIsReconnecting(); + _showReconnectingUI = service.GetShowReconnectingUI(); + _showPostCompileReconnectingUI = service.GetShowPostCompileReconnectingUI(); + } + + internal static UnityCliLoopEditorSessionStateSnapshot Capture( + UnityCliLoopEditorSessionStateService service) + { + return new UnityCliLoopEditorSessionStateSnapshot(service); + } + + internal void Restore(UnityCliLoopEditorSessionStateService service) + { + service.SetIsServerRunning(_isServerRunning); + service.SetIsAfterCompile(_isAfterCompile); + service.SetIsDomainReloadInProgress(_isDomainReloadInProgress); + service.SetIsReconnecting(_isReconnecting); + service.SetShowReconnectingUI(_showReconnectingUI); + service.SetShowPostCompileReconnectingUI(_showPostCompileReconnectingUI); + } + } +} diff --git a/Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs.meta b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs.meta new file mode 100644 index 000000000..aa969426b --- /dev/null +++ b/Assets/Tests/Editor/UnityCliLoopEditorSessionStateTestFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00e77cfcba0df45c393fdc21e6a5190a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs b/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs index 70485a4a6..d5ca84981 100644 --- a/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs @@ -34,6 +34,8 @@ public class UnityCliLoopEditorSettingsRecoveryTests private string _tempFileContent; private UnityCliLoopEditorSettingsService _editorSettingsService; private UnityCliLoopEditorSettingsRepository _editorSettingsRepository; + private UnityCliLoopEditorSessionStateService _sessionStateService; + private UnityCliLoopEditorSessionStateSnapshot _originalSessionState; [SetUp] public void SetUp() @@ -62,6 +64,9 @@ public void SetUp() _editorSettingsService = UnityCliLoopEditorSettingsTestFactory.CreateServiceWithRepository(out _editorSettingsRepository); _editorSettingsRepository.InvalidateCache(); + _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); + _originalSessionState = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); + _sessionStateService.ClearAll(); } [TearDown] @@ -71,6 +76,7 @@ public void TearDown() RestoreFile(LegacySettingsFilePath, _legacySettingsFileExisted, _legacySettingsFileContent); RestoreFile(BackupFilePath, _backupFileExisted, _backupFileContent); RestoreFile(TempFilePath, _tempFileExisted, _tempFileContent); + _originalSessionState.Restore(_sessionStateService); _editorSettingsRepository.InvalidateCache(); } @@ -237,6 +243,12 @@ public void RecoverSettingsFileIfNeeded_WhenLegacyPortFieldsExist_ShouldRemoveTh "\"serverTransportKind\":\"tcp\"," + "\"projectRootPath\":\"/stale/project\"," + "\"serverSessionId\":\"stale-session\"," + + "\"isServerRunning\":true," + + "\"isAfterCompile\":true," + + "\"isDomainReloadInProgress\":true," + + "\"isReconnecting\":true," + + "\"showReconnectingUI\":true," + + "\"showPostCompileReconnectingUI\":true," + "\"connectedLLMTools\":[{\"Name\":\"codex\",\"Endpoint\":\"/tmp/uloop/test.sock#1\",\"Port\":18449}]" + "}"); @@ -248,6 +260,12 @@ public void RecoverSettingsFileIfNeeded_WhenLegacyPortFieldsExist_ShouldRemoveTh StringAssert.DoesNotContain("serverTransportKind", recoveredJson); StringAssert.DoesNotContain("projectRootPath", recoveredJson); StringAssert.DoesNotContain("serverSessionId", recoveredJson); + StringAssert.DoesNotContain("isServerRunning", recoveredJson); + StringAssert.DoesNotContain("isAfterCompile", recoveredJson); + StringAssert.DoesNotContain("isDomainReloadInProgress", recoveredJson); + StringAssert.DoesNotContain("isReconnecting", recoveredJson); + StringAssert.DoesNotContain("showReconnectingUI", recoveredJson); + StringAssert.DoesNotContain("showPostCompileReconnectingUI", recoveredJson); StringAssert.DoesNotContain("connectedLLMTools", recoveredJson); StringAssert.DoesNotContain("\"Port\"", recoveredJson); } @@ -274,16 +292,18 @@ public void SetInstallSkillsFlat_PersistsValue() [Test] public void UpdateSessionState_WhenStartingServer_ShouldNotPersistRuntimeIdentity() { + _editorSettingsService.SaveSettings(new UnityCliLoopEditorSettingsData { showDeveloperTools = true }); UnityCliLoopServerStartupService service = new UnityCliLoopServerStartupService( new TestServerInstanceFactory(), - _editorSettingsService); + _sessionStateService); ServiceResult result = service.UpdateSessionState(true); Assert.IsTrue(result.Success, "Session update should succeed"); - Assert.IsTrue(_editorSettingsService.GetIsServerRunning(), "Server running state should be persisted"); + Assert.IsTrue(_sessionStateService.GetIsServerRunning(), "Server running state should be kept for this Editor session"); string savedJson = File.ReadAllText(SettingsFilePath); + StringAssert.DoesNotContain("isServerRunning", savedJson); StringAssert.DoesNotContain("projectRootPath", savedJson); StringAssert.DoesNotContain("serverSessionId", savedJson); } diff --git a/Assets/Tests/Editor/UnityCliLoopPackageRemovalSettingsResetterTests.cs b/Assets/Tests/Editor/UnityCliLoopPackageRemovalSettingsResetterTests.cs index 52a2a97ad..e417b1bcb 100644 --- a/Assets/Tests/Editor/UnityCliLoopPackageRemovalSettingsResetterTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopPackageRemovalSettingsResetterTests.cs @@ -100,7 +100,6 @@ public void ResetSetupWizardState_WhenWizardStateExists_ClearsOnlyWizardFields() Assert.That(resetSettings.showDeveloperTools, Is.EqualTo(settings.showDeveloperTools)); Assert.That(resetSettings.showToolSettings, Is.EqualTo(settings.showToolSettings)); Assert.That(resetSettings.installSkillsFlat, Is.EqualTo(settings.installSkillsFlat)); - Assert.That(resetSettings.isServerRunning, Is.EqualTo(settings.isServerRunning)); } [Test] @@ -121,7 +120,6 @@ public void ResetSetupWizardStateIfPackageRemoved_WhenOwnPackageRemoved_ClearsSt Assert.That(updatedSettings.showDeveloperTools, Is.EqualTo(settings.showDeveloperTools)); Assert.That(updatedSettings.showToolSettings, Is.EqualTo(settings.showToolSettings)); Assert.That(updatedSettings.installSkillsFlat, Is.EqualTo(settings.installSkillsFlat)); - Assert.That(updatedSettings.isServerRunning, Is.EqualTo(settings.isServerRunning)); } private static UnityCliLoopEditorSettingsData CreateSettingsWithNonWizardPreferences() @@ -133,13 +131,7 @@ private static UnityCliLoopEditorSettingsData CreateSettingsWithNonWizardPrefere suppressSetupWizardAutoShow = true, showUnityCliLoopSecuritySetting = false, showToolSettings = false, - installSkillsFlat = false, - isServerRunning = false, - isAfterCompile = true, - isDomainReloadInProgress = true, - isReconnecting = true, - showReconnectingUI = true, - showPostCompileReconnectingUI = true + installSkillsFlat = false }; } diff --git a/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs b/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs index 1582ef3b9..cbb660c99 100644 --- a/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs @@ -14,6 +14,23 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class UnityCliLoopServerControllerRecoveryTests { + private UnityCliLoopEditorSessionStateService _sessionStateService; + private UnityCliLoopEditorSessionStateSnapshot _originalSessionState; + + [SetUp] + public void SetUp() + { + _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); + _originalSessionState = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); + _sessionStateService.ClearAll(); + } + + [TearDown] + public void TearDown() + { + _originalSessionState.Restore(_sessionStateService); + } + [Test] public void ScheduleStartupRecovery_WhenCalled_ExposesRecoveryTaskBeforeDeferredActionRuns() { @@ -91,8 +108,6 @@ public async Task StartRecoveryIfNeededAsync_WhenReadinessSucceeds_ShouldPublish TestServerInstanceFactory serverInstanceFactory = new(); UnityCliLoopServerLifecycleRegistryService lifecycleRegistry = new UnityCliLoopServerLifecycleRegistryService(); - UnityCliLoopEditorSettingsService editorSettingsService = - UnityCliLoopEditorSettingsTestFactory.CreateService(); ServerReadinessStateStore stateStore = CreateTestStateStore(); TestReadinessProbe readinessProbe = new(); int serverStartedCount = 0; @@ -100,8 +115,8 @@ public async Task StartRecoveryIfNeededAsync_WhenReadinessSucceeds_ShouldPublish UnityCliLoopServerControllerService service = new( serverInstanceFactory, lifecycleRegistry, - new DomainReloadDetectionFileService(editorSettingsService, stateStore), - editorSettingsService, + new DomainReloadDetectionFileService(_sessionStateService, stateStore), + _sessionStateService, stateStore, readinessProbe, new TestDomainReloadLifecycle()); @@ -137,24 +152,22 @@ public async Task ProbeReadinessWithTimeoutAsync_WhenProbeDoesNotComplete_Throws Assert.That(readinessProbe.CallCount, Is.EqualTo(1)); } - private static UnityCliLoopServerControllerService CreateControllerService() + private UnityCliLoopServerControllerService CreateControllerService() { return CreateControllerService(new TestReadinessProbe()); } - private static UnityCliLoopServerControllerService CreateControllerService(TestReadinessProbe readinessProbe) + private UnityCliLoopServerControllerService CreateControllerService(TestReadinessProbe readinessProbe) { TestServerInstanceFactory serverInstanceFactory = new(); UnityCliLoopServerLifecycleRegistryService lifecycleRegistry = new UnityCliLoopServerLifecycleRegistryService(); - UnityCliLoopEditorSettingsService editorSettingsService = - UnityCliLoopEditorSettingsTestFactory.CreateService(); ServerReadinessStateStore stateStore = CreateTestStateStore(); return new UnityCliLoopServerControllerService( serverInstanceFactory, lifecycleRegistry, - new DomainReloadDetectionFileService(editorSettingsService, stateStore), - editorSettingsService, + new DomainReloadDetectionFileService(_sessionStateService, stateStore), + _sessionStateService, stateStore, readinessProbe, new TestDomainReloadLifecycle()); diff --git a/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs b/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs index 00f52a6bd..74725b393 100644 --- a/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs @@ -11,6 +11,23 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class UnityCliLoopServerStartupProtectionTests { + private UnityCliLoopEditorSessionStateService _sessionStateService; + private UnityCliLoopEditorSessionStateSnapshot _originalSessionState; + + [SetUp] + public void SetUp() + { + _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); + _originalSessionState = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); + _sessionStateService.ClearAll(); + } + + [TearDown] + public void TearDown() + { + _originalSessionState.Restore(_sessionStateService); + } + [Test] public void ClearStartupProtection_ResetsProtectionWindow() { @@ -31,30 +48,17 @@ public void OnBeforeAssemblyReload_ShouldClearStartupProtectionBeforeRecovery() { // Tests that assembly-reload recovery clears the startup protection window before shutdown. UnityCliLoopServerControllerService service = CreateControllerService(); - UnityCliLoopEditorSettingsService editorSettingsService = - UnityCliLoopEditorSettingsTestFactory.CreateService(); - - UnityCliLoopEditorSettingsData originalSettings = CloneSettings(editorSettingsService.GetSettings()); - - try - { - service.RegisterRecoveredServer(new TestServerInstance()); - service.ActivateStartupProtection(60000); + service.RegisterRecoveredServer(new TestServerInstance()); + service.ActivateStartupProtection(60000); - Assert.IsTrue(service.IsStartupProtectionActive(), "Startup protection should be active before reload"); + Assert.IsTrue(service.IsStartupProtectionActive(), "Startup protection should be active before reload"); - service.OnBeforeAssemblyReload(); + service.OnBeforeAssemblyReload(); - Assert.IsFalse( - service.IsStartupProtectionActive(), - "Assembly reload recovery should clear startup protection so the server can restart" - ); - } - finally - { - editorSettingsService.SaveSettings(originalSettings); - service.ClearStartupProtection(); - } + Assert.IsFalse( + service.IsStartupProtectionActive(), + "Assembly reload recovery should clear startup protection so the server can restart" + ); } [Test] @@ -87,30 +91,22 @@ public void PrepareForServerShutdown_ShouldClearStartupProtectionBeforeShutdown( ); } - private static UnityCliLoopEditorSettingsData CloneSettings(UnityCliLoopEditorSettingsData settings) - { - string json = UnityEngine.JsonUtility.ToJson(settings); - return UnityEngine.JsonUtility.FromJson(json); - } - - private static UnityCliLoopServerControllerService CreateControllerService() + private UnityCliLoopServerControllerService CreateControllerService() { return CreateControllerService(new TestDomainReloadLifecycle()); } - private static UnityCliLoopServerControllerService CreateControllerService(TestDomainReloadLifecycle domainReloadLifecycle) + private UnityCliLoopServerControllerService CreateControllerService(TestDomainReloadLifecycle domainReloadLifecycle) { TestServerInstanceFactory serverInstanceFactory = new(); UnityCliLoopServerLifecycleRegistryService lifecycleRegistry = new UnityCliLoopServerLifecycleRegistryService(); - UnityCliLoopEditorSettingsService editorSettingsService = - UnityCliLoopEditorSettingsTestFactory.CreateService(); ServerReadinessStateStore stateStore = CreateTestStateStore(); return new UnityCliLoopServerControllerService( serverInstanceFactory, lifecycleRegistry, - new DomainReloadDetectionFileService(editorSettingsService, stateStore), - editorSettingsService, + new DomainReloadDetectionFileService(_sessionStateService, stateStore), + _sessionStateService, stateStore, new TestReadinessProbe(), domainReloadLifecycle); diff --git a/Packages/src/Editor/Application/SessionRecoveryService.cs b/Packages/src/Editor/Application/SessionRecoveryService.cs index 51b089a2e..a0e0d8f7a 100644 --- a/Packages/src/Editor/Application/SessionRecoveryService.cs +++ b/Packages/src/Editor/Application/SessionRecoveryService.cs @@ -23,45 +23,45 @@ public sealed class SessionRecoveryService { private readonly IUnityCliLoopServerRecoveryCoordinator _recoveryCoordinator; private readonly IDomainReloadDetectionService _domainReloadDetectionService; - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; + private readonly UnityCliLoopEditorSessionStateService _sessionStateService; public SessionRecoveryService( IUnityCliLoopServerRecoveryCoordinator recoveryCoordinator, IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService) + UnityCliLoopEditorSessionStateService sessionStateService) { System.Diagnostics.Debug.Assert(recoveryCoordinator != null, "recoveryCoordinator must not be null"); System.Diagnostics.Debug.Assert(domainReloadDetectionService != null, "domainReloadDetectionService must not be null"); - System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); + System.Diagnostics.Debug.Assert(sessionStateService != null, "sessionStateService must not be null"); _recoveryCoordinator = recoveryCoordinator ?? throw new System.ArgumentNullException(nameof(recoveryCoordinator)); _domainReloadDetectionService = domainReloadDetectionService ?? throw new System.ArgumentNullException(nameof(domainReloadDetectionService)); - _editorSettingsService = editorSettingsService - ?? throw new System.ArgumentNullException(nameof(editorSettingsService)); + _sessionStateService = sessionStateService + ?? throw new System.ArgumentNullException(nameof(sessionStateService)); } public async Task RestoreServerStateIfNeededAsync(CancellationToken ct) { ct.ThrowIfCancellationRequested(); - bool wasRunning = _editorSettingsService.GetIsServerRunning(); - bool isAfterCompile = _editorSettingsService.GetIsAfterCompile(); + bool wasRunning = _sessionStateService.GetIsServerRunning(); + bool isAfterCompile = _sessionStateService.GetIsAfterCompile(); IUnityCliLoopServerInstance currentServer = _recoveryCoordinator.CurrentServer; if (currentServer?.IsRunning == true) { if (isAfterCompile) { - _editorSettingsService.ClearAfterCompileFlag(); + _sessionStateService.ClearAfterCompileFlag(); } return ValidationResult.Success(); } if (isAfterCompile) { - _editorSettingsService.ClearAfterCompileFlag(); + _sessionStateService.ClearAfterCompileFlag(); } if (wasRunning && (currentServer == null || !currentServer.IsRunning)) @@ -84,10 +84,10 @@ public async Task StartReconnectionUITimeoutAsync(CancellationToken ct) await EditorDelay.DelayFrame(timeoutFrames, ct); ct.ThrowIfCancellationRequested(); - bool isStillShowingUI = _editorSettingsService.GetShowReconnectingUI(); + bool isStillShowingUI = _sessionStateService.GetShowReconnectingUI(); if (isStillShowingUI) { - _editorSettingsService.ClearReconnectingFlags(); + _sessionStateService.ClearReconnectingFlags(); } } } diff --git a/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs b/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs index 029532222..940c7c9a8 100644 --- a/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs +++ b/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs @@ -8,17 +8,17 @@ namespace io.github.hatayama.UnityCliLoop.Application public class UnityCliLoopServerStartupService { private readonly IUnityCliLoopServerInstanceFactory _serverInstanceFactory; - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; + private readonly UnityCliLoopEditorSessionStateService _sessionStateService; public UnityCliLoopServerStartupService( IUnityCliLoopServerInstanceFactory serverInstanceFactory, - UnityCliLoopEditorSettingsService editorSettingsService) + UnityCliLoopEditorSessionStateService sessionStateService) { System.Diagnostics.Debug.Assert(serverInstanceFactory != null, "serverInstanceFactory must not be null"); - System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); + System.Diagnostics.Debug.Assert(sessionStateService != null, "sessionStateService must not be null"); _serverInstanceFactory = serverInstanceFactory ?? throw new System.ArgumentNullException(nameof(serverInstanceFactory)); - _editorSettingsService = editorSettingsService ?? throw new System.ArgumentNullException(nameof(editorSettingsService)); + _sessionStateService = sessionStateService ?? throw new System.ArgumentNullException(nameof(sessionStateService)); } public ServiceResult StartServer() @@ -60,11 +60,11 @@ public ServiceResult UpdateSessionState(bool isRunning) { if (!isRunning) { - _editorSettingsService.ClearServerSession(); + _sessionStateService.ClearServerSession(); return ServiceResult.SuccessResult(true); } - _editorSettingsService.SetIsServerRunning(true); + _sessionStateService.SetIsServerRunning(true); return ServiceResult.SuccessResult(true); } } diff --git a/Packages/src/Editor/Application/UseCases/DomainReloadRecoveryUseCase.cs b/Packages/src/Editor/Application/UseCases/DomainReloadRecoveryUseCase.cs index 6b60207d7..1ead0f06e 100644 --- a/Packages/src/Editor/Application/UseCases/DomainReloadRecoveryUseCase.cs +++ b/Packages/src/Editor/Application/UseCases/DomainReloadRecoveryUseCase.cs @@ -15,23 +15,23 @@ public class DomainReloadRecoveryUseCase { private readonly SessionRecoveryService _sessionRecoveryService; private readonly IDomainReloadDetectionService _domainReloadDetectionService; - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; + private readonly UnityCliLoopEditorSessionStateService _sessionStateService; public DomainReloadRecoveryUseCase( SessionRecoveryService sessionRecoveryService, IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService) + UnityCliLoopEditorSessionStateService sessionStateService) { System.Diagnostics.Debug.Assert(sessionRecoveryService != null, "sessionRecoveryService must not be null"); System.Diagnostics.Debug.Assert(domainReloadDetectionService != null, "domainReloadDetectionService must not be null"); - System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); + System.Diagnostics.Debug.Assert(sessionStateService != null, "sessionStateService must not be null"); _sessionRecoveryService = sessionRecoveryService ?? throw new System.ArgumentNullException(nameof(sessionRecoveryService)); _domainReloadDetectionService = domainReloadDetectionService ?? throw new System.ArgumentNullException(nameof(domainReloadDetectionService)); - _editorSettingsService = editorSettingsService - ?? throw new System.ArgumentNullException(nameof(editorSettingsService)); + _sessionStateService = sessionStateService + ?? throw new System.ArgumentNullException(nameof(sessionStateService)); } /// @@ -49,7 +49,7 @@ public ServiceResult ExecuteBeforeDomainReload(IUnityCliLoopServerInstan // 3. Fallback to session state if instance is null but session says server was running // Handles case where bridge server instance became null unexpectedly - if (currentServer == null && _editorSettingsService.GetIsServerRunning()) + if (currentServer == null && _sessionStateService.GetIsServerRunning()) { serverRunning = true; VibeLogger.LogWarning( diff --git a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs index 69931f651..b26e1703c 100644 --- a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs +++ b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs @@ -16,13 +16,15 @@ internal UnityCliLoopApplicationServices Register() ToolSettingsService toolSettingsService = new(toolSettingsRepository); UnityCliLoopEditorSettingsRepository editorSettingsRepository = new(); UnityCliLoopEditorSettingsService editorSettingsService = new(editorSettingsRepository); + UnityCliLoopEditorSessionStateRepository sessionStateRepository = new(); + UnityCliLoopEditorSessionStateService sessionStateService = new(sessionStateRepository); ServerReadinessStateStore serverReadinessStateStore = new(UnityCliLoopPathResolver.GetProjectRoot()); UnityCliLoopFirstPartyServerLifecycleBinding firstPartyServerLifecycle = new(new ProjectIpcWarmupClient()); ULoopSettingsRepository uLoopSettingsRepository = new( toolSettingsService, editorSettingsService); DomainReloadDetectionFileService domainReloadDetectionService = new( - editorSettingsService, + sessionStateService, serverReadinessStateStore); ULoopSettings.RegisterService(uLoopSettingsRepository); MainThreadSwitcher.RegisterService(new EditorMainThreadDispatcher()); @@ -42,16 +44,14 @@ internal UnityCliLoopApplicationServices Register() CliSetupApplicationFacade.RegisterService(new CliSetupApplicationService( new CliInstallationDetector(), new NativeCliInstallerService())); - UnityCliLoopBridgeServerInstanceFactory serverFactory = new( - domainReloadDetectionService, - editorSettingsService); + UnityCliLoopBridgeServerInstanceFactory serverFactory = new(domainReloadDetectionService); UnityCliLoopServerLifecycleRegistryService lifecycleRegistry = new(); lifecycleRegistry.RegisterSource(serverFactory); UnityCliLoopServerControllerService controllerService = new( serverFactory, lifecycleRegistry, domainReloadDetectionService, - editorSettingsService, + sessionStateService, serverReadinessStateStore, firstPartyServerLifecycle, firstPartyServerLifecycle); @@ -61,7 +61,8 @@ internal UnityCliLoopApplicationServices Register() return new UnityCliLoopApplicationServices( domainReloadDetectionService, - editorSettingsService); + editorSettingsService, + sessionStateService); } } @@ -69,13 +70,16 @@ internal sealed class UnityCliLoopApplicationServices { internal UnityCliLoopApplicationServices( IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService) + UnityCliLoopEditorSettingsService editorSettingsService, + UnityCliLoopEditorSessionStateService sessionStateService) { DomainReloadDetectionService = domainReloadDetectionService; EditorSettingsService = editorSettingsService; + SessionStateService = sessionStateService; } internal IDomainReloadDetectionService DomainReloadDetectionService { get; } internal UnityCliLoopEditorSettingsService EditorSettingsService { get; } + internal UnityCliLoopEditorSessionStateService SessionStateService { get; } } } diff --git a/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs b/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs index 3020e51c8..414166f63 100644 --- a/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs +++ b/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs @@ -24,7 +24,9 @@ internal void Initialize() ApplicationEditorStartup.Initialize(applicationServices.DomainReloadDetectionService); FirstPartyToolsEditorStartup.Initialize(); InfrastructureEditorStartup.Initialize(applicationServices.EditorSettingsService); - PresentationEditorStartup.Initialize(applicationServices.EditorSettingsService); + PresentationEditorStartup.Initialize( + applicationServices.EditorSettingsService, + applicationServices.SessionStateService); } } } diff --git a/Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs b/Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs new file mode 100644 index 000000000..20288037d --- /dev/null +++ b/Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs @@ -0,0 +1,152 @@ +using System; +using System.Diagnostics; + +namespace io.github.hatayama.UnityCliLoop.Domain +{ + public interface IUnityCliLoopEditorSessionStatePort + { + bool GetIsServerRunning(); + void SetIsServerRunning(bool isServerRunning); + bool GetIsAfterCompile(); + void SetIsAfterCompile(bool isAfterCompile); + bool GetIsDomainReloadInProgress(); + void SetIsDomainReloadInProgress(bool isDomainReloadInProgress); + bool GetIsReconnecting(); + void SetIsReconnecting(bool isReconnecting); + bool GetShowReconnectingUI(); + void SetShowReconnectingUI(bool showReconnectingUI); + bool GetShowPostCompileReconnectingUI(); + void SetShowPostCompileReconnectingUI(bool showPostCompileReconnectingUI); + } + + /// + /// Coordinates Unity Editor session-scoped runtime flags through the storage port owned by Infrastructure. + /// + public sealed class UnityCliLoopEditorSessionStateService + { + private readonly IUnityCliLoopEditorSessionStatePort _sessionStatePort; + + public UnityCliLoopEditorSessionStateService(IUnityCliLoopEditorSessionStatePort sessionStatePort) + { + Debug.Assert(sessionStatePort != null, "sessionStatePort must not be null"); + + _sessionStatePort = sessionStatePort ?? throw new ArgumentNullException(nameof(sessionStatePort)); + } + + public bool GetIsServerRunning() + { + return _sessionStatePort.GetIsServerRunning(); + } + + public void SetIsServerRunning(bool isServerRunning) + { + _sessionStatePort.SetIsServerRunning(isServerRunning); + } + + public bool GetIsAfterCompile() + { + return _sessionStatePort.GetIsAfterCompile(); + } + + public void SetIsAfterCompile(bool isAfterCompile) + { + _sessionStatePort.SetIsAfterCompile(isAfterCompile); + } + + public bool GetIsDomainReloadInProgress() + { + return _sessionStatePort.GetIsDomainReloadInProgress(); + } + + public void SetIsDomainReloadInProgress(bool isDomainReloadInProgress) + { + _sessionStatePort.SetIsDomainReloadInProgress(isDomainReloadInProgress); + } + + public bool GetIsReconnecting() + { + return _sessionStatePort.GetIsReconnecting(); + } + + public void SetIsReconnecting(bool isReconnecting) + { + _sessionStatePort.SetIsReconnecting(isReconnecting); + } + + public bool GetShowReconnectingUI() + { + return _sessionStatePort.GetShowReconnectingUI(); + } + + public void SetShowReconnectingUI(bool showReconnectingUI) + { + _sessionStatePort.SetShowReconnectingUI(showReconnectingUI); + } + + public bool GetShowPostCompileReconnectingUI() + { + return _sessionStatePort.GetShowPostCompileReconnectingUI(); + } + + public void SetShowPostCompileReconnectingUI(bool showPostCompileReconnectingUI) + { + _sessionStatePort.SetShowPostCompileReconnectingUI(showPostCompileReconnectingUI); + } + + public void MarkDomainReloadStarted(bool serverIsRunning) + { + SetIsDomainReloadInProgress(true); + if (!serverIsRunning) + { + return; + } + + SetIsServerRunning(true); + SetIsAfterCompile(true); + SetIsReconnecting(true); + SetShowReconnectingUI(true); + SetShowPostCompileReconnectingUI(true); + } + + public void ClearServerSession() + { + SetIsServerRunning(false); + } + + public void ClearAfterCompileFlag() + { + SetIsAfterCompile(false); + } + + public void ClearReconnectingFlags() + { + SetIsReconnecting(false); + SetShowReconnectingUI(false); + } + + public void ClearPostCompileReconnectingUI() + { + SetShowPostCompileReconnectingUI(false); + } + + public void ClearDomainReloadFlag() + { + SetIsDomainReloadInProgress(false); + } + + public void ClearDomainReloadRecoveryFlags() + { + SetIsDomainReloadInProgress(false); + SetIsAfterCompile(false); + SetIsReconnecting(false); + SetShowReconnectingUI(false); + SetShowPostCompileReconnectingUI(false); + } + + public void ClearAll() + { + ClearServerSession(); + ClearDomainReloadRecoveryFlags(); + } + } +} diff --git a/Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs.meta b/Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs.meta new file mode 100644 index 000000000..c9a97be34 --- /dev/null +++ b/Packages/src/Editor/Domain/UnityCliLoopEditorSessionStateService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b4c53e076c774db0b73bd0b4bb1c5ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs b/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs index f0c2b6fec..f54df57a0 100644 --- a/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs +++ b/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs @@ -12,11 +12,5 @@ public record UnityCliLoopEditorSettingsData public bool showUnityCliLoopSecuritySetting = true; public bool showToolSettings = true; public bool installSkillsFlat = true; - public bool isServerRunning = true; - public bool isAfterCompile = false; - public bool isDomainReloadInProgress = false; - public bool isReconnecting = false; - public bool showReconnectingUI = false; - public bool showPostCompileReconnectingUI = false; } } diff --git a/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsService.cs b/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsService.cs index f095099fe..e16a541f7 100644 --- a/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsService.cs +++ b/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsService.cs @@ -16,19 +16,6 @@ public interface IUnityCliLoopEditorSettingsPort void SetShowUnityCliLoopSecuritySetting(bool showUnityCliLoopSecuritySetting); void SetShowToolSettings(bool showToolSettings); void SetInstallSkillsFlat(bool installSkillsFlat); - bool GetIsServerRunning(); - void SetIsServerRunning(bool isServerRunning); - bool GetIsAfterCompile(); - bool GetIsDomainReloadInProgress(); - void SetIsDomainReloadInProgress(bool isDomainReloadInProgress); - void SetIsReconnecting(bool isReconnecting); - bool GetShowReconnectingUI(); - void SetShowReconnectingUI(bool showReconnectingUI); - void ClearServerSession(); - void ClearAfterCompileFlag(); - void ClearReconnectingFlags(); - void ClearPostCompileReconnectingUI(); - void ClearDomainReloadFlag(); } /// @@ -99,70 +86,5 @@ public void SetInstallSkillsFlat(bool installSkillsFlat) { _settingsPort.SetInstallSkillsFlat(installSkillsFlat); } - - public bool GetIsServerRunning() - { - return _settingsPort.GetIsServerRunning(); - } - - public void SetIsServerRunning(bool isServerRunning) - { - _settingsPort.SetIsServerRunning(isServerRunning); - } - - public bool GetIsAfterCompile() - { - return _settingsPort.GetIsAfterCompile(); - } - - public bool GetIsDomainReloadInProgress() - { - return _settingsPort.GetIsDomainReloadInProgress(); - } - - public void SetIsDomainReloadInProgress(bool isDomainReloadInProgress) - { - _settingsPort.SetIsDomainReloadInProgress(isDomainReloadInProgress); - } - - public void SetIsReconnecting(bool isReconnecting) - { - _settingsPort.SetIsReconnecting(isReconnecting); - } - - public bool GetShowReconnectingUI() - { - return _settingsPort.GetShowReconnectingUI(); - } - - public void SetShowReconnectingUI(bool showReconnectingUI) - { - _settingsPort.SetShowReconnectingUI(showReconnectingUI); - } - - public void ClearServerSession() - { - _settingsPort.ClearServerSession(); - } - - public void ClearAfterCompileFlag() - { - _settingsPort.ClearAfterCompileFlag(); - } - - public void ClearReconnectingFlags() - { - _settingsPort.ClearReconnectingFlags(); - } - - public void ClearPostCompileReconnectingUI() - { - _settingsPort.ClearPostCompileReconnectingUI(); - } - - public void ClearDomainReloadFlag() - { - _settingsPort.ClearDomainReloadFlag(); - } } } diff --git a/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs b/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs index b761badbf..509605a8b 100644 --- a/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs +++ b/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs @@ -8,26 +8,30 @@ namespace io.github.hatayama.UnityCliLoop.Infrastructure { /// - /// Infrastructure implementation that persists Domain Reload readiness state through server state and editor settings. + /// Infrastructure implementation that persists Domain Reload readiness state through server state and Editor SessionState. /// public sealed class DomainReloadDetectionFileService : IDomainReloadDetectionService { - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; + private readonly UnityCliLoopEditorSessionStateService _sessionStateService; private readonly ServerReadinessStateStore _stateStore; + private readonly IUnityCliLoopEditorLegacySessionStateReader _legacySessionStateReader; public DomainReloadDetectionFileService() - : this(new UnityCliLoopEditorSettingsService(new UnityCliLoopEditorSettingsRepository())) + : this(new UnityCliLoopEditorSessionStateService(new UnityCliLoopEditorSessionStateRepository())) { } internal DomainReloadDetectionFileService( - UnityCliLoopEditorSettingsService editorSettingsService, - ServerReadinessStateStore stateStore = null) + UnityCliLoopEditorSessionStateService sessionStateService, + ServerReadinessStateStore stateStore = null, + IUnityCliLoopEditorLegacySessionStateReader legacySessionStateReader = null) { - UnityEngine.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); + UnityEngine.Debug.Assert(sessionStateService != null, "sessionStateService must not be null"); - _editorSettingsService = editorSettingsService ?? throw new ArgumentNullException(nameof(editorSettingsService)); + _sessionStateService = sessionStateService ?? throw new ArgumentNullException(nameof(sessionStateService)); _stateStore = stateStore ?? new ServerReadinessStateStore(UnityCliLoopPathResolver.GetProjectRoot()); + _legacySessionStateReader = + legacySessionStateReader ?? new UnityCliLoopEditorLegacySessionStateReader(); } private static bool IsBackgroundUnityProcess() @@ -83,28 +87,7 @@ public void StartDomainReload(string correlationId, bool serverIsRunning) null, null); - // Save session state if server is running - if (serverIsRunning) - { - _editorSettingsService.UpdateSettings(s => - { - UnityCliLoopEditorSettingsData updatedSettings = s with - { - isDomainReloadInProgress = true, - isServerRunning = true, - isAfterCompile = true, - isReconnecting = true, - showReconnectingUI = true, - showPostCompileReconnectingUI = true - }; - - return updatedSettings; - }); - } - else - { - _editorSettingsService.SetIsDomainReloadInProgress(true); - } + _sessionStateService.MarkDomainReloadStarted(serverIsRunning); UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(true); @@ -132,7 +115,8 @@ public void CompleteDomainReload(string correlationId) return; } - bool serverWillRecover = _editorSettingsService.GetIsServerRunning(); + MigrateLegacySessionStateIfNeeded(); + bool serverWillRecover = _sessionStateService.GetIsServerRunning(); _stateStore.Write( serverWillRecover ? ServerReadinessPhase.Recovering : ServerReadinessPhase.Stopped, @@ -142,7 +126,7 @@ public void CompleteDomainReload(string correlationId) null); // Clear Domain Reload completion flag - _editorSettingsService.ClearDomainReloadFlag(); + _sessionStateService.ClearDomainReloadFlag(); UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); // Log recording @@ -162,14 +146,7 @@ public void RollbackDomainReloadStart(string correlationId) return; } - _editorSettingsService.UpdateSettings(s => s with - { - isDomainReloadInProgress = false, - isAfterCompile = false, - isReconnecting = false, - showReconnectingUI = false, - showPostCompileReconnectingUI = false - }); + _sessionStateService.ClearDomainReloadRecoveryFlags(); UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); _stateStore.Write( ServerReadinessPhase.Failed, @@ -191,7 +168,46 @@ public void RollbackDomainReloadStart(string correlationId) /// True if reconnection UI display is required public bool ShouldShowReconnectingUI() { - return _editorSettingsService.GetShowReconnectingUI(); + return _sessionStateService.GetShowReconnectingUI(); + } + + private void MigrateLegacySessionStateIfNeeded() + { + UnityCliLoopEditorLegacySessionState legacySessionState = _legacySessionStateReader.Read(); + if (!legacySessionState.HasDomainReloadRecoveryState) + { + return; + } + + if (legacySessionState.IsServerRunning) + { + _sessionStateService.SetIsServerRunning(true); + } + + if (legacySessionState.IsAfterCompile) + { + _sessionStateService.SetIsAfterCompile(true); + } + + if (legacySessionState.IsDomainReloadInProgress) + { + _sessionStateService.SetIsDomainReloadInProgress(true); + } + + if (legacySessionState.IsReconnecting) + { + _sessionStateService.SetIsReconnecting(true); + } + + if (legacySessionState.ShowReconnectingUI) + { + _sessionStateService.SetShowReconnectingUI(true); + } + + if (legacySessionState.ShowPostCompileReconnectingUI) + { + _sessionStateService.SetShowPostCompileReconnectingUI(true); + } } } } diff --git a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs index 4fb85cd3d..d19296110 100644 --- a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs +++ b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs @@ -22,7 +22,7 @@ public sealed class UnityCliLoopServerControllerService : private readonly IUnityCliLoopServerInstanceFactory _serverInstanceFactory; private readonly UnityCliLoopServerLifecycleRegistryService _serverLifecycleRegistry; private readonly IDomainReloadDetectionService _domainReloadDetectionService; - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; + private readonly UnityCliLoopEditorSessionStateService _sessionStateService; private readonly SessionRecoveryService _sessionRecoveryService; private readonly ServerReadinessStateStore _stateStore; private readonly IUnityCliLoopServerReadinessProbe _readinessProbe; @@ -36,7 +36,7 @@ internal UnityCliLoopServerControllerService( IUnityCliLoopServerInstanceFactory serverInstanceFactory, UnityCliLoopServerLifecycleRegistryService serverLifecycleRegistry, IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService, + UnityCliLoopEditorSessionStateService sessionStateService, ServerReadinessStateStore stateStore, IUnityCliLoopServerReadinessProbe readinessProbe, IUnityCliLoopServerDomainReloadLifecycle domainReloadLifecycle) @@ -44,7 +44,7 @@ internal UnityCliLoopServerControllerService( System.Diagnostics.Debug.Assert(serverInstanceFactory != null, "serverInstanceFactory must not be null"); System.Diagnostics.Debug.Assert(serverLifecycleRegistry != null, "serverLifecycleRegistry must not be null"); System.Diagnostics.Debug.Assert(domainReloadDetectionService != null, "domainReloadDetectionService must not be null"); - System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); + System.Diagnostics.Debug.Assert(sessionStateService != null, "sessionStateService must not be null"); System.Diagnostics.Debug.Assert(stateStore != null, "stateStore must not be null"); System.Diagnostics.Debug.Assert(readinessProbe != null, "readinessProbe must not be null"); System.Diagnostics.Debug.Assert(domainReloadLifecycle != null, "domainReloadLifecycle must not be null"); @@ -52,14 +52,14 @@ internal UnityCliLoopServerControllerService( _serverInstanceFactory = serverInstanceFactory ?? throw new ArgumentNullException(nameof(serverInstanceFactory)); _serverLifecycleRegistry = serverLifecycleRegistry ?? throw new ArgumentNullException(nameof(serverLifecycleRegistry)); _domainReloadDetectionService = domainReloadDetectionService ?? throw new ArgumentNullException(nameof(domainReloadDetectionService)); - _editorSettingsService = editorSettingsService ?? throw new ArgumentNullException(nameof(editorSettingsService)); + _sessionStateService = sessionStateService ?? throw new ArgumentNullException(nameof(sessionStateService)); _stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore)); _readinessProbe = readinessProbe ?? throw new ArgumentNullException(nameof(readinessProbe)); _domainReloadLifecycle = domainReloadLifecycle ?? throw new ArgumentNullException(nameof(domainReloadLifecycle)); _sessionRecoveryService = new SessionRecoveryService( this, _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); } private bool IsBackgroundUnityProcess() @@ -212,7 +212,7 @@ private async Task StartServerWithUseCaseAsync() } UnityCliLoopServerStartupService startupService = - new UnityCliLoopServerStartupService(_serverInstanceFactory, _editorSettingsService); + new UnityCliLoopServerStartupService(_serverInstanceFactory, _sessionStateService); UnityCliLoopServerInitializationUseCase useCase = new UnityCliLoopServerInitializationUseCase( new EditorSecurityValidationService(), @@ -263,7 +263,7 @@ internal async Task StopServerWithUseCaseAsync() PrepareForServerShutdown(); UnityCliLoopServerStartupService startupService = - new UnityCliLoopServerStartupService(_serverInstanceFactory, _editorSettingsService); + new UnityCliLoopServerStartupService(_serverInstanceFactory, _sessionStateService); UnityCliLoopServerShutdownUseCase useCase = new UnityCliLoopServerShutdownUseCase(startupService, this); System.Threading.CancellationToken cancellationToken = System.Threading.CancellationToken.None; @@ -276,7 +276,7 @@ internal async Task StopServerWithUseCaseAsync() _bridgeServer = null; // Clear session state to reflect server stopped - _editorSettingsService.ClearServerSession(); + _sessionStateService.ClearServerSession(); WriteServerState(ServerReadinessPhase.Stopped, generationId, "manual-stop", null); } else @@ -301,7 +301,7 @@ internal void OnBeforeAssemblyReload() new DomainReloadRecoveryUseCase( _sessionRecoveryService, _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); ServiceResult result = useCase.ExecuteBeforeDomainReload(_bridgeServer); // Clear instance if server shutdown succeeded @@ -326,7 +326,7 @@ private async Task ExecuteAfterDomainReloadRecoveryAsync(CancellationToken cance new DomainReloadRecoveryUseCase( _sessionRecoveryService, _domainReloadDetectionService, - _editorSettingsService); + _sessionStateService); ServiceResult result = await useCase.ExecuteAfterDomainReloadAsync(cancellationToken); if (!result.Success) @@ -349,13 +349,13 @@ private async Task RestoreServerStateIfNeeded() return; } - bool isAfterCompile = _editorSettingsService.GetIsAfterCompile(); + bool isAfterCompile = _sessionStateService.GetIsAfterCompile(); if (_bridgeServer?.IsRunning == true) { if (isAfterCompile) { - _editorSettingsService.ClearAfterCompileFlag(); + _sessionStateService.ClearAfterCompileFlag(); } return; @@ -363,7 +363,7 @@ private async Task RestoreServerStateIfNeeded() if (isAfterCompile) { - _editorSettingsService.ClearAfterCompileFlag(); + _sessionStateService.ClearAfterCompileFlag(); } await StartRecoveryIfNeededAsync(isAfterCompile, CancellationToken.None); @@ -390,7 +390,7 @@ private void OnEditorQuitting() _bridgeServer = null; } } - _editorSettingsService.ClearServerSession(); + _sessionStateService.ClearServerSession(); WriteServerState(ServerReadinessPhase.Stopped, generationId, "editor-quitting", null); } @@ -449,7 +449,7 @@ private async Task ExecuteTrackedRecoveryAsync(Func recoveryAction) VibeLogger.LogError( "server_recovery_failed", message); - _editorSettingsService.ClearServerSession(); + _sessionStateService.ClearServerSession(); throw new InvalidOperationException(message, ex); } } @@ -569,8 +569,8 @@ public async Task StartRecoveryIfNeededAsync(bool isAfterCompile, CancellationTo if (!started) { // Ensure session reflects stopped state on failure - _editorSettingsService.ClearServerSession(); - _editorSettingsService.ClearReconnectingFlags(); + _sessionStateService.ClearServerSession(); + _sessionStateService.ClearReconnectingFlags(); string message = "Unity CLI Loop server recovery failed because the project IPC endpoint could not be bound within 5000ms."; WriteServerState(ServerReadinessPhase.Failed, generationId, "server-recovery", message); Debug.LogError($"[{UnityCliLoopConstants.PROJECT_NAME}] {message}"); @@ -581,8 +581,8 @@ public async Task StartRecoveryIfNeededAsync(bool isAfterCompile, CancellationTo SaveRunningServerState(); // Clear reconnection-related flags on successful recovery - _editorSettingsService.ClearReconnectingFlags(); - _editorSettingsService.ClearPostCompileReconnectingUI(); + _sessionStateService.ClearReconnectingFlags(); + _sessionStateService.ClearPostCompileReconnectingUI(); UnityCliLoopToolRegistrar.WarmupRegistry(); await MarkServerReadyAsync(generationId, "server-recovery", cancellationToken); @@ -664,7 +664,7 @@ private async Task TryBindWithWaitAsync( private void SaveRunningServerState() { - _editorSettingsService.SetIsServerRunning(true); + _sessionStateService.SetIsServerRunning(true); } private async Task MarkServerReadyAsync( diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs new file mode 100644 index 000000000..c3c0305fc --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs @@ -0,0 +1,101 @@ +using System.IO; +using System.Security; + +using Newtonsoft.Json.Linq; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal interface IUnityCliLoopEditorLegacySessionStateReader + { + UnityCliLoopEditorLegacySessionState Read(); + } + + internal readonly struct UnityCliLoopEditorLegacySessionState + { + internal UnityCliLoopEditorLegacySessionState( + bool isServerRunning, + bool isAfterCompile, + bool isDomainReloadInProgress, + bool isReconnecting, + bool showReconnectingUI, + bool showPostCompileReconnectingUI) + { + IsServerRunning = isServerRunning; + IsAfterCompile = isAfterCompile; + IsDomainReloadInProgress = isDomainReloadInProgress; + IsReconnecting = isReconnecting; + ShowReconnectingUI = showReconnectingUI; + ShowPostCompileReconnectingUI = showPostCompileReconnectingUI; + } + + internal bool HasDomainReloadRecoveryState => IsAfterCompile || IsDomainReloadInProgress; + internal bool IsServerRunning { get; } + internal bool IsAfterCompile { get; } + internal bool IsDomainReloadInProgress { get; } + internal bool IsReconnecting { get; } + internal bool ShowReconnectingUI { get; } + internal bool ShowPostCompileReconnectingUI { get; } + } + + /// + /// Reads legacy JSON runtime flags only for the first reload that crosses the SessionState migration. + /// + internal sealed class UnityCliLoopEditorLegacySessionStateReader : IUnityCliLoopEditorLegacySessionStateReader + { + private string SettingsFilePath => Path.Combine( + UnityCliLoopConstants.USER_SETTINGS_FOLDER, + UnityCliLoopConstants.SETTINGS_FILE_NAME); + + public UnityCliLoopEditorLegacySessionState Read() + { + if (!File.Exists(SettingsFilePath)) + { + return new UnityCliLoopEditorLegacySessionState(); + } + + FileInfo fileInfo = new(SettingsFilePath); + if (fileInfo.Length > UnityCliLoopConstants.MAX_SETTINGS_SIZE_BYTES) + { + throw new SecurityException("Settings file exceeds size limit"); + } + + string json = File.ReadAllText(SettingsFilePath); + if (string.IsNullOrWhiteSpace(json)) + { + return new UnityCliLoopEditorLegacySessionState(); + } + + JToken settingsToken = JToken.Parse(json); + if (settingsToken is not JObject settingsObject) + { + return new UnityCliLoopEditorLegacySessionState(); + } + + return new UnityCliLoopEditorLegacySessionState( + ReadBool(settingsObject, "isServerRunning"), + ReadBool(settingsObject, "isAfterCompile"), + ReadBool(settingsObject, "isDomainReloadInProgress"), + ReadBool(settingsObject, "isReconnecting"), + ReadBool(settingsObject, "showReconnectingUI"), + ReadBool(settingsObject, "showPostCompileReconnectingUI")); + } + + private static bool ReadBool(JObject settingsObject, string propertyName) + { + JToken propertyValue = settingsObject[propertyName]; + if (propertyValue == null) + { + return false; + } + + if (propertyValue.Type != JTokenType.Boolean) + { + return false; + } + + return propertyValue.Value(); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs.meta b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs.meta new file mode 100644 index 000000000..991bb57fd --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf7b8cd529e1a4092abdf1e52cb3839d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs new file mode 100644 index 000000000..e55b80e06 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs @@ -0,0 +1,90 @@ +using UnityEditor; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Stores Unity CLI Loop runtime flags in Unity SessionState for the current Editor session. + /// + public sealed class UnityCliLoopEditorSessionStateRepository : IUnityCliLoopEditorSessionStatePort + { + private const string KeyPrefix = "io.github.hatayama.uloopmcp.editorSession."; + private const string IsServerRunningKey = KeyPrefix + "isServerRunning"; + private const string IsAfterCompileKey = KeyPrefix + "isAfterCompile"; + private const string IsDomainReloadInProgressKey = KeyPrefix + "isDomainReloadInProgress"; + private const string IsReconnectingKey = KeyPrefix + "isReconnecting"; + private const string ShowReconnectingUIKey = KeyPrefix + "showReconnectingUI"; + private const string ShowPostCompileReconnectingUIKey = KeyPrefix + "showPostCompileReconnectingUI"; + + public bool GetIsServerRunning() + { + return GetBool(IsServerRunningKey); + } + + public void SetIsServerRunning(bool isServerRunning) + { + SetBool(IsServerRunningKey, isServerRunning); + } + + public bool GetIsAfterCompile() + { + return GetBool(IsAfterCompileKey); + } + + public void SetIsAfterCompile(bool isAfterCompile) + { + SetBool(IsAfterCompileKey, isAfterCompile); + } + + public bool GetIsDomainReloadInProgress() + { + return GetBool(IsDomainReloadInProgressKey); + } + + public void SetIsDomainReloadInProgress(bool isDomainReloadInProgress) + { + SetBool(IsDomainReloadInProgressKey, isDomainReloadInProgress); + } + + public bool GetIsReconnecting() + { + return GetBool(IsReconnectingKey); + } + + public void SetIsReconnecting(bool isReconnecting) + { + SetBool(IsReconnectingKey, isReconnecting); + } + + public bool GetShowReconnectingUI() + { + return GetBool(ShowReconnectingUIKey); + } + + public void SetShowReconnectingUI(bool showReconnectingUI) + { + SetBool(ShowReconnectingUIKey, showReconnectingUI); + } + + public bool GetShowPostCompileReconnectingUI() + { + return GetBool(ShowPostCompileReconnectingUIKey); + } + + public void SetShowPostCompileReconnectingUI(bool showPostCompileReconnectingUI) + { + SetBool(ShowPostCompileReconnectingUIKey, showPostCompileReconnectingUI); + } + + private static bool GetBool(string key) + { + return SessionState.GetBool(key, false); + } + + private static void SetBool(string key, bool value) + { + SessionState.SetBool(key, value); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs.meta b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs.meta new file mode 100644 index 000000000..8dd31817b --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSessionStateRepository.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 788cfe30063544c2e871e8531971f4e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs index 0ae2b53fb..dae929f2d 100644 --- a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs @@ -28,7 +28,13 @@ public sealed class UnityCliLoopEditorSettingsRepository : IUnityCliLoopEditorSe "serverTransportKind", "projectRootPath", "serverSessionId", - "connectedLLMTools" + "connectedLLMTools", + "isServerRunning", + "isAfterCompile", + "isDomainReloadInProgress", + "isReconnecting", + "showReconnectingUI", + "showPostCompileReconnectingUI" }; private UnityCliLoopEditorSettingsData _cachedSettings; @@ -154,125 +160,6 @@ public void SetInstallSkillsFlat(bool installSkillsFlat) SaveSettings(newSettings); } - /// - /// Gets the server running state. - /// - public bool GetIsServerRunning() - { - return GetSettings().isServerRunning; - } - - /// - /// Sets the server running state. - /// - public void SetIsServerRunning(bool isServerRunning) - { - UnityCliLoopEditorSettingsData settings = GetSettings(); - UnityCliLoopEditorSettingsData newSettings = settings with { isServerRunning = isServerRunning }; - SaveSettings(newSettings); - } - - /// - /// Gets the after compile flag. - /// - public bool GetIsAfterCompile() - { - return GetSettings().isAfterCompile; - } - - /// - /// Gets the domain reload in progress flag. - /// - public bool GetIsDomainReloadInProgress() - { - return GetSettings().isDomainReloadInProgress; - } - - /// - /// Sets the domain reload in progress flag. - /// - public void SetIsDomainReloadInProgress(bool isDomainReloadInProgress) - { - UnityCliLoopEditorSettingsData settings = GetSettings(); - UnityCliLoopEditorSettingsData newSettings = settings with { isDomainReloadInProgress = isDomainReloadInProgress }; - SaveSettings(newSettings); - } - - /// - /// Sets the reconnecting flag. - /// - public void SetIsReconnecting(bool isReconnecting) - { - UnityCliLoopEditorSettingsData settings = GetSettings(); - UnityCliLoopEditorSettingsData newSettings = settings with { isReconnecting = isReconnecting }; - SaveSettings(newSettings); - } - - /// - /// Gets the show reconnecting UI flag. - /// - public bool GetShowReconnectingUI() - { - return GetSettings().showReconnectingUI; - } - - /// - /// Sets the show reconnecting UI flag. - /// - public void SetShowReconnectingUI(bool showReconnectingUI) - { - UnityCliLoopEditorSettingsData settings = GetSettings(); - UnityCliLoopEditorSettingsData newSettings = settings with { showReconnectingUI = showReconnectingUI }; - SaveSettings(newSettings); - } - - /// - /// Clear server session. - /// - public void ClearServerSession() - { - UpdateSettings(settings => settings with - { - isServerRunning = false - }); - } - - /// - /// Clear after compile flag. - /// - public void ClearAfterCompileFlag() - { - UpdateSettings(s => s with { isAfterCompile = false }); - } - - /// - /// Clear reconnecting flags. - /// - public void ClearReconnectingFlags() - { - UpdateSettings(s => s with - { - isReconnecting = false, - showReconnectingUI = false - }); - } - - /// - /// Clear post compile reconnecting UI. - /// - public void ClearPostCompileReconnectingUI() - { - UpdateSettings(s => s with { showPostCompileReconnectingUI = false }); - } - - /// - /// Clear domain reload flag. - /// - public void ClearDomainReloadFlag() - { - SetIsDomainReloadInProgress(false); - } - /// /// Loads the settings file. /// diff --git a/Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs b/Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs index d853d0a03..992ef16ef 100644 --- a/Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs +++ b/Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs @@ -35,34 +35,23 @@ public event Action ServerStopping } public event Action ServerLoopExited; private readonly IDomainReloadDetectionService _domainReloadDetectionService; - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; public UnityCliLoopBridgeServerInstanceFactory() - : this(CreateDefaultEditorSettingsService()) + : this(new DomainReloadDetectionFileService()) { } - private UnityCliLoopBridgeServerInstanceFactory(UnityCliLoopEditorSettingsService editorSettingsService) - : this(new DomainReloadDetectionFileService(editorSettingsService), editorSettingsService) - { - } - - internal UnityCliLoopBridgeServerInstanceFactory( - IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService) + internal UnityCliLoopBridgeServerInstanceFactory(IDomainReloadDetectionService domainReloadDetectionService) { System.Diagnostics.Debug.Assert(domainReloadDetectionService != null, "domainReloadDetectionService must not be null"); - System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); _domainReloadDetectionService = domainReloadDetectionService ?? throw new ArgumentNullException(nameof(domainReloadDetectionService)); - _editorSettingsService = editorSettingsService - ?? throw new ArgumentNullException(nameof(editorSettingsService)); } public IUnityCliLoopServerInstance Create() { - UnityCliLoopBridgeServer server = new(_domainReloadDetectionService, _editorSettingsService); + UnityCliLoopBridgeServer server = new(_domainReloadDetectionService); server.ServerLoopExited += NotifyServerLoopExited; return server; @@ -72,11 +61,6 @@ private void NotifyServerLoopExited() { ServerLoopExited?.Invoke(); } - - private static UnityCliLoopEditorSettingsService CreateDefaultEditorSettingsService() - { - return new UnityCliLoopEditorSettingsService(new UnityCliLoopEditorSettingsRepository()); - } } /// @@ -92,7 +76,6 @@ public class UnityCliLoopBridgeServer : IUnityCliLoopServerInstance // Subscribers must marshal to main thread before accessing Unity APIs. public event Action ServerLoopExited; private readonly IDomainReloadDetectionService _domainReloadDetectionService; - private readonly UnityCliLoopEditorSettingsService _editorSettingsService; // HResult error codes for normal disconnection detection private static readonly HashSet NormalDisconnectionHResults = new() @@ -118,31 +101,16 @@ public class UnityCliLoopBridgeServer : IUnityCliLoopServerInstance private const int ClientDisconnectMonitorPollMilliseconds = 100; public UnityCliLoopBridgeServer() - : this(CreateDefaultEditorSettingsService()) + : this(new DomainReloadDetectionFileService()) { } - private UnityCliLoopBridgeServer(UnityCliLoopEditorSettingsService editorSettingsService) - : this(new DomainReloadDetectionFileService(editorSettingsService), editorSettingsService) - { - } - - internal UnityCliLoopBridgeServer( - IDomainReloadDetectionService domainReloadDetectionService, - UnityCliLoopEditorSettingsService editorSettingsService) + internal UnityCliLoopBridgeServer(IDomainReloadDetectionService domainReloadDetectionService) { System.Diagnostics.Debug.Assert(domainReloadDetectionService != null, "domainReloadDetectionService must not be null"); - System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); _domainReloadDetectionService = domainReloadDetectionService ?? throw new ArgumentNullException(nameof(domainReloadDetectionService)); - _editorSettingsService = editorSettingsService - ?? throw new ArgumentNullException(nameof(editorSettingsService)); - } - - private static UnityCliLoopEditorSettingsService CreateDefaultEditorSettingsService() - { - return new UnityCliLoopEditorSettingsService(new UnityCliLoopEditorSettingsRepository()); } /// @@ -383,7 +351,7 @@ private async Task ServerLoopAsync(CancellationToken cancellationToken) catch (ThreadAbortException ex) { // Log and re-throw ThreadAbortException - if (!_editorSettingsService.GetIsDomainReloadInProgress()) + if (!DomainReloadStateRegistry.IsDomainReloadInProgress()) { OnError?.Invoke($"Unexpected thread abort: {ex.Message}"); } @@ -451,7 +419,7 @@ private async Task AcceptClientAsync(IBridgeTransportLis catch (ThreadAbortException ex) { // Log and re-throw ThreadAbortException - if (!_editorSettingsService.GetIsDomainReloadInProgress()) + if (!DomainReloadStateRegistry.IsDomainReloadInProgress()) { OnError?.Invoke($"Unexpected thread abort: {ex.Message}"); } diff --git a/Packages/src/Editor/Presentation/PresentationEditorStartup.cs b/Packages/src/Editor/Presentation/PresentationEditorStartup.cs index 9f9b0d2c1..65b57756d 100644 --- a/Packages/src/Editor/Presentation/PresentationEditorStartup.cs +++ b/Packages/src/Editor/Presentation/PresentationEditorStartup.cs @@ -7,9 +7,11 @@ namespace io.github.hatayama.UnityCliLoop.Presentation /// internal static class PresentationEditorStartup { - internal static void Initialize(UnityCliLoopEditorSettingsService editorSettingsService) + internal static void Initialize( + UnityCliLoopEditorSettingsService editorSettingsService, + UnityCliLoopEditorSessionStateService sessionStateService) { - UnityCliLoopSettingsWindow.InitializeEditorServices(editorSettingsService); + UnityCliLoopSettingsWindow.InitializeEditorServices(editorSettingsService, sessionStateService); SetupWizardWindow.InitializeForEditorStartup(editorSettingsService); ThirdPartyToolMigrationWizardWindow.InitializeForEditorStartup(); } diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs index 05c0c5dc8..e439783de 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs @@ -24,6 +24,7 @@ public class UnityCliLoopSettingsWindow : EditorWindow private const int ToolSettingsRegistryWarmupMaxAttempts = 5; private static UnityCliLoopEditorSettingsService RegisteredEditorSettingsService; + private static UnityCliLoopEditorSessionStateService RegisteredSessionStateService; private UnityCliLoopSettingsWindowUI _view; private UnityCliLoopSettingsModel _model; @@ -31,6 +32,7 @@ public class UnityCliLoopSettingsWindow : EditorWindow private SkillSetupUseCase _skillSetupUseCase; private ToolSettingsUseCase _toolSettingsUseCase; private UnityCliLoopEditorSettingsService _editorSettingsService; + private UnityCliLoopEditorSessionStateService _sessionStateService; private SkillsTarget _skillsTarget = SkillsTarget.Claude; private bool _installSkillsFlat; @@ -54,12 +56,17 @@ public static void ShowWindow() window.Show(); } - internal static void InitializeEditorServices(UnityCliLoopEditorSettingsService editorSettingsService) + internal static void InitializeEditorServices( + UnityCliLoopEditorSettingsService editorSettingsService, + UnityCliLoopEditorSessionStateService sessionStateService) { System.Diagnostics.Debug.Assert(editorSettingsService != null, "editorSettingsService must not be null"); + System.Diagnostics.Debug.Assert(sessionStateService != null, "sessionStateService must not be null"); RegisteredEditorSettingsService = editorSettingsService ?? throw new ArgumentNullException(nameof(editorSettingsService)); + RegisteredSessionStateService = sessionStateService + ?? throw new ArgumentNullException(nameof(sessionStateService)); } private void OnEnable() @@ -106,6 +113,7 @@ private void InitializeApplicationServices() _skillSetupUseCase = SkillSetupUseCaseRegistry.GetRegisteredUseCase(); _toolSettingsUseCase = ToolSettingsUseCaseRegistry.GetRegisteredUseCase(); _editorSettingsService = GetEditorSettingsService(); + _sessionStateService = GetSessionStateService(); } private void InitializeView() @@ -156,7 +164,7 @@ private void RestoreSessionState() private async void HandlePostCompileMode() { _model.EnablePostCompileMode(); - _editorSettingsService.SetShowReconnectingUI(false); + _sessionStateService.SetShowReconnectingUI(false); Task recoveryTask = UnityCliLoopServerApplicationFacade.RecoveryTask; if (recoveryTask != null && !recoveryTask.IsCompleted) @@ -164,11 +172,11 @@ private async void HandlePostCompileMode() await recoveryTask; } - bool isAfterCompile = _editorSettingsService.GetIsAfterCompile(); + bool isAfterCompile = _sessionStateService.GetIsAfterCompile(); if (isAfterCompile) { - _editorSettingsService.ClearAfterCompileFlag(); + _sessionStateService.ClearAfterCompileFlag(); return; } @@ -866,6 +874,16 @@ private static UnityCliLoopEditorSettingsService GetEditorSettingsService() return RegisteredEditorSettingsService; } + private static UnityCliLoopEditorSessionStateService GetSessionStateService() + { + if (RegisteredSessionStateService == null) + { + throw new InvalidOperationException("Unity CLI Loop editor session state service is not registered."); + } + + return RegisteredSessionStateService; + } + private void HandleRefreshSkillsState() { RefreshSelectedTargetInstallStateFast(); From 7176339788efc883163ae80e5f74f87b38bb6996 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 19 May 2026 10:14:00 +0900 Subject: [PATCH 2/2] Consume migrated legacy session flags Remove legacy JSON recovery flags immediately after applying them to SessionState so upgrade migration cannot reapply stale reload state on later domain reloads. Also align the newly added settings-window session registration with the existing Debug.Assert contract. --- .../DomainReloadDetectionServiceTests.cs | 71 +++++++++++++++++++ .../DomainReloadDetectionFileService.cs | 2 + ...tyCliLoopEditorLegacySessionStateReader.cs | 50 +++++++++++++ .../UnityCliLoopSettingsWindow.cs | 3 +- 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs index 1843250ae..5307cb2a8 100644 --- a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs +++ b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs @@ -1,8 +1,11 @@ +using System.IO; + using NUnit.Framework; using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { @@ -11,14 +14,27 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class DomainReloadDetectionServiceTests { + private static readonly string SettingsFilePath = + Path.Combine(UnityCliLoopConstants.USER_SETTINGS_FOLDER, UnityCliLoopConstants.SETTINGS_FILE_NAME); + private UnityCliLoopEditorSessionStateService _sessionStateService; private UnityCliLoopEditorSessionStateSnapshot _originalSessionState; private IDomainReloadDetectionService _domainReloadDetectionService; private ServerReadinessStateStore _stateStore; + private bool _settingsFileExisted; + private string _settingsFileContent; [SetUp] public void SetUp() { + _settingsFileExisted = File.Exists(SettingsFilePath); + _settingsFileContent = _settingsFileExisted ? File.ReadAllText(SettingsFilePath) : null; + if (!Directory.Exists(UnityCliLoopConstants.USER_SETTINGS_FOLDER)) + { + Directory.CreateDirectory(UnityCliLoopConstants.USER_SETTINGS_FOLDER); + } + + DeleteIfExists(SettingsFilePath); _sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); _originalSessionState = UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(_sessionStateService); _sessionStateService.ClearAll(); @@ -33,6 +49,7 @@ public void TearDown() _originalSessionState.Restore(_sessionStateService); UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); _stateStore.Delete(); + RestoreFile(SettingsFilePath, _settingsFileExisted, _settingsFileContent); } [Test] @@ -117,6 +134,37 @@ public void CompleteDomainReload_WhenLegacyStateOnlySaysRunning_IgnoresStaleRunn Assert.That(state.Phase, Is.EqualTo("stopped")); } + [Test] + public void CompleteDomainReload_WhenLegacyReloadStateWasMigrated_DoesNotReapplyLegacyJson() + { + // Verifies that legacy JSON recovery state is consumed after the first migration reload. + File.WriteAllText( + SettingsFilePath, + "{" + + "\"isServerRunning\":true," + + "\"isAfterCompile\":true," + + "\"isDomainReloadInProgress\":true," + + "\"isReconnecting\":true," + + "\"showReconnectingUI\":true," + + "\"showPostCompileReconnectingUI\":true" + + "}"); + _domainReloadDetectionService = new DomainReloadDetectionFileService( + _sessionStateService, + _stateStore, + new UnityCliLoopEditorLegacySessionStateReader()); + + _domainReloadDetectionService.CompleteDomainReload("first-correlation"); + _sessionStateService.ClearAll(); + + _domainReloadDetectionService.CompleteDomainReload("second-correlation"); + + Assert.That(_sessionStateService.GetIsServerRunning(), Is.False); + Assert.That(_sessionStateService.GetIsAfterCompile(), Is.False); + Assert.That(_sessionStateService.GetIsReconnecting(), Is.False); + ServerReadinessState state = _stateStore.Read(); + Assert.That(state.Phase, Is.EqualTo("stopped")); + } + private static ServerReadinessStateStore CreateTestStateStore() { string projectRoot = System.IO.Path.Combine( @@ -126,6 +174,25 @@ private static ServerReadinessStateStore CreateTestStateStore() return new ServerReadinessStateStore(projectRoot); } + private static void RestoreFile(string path, bool existed, string content) + { + if (existed) + { + File.WriteAllText(path, content); + return; + } + + DeleteIfExists(path); + } + + private static void DeleteIfExists(string path) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + private sealed class TestLegacySessionStateReader : IUnityCliLoopEditorLegacySessionStateReader { private readonly UnityCliLoopEditorLegacySessionState _legacySessionState; @@ -139,6 +206,10 @@ public UnityCliLoopEditorLegacySessionState Read() { return _legacySessionState; } + + public void Clear() + { + } } } } diff --git a/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs b/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs index 509605a8b..849a34311 100644 --- a/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs +++ b/Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs @@ -208,6 +208,8 @@ private void MigrateLegacySessionStateIfNeeded() { _sessionStateService.SetShowPostCompileReconnectingUI(true); } + + _legacySessionStateReader.Clear(); } } } diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs index c3c0305fc..4365e6eec 100644 --- a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs @@ -1,6 +1,7 @@ using System.IO; using System.Security; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -10,6 +11,7 @@ namespace io.github.hatayama.UnityCliLoop.Infrastructure internal interface IUnityCliLoopEditorLegacySessionStateReader { UnityCliLoopEditorLegacySessionState Read(); + void Clear(); } internal readonly struct UnityCliLoopEditorLegacySessionState @@ -44,6 +46,16 @@ internal UnityCliLoopEditorLegacySessionState( /// internal sealed class UnityCliLoopEditorLegacySessionStateReader : IUnityCliLoopEditorLegacySessionStateReader { + private static readonly string[] LegacySessionStateKeys = + { + "isServerRunning", + "isAfterCompile", + "isDomainReloadInProgress", + "isReconnecting", + "showReconnectingUI", + "showPostCompileReconnectingUI" + }; + private string SettingsFilePath => Path.Combine( UnityCliLoopConstants.USER_SETTINGS_FOLDER, UnityCliLoopConstants.SETTINGS_FILE_NAME); @@ -82,6 +94,44 @@ public UnityCliLoopEditorLegacySessionState Read() ReadBool(settingsObject, "showPostCompileReconnectingUI")); } + public void Clear() + { + if (!File.Exists(SettingsFilePath)) + { + return; + } + + FileInfo fileInfo = new(SettingsFilePath); + if (fileInfo.Length > UnityCliLoopConstants.MAX_SETTINGS_SIZE_BYTES) + { + throw new SecurityException("Settings file exceeds size limit"); + } + + JToken settingsToken; + using (StreamReader reader = File.OpenText(SettingsFilePath)) + { + settingsToken = JToken.ReadFrom(new JsonTextReader(reader)); + } + + if (settingsToken is not JObject settingsObject) + { + return; + } + + bool removed = false; + foreach (string legacyKey in LegacySessionStateKeys) + { + removed |= settingsObject.Remove(legacyKey); + } + + if (!removed) + { + return; + } + + AtomicFileWriter.Write(SettingsFilePath, settingsToken.ToString(Formatting.Indented)); + } + private static bool ReadBool(JObject settingsObject, string propertyName) { JToken propertyValue = settingsObject[propertyName]; diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs index e439783de..33affb364 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs @@ -65,8 +65,7 @@ internal static void InitializeEditorServices( RegisteredEditorSettingsService = editorSettingsService ?? throw new ArgumentNullException(nameof(editorSettingsService)); - RegisteredSessionStateService = sessionStateService - ?? throw new ArgumentNullException(nameof(sessionStateService)); + RegisteredSessionStateService = sessionStateService; } private void OnEnable()