converge everything into one data folder

This commit is contained in:
legop3
2026-09-13 22:10:15 -04:00
parent b199f45eb2
commit 17b1404157
13 changed files with 860 additions and 40 deletions
@@ -6,6 +6,7 @@ const EventEmitter = require('events');
const io = require('../../globals/io');
const logger = require('../../globals/logger').child('audioForwardService');
const { loadConfig } = require('../../helpers/configLoader');
const { resolveRuntimePath } = require('../../helpers/dataPaths');
const roverManager = require('../roverManager');
const turnService = require('../turnService');
const { isMuted, isVerified, verificationEvents } = require('../verificationService');
@@ -25,7 +26,13 @@ const streamSuffix =
typeof audioForwardConfig.streamSuffix === 'string' && audioForwardConfig.streamSuffix.trim()
? audioForwardConfig.streamSuffix.trim()
: '-fwd';
const runtimeDir = path.resolve(audioForwardConfig.runtimeDir || '/tmp/mrr-audio-forward');
/*
FIFOs and uploaded clips are disposable, but they are deliberately created
and managed by this application. A fixed path below SERVER_DATA_DIR keeps the
Node process from writing to an unrelated host temp directory and prevents a
configuration value from escaping the server's filesystem boundary.
*/
const runtimeDir = resolveRuntimePath('audio-forward');
const uploadsDir = path.join(runtimeDir, 'uploads');
const maxUploadBytes = Number.isFinite(audioForwardConfig.maxUploadBytes)
? Math.max(256 * 1024, Math.floor(audioForwardConfig.maxUploadBytes))
+2 -1
View File
@@ -3,12 +3,13 @@
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
const fsp = require('fs/promises');
const path = require('path');
const { resolveRoverSnapshotDir } = require('../../helpers/dataPaths');
const roverManager = require('../roverManager');
const { getRoomCameras } = require('../roomCameraService');
const { getRoomCameraState } = require('../roomCameraService');
const { getReplayHealthSnapshot } = require('../replayEngineV2');
const ROVER_SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
const ROVER_SNAPSHOT_DIR = resolveRoverSnapshotDir();
const HEALTH_INTERVAL_MS = 5000;
const ROOM_CAMERA_STALE_MS = 5000;
const ROVER_SNAPSHOT_STALE_MS = 5000;
@@ -5,7 +5,7 @@ const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const yaml = require('js-yaml');
const { resolveDataPath } = require('../../helpers/dataPaths');
const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
const { buildMediaMtxConfig } = require('./config');
function createMediaMtxSupervisor(deps) {
@@ -52,6 +52,16 @@ function createMediaMtxSupervisor(deps) {
logger.info(`Starting MediaMTX with generated config ${configPath}`);
child = spawnProcess(mediaMtxBin, [configPath], {
stdio: ['ignore', 'pipe', 'pipe'],
/*
MediaMTX passes its environment to runOnReady hooks. Supplying the
resolved value here also covers development starts where
SERVER_DATA_DIR was omitted, so the installed snapshot writer and every
Node snapshot reader still converge on the same canonical data root.
*/
env: {
...process.env,
SERVER_DATA_DIR: resolveDataDir(),
},
});
forwardLines(child.stdout, 'info');
@@ -0,0 +1,66 @@
// MediaMTX Supervisor Tests
// Purpose: Verifies that generated MediaMTX state and child hooks inherit the server's single data root.
// Scope: Uses a child-process double and a temporary directory; no listener or background process is started.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const EventEmitter = require('events');
const { PassThrough } = require('stream');
const { createMediaMtxSupervisor } = require('./supervisor');
test('passes the resolved data root to MediaMTX runOnReady hooks', () => {
const temporaryDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'multirover-mediamtx-supervisor-'));
const generatedConfigPath = path.join(temporaryDataDir, 'mediamtx.yml');
const previousDataDir = process.env.SERVER_DATA_DIR;
let invocation = null;
process.env.SERVER_DATA_DIR = temporaryDataDir;
const spawnProcess = (command, args, options) => {
const child = new EventEmitter();
child.stdout = new PassThrough();
child.stderr = new PassThrough();
child.kill = (signal) => {
child.emit('exit', 0, signal);
};
invocation = { command, args, options };
return child;
};
const logger = {
info() {},
warn() {},
error() {},
};
try {
const supervisor = createMediaMtxSupervisor({
config: { media: { additionalHosts: ['media.example.test'] } },
serverPort: 8080,
logger,
mediaMtxBin: '/test/bin/mediamtx',
snapshotWriterPath: '/test/bin/rover-snapshot-writer',
configPath: generatedConfigPath,
spawnProcess,
});
supervisor.start();
assert.equal(invocation.command, '/test/bin/mediamtx');
assert.deepEqual(invocation.args, [generatedConfigPath]);
assert.equal(invocation.options.env.SERVER_DATA_DIR, temporaryDataDir);
assert.equal(fs.existsSync(generatedConfigPath), true);
supervisor.stop();
} finally {
/*
Restore process-global state and delete only the test-owned directory so a
failed assertion cannot alter later tests or leave generated YAML behind.
*/
if (previousDataDir === undefined) delete process.env.SERVER_DATA_DIR;
else process.env.SERVER_DATA_DIR = previousDataDir;
fs.rmSync(temporaryDataDir, { recursive: true, force: true });
}
});
@@ -10,6 +10,7 @@ const { Cam } = require('onvif');
const io = require('../../globals/io');
const logger = require('../../globals/logger').child('ptzCamera');
const { loadConfig } = require('../../helpers/configLoader');
const { resolveRoverSnapshotDir } = require('../../helpers/dataPaths');
const { isFeatureEnabled } = require('../../helpers/features');
const {
shouldUseSnapshotsForNonTurnVideo,
@@ -43,7 +44,7 @@ const STOP_MOTION = Object.freeze({ pan: 0, tilt: 0, zoom: 0 });
// explicitly disables replay for the camera.
const DEFAULT_REPLAY_ENABLED = true;
const DEFAULT_PTZ_COLOR = '#387bf8';
const SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
const SNAPSHOT_DIR = resolveRoverSnapshotDir();
const SNAPSHOT_POLL_MS = 300;
const SNAPSHOT_STREAM_INTERVAL_MS = 2000;
const SPOTLIGHT_VERIFY_DELAY_MS = 1200;
@@ -1,8 +1,8 @@
// Replay Builder Pipeline
// Purpose: Assembles selected buffered segments into final replay output video with optional sidebar.
// Scope: Owns concat/probe/layout/transcode pipeline and returns replay buffer plus source usage metadata.
const os = require('os');
const path = require('path');
const { resolveRuntimePath } = require('../../helpers/dataPaths');
const { getActiveDrivers } = require('../turnService');
const { getNickname } = require('../nicknameService');
const { getRecentMessages } = require('../chatService');
@@ -124,7 +124,15 @@ function createReplayBuilder({ execFileAsync, fsp, ensureDir, renderSidebarVideo
durationMs: BUILD_DURATION_MS,
});
const resolvedTitle = sanitizeReplayTitle(title, resolveDefaultReplayTitle(requester, sources));
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'mrr-replay-v2-'));
/*
A replay build is disposable, but every intermediate concat list, pinned
segment, and ffmpeg output is created on the server's behalf. Create a
unique workspace below SERVER_DATA_DIR so the application never spills
those writes into the host-wide temporary directory.
*/
const buildRoot = resolveRuntimePath('replay-builds');
await ensureDir(buildRoot);
const tmpDir = await fsp.mkdtemp(path.join(buildRoot, 'build-'));
try {
const usedSources = [];
@@ -4,11 +4,11 @@
const { execFile } = require('child_process');
const EventEmitter = require('events');
const fsp = require('fs/promises');
const os = require('os');
const path = require('path');
const { promisify } = require('util');
const logger = require('../../globals/logger').child('roomCameraReplay');
const { resolveRuntimePath } = require('../../helpers/dataPaths');
const execFileAsync = promisify(execFile);
@@ -118,7 +118,15 @@ async function buildRoomCameraReplayVideo({ cameraId = null } = {}, { getRoomCam
});
if (!cameraEntries.length) throw new Error('No camera frames available yet');
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'rover-replay-'));
/*
Hundreds of frame images can be produced for one room-camera replay. They
are temporary, but the Node application owns them, so both the workspace
and final intermediate video stay under the configured data root until the
existing finally block removes them.
*/
const buildRoot = resolveRuntimePath('room-camera-replay-builds');
await fsp.mkdir(buildRoot, { recursive: true });
const tmpDir = await fsp.mkdtemp(path.join(buildRoot, 'build-'));
try {
const firstFramePaths = [];
for (let i = 0; i < cameraEntries.length; i += 1) {
@@ -5,8 +5,9 @@ const EventEmitter = require('events');
const fs = require('fs/promises');
const path = require('path');
const logger = require('../../globals/logger').child('roverSnapshot');
const { resolveRoverSnapshotDir } = require('../../helpers/dataPaths');
const SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
const SNAPSHOT_DIR = resolveRoverSnapshotDir();
const POLL_INTERVAL_MS = 300;
const roverState = new Map();
const events = new EventEmitter();