@hienlh/ppm 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/cli/commands/stop.ts +26 -9
- package/src/server/index.ts +80 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.4] - 2026-03-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Windows: daemon process dies when parent exits** — use PowerShell `Start-Process` for truly detached daemon and tunnel processes on Windows
|
|
7
|
+
- **Windows: `ppm status -a` / `stop -a` crash** — replaced `pgrep` (Unix-only) with `wmic` on Windows
|
|
8
|
+
- Daemon startup now verifies child process is alive after 500ms — shows clear error + suggests `-f` if daemon fails
|
|
9
|
+
|
|
3
10
|
## [0.5.3] - 2026-03-18
|
|
4
11
|
|
|
5
12
|
### Added
|
package/package.json
CHANGED
|
@@ -61,6 +61,25 @@ interface ProcessInfo {
|
|
|
61
61
|
|
|
62
62
|
function findSystemProcesses(name: string): ProcessInfo[] {
|
|
63
63
|
try {
|
|
64
|
+
if (process.platform === "win32") {
|
|
65
|
+
// Windows: use wmic to list processes matching name
|
|
66
|
+
const result = Bun.spawnSync(
|
|
67
|
+
["wmic", "process", "where", `CommandLine like '%${name}%'`, "get", "ProcessId,CommandLine", "/format:csv"],
|
|
68
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
69
|
+
);
|
|
70
|
+
const output = result.stdout.toString().trim();
|
|
71
|
+
if (!output) return [];
|
|
72
|
+
return output.split("\n").slice(1) // skip header
|
|
73
|
+
.map((line) => {
|
|
74
|
+
const parts = line.trim().split(",");
|
|
75
|
+
if (parts.length < 3) return null;
|
|
76
|
+
const pid = parseInt(parts[parts.length - 1]!, 10);
|
|
77
|
+
const cmdLine = parts.slice(1, -1).join(",");
|
|
78
|
+
return { pid, command: name, args: cmdLine };
|
|
79
|
+
})
|
|
80
|
+
.filter((p): p is ProcessInfo => p !== null && !isNaN(p.pid) && p.args.includes(name));
|
|
81
|
+
}
|
|
82
|
+
// macOS/Linux: use pgrep
|
|
64
83
|
const result = Bun.spawnSync(["pgrep", "-afl", name], { stdout: "pipe", stderr: "ignore" });
|
|
65
84
|
const output = result.stdout.toString().trim();
|
|
66
85
|
if (!output) return [];
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -17,20 +17,37 @@ function killPid(pid: number, label: string): boolean {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
function findPidsByName(name: string): number[] {
|
|
21
21
|
try {
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
// Windows: use wmic to find processes
|
|
24
|
+
const result = Bun.spawnSync(
|
|
25
|
+
["wmic", "process", "where", `CommandLine like '%${name}%'`, "get", "ProcessId", "/format:csv"],
|
|
26
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
27
|
+
);
|
|
28
|
+
const output = result.stdout.toString().trim();
|
|
29
|
+
if (!output) return [];
|
|
30
|
+
return output.split("\n").slice(1)
|
|
31
|
+
.map((line) => parseInt(line.trim().split(",").pop() ?? "", 10))
|
|
32
|
+
.filter((pid) => !isNaN(pid) && pid !== process.pid);
|
|
33
|
+
}
|
|
34
|
+
// macOS/Linux: use pgrep
|
|
22
35
|
const result = Bun.spawnSync(["pgrep", "-fl", name], { stdout: "pipe", stderr: "ignore" });
|
|
23
36
|
const output = result.stdout.toString().trim();
|
|
24
|
-
if (!output) return
|
|
25
|
-
|
|
37
|
+
if (!output) return [];
|
|
38
|
+
return output.split("\n")
|
|
26
39
|
.map((line) => parseInt(line.trim(), 10))
|
|
27
40
|
.filter((pid) => !isNaN(pid) && pid !== process.pid);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
} catch { return []; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function killAllByName(name: string): number {
|
|
45
|
+
const pids = findPidsByName(name);
|
|
46
|
+
let killed = 0;
|
|
47
|
+
for (const pid of pids) {
|
|
48
|
+
if (killPid(pid, name)) killed++;
|
|
49
|
+
}
|
|
50
|
+
return killed;
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
export async function stopServer(options?: { all?: boolean }) {
|
package/src/server/index.ts
CHANGED
|
@@ -254,14 +254,32 @@ export async function startServer(options: {
|
|
|
254
254
|
const tunnelLog = resolve(ppmDir, "tunnel.log");
|
|
255
255
|
// Truncate old log so we only match the new tunnel URL
|
|
256
256
|
writeFs(tunnelLog, "");
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
|
|
258
|
+
if (process.platform === "win32") {
|
|
259
|
+
// Windows: use PowerShell for detached tunnel process
|
|
260
|
+
const psCmd = [
|
|
261
|
+
`$p = Start-Process -PassThru -WindowStyle Hidden`,
|
|
262
|
+
`-FilePath '${bin.replace(/\\/g, "\\\\")}'`,
|
|
263
|
+
`-ArgumentList 'tunnel','--url','http://localhost:${port}'`,
|
|
264
|
+
`-RedirectStandardError '${tunnelLog.replace(/\\/g, "\\\\")}'`,
|
|
265
|
+
`; Write-Output $p.Id`,
|
|
266
|
+
].join(" ");
|
|
267
|
+
const result = Bun.spawnSync({
|
|
268
|
+
cmd: ["powershell", "-NoProfile", "-Command", psCmd],
|
|
269
|
+
stdout: "pipe", stderr: "pipe",
|
|
270
|
+
});
|
|
271
|
+
tunnelPid = parseInt(result.stdout.toString().trim(), 10);
|
|
272
|
+
if (isNaN(tunnelPid)) tunnelPid = null;
|
|
273
|
+
} else {
|
|
274
|
+
const tfd = openFd(tunnelLog, "a");
|
|
275
|
+
const tunnelProc = Bun.spawn({
|
|
276
|
+
cmd: [bin, "tunnel", "--url", `http://localhost:${port}`],
|
|
277
|
+
stdio: ["ignore", "ignore", tfd],
|
|
278
|
+
env: process.env,
|
|
279
|
+
});
|
|
280
|
+
tunnelProc.unref();
|
|
281
|
+
tunnelPid = tunnelProc.pid;
|
|
282
|
+
}
|
|
265
283
|
|
|
266
284
|
// Parse URL from tunnel.log (poll stderr output)
|
|
267
285
|
const urlRegex = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
@@ -283,22 +301,64 @@ export async function startServer(options: {
|
|
|
283
301
|
const logFile = resolve(ppmDir, "ppm.log");
|
|
284
302
|
const logFd = openSync(logFile, "a");
|
|
285
303
|
const { resolve: resolvePath } = await import("node:path");
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
304
|
+
const script = resolvePath(import.meta.dir, "index.ts");
|
|
305
|
+
const args = ["__serve__", String(port), host, options.config ?? ""];
|
|
306
|
+
|
|
307
|
+
let childPid: number;
|
|
308
|
+
|
|
309
|
+
if (process.platform === "win32") {
|
|
310
|
+
// Windows: Bun.spawn child may die when parent exits (same job object).
|
|
311
|
+
// Use PowerShell Start-Process to create a truly detached process.
|
|
312
|
+
const bunExe = process.execPath.replace(/\\/g, "\\\\");
|
|
313
|
+
const argStr = ["run", script, ...args].map((a) => `'${a}'`).join(",");
|
|
314
|
+
const psCmd = [
|
|
315
|
+
`$p = Start-Process -PassThru -WindowStyle Hidden`,
|
|
316
|
+
`-FilePath '${bunExe}'`,
|
|
317
|
+
`-ArgumentList ${argStr}`,
|
|
318
|
+
`-RedirectStandardOutput '${logFile.replace(/\\/g, "\\\\")}'`,
|
|
319
|
+
`-RedirectStandardError '${logFile.replace(/\\/g, "\\\\")}'`,
|
|
320
|
+
`; Write-Output $p.Id`,
|
|
321
|
+
].join(" ");
|
|
322
|
+
const result = Bun.spawnSync({
|
|
323
|
+
cmd: ["powershell", "-NoProfile", "-Command", psCmd],
|
|
324
|
+
stdout: "pipe",
|
|
325
|
+
stderr: "pipe",
|
|
326
|
+
});
|
|
327
|
+
childPid = parseInt(result.stdout.toString().trim(), 10);
|
|
328
|
+
if (isNaN(childPid)) {
|
|
329
|
+
console.error(" ✗ Failed to start daemon on Windows.");
|
|
330
|
+
console.error(` ${result.stderr.toString().trim()}`);
|
|
331
|
+
console.error(" Try: ppm start -f (foreground mode)");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
// macOS/Linux: Bun.spawn + unref works fine
|
|
336
|
+
const child = Bun.spawn({
|
|
337
|
+
cmd: [process.execPath, "run", script, ...args],
|
|
338
|
+
stdio: ["ignore", logFd, logFd],
|
|
339
|
+
env: process.env,
|
|
340
|
+
});
|
|
341
|
+
child.unref();
|
|
342
|
+
childPid = child.pid;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Verify daemon is alive after brief startup
|
|
346
|
+
await Bun.sleep(500);
|
|
347
|
+
let alive = false;
|
|
348
|
+
try { process.kill(childPid, 0); alive = true; } catch {}
|
|
349
|
+
if (!alive) {
|
|
350
|
+
console.error(" ✗ Daemon exited immediately after start.");
|
|
351
|
+
console.error(" Check logs: ppm logs");
|
|
352
|
+
console.error(" Or try: ppm start -f (foreground mode)");
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
295
355
|
|
|
296
356
|
// Write status file with both PIDs
|
|
297
|
-
const status = { pid:
|
|
357
|
+
const status = { pid: childPid, port, host, shareUrl, tunnelPid };
|
|
298
358
|
writeFileSync(statusFile, JSON.stringify(status));
|
|
299
|
-
writeFileSync(pidFile, String(
|
|
359
|
+
writeFileSync(pidFile, String(childPid));
|
|
300
360
|
|
|
301
|
-
console.log(`\n PPM v${VERSION} daemon started (PID: ${
|
|
361
|
+
console.log(`\n PPM v${VERSION} daemon started (PID: ${childPid})\n`);
|
|
302
362
|
console.log(` ➜ Local: http://localhost:${port}/`);
|
|
303
363
|
if (shareUrl) {
|
|
304
364
|
console.log(` ➜ Share: ${shareUrl}`);
|