|
|
|
@@ -17,7 +17,8 @@
|
|
|
|
|
* 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
|
|
|
|
|
* 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
|
|
|
|
|
* 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 { 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;
|
|
|
|
|
/** 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. */
|
|
|
|
@@ -53,64 +122,13 @@ function createSplListener(opts) {
|
|
|
|
|
let stallTimer = null;
|
|
|
|
|
let startedAt = 0;
|
|
|
|
|
let meta = null;
|
|
|
|
|
let building = false;
|
|
|
|
|
let buildFailed = false;
|
|
|
|
|
let lastFrameWall = 0;
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
/** '' when the tool is ready, otherwise what to say about it. */
|
|
|
|
|
function ensureBinary() {
|
|
|
|
|
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 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;
|
|
|
|
|
return ensureCaptureBinary({ moduleDir, log, onReady() {
|
|
|
|
|
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) {
|
|
|
|
@@ -135,9 +153,13 @@ function createSplListener(opts) {
|
|
|
|
|
const missing = ensureBinary();
|
|
|
|
|
if (missing) {
|
|
|
|
|
onUpdate({ error: missing });
|
|
|
|
|
// A build in flight calls start() itself when it lands; anything else
|
|
|
|
|
// is config-shaped and should back off hard rather than spin.
|
|
|
|
|
if (!building) scheduleRestart(BAD_CONFIG_RETRY_MS);
|
|
|
|
|
// onReady is only the FAST path (a successful build restarts us at
|
|
|
|
|
// once). The slow retry must stay armed regardless: a failed build
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -236,4 +258,4 @@ function createSplListener(opts) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { createSplListener };
|
|
|
|
|
module.exports = { createSplListener, ensureCaptureBinary };
|
|
|
|
|