bend_puzzle/scripts/service-runner.js
33333-33333 9d70afb4cc
Some checks failed
BEND FIELD CI / release (push) Has been cancelled
BEND FIELD CI / production-bridge (push) Has been cancelled
t
2026-08-01 22:31:04 +09:00

70 lines
3.8 KiB
JavaScript

'use strict';
const fs=require('fs');
const path=require('path');
const {spawn}=require('child_process');
const ROOT=path.resolve(__dirname,'..');
const SERVER_FILE=path.join(ROOT,'server.js');
const IDENTITY_FILE=path.resolve(process.env.LINK_FIELD_SERVICE_IDENTITY_FILE||path.join(process.env.LINK_FIELD_SERVICE_DIR||ROOT,'service.identity.json'));
const NONCE=String(process.env.LINK_FIELD_SERVICE_NONCE||'');
const LOG_FILE=String(process.env.LINK_FIELD_LOG_FILE||'');
const PORT_FILE=path.join(String(process.env.LINK_FIELD_PUBLIC_DIR||ROOT),'.linkfield-port');
const RESTART_WINDOW_MS=5*60*1000;
const MAX_RESTARTS=10;
const MAX_LOG_BYTES=Math.max(4096,Number(process.env.LINK_FIELD_LOG_MAX_BYTES)||10*1024*1024);
const LOG_ROTATE_INTERVAL_MS=Math.max(100,Number(process.env.LINK_FIELD_LOG_ROTATE_INTERVAL_MS)||30_000);
const LOG_GENERATIONS=5;
let child=null,stopping=false,restarts=[];
function writeIdentity(){
const value={pid:process.pid,root:ROOT,nonce:NONCE,heartbeatAt:Date.now(),childPid:child?.pid||null};
const temporary=`${IDENTITY_FILE}.${process.pid}.tmp`;
fs.mkdirSync(path.dirname(IDENTITY_FILE),{recursive:true,mode:0o700});
fs.writeFileSync(temporary,`${JSON.stringify(value)}\n`,{encoding:'utf8',mode:0o600});
fs.renameSync(temporary,IDENTITY_FILE);
}
function removeIdentity(){
try{const value=JSON.parse(fs.readFileSync(IDENTITY_FILE,'utf8'));if(value?.pid===process.pid&&value?.nonce===NONCE)fs.unlinkSync(IDENTITY_FILE)}catch(error){if(error?.code!=='ENOENT')console.warn(`Service identity cleanup warning: ${error.message}`)}
try{fs.unlinkSync(PORT_FILE)}catch(error){if(error?.code!=='ENOENT')console.warn(`Bridge-port cleanup warning: ${error.message}`)}
}
function rotateLiveLog(){
if(!LOG_FILE)return;
let stat;try{stat=fs.statSync(LOG_FILE)}catch(error){if(error?.code==='ENOENT')return;throw error}
if(stat.size<MAX_LOG_BYTES)return;
try{fs.unlinkSync(`${LOG_FILE}.${LOG_GENERATIONS}`)}catch(error){if(error?.code!=='ENOENT')throw error}
for(let index=LOG_GENERATIONS-1;index>=1;index--)try{fs.renameSync(`${LOG_FILE}.${index}`,`${LOG_FILE}.${index+1}`)}catch(error){if(error?.code!=='ENOENT')throw error}
fs.copyFileSync(LOG_FILE,`${LOG_FILE}.1`);fs.truncateSync(LOG_FILE,0);
}
function startChild(){
if(stopping)return;
const now=Date.now();restarts=restarts.filter(value=>now-value<RESTART_WINDOW_MS);
if(restarts.length>=MAX_RESTARTS){console.error(`LinkField stopped after ${MAX_RESTARTS} crashes in five minutes.`);removeIdentity();process.exit(1);return}
restarts.push(now);
child=spawn(process.execPath,[SERVER_FILE],{cwd:ROOT,stdio:'inherit',env:{...process.env,LINK_FIELD_SUPERVISED:'1'}});
writeIdentity();
child.once('exit',(code,signal)=>{
const lifetime=Date.now()-now;child=null;writeIdentity();
if(stopping){removeIdentity();process.exit(0);return}
console.error(`LinkField server exited (${signal||code}); restarting.`);
if(lifetime>60_000)restarts=[];
setTimeout(startChild,Math.min(10_000,500*Math.max(1,restarts.length)));
});
}
function shutdown(signal){
if(stopping)return;stopping=true;console.log(`LinkField supervisor received ${signal}; stopping.`);
if(child&&child.exitCode==null){child.kill('SIGTERM');const timer=setTimeout(()=>{if(child&&child.exitCode==null)child.kill('SIGKILL')},5000);timer.unref?.()}
else{removeIdentity();process.exit(0)}
}
process.once('SIGTERM',()=>shutdown('SIGTERM'));
process.once('SIGINT',()=>shutdown('SIGINT'));
process.once('exit',removeIdentity);
const heartbeat=setInterval(()=>{try{writeIdentity()}catch(error){console.error(`Service heartbeat failed: ${error.message}`)}},1000);heartbeat.unref?.();
const logRotation=setInterval(()=>{try{rotateLiveLog()}catch(error){console.error(`Live log rotation failed: ${error.message}`)}},LOG_ROTATE_INTERVAL_MS);logRotation.unref?.();
startChild();