fix: wait for mobilecli to confirm screen recording started

mobile_start_screen_recording spawned mobilecli with --silent and stdio
ignored, then reported success immediately. A device-not-found or any other
startup failure was invisible until stop_screen_recording found no file.

Now capture mobilecli output and resolve only after it prints
"Screen recording has started". Reject on spawn error, early exit (with
mobilecli's own message), or a 30s startup timeout.

Salvaged from #383. Refs #359

Co-authored-by: Cosm1cAC <cosm1cac@github.com>
This commit is contained in:
gmegidish
2026-09-08 20:15:03 +02:00
co-authored by Cosm1cAC
parent f40600cd21
commit e1316178a9
3 changed files with 90 additions and 2 deletions
+55
View File
@@ -67,6 +67,7 @@ export interface MobilecliDevicesResponse {
const TIMEOUT = 30000;
const MAX_BUFFER_SIZE = 1024 * 1024 * 8;
const SCREEN_RECORDING_STARTED = "Screen recording has started";
const DEFAULT_ALLOCATE_TIMEOUT_SECONDS = 900; // matches mobilecli's own "remote allocate --wait" default
export class Mobilecli {
@@ -98,6 +99,60 @@ export class Mobilecli {
});
}
/**
* Spawns `mobilecli screenrecord` and resolves only once mobilecli reports that
* recording has started. Rejects on spawn error, early exit, or startup timeout.
*/
public startScreenRecording(args: string[]): Promise<ChildProcess> {
const child = this.spawnCommand(args, true);
const stdout = child.stdout!;
const stderr = child.stderr!;
return new Promise((resolve, reject) => {
let output = "";
let settled = false;
const finish = () => {
settled = true;
clearTimeout(timer);
stdout.off("data", onData);
stderr.off("data", onData);
stdout.resume();
stderr.resume();
};
const fail = (error: Error) => {
if (settled) {
return;
}
finish();
reject(error);
};
const onData = (chunk: Buffer) => {
output = (output + chunk.toString()).slice(-MAX_BUFFER_SIZE);
if (!settled && output.includes(SCREEN_RECORDING_STARTED)) {
finish();
resolve(child);
}
};
const timer = setTimeout(() => {
child.kill();
fail(new Error("Timed out waiting for mobilecli to start screen recording"));
}, TIMEOUT);
stdout.on("data", onData);
stderr.on("data", onData);
child.once("error", fail);
child.once("exit", (code, signal) => {
const status = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
fail(new Error(output.trim() || `mobilecli exited before screen recording started (${status})`));
});
});
}
public executeCommandBuffer(args: string[]): Buffer {
const path = this.getPath();
return execFileSync(path, args, {
+2 -2
View File
@@ -999,12 +999,12 @@ export const createMcpServer = (): McpServer => {
const outputPath = output || path.join(os.tmpdir(), `screen-recording-${Date.now()}.mp4`);
const args = ["screenrecord", "--device", device, "--output", outputPath, "--silent"];
const args = ["screenrecord", "--device", device, "--output", outputPath];
if (timeLimit !== undefined) {
args.push("--time-limit", String(timeLimit));
}
const child = mobilecli.spawnCommand(args);
const child = await mobilecli.startScreenRecording(args);
const cleanup = () => {
activeRecordings.delete(device);
+33
View File
@@ -116,4 +116,37 @@ test.describe("mobilecli", () => {
expect(calls[0].args).toEqual(["devices", "--include-offline", "--platform", "android", "--type", "emulator"]);
});
});
test.describe("startScreenRecording", () => {
test.beforeEach(() => {
process.env.MOBILECLI_PATH = process.execPath;
});
test.afterEach(() => {
delete process.env.MOBILECLI_PATH;
});
test("rejects when mobilecli exits before reporting that recording started", async () => {
const mobilecli = new Mobilecli();
await expect(mobilecli.startScreenRecording([
"-e",
"process.stderr.write('device not found\\n'); process.exit(1);",
])).rejects.toThrow("device not found");
});
test("resolves only after mobilecli reports that recording started", async () => {
const mobilecli = new Mobilecli();
const child = await mobilecli.startScreenRecording([
"-e",
"process.stderr.write('Screen recording has started\\n'); setTimeout(() => {}, 5000);",
]);
try {
expect(child.exitCode).toBeNull();
} finally {
child.kill();
}
});
});
});