@hienlh/ppm 0.8.17 → 0.8.18

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.18] - 2026-03-24
4
+
5
+ ### Fixed
6
+ - **Restart reliability**: Kill orphan processes holding the port via `lsof`/`netstat` (cross-platform), not just the PID from status.json — fixes restart failures when old server process outlives its PID record
7
+
3
8
  ## [0.8.17] - 2026-03-24
4
9
 
5
10
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.17",
3
+ "version": "0.8.18",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -70,6 +70,7 @@ export async function restartServer(options: { config?: string }) {
70
70
  writeFileSync(workerPath, `
71
71
  import { readFileSync, writeFileSync, openSync, unlinkSync, appendFileSync } from "node:fs";
72
72
  import { createServer } from "node:net";
73
+ import { spawnSync } from "node:child_process";
73
74
 
74
75
  // Ignore SIGHUP — when we kill the old server, the terminal PTY dies and
75
76
  // SIGHUP is sent to the entire process group. Without this, the worker
@@ -87,8 +88,26 @@ async function main() {
87
88
  try { writeFileSync(P.resultFile, JSON.stringify({ ok, message: msg })); } catch {}
88
89
  };
89
90
 
90
- // Kill old server
91
+ // Kill old server PID
91
92
  try { process.kill(P.serverPid); log("INFO", "Restart: killed old server PID " + P.serverPid); } catch {}
93
+ await Bun.sleep(500);
94
+
95
+ // Force-kill any process still holding the port (handles orphan/zombie processes)
96
+ const killByPort = () => {
97
+ try {
98
+ if (process.platform === "win32") {
99
+ const r = Bun.spawnSync(["cmd", "/c", "netstat -ano | findstr :" + P.port + " | findstr LISTENING"]);
100
+ const lines = r.stdout.toString().trim().split("\\n");
101
+ const pids = new Set(lines.map((l: string) => l.trim().split(/\\s+/).pop()).filter(Boolean));
102
+ for (const pid of pids) { try { process.kill(Number(pid)); } catch {} }
103
+ } else {
104
+ const r = Bun.spawnSync(["lsof", "-t", "-i", ":" + P.port]);
105
+ const pids = r.stdout.toString().trim().split("\\n").filter(Boolean);
106
+ for (const pid of pids) { try { process.kill(Number(pid)); } catch {} }
107
+ }
108
+ } catch {}
109
+ };
110
+ killByPort();
92
111
 
93
112
  // Wait for port to be free (up to 5s)
94
113
  const start = Date.now();
@@ -100,25 +119,51 @@ async function main() {
100
119
  .listen(P.port, P.host);
101
120
  });
102
121
  if (!inUse) break;
122
+ killByPort();
103
123
  await Bun.sleep(200);
104
124
  }
105
125
 
106
- // Spawn new server
107
- const logFd = openSync(P.ppmDir + "/ppm.log", "a");
108
- const child = Bun.spawn({
109
- cmd: [process.execPath, "run", P.serverScript, "__serve__", String(P.port), P.host, P.config],
110
- stdio: ["ignore", logFd, logFd],
111
- env: process.env,
112
- });
113
- child.unref();
126
+ // Spawn new server — on Windows use PowerShell Start-Process for true detach
127
+ // (Bun.spawn + unref on Windows keeps child in same job object → dies when worker exits)
128
+ let childPid: number;
129
+ const serverArgs = ["run", P.serverScript, "__serve__", String(P.port), P.host, P.config].filter(Boolean);
130
+
131
+ if (process.platform === "win32") {
132
+ const bunExe = process.execPath.replace(/\\\\/g, "\\\\\\\\");
133
+ const logPath = (P.ppmDir + "/ppm.log").replace(/\\//g, "\\\\").replace(/\\\\/g, "\\\\\\\\");
134
+ const errPath = (P.ppmDir + "/ppm.err.log").replace(/\\//g, "\\\\").replace(/\\\\/g, "\\\\\\\\");
135
+ const argStr = serverArgs.map((a: string) => "'" + (a || "_") + "'").join(",");
136
+ const psCmd = "$p = Start-Process -PassThru -WindowStyle Hidden"
137
+ + " -FilePath '" + bunExe + "'"
138
+ + " -ArgumentList " + argStr
139
+ + " -RedirectStandardOutput '" + logPath + "'"
140
+ + " -RedirectStandardError '" + errPath + "'"
141
+ + "; Write-Output $p.Id";
142
+ const r = spawnSync("powershell", ["-NoProfile", "-Command", psCmd], { stdio: ["ignore", "pipe", "pipe"] });
143
+ childPid = parseInt(String(r.stdout).trim(), 10);
144
+ if (isNaN(childPid)) {
145
+ log("ERROR", "Failed to start server via PowerShell: " + String(r.stderr).trim());
146
+ writeResult(false, "Failed to start server on Windows");
147
+ process.exit(1);
148
+ }
149
+ } else {
150
+ const logFd = openSync(P.ppmDir + "/ppm.log", "a");
151
+ const child = Bun.spawn({
152
+ cmd: [process.execPath, ...serverArgs],
153
+ stdio: ["ignore", logFd, logFd],
154
+ env: process.env,
155
+ });
156
+ child.unref();
157
+ childPid = child.pid;
158
+ }
114
159
 
115
160
  // Update status.json with new PID, keep tunnel info
116
161
  try {
117
162
  const status = JSON.parse(readFileSync(P.statusFile, "utf-8"));
118
- status.pid = child.pid;
163
+ status.pid = childPid;
119
164
  status.serverScript = P.serverScript;
120
165
  writeFileSync(P.statusFile, JSON.stringify(status));
121
- writeFileSync(P.pidFile, String(child.pid));
166
+ writeFileSync(P.pidFile, String(childPid));
122
167
  } catch {}
123
168
 
124
169
  // Remove restarting flag
@@ -147,7 +192,7 @@ async function main() {
147
192
  } catch {}
148
193
 
149
194
  if (ready) {
150
- let msg = "Restart complete (PID: " + child.pid + ", port: " + P.port + ")";
195
+ let msg = "Restart complete (PID: " + childPid + ", port: " + P.port + ")";
151
196
  if (shareUrl && tunnelPid) {
152
197
  msg += tunnelAlive ? " — tunnel alive" : " — tunnel dead, run 'ppm stop && ppm start --share'";
153
198
  }
@@ -155,7 +200,7 @@ async function main() {
155
200
  writeResult(true, msg);
156
201
  } else {
157
202
  let alive = false;
158
- try { process.kill(child.pid, 0); alive = true; } catch {}
203
+ try { process.kill(childPid, 0); alive = true; } catch {}
159
204
  const msg = alive
160
205
  ? "Server started but not responding on port " + P.port + ". Check: ppm logs"
161
206
  : "Server crashed on startup. Check: ppm logs";