mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
109 lines
3.6 KiB
JavaScript
109 lines
3.6 KiB
JavaScript
// MediaMTX Child Supervisor
|
|
// Purpose: Writes the generated runtime configuration and owns the MediaMTX child process lifecycle.
|
|
// Scope: Starts exactly one child, forwards its logs, and lets systemd restart the coherent server/media pair.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
const yaml = require('js-yaml');
|
|
const { resolveDataPath } = require('../../helpers/dataPaths');
|
|
const { buildMediaMtxConfig } = require('./config');
|
|
|
|
function createMediaMtxSupervisor(deps) {
|
|
const {
|
|
config,
|
|
serverPort,
|
|
logger,
|
|
mediaMtxBin = process.env.MEDIAMTX_BIN || '/usr/local/bin/mediamtx',
|
|
configPath = resolveDataPath('mediamtx.yml'),
|
|
snapshotWriterPath = process.env.ROVER_SNAPSHOT_WRITER_BIN || '/usr/local/bin/rover-snapshot-writer.sh',
|
|
spawnProcess = spawn,
|
|
} = deps;
|
|
|
|
let child = null;
|
|
let stopping = false;
|
|
let stoppedCallback = null;
|
|
|
|
function forwardLines(stream, level) {
|
|
let pending = '';
|
|
stream.setEncoding('utf8');
|
|
stream.on('data', (chunk) => {
|
|
pending += chunk;
|
|
const lines = pending.split(/\r?\n/);
|
|
pending = lines.pop() || '';
|
|
lines.filter(Boolean).forEach((line) => logger[level](line));
|
|
});
|
|
stream.on('end', () => {
|
|
if (pending) logger[level](pending);
|
|
});
|
|
}
|
|
|
|
function start() {
|
|
if (child) return child;
|
|
|
|
const generatedConfig = buildMediaMtxConfig({ config, serverPort, snapshotWriterPath });
|
|
/*
|
|
Generated MediaMTX state belongs beside the server's other owned data. Using the shared
|
|
data-path helper honors SERVER_DATA_DIR as well as the normal server/data directory and
|
|
avoids introducing a systemd-created /run directory with separate permission rules.
|
|
*/
|
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
fs.writeFileSync(configPath, yaml.dump(generatedConfig, { noRefs: true, lineWidth: 120 }), { mode: 0o640 });
|
|
|
|
logger.info(`Starting MediaMTX with generated config ${configPath}`);
|
|
child = spawnProcess(mediaMtxBin, [configPath], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
forwardLines(child.stdout, 'info');
|
|
forwardLines(child.stderr, 'warn');
|
|
|
|
child.once('error', (err) => {
|
|
logger.error('Unable to start MediaMTX', err);
|
|
if (!stopping) {
|
|
/*
|
|
A spawn failure does not reliably emit the normal exit event on every platform.
|
|
Fail the parent here as well so the server can never stay nominally online without
|
|
its required media child and systemd gets the opportunity to repair the launch.
|
|
*/
|
|
process.exit(1);
|
|
}
|
|
});
|
|
child.once('exit', (code, signal) => {
|
|
child = null;
|
|
if (stopping) {
|
|
stoppedCallback?.();
|
|
stoppedCallback = null;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
MediaMTX is required for every live media path. Exiting the parent is intentionally
|
|
simpler and safer than maintaining a second retry policy inside Node: systemd already
|
|
restarts multirover.service, producing one clean server/MediaMTX lifecycle.
|
|
*/
|
|
logger.error(`MediaMTX exited unexpectedly (code=${code ?? 'none'} signal=${signal || 'none'})`);
|
|
process.exit(1);
|
|
});
|
|
return child;
|
|
}
|
|
|
|
function stop(onStopped) {
|
|
stopping = true;
|
|
stoppedCallback = typeof onStopped === 'function' ? onStopped : null;
|
|
if (!child) {
|
|
stoppedCallback?.();
|
|
stoppedCallback = null;
|
|
return;
|
|
}
|
|
try {
|
|
child.kill('SIGTERM');
|
|
} catch (err) {
|
|
logger.warn('Unable to stop MediaMTX cleanly', err);
|
|
}
|
|
}
|
|
|
|
return { start, stop };
|
|
}
|
|
|
|
module.exports = { createMediaMtxSupervisor };
|