Skip to content

[Bug] Windows focus nudge focuses the wrong editor, never restores focus, and loops forever #1407

Description

@JasonAlwaysGeeky

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
  • mcpforunityserver 10.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: 1 in Player Settings, so PlayMode tests do not need focus to progress

Steps to reproduce

  1. Open two or more Unity editors on Windows.
  2. From an MCP client, call run_tests with mode: PlayMode against the editor that is not the lowest-PID Unity process, using a test that runs longer than 3 seconds.
  3. Poll get_test_job with wait_timeout: 60 while 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 1

That 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 -projectpath argument in Win32_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 GetWindowText return value, and restore focus by hwnd.
  • 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_suspected to 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions