@danhachuel/pandanews 0.4.0 → 0.4.1
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 +9 -0
- package/backend/app/main.py +1 -1
- package/package.json +3 -2
- package/scripts/start.js +185 -76
- package/scripts/test-start-isolation.mjs +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
|
|
4
4
|
|
|
5
|
+
## 0.4.01 (2026-11-08)
|
|
6
|
+
|
|
7
|
+
- **Isolamento de processos (Windows/MobaXterm)**: o backend Python roda em grupo de processos próprio (`detached: true` → `CREATE_NEW_PROCESS_GROUP`) com `windowsHide: true` (sem janela fantasma) e `stdio` em fd do arquivo — um Ctrl+C no terminal agora derruba **apenas o launcher Node**; o backend e as gerações em andamento sobrevivem
|
|
8
|
+
- **Fim do processo órfão na porta 8000**: ao reabrir o app, o launcher detecta o backend vivo (`backend.pid` + probe de porta) e o **reaproveita**, em vez de subir uma segunda instância
|
|
9
|
+
- **Log sem terminal**: a inicialização do uvicorn e as requisições ficam registradas em `data/backend.log` mesmo com o terminal fechado (auditoria preservada)
|
|
10
|
+
- Encerramento do backend **sempre por código**: `taskkill /PID /F /T` no Windows, `kill` no grupo no Unix — sinais do terminal nunca chegam ao backend
|
|
11
|
+
- `unref()` no processo filho + watchdog de saúde (polling de porta a cada 10s; 3 falhas reerguem o backend silenciosamente)
|
|
12
|
+
- Teste de regressão `scripts/test-start-isolation.mjs` (`npm test`) roda no CI verificando as flags de isolamento — passo adicionado ao workflow de publish
|
|
13
|
+
|
|
5
14
|
## 0.4.00 (2026-10-30)
|
|
6
15
|
|
|
7
16
|
- **Supabase opcional (Fase 6)**: detecção automática de credenciais (`SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` no ambiente ou em Configurações); teste de conexão real (`POST /configuracoes/testar-supabase`); schema SQL pronto para o SQL Editor (`GET /configuracoes/schema-supabase`); migração dos dados locais SQLite → Supabase (`POST /configuracoes/migrar-supabase`) com relatório por tabela. O app continua 100% local e funcional sem Supabase
|
package/backend/app/main.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danhachuel/pandanews",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "SaaS local de criação e automação de múltiplos blogs de notícias com IA (FastAPI + React + Tailwind).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build:frontend": "node scripts/build.js"
|
|
23
|
+
"build:frontend": "node scripts/build.js",
|
|
24
|
+
"test": "node scripts/test-start-isolation.mjs"
|
|
24
25
|
},
|
|
25
26
|
"repository": {
|
|
26
27
|
"type": "git",
|
package/scripts/start.js
CHANGED
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
// Fluxo de um único comando: se o runtime não estiver provisionado, executa o
|
|
4
4
|
// install completo (Python portátil + dependências) antes de subir os
|
|
5
5
|
// servidores. Ambas as portas têm fallback automático (API: 8000, 8001, ...;
|
|
6
|
-
// UI: 4030, 4031, ...). O sistema NÃO abre o navegador
|
|
7
|
-
// no terminal e o usuário decide quando abrir.
|
|
6
|
+
// UI: 4030, 4031, ...). O sistema NÃO abre o navegador.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
// ISOLAMENTO DE PROCESSOS (Windows/MobaXterm): o backend Python roda em grupo
|
|
9
|
+
// de processos próprio (detached + windowsHide) com stdio direto no arquivo
|
|
10
|
+
// <home>/data/backend.log — não herda o console do launcher. Um Ctrl+C ou
|
|
11
|
+
// perda do terminal derruba apenas o launcher Node; o backend e as gerações
|
|
12
|
+
// em andamento sobrevivem. O encerramento do backend é SEMPRE por código
|
|
13
|
+
// (SIGTERM + taskkill /PID /T), nunca por propagação de sinal do terminal.
|
|
14
|
+
// Se uma instância anterior morreu deixando backend vivo, o launcher detecta
|
|
15
|
+
// (backend.pid + health) e reaproveita em vez de subir uma segunda.
|
|
16
|
+
import { appendFileSync, closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
14
18
|
import { createServer, request as httpRequest } from 'node:http';
|
|
15
19
|
import net from 'node:net';
|
|
16
20
|
import { join, extname } from 'node:path';
|
|
@@ -19,14 +23,16 @@ import { resolveHome, runInstall, PKG_ROOT } from './install.js';
|
|
|
19
23
|
const API_PORT_BASE = Number(process.env.PANDANEWS_API_PORT || 8000);
|
|
20
24
|
const UI_PORT_BASE = Number(process.env.PANDANEWS_UI_PORT || 4030);
|
|
21
25
|
const PORT_MAX_TRIES = 20;
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
26
|
+
const BACKEND_POLL_ATTEMPTS = 60; // até 30s para o backend iniciar
|
|
27
|
+
const BACKEND_POLL_MS = 500;
|
|
28
|
+
const UI_POLL_ATTEMPTS = 20; // até 10s para a UI antiga responder
|
|
25
29
|
|
|
26
30
|
let activeBackendPort = API_PORT_BASE;
|
|
27
31
|
|
|
28
32
|
const FAVICON_URI = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%230b1220'/%3E%3Ccircle cx='25' cy='28' r='13' fill='%23e6edf3'/%3E%3Ccircle cx='75' cy='28' r='13' fill='%23e6edf3'/%3E%3Ccircle cx='50' cy='52' r='30' fill='%23e6edf3'/%3E%3Cellipse cx='37' cy='48' rx='9' ry='11' fill='%230b1220' transform='rotate(-15 37 48)'/%3E%3Cellipse cx='63' cy='48' rx='9' ry='11' fill='%230b1220' transform='rotate(15 63 48)'/%3E%3Ccircle cx='38' cy='49' r='3' fill='%2322c55e'/%3E%3Ccircle cx='62' cy='49' r='3' fill='%2322c55e'/%3E%3Cellipse cx='50' cy='62' rx='5' ry='4' fill='%230b1220'/%3E%3C/svg%3E";
|
|
29
33
|
|
|
34
|
+
// ---------------------------------------------------------------- utilidades
|
|
35
|
+
|
|
30
36
|
function venvBin(home) {
|
|
31
37
|
return process.platform === 'win32'
|
|
32
38
|
? join(home, '.runtime', 'venv', 'Scripts', 'python.exe')
|
|
@@ -44,25 +50,23 @@ const MIME = {
|
|
|
44
50
|
'.ico': 'image/x-icon',
|
|
45
51
|
};
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</body>
|
|
62
|
-
</html>`;
|
|
53
|
+
// Testa se uma porta aceita conexão TCP (uvicorn ouvindo).
|
|
54
|
+
function portaAberta(port) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const socket = net.connect({ host: '127.0.0.1', port });
|
|
57
|
+
socket.once('connect', () => {
|
|
58
|
+
socket.destroy();
|
|
59
|
+
resolve(true);
|
|
60
|
+
});
|
|
61
|
+
socket.once('error', () => resolve(false));
|
|
62
|
+
socket.setTimeout(500, () => {
|
|
63
|
+
socket.destroy();
|
|
64
|
+
resolve(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
//
|
|
69
|
+
// Encontra a primeira porta livre a partir de base (probe real de bind).
|
|
66
70
|
function probePort(port) {
|
|
67
71
|
return new Promise((resolve) => {
|
|
68
72
|
const srv = net.createServer();
|
|
@@ -79,6 +83,40 @@ async function findFreePort(base, maxTries) {
|
|
|
79
83
|
throw new Error(`Nenhuma porta livre entre ${base} e ${base + maxTries - 1}`);
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
// Encerra o processo por PID e (no Windows) a árvore de processos /T.
|
|
87
|
+
function encerrarPorPid(pid, motivo) {
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === 'win32') {
|
|
90
|
+
spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], { windowsHide: true });
|
|
91
|
+
} else {
|
|
92
|
+
process.kill(pid, 'SIGTERM');
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
/* processo já não existia */
|
|
96
|
+
}
|
|
97
|
+
console.log(motivo);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------- UI estática/proxy
|
|
101
|
+
|
|
102
|
+
function placeholderPage() {
|
|
103
|
+
return `<!DOCTYPE html>
|
|
104
|
+
<html lang="pt-BR">
|
|
105
|
+
<head>
|
|
106
|
+
<meta charset="utf-8">
|
|
107
|
+
<title>PandaNews</title>
|
|
108
|
+
<link rel="icon" href="${FAVICON_URI}">
|
|
109
|
+
</head>
|
|
110
|
+
<body style="font-family: Inter, system-ui, sans-serif; background: #0b1220; color: #e6edf3; display: flex; min-height: 100vh; align-items: center; justify-content: center;">
|
|
111
|
+
<div style="text-align: center;">
|
|
112
|
+
<h1 style="font-size: 3rem; margin: 0;">🐼 PandaNews</h1>
|
|
113
|
+
<p style="color: #9aa7b5;">Backend ativo em <a style="color: #22c55e;" href="/api/v1/health">/api/v1/health</a></p>
|
|
114
|
+
<p style="color: #9aa7b5;">A UI React completa chega na versão 0.2.00.</p>
|
|
115
|
+
</div>
|
|
116
|
+
</body>
|
|
117
|
+
</html>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
// Encaminha /api/* para o backend FastAPI (na porta ativa).
|
|
83
121
|
function proxyApi(req, res) {
|
|
84
122
|
const options = {
|
|
@@ -154,62 +192,103 @@ function listenUi(port, attempt) {
|
|
|
154
192
|
server.listen(port, '127.0.0.1', () => {
|
|
155
193
|
uiServer = server;
|
|
156
194
|
console.log(`PandaNews ativo em http://127.0.0.1:${port} — abra no seu navegador.`);
|
|
157
|
-
console.log('Pressione Ctrl+C para encerrar.');
|
|
195
|
+
console.log('Pressione Ctrl+C para encerrar o launcher.');
|
|
158
196
|
});
|
|
159
197
|
}
|
|
160
198
|
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
// ------------------------------------------------------- backend destacado
|
|
200
|
+
|
|
201
|
+
let backendPid = null; // PID do backend que ESTA sessão iniciou (null = reaproveitado)
|
|
202
|
+
|
|
203
|
+
// Descobre um backend já em execução: arquivo backend.pid + porta respondendo.
|
|
204
|
+
async function backendJaRodando(home) {
|
|
205
|
+
const pidFile = join(home, 'data', 'backend.pid');
|
|
206
|
+
if (!existsSync(pidFile)) return null;
|
|
207
|
+
const pid = Number(readFileSync(pidFile, 'utf8').trim());
|
|
208
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
209
|
+
// procura o backend em qualquer porta candidata (a que gravou o pid)
|
|
210
|
+
for (let port = API_PORT_BASE; port < API_PORT_BASE + PORT_MAX_TRIES; port++) {
|
|
211
|
+
if (await portaAberta(port)) return { pid, port };
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
163
215
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
216
|
+
async function esperarBackend(port) {
|
|
217
|
+
for (let i = 0; i < BACKEND_POLL_ATTEMPTS; i++) {
|
|
218
|
+
if (await portaAberta(port)) return true;
|
|
219
|
+
await new Promise((r) => setTimeout(r, BACKEND_POLL_MS));
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Sobe o uvicorn em grupo de processos próprio: detached (novo grupo no
|
|
225
|
+
// Windows), windowsHide (sem janela preta) e stdio direto no arquivo de log.
|
|
226
|
+
async function startBackend(home) {
|
|
227
|
+
const port = await findFreePort(API_PORT_BASE, PORT_MAX_TRIES);
|
|
228
|
+
if (port !== API_PORT_BASE) {
|
|
229
|
+
console.log(`Porta ${API_PORT_BASE} ocupada — backend usando ${port}.`);
|
|
230
|
+
}
|
|
231
|
+
activeBackendPort = port;
|
|
232
|
+
|
|
233
|
+
const logPath = join(home, 'data', 'backend.log');
|
|
234
|
+
const bootstrapLine = `--- ${new Date().toISOString()} — iniciando backend (porta ${port}) ---\n`;
|
|
235
|
+
appendFileSync(logPath, bootstrapLine);
|
|
236
|
+
// stdio por FD (obrigatório com detached no Windows: async forks não aceitam path)
|
|
237
|
+
const logFd = openSync(logPath, 'a');
|
|
238
|
+
|
|
239
|
+
console.log(`Iniciando backend FastAPI em http://127.0.0.1:${port} ... (log: ${logPath})`);
|
|
240
|
+
const python = spawn(
|
|
170
241
|
venvBin(home),
|
|
171
|
-
['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(
|
|
242
|
+
['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(port)],
|
|
172
243
|
{
|
|
173
244
|
cwd: PKG_ROOT,
|
|
174
|
-
|
|
175
|
-
|
|
245
|
+
detached: true, // CREATE_NEW_PROCESS_GROUP no Windows — imune a Ctrl+C do console
|
|
246
|
+
windowsHide: true, // sem janela de console fantasma
|
|
247
|
+
stdio: ['ignore', logFd, logFd], // saída direto no arquivo (append) — não herda console
|
|
248
|
+
env: { ...process.env, PANDANEWS_HOME: home, PYTHONUNBUFFERED: '1' },
|
|
176
249
|
},
|
|
177
250
|
);
|
|
178
|
-
|
|
251
|
+
closeSync(logFd); // o filho tem sua própria cópia herdada
|
|
252
|
+
python.unref(); // o launcher não morre com o backend e vice-versa
|
|
253
|
+
backendPid = python.pid ?? null;
|
|
254
|
+
if (backendPid) {
|
|
255
|
+
try {
|
|
256
|
+
writeFileSync(join(home, 'data', 'backend.pid'), String(backendPid));
|
|
257
|
+
} catch {
|
|
258
|
+
/* melhor esforço */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
179
261
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
uvicorn.stderr.on('data', (chunk) => {
|
|
189
|
-
process.stderr.write(chunk);
|
|
190
|
-
logStream.write(chunk);
|
|
191
|
-
});
|
|
262
|
+
if (await esperarBackend(port)) {
|
|
263
|
+
console.log(`Backend no ar em http://127.0.0.1:${port} (sobrevive a Ctrl+C — encerrado apenas por este launcher).`);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
console.error('Backend não respondeu a tempo — verifique o log completo em:');
|
|
267
|
+
console.error(` ${logPath}`);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
192
270
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
271
|
+
// Watchdog de saúde: spawn destacado não emite eventos de exit confiáveis —
|
|
272
|
+
// a porta é polidada a cada 10s e 3 falhas consecutivas reerguem o backend.
|
|
273
|
+
let falhasSaude = 0;
|
|
274
|
+
function iniciarWatchdog(home) {
|
|
275
|
+
setInterval(async () => {
|
|
276
|
+
if (await portaAberta(activeBackendPort)) {
|
|
277
|
+
falhasSaude = 0;
|
|
197
278
|
return;
|
|
198
279
|
}
|
|
199
|
-
|
|
200
|
-
|
|
280
|
+
falhasSaude += 1;
|
|
281
|
+
if (falhasSaude >= 3) {
|
|
282
|
+
console.error('Backend não responde — reiniciando...');
|
|
283
|
+
falhasSaude = 0;
|
|
284
|
+
backendPid = null;
|
|
285
|
+
await startBackend(home);
|
|
201
286
|
}
|
|
202
|
-
|
|
203
|
-
if (backendRestarts > BACKEND_MAX_RESTARTS) {
|
|
204
|
-
console.error(`Backend caiu ${backendRestarts - 1}x seguidas — desistindo.`);
|
|
205
|
-
console.error(`Log completo do backend: ${logPath}`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
console.error(`Backend encerrou com código ${code} — reiniciando em 3s (tentativa ${backendRestarts}/${BACKEND_MAX_RESTARTS})...`);
|
|
209
|
-
setTimeout(() => startBackend(home), BACKEND_RESTART_DELAY_MS);
|
|
210
|
-
});
|
|
287
|
+
}, 10000).unref();
|
|
211
288
|
}
|
|
212
289
|
|
|
290
|
+
// ---------------------------------------------------------------- execução
|
|
291
|
+
|
|
213
292
|
export async function runStart() {
|
|
214
293
|
const home = resolveHome();
|
|
215
294
|
if (!existsSync(join(home, '.runtime', '.ready'))) {
|
|
@@ -223,19 +302,49 @@ export async function runStart() {
|
|
|
223
302
|
return;
|
|
224
303
|
}
|
|
225
304
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
305
|
+
// 1) reaproveita backend de sessão anterior (launcher morto por Ctrl+C),
|
|
306
|
+
// eliminando o processo órfão perceptível na porta 8000
|
|
307
|
+
const existente = await backendJaRodando(home);
|
|
308
|
+
let backendOk;
|
|
309
|
+
if (existente && existente.port === Number(process.env.PANDANEWS_API_PORT || API_PORT_BASE)) {
|
|
310
|
+
console.log(`Backend já em execução (PID ${existente.pid}) — reaproveitando em http://127.0.0.1:${existente.port}.`);
|
|
311
|
+
activeBackendPort = existente.port;
|
|
312
|
+
backendOk = true;
|
|
313
|
+
} else {
|
|
314
|
+
if (existente) {
|
|
315
|
+
// backend rodando em outra porta: mantemos e deixamos nossa UI usá-la
|
|
316
|
+
console.log(`Backend já em execução (PID ${existente.pid}) na porta ${existente.port} — reaproveitando.`);
|
|
317
|
+
activeBackendPort = existente.port;
|
|
318
|
+
backendOk = true;
|
|
319
|
+
} else {
|
|
320
|
+
backendOk = await startBackend(home);
|
|
321
|
+
}
|
|
230
322
|
}
|
|
231
323
|
|
|
232
|
-
|
|
324
|
+
// 2) interface
|
|
233
325
|
listenUi(UI_PORT_BASE, 1);
|
|
234
326
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
327
|
+
// watchdog de saúde do backend (reergue em quedas silenciosas)
|
|
328
|
+
iniciarWatchdog(home);
|
|
329
|
+
|
|
330
|
+
// 3) sinais: encerramento SEMPRE por código, nunca por propagação do terminal
|
|
331
|
+
process.on('SIGINT', () => {
|
|
332
|
+
console.log('\nEncerrando PandaNews...');
|
|
333
|
+
if (backendPid) {
|
|
334
|
+
encerrarPorPid(backendPid, `Backend (PID ${backendPid}) encerrado — log: ${join(home, 'data', 'backend.log')}`);
|
|
335
|
+
try {
|
|
336
|
+
unlinkSync(join(home, 'data', 'backend.pid'));
|
|
337
|
+
} catch {
|
|
338
|
+
/* melhor esforço */
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
console.log('Backend reaproveitado de sessão anterior — segue rodando em background.');
|
|
342
|
+
}
|
|
343
|
+
process.exit(0);
|
|
238
344
|
});
|
|
239
|
-
process.on('SIGINT', () => process.exit(0));
|
|
240
345
|
process.on('SIGTERM', () => process.exit(0));
|
|
346
|
+
|
|
347
|
+
if (!backendOk) {
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
}
|
|
241
350
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Teste de regressão do isolamento de processos do launcher (scripts/start.js).
|
|
2
|
+
//
|
|
3
|
+
// Falha se as flags de isolamento forem removidas: detached (novo grupo de
|
|
4
|
+
// processos — imune a Ctrl+C do console no Windows), windowsHide (sem janela
|
|
5
|
+
// fantasma), stdio no arquivo (backend sobrevive ao terminal), unref e o
|
|
6
|
+
// arquivo backend.pid (encerramento gracioso por código).
|
|
7
|
+
//
|
|
8
|
+
// Roda em qualquer plataforma, inclusive no CI (Linux/ubuntu) — sem exigir o
|
|
9
|
+
// runtime Python completo.
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const src = readFileSync(join(root, 'scripts', 'start.js'), 'utf8');
|
|
16
|
+
|
|
17
|
+
const verificacoes = [
|
|
18
|
+
[/detached:\s*true/, 'detached: true — grupo de processos próprio (CREATE_NEW_PROCESS_GROUP no Windows)'],
|
|
19
|
+
[/windowsHide:\s*true/, 'windowsHide: true — sem janela de console fantasma'],
|
|
20
|
+
[/stdio:\s*\['ignore',\s*logFd,\s*logFd\]/, "stdio: ['ignore', logFd, logFd] — saída do backend no arquivo (fd)"],
|
|
21
|
+
[/openSync\(logPath, 'a'\)/, 'openSync do log em append (fd reaproveitado pelo filho)'],
|
|
22
|
+
[/PYTHONUNBUFFERED/, 'PYTHONUNBUFFERED=1 — logs do backend sem buffer'],
|
|
23
|
+
[/backend\.pid/, 'backend.pid — PID gravado para encerramento gracioso'],
|
|
24
|
+
[/\.unref\(\)/, 'unref() — launcher não acopla o ciclo de vida ao backend'],
|
|
25
|
+
[/taskkill/, 'encerramento no Windows via taskkill /PID /T (árvore de processos)'],
|
|
26
|
+
[/process\.kill/, 'encerramento no Unix via signal no grupo de processos'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let falhas = 0;
|
|
30
|
+
for (const [regex, descricao] of verificacoes) {
|
|
31
|
+
const ok = regex.test(src);
|
|
32
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} — ${descricao}`);
|
|
33
|
+
if (!ok) falhas += 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (falhas > 0) {
|
|
37
|
+
console.error(`Isolamento de processos: ${falhas} verificação(ões) falharam.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.log('Isolamento de processos: todas as verificações passaram.');
|