mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-18 18:40:47 -04:00
138 lines
5.9 KiB
JavaScript
138 lines
5.9 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 { requireLockdownAdministrator, requireRecentPassword } = require('../adminConfigurationService');
|
|
const lifecycleClient = require('./lifecycleClient');
|
|
const { setAdminReason } = require('../adminReasonService');
|
|
const roverManager = require('../roverManager');
|
|
const assignmentService = require('../assignmentService');
|
|
|
|
const database = getConfigurationDatabase();
|
|
let restartPending = false;
|
|
|
|
function actorFor(socket) {
|
|
return socket?.data?.user?.username || socket.id;
|
|
}
|
|
|
|
function notifyDriversOfRestart(actor) {
|
|
// All requested restart paths share this persistent explanation. Updating
|
|
// the reason does not change server mode or arrange to clear it at startup.
|
|
const message = 'Server is restarting...';
|
|
setAdminReason(message, { by: actor });
|
|
for (const [roverId, rover] of roverManager.rovers) {
|
|
// Releasing control mutates the set, so snapshot the current drivers and
|
|
// send each the existing removal notice before their assignment changes.
|
|
for (const socketId of [...rover.drivers]) {
|
|
assignmentService.forceReleaseWithNotice(roverId, socketId, {
|
|
title: message,
|
|
message,
|
|
reasonCode: 'application-restart',
|
|
actor,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 });
|
|
notifyDriversOfRestart(actor);
|
|
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:lifecycleStatus', (_payload = {}, cb = () => {}) => {
|
|
Promise.resolve()
|
|
.then(() => requireLockdownAdministrator(socket))
|
|
.then(() => lifecycleClient.getLifecycleStatus())
|
|
.then((lifecycle) => cb({ success: true, lifecycle }))
|
|
.catch((error) => cb({ error: error.message, code: error.code || null }));
|
|
});
|
|
|
|
socket.on('server:checkForUpdate', (_payload = {}, cb = () => {}) => {
|
|
Promise.resolve()
|
|
.then(() => requireRecentPassword(socket))
|
|
.then(() => lifecycleClient.checkForUpdate())
|
|
.then((lifecycle) => {
|
|
database.recordAuditEvent(actorFor(socket), 'application.update-check-requested', {});
|
|
cb({ success: true, lifecycle });
|
|
})
|
|
.catch((error) => cb({ error: error.message, code: error.code || null }));
|
|
});
|
|
|
|
socket.on('server:updateApplication', (_payload = {}, cb = () => {}) => {
|
|
Promise.resolve()
|
|
.then(() => requireRecentPassword(socket))
|
|
.then(() => lifecycleClient.updateApplication())
|
|
.then((lifecycle) => {
|
|
// Wait for controller acceptance so rejected updates do not kick users.
|
|
notifyDriversOfRestart(actorFor(socket));
|
|
database.recordAuditEvent(actorFor(socket), 'application.update-requested', {});
|
|
io.emit('server:restarting', { reason: 'application-update' });
|
|
cb({ success: true, lifecycle });
|
|
})
|
|
.catch((error) => cb({ error: error.message, code: error.code || null }));
|
|
});
|
|
|
|
socket.on('server:restartApplication', (_payload = {}, cb = () => {}) => {
|
|
Promise.resolve().then(async () => {
|
|
requireRecentPassword(socket);
|
|
if (restartPending) throw new Error('Application restart already pending.');
|
|
const actor = actorFor(socket);
|
|
try {
|
|
const lifecycle = await lifecycleClient.restartApplication();
|
|
restartPending = true;
|
|
// Container restarts bypass the process-signal path, but need the same
|
|
// persistent reason and removal notices once the controller accepts.
|
|
notifyDriversOfRestart(actor);
|
|
database.recordAuditEvent(actor, 'application.restart-requested', { reason: 'administrator-requested' });
|
|
logger.warn('Application container restart requested', { actor });
|
|
io.emit('server:restarting', { reason: 'administrator-requested' });
|
|
cb({ success: true, lifecycle });
|
|
} catch (error) {
|
|
/*
|
|
Legacy installations intentionally have no controller socket. Keep
|
|
their existing process-signal restart during migration, but never
|
|
bypass a real controller rejection such as an operation conflict.
|
|
*/
|
|
if (!['ENOENT', 'ECONNREFUSED'].includes(error.code)) throw error;
|
|
requestApplicationRestart({ actor });
|
|
cb({ success: true, legacy: true });
|
|
}
|
|
}).catch((error) => cb({ error: error.message, code: error.code || null }));
|
|
});
|
|
});
|
|
|
|
module.exports = {
|
|
isApplicationRestartPending: () => restartPending,
|
|
requestApplicationRestart,
|
|
};
|