/** * Out-of-process observability for a running (or starting, or wedged) editor. * * Every other sensor in ue-mcp runs on the game thread: `ExecuteOnGameThread` * gates each bridge request, so when the game thread sits inside a modal loop, * a long FSlowTask, or a startup phase that predates the plugin module, the * bridge can only answer "Handler execution timed out". That is the whole * problem class behind #804 and its predecessors - the agent goes blind exactly * when the user needs to know what the engine is doing. * * Nothing in this file touches the bridge. It reads state the OS and the engine * publish regardless of what the game thread is doing: * * - the process table (PID + command line + hung/responding state) * - `Saved/Logs/.log`, written from the first millisecond of startup * - native top-level windows (the pre-Slate "modules are out of date" prompt * is a plain Win32 message box and is readable from outside) * - `Saved/UE_MCP_Bridge/status.json`, the snapshot the plugin flushes from a * writer thread that keeps running while the game thread is blocked */ export interface EditorProcess { pid: number; commandLine: string; /** The .uproject this process has open, absolute and normalised, if any. */ projectPath: string | null; /** * #804: a headless shard (`-server` / `-game` / `-nullrhi` / `-unattended` / * a commandlet) is the same binary as an interactive editor. Matching by * image name alone made two running shards look like "the editor is already * running" while the bridge simultaneously reported nothing connected. */ headless: boolean; /** false when the OS says the window is not pumping messages (wedged). */ responding: boolean; windowTitle: string | null; } /** * The .uproject argument on a process command line, absolute and resolved. * * #967/#970/#965: this used to be one lazy regex over the whole command line, * `[A-Za-z]:[\\/][^"]*?\.uproject`. `[^"]` matches spaces, so on the unquoted * command line Windows reports for a spawned editor * * C:\...\UnrealEditor.exe C:\work\Demo\Demo.uproject * * the match started at the drive letter of the EXECUTABLE and ran all the way * to the end, and every process came back with a "project path" that was the * exe and the project concatenated. Nothing ever equalled that, so * editorOwnsProject was false for the very editor that had the project open: * stop_editor refused to stop a healthy editor while printing that same * concatenation back as its evidence, and get_engine_state reported no process * for a project whose editor was answering on the bridge. * * So: find where the .uproject argument ENDS (a token boundary, not just any * occurrence of the extension), then walk back to the last place a path could * have begun. That handles the quoted form, the unquoted form, `-Project=` with * and without quotes, and POSIX roots, without assuming a path has no spaces. */ export declare function extractProjectPath(commandLine: string): string | null; /** * Every UnrealEditor process on this machine, with enough detail to tell an * interactive editor for one project apart from a headless shard for another. */ export declare function listEditorProcesses(): Promise; /** * Does this process have exactly `projectPath` open? * * The `.uproject` on the command line is the only thing that ties a process to * a project, so it is the only thing this looks at. A process whose command * line could not be read is never a positive match: every lifecycle action that * can stop or attach to an editor is scoped through this, and "might be ours" * is the wrong answer to give any of them (#819). */ export declare function editorOwnsProject(proc: EditorProcess, projectPath: string): boolean; /** * Narrow a process list to the interactive editors for one project. Pure, so * the matching rule can be tested without a process table. * * Headless shards are excluded, and so are editors for other projects - * launching a second project while a shard or another project's editor runs is * legitimate (#804). */ export declare function selectEditorsForProject(processes: EditorProcess[], projectPath?: string | null): EditorProcess[]; /** The interactive editor(s) holding `projectPath` open, from the live process table. */ export declare function findInteractiveEditors(projectPath?: string | null): Promise; /** The running editor with this PID, or null when it is gone. */ export declare function findEditorByPid(pid: number): Promise; export interface StartupPhase { /** Short phase label derived from the newest matching marker. */ phase: string; /** True when the marker means the editor is waiting on a human. */ blocking: boolean; /** The log line the phase was derived from. */ evidence: string; } export interface LogState { logPath: string | null; /** Seconds since the log file was last written. High = the process is stuck. */ secondsSinceWrite: number | null; phase: string; blocking: boolean; lastLine: string | null; tail: string[]; errors: string[]; warnings: string[]; } /** * What the engine's own log says right now. This is the only sensor that works * before the plugin module exists, which is where the "editor launched but the * bridge never came up" reports have always come from. */ /** * The engine root the project was last actually opened with, from its own log. * * Unreal writes `LogInit: Base Directory: /Engine/Binaries//` * in the first few lines of every run, which is the only durable record of * which engine tree launched an editor. It survives the editor exiting, so it * answers "which engine is this project on" while the editor is stopped, which * is exactly when a full rebuild has to be resolved (#959, #974). * * Returns null when the log is absent, unreadable, or has no such line. */ export declare function readEngineRootFromLog(projectPath: string | null | undefined): string | null; export declare function readLogState(projectPath: string | null | undefined, tailLines?: number): LogState; export interface NativeWindow { handle: string; className: string; title: string; /** Static text children, which is where a message box keeps its message. */ text: string[]; hung: boolean; } /** * Visible top-level windows belonging to the given PIDs, with their child text. * Windows only; returns [] elsewhere. Compiling the interop type costs about a * second, so call this on demand (a stalled wait, an explicit state query), * never on a hot path. */ export declare function readNativeWindows(pids: number[]): Promise; /** Windows that look like a prompt waiting on a human. */ export declare function dialogLikeWindows(windows: NativeWindow[]): NativeWindow[]; export interface EngineSnapshot { writtenAt?: string; ageSeconds?: number; /** Which process this snapshot describes. Absent on plugin builds before #990. */ pid?: number; phase?: string; /** * Null until the engine loop starts ticking. Startup has no tick loop to * stall, so a number there would be a false alarm on every cold launch. */ gameThreadStalledSeconds?: number | null; gameThreadTicking?: boolean; modulesLoaded?: number; slowTask?: { name: string; fraction: number; stack?: Array<{ name: string; fraction: number; }>; } | null; modal?: { title: string; message: string; buttons: string[]; } | null; compiling?: { shaders: number; assets: number; }; handler?: { method: string; elapsedSeconds: number; } | null; [key: string]: unknown; } export declare function readEngineSnapshot(projectPath: string | null | undefined): EngineSnapshot | null; export interface EngineState { running: boolean; processes: EditorProcess[]; log: LogState; snapshot: EngineSnapshot | null; dialogs: NativeWindow[]; /** * #965: the out-of-process probe could not run. Reported on its own rather * than folded into an empty `processes`, because "I could not look" and * "there is no editor" are different answers and only one of them is a * reason to conclude the editor is down. */ processProbeFailed: boolean; /** What `running` is asserted on. A bridge reply outranks the process table. */ runningEvidence: "bridge-snapshot" | "process-table" | "none"; /** Where `snapshot` came from. */ snapshotSource: "bridge" | "status.json" | "none"; /** One line an agent can act on without reading any of the above. */ summary: string; /** True when something is waiting on a human answer. */ blocked: boolean; } /** * Everything that can be known about the editor without asking the game thread. * `probeWindows` costs about a second on Windows, so it defaults off and should * be turned on when something already looks wrong. */ export declare function readEngineState(projectPath: string | null | undefined, opts?: { probeWindows?: boolean; }): Promise; /** * Fold in a snapshot the editor served over its own bridge. * * #965: one report contained `"running": false, "processes": []` and, in the * same object, a live snapshot with an uptime and a ticking game thread, served * BY the editor over the bridge. An editor that answers a request is running; * a process table that came back empty is a failed measurement, not a fact * about the world. So a bridge reply sets `running` and the process probe's * silence is reported as its own field. */ export declare function withBridgeSnapshot(state: EngineState, snapshot: EngineSnapshot): EngineState;