mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
server restart thingy yay
This commit is contained in:
@@ -342,6 +342,16 @@ function createConfigurationDatabase({ databasePath = DEFAULT_DATABASE_PATH } =
|
||||
}));
|
||||
}
|
||||
|
||||
function recordAuditEvent(actor, action, details = {}) {
|
||||
/*
|
||||
Operational admin services need the same persistent audit trail as
|
||||
configuration changes, but they must not gain access to the underlying
|
||||
statement or database handle. This narrow method retains the existing
|
||||
redacted-details contract at the database boundary.
|
||||
*/
|
||||
writeAudit(actor, action, details);
|
||||
}
|
||||
|
||||
const importConfigurationFileTransaction = db.transaction(({ config, administrators, actor, source }) => {
|
||||
// A setup upload initializes an empty installation; it is deliberately not
|
||||
// a general-purpose replacement path for a running server's configuration.
|
||||
@@ -377,6 +387,7 @@ function createConfigurationDatabase({ databasePath = DEFAULT_DATABASE_PATH } =
|
||||
countLockdownAdministrators,
|
||||
isSetupComplete,
|
||||
listAuditEvents,
|
||||
recordAuditEvent,
|
||||
importConfigurationFile,
|
||||
close: () => db.close(),
|
||||
};
|
||||
|
||||
@@ -208,4 +208,5 @@ io.on('connection', (socket) => {
|
||||
module.exports = {
|
||||
PASSWORD_CONFIRMATION_WINDOW_MS,
|
||||
requireLockdownAdministrator,
|
||||
requireRecentPassword,
|
||||
};
|
||||
|
||||
@@ -15,3 +15,16 @@ httpServer.listen(config.port, () => {
|
||||
*/
|
||||
startMediaMtx();
|
||||
});
|
||||
|
||||
function stopAcceptingConnections() {
|
||||
/*
|
||||
Child-process services already own their SIGTERM cleanup. The HTTP service
|
||||
only stops accepting new work; MediaMTX's bounded signal handler remains
|
||||
responsible for ending the Node process even if an existing socket keeps
|
||||
the close callback waiting.
|
||||
*/
|
||||
if (httpServer.listening) httpServer.close();
|
||||
}
|
||||
|
||||
process.once('SIGINT', stopAcceptingConnections);
|
||||
process.once('SIGTERM', stopAcceptingConnections);
|
||||
|
||||
@@ -1,55 +1,54 @@
|
||||
// server Control Service
|
||||
// Purpose: Defines the server Control Service module and the helpers/state used by this service unit.
|
||||
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
|
||||
const { spawn } = require('child_process');
|
||||
// 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 { isAdmin } = require('../roleService');
|
||||
const { sendAlert } = require('../alertService');
|
||||
const { getConfigurationDatabase } = require('../../configuration');
|
||||
const { requireRecentPassword } = require('../adminConfigurationService');
|
||||
|
||||
const ALERT_COLOR = '#ff5722';
|
||||
let rebootPending = false;
|
||||
const database = getConfigurationDatabase();
|
||||
let restartPending = false;
|
||||
|
||||
function scheduleSystemReboot() {
|
||||
if (rebootPending) {
|
||||
throw new Error('Server reboot already pending');
|
||||
}
|
||||
rebootPending = true;
|
||||
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(() => {
|
||||
logger.warn('Issuing system reboot command');
|
||||
try {
|
||||
const child = spawn('systemctl', ['reboot'], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
child.unref();
|
||||
} catch (err) {
|
||||
rebootPending = false;
|
||||
logger.error('Server reboot command failed', err.message);
|
||||
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);
|
||||
}
|
||||
}, 400);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('server:reboot', (_, cb = () => {}) => {
|
||||
if (!isAdmin(socket)) {
|
||||
cb({ error: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
socket.on('server:restartApplication', (_payload = {}, cb = () => {}) => {
|
||||
try {
|
||||
scheduleSystemReboot();
|
||||
const who = socket?.data?.user?.username || socket.id;
|
||||
logger.warn('Server reboot requested', { by: who });
|
||||
sendAlert({
|
||||
color: ALERT_COLOR,
|
||||
title: 'Server Reboot',
|
||||
message: `Reboot requested by ${who}`,
|
||||
});
|
||||
requireRecentPassword(socket);
|
||||
const actor = actorFor(socket);
|
||||
if (restartPending) throw new Error('Application restart already pending.');
|
||||
database.recordAuditEvent(actor, 'application.restart-requested');
|
||||
scheduleApplicationRestart();
|
||||
logger.warn('Application restart requested', { actor });
|
||||
cb({ success: true });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
// Every connected browser receives one explicit reason for the upcoming
|
||||
// disconnect instead of interpreting the brief outage as a network fault.
|
||||
io.emit('server:restarting', { reason: 'administrator-requested' });
|
||||
} catch (error) {
|
||||
cb({ error: error.message, code: error.code || null });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user