From 98f21a4b05155e343189c4a0d13de365ba103201 Mon Sep 17 00:00:00 2001 From: Dicken Date: Sat, 11 Jul 2026 07:03:21 +0200 Subject: [PATCH] fix: MakerWorld haengender Puppeteer-Prozess - Lock-Cleanup + hartes Timeout --- backend/src/tools/makerworld/routes.js | 89 ++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/backend/src/tools/makerworld/routes.js b/backend/src/tools/makerworld/routes.js index 4e44fba..e6a90a2 100644 --- a/backend/src/tools/makerworld/routes.js +++ b/backend/src/tools/makerworld/routes.js @@ -32,6 +32,7 @@ const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 const cacheByUser = new Map(); // userId -> { data, fetchedAt, error } const pendingLoginByUser = new Map(); // userId -> { browser, page, codeField, startedAt } const queueByUser = new Map(); // userId -> Promise (Serialisierung je Nutzer) +const activeBrowserByUser = new Map(); // userId -> zuletzt gestarteter Browser (für Force-Kill bei Timeout) function getCache(userId) { if (!cacheByUser.has(userId)) cacheByUser.set(userId, { data: null, fetchedAt: 0, error: null }); @@ -110,18 +111,51 @@ function mapStatisticalData(parsed) { } // ── Browser-Helfer ──────────────────────────────────────────────────────────── +// Nach einem abgebrochenen Vorgang (Container-Neustart mitten im Betrieb, +// Absturz o.ä.) kann Chromium veraltete Sperrdateien im Profil hinterlassen — +// dann hängt der nächste Start ewig, statt einen Fehler zu werfen. Vor jedem +// Start räumen wir das defensiv weg. +function cleanupStaleLocks(profileDir) { + for (const name of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) { + try { fs.rmSync(`${profileDir}/${name}`, { force: true }); } catch {} + } +} + async function launchBrowser(userId) { let puppeteer; try { puppeteer = require('puppeteer-core'); } catch { throw new Error('puppeteer-core nicht installiert'); } const profileDir = `${PROFILE_BASE_DIR}/${userId}`; fs.mkdirSync(profileDir, { recursive: true }); - return puppeteer.launch({ + cleanupStaleLocks(profileDir); + const browser = await puppeteer.launch({ executablePath: '/usr/bin/chromium-browser', userDataDir: profileDir, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'], headless: true, }); + activeBrowserByUser.set(userId, browser); + return browser; +} + +function forceKillBrowser(userId) { + const b = activeBrowserByUser.get(userId); + if (!b) return; + try { b.process()?.kill('SIGKILL'); } catch {} +} + +// Harte Zeitgrenze um einen kompletten Browser-Vorgang — falls irgendwo (Launch, +// Navigation, Screenshot, Close) etwas hängt, wird der Prozess hart beendet statt +// die komplette Warteschlange dieses Nutzers für immer zu blockieren. +function withHardTimeout(promise, ms, onTimeout) { + let timer; + const timeout = new Promise((_, reject) => { + timer = setTimeout(async () => { + try { await onTimeout?.(); } catch {} + reject(new Error(`Zeitüberschreitung nach ${Math.round(ms / 1000)}s — Vorgang wurde abgebrochen`)); + }, ms); + }); + return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } async function getNextDataFromPage(page) { @@ -309,9 +343,12 @@ async function injectCookiesInner(userId, cookieString) { } } -const fetchStatsViaBrowser = (userId, forceRefresh) => serialized(userId, () => fetchStatsViaBrowserInner(userId, forceRefresh)); -const submitLoginCode = (userId, code) => serialized(userId, () => submitLoginCodeInner(userId, code)); -const injectCookies = (userId, cookieString) => serialized(userId, () => injectCookiesInner(userId, cookieString)); +const fetchStatsViaBrowser = (userId, forceRefresh) => + serialized(userId, () => withHardTimeout(fetchStatsViaBrowserInner(userId, forceRefresh), 60000, () => forceKillBrowser(userId))); +const submitLoginCode = (userId, code) => + serialized(userId, () => withHardTimeout(submitLoginCodeInner(userId, code), 60000, () => forceKillBrowser(userId))); +const injectCookies = (userId, cookieString) => + serialized(userId, () => withHardTimeout(injectCookiesInner(userId, cookieString), 45000, () => forceKillBrowser(userId))); // ── Routes (alle nur "authenticate" — jeder verwaltet ausschließlich sich selbst) ── @@ -322,20 +359,39 @@ router.get('/stats', authenticate, async (req, res) => { if (!configured) { return res.json({ configured: false, data: null, error: null, fetchedAt: null, needsCode: false }); } - const result = await fetchStatsViaBrowser(userId, false); - res.json({ configured: true, ...result }); + try { + const result = await fetchStatsViaBrowser(userId, false); + res.json({ configured: true, ...result }); + } catch (e) { + cache.error = e.message; + res.json({ configured: true, data: cache.data, error: cache.error, fetchedAt: cache.fetchedAt, needsCode: false }); + } }); router.post('/refresh', authenticate, async (req, res) => { - const result = await fetchStatsViaBrowser(req.user.id, true); - res.json(result); + const userId = req.user.id; + const cache = getCache(userId); + try { + const result = await fetchStatsViaBrowser(userId, true); + res.json(result); + } catch (e) { + cache.error = e.message; + res.json({ data: cache.data, error: cache.error, fetchedAt: cache.fetchedAt, needsCode: false }); + } }); router.post('/verify-code', authenticate, async (req, res) => { const { code } = req.body; if (!code || typeof code !== 'string') return res.status(400).json({ error: 'Code fehlt' }); - const result = await submitLoginCode(req.user.id, code.trim()); - res.json(result); + const userId = req.user.id; + const cache = getCache(userId); + try { + const result = await submitLoginCode(userId, code.trim()); + res.json(result); + } catch (e) { + cache.error = e.message; + res.json({ data: cache.data, error: cache.error, fetchedAt: cache.fetchedAt, needsCode: false }); + } }); router.get('/login-status', authenticate, (req, res) => { @@ -365,9 +421,16 @@ router.get('/debug-screenshot', authenticate, (req, res) => { router.post('/credentials', authenticate, async (req, res) => { const { username, password } = req.body; if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' }); - saveCredentials(req.user.id, username.trim(), password); - const result = await fetchStatsViaBrowser(req.user.id, true); - res.json(result); + const userId = req.user.id; + saveCredentials(userId, username.trim(), password); + const cache = getCache(userId); + try { + const result = await fetchStatsViaBrowser(userId, true); + res.json(result); + } catch (e) { + cache.error = e.message; + res.json({ data: cache.data, error: cache.error, fetchedAt: cache.fetchedAt, needsCode: false }); + } }); // Eigene Zugangsdaten entfernen (Profil/Cookies bleiben unangetastet liegen)