mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
268c017c63
## Problem
`vega virtual-device stop` exits non-zero with **"virtual device not
running"** for any VVD the `vega` CLI is not tracking — and that is
**every VVD argent boots**. `boot-device` starts a VVD via `vega
virtual-device start -t N`, which returns once boot completes (rather
than staying foreground), so the CLI never registers the device. `vega
virtual-device status` then reports `running:false` and `stop` is a
no-op-that-throws.
That error threw straight out of `stopVvd`, so **`boot-device
{force:true}` failed outright and left the VVD running** — even though
argent's *detection* (`isVvdRunning` / `listRunningVvdConsolePorts`)
already deliberately trusts the OS process table over the flaky CLI.
Repro (against a VVD argent booted):
```
boot-device {vvdImage:"tv", force:true}
→ [Tool:boot-device] vega virtual-device stop failed: virtual device not running
at stopVvd (vega-vvd.ts) → bootVegaImpl (boot-device.ts)
```
## Fix
Make **stop** symmetric with **detection**:
- Tolerate the CLI's stale failure (`vega virtual-device stop` is now
best-effort).
- Terminate any VVD emulator process the `ps` probe still finds —
SIGTERM, then SIGKILL the stragglers, verifying via `isVvdRunning`
between.
- Killing by pid demands a **positive** emulator identity — the VVD
binary name **and** a `-ports`/`-qmp` signal — so a process that merely
mentions a `…/vega-virtual-device` path in its argv (e.g. a git command
on a branch of that name) is never signalled.
Also stopped pointing users at `vega virtual-device stop` for an
argent-booted VVD (the boot-device error message + the `argent-vega`
skill); the reliable restart is `boot-device {force:true}`.
## Verification
- `npx vitest run --root packages/tool-server` → **1430 passed**. New
unit tests cover `parseVvdPids` (identity/safety incl. the git-ref
guard) and `stopVvd` (no-kill on clean stop; kill-without-rethrow when
the CLI refuses; SIGTERM→SIGKILL escalation).
- **Live on a VVD**: reproduced the orphan state (argent boot →
`status:false` while pid alive), then `boot-device force:true` — old pid
gone, a fresh single VVD running, `booted:true`. Previously it threw.
126 lines
5.4 KiB
TypeScript
126 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
// The running VVD's console port / pids come from the process table; mock that source.
|
|
const listRunningVvdConsolePorts = vi.fn();
|
|
const listRunningVvdPids = vi.fn();
|
|
vi.mock("../src/utils/vega-process", () => ({
|
|
listRunningVvdConsolePorts: (...a: unknown[]) => listRunningVvdConsolePorts(...a),
|
|
listRunningVvdPids: (...a: unknown[]) => listRunningVvdPids(...a),
|
|
}));
|
|
|
|
// stopVvd asks the CLI to stop first; mock it so we can make it succeed or throw.
|
|
const runVega = vi.fn();
|
|
vi.mock("../src/utils/vega-cli", () => ({
|
|
runVega: (...a: unknown[]) => runVega(...a),
|
|
}));
|
|
|
|
// discoverVegaConsolePort also confirms the VVD's `emulator-<port>` is adb-ready;
|
|
// mock the `adb devices` call but keep the real parser.
|
|
const runAdb = vi.fn();
|
|
vi.mock("../src/utils/adb", async () => {
|
|
const actual = await vi.importActual<typeof import("../src/utils/adb")>("../src/utils/adb");
|
|
return { ...actual, runAdb: (...a: unknown[]) => runAdb(...a) };
|
|
});
|
|
|
|
import { discoverVegaConsolePort, MultipleVegaDevicesError, stopVvd } from "../src/utils/vega-vvd";
|
|
|
|
function adbDevices(...serials: string[]): { stdout: string; stderr: string } {
|
|
return {
|
|
stdout: ["List of devices attached", ...serials.map((s) => `${s}\tdevice`)].join("\n") + "\n",
|
|
stderr: "",
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
listRunningVvdConsolePorts.mockReset();
|
|
listRunningVvdPids.mockReset();
|
|
runVega.mockReset();
|
|
runAdb.mockReset();
|
|
runAdb.mockResolvedValue(adbDevices("emulator-5556")); // adb-ready by default
|
|
});
|
|
|
|
describe("discoverVegaConsolePort", () => {
|
|
it("returns the sole running VVD's console port once it is adb-ready", async () => {
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set([5556]));
|
|
expect(await discoverVegaConsolePort()).toBe(5556);
|
|
});
|
|
|
|
it("throws an actionable error when no VVD process is running", async () => {
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set());
|
|
await expect(discoverVegaConsolePort()).rejects.toThrow(/No running Vega Virtual Device/);
|
|
});
|
|
|
|
it("throws a typed MultipleVegaDevicesError when more than one VVD is running", async () => {
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set([5554, 5556]));
|
|
await expect(discoverVegaConsolePort()).rejects.toBeInstanceOf(MultipleVegaDevicesError);
|
|
await expect(discoverVegaConsolePort()).rejects.toThrow(
|
|
/Multiple Vega Virtual Devices detected/
|
|
);
|
|
});
|
|
|
|
it("waits for adb and throws a clear error if the VVD's emulator never registers", async () => {
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set([5556]));
|
|
runAdb.mockResolvedValue(adbDevices()); // emulator-5556 absent from `adb devices`
|
|
await expect(discoverVegaConsolePort({ adbReadyTimeoutMs: 0 })).rejects.toThrow(
|
|
/has not registered with adb/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("stopVvd", () => {
|
|
let kill: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
// Don't actually signal real processes from the test runner.
|
|
kill = vi.spyOn(process, "kill").mockImplementation(() => true);
|
|
});
|
|
afterEach(() => kill.mockRestore());
|
|
|
|
it("does not signal anything when the CLI stop succeeds and no VVD process remains", async () => {
|
|
runVega.mockResolvedValue({ stdout: "", stderr: "" });
|
|
listRunningVvdPids.mockResolvedValue([]);
|
|
await stopVvd();
|
|
expect(runVega).toHaveBeenCalledWith(
|
|
["virtual-device", "stop"],
|
|
expect.objectContaining({ timeoutMs: expect.any(Number) })
|
|
);
|
|
expect(kill).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("kills a VVD the CLI refused to stop, and does not rethrow the CLI failure", async () => {
|
|
// The CLI exits non-zero ("virtual device not running") for a VVD it lost track of.
|
|
runVega.mockRejectedValue(
|
|
new Error("vega virtual-device stop failed: virtual device not running")
|
|
);
|
|
listRunningVvdPids.mockResolvedValue([75137]);
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set()); // isVvdRunning → gone after SIGTERM
|
|
await expect(stopVvd({ killGraceMs: 50, verifyPollMs: 5 })).resolves.toBeUndefined();
|
|
expect(kill).toHaveBeenCalledWith(75137, "SIGTERM");
|
|
expect(kill).not.toHaveBeenCalledWith(75137, "SIGKILL");
|
|
});
|
|
|
|
it("escalates to SIGKILL when SIGTERM leaves the VVD running", async () => {
|
|
runVega.mockResolvedValue({ stdout: "", stderr: "" });
|
|
listRunningVvdPids.mockResolvedValue([75137]);
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set([5554])); // still running through the grace window
|
|
await stopVvd({ killGraceMs: 20, verifyPollMs: 5 });
|
|
expect(kill).toHaveBeenCalledWith(75137, "SIGTERM");
|
|
expect(kill).toHaveBeenCalledWith(75137, "SIGKILL");
|
|
});
|
|
|
|
it("swallows a throwing process.kill (ESRCH/EPERM) and still signals every pid", async () => {
|
|
// signalQuietly must absorb a kill that races the process exiting (ESRCH) or
|
|
// hits a pid we don't own (EPERM) — stopVvd must still resolve and must keep
|
|
// signalling the remaining pids rather than aborting on the first throw.
|
|
runVega.mockResolvedValue({ stdout: "", stderr: "" });
|
|
listRunningVvdPids.mockResolvedValue([75137, 80001]);
|
|
listRunningVvdConsolePorts.mockResolvedValue(new Set()); // gone after SIGTERM → no SIGKILL pass
|
|
kill.mockImplementation(() => {
|
|
throw Object.assign(new Error("kill ESRCH"), { code: "ESRCH" });
|
|
});
|
|
await expect(stopVvd({ killGraceMs: 50, verifyPollMs: 5 })).resolves.toBeUndefined();
|
|
expect(kill).toHaveBeenCalledWith(75137, "SIGTERM");
|
|
expect(kill).toHaveBeenCalledWith(80001, "SIGTERM");
|
|
});
|
|
});
|