Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ cd ./MyGame && unity-mcp-cli open
| `--launch-dismiss-timeout-ms <ms>` | — | No | Overall timeout (milliseconds) for the launch-errors auto-dismiss polling loop (default: `30000`) |
| `--launch-dismiss-poll-interval-ms <ms>` | — | No | Polling tick interval (milliseconds) for the launch-errors auto-dismiss loop (default: `1500`) |

The editor process is spawned in detached mode. By default, after spawning the editor, `open` polls for Unity's "compile errors at launch" dialog and clicks `Ignore` so the editor finishes initialising — without this, any in-Editor automation that needs to run after a state where Unity itself can't compile (e.g. the NuGet dependency resolver) cannot self-heal. The dialog appears within seconds of editor launch when it appears at all, so when no dialog has been seen within a short grace window after polling starts the loop exits early — the no-dialog case adds at most that grace window's delay, never the full `--launch-dismiss-timeout-ms`. If the dialog is observed (and successfully dismissed), polling continues until the overall timeout so a re-appearing dialog (resolver fixes one error → dialog re-surfaces with the next) is dismissed again. Library-mode callers can supply an `AbortSignal` (`launchDismissAbortSignal` on `OpenProjectOptions`) to abort the loop the instant their own readiness signal fires.
The editor process is spawned in detached mode. By default, after spawning the editor, `open` polls for Unity's "compile errors at launch" dialog (`"Enter Safe Mode?"` on Unity 2020.2+, `"Hold On" / "Compiler Errors"` on older releases) and clicks `Ignore` so the editor finishes initialising — without this, any in-Editor automation that needs to run after a state where Unity itself can't compile (e.g. the NuGet dependency resolver) cannot self-heal. The dialog is surfaced after Unity has booted, connected to Package Manager, and started compiling — empirically ~6s on a fast machine and longer on a slow one — so the polling loop has a grace window after which it exits early if no dialog has been seen. The grace window has to cover Unity's full startup phase or the loop bails out before the dialog ever appears (issue #737); it never runs the full `--launch-dismiss-timeout-ms` in the no-dialog case. If the dialog is observed (and successfully dismissed), polling continues until the overall timeout so a re-appearing dialog (resolver fixes one error → dialog re-surfaces with the next) is dismissed again. Library-mode callers can supply an `AbortSignal` (`launchDismissAbortSignal` on `OpenProjectOptions`) to abort the loop the instant their own readiness signal fires.

### Auto-dismiss platform requirements

Expand Down
22 changes: 15 additions & 7 deletions cli/src/lib/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,11 @@ export interface _PollAndDismissOptionsForTests {
abortSignal?: AbortSignal;
/**
* Grace window (ms) after polling starts: if no dialog has been
* observed within this window, exit early. Defaults to 3000.
* Test override only — production callers do not configure this.
* observed within this window, exit early. Test override only —
* production callers do not configure this.
*
* @see PollAndDismissOptions.noDialogGraceMs for the production
* default and rationale.
*/
noDialogGraceMs?: number;
}
Expand Down Expand Up @@ -400,10 +403,15 @@ interface PollAndDismissOptions {
/**
* Grace window in ms after polling starts: if no dialog has been
* observed (and no permanent error has been seen) within this
* window, exit early. Defaults to 3000. The Unity launch-errors
* dialog appears within seconds of editor launch when it appears
* at all — running the full timeoutMs in the no-dialog case
* contradicts the README's "no extra delay" claim.
* window, exit early. Defaults to `15000`. The Unity launch-errors
* dialog (`"Enter Safe Mode?"` on Unity 2020.2+) is shown after the
* editor process has booted, connected to the Package Manager, and
* started compiling — empirically that takes ~6s on a fast machine
* and longer on a slow one. The default has to cover that startup
* window or the loop exits before Unity has had a chance to surface
* the dialog at all (issue #737). Running the full `timeoutMs` in
* the no-dialog case is still wasteful, so we keep a grace cutoff
* — just one large enough to pass Unity's startup phase.
*/
noDialogGraceMs?: number;
}
Expand Down Expand Up @@ -463,7 +471,7 @@ async function pollAndDismissLaunchErrors(opts: PollAndDismissOptions): Promise<
const start = Date.now();
const deadline = start + Math.max(0, opts.timeoutMs);
const interval = Math.max(50, opts.intervalMs);
const graceMs = Math.max(0, opts.noDialogGraceMs ?? 3000);
const graceMs = Math.max(0, opts.noDialogGraceMs ?? 15000);
const seenErrorMessages = new Set<string>();
let dismissedAtLeastOnce = false;
let aborted = opts.abortSignal?.aborted ?? false;
Expand Down
12 changes: 8 additions & 4 deletions cli/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,10 +329,14 @@ export interface OpenProjectOptions {
* `wait-for-ready` poll) so the dismissal loop does not keep
* ticking after Unity has finished initialising.
*
* When omitted, the loop falls back to a short grace window after
* the editor process is spawned: if no dialog has been observed
* within ~3s of polling, the loop exits early on the assumption
* that the dialog is not going to appear for this launch.
* When omitted, the loop falls back to a grace window after the
* editor process is spawned: if no dialog has been observed within
* ~15s of polling, the loop exits early on the assumption that the
* dialog is not going to appear for this launch. The grace window
* has to cover Unity's full startup phase (process spawn → Package
* Manager connect → first compile pass) because the launch-errors
* dialog (`"Enter Safe Mode?"` on Unity 2020.2+) appears at the end
* of that phase, not the start of it (issue #737).
*/
launchDismissAbortSignal?: AbortSignal;
/**
Expand Down
3 changes: 2 additions & 1 deletion cli/src/utils/launch-error-dismiss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type DismissOutcome =
* without grepping the implementation.
*/
export const LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS: readonly string[] = [
'Safe Mode', // Unity 2020.2+ ("Enter Safe Mode?") — the launch-errors dialog on every modern Unity (2022 LTS, 6000.x)
'Compiler Errors', // Unity 2020+ ("Hold On" + "Compiler Errors on Launch")
'Hold On', // Generic Unity progress dialog wrapping the launch-errors variant
'Compile Errors', // Older Unity dialog spelling
Expand Down Expand Up @@ -157,7 +158,7 @@ namespace UnityMcp.LaunchErrors {
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern IntPtr SendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
const uint BM_CLICK = 0x00F5;
static readonly string[] TitleFragments = new[] { "Compiler Errors", "Hold On", "Compile Errors", "Scripts have compiler errors" };
static readonly string[] TitleFragments = new[] { ${LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS.map((f) => JSON.stringify(f)).join(', ')} };
static readonly string[] ButtonLabels = new[] { "${DISMISS_BUTTON_LABEL}", "&${DISMISS_BUTTON_LABEL}" };
public static string TryDismiss(int[] unityPids) {
var unityPidSet = new HashSet<uint>();
Expand Down
18 changes: 18 additions & 0 deletions cli/tests/launch-error-dismiss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ describe('LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS', () => {
expect(fragments).toContain('compiler errors');
expect(fragments).toContain('hold on');
expect(fragments).toContain('compile errors');
// Unity 2020.2+ renamed the launch-errors dialog to
// `"Enter Safe Mode?"`. The matcher MUST include a fragment that
// matches that title or every modern Unity (2022 LTS, 6000.x)
// boots straight past the auto-dismiss path (issue #737).
expect(fragments).toContain('safe mode');
});

it('matches the actual Unity 2022.3+ "Enter Safe Mode?" dialog title', () => {
// Empirically observed title for the launch-errors dialog on
// Unity 2022.3.62f3 / Windows. The matcher in the PowerShell
// and AppleScript paths is case-insensitive substring, so at
// least one fragment MUST be a case-insensitive substring of
// this exact title or the dismiss is unreachable.
const realTitle = 'Enter Safe Mode?';
const matched = LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS.some((frag) =>
realTitle.toLowerCase().includes(frag.toLowerCase()),
);
expect(matched).toBe(true);
});

it('is non-empty (a zero-fragment matcher would never match anything)', () => {
Expand Down
Loading