Spaces:
Sleeping
Sleeping
| import { execFile } from 'node:child_process'; | |
| import http from 'node:http'; | |
| import { promisify } from 'node:util'; | |
| import { loadLocalEnv } from '../gaokao/lib/env.mjs'; | |
| loadLocalEnv(); | |
| const execFileAsync = promisify(execFile); | |
| const intervalMinutes = Number(process.env.WORLDCUP_SYNC_INTERVAL_MINUTES || 30); | |
| const stopAt = new Date(process.env.WORLDCUP_SYNC_UNTIL || '2026-07-20T00:00:00Z'); | |
| const intervalMs = Math.max(5, intervalMinutes) * 60 * 1000; | |
| const port = Number(process.env.PORT || 7860); | |
| const runTimeoutMs = Number(process.env.WORLDCUP_SYNC_RUN_TIMEOUT_MS || 10 * 60 * 1000); | |
| let timer = null; | |
| let runPromise = null; | |
| const health = { | |
| status: 'starting', | |
| started_at: new Date().toISOString(), | |
| last_run_started_at: null, | |
| last_run_finished_at: null, | |
| last_run_status: null, | |
| last_error: null, | |
| next_run_at: null, | |
| stop_at: stopAt.toISOString(), | |
| }; | |
| function isStopped() { | |
| return new Date() >= stopAt; | |
| } | |
| function isDue() { | |
| if (health.status !== 'waiting' || !health.next_run_at) return false; | |
| return new Date(health.next_run_at).getTime() <= Date.now(); | |
| } | |
| function scheduleNextRun(from = new Date()) { | |
| if (timer) clearTimeout(timer); | |
| timer = null; | |
| if (isStopped()) { | |
| health.status = 'stopped'; | |
| health.next_run_at = null; | |
| console.log('[worldcup-sync-daemon] stop date reached; no more runs scheduled.'); | |
| return; | |
| } | |
| const nextAtMs = Math.min(from.getTime() + intervalMs, stopAt.getTime()); | |
| const waitMs = Math.max(0, nextAtMs - Date.now()); | |
| health.next_run_at = new Date(nextAtMs).toISOString(); | |
| console.log(`[worldcup-sync-daemon] next run in ${Math.round(waitMs / 60000)} minutes`); | |
| timer = setTimeout(() => { | |
| triggerRun('timer').catch((error) => { | |
| console.error('[worldcup-sync-daemon] timer trigger failed:', error.message || error); | |
| }); | |
| }, waitMs); | |
| } | |
| async function triggerRun(reason) { | |
| if (runPromise) return runPromise; | |
| if (isStopped()) { | |
| health.status = 'stopped'; | |
| health.next_run_at = null; | |
| return null; | |
| } | |
| console.log(`[worldcup-sync-daemon] run triggered by ${reason}`); | |
| runPromise = (async () => { | |
| await runOnce(); | |
| scheduleNextRun(new Date()); | |
| })().finally(() => { | |
| runPromise = null; | |
| }); | |
| return runPromise; | |
| } | |
| function startHealthServer() { | |
| const server = http.createServer((req, res) => { | |
| if (req.url === '/' || req.url === '/healthz') { | |
| const dueTriggered = isDue(); | |
| if (dueTriggered) { | |
| triggerRun('health-request-overdue').catch((error) => { | |
| console.error('[worldcup-sync-daemon] overdue health trigger failed:', error.message || error); | |
| }); | |
| } | |
| res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' }); | |
| res.end(JSON.stringify({ ...health, due_triggered: dueTriggered }, null, 2)); | |
| return; | |
| } | |
| res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' }); | |
| res.end(JSON.stringify({ error: 'not_found' })); | |
| }); | |
| server.listen(port, '0.0.0.0', () => { | |
| console.log(`[worldcup-sync-daemon] health server listening on 0.0.0.0:${port}`); | |
| }); | |
| } | |
| async function runOnce() { | |
| const startedAt = new Date(); | |
| health.status = 'running'; | |
| health.last_run_started_at = startedAt.toISOString(); | |
| health.last_run_finished_at = null; | |
| health.last_run_status = 'running'; | |
| health.last_error = null; | |
| health.next_run_at = null; | |
| console.log(`[worldcup-sync-daemon] sync started at ${startedAt.toISOString()}`); | |
| try { | |
| const result = await execFileAsync(process.execPath, ['scripts/worldcup/sync-worldcup-data.mjs'], { | |
| cwd: process.cwd(), | |
| maxBuffer: 1024 * 1024 * 20, | |
| timeout: runTimeoutMs, | |
| killSignal: 'SIGTERM', | |
| }); | |
| if (result.stdout) process.stdout.write(result.stdout); | |
| if (result.stderr) process.stderr.write(result.stderr); | |
| health.status = 'waiting'; | |
| health.last_run_status = 'success'; | |
| health.last_run_finished_at = new Date().toISOString(); | |
| console.log(`[worldcup-sync-daemon] sync finished in ${Math.round((Date.now() - startedAt.getTime()) / 1000)}s`); | |
| } catch (error) { | |
| if (error.stdout) process.stdout.write(error.stdout); | |
| if (error.stderr) process.stderr.write(error.stderr); | |
| health.status = 'waiting'; | |
| health.last_run_status = 'failed'; | |
| health.last_run_finished_at = new Date().toISOString(); | |
| health.last_error = error.message || String(error); | |
| console.error('[worldcup-sync-daemon] sync failed:', error.message || error); | |
| } | |
| } | |
| console.log(`[worldcup-sync-daemon] interval=${intervalMinutes} minutes stop_at=${stopAt.toISOString()}`); | |
| startHealthServer(); | |
| triggerRun('startup').catch((error) => { | |
| console.error('[worldcup-sync-daemon] startup trigger failed:', error.message || error); | |
| }); | |