diff --git a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs index 977ed3a86..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,27 +14,42 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class DomainReloadDetectionServiceTests { - private UnityCliLoopEditorSettingsData _originalSettings; - private UnityCliLoopEditorSettingsService _editorSettingsService; + 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() { - _editorSettingsService = UnityCliLoopEditorSettingsTestFactory.CreateService(); - _originalSettings = CloneSettings(_editorSettingsService.GetSettings()); + _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(); _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(); + RestoreFile(SettingsFilePath, _settingsFileExisted, _settingsFileContent); } [Test] @@ -43,34 +61,108 @@ 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")); + } + + [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() @@ -81,5 +173,43 @@ private static ServerReadinessStateStore CreateTestStateStore() System.Guid.NewGuid().ToString("N")); 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; + + internal TestLegacySessionStateReader(UnityCliLoopEditorLegacySessionState legacySessionState) + { + _legacySessionState = legacySessionState; + } + + public UnityCliLoopEditorLegacySessionState Read() + { + return _legacySessionState; + } + + public void Clear() + { + } + } } } 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..849a34311 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,48 @@ 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); + } + + _legacySessionStateReader.Clear(); } } } 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..4365e6eec --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorLegacySessionStateReader.cs @@ -0,0 +1,151 @@ +using System.IO; +using System.Security; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal interface IUnityCliLoopEditorLegacySessionStateReader + { + UnityCliLoopEditorLegacySessionState Read(); + void Clear(); + } + + 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 static readonly string[] LegacySessionStateKeys = + { + "isServerRunning", + "isAfterCompile", + "isDomainReloadInProgress", + "isReconnecting", + "showReconnectingUI", + "showPostCompileReconnectingUI" + }; + + 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")); + } + + 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]; + 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..33affb364 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,16 @@ 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; } private void OnEnable() @@ -106,6 +112,7 @@ private void InitializeApplicationServices() _skillSetupUseCase = SkillSetupUseCaseRegistry.GetRegisteredUseCase(); _toolSettingsUseCase = ToolSettingsUseCaseRegistry.GetRegisteredUseCase(); _editorSettingsService = GetEditorSettingsService(); + _sessionStateService = GetSessionStateService(); } private void InitializeView() @@ -156,7 +163,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 +171,11 @@ private async void HandlePostCompileMode() await recoveryTask; } - bool isAfterCompile = _editorSettingsService.GetIsAfterCompile(); + bool isAfterCompile = _sessionStateService.GetIsAfterCompile(); if (isAfterCompile) { - _editorSettingsService.ClearAfterCompileFlag(); + _sessionStateService.ClearAfterCompileFlag(); return; } @@ -866,6 +873,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();