Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d6edc2c8 | ||
|
|
9313c9e42a |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "spl-meter",
|
"id": "spl-meter",
|
||||||
"name": "SPL Meter",
|
"name": "SPL Meter",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"proddash": ">=1.5.0",
|
"proddash": ">=1.5.0",
|
||||||
"description": "Calibrated sound level, Leq and RTA from an audio input on this machine",
|
"description": "Calibrated sound level, Leq and RTA from an audio input on this machine",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { createSplListener } = require('./spl-listener');
|
const { createSplListener, ensureCaptureBinary } = require('./spl-listener');
|
||||||
|
|
||||||
/** Reference level of a sound level calibrator. Effectively always 94 dB. */
|
/** Reference level of a sound level calibrator. Effectively always 94 dB. */
|
||||||
const CALIBRATOR_DB = 94;
|
const CALIBRATOR_DB = 94;
|
||||||
@@ -114,11 +114,19 @@ module.exports = {
|
|||||||
error: '',
|
error: '',
|
||||||
streams: new Set(),
|
streams: new Set(),
|
||||||
silentSince: null,
|
silentSince: null,
|
||||||
|
toolMissing: '',
|
||||||
listener: null,
|
listener: null,
|
||||||
lastSent: '',
|
lastSent: '',
|
||||||
pinger: null,
|
pinger: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Before anything else: the device picker shells out to this tool, so a
|
||||||
|
// machine that installed the module from the catalog (source only — the
|
||||||
|
// binary is gitignored) needs the build kicked off here. Waiting for a
|
||||||
|
// listener would never happen: no tool means no device list, and no
|
||||||
|
// device means no listener.
|
||||||
|
state.toolMissing = ensureCaptureBinary({ moduleDir, log });
|
||||||
|
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
state.error = 'Sound level measurement is off — turn it on in ProdDash settings';
|
state.error = 'Sound level measurement is off — turn it on in ProdDash settings';
|
||||||
return { stop() { teardown(); }, health: () => ({ status: 'error', message: state.error }) };
|
return { stop() { teardown(); }, health: () => ({ status: 'error', message: state.error }) };
|
||||||
@@ -177,6 +185,9 @@ module.exports = {
|
|||||||
return {
|
return {
|
||||||
stop() { teardown(); },
|
stop() { teardown(); },
|
||||||
health() {
|
health() {
|
||||||
|
if (state.toolMissing && !fsExists(path.join(moduleDir, 'spl-capture'))) {
|
||||||
|
return { status: 'error', message: state.toolMissing };
|
||||||
|
}
|
||||||
if (state.error) return { status: 'error', message: state.error };
|
if (state.error) return { status: 'error', message: state.error };
|
||||||
if (!state.frame) return { status: 'connecting', message: 'waiting for the first reading' };
|
if (!state.frame) return { status: 'connecting', message: 'waiting for the first reading' };
|
||||||
const quiet = silentMs();
|
const quiet = silentMs();
|
||||||
@@ -300,6 +311,13 @@ module.exports = {
|
|||||||
/** Device list for the admin page. Runs the capture tool's --list,
|
/** Device list for the admin page. Runs the capture tool's --list,
|
||||||
which is the only thing that knows what CoreAudio can see. */
|
which is the only thing that knows what CoreAudio can see. */
|
||||||
'GET /devices': async (req, res) => {
|
'GET /devices': async (req, res) => {
|
||||||
|
const missing = ensureCaptureBinary({ moduleDir: __dirname, log });
|
||||||
|
if (missing) {
|
||||||
|
if (state) state.toolMissing = missing;
|
||||||
|
log(`device list unavailable — ${missing}`);
|
||||||
|
json(res, 200, { options: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const devices = await listDevices();
|
const devices = await listDevices();
|
||||||
json(res, 200, {
|
json(res, 200, {
|
||||||
@@ -438,6 +456,10 @@ function listDevices() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fsExists(p) {
|
||||||
|
try { return require('fs').existsSync(p); } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
function json(res, code, body) {
|
function json(res, code, body) {
|
||||||
const text = JSON.stringify(body);
|
const text = JSON.stringify(body);
|
||||||
res.writeHead(code, {
|
res.writeHead(code, {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
* module's ltc-capture. It normally arrives already built — compiled beside
|
* module's ltc-capture. It normally arrives already built — compiled beside
|
||||||
* the app, or copied from any Mac of the same architecture. When it is
|
* the app, or copied from any Mac of the same architecture. When it is
|
||||||
* missing this will build it, but in the background and never on the event
|
* missing this will build it, but in the background and never on the event
|
||||||
* loop: see ensureBinary for what that cost the first time it was wrong.
|
* loop, and from module scope so the device picker can trigger it too — see
|
||||||
|
* ensureCaptureBinary for what both of those cost the first time round.
|
||||||
*
|
*
|
||||||
* The same macOS gotcha applies as for LTC, and is worth repeating because
|
* The same macOS gotcha applies as for LTC, and is worth repeating because
|
||||||
* it costs an afternoon every time: a ProdDash launched over SSH or from a
|
* it costs an afternoon every time: a ProdDash launched over SSH or from a
|
||||||
@@ -30,6 +31,74 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const BUILD_HELP = 'Sound level capture tool missing — build it on any Apple Silicon Mac with '
|
||||||
|
+ '`swiftc -O -o modules/spl-meter/spl-capture modules/spl-meter/spl-capture.swift` '
|
||||||
|
+ 'and copy it next to spl-capture.swift';
|
||||||
|
const BUILDING = 'Building the capture tool — this takes a minute or two';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the capture tool if it is missing, WITHOUT blocking, and shared by
|
||||||
|
* everything that needs it.
|
||||||
|
*
|
||||||
|
* This lives at module scope rather than inside a listener because of a trap
|
||||||
|
* the first version walked into: the admin page's device picker runs the tool
|
||||||
|
* with --list, but the build only ran when a listener started, and a listener
|
||||||
|
* only starts once a device has been picked. On a machine that got the module
|
||||||
|
* from the catalog — source only, the binary being gitignored — the picker
|
||||||
|
* was empty, and nothing would ever fill it.
|
||||||
|
*
|
||||||
|
* It also must never block. It was a spawnSync once, copied from the LTC
|
||||||
|
* listener where the source is a third the size and builds in seconds. Here
|
||||||
|
* it took nearly two minutes on the production Mac and held the Node event
|
||||||
|
* loop for all of it, freezing every other module, then did it again on the
|
||||||
|
* next retry.
|
||||||
|
*
|
||||||
|
* The binary should normally already be there: built beside the app, or
|
||||||
|
* copied from any Mac of the same architecture. This is the fallback.
|
||||||
|
*
|
||||||
|
* @returns {string} '' when the tool is ready, otherwise what to say.
|
||||||
|
*/
|
||||||
|
let building = false;
|
||||||
|
let buildFailed = false;
|
||||||
|
|
||||||
|
function ensureCaptureBinary({ moduleDir, log, onReady }) {
|
||||||
|
const bin = path.join(moduleDir, 'spl-capture');
|
||||||
|
if (fs.existsSync(bin)) return '';
|
||||||
|
const src = bin + '.swift';
|
||||||
|
if (!fs.existsSync(src)) return 'spl-capture.swift is missing from the module folder';
|
||||||
|
if (building) return BUILDING;
|
||||||
|
if (buildFailed) return BUILD_HELP;
|
||||||
|
|
||||||
|
building = true;
|
||||||
|
log('spl-capture binary missing — building it in the background');
|
||||||
|
const build = spawn('swiftc', ['-O', '-o', bin, src], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||||
|
let buildErr = '';
|
||||||
|
build.stderr.setEncoding('utf8');
|
||||||
|
build.stderr.on('data', (t) => { buildErr = (buildErr + t).slice(-800); });
|
||||||
|
const giveUp = setTimeout(() => {
|
||||||
|
try { build.kill('SIGKILL'); } catch { /* already gone */ }
|
||||||
|
}, 300000);
|
||||||
|
build.on('error', (err) => {
|
||||||
|
clearTimeout(giveUp);
|
||||||
|
building = false; buildFailed = true;
|
||||||
|
log(`swiftc could not be run: ${err.message}`);
|
||||||
|
});
|
||||||
|
build.on('exit', (code) => {
|
||||||
|
clearTimeout(giveUp);
|
||||||
|
building = false;
|
||||||
|
if (code === 0 && fs.existsSync(bin)) {
|
||||||
|
log('spl-capture built');
|
||||||
|
buildFailed = false;
|
||||||
|
if (typeof onReady === 'function') onReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buildFailed = true;
|
||||||
|
log(`spl-capture build failed (exit ${code})`
|
||||||
|
+ (buildErr ? `: ${buildErr.trim().split('\n').pop()}` : ''));
|
||||||
|
});
|
||||||
|
return BUILDING;
|
||||||
|
}
|
||||||
|
|
||||||
const RESTART_MS = 5000;
|
const RESTART_MS = 5000;
|
||||||
/** An immediate exit is config-shaped (bad device, channel out of range) —
|
/** An immediate exit is config-shaped (bad device, channel out of range) —
|
||||||
retrying every 5 s would only fill the log with the same sentence. */
|
retrying every 5 s would only fill the log with the same sentence. */
|
||||||
@@ -53,64 +122,13 @@ function createSplListener(opts) {
|
|||||||
let stallTimer = null;
|
let stallTimer = null;
|
||||||
let startedAt = 0;
|
let startedAt = 0;
|
||||||
let meta = null;
|
let meta = null;
|
||||||
let building = false;
|
|
||||||
let buildFailed = false;
|
|
||||||
let lastFrameWall = 0;
|
let lastFrameWall = 0;
|
||||||
|
|
||||||
const BUILD_HELP = 'Sound level capture tool missing — build it on any Apple Silicon Mac with '
|
/** '' when the tool is ready, otherwise what to say about it. */
|
||||||
+ '`swiftc -O -o modules/spl-meter/spl-capture modules/spl-meter/spl-capture.swift` '
|
|
||||||
+ 'and copy it next to spl-capture.swift';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the capture tool if it is missing, WITHOUT blocking.
|
|
||||||
*
|
|
||||||
* This used to be a spawnSync, copied from the LTC listener where the
|
|
||||||
* source is a third the size and builds in a few seconds. Here it took
|
|
||||||
* nearly two minutes on the production Mac and froze the entire ProdDash
|
|
||||||
* server for every second of it — one module's optional helper took the
|
|
||||||
* whole dashboard down, then did it again on every retry. Nothing that
|
|
||||||
* runs while a service might be on is allowed to hold the event loop.
|
|
||||||
*
|
|
||||||
* The build is a fallback for a module installed from the catalog on a
|
|
||||||
* machine with a Swift toolchain. The normal path is a binary built
|
|
||||||
* alongside the app, or copied from any same-architecture Mac.
|
|
||||||
*
|
|
||||||
* @returns {string} '' when the binary is ready, otherwise what to say.
|
|
||||||
*/
|
|
||||||
function ensureBinary() {
|
function ensureBinary() {
|
||||||
if (fs.existsSync(bin)) return '';
|
return ensureCaptureBinary({ moduleDir, log, onReady() {
|
||||||
const src = bin + '.swift';
|
if (!stopped) { clearTimeout(restartTimer); restartTimer = null; start(); }
|
||||||
if (!fs.existsSync(src)) return 'spl-capture.swift is missing from the module folder';
|
} });
|
||||||
if (building) return 'Building the capture tool — this takes a minute or two';
|
|
||||||
if (buildFailed) return BUILD_HELP;
|
|
||||||
|
|
||||||
building = true;
|
|
||||||
log('spl-capture binary missing — building it in the background');
|
|
||||||
const build = spawn('swiftc', ['-O', '-o', bin, src], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
||||||
let buildErr = '';
|
|
||||||
build.stderr.setEncoding('utf8');
|
|
||||||
build.stderr.on('data', (t) => { buildErr = (buildErr + t).slice(-800); });
|
|
||||||
const giveUp = setTimeout(() => {
|
|
||||||
try { build.kill('SIGKILL'); } catch { /* already gone */ }
|
|
||||||
}, 300000);
|
|
||||||
build.on('error', (err) => {
|
|
||||||
clearTimeout(giveUp);
|
|
||||||
building = false; buildFailed = true;
|
|
||||||
log(`swiftc could not be run: ${err.message}`);
|
|
||||||
});
|
|
||||||
build.on('exit', (code) => {
|
|
||||||
clearTimeout(giveUp);
|
|
||||||
building = false;
|
|
||||||
if (code === 0 && fs.existsSync(bin)) {
|
|
||||||
log('spl-capture built');
|
|
||||||
buildFailed = false;
|
|
||||||
if (!stopped) { clearTimeout(restartTimer); restartTimer = null; start(); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
buildFailed = true;
|
|
||||||
log(`spl-capture build failed (exit ${code})` + (buildErr ? `: ${buildErr.trim().split('\n').pop()}` : ''));
|
|
||||||
});
|
|
||||||
return 'Building the capture tool — this takes a minute or two';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleRestart(ms) {
|
function scheduleRestart(ms) {
|
||||||
@@ -135,9 +153,13 @@ function createSplListener(opts) {
|
|||||||
const missing = ensureBinary();
|
const missing = ensureBinary();
|
||||||
if (missing) {
|
if (missing) {
|
||||||
onUpdate({ error: missing });
|
onUpdate({ error: missing });
|
||||||
// A build in flight calls start() itself when it lands; anything else
|
// onReady is only the FAST path (a successful build restarts us at
|
||||||
// is config-shaped and should back off hard rather than spin.
|
// once). The slow retry must stay armed regardless: a failed build
|
||||||
if (!building) scheduleRestart(BAD_CONFIG_RETRY_MS);
|
// never calls back, and a binary can also arrive from outside — scp'd
|
||||||
|
// in on a machine whose toolchain can't build (AB's broken CLT).
|
||||||
|
// Waiting on onReady alone deadlocked exactly that way in the Timers
|
||||||
|
// module: "building…" forever, with a working binary on disk.
|
||||||
|
scheduleRestart(BAD_CONFIG_RETRY_MS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,4 +258,4 @@ function createSplListener(opts) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createSplListener };
|
module.exports = { createSplListener, ensureCaptureBinary };
|
||||||
|
|||||||
Reference in New Issue
Block a user