Files
MultiRoombaRover/server/src/services/serverControlService/index.js
T
2026-09-14 19:09:02 -04:00

64 lines
2.4 KiB
JavaScript

// server Control Service
// Purpose: Lets a lockdown administrator restart the Node application without rebooting or controlling the host.
// Scope: Authorizes and announces one restart request; existing SIGTERM hooks own service and child-process cleanup.
const io = require('../../globals/io');
const logger = require('../../globals/logger').child('serverControlService');
const { getConfigurationDatabase } = require('../../configuration');
const { requireRecentPassword } = require('../adminConfigurationService');
const database = getConfigurationDatabase();
let restartPending = false;
function actorFor(socket) {
return socket?.data?.user?.username || socket.id;
}
function scheduleApplicationRestart() {
if (restartPending) throw new Error('Application restart already pending.');
restartPending = true;
/*
Socket acknowledgements are asynchronous network writes. This short delay
lets the response and restarting notification leave before SIGTERM invokes
the cleanup hooks already owned by the server's long-running services.
*/
setTimeout(() => {
try {
process.kill(process.pid, 'SIGTERM');
} catch (error) {
// If signaling fails, this process is still usable and must allow the
// administrator to try again instead of remaining permanently pending.
restartPending = false;
logger.error('Application restart signal failed', error.message);
}
}, 250);
}
function requestApplicationRestart({ actor, reason = 'administrator-requested' }) {
if (restartPending) throw new Error('Application restart already pending.');
database.recordAuditEvent(actor, 'application.restart-requested', { reason });
scheduleApplicationRestart();
logger.warn('Application restart requested', { actor, reason });
// Restore and ordinary admin restarts share this one browser contract, so
// clients can explain the disconnect without knowing which control invoked it.
io.emit('server:restarting', { reason });
}
io.on('connection', (socket) => {
socket.on('server:restartApplication', (_payload = {}, cb = () => {}) => {
try {
requireRecentPassword(socket);
const actor = actorFor(socket);
requestApplicationRestart({ actor });
cb({ success: true });
} catch (error) {
cb({ error: error.message, code: error.code || null });
}
});
});
module.exports = {
isApplicationRestartPending: () => restartPending,
requestApplicationRestart,
};