create-caspian-app 1.4.6 → 1.5.0

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.
@@ -296,7 +296,7 @@ async function shutdown(exitCode: number): Promise<void> {
296
296
  bs.exit();
297
297
  }
298
298
 
299
- stopPythonServer();
299
+ await stopPythonServer();
300
300
  process.exit(exitCode);
301
301
  }
302
302
 
@@ -3,9 +3,12 @@ import { platform } from "os";
3
3
  import { existsSync } from "fs";
4
4
  import { join } from "path";
5
5
  import { Socket } from "net";
6
+ import { randomBytes } from "crypto";
6
7
 
7
8
  let pythonProcess: ChildProcess | null = null;
9
+ let pythonControlToken: string | null = null;
8
10
  let isRestarting = false;
11
+ const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 7000;
9
12
 
10
13
  function isWindows(): boolean {
11
14
  return platform() === "win32";
@@ -114,23 +117,68 @@ async function killProcessTree(child: ChildProcess): Promise<void> {
114
117
  }
115
118
  }
116
119
 
120
+ export function requestGracefulShutdown(
121
+ child: ChildProcess,
122
+ controlToken: string | null,
123
+ timeout = GRACEFUL_SHUTDOWN_TIMEOUT_MS,
124
+ ): Promise<boolean> {
125
+ if (child.exitCode !== null) return Promise.resolve(true);
126
+ const stdin = child.stdin;
127
+ if (!controlToken || !stdin?.writable) return Promise.resolve(false);
128
+
129
+ return new Promise((resolve) => {
130
+ let settled = false;
131
+ const finish = (stopped: boolean) => {
132
+ if (settled) return;
133
+ settled = true;
134
+ clearTimeout(timer);
135
+ child.off("exit", onExit);
136
+ child.off("close", onExit);
137
+ resolve(stopped);
138
+ };
139
+ const onExit = () => finish(true);
140
+ const timer = setTimeout(() => finish(false), timeout);
141
+
142
+ child.once("exit", onExit);
143
+ child.once("close", onExit);
144
+ stdin.write(`shutdown:${controlToken}\n`, (error) => {
145
+ if (error) finish(false);
146
+ });
147
+ });
148
+ }
149
+
150
+ async function stopPythonProcess(
151
+ child: ChildProcess,
152
+ controlToken: string | null,
153
+ ): Promise<void> {
154
+ const stoppedGracefully = await requestGracefulShutdown(child, controlToken);
155
+ if (stoppedGracefully) return;
156
+
157
+ console.warn(
158
+ "Warning: Python server did not stop gracefully; forcing process termination.",
159
+ );
160
+ await killProcessTree(child);
161
+ }
162
+
117
163
  function spawnPython(port: number, browserSyncPort?: number): ChildProcess {
118
164
  const pythonPath = getVenvPythonPath();
119
165
  const args = ["-u", "main.py"];
120
166
 
121
167
  console.log(`-> Starting Python server on port ${port}...`);
122
168
 
169
+ pythonControlToken = randomBytes(32).toString("hex");
123
170
  const env = {
124
171
  ...process.env,
125
172
  PYTHONUNBUFFERED: "1",
126
173
  PORT: String(port),
174
+ CASPIAN_DEV_CONTROL_TOKEN: pythonControlToken,
127
175
  ...(browserSyncPort
128
176
  ? { CASPIAN_BROWSER_SYNC_PORT: String(browserSyncPort) }
129
177
  : {}),
130
178
  };
131
179
 
132
180
  const child = spawn(pythonPath, args, {
133
- stdio: "inherit",
181
+ stdio: ["pipe", "inherit", "inherit"],
134
182
  shell: false,
135
183
  detached: !isWindows(),
136
184
  env,
@@ -158,10 +206,12 @@ export async function restartPythonServer(
158
206
  try {
159
207
  console.log("-> Restarting Python server...");
160
208
  const prev = pythonProcess;
209
+ const prevControlToken = pythonControlToken;
161
210
  pythonProcess = null;
211
+ pythonControlToken = null;
162
212
 
163
213
  if (prev) {
164
- await killProcessTree(prev);
214
+ await stopPythonProcess(prev, prevControlToken);
165
215
  await waitForPortRelease(port);
166
216
  }
167
217
 
@@ -171,10 +221,12 @@ export async function restartPythonServer(
171
221
  }
172
222
  }
173
223
 
174
- export function stopPythonServer(): void {
224
+ export async function stopPythonServer(): Promise<void> {
175
225
  const prev = pythonProcess;
226
+ const prevControlToken = pythonControlToken;
176
227
  pythonProcess = null;
177
- if (prev) killProcessTree(prev);
228
+ pythonControlToken = null;
229
+ if (prev) await stopPythonProcess(prev, prevControlToken);
178
230
  }
179
231
 
180
232
  export async function waitForHttpHealth(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "1.4.6",
3
+ "version": "1.5.0",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",