2025-10-28 19:14:51 +01:00
// Load lightweight internal alias resolver to enable require("logger")
require ( "../js/alias-resolver" );
2026-03-07 16:34:28 +01:00
const { spawn } = require ( "node:child_process" );
const fs = require ( "node:fs" );
const path = require ( "node:path" );
const net = require ( "node:net" );
const http = require ( "node:http" );
2025-10-28 19:14:51 +01:00
const Log = require ( "logger" );
const { getConfigFilePath } = require ( "#server_functions" );
const RESTART_DELAY_MS = 500 ;
const PORT_CHECK_MAX_ATTEMPTS = 20 ;
const PORT_CHECK_INTERVAL_MS = 500 ;
let child = null ;
let restartTimer = null ;
let isShuttingDown = false ;
let isRestarting = false ;
let serverConfig = null ;
const rootDir = path . join ( __dirname , ".." );
/**
* Get the server configuration (port and address)
* @returns {{port: number, address: string}} The server config
*/
function getServerConfig () {
if ( serverConfig ) return serverConfig ;
try {
const configPath = getConfigFilePath ();
delete require . cache [ require . resolve ( configPath )];
const config = require ( configPath );
serverConfig = {
port : global . mmPort || config . port || 8080 ,
address : config . address || "localhost"
};
2026-04-02 08:56:27 +02:00
} catch {
2025-10-28 19:14:51 +01:00
serverConfig = { port : 8080 , address : "localhost" };
}
return serverConfig ;
}
/**
* Check if a port is available on the configured address
* @param {number} port The port to check
* @returns {Promise<boolean>} True if port is available
*/
function isPortAvailable ( port ) {
return new Promise (( resolve ) => {
const server = net . createServer ();
server . once ( "error" , () => {
resolve ( false );
});
server . once ( "listening" , () => {
server . close ();
resolve ( true );
});
// Use the same address as the actual server will bind to
const { address } = getServerConfig ();
server . listen ( port , address );
});
}
/**
* Wait until port is available
* @param {number} port The port to wait for
* @param {number} maxAttempts Maximum number of attempts
* @returns {Promise<void>}
*/
async function waitForPort ( port , maxAttempts = PORT_CHECK_MAX_ATTEMPTS ) {
for ( let i = 0 ; i < maxAttempts ; i ++ ) {
if ( await isPortAvailable ( port )) {
Log . info ( `Port ${ port } is now available` );
return ;
}
await new Promise (( resolve ) => setTimeout ( resolve , PORT_CHECK_INTERVAL_MS ));
}
Log . warn ( `Port ${ port } still not available after ${ maxAttempts } attempts` );
}
/**
* Start the server process
*/
function startServer () {
// Start node directly instead of via npm to avoid process tree issues
child = spawn ( "node" , [ "./serveronly" ], {
stdio : "inherit" ,
cwd : path . join ( __dirname , ".." )
});
child . on ( "error" , ( error ) => {
Log . error ( "Failed to start server process:" , error . message );
child = null ;
});
child . on ( "exit" , ( code , signal ) => {
child = null ;
if ( isShuttingDown ) {
return ;
}
if ( isRestarting ) {
// Expected restart - don't log as error
isRestarting = false ;
} else {
// Unexpected exit
Log . error ( `Server exited unexpectedly with code ${ code } and signal ${ signal } ` );
}
});
}
/**
* Send reload notification to all connected clients
*/
function notifyClientsToReload () {
const { port , address } = getServerConfig ();
const options = {
hostname : address ,
port : port ,
path : "/reload" ,
method : "GET"
};
const req = http . request ( options , ( res ) => {
if ( res . statusCode === 200 ) {
Log . info ( "Reload notification sent to clients" );
}
});
req . on ( "error" , ( err ) => {
// Server might not be running yet, ignore
Log . debug ( `Could not send reload notification: ${ err . message } ` );
});
req . end ();
}
/**
* Restart the server process
* @param {string} reason The reason for the restart
*/
2026-02-25 10:55:56 +01:00
function restartServer ( reason ) {
2025-10-28 19:14:51 +01:00
if ( restartTimer ) clearTimeout ( restartTimer );
2026-02-25 10:55:56 +01:00
restartTimer = setTimeout (() => {
2025-10-28 19:14:51 +01:00
Log . info ( reason );
if ( child ) {
isRestarting = true ;
// Get the actual port being used
const { port } = getServerConfig ();
// Notify clients to reload before restart
notifyClientsToReload ();
// Set up one-time listener for the exit event
child . once ( "exit" , async () => {
// Wait until port is actually available
await waitForPort ( port );
// Reset config cache in case it changed
serverConfig = null ;
startServer ();
});
child . kill ( "SIGTERM" );
} else {
startServer ();
}
}, RESTART_DELAY_MS );
}
/**
* Watch a specific file for changes and restart the server on change
* Watches the parent directory to handle editors that use atomic writes
* @param {string} file The file path to watch
*/
function watchFile ( file ) {
try {
const fileName = path . basename ( file );
const dirName = path . dirname ( file );
const watcher = fs . watch ( dirName , ( _eventType , changedFile ) => {
// Only trigger for the specific file we're interested in
if ( changedFile !== fileName ) return ;
Log . info ( `[watchFile] Change detected in: ${ file } ` );
if ( restartTimer ) clearTimeout ( restartTimer );
restartTimer = setTimeout (() => {
Log . info ( `[watchFile] Triggering restart due to change in: ${ file } ` );
restartServer ( `File changed: ${ path . basename ( file ) } — restarting...` );
}, RESTART_DELAY_MS );
});
watcher . on ( "error" , ( error ) => {
Log . error ( `Watcher error for ${ file } :` , error . message );
});
Log . log ( `Watching file: ${ file } ` );
} catch ( error ) {
Log . error ( `Failed to watch file ${ file } :` , error . message );
}
}
startServer ();
// Setup file watching based on config
try {
const configPath = getConfigFilePath ();
delete require . cache [ require . resolve ( configPath )];
const config = require ( configPath );
let watchTargets = [];
if ( Array . isArray ( config . watchTargets ) && config . watchTargets . length > 0 ) {
watchTargets = config . watchTargets . filter (( target ) => typeof target === "string" && target . trim () !== "" );
}
if ( watchTargets . length === 0 ) {
Log . warn ( "Watch mode is enabled but no watchTargets are configured. No files will be monitored. Set the watchTargets array in your config.js to enable file watching." );
}
Log . log ( `Watch mode enabled. Watching ${ watchTargets . length } file(s)` );
// Watch each target file
for ( const target of watchTargets ) {
const targetPath = path . isAbsolute ( target )
? target
: path . join ( rootDir , target );
// Check if file exists
if ( ! fs . existsSync ( targetPath )) {
Log . warn ( `Watch target does not exist: ${ targetPath } ` );
continue ;
}
// Check if it's a file (directories are not supported)
const stats = fs . statSync ( targetPath );
if ( stats . isFile ()) {
watchFile ( targetPath );
} else {
Log . warn ( `Watch target is not a file (directories not supported): ${ targetPath } ` );
}
}
2026-04-02 08:56:27 +02:00
} catch {
2025-10-28 19:14:51 +01:00
// Config file might not exist or be invalid, use fallback targets
Log . warn ( "Could not load watchTargets from config." );
}
process . on ( "SIGINT" , () => {
isShuttingDown = true ;
if ( restartTimer ) clearTimeout ( restartTimer );
if ( child ) child . kill ( "SIGTERM" );
process . exit ( 0 );
});