mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): a background preview launch keeps only the server its own child started (#4914)
* fix(cli): a background preview launch keeps only the server its own child started * fix(cli): the detached preview child always starts its own server
This commit is contained in:
@@ -52,6 +52,7 @@ function reachableChildDependencies(stateHome: string, { immortal = false } = {}
|
||||
kill: vi.fn(() => {
|
||||
killed = true;
|
||||
}),
|
||||
isDescendant: (child: number, ancestor: number) => child === 9876 && ancestor === 4321,
|
||||
stateHome,
|
||||
};
|
||||
}
|
||||
@@ -86,7 +87,21 @@ describe("background preview lifecycle", () => {
|
||||
"--open",
|
||||
"--json",
|
||||
]),
|
||||
).toEqual(["/opt/hyperframes/cli.js", "preview", projectDir, "--foreground", "--no-open"]);
|
||||
).toEqual([
|
||||
"/opt/hyperframes/cli.js",
|
||||
"preview",
|
||||
projectDir,
|
||||
"--foreground",
|
||||
"--no-open",
|
||||
"--force-new",
|
||||
]);
|
||||
});
|
||||
|
||||
it("makes the detached child start its own server, never reuse a sibling launch's", () => {
|
||||
const args = buildBackgroundPreviewArgs(["cli.js", "preview", projectDir, "--force-new"]);
|
||||
|
||||
expect(args.filter((arg) => arg === "--force-new")).toHaveLength(1);
|
||||
expect(args.at(-1)).toBe("--force-new");
|
||||
});
|
||||
|
||||
it("reuses an already-running server for the same project", async () => {
|
||||
@@ -427,6 +442,7 @@ describe("background preview lifecycle", () => {
|
||||
spawned = true;
|
||||
return { pid: 4321, unref: vi.fn() };
|
||||
},
|
||||
isDescendant: (child, ancestor) => child === 9876 && ancestor === 4321,
|
||||
stateHome,
|
||||
});
|
||||
|
||||
@@ -483,6 +499,47 @@ describe("background preview lifecycle", () => {
|
||||
expect(existsSync(previewSessionPath(projectDir, stateHome))).toBe(false);
|
||||
});
|
||||
|
||||
it("two launches at once each keep the server their own child started", async () => {
|
||||
const stateHome = mkdtempSync(join(tmpdir(), "hf-preview-state-"));
|
||||
// A's server (3002) answers only after B's (3003) has appeared, and after A's pre-launch snapshot.
|
||||
const children: Record<number, ActiveServer & { after: number }> = {
|
||||
4321: { ...server, port: 3002, pid: "9876", after: 3 },
|
||||
5555: { ...server, port: 3003, pid: "7777", after: 0 },
|
||||
};
|
||||
const parent: Record<number, number> = { 9876: 4321, 7777: 5555 };
|
||||
const spawnedAt = new Map<number, number>();
|
||||
let scans = 0;
|
||||
let secondSpawned!: () => void;
|
||||
const secondUp = new Promise<void>((done) => (secondSpawned = done));
|
||||
const scan = async () => {
|
||||
scans++;
|
||||
return [...spawnedAt]
|
||||
.filter(([wrapper, at]) => scans - at > children[wrapper]!.after)
|
||||
.map(([wrapper]) => children[wrapper]!);
|
||||
};
|
||||
const launch = (wrapper: number, sleep: () => Promise<void>, preferredPort?: number) =>
|
||||
startBackgroundPreview(projectDir, 3002, {
|
||||
scan,
|
||||
spawn: () => {
|
||||
spawnedAt.set(wrapper, scans);
|
||||
if (wrapper === 5555) secondSpawned();
|
||||
return { pid: wrapper, unref: vi.fn() };
|
||||
},
|
||||
sleep,
|
||||
kill: vi.fn(),
|
||||
isDescendant: (child: number, ancestor: number) => parent[child] === ancestor,
|
||||
stateHome,
|
||||
preferredPort,
|
||||
});
|
||||
|
||||
const first = launch(4321, () => secondUp, 3002);
|
||||
await vi.waitFor(() => expect(spawnedAt.has(4321)).toBe(true));
|
||||
const second = launch(5555, async () => {});
|
||||
|
||||
await expect(first).resolves.toMatchObject({ type: "started", port: 3002, pid: 9876 });
|
||||
await expect(second).resolves.toMatchObject({ type: "started", port: 3003, pid: 7777 });
|
||||
});
|
||||
|
||||
it("keeps the next free port when no explicit port was requested", async () => {
|
||||
const stateHome = mkdtempSync(join(tmpdir(), "hf-preview-state-"));
|
||||
const dependencies = reachableChildDependencies(stateHome);
|
||||
|
||||
@@ -258,10 +258,16 @@ function startedServer(
|
||||
servers: ActiveServer[],
|
||||
projectDir: string,
|
||||
preLaunchPorts: Set<number>,
|
||||
browserGpuMode?: BrowserGpuMode,
|
||||
launchedPid: number,
|
||||
dependencies: LifecycleDependencies,
|
||||
): ActiveServer | null {
|
||||
const candidates = servers.filter((server) => !preLaunchPorts.has(server.port));
|
||||
return matchingServer(candidates, projectDir, browserGpuMode);
|
||||
const isDescendant = dependencies.isDescendant ?? isProcessDescendant;
|
||||
const ours = (server: ActiveServer) => {
|
||||
const pid = Number(server.pid);
|
||||
return pid === launchedPid || isDescendant(pid, launchedPid);
|
||||
};
|
||||
const candidates = servers.filter((server) => !preLaunchPorts.has(server.port) && ours(server));
|
||||
return matchingServer(candidates, projectDir, dependencies.browserGpuMode);
|
||||
}
|
||||
|
||||
export function buildBackgroundPreviewArgs(argv: string[]): string[] {
|
||||
@@ -273,9 +279,10 @@ export function buildBackgroundPreviewArgs(argv: string[]): string[] {
|
||||
!arg.startsWith("--foreground=") &&
|
||||
arg !== "--open" &&
|
||||
arg !== "--no-open" &&
|
||||
arg !== "--force-new" &&
|
||||
arg !== "--json",
|
||||
);
|
||||
return [...filtered, "--foreground", "--no-open"];
|
||||
return [...filtered, "--foreground", "--no-open", "--force-new"];
|
||||
}
|
||||
|
||||
export async function readBackgroundPreviewStatus(
|
||||
@@ -547,7 +554,7 @@ export async function startBackgroundPreview(
|
||||
);
|
||||
|
||||
const kill = dependencies.kill ?? stopProcess;
|
||||
const server = await awaitStartedServer(projectDir, startPort, preLaunchPorts, dependencies);
|
||||
const server = await awaitStartedServer(projectDir, startPort, preLaunchPorts, pid, dependencies);
|
||||
if (!server) {
|
||||
await kill(pid);
|
||||
throw new Error(`background preview did not become ready; see ${logPath}`);
|
||||
@@ -584,6 +591,7 @@ async function awaitStartedServer(
|
||||
projectDir: string,
|
||||
startPort: number,
|
||||
preLaunchPorts: Set<number>,
|
||||
launchedPid: number,
|
||||
dependencies: LifecycleDependencies,
|
||||
): Promise<ActiveServer | null> {
|
||||
const scan = dependencies.scan ?? scanActiveServers;
|
||||
@@ -593,7 +601,8 @@ async function awaitStartedServer(
|
||||
await scan(startPort),
|
||||
projectDir,
|
||||
preLaunchPorts,
|
||||
dependencies.browserGpuMode,
|
||||
launchedPid,
|
||||
dependencies,
|
||||
);
|
||||
if (server) return server;
|
||||
await sleep(200);
|
||||
|
||||
Reference in New Issue
Block a user