2026-08-01 16:06:14 +09:00
'use strict' ;
const fs = require ( 'fs' ) ;
const fsp = fs . promises ;
const os = require ( 'os' ) ;
const path = require ( 'path' ) ;
2026-08-01 22:31:04 +09:00
const http = require ( 'http' ) ;
const crypto = require ( 'crypto' ) ;
2026-08-01 16:06:14 +09:00
const { spawn , execFile } = require ( 'child_process' ) ;
2026-08-01 22:31:04 +09:00
const { createWorldBackup , verifyWorldBackup , restoreWorldBackup } = require ( '../server/world-backup' ) ;
const { checkPublicDeployment } = require ( '../server/public-health' ) ;
const { renderApacheBootstrap , replaceManagedBlock } = require ( '../server/apache-bridge' ) ;
2026-08-01 16:06:14 +09:00
const ROOT = path . resolve ( _ _dirname , '..' ) ;
const SERVER _FILE = path . join ( ROOT , 'server.js' ) ;
2026-08-01 22:31:04 +09:00
const RUNNER _FILE = path . join ( ROOT , 'scripts' , 'service-runner.js' ) ;
2026-08-01 16:06:14 +09:00
const SERVICE _ROOT = path . resolve ( process . env . LINK _FIELD _SERVICE _DIR || path . join ( os . homedir ( ) , '.local' , 'share' , 'LinkField' , 'service' ) ) ;
const PID _FILE = path . join ( SERVICE _ROOT , 'server.pid' ) ;
2026-08-01 22:31:04 +09:00
const IDENTITY _FILE = path . join ( SERVICE _ROOT , 'service.identity.json' ) ;
2026-08-01 16:06:14 +09:00
const LOG _FILE = path . resolve ( process . env . LINK _FIELD _LOG _FILE || path . join ( SERVICE _ROOT , 'server.log' ) ) ;
2026-08-01 22:31:04 +09:00
const TEST _DATA _ROOT = String ( process . env . LINK _FIELD _TEST _DATA _ROOT || '' ) . trim ( ) ;
const WORLD _DIR = TEST _DATA _ROOT ? path . join ( path . resolve ( TEST _DATA _ROOT ) , 'world' ) : path . resolve ( String ( process . env . LINK _FIELD _WORLD _DIR || '/link-field/world' ) ) ;
const BACKUP _DIR = path . resolve ( String ( process . env . LINK _FIELD _BACKUP _DIR || path . join ( path . dirname ( WORLD _DIR ) , 'backups' ) ) ) ;
2026-08-01 16:06:14 +09:00
const STARTUP _TIMEOUT _MS = 12_000 ;
const POLL _INTERVAL _MS = 100 ;
2026-08-01 22:31:04 +09:00
const MAX _LOG _BYTES = 10 * 1024 * 1024 ;
const LOG _GENERATIONS = 5 ;
2026-08-01 16:06:14 +09:00
const PUBLIC _ENTRIES = Object . freeze ( [
'index.html' ,
'style.css' ,
'favicon.svg' ,
'favicon.ico' ,
'build-meta.js' ,
'runtime-config.js' ,
'shared-contracts.js' ,
'store-catalog.generated.js' ,
'store-catalog.json' ,
'puzzle-patterns.js' ,
'puzzle-core.js' ,
'app-logic.js' ,
'archive-codec.js' ,
'field-persistence.js' ,
'field-persistence-worker.js' ,
'puzzle-worker.js' ,
'app.js' ,
'api-bridge.php' ,
'assets' ,
'client' ,
] ) ;
function sleep ( ms ) {
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
}
function execFileText ( command , args ) {
return new Promise ( ( resolve , reject ) => execFile ( command , args , { encoding : 'utf8' } , ( error , stdout ) => error ? reject ( error ) : resolve ( stdout ) ) ) ;
}
2026-08-01 22:31:04 +09:00
function rootMatches ( value ) {
if ( typeof value !== 'string' || ! value . trim ( ) ) return false ;
const actual = path . resolve ( value ) , expected = ROOT ;
return process . platform === 'win32' ? actual . toLowerCase ( ) === expected . toLowerCase ( ) : actual === expected ;
}
async function readWorldInstanceLock ( worldDir = WORLD _DIR ) {
try {
const raw = ( await fsp . readFile ( path . join ( worldDir , 'server.pid' ) , 'utf8' ) ) . trim ( ) ;
if ( /^\d+$/ . test ( raw ) ) return { pid : Number ( raw ) , legacy : true } ;
const value = JSON . parse ( raw ) ; return value && typeof value === 'object' ? value : null ;
} catch ( _ ) { return null }
}
function isFreshRuntimeRecord ( record , maxAgeMs ) {
return Boolean ( record && rootMatches ( record . root ) && Number . isSafeInteger ( Number ( record . pid ) ) && Number ( record . pid ) > 0 && isProcessRunning ( Number ( record . pid ) ) && Date . now ( ) - Number ( record . heartbeatAt || 0 ) < maxAgeMs ) ;
}
async function isLinkFieldRoot ( value ) {
if ( rootMatches ( value ) ) return true ;
if ( typeof value !== 'string' || ! value . trim ( ) ) return false ;
try { const pkg = JSON . parse ( await fsp . readFile ( path . join ( path . resolve ( value ) , 'package.json' ) , 'utf8' ) ) ; return [ 'link-field-v47-shared-world' , 'link-field-shared-world' ] . includes ( pkg ? . name ) } catch ( _ ) { return false }
}
async function isTrustedRuntimeRecord ( record , maxAgeMs ) {
const pid = Number ( record ? . pid ) , age = Date . now ( ) - Number ( record ? . heartbeatAt || 0 ) ;
return Boolean ( Number . isSafeInteger ( pid ) && pid > 0 && pid !== process . pid && isProcessRunning ( pid ) && age >= - 5000 && age < maxAgeMs && await isLinkFieldRoot ( record ? . root ) ) ;
}
async function processExecutableIsNode ( pid ) {
if ( ! Number . isSafeInteger ( pid ) || pid <= 0 || pid === process . pid || ! isProcessRunning ( pid ) ) return false ;
if ( process . platform === 'win32' ) {
try { const output = await execFileText ( 'powershell.exe' , [ '-NoProfile' , '-NonInteractive' , '-Command' , ` (Get-Process -Id ${ pid } -ErrorAction Stop).Path ` ] ) ; return [ 'node.exe' , 'nodejs.exe' ] . includes ( path . basename ( output . trim ( ) ) . toLowerCase ( ) ) } catch ( _ ) { return false }
}
try { return [ 'node' , 'nodejs' ] . includes ( path . basename ( await fsp . readlink ( ` /proc/ ${ pid } /exe ` ) ) . toLowerCase ( ) ) } catch ( _ ) { return false }
}
async function legacyLinkFieldPids ( { worldDir = WORLD _DIR } = { } ) {
const matches = [ ] ;
const [ identity , lock ] = await Promise . all ( [ readIdentity ( ) , readWorldInstanceLock ( worldDir ) ] ) ;
if ( await isTrustedRuntimeRecord ( identity , 5_000 ) ) matches . push ( Number ( identity . pid ) ) ;
if ( await isTrustedRuntimeRecord ( lock , 15_000 ) ) matches . push ( Number ( lock . pid ) ) ;
if ( process . platform === 'win32' ) {
return [ ... new Set ( matches . filter ( pid => pid !== process . pid ) ) ] ;
}
2026-08-01 16:06:14 +09:00
let output ;
try { output = await execFileText ( 'ps' , [ '-ax' , '-o' , 'pid=' , '-o' , 'comm=' , '-o' , 'command=' ] ) ; }
2026-08-01 22:31:04 +09:00
catch ( _ ) { return [ ... new Set ( matches . filter ( pid => pid !== process . pid ) ) ] ; }
2026-08-01 16:06:14 +09:00
for ( const line of output . split ( /\r?\n/ ) ) {
const match = line . match ( /^\s*(\d+)\s+(\S+)\s+(.+)$/ ) ; if ( ! match ) continue ;
const pid = Number ( match [ 1 ] ) , executable = path . basename ( match [ 2 ] ) . toLowerCase ( ) , command = match [ 3 ] ;
2026-08-01 22:31:04 +09:00
if ( pid === process . pid || ! Number . isSafeInteger ( pid ) || ! [ 'node' , 'nodejs' ] . includes ( executable ) || ! /(?:^|[\s/])(?:server\.js|service-runner\.js)(?:\s|$)/ . test ( command ) ) continue ;
2026-08-01 16:06:14 +09:00
let cwd = '' ; try { cwd = await fsp . readlink ( ` /proc/ ${ pid } /cwd ` ) } catch ( _ ) { }
const candidates = [ cwd ] ; const absolute = command . match ( /(?:^|\s)(\/[^\s]*\/server\.js)(?:\s|$)/ ) ; if ( absolute ) candidates . push ( path . dirname ( absolute [ 1 ] ) ) ;
let linkField = false ;
for ( const directory of candidates . filter ( Boolean ) ) {
2026-08-01 22:31:04 +09:00
try { const pkg = JSON . parse ( await fsp . readFile ( path . join ( directory , 'package.json' ) , 'utf8' ) ) ; if ( [ 'link-field-v47-shared-world' , 'link-field-shared-world' ] . includes ( pkg ? . name ) ) { linkField = true ; break } } catch ( _ ) { }
2026-08-01 16:06:14 +09:00
}
if ( linkField ) matches . push ( pid ) ;
}
2026-08-01 22:31:04 +09:00
return [ ... new Set ( matches . filter ( pid => pid !== process . pid ) ) ] ;
2026-08-01 16:06:14 +09:00
}
async function stopLegacyLinkFieldServers ( ) {
2026-08-01 22:31:04 +09:00
const stopped = new Set ( ) , deadline = Date . now ( ) + 5_000 ; let quietSince = 0 ;
while ( Date . now ( ) < deadline ) {
const active = await legacyLinkFieldPids ( ) , pids = active . filter ( pid => ! stopped . has ( pid ) ) ;
for ( const pid of pids ) {
stopped . add ( pid ) ;
if ( ! await terminateProcess ( pid , 2_000 ) ) throw new Error ( ` Older LinkField process PID ${ pid } did not stop. ` ) ;
}
if ( active . length || pids . length ) quietSince = 0 ; else if ( ! quietSince ) quietSince = Date . now ( ) ;
if ( quietSince && Date . now ( ) - quietSince >= 1_500 ) break ;
await sleep ( pids . length ? 300 : 150 ) ;
}
const remaining = await legacyLinkFieldPids ( ) ;
if ( remaining . length ) throw new Error ( ` Older LinkField process PID ${ remaining . join ( ', ' ) } kept restarting and could not be stopped. ` ) ;
return [ ... stopped ] ;
2026-08-01 16:06:14 +09:00
}
function isProcessRunning ( pid ) {
if ( ! Number . isSafeInteger ( pid ) || pid <= 0 ) return false ;
try {
process . kill ( pid , 0 ) ;
return true ;
} catch ( error ) {
return error ? . code === 'EPERM' ;
}
}
2026-08-01 22:31:04 +09:00
async function readServiceRecord ( ) {
2026-08-01 16:06:14 +09:00
try {
2026-08-01 22:31:04 +09:00
const raw = ( await fsp . readFile ( PID _FILE , 'utf8' ) ) . trim ( ) ;
if ( /^\d+$/ . test ( raw ) ) { const pid = Number ( raw ) ; return Number . isSafeInteger ( pid ) && pid > 0 ? { pid , nonce : null , legacy : true } : null }
const value = JSON . parse ( raw ) , pid = Number ( value ? . pid ) ;
return Number . isSafeInteger ( pid ) && pid > 0 ? { pid , nonce : typeof value . nonce === 'string' ? value . nonce : null , root : value . root } : null ;
2026-08-01 16:06:14 +09:00
} catch ( error ) {
if ( error ? . code === 'ENOENT' ) return null ;
2026-08-01 22:31:04 +09:00
return null ;
2026-08-01 16:06:14 +09:00
}
}
2026-08-01 22:31:04 +09:00
async function readPid ( ) { return ( await readServiceRecord ( ) ) ? . pid || null }
async function readIdentity ( ) { try { return JSON . parse ( await fsp . readFile ( IDENTITY _FILE , 'utf8' ) ) } catch ( error ) { if ( error ? . code === 'ENOENT' ) return null ; return null } }
async function isManagedService ( pid ) {
if ( ! isProcessRunning ( pid ) ) return false ; const [ identity , record ] = await Promise . all ( [ readIdentity ( ) , readServiceRecord ( ) ] ) ;
return Boolean ( identity && record && record . pid === pid && identity . pid === pid && path . resolve ( identity . root || '' ) === ROOT && path . resolve ( record . root || ROOT ) === ROOT && ( ! record . nonce || identity . nonce === record . nonce ) && Date . now ( ) - Number ( identity . heartbeatAt || 0 ) < 5000 ) ;
}
async function terminateProcess ( pid , timeoutMs = 5000 ) {
if ( ! isProcessRunning ( pid ) ) return true ;
try { process . kill ( pid , 'SIGTERM' ) } catch ( error ) { if ( error ? . code !== 'ESRCH' ) throw error }
const started = Date . now ( ) ; while ( isProcessRunning ( pid ) && Date . now ( ) - started < timeoutMs ) await sleep ( 100 ) ;
if ( isProcessRunning ( pid ) ) { try { process . kill ( pid , 'SIGKILL' ) } catch ( error ) { if ( error ? . code !== 'ESRCH' ) throw error } }
const killedAt = Date . now ( ) ; while ( isProcessRunning ( pid ) && Date . now ( ) - killedAt < 2000 ) await sleep ( 50 ) ;
return ! isProcessRunning ( pid ) ;
}
2026-08-01 16:06:14 +09:00
async function removeStalePid ( ) {
const pid = await readPid ( ) ;
2026-08-01 22:31:04 +09:00
if ( pid && await isManagedService ( pid ) ) return pid ;
const identity = await readIdentity ( ) ;
if ( isFreshRuntimeRecord ( identity , 5_000 ) && typeof identity . nonce === 'string' && identity . nonce ) {
await fsp . writeFile ( PID _FILE , ` ${ JSON . stringify ( { pid : Number ( identity . pid ) , nonce : identity . nonce , root : ROOT } )} \n ` , { encoding : 'utf8' , mode : 0o600 } ) ;
return Number ( identity . pid ) ;
}
2026-08-01 16:06:14 +09:00
await fsp . unlink ( PID _FILE ) . catch ( error => {
if ( error ? . code !== 'ENOENT' ) throw error ;
} ) ;
2026-08-01 22:31:04 +09:00
await fsp . unlink ( IDENTITY _FILE ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
2026-08-01 16:06:14 +09:00
return null ;
}
2026-08-01 22:31:04 +09:00
async function recoverWorldLock ( ) {
await fsp . mkdir ( SERVICE _ROOT , { recursive : true , mode : 0o700 } ) ;
const stopped = new Set ( ) , deadline = Date . now ( ) + 15_000 ; let quietSince = 0 ;
while ( Date . now ( ) < deadline ) {
const candidates = new Set ( await legacyLinkFieldPids ( ) ) , identity = await readIdentity ( ) , lock = await readWorldInstanceLock ( ) ;
if ( identity && Date . now ( ) - Number ( identity . heartbeatAt || 0 ) < 5_000 && await processExecutableIsNode ( Number ( identity . pid ) ) ) candidates . add ( Number ( identity . pid ) ) ;
const lockPid = Number ( lock ? . pid ) ;
if ( Number . isSafeInteger ( lockPid ) && lockPid > 0 && isProcessRunning ( lockPid ) ) {
if ( ! await processExecutableIsNode ( lockPid ) ) throw new Error ( ` Refusing recovery: world-lock PID ${ lockPid } is not a verified Node process. ` ) ;
candidates . add ( lockPid ) ;
} else if ( lock ) { await fsp . unlink ( path . join ( WORLD _DIR , 'server.pid' ) ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) }
const active = [ ... candidates ] . filter ( pid => pid !== process . pid && isProcessRunning ( pid ) ) ;
if ( active . length ) { quietSince = 0 ; for ( const pid of active ) { if ( ! await terminateProcess ( pid , 3_000 ) ) throw new Error ( ` LinkField recovery could not stop PID ${ pid } . ` ) ; stopped . add ( pid ) } }
else if ( ! quietSince ) quietSince = Date . now ( ) ;
if ( quietSince && Date . now ( ) - quietSince >= 2_500 ) break ;
await sleep ( active . length ? 300 : 150 ) ;
}
const remaining = await readWorldInstanceLock ( ) , remainingPid = Number ( remaining ? . pid ) ;
if ( Number . isSafeInteger ( remainingPid ) && remainingPid > 0 && isProcessRunning ( remainingPid ) ) throw new Error ( ` World-lock PID ${ remainingPid } is still active after recovery. ` ) ;
await fsp . unlink ( path . join ( WORLD _DIR , 'server.pid' ) ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
await fsp . unlink ( PID _FILE ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
await fsp . unlink ( IDENTITY _FILE ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
await fsp . unlink ( path . join ( resolvePublicDir ( ) , '.linkfield-port' ) ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
console . log ( stopped . size ? ` Recovered the LinkField world lock and stopped ${ stopped . size } old process ${ stopped . size === 1 ? '' : 'es' } . ` : 'Removed a stale LinkField world lock.' ) ;
console . log ( 'Run npm start now.' ) ;
return [ ... stopped ] ;
}
async function rotateLogs ( ) {
let stat ; try { stat = await fsp . stat ( LOG _FILE ) } catch ( error ) { if ( error ? . code === 'ENOENT' ) return ; if ( error ) throw error }
if ( ! stat || stat . size < MAX _LOG _BYTES ) return ;
await fsp . unlink ( ` ${ LOG _FILE } . ${ LOG _GENERATIONS } ` ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
for ( let index = LOG _GENERATIONS - 1 ; index >= 1 ; index -- ) { try { await fsp . rename ( ` ${ LOG _FILE } . ${ index } ` , ` ${ LOG _FILE } . ${ index + 1 } ` ) } catch ( error ) { if ( error ? . code !== 'ENOENT' ) throw error } }
await fsp . rename ( LOG _FILE , ` ${ LOG _FILE } .1 ` ) ;
}
2026-08-01 16:06:14 +09:00
function isWithin ( parent , child ) {
const relative = path . relative ( parent , child ) ;
return relative === '' || ( ! relative . startsWith ( ` .. ${ path . sep } ` ) && relative !== '..' && ! path . isAbsolute ( relative ) ) ;
}
function resolvePublicDir ( ) {
const explicit = String ( process . env . LINK _FIELD _PUBLIC _DIR || '' ) . trim ( ) ;
if ( explicit ) return path . resolve ( explicit ) ;
const home = os . homedir ( ) ;
const defaultDir = path . join ( home , 'public_html' , 'link-field' ) ;
const defaultParent = path . dirname ( defaultDir ) ;
const rootParent = path . dirname ( ROOT ) ;
const rootName = path . basename ( ROOT ) . toLowerCase ( ) ;
const parentName = path . basename ( rootParent ) . toLowerCase ( ) ;
if ( path . resolve ( ROOT ) === path . resolve ( defaultDir ) ) return ROOT ;
if ( isWithin ( defaultDir , ROOT ) ) return defaultDir ;
if ( parentName === 'link-field' && /^link-field-v\d/i . test ( rootName ) ) return rootParent ;
if ( isWithin ( defaultParent , ROOT ) && /^link-field-v\d/i . test ( rootName ) ) return defaultDir ;
return defaultDir ;
}
async function copyEntry ( source , destination ) {
2026-08-01 22:31:04 +09:00
const stat = await fsp . lstat ( source ) ;
if ( stat . isSymbolicLink ( ) ) return ;
2026-08-01 16:06:14 +09:00
if ( stat . isDirectory ( ) ) {
await fsp . mkdir ( destination , { recursive : true , mode : 0o755 } ) ;
const entries = await fsp . readdir ( source , { withFileTypes : true } ) ;
for ( const entry of entries ) {
await copyEntry ( path . join ( source , entry . name ) , path . join ( destination , entry . name ) ) ;
}
return ;
}
if ( ! stat . isFile ( ) ) return ;
await fsp . mkdir ( path . dirname ( destination ) , { recursive : true , mode : 0o755 } ) ;
await fsp . copyFile ( source , destination ) ;
await fsp . chmod ( destination , 0o644 ) . catch ( ( ) => { } ) ;
}
2026-08-01 22:31:04 +09:00
function assertSafePublicDir ( publicDir ) {
const resolved = path . resolve ( publicDir ) , root = path . parse ( resolved ) . root , home = path . resolve ( os . homedir ( ) ) ;
if ( resolved === root || resolved === home || path . dirname ( resolved ) === resolved ) throw new Error ( ` Unsafe LinkField public directory: ${ resolved } ` ) ;
return resolved ;
}
async function writeApacheBootstrap ( publicDir ) {
const apacheFile = path . join ( publicDir , '.htaccess' ) ; let existing = '' ;
try { existing = await fsp . readFile ( apacheFile , 'utf8' ) } catch ( error ) { if ( error ? . code !== 'ENOENT' ) throw error }
await fsp . writeFile ( apacheFile , replaceManagedBlock ( existing , renderApacheBootstrap ( ) ) , { encoding : 'utf8' , mode : 0o644 } ) ;
}
2026-08-01 16:06:14 +09:00
async function deployPublicFiles ( publicDir = resolvePublicDir ( ) ) {
2026-08-01 22:31:04 +09:00
publicDir = assertSafePublicDir ( publicDir ) ;
2026-08-01 16:06:14 +09:00
const manifest = {
app : 'LinkField' ,
version : require ( '../build-meta' ) . APP _VERSION ,
source : ROOT ,
deployedAt : new Date ( ) . toISOString ( ) ,
2026-08-01 22:31:04 +09:00
entries : [ ... PUBLIC _ENTRIES ] ,
2026-08-01 16:06:14 +09:00
} ;
2026-08-01 22:31:04 +09:00
if ( path . resolve ( publicDir ) === ROOT ) { await writeApacheBootstrap ( publicDir ) ; await fsp . writeFile ( path . join ( publicDir , '.linkfield-deployment.json' ) , ` ${ JSON . stringify ( manifest , null , 2 ) } \n ` , { encoding : 'utf8' , mode : 0o644 } ) ; return publicDir }
const parent = path . dirname ( publicDir ) , name = path . basename ( publicDir ) , stamp = ` ${ process . pid } - ${ Date . now ( ) } ` , stage = path . join ( parent , ` . ${ name } .linkfield-stage- ${ stamp } ` ) , backup = path . join ( parent , ` . ${ name } .linkfield-old- ${ stamp } ` ) ;
await fsp . mkdir ( parent , { recursive : true , mode : 0o755 } ) ;
const artifacts = ( await fsp . readdir ( parent , { withFileTypes : true } ) ) . filter ( entry => entry . isDirectory ( ) && ( entry . name . startsWith ( ` . ${ name } .linkfield-stage- ` ) || entry . name . startsWith ( ` . ${ name } .linkfield-old- ` ) ) ) . map ( entry => path . join ( parent , entry . name ) ) . sort ( ) ;
if ( ! fs . existsSync ( publicDir ) ) { const recoverable = [ ... artifacts ] . reverse ( ) . find ( item => path . basename ( item ) . startsWith ( ` . ${ name } .linkfield-old- ` ) ) ; if ( recoverable ) { await fsp . rename ( recoverable , publicDir ) ; artifacts . splice ( artifacts . indexOf ( recoverable ) , 1 ) } }
for ( const artifact of artifacts ) await fsp . rm ( artifact , { recursive : true , force : true } ) ;
try {
if ( fs . existsSync ( publicDir ) ) await copyEntry ( publicDir , stage ) ; else await fsp . mkdir ( stage , { recursive : true , mode : 0o755 } ) ;
let previous = null ; try { previous = JSON . parse ( await fsp . readFile ( path . join ( stage , '.linkfield-deployment.json' ) , 'utf8' ) ) } catch ( _ ) { }
for ( const retired of Array . isArray ( previous ? . entries ) ? previous . entries . filter ( entry => ! PUBLIC _ENTRIES . includes ( entry ) ) : [ ] ) { const target = path . resolve ( stage , retired ) ; if ( target . startsWith ( ` ${ stage } ${ path . sep } ` ) ) await fsp . rm ( target , { recursive : true , force : true } ) }
for ( const entry of PUBLIC _ENTRIES ) { const source = path . join ( ROOT , entry ) , destination = path . join ( stage , entry ) ; await fsp . rm ( destination , { recursive : true , force : true } ) ; try { await copyEntry ( source , destination ) } catch ( error ) { if ( error ? . code !== 'ENOENT' ) throw error } }
await writeApacheBootstrap ( stage ) ;
await fsp . writeFile ( path . join ( stage , '.linkfield-deployment.json' ) , ` ${ JSON . stringify ( manifest , null , 2 ) } \n ` , { encoding : 'utf8' , mode : 0o644 } ) ;
if ( fs . existsSync ( publicDir ) ) await fsp . rename ( publicDir , backup ) ;
try { await fsp . rename ( stage , publicDir ) } catch ( error ) { if ( fs . existsSync ( backup ) && ! fs . existsSync ( publicDir ) ) await fsp . rename ( backup , publicDir ) ; throw error }
await fsp . rm ( backup , { recursive : true , force : true } ) ;
} catch ( error ) { await fsp . rm ( stage , { recursive : true , force : true } ) . catch ( ( ) => { } ) ; throw error }
2026-08-01 16:06:14 +09:00
return publicDir ;
}
async function tailLog ( lines = 20 ) {
try {
const text = await fsp . readFile ( LOG _FILE , 'utf8' ) ;
return text . trimEnd ( ) . split ( /\r?\n/ ) . slice ( - lines ) . join ( '\n' ) ;
} catch ( error ) {
return error ? . code === 'ENOENT' ? '' : ` Could not read log: ${ error . message } ` ;
}
}
2026-08-01 22:31:04 +09:00
function localStatus ( port ) {
return new Promise ( ( resolve , reject ) => { const request = http . get ( { hostname : '127.0.0.1' , port , path : '/api/cloud/status' , timeout : 1500 , headers : { connection : 'close' } } , response => { let body = '' ; response . setEncoding ( 'utf8' ) ; response . on ( 'data' , chunk => { if ( body . length < 65536 ) body += chunk } ) ; response . on ( 'end' , ( ) => { try { const value = JSON . parse ( body ) ; if ( response . statusCode === 200 && value ? . available === true && value ? . appVersion === require ( '../build-meta' ) . APP _VERSION ) resolve ( value ) ; else reject ( new Error ( ` Local health returned HTTP ${ response . statusCode } ` ) ) } catch ( error ) { reject ( error ) } } ) } ) ; request . on ( 'timeout' , ( ) => request . destroy ( new Error ( 'Local health timed out' ) ) ) ; request . on ( 'error' , reject ) } ) ;
}
async function publicStatus ( publicUrl ) {
const result = await checkPublicDeployment ( publicUrl , { appVersion : require ( '../build-meta' ) . APP _VERSION , timeoutMs : 10_000 } ) ; return result ? . api || null ;
}
function configuredPublicUrl ( ) { return String ( process . env . LINK _FIELD _PUBLIC _URL || '' ) . trim ( ) }
2026-08-01 16:06:14 +09:00
async function waitForStartup ( pid , publicDir ) {
const portFile = path . join ( publicDir , '.linkfield-port' ) ;
const started = Date . now ( ) ;
while ( Date . now ( ) - started < STARTUP _TIMEOUT _MS ) {
if ( ! isProcessRunning ( pid ) ) {
const log = await tailLog ( ) ;
throw new Error ( ` LinkField exited during startup. ${ log ? ` \n \n ${ log } ` : '' } ` ) ;
}
try {
const port = Number ( ( await fsp . readFile ( portFile , 'utf8' ) ) . trim ( ) ) ;
2026-08-01 22:31:04 +09:00
if ( Number . isSafeInteger ( port ) && port > 0 && port <= 65535 && await isManagedService ( pid ) ) { try { await localStatus ( port ) ; return port } catch ( _ ) { } }
2026-08-01 16:06:14 +09:00
} catch ( error ) {
if ( error ? . code !== 'ENOENT' ) throw error ;
}
await sleep ( POLL _INTERVAL _MS ) ;
}
const log = await tailLog ( ) ;
throw new Error ( ` LinkField did not finish startup within ${ STARTUP _TIMEOUT _MS / 1000 } seconds. ${ log ? ` \n \n ${ log } ` : '' } ` ) ;
}
async function start ( ) {
await fsp . mkdir ( SERVICE _ROOT , { recursive : true , mode : 0o700 } ) ;
const existingPid = await removeStalePid ( ) ;
2026-08-01 22:31:04 +09:00
let publicDir = null ;
2026-08-01 16:06:14 +09:00
if ( existingPid ) {
2026-08-01 22:31:04 +09:00
publicDir = await deployPublicFiles ( ) ;
2026-08-01 16:06:14 +09:00
console . log ( ` Replacing the running LinkField server (PID ${ existingPid } ) with v ${ require ( '../build-meta' ) . APP _VERSION } . ` ) ;
await stop ( { quiet : true } ) ;
}
const stoppedLegacy = await stopLegacyLinkFieldServers ( ) ;
if ( stoppedLegacy . length ) console . log ( ` Stopped ${ stoppedLegacy . length } older LinkField server process ${ stoppedLegacy . length === 1 ? '' : 'es' } . ` ) ;
2026-08-01 22:31:04 +09:00
const unresolvedLock = await readWorldInstanceLock ( ) , unresolvedPid = Number ( unresolvedLock ? . pid ) ;
if ( Number . isSafeInteger ( unresolvedPid ) && unresolvedPid > 0 && isProcessRunning ( unresolvedPid ) ) throw new Error ( ` A legacy LinkField world lock is still held by PID ${ unresolvedPid } . Run npm run recover once, then run npm start again. ` ) ;
if ( ! publicDir ) publicDir = await deployPublicFiles ( ) ;
2026-08-01 16:06:14 +09:00
await fsp . unlink ( path . join ( publicDir , '.linkfield-port' ) ) . catch ( error => { if ( error ? . code !== 'ENOENT' ) throw error } ) ;
await fsp . mkdir ( path . dirname ( LOG _FILE ) , { recursive : true , mode : 0o755 } ) ;
2026-08-01 22:31:04 +09:00
await rotateLogs ( ) ;
2026-08-01 16:06:14 +09:00
const logFd = fs . openSync ( LOG _FILE , 'a' ) ;
let child ;
2026-08-01 22:31:04 +09:00
const nonce = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
2026-08-01 16:06:14 +09:00
try {
2026-08-01 22:31:04 +09:00
child = spawn ( process . execPath , [ RUNNER _FILE ] , {
2026-08-01 16:06:14 +09:00
cwd : ROOT ,
detached : true ,
stdio : [ 'ignore' , logFd , logFd ] ,
2026-08-01 22:31:04 +09:00
env : { ... process . env , LINK _FIELD _PUBLIC _DIR : publicDir , LINK _FIELD _SERVICE _DIR : SERVICE _ROOT , LINK _FIELD _SERVICE _IDENTITY _FILE : IDENTITY _FILE , LINK _FIELD _SERVICE _NONCE : nonce , LINK _FIELD _LOG _FILE : LOG _FILE } ,
2026-08-01 16:06:14 +09:00
} ) ;
} finally {
fs . closeSync ( logFd ) ;
}
if ( ! child . pid ) throw new Error ( 'Could not start the LinkField background process.' ) ;
try {
2026-08-01 22:31:04 +09:00
await fsp . writeFile ( PID _FILE , ` ${ JSON . stringify ( { pid : child . pid , nonce , root : ROOT } )} \n ` , { encoding : 'utf8' , mode : 0o600 } ) ;
child . unref ( ) ;
2026-08-01 16:06:14 +09:00
const port = await waitForStartup ( child . pid , publicDir ) ;
2026-08-01 22:31:04 +09:00
const publicEndpoint = await publicStatus ( configuredPublicUrl ( ) ) ;
2026-08-01 16:06:14 +09:00
console . log ( 'LinkField started in the background. The command prompt is available again.' ) ;
console . log ( ` PID: ${ child . pid } ` ) ;
console . log ( ` Local port: ${ port } ` ) ;
console . log ( ` Public directory: ${ publicDir } ` ) ;
console . log ( ` Log: ${ LOG _FILE } ` ) ;
2026-08-01 22:31:04 +09:00
if ( publicEndpoint ) console . log ( ` Public health: ${ publicEndpoint } ` ) ;
console . log ( ` Check: ${ process . execPath } scripts/public-smoke-test.js https://YOUR-HOST/~333/link-field/ ` ) ;
2026-08-01 16:06:14 +09:00
} catch ( error ) {
2026-08-01 22:31:04 +09:00
const stopped = ! child ? . pid || await terminateProcess ( child . pid ) . catch ( ( ) => false ) ;
if ( ! stopped ) throw new Error ( ` ${ error . message } \n LinkField supervisor PID ${ child . pid } could not be stopped; its PID record was preserved for a safe manual stop. ` ) ;
2026-08-01 16:06:14 +09:00
await fsp . unlink ( PID _FILE ) . catch ( ( ) => { } ) ;
2026-08-01 22:31:04 +09:00
await fsp . unlink ( IDENTITY _FILE ) . catch ( ( ) => { } ) ;
await fsp . unlink ( path . join ( publicDir , '.linkfield-port' ) ) . catch ( ( ) => { } ) ;
2026-08-01 16:06:14 +09:00
throw error ;
}
}
async function stop ( { quiet = false } = { } ) {
const pid = await readPid ( ) ;
if ( ! pid || ! isProcessRunning ( pid ) ) {
await fsp . unlink ( PID _FILE ) . catch ( ( ) => { } ) ;
2026-08-01 22:31:04 +09:00
await fsp . unlink ( IDENTITY _FILE ) . catch ( ( ) => { } ) ;
await fsp . unlink ( path . join ( resolvePublicDir ( ) , '.linkfield-port' ) ) . catch ( ( ) => { } ) ;
2026-08-01 16:06:14 +09:00
if ( ! quiet ) console . log ( 'LinkField is not running.' ) ;
return false ;
}
2026-08-01 22:31:04 +09:00
if ( ! await isManagedService ( pid ) ) throw new Error ( ` Refusing to stop PID ${ pid } : it is not a verified LinkField supervisor. ` ) ;
if ( ! await terminateProcess ( pid ) ) throw new Error ( ` LinkField supervisor PID ${ pid } did not stop. ` ) ;
2026-08-01 16:06:14 +09:00
await fsp . unlink ( PID _FILE ) . catch ( ( ) => { } ) ;
2026-08-01 22:31:04 +09:00
await fsp . unlink ( IDENTITY _FILE ) . catch ( ( ) => { } ) ;
await fsp . unlink ( path . join ( resolvePublicDir ( ) , '.linkfield-port' ) ) . catch ( ( ) => { } ) ;
2026-08-01 16:06:14 +09:00
if ( ! quiet ) console . log ( ` LinkField stopped (PID ${ pid } ). ` ) ;
return true ;
}
async function status ( ) {
const pid = await readPid ( ) ;
const publicDir = resolvePublicDir ( ) ;
2026-08-01 22:31:04 +09:00
if ( ! pid || ! await isManagedService ( pid ) ) {
2026-08-01 16:06:14 +09:00
console . log ( 'LinkField is stopped.' ) ;
process . exitCode = 1 ;
return ;
}
let port = '' ;
try { port = ( await fsp . readFile ( path . join ( publicDir , '.linkfield-port' ) , 'utf8' ) ) . trim ( ) ; }
catch { }
2026-08-01 22:31:04 +09:00
let healthy = false , publicEndpoint = null ; if ( port ) try { await localStatus ( Number ( port ) ) ; publicEndpoint = await publicStatus ( configuredPublicUrl ( ) ) ; healthy = true } catch ( _ ) { }
console . log ( ` LinkField is ${ healthy ? 'healthy' : 'unhealthy' } (supervisor PID ${ pid } ${ port ? ` , port ${ port } ` : '' } ). ` ) ;
2026-08-01 16:06:14 +09:00
console . log ( ` Public directory: ${ publicDir } ` ) ;
2026-08-01 22:31:04 +09:00
console . log ( ` Log: ${ LOG _FILE } ` ) ; if ( publicEndpoint ) console . log ( ` Public health: ${ publicEndpoint } ` ) ; if ( ! healthy ) process . exitCode = 1 ;
2026-08-01 16:06:14 +09:00
}
async function foreground ( ) {
const publicDir = await deployPublicFiles ( ) ;
process . env . LINK _FIELD _PUBLIC _DIR = publicDir ;
const { main } = require ( '../server' ) ;
await main ( ) ;
console . log ( ` LinkField is running in the foreground. Public directory: ${ publicDir } ` ) ;
}
2026-08-01 22:31:04 +09:00
async function assertServiceStopped ( ) { const pid = await readPid ( ) ; if ( pid && await isManagedService ( pid ) ) throw new Error ( 'Stop LinkField before running an offline backup or restore.' ) ; try { const raw = ( await fsp . readFile ( path . join ( WORLD _DIR , 'server.pid' ) , 'utf8' ) ) . trim ( ) , lock = /^\d+$/ . test ( raw ) ? { pid : Number ( raw ) } : JSON . parse ( raw ) ; if ( isProcessRunning ( Number ( lock ? . pid ) ) ) throw new Error ( 'A LinkField world process is still active. Stop it before backup or restore.' ) } catch ( error ) { if ( error ? . code !== 'ENOENT' && ! /Unexpected token|Unexpected end/ . test ( error ? . message || '' ) ) throw error } }
async function backup ( ) { await assertServiceStopped ( ) ; const result = await createWorldBackup ( WORLD _DIR , BACKUP _DIR , { retain : 7 , appVersion : require ( '../build-meta' ) . APP _VERSION } ) ; console . log ( ` LinkField backup created: ${ result . destination } ` ) ; return result }
async function listBackups ( ) { await fsp . mkdir ( BACKUP _DIR , { recursive : true , mode : 0o700 } ) ; const names = ( await fsp . readdir ( BACKUP _DIR , { withFileTypes : true } ) ) . filter ( entry => entry . isDirectory ( ) && /^\d{4}-/ . test ( entry . name ) ) . map ( entry => entry . name ) . sort ( ) . reverse ( ) ; for ( const name of names ) { try { const result = await verifyWorldBackup ( BACKUP _DIR , name ) ; console . log ( ` ${ name } revision= ${ result . manifest . worldRevision } files= ${ result . manifest . fileCount } ` ) } catch ( error ) { console . log ( ` ${ name } INVALID ${ error . message } ` ) } } return names }
async function restore ( name ) { await assertServiceStopped ( ) ; if ( ! name ) throw new Error ( 'Specify a backup name from npm run backups.' ) ; const result = await restoreWorldBackup ( WORLD _DIR , BACKUP _DIR , name ) ; console . log ( ` LinkField restored backup ${ result . restored } . Previous data remains at ${ result . previous } ` ) ; return result }
2026-08-01 16:06:14 +09:00
async function main ( ) {
const command = String ( process . argv [ 2 ] || 'start' ) . toLowerCase ( ) ;
if ( command === 'start' ) return start ( ) ;
if ( command === 'stop' ) return stop ( ) ;
if ( command === 'restart' ) { await stop ( { quiet : true } ) ; return start ( ) ; }
if ( command === 'status' ) return status ( ) ;
if ( command === 'foreground' ) return foreground ( ) ;
2026-08-01 22:31:04 +09:00
if ( command === 'backup' ) return backup ( ) ;
if ( command === 'backups' ) return listBackups ( ) ;
if ( command === 'restore' ) return restore ( process . argv [ 3 ] ) ;
if ( command === 'recover' ) return recoverWorldLock ( ) ;
2026-08-01 16:06:14 +09:00
if ( command === 'deploy' ) {
const publicDir = await deployPublicFiles ( ) ;
console . log ( ` LinkField public files deployed to ${ publicDir } ` ) ;
return ;
}
throw new Error ( ` Unknown service command: ${ command } ` ) ;
}
2026-08-01 22:31:04 +09:00
module . exports = Object . freeze ( { ROOT , SERVICE _ROOT , PID _FILE , IDENTITY _FILE , LOG _FILE , WORLD _DIR , BACKUP _DIR , PUBLIC _ENTRIES , isProcessRunning , legacyLinkFieldPids , recoverWorldLock , resolvePublicDir , deployPublicFiles , start , stop , status , backup , listBackups , restore } ) ;
2026-08-01 16:06:14 +09:00
if ( require . main === module ) main ( ) . catch ( error => {
console . error ( ` LinkField service command failed: ${ error . message } ` ) ;
process . exitCode = 1 ;
} ) ;