Skip to content

Commit bc5ef99

Browse files
committed
fix(cli): wait for managed server shutdown
1 parent 6ad533d commit bc5ef99

4 files changed

Lines changed: 79 additions & 3 deletions

File tree

packages/cli/src/pm2-control.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ import {
3333
describe("pm2-control", () => {
3434
const originalHome = process.env.HOME;
3535
const originalUserProfile = process.env.USERPROFILE;
36+
const originalRuntimeDir = process.env.CODER_STUDIO_RUNTIME_DIR;
37+
const originalRuntimeJsonPath = process.env.CODER_STUDIO_RUNTIME_JSON_PATH;
3638
let testHomeDir: string;
3739

3840
beforeEach(() => {
3941
testHomeDir = mkdtempSync(join(tmpdir(), "cs-pm2-control-home-"));
4042
process.env.HOME = testHomeDir;
4143
process.env.USERPROFILE = testHomeDir;
44+
process.env.CODER_STUDIO_RUNTIME_DIR = join(testHomeDir, ".coder-studio");
45+
delete process.env.CODER_STUDIO_RUNTIME_JSON_PATH;
4246

4347
connect.mockImplementation((callback: (error: Error | null) => void) => callback(null));
4448
disconnect.mockImplementation(() => undefined);
@@ -80,6 +84,18 @@ describe("pm2-control", () => {
8084
process.env.USERPROFILE = originalUserProfile;
8185
}
8286

87+
if (originalRuntimeDir === undefined) {
88+
delete process.env.CODER_STUDIO_RUNTIME_DIR;
89+
} else {
90+
process.env.CODER_STUDIO_RUNTIME_DIR = originalRuntimeDir;
91+
}
92+
93+
if (originalRuntimeJsonPath === undefined) {
94+
delete process.env.CODER_STUDIO_RUNTIME_JSON_PATH;
95+
} else {
96+
process.env.CODER_STUDIO_RUNTIME_JSON_PATH = originalRuntimeJsonPath;
97+
}
98+
8399
if (existsSync(testHomeDir)) {
84100
rmSync(testHomeDir, { recursive: true, force: true });
85101
}
@@ -183,6 +199,10 @@ describe("pm2-control", () => {
183199
callback(null, [])
184200
)
185201
.mockImplementationOnce(
202+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
203+
callback(null, [])
204+
)
205+
.mockImplementation(
186206
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
187207
callback(null, [{ pid: 424242, pm2_env: { status: "online", restart_time: 0 } }])
188208
);

packages/cli/src/pm2-control.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const MANAGED_SERVER_NAME = "coder-studio-server";
88
const PM2_RESTART_DELAY_MS = 2000;
99
const PM2_MIN_UPTIME = "5s";
1010
const PM2_MAX_RESTARTS = 10;
11+
const PM2_DELETE_WAIT_MS = 5000;
1112
const STARTUP_POLL_INTERVAL_MS = 100;
1213
const STARTUP_FAILURE_GUIDANCE =
1314
"Run `coder-studio logs` for details or `coder-studio serve --foreground` for interactive debugging.";
@@ -236,6 +237,26 @@ const waitForManagedServerExit = async (): Promise<void> => {
236237
}
237238
};
238239

240+
const waitForManagedServerDeletion = async (waitMs: number): Promise<void> => {
241+
const deadline = Date.now() + waitMs;
242+
243+
while (Date.now() <= deadline) {
244+
const processes = await withPm2Connection(describeManagedServer);
245+
if (processes.length === 0) {
246+
return;
247+
}
248+
249+
const remainingMs = deadline - Date.now();
250+
if (remainingMs <= 0) {
251+
break;
252+
}
253+
254+
await sleep(Math.min(STARTUP_POLL_INTERVAL_MS, remainingMs));
255+
}
256+
257+
throw new Error(`Timed out waiting for the managed server to stop after ${waitMs}ms.`);
258+
};
259+
239260
const ensureLogDirectory = (): void => {
240261
mkdirSync(join(homedir(), ".coder-studio", "logs"), { recursive: true });
241262
};
@@ -286,8 +307,8 @@ export const deleteManagedServer = async ({
286307
ignoreMissing = false,
287308
}: {
288309
ignoreMissing?: boolean;
289-
} = {}): Promise<boolean> =>
290-
withPm2Connection(async () => {
310+
} = {}): Promise<boolean> => {
311+
const deleted = await withPm2Connection(async () => {
291312
const processes = await describeManagedServer();
292313
if (processes.length === 0) {
293314
return false;
@@ -305,6 +326,14 @@ export const deleteManagedServer = async ({
305326
}
306327
});
307328

329+
if (!deleted) {
330+
return false;
331+
}
332+
333+
await waitForManagedServerDeletion(PM2_DELETE_WAIT_MS);
334+
return true;
335+
};
336+
308337
export const startManagedServer = async ({
309338
script,
310339
cwd,
@@ -402,6 +431,14 @@ export const getManagedServerStatus = async (): Promise<ManagedServerStatus> =>
402431
};
403432
}
404433

434+
if (pm2Pid === null || pm2Pid === 0) {
435+
return {
436+
status: "stopped",
437+
pm2Pid: null,
438+
restartCount,
439+
};
440+
}
441+
405442
return {
406443
status: "errored",
407444
pm2Pid,

packages/cli/src/server-control.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,25 @@ describe("server-control", () => {
149149
});
150150
});
151151

152+
it("reports stopped when pm2 is no longer running and runtime is absent", async () => {
153+
getManagedServerStatus.mockResolvedValue({
154+
status: "starting",
155+
pm2Pid: null,
156+
restartCount: 0,
157+
});
158+
159+
await expect(getServerStatus()).resolves.toEqual({
160+
status: "stopped",
161+
pid: null,
162+
host: null,
163+
port: null,
164+
restartCount: 0,
165+
outFile: "/tmp/server.out.log",
166+
errFile: "/tmp/server.err.log",
167+
startedAt: null,
168+
});
169+
});
170+
152171
it("cleans stale runtime when pm2 reports stopped", async () => {
153172
writeRuntimeConfig({
154173
host: "127.0.0.1",

packages/cli/src/server-control.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function getServerStatus(): Promise<ServerStatus> {
3131
const runtime = readRuntimeConfig();
3232
const { outFile, errFile } = getLogPaths();
3333

34-
if (managedStatus.status === "stopped") {
34+
if (managedStatus.status === "stopped" || (managedStatus.pm2Pid === null && runtime === null)) {
3535
if (runtime) {
3636
deleteRuntimeConfig();
3737
}

0 commit comments

Comments
 (0)