Repository navigation
[Bug] Windows focus nudge focuses the wrong editor, never restores focus, and loops forever #1407
Description
Summary
On Windows with more than one Unity editor open, the test-job focus nudge (utils/focus_nudge.py, called from services/tools/run_tests.py) repeatedly pulls an unrelated Unity editor to the foreground, never gives focus back, and keeps doing it for as long as the job reports running. In practice the desktop is unusable while an agent polls get_test_job: a Unity window takes the foreground for up to 12 seconds out of every ~17.
Environment
- Windows 11 Pro 10.0.26200
mcpforunityserver10.1.0 (the relevant code is identical in 10.2.0), stdio transport- Unity 6000.3.9f1, four editors open at once (four worktrees of one project)
runInBackground: 1in Player Settings, so PlayMode tests do not need focus to progress
Steps to reproduce
- Open two or more Unity editors on Windows.
- From an MCP client, call
run_testswithmode: PlayModeagainst the editor that is not the lowest-PID Unity process, using a test that runs longer than 3 seconds. - Poll
get_test_jobwithwait_timeout: 60while a non-Unity window has focus.
Expected
If a nudge is needed at all, the editor running the job is focused briefly and focus then returns to the window I was using.
Actual
Four separate problems, all in the Windows path:
1. The wrong editor is focused. _focus_app passes unity_project_path only to the macOS implementation. _focus_app_windows(window_title) takes no project path and runs:
$unity = Get-Process | Where-Object {$_.MainWindowTitle -like "*Unity*"} | Select-Object -First 1That is simply the lowest-PID process with "Unity" in its title, regardless of which editor owns the job. (It would also match any non-Unity window whose title contains "Unity".) Because the editor that owns the job never receives focus, the nudge cannot un-stall it, so reset_nudge_backoff() is never reached and the loop runs at maximum cadence.
2. Focus is never restored. _get_frontmost_app_windows does not discard the return value of GetWindowText:
[Win32]::GetWindowText($hwnd, $sb, 256) # int return value is written to stdout
$sb.ToString()so stdout is "<length>\n<title>" and that whole string becomes the app name. The server log shows it:
utils.focus_nudge - INFO - Nudging Unity focus (interval: 10.0s, consecutive: 6, duration: 12.0s, will return to 139
<window title here>)
The restore step then looks for MainWindowTitle -eq '139\n<title>', which never matches. The PowerShell script still exits 0, so the server logs Returned focus to 139... even though nothing happened. Fix: [void][Win32]::GetWindowText(...) or | Out-Null. Restoring by the saved hwnd rather than by title would be more robust, since titles change and are not unique.
3. The stall heuristic fires on healthy tests. should_nudge treats "no last_update_unix_ms change for 3s + editor unfocused" as a stall. TestJobManager only bumps LastUpdateUnixMs on run start, test start, test finish and run finish, so any single test longer than 3 seconds counts as stalled, even when the editor is running in the background and the test is progressing normally.
4. There is no cap and no opt-out. A job that never progresses (in my case a test that hung at completed=0, total=2, then lost its current-test state across a domain reload and stayed running) is nudged indefinitely at 12s focus / 10s interval. The only environment variables are UNITY_MCP_NUDGE_BASE_INTERVAL_S, UNITY_MCP_NUDGE_MAX_INTERVAL_S and UNITY_MCP_NUDGE_DURATION_S, and _parse_env_float rejects values <= 0, so there is no supported way to switch it off.
Also worth noting: the nudge log line had no for <project path> suffix, so _get_unity_project_path(unity_instance) resolved to None for a stdio session with an explicitly pinned instance. On Windows that makes no difference today because of problem 1, but it will matter once the Windows path honours the project path.
Log excerpt
13:44:37 Tool 'run_tests' returned: ... job_id='5cab…', status='running', mode='PlayMode'
13:44:41 Tool 'get_test_job' called with ... 'wait_timeout': 60
13:44:44 Test job 5cab… appears stalled (unfocused Unity), attempting nudge...
13:44:44 Nudging Unity focus (interval: 1.0s, consecutive: 0, duration: 3.0s, will return to 6
13:44:47 Unity activation didn't complete - current app is 6
...
13:45:40 Nudging Unity focus (interval: 8.0s, consecutive: 3, duration: 12.0s, will return to 97
13:45:59 Nudging Unity focus (interval: 10.0s, consecutive: 4, duration: 12.0s, will return to 6
(repeats every ~17s until the client exited play mode at 13:50:02)
Suggested fixes
- Windows: resolve the target editor by project path, for example by matching the
-projectpathargument inWin32_Process.CommandLine, the same way the macOS path uses_find_unity_pid_by_project_path. If no match is found, do not nudge rather than focusing an arbitrary editor. - Windows: suppress the
GetWindowTextreturn value, and restore focus byhwnd. - Add an explicit opt-out, for example
UNITY_MCP_DISABLE_FOCUS_NUDGE=1. - Cap consecutive nudges that produce no progress (three or four), then stop and surface
stuck_suspectedto the client instead. - Skip the nudge when the editor reports
Application.runInBackground == true, or have the editor send a heartbeat during long tests so a slow test is not mistaken for a stalled one.
Workaround
Set both UNITY_MCP_NUDGE_BASE_INTERVAL_S and UNITY_MCP_NUDGE_MAX_INTERVAL_S to 1000000000 in the environment the server is launched from. _last_nudge_time starts at 0.0 and time.monotonic() never reaches 1e9, so the rate limiter skips every nudge.
Activity