diff --git a/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/InputSimulationRunInBackgroundScope.cs b/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/InputSimulationRunInBackgroundScope.cs index c3a0380bc6..340e6f778f 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/InputSimulationRunInBackgroundScope.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/InputSimulationRunInBackgroundScope.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using io.github.hatayama.UnityCliLoop.ToolContracts; + namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// @@ -29,6 +31,12 @@ public static InputSimulationRunInBackgroundScope Enable() public void Dispose() { + // Precondition: Application.runInBackground is main-thread-only, so callers resuming + // from ConfigureAwait(false) continuations must switch back to the main thread first. + UnityEngine.Debug.Assert( + MainThreadSwitcher.IsMainThread, + "InputSimulationRunInBackgroundScope.Dispose must be called on the main thread."); + if (_isDisposed) { return; diff --git a/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/UnityCLILoop.FirstPartyTools.Common.InputSimulation.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/UnityCLILoop.FirstPartyTools.Common.InputSimulation.Editor.asmdef index 830a323f36..763c0aa031 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/UnityCLILoop.FirstPartyTools.Common.InputSimulation.Editor.asmdef +++ b/Packages/src/Editor/FirstPartyTools/Common/InputSimulation/UnityCLILoop.FirstPartyTools.Common.InputSimulation.Editor.asmdef @@ -1,7 +1,9 @@ { "name": "UnityCLILoop.FirstPartyTools.Common.InputSimulation.Editor", "rootNamespace": "io.github.hatayama.UnityCliLoop.FirstPartyTools", - "references": [], + "references": [ + "GUID:fc3fd32eddbee40e39c2d76dc184957b" + ], "includePlatforms": [ "Editor" ], diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs index 3c539fe5ad..f467917a12 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs @@ -104,49 +104,59 @@ public async Task ExecuteAsync( correlationId: correlationId ); - using InputSimulationRunInBackgroundScope runInBackgroundScope = InputSimulationRunInBackgroundScope.Enable(); - - EnsureOverlayExists(); + // Why not `using`: the executor awaits below use ConfigureAwait(false), so this method + // can resume on a thread-pool thread. Application.runInBackground is main-thread-only, + // so the scope must be disposed after switching back to the main thread. + InputSimulationRunInBackgroundScope runInBackgroundScope = InputSimulationRunInBackgroundScope.Enable(); + try + { + EnsureOverlayExists(); - SimulateKeyboardResponse response; + SimulateKeyboardResponse response; - switch (parameters.Action) + switch (parameters.Action) + { + case UnityCliLoopKeyboardAction.Press: + response = await KeyboardInputActionExecutor.ExecutePress( + keyboard, + key, + parameters.Duration, + ct).ConfigureAwait(false); + break; + + case UnityCliLoopKeyboardAction.KeyDown: + response = await KeyboardInputActionExecutor.ExecuteKeyDown(keyboard, key, ct).ConfigureAwait(false); + break; + + case UnityCliLoopKeyboardAction.KeyUp: + response = await KeyboardInputActionExecutor.ExecuteKeyUp(keyboard, key, ct).ConfigureAwait(false); + break; + + default: + // Only reachable when an out-of-range enum value is cast from an integer; + // surface as a Success=false response so the CLI treats it as a normal validation failure. + return new SimulateKeyboardResponse + { + Success = false, + Message = $"Unknown keyboard action: {parameters.Action}", + Action = parameters.Action.ToString() + }; + } + + VibeLogger.LogInfo( + "simulate_keyboard_complete", + $"Keyboard simulation completed: {response.Message}", + new { Action = parameters.Action.ToString(), Success = response.Success }, + correlationId: correlationId + ); + + return response; + } + finally { - case UnityCliLoopKeyboardAction.Press: - response = await KeyboardInputActionExecutor.ExecutePress( - keyboard, - key, - parameters.Duration, - ct).ConfigureAwait(false); - break; - - case UnityCliLoopKeyboardAction.KeyDown: - response = await KeyboardInputActionExecutor.ExecuteKeyDown(keyboard, key, ct).ConfigureAwait(false); - break; - - case UnityCliLoopKeyboardAction.KeyUp: - response = await KeyboardInputActionExecutor.ExecuteKeyUp(keyboard, key, ct).ConfigureAwait(false); - break; - - default: - // Only reachable when an out-of-range enum value is cast from an integer; - // surface as a Success=false response so the CLI treats it as a normal validation failure. - return new SimulateKeyboardResponse - { - Success = false, - Message = $"Unknown keyboard action: {parameters.Action}", - Action = parameters.Action.ToString() - }; + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + runInBackgroundScope.Dispose(); } - - VibeLogger.LogInfo( - "simulate_keyboard_complete", - $"Keyboard simulation completed: {response.Message}", - new { Action = parameters.Action.ToString(), Success = response.Success }, - correlationId: correlationId - ); - - return response; #endif } diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs index ef3de4fe4a..9bc0fa565d 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs @@ -97,46 +97,56 @@ public async Task ExecuteAsync( correlationId: correlationId ); - using InputSimulationRunInBackgroundScope runInBackgroundScope = InputSimulationRunInBackgroundScope.Enable(); - - EnsureOverlayExists(); - - SimulateMouseInputResponse response; - - switch (parameters.Action) + // Why not `using`: the executor awaits below use ConfigureAwait(false), so this method + // can resume on a thread-pool thread. Application.runInBackground is main-thread-only, + // so the scope must be disposed after switching back to the main thread. + InputSimulationRunInBackgroundScope runInBackgroundScope = InputSimulationRunInBackgroundScope.Enable(); + try { - case UnityCliLoopMouseInputAction.Click: - response = await MouseInputPressActionExecutor.ExecuteClick(mouse, parameters, ct).ConfigureAwait(false); - break; - - case UnityCliLoopMouseInputAction.LongPress: - response = await MouseInputPressActionExecutor.ExecuteLongPress(mouse, parameters, ct).ConfigureAwait(false); - break; + EnsureOverlayExists(); - case UnityCliLoopMouseInputAction.MoveDelta: - response = await MouseInputMotionActionExecutor.ExecuteMoveDelta(mouse, parameters, ct).ConfigureAwait(false); - break; + SimulateMouseInputResponse response; - case UnityCliLoopMouseInputAction.Scroll: - response = await MouseInputMotionActionExecutor.ExecuteScroll(mouse, parameters, ct).ConfigureAwait(false); - break; - - case UnityCliLoopMouseInputAction.SmoothDelta: - response = await MouseInputMotionActionExecutor.ExecuteSmoothDelta(mouse, parameters, ct).ConfigureAwait(false); - break; - - default: - throw new ArgumentException($"Unknown mouse input action: {parameters.Action}"); + switch (parameters.Action) + { + case UnityCliLoopMouseInputAction.Click: + response = await MouseInputPressActionExecutor.ExecuteClick(mouse, parameters, ct).ConfigureAwait(false); + break; + + case UnityCliLoopMouseInputAction.LongPress: + response = await MouseInputPressActionExecutor.ExecuteLongPress(mouse, parameters, ct).ConfigureAwait(false); + break; + + case UnityCliLoopMouseInputAction.MoveDelta: + response = await MouseInputMotionActionExecutor.ExecuteMoveDelta(mouse, parameters, ct).ConfigureAwait(false); + break; + + case UnityCliLoopMouseInputAction.Scroll: + response = await MouseInputMotionActionExecutor.ExecuteScroll(mouse, parameters, ct).ConfigureAwait(false); + break; + + case UnityCliLoopMouseInputAction.SmoothDelta: + response = await MouseInputMotionActionExecutor.ExecuteSmoothDelta(mouse, parameters, ct).ConfigureAwait(false); + break; + + default: + throw new ArgumentException($"Unknown mouse input action: {parameters.Action}"); + } + + VibeLogger.LogInfo( + "simulate_mouse_input_complete", + $"Mouse input simulation completed: {response.Message}", + new { Action = parameters.Action.ToString(), Success = response.Success }, + correlationId: correlationId + ); + + return response; + } + finally + { + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + runInBackgroundScope.Dispose(); } - - VibeLogger.LogInfo( - "simulate_mouse_input_complete", - $"Mouse input simulation completed: {response.Message}", - new { Action = parameters.Action.ToString(), Success = response.Success }, - correlationId: correlationId - ); - - return response; #endif }