This commit is contained in:
seb
2026-09-29 10:27:23 +02:00
parent 62d46a1df3
commit 3089cc7252
8 changed files with 454 additions and 34 deletions

View File

@@ -1,3 +1,5 @@
import http from 'http';
import https from 'https';
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
@@ -9,6 +11,7 @@ import env from '../config/env.js';
import { authMiddleware } from './middleware/auth.js';
import { ensureConnection } from '../db/store.js';
import { closeAll as closeAllSse } from './sse.js';
import { getOrCreateCert } from '../util/cert.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -36,6 +39,7 @@ async function loadRoutes(app, basePath = '') {
}
let server;
let httpsServer;
let starting;
let startedAt;
@@ -95,8 +99,34 @@ async function createApi() {
return app;
}
function listenServer(instance, port, protocol) {
return new Promise((resolve, reject) => {
instance.once('listening', () => {
resolve();
});
instance.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
let detail = '';
try {
const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8' }).trim();
if (pids) detail = ` (PID ${pids.replace(/\n/g, ', ')})`;
} catch {
// lsof unavailable or no match
}
console.error(`Port ${port} (${protocol}) is already in use${detail}. Check with: lsof -i :${port}`);
} else {
console.error(`${protocol} server error:`, err.message);
}
reject(err);
});
instance.listen(port);
});
}
export async function startApi() {
if (server) {
if (server || httpsServer) {
return server;
}
if (starting) {
@@ -109,31 +139,22 @@ export async function startApi() {
startedAt = new Date().toISOString();
const app = await createApi();
await new Promise((resolve, reject) => {
const instance = app.listen(env.port);
const httpInstance = http.createServer(app);
await listenServer(httpInstance, env.port, 'HTTP');
server = httpInstance;
console.log(`API listening on port ${env.port} (HTTP)`);
instance.once('listening', () => {
server = instance;
console.log(`API listening on port ${env.port}`);
resolve();
});
instance.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
let detail = '';
try {
const pids = execSync(`lsof -ti :${env.port}`, { encoding: 'utf8' }).trim();
if (pids) detail = ` (PID ${pids.replace(/\n/g, ', ')})`;
} catch {
// lsof unavailable or no match
}
console.error(`Port ${env.port} is already in use${detail}. Check with: lsof -i :${env.port}`);
} else {
console.error('Server error:', err.message);
}
reject(err);
});
});
if (env.httpsPort) {
try {
const tls = await getOrCreateCert();
const httpsInstance = https.createServer(tls, app);
await listenServer(httpsInstance, env.httpsPort, 'HTTPS');
httpsServer = httpsInstance;
console.log(`API listening on port ${env.httpsPort} (HTTPS self-signed)`);
} catch (err) {
console.error('Failed to start HTTPS server:', err.message);
}
}
return server;
})();
@@ -146,13 +167,26 @@ export async function startApi() {
}
export async function stopApi() {
if (!server) return;
if (!server && !httpsServer) return;
// Long-lived SSE (and keep-alive) sockets otherwise block server.close()
// forever — node --watch hangs on "Waiting for graceful termination...".
closeAllSse();
await new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
server.closeAllConnections();
});
server = undefined;
const closers = [];
if (server) {
const s = server;
server = undefined;
closers.push(new Promise((resolve, reject) => {
s.close((err) => (err ? reject(err) : resolve()));
s.closeAllConnections();
}));
}
if (httpsServer) {
const s = httpsServer;
httpsServer = undefined;
closers.push(new Promise((resolve, reject) => {
s.close((err) => (err ? reject(err) : resolve()));
s.closeAllConnections();
}));
}
await Promise.all(closers);
}