MovixOpenSource/API/Mainapi/server.js
Movix 8657a856f8 feat: live TV FCTV, fournisseur OAuth, sources J1F/Cinestream, modération IA & durcissement
feat: live TV FCTV, fournisseur OAuth, sources J1F/Cinestream, modération IA & durcissement

Gros lot de travail synchronisé depuis le repo de dev. Les grands axes :
migration de domaine (movix.date + miroirs), deux nouvelles sources films/séries
(1jour1film, Cinestream) et une n ouvelle source livetv, un fournisseur OAuth « Se connecter avec Movix », une
modération de contenu par IA revue, plus un lot de durcissement mémoire/réseau,
sécurité et perf côté API et front.

## 🌐 Domaines & miroirs

- Domaine canonique `movix.chat`/`movix.golf`/`movix.tax` → **movix.date** partout
  (front, allow-lists CORS/domaine, Discord RPC). Miroir `movix.health` →
  **movix.online** (Footer, Header, pages d'aide CaMarchePlus/Dns/Miroirs). Lien
  Telegram du footer → `mysticsaba.com`. ~10 clés i18n mises à jour.

## 🎬 Sources de streaming (VOD)

- **J1F (1jour1film)** — nouvelle source de bout en bout (`routes/j1f.js`, monté sur
  `/api/j1f`). Résout le domaine tournant via une page `/go/`, décode les blobs base64
  `J1F_SRV`/`j1fEpsData` (Dooplay/WP), ne garde que les players uniques, ignore les
  agrégateurs (frembed/vidsrc/videasy). Cache stale-while-revalidate dédié. Players 1J1F
  lus via l'extension/userscript.
- **Cinestream** — nouvelle source films (`routes/cinestream.js`, scrape Next.js
  `cinestream.info`, match par id TMDB autoritaire). Wiflix bascule ses **films** sur
  Cinestream (bot-shield flemmix pénible sur les films) ; la **TV** reste sur flemmix.
- **Wiflix** — `flemmix.farm` → **`flemmix.golf`** ; handshake cookie
  (`makeWiflixSearchRequest`, `h_check=25`) ; sélecteurs de template corrigés.
- **Cpasmal** — axios+proxy → `makeCpasmalRequest` (CycleTLS JA3) car Cloudflare 403 ;
  recherche GET → POST form (le WAF bloque le GET).
- **Fstream** — cookie anti-bot `fsschal` ; source épisodes JS statique
  (`/static/series/<id>.js`) avant l'AJAX `episodes_p.php` ; fallback `get_seasons.php`.
- **FranceTV** — `fetchSeasonEpisodesViaDeepPage` (pagination JSON « Afficher plus »).
- **Coflix** — `coflix.date` → `coflix.trade` ; logs 429 silencés en double.
- **proxyManager** — cinq helpers de contournement Cloudflare exportés
  (`makeCinestreamRequest`, `make1j1fRequest`, `makeCpasmalRequest`, handshake Wiflix
  `makeWiflixSearchRequest`/`wiflixHandshake`, `classifyCloudflare429` worker-vs-site) ;
  CycleTLS + rotation ProxyScrape, ne throw jamais sur statut HTTP. Coflix/LecteurVideo
  quittent les Cloudflare Workers pour ProxyScrape.

## 📺 Live TV — remplacement de Sosplay/Vavoo par FCTV33

- Source sport **Sosplay → FCTV33** (API RBTV, protobuf brut) réécrite de bout en bout
  dans `liveTvRoutes.js`, **Vavoo retiré** (manifest/catalogue/stream) — tout passe par
  `/api/livetv/*`.
- Pipeline neuf : décodeur protobuf maison, signature `sfver` (bs keys + MD5),
  auto-découverte des domaines API/player tournants (`hubu.ru/fctvlink` + Nuxt SSR /
  `params` rot47, cache 30 min), scraper de matchs (`scrapeFctvMatches` :
  équipes/logos, ligue, pays, statut live, tri live-first), résolution de flux (HLS
  natif d'abord, embed en secours), token CDN (XOR keystream + base64, gate
  `rb-session`).
- Route `GET /api/livetv/fctv/playlist` : proxy m3u8 intelligent (résout le token,
  réécrit les segments, retry sur token périmé, modes `raw` extension / `proxy` VIP).
- **Matchs plus réservés aux VIP** (seul `iptv_` le reste) : flux natif proxifié pour
  les VIP, stub `_fctvLocal` résolu côté client par
  l'extension/userscript (`RESOLVE_FCTV`), fallback embed sinon.
- Front `LiveTV.tsx` : `matches` → `fctv`, UI accordéon par match (logos, badges
  ligue/pays, score/timer live, favori), disclaimer, fix icône `music`. Rotations
  d'hôtes : `witv.team`→`witv.football`, `livetv876.me`→`livetv901.me`.

## 🧩 Extensions · Userscript · Discord RPC

- **FCTV en lecture native** : handlers `SETUP_FCTV_HEADERS`/`RESOLVE_FCTV` (règle DNR
  Referer/Origin/UA sur `/cfall/s*` + résolveur local IP-bindé : protobuf-lite + rot47
  + token XOR).
- **Vavoo retiré** , `cinepulse.lol` → `purstream.mx`.
- Fix extracteur **Sibnet** (`Range: bytes=0-0`) ; fix bug userscript `matchToRegex`
  (patterns ancrés `^…$` cassant `/cfall/s` → dé-ancrés) ; override dev
  `localhost:25565`.
- Bumps de version (Chrome 1.3.3→1.3.8, Firefox 1.5.1→1.5.5, userscript 1.4.0→1.4.7) ;
  PreMid nouveau `clientId` Discord + `regExp` élargi (1.0.3→1.0.4).

## 🔑 OAuth (fournisseur « Se connecter avec Movix ») & codes d'accès

- Refonte du flux d'autorisation OAuth (`OAuthAuthorizePage.tsx`) + routes
  (`routes/authRoutes.js`, `routes/oauth.js`) et stockage des clients OAuth étoffé
  (`utils/oauthClientsDb.js`, `routes/adminOauthApps.js` pour la gestion admin des apps).
- `AccessCodeForm.tsx` retouché en cohérence avec le nouveau flux ; `checkVip.js` ajusté.
- Session centralisée : `accountAuth.ts` expose `clearStoredAuthSession()` /
  `broadcastAuthChange()` (liste `AUTH_KEYS` unifiée), désormais utilisés partout à la
  déconnexion.

## 💬 Modération de contenu (IA)

- Modèle OpenRouter `google/gemini-2.5-flash-lite` → `deepseek/deepseek-v4-flash`
  (`commentsRoutes.js`, `sharedListsRoutes.js`) ; fonctions/logs renommés
  `moderateContentWithGemini` → `moderateContentWithAI` (découplés du nom du modèle).
- Prompt durci (critères INSULTES / DEMANDE_AJOUT resserrés) pour éviter les faux
  positifs sur questions, signalements de bug et plaintes de contenu manquant.
- `AdminComments.tsx` : libellé « par Gemini » → clé i18n générique.

## 🧊 Hydracker / Darkiworld

- **Sortie du debrid** : AllDebrid/RealDebrid supprimés (`utils/hydrackerLive.js` :
  branche provider retirée ; vars `ALLDEBRID_*`/`REALDEBRID_*` sorties du `.env`) —
  hydracker renvoie directement `rawUrl`/`directDL`. Liens servis depuis les snapshots
  SQLite locaux (`utils/darkiworldSqlite.js`, `darkino-backups/`).
- **Anti-scraping** sur `/decode/:id` (`routes/darkiworld.js`) : watchlist + alerte
  webhook Discord (`DISCORD_SCRAPER_WEBHOOK`) et empoisonnement/blocage de réponse pour
  les IP listées (`SCRAPER_BLOCKED_IPS`).
- **Mémoire** : cache d'agents proxy borné (`utils/lruMap.js`) qui détruit les sockets
  des agents évincés.

## 🛠️ Admin — propriété des liens & équipe

- **Propriété des liens** : les liens streaming gagnent l'attribution `added_by` et
  l'ownership est appliquée des deux côtés (uploader = ses liens, admin = tous). GET
  public masqué (`stripLinkOwner`), `GET /admin/streaming-links/:type/:id` authentifié
  conserve le tag ; `DELETE /admin/links` par URL unique ; `PUT /admin/links` masse
  admin-only.
- **Équipe** : `GET/POST/DELETE /admin/admins` + `GET /admin/team/history` (audit),
  identité via `adminIdentity.js` (option `preferProfile` → premier profil Movix). UI
  `AdminTeam.tsx`.
- **Privilèges** (`middleware/auth.js`) : `isAdmin` rejette `role !== 'admin'` (403) —
  les uploaders ne passent plus les routes admin-only. Migration
  `add_role_to_admins.sql`.

## 🎁 Wrapped v2

- Cartes de partage : logique canvas extraite (`utils/wrappedCanvas.ts`,
  `utils/wrappedShareCards.ts`), formats de partage supplémentaires + preview.
- Nouvelles slides : jour record, répartition par jour de semaine, champion de rewatch,
  âge ciné (année de sortie médiane pondérée par le temps de visionnage).
- Perf : données depuis le payload backend (zéro re-fetch TMDB), trailer lazy,
  percentile non bloquant (réchauffe en arrière-plan, lock Redis `NX`), shuffle seedé.

## 🔐 Auth · Profils · VIP

- **Pseudo forcé** : `validateProfileName` (1–32 chars, strip control/zero-width, +
  `maxLength={32}` côté UI Profile/WatchParty) ; les pseudos OAuth trop longs →
  `requiresUsernameChange` + modale bloquante `RequireUsernameChange.tsx`.
- **Sync durci** (`ProfileContext.tsx`, `syncPolicy.js`) : anti-replay cross-user,
  filtrage des ops à clés non-syncables (`access_code`/`access_code_expires` exclues),
  « wipe guard » qui préserve le local sur profil vide inattendu, dédupe des
  `loadProfiles()` concurrents. Logout centralisé (voir OAuth).
- **VIP** : `is_vip` préservé au chargement de profil (corrige un downgrade VIP
  inter-onglets qui relançait la popup pub) ; `vipDonations.js` poll de secours
  `payment-status.php` (via `ipn_token`, colonne `paygate_ipn_token`) → plus d'invoices
  bloquées.

## ▶️ Lecteur (HLS)

- Basse latence opt-in : `isLowLatencyEnabled('movies')` (scope movies/livetv,
  `lowLatencyPref.ts`) au lieu du mode forcé ; `FloatingPlayer` aligné.
- Sous-titres cinep : wrapper m3u8 synthétique pour les URI `.vtt`/`.srt` directes,
  `isSubtitleLoadError` (une piste cassée ne tue plus la vidéo), rendu `<i>/<b>/<u>`.
- Menu sources regroupé par langue (VF/VFQ/VFF/VOSTFR).

## 📡 Cast (AirPlay / Chromecast)

- Retrait du contournement proxy darkibox (`buildApiProxyUrl`) → cast direct de l'URL ;
  `runtime.ts` supprime `API_PROXY_BASE_URL`/`buildApiProxyUrl` devenus morts.
- Paramètre `streamType` (`BUFFERED` par défaut) propagé jusqu'à
  `loadMediaOnCastWithFallback`.

## 📢 Pubs

- Modes de popup (`adPopupMode.ts`) : `normal`, `auto`, `click-anywhere` ; toggle « +18 »
  séparé (`adAdultMode.ts`). `SCRIPT_AD_MODE_ENABLED` → `false`, URL du script changée.

## 🔎 Recherche & priorité des sources

- `searchUtils.ts` : le terme original contourne le filtre « trop générique » (fix
  zéro-résultat sur titres courts : Baki, Gantz).
- `animeSignals.ts` : détection donghua (CN/TW/HK ou `zh` + Animation, hors C-drama).
- `sourceAutoSelect.ts` : `VFF`/`VFQ` normalisés en `vf` (sinon reléguées derrière
  VOSTFR). `sourcePriorityPrefs.ts` : schéma v2 → v3 (nouvel ordre par défaut + `j1f`,
  migration non destructive des ordres personnalisés). `extractionPrefs.ts` : +daddylive.

## 🔒 Sécurité

- Sanitisation d'URL avant rendu en `href` : `CommentsSection.tsx` (liens markdown
  `http(s)://`) et `Greenlight/AdminLinkSubmissions.tsx` — bloque les `javascript:`.

## ⚙️ Infra & perf

- **server.js** : recyclage périodique des workers (12h) + `[memstats]` + hook
  `SIGUSR2` → heap-snapshot.
- **app.js** : monte `/api/j1f`, retire la route statique `/oauth-icons`, passe
  `makeCpasmalRequest` au router Cpasmal, `coflix.date` → `coflix.trade`.
- **lazyWithRetry.ts / main.tsx / ErrorBoundary.tsx** : récupération chunk-load réécrite
  (retry + reload budgété), garde `removeChild/insertBefore`, écran soft « Mise à jour »
  au lieu du crash-report.
- **UI** : `TopProgressBar` écrit `transform` directement sur un ref (compositor-only,
  plus de re-render) ; `blur-text` relâche `will-change` après l'animation.
- **proxiesembed/server.py** : `curl_cffi` (JA3) pour `cinep-proxy` + retry SOCKS5 sur
  403 ; lookup clé VIP réaligné sur `checkVip.js` ; log WARNING par défaut ; SOCKS5
  forcé IPv4 ; wrapper VTT/SRT pour hls.js ; re-label `application/zip` → `video/mp2t` ;
  `cinepulse.lol` → `purstream.mx`.

## 🧹 Nettoyage & suppressions

- **Front** : carte promo « Loadix » sur Download (remplace l'encart beta),
  « 1jour1film » ajouté à « C'est quoi Movix », fallback Darkino/Nightflix gutté en
  no-op (`api.movix.chat` décommissionné), classes CSS mortes retirées, fix
  `fetchPriority` → `fetchpriority`.
- **Backend** : scrapers/utilitaires morts retirés — `routes/dragiv.js`,
  `routes/topstream.js`, `routes/proxy.js`, `utils/cloudflareSession.js`,
  `utils/hydrackerBatch.js`, `utils/hydrackerQueue.js'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 18:22:13 +02:00

337 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const cluster = require('cluster');
const os = require('os');
require('dotenv').config();
// === CLUSTER MODE CONFIGURATION (au tout début pour éviter que le master charge tout) ===
const NUM_WORKERS = parseInt(process.env.NUM_WORKERS) || 6; // 6 coeurs physiques sur le serveur
if (cluster.isPrimary ?? cluster.isMaster) {
// === MODE MASTER — ne charge RIEN d'autre (pas de MySQL, Redis, Express, etc.) ===
console.log(`🚀 Master process ${process.pid} démarré en mode cluster`);
console.log(`📊 Création de ${NUM_WORKERS} workers...`);
for (let i = 0; i < NUM_WORKERS; i++) {
const worker = cluster.fork();
console.log(
`✓ Worker ${worker.process.pid} créé (${i + 1}/${NUM_WORKERS})`,
);
}
// Anti-fork-bomb : limiter les redémarrages rapides
const workerRestarts = new Map(); // pid -> [timestamps]
const MAX_RESTARTS = 5;
const RESTART_WINDOW_MS = 60000; // 1 minute
let isShuttingDown = false; // Flag pour empêcher le redémarrage des workers pendant le shutdown
cluster.on('exit', (worker, code, signal) => {
if (signal) {
console.warn(`⚠️ Worker ${worker.process.pid} tué par le signal ${signal}`);
} else if (code !== 0) {
console.error(`❌ Worker ${worker.process.pid} terminé avec le code ${code}`);
} else {
console.log(` Worker ${worker.process.pid} terminé normalement`);
}
// Ne pas redémarrer les workers si le master est en cours d'arrêt
if (isShuttingDown) {
console.log(`🛑 Shutdown en cours — worker ${worker.process.pid} ne sera pas redémarré`);
return;
}
// Vérifier le taux de redémarrage pour éviter la boucle infinie
const now = Date.now();
const restarts = workerRestarts.get('global') || [];
const recentRestarts = restarts.filter(t => now - t < RESTART_WINDOW_MS);
recentRestarts.push(now);
workerRestarts.set('global', recentRestarts);
if (recentRestarts.length > MAX_RESTARTS) {
console.error(`🚨 Trop de redémarrages (${recentRestarts.length} en ${RESTART_WINDOW_MS / 1000}s) — arrêt du fork`);
return;
}
console.log(`🔄 Redémarrage d'un nouveau worker...`);
const newWorker = cluster.fork();
console.log(`✅ Nouveau worker ${newWorker.process.pid} créé`);
});
// Graceful shutdown master
const shutdownMaster = () => {
if (isShuttingDown) return; // Guard: SIGINT + SIGTERM peuvent arriver quasi-simultanement
isShuttingDown = true;
console.log('\n🛑 Signal de fermeture reçu par le master...');
console.log('📤 Envoi du signal de fermeture à tous les workers...');
for (const id in cluster.workers) {
cluster.workers[id].send('shutdown');
}
let workersAlive = Object.keys(cluster.workers).length;
const checkInterval = setInterval(() => {
workersAlive = Object.keys(cluster.workers).length;
if (workersAlive === 0) {
clearInterval(checkInterval);
console.log('✅ Tous les workers sont arrêtés. Arrêt du master.');
process.exit(0);
}
}, 100);
setTimeout(() => {
console.warn('⚠️ Timeout atteint (30s). Arrêt forcé du master.');
process.exit(1);
}, 30000);
};
process.on('SIGTERM', shutdownMaster);
process.on('SIGINT', shutdownMaster);
console.log(`
╔═══════════════════════════════════════════════════════╗
║ 🎯 Mode Cluster Actif ║
║ 👷 Workers: ${NUM_WORKERS.toString().padEnd(42, ' ')}
║ 🔒 Redis Locks: Verrous distribués entre workers ║
║ ⚛️ Atomic writes: Garantis entre les processus ║
╚═══════════════════════════════════════════════════════╝
`);
// === WORKER RECYCLE (anti-leak mitigation) ================================
// Root cause of the multi-day RSS climb is not yet confirmed. Recycling each
// worker one-by-one on a long interval caps how far any slow leak can grow
// before that worker's memory is reclaimed by a fresh fork. Each worker gets
// the same graceful 'shutdown' message the SIGTERM path sends, so in-flight
// requests drain via server.close() first; the cluster.on('exit') handler
// above then forks the replacement.
//
// Tunables (env):
// WORKER_RECYCLE_INTERVAL_MS default 12h — set 0 to disable entirely
// WORKER_RECYCLE_STAGGER_MS default 5min — gap between each worker so
// capacity never drops hard
const WORKER_RECYCLE_INTERVAL_MS = (() => {
const raw = process.env.WORKER_RECYCLE_INTERVAL_MS;
if (raw === undefined || raw === '') return 12 * 60 * 60 * 1000;
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 12 * 60 * 60 * 1000;
})();
const WORKER_RECYCLE_STAGGER_MS =
parseInt(process.env.WORKER_RECYCLE_STAGGER_MS, 10) || (5 * 60 * 1000);
if (WORKER_RECYCLE_INTERVAL_MS > 0) {
console.log(
`🔁 Worker recycle: every ${Math.round(WORKER_RECYCLE_INTERVAL_MS / 3600000)}h, ` +
`stagger ${Math.round(WORKER_RECYCLE_STAGGER_MS / 60000)}min`,
);
const recycleTimer = setInterval(() => {
if (isShuttingDown) return;
const workers = Object.values(cluster.workers || {});
if (workers.length === 0) return;
console.log(`🔁 Worker recycle cycle starting — ${workers.length} workers`);
workers.forEach((worker, idx) => {
setTimeout(() => {
if (isShuttingDown || worker.isDead()) return;
try {
console.log(`🔁 Recycling worker pid=${worker.process.pid}`);
worker.send('shutdown');
// Force-kill fallback if the worker's graceful 15s server.close +
// resource cleanup hasn't exited it within 35s.
setTimeout(() => {
try {
if (!worker.isDead()) worker.kill('SIGKILL');
} catch (_) { /* ignore */ }
}, 35000);
} catch (e) {
console.warn(`🔁 Recycle worker ${worker.process?.pid} failed: ${e.message}`);
}
}, idx * WORKER_RECYCLE_STAGGER_MS);
});
}, WORKER_RECYCLE_INTERVAL_MS);
recycleTimer.unref();
} else {
console.log('🔁 Worker recycle disabled (WORKER_RECYCLE_INTERVAL_MS=0)');
}
// Le master ne fait RIEN d'autre — pas de require express, mysql, redis, etc.
return;
}
// =============================================================================
// === WORKER PROCESS ONLY (below) =============================================
// =============================================================================
process.env.UV_THREADPOOL_SIZE = 8; // 8 threads libuv par worker (6 workers x 8 = 48 threads total)
const http = require('http');
const https = require('https');
const v8 = require('v8');
const path = require('path');
const { app, appReady } = require('./app');
const { redis } = require('./config/redis');
const { shutdownCycleTLS, refreshProxyScrapeProxies } = require('./utils/proxyManager');
const { getPool } = require('./mysqlPool');
const PORT = 25565;
// ===========================================================================
// === MEMORY DIAGNOSTICS (worker) ===========================================
// ===========================================================================
// The cluster RSS climbs continuously over multi-day uptime with no confirmed
// root cause yet. These hooks gather the evidence:
// - Periodic [memstats] line — feed it to a graph to see the leak slope and
// which segment (heap vs external/arrayBuffers vs sockets) is growing.
// - SIGUSR2 → v8 heap snapshot on disk — open in Chrome DevTools, compare two
// snapshots taken hours apart to find the retained object class.
// Trigger: kill -USR2 <worker-pid>
//
// Both are read-only and effectively free; safe to keep in production.
const MEMORY_LOG_INTERVAL_MS = parseInt(process.env.MEMORY_LOG_INTERVAL_MS, 10) || (5 * 60 * 1000);
const HEAPDUMP_DIR = process.env.HEAPDUMP_DIR || os.tmpdir();
function fmtMB(n) {
return `${(Number(n || 0) / 1024 / 1024).toFixed(1)}MB`;
}
// http.globalAgent.sockets is keyed by `host:port` — its key count is the
// number of distinct upstream origins currently holding live sockets. A
// climbing count points at keep-alive socket pool churn.
function countAgentHosts(agent, prop) {
try {
return agent && agent[prop] ? Object.keys(agent[prop]).length : 0;
} catch (_) {
return 0;
}
}
function logMemoryUsage() {
const m = process.memoryUsage();
console.log(
`[memstats] pid=${process.pid} uptime=${Math.round(process.uptime())}s ` +
`rss=${fmtMB(m.rss)} heapTotal=${fmtMB(m.heapTotal)} heapUsed=${fmtMB(m.heapUsed)} ` +
`external=${fmtMB(m.external)} arrayBuffers=${fmtMB(m.arrayBuffers)} ` +
`httpHosts=${countAgentHosts(http.globalAgent, 'sockets')}/` +
`${countAgentHosts(http.globalAgent, 'freeSockets')} ` +
`httpsHosts=${countAgentHosts(https.globalAgent, 'sockets')}/` +
`${countAgentHosts(https.globalAgent, 'freeSockets')}`,
);
}
const memoryLogTimer = setInterval(logMemoryUsage, MEMORY_LOG_INTERVAL_MS);
memoryLogTimer.unref();
// SIGUSR2 — write a heap snapshot. nodemon also uses SIGUSR2, but production
// runs node directly so there is no conflict here.
process.on('SIGUSR2', () => {
const snapshotPath = path.join(
HEAPDUMP_DIR,
`heap-${process.pid}-${Date.now()}.heapsnapshot`,
);
try {
const start = Date.now();
v8.writeHeapSnapshot(snapshotPath);
console.warn(
`[heapdump] pid=${process.pid} written ${snapshotPath} in ${Date.now() - start}ms`,
);
} catch (e) {
console.error(`[heapdump] pid=${process.pid} failed: ${e.message}`);
}
});
// ---------------------------------------------------------------------------
// startServer — create HTTP server with retry logic
// ---------------------------------------------------------------------------
const startServer = async (retries = 3) => {
try {
await appReady;
} catch (error) {
if (retries > 0) {
console.error(`[BOOTSTRAP] Échec avant listen: ${error.message}`);
console.log(`Redémarrage... (${retries} restantes)`);
return setTimeout(() => startServer(retries - 1), 5000);
}
console.error('Échec du bootstrap applicatif après plusieurs tentatives');
process.exit(1);
}
try {
await refreshProxyScrapeProxies({ force: false, silent: false });
} catch (error) {
console.warn(`[PROXYSCRAPE] Initialisation incomplete avant listen: ${error.message}`);
}
const server = http.createServer(app);
// Configure Keep-Alive settings
server.keepAliveTimeout = 65000; // 65 secondes
server.headersTimeout = 66000; // 66 secondes
server.maxRequestsPerSocket = 0; // Illimité
server.requestTimeout = 300000; // 5 minutes timeout to prevent hung sockets
// Backlog increased to 4096 to handle burst connections
server.listen(PORT, '0.0.0.0', 4096, () => {
console.log(`Serveur démarré sur le port ${PORT} - Process ${process.pid}`);
console.log(`Keep-Alive configuré: timeout=${server.keepAliveTimeout}ms, max=1000`);
console.log(`Performance tuning: UV_THREADPOOL_SIZE=${process.env.UV_THREADPOOL_SIZE}, RequestTimeout=${server.requestTimeout}ms`);
});
server.on('error', (err) => {
console.error('Erreur de démarrage:', err);
if (retries > 0) {
console.log(`Redémarrage... (${retries} restantes)`);
setTimeout(() => startServer(retries - 1), 5000);
} else {
console.error('Échec du démarrage après plusieurs tentatives');
process.exit(1);
}
});
return server;
};
// === DÉMARRAGE DU WORKER ===
let activeServer = null;
startServer().then((server) => {
activeServer = server;
console.log(`✅ Worker ${process.pid} - Serveur démarré sur le port ${PORT}`);
logMemoryUsage(); // baseline [memstats] line at boot
}).catch((error) => {
console.error(`❌ Worker ${process.pid} - Échec du démarrage:`, error);
process.exit(1);
});
// Graceful shutdown worker — cleanup all resources
const { setShuttingDown, isShuttingDown } = require('./utils/shutdownFlag');
let isWorkerShuttingDown = false;
const shutdownWorker = async () => {
if (isWorkerShuttingDown) return;
isWorkerShuttingDown = true;
setShuttingDown();
console.log(`\n🛑 Worker ${process.pid} - Signal de fermeture reçu...`);
// 1. Arrêter d'accepter de nouvelles connexions et attendre les requêtes en cours
if (activeServer) {
await new Promise((resolve) => {
// Empêcher les nouvelles connexions keep-alive de prolonger le shutdown
activeServer.keepAliveTimeout = 1;
activeServer.close(() => {
console.log(`✅ Worker ${process.pid} - Serveur HTTP fermé (plus de requêtes en cours)`);
resolve();
});
// Force-close après 15s si des connexions trainent
setTimeout(() => {
console.warn(`⚠️ Worker ${process.pid} - Timeout 15s, fermeture forcée des connexions`);
activeServer.closeAllConnections();
resolve();
}, 15000);
});
}
// 2. Cleanup des ressources
try { await redis.quit(); } catch { /* ignore */ }
try { await shutdownCycleTLS(); } catch { /* ignore */ }
try { const pool = getPool(); if (pool) await pool.end(); } catch { /* ignore */ }
process.exit(0);
};
process.on('SIGTERM', shutdownWorker);
process.on('SIGINT', shutdownWorker);
process.on('message', (msg) => {
if (msg === 'shutdown') shutdownWorker();
});