Compare commits

...
Author SHA1 Message Date
Waters ChurchandClaude Fable 5.1 0929af46ca SPL Meter 0.5.0: the capture tool ships prebuilt, for Apple silicon and Intel
Same move as Timers 1.7.0. The capture tool existed only where the macOS
app build had compiled it, with the module trying swiftc on the booth as a
fallback — NA has been logging "spawn spl-capture ENOENT" since its
catalog install. Under the module guide's rule it now ships built:
bin/spl-capture-macos-arm64 and bin/spl-capture-macos-x86_64, signed with
the Developer ID (hardened runtime, audio-input entitlement), from
native/spl-capture.swift via native/build.sh. captureTool() picks the
binary for this platform; where there is none — Windows, until a Windows
implementation exists — measurement reports itself off in the health line
and the device picker while the module stays mounted. The runtime build
and its "building…" state are gone, and the old top-level binary is no
longer gitignored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 10:12:42 -04:00
michael c11389473f Merge pull request 'Release builds: Intel disk images, a Windows bundle, and where to download them' (#24) from claude/release-builds into main 2026-09-30 09:58:11 -04:00
Waters ChurchandClaude Fable 5.1 c6a44e6ddc Windows build script: executable, like the Mac one
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 09:54:51 -04:00
Waters ChurchandClaude Fable 5.1 2f14b30d68 Windows bundle: no modules either, and the read-me says where they come from
The same change the Mac build got in 1.10.0: the bundle carries the shell
alone; the admin page's catalog installs modules into %APPDATA%\ProdDash.
README.txt gains the step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 09:46:51 -04:00
Waters Church b5d39c60dc Merge remote-tracking branch 'origin/main' into claude/release-builds 2026-09-30 09:46:12 -04:00
michael 407eaecc23 Merge pull request 'Shell 1.10.0: a packaged ProdDash carries no modules' (#36) from claude/unbundled-shell into main 2026-09-30 09:42:23 -04:00
Waters ChurchandClaude Fable 5.1 b5c539ba05 Release builds: Intel disk images, a Windows bundle, and where to download them
launcher/macos/build.sh takes ARCH=x86_64 for Intel Macs — the Swift target
and the embedded Node follow (nodejs.org calls that one darwin-x64). The
default stays arm64.

launcher/windows/build.sh is new: the same payload plus Node for Windows,
checksum-verified against nodejs.org, and ProdDash.bat, which keeps the
server running and starts it again on exit code 75 — the admin page's update
restart — the way the macOS launcher does. No Windows launcher app yet.

The README's Run it section now points at the Releases page and the
ProdDash-releases repo that holds the files, one line each for Apple
silicon, Intel and Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 17:53:19 -04:00
13 changed files with 268 additions and 120 deletions
+2 -3
View File
@@ -14,6 +14,5 @@ modules/propresenter-timers/ltc-capture
# the launcher's build output, and its cache of the Node runtime it embeds
launcher/macos/build/
launcher/macos/.cache/
# Compiled per-machine, source sits next to it (see spl-capture.swift).
modules/spl-meter/spl-capture
launcher/windows/build/
launcher/windows/.cache/
+26 -2
View File
@@ -30,9 +30,19 @@ and a clear degraded state, then recover on their own.
## Run it
Ready-made builds are on the
[Releases page](http://chronicles-ab.waterschurch.org:3000/michael/ProdDash/releases);
the files themselves live in
[ProdDash-releases](http://chronicles-ab.waterschurch.org:3000/michael/ProdDash-releases).
There is a disk image for Apple silicon Macs, one for Intel Macs, and a
bundle for 64-bit Windows. Every build updates itself afterwards from the
admin page, so the download only has to be recent, not current.
### macOS — the app
Open the disk image and drag **ProdDash** to Applications. That's the whole
Download the disk image for your Mac — `ProdDash-<version>-arm64.dmg` for
Apple silicon, `ProdDash-<version>-x86_64.dmg` for Intel — open it and drag
**ProdDash** to Applications. That's the whole
installation: the app carries the dashboard and its own Node runtime, so
nothing else has to be installed. It carries no modules — open **Admin →
Available modules**, tick the ones this machine needs and press **Install
@@ -45,12 +55,26 @@ a launchd job) simply doesn't have them, and an LTC input goes silently to
zeroes.
Build the disk image from a checkout with
[launcher/macos](launcher/macos)/`build.sh`:
[launcher/macos](launcher/macos)/`build.sh` (`ARCH=x86_64` in front of it
for Intel):
```bash
cd launcher/macos && ./build.sh --dmg
```
### Windows — the bundle
Download `ProdDash-<version>-win-x64.zip`, unzip it somewhere you can write
to — `C:\ProdDash`, say, not Program Files, because updates write into the
folder — and double-click **ProdDash.bat**. The window it opens is the
dashboard running: it stays open, and it starts the server again when the
admin page applies an update. Allow `node.exe` on private networks when the
firewall asks, so other screens can reach it. Settings and layouts live in
`%APPDATA%\ProdDash`, so a new bundle never loses them. To start with
Windows, put a shortcut to `ProdDash.bat` in `shell:startup`. There is no
menu-bar-style launcher for Windows yet; the bundle is built with
[launcher/windows](launcher/windows)/`build.sh`.
### Anywhere else, or from a checkout
Requires [Node.js](https://nodejs.org) 18 or newer.
+1
View File
@@ -51,6 +51,7 @@ the cycle rather than spinning forever.
```bash
./build.sh # build/ProdDash.app — the whole application
./build.sh --dmg # …and build/ProdDash-<version>-arm64.dmg
ARCH=x86_64 ./build.sh --dmg # the same for Intel Macs (…-x86_64.dmg)
./build.sh --install # …and copy it to /Applications
./build.sh --run # …and open it
./build.sh --notarize # with --dmg: notarise and staple (Developer ID)
+10 -3
View File
@@ -13,6 +13,8 @@
# ./build.sh --dev launcher only — no Node, no server payload, so
# it runs against the checkout. Seconds, not
# minutes, while working on the launcher itself.
# ARCH=x86_64 ./build.sh the same for Intel Macs (default: arm64, Apple
# silicon); the Node it embeds follows.
#
# Signing: a "Developer ID Application" certificate is used when one is
# installed (override with CODESIGN_IDENTITY). Without one the app is signed
@@ -29,8 +31,13 @@ LAUNCHER_VERSION="1.0.0"
# The Node the app ships. Bump it here; the download is checksum-verified
# against nodejs.org's own SHASUMS256.txt and cached in .cache/.
NODE_VERSION="v24.21.0"
ARCH="arm64"
ARCH="${ARCH:-arm64}" # arm64 (Apple silicon) or x86_64 (Intel)
DEPLOYMENT_TARGET="13.0"
case "$ARCH" in
arm64) NODE_ARCH="arm64" ;;
x86_64) NODE_ARCH="x64" ;; # nodejs.org's name for it
*) echo "build.sh: ARCH must be arm64 or x86_64, not $ARCH" >&2; exit 2 ;;
esac
REPO_ROOT="../.."
OUT_DIR="build"
@@ -108,7 +115,7 @@ fi
# ── the server, and the Node that runs it ────────────────────────────────
if [[ $DEV -eq 0 ]]; then
TARBALL="node-$NODE_VERSION-darwin-$ARCH.tar.gz"
TARBALL="node-$NODE_VERSION-darwin-$NODE_ARCH.tar.gz"
mkdir -p "$CACHE_DIR"
if [[ ! -f "$CACHE_DIR/$TARBALL" ]]; then
echo " fetching Node $NODE_VERSION (~50 MB, cached for next time)"
@@ -127,7 +134,7 @@ if [[ $DEV -eq 0 ]]; then
echo " embedding Node $NODE_VERSION"
mkdir -p "$CONTENTS/Resources/node/bin"
tar -xzf "$CACHE_DIR/$TARBALL" -C "$CONTENTS/Resources/node/bin" --strip-components 2 \
"node-$NODE_VERSION-darwin-$ARCH/bin/node"
"node-$NODE_VERSION-darwin-$NODE_ARCH/bin/node"
echo " embedding ProdDash $VERSION (no modules — the catalog installs them)"
mkdir -p "$PAYLOAD/config"
+121
View File
@@ -0,0 +1,121 @@
#!/bin/zsh
# Build the Windows bundle — ProdDash-<version>-win-x64.zip: the dashboard,
# its own Node runtime and a start script, so nothing has to be installed on
# the Windows machine. Unzip, double-click ProdDash.bat, done. It carries no
# modules: those come from the admin page's catalog into %APPDATA%\ProdDash.
#
# ./build.sh build/ProdDash-<version>-win-x64.zip
#
# Builds on macOS or Linux (zsh, curl, shasum, unzip, zip). Node for Windows
# comes from nodejs.org, checksum-verified against its SHASUMS256.txt and
# cached in .cache/. There is no Windows launcher app yet: ProdDash.bat keeps
# the server running and starts it again when the admin page applies an
# update (the server exits with code 75 to ask for that).
set -e -u
cd "$(dirname "$0")"
APP_NAME="ProdDash"
# The Node the bundle ships — keep in step with launcher/macos/build.sh.
NODE_VERSION="v24.21.0"
NODE_DIST="node-$NODE_VERSION-win-x64"
REPO_ROOT="../.."
OUT_DIR="build"
CACHE_DIR=".cache"
for tool in curl shasum unzip zip; do
command -v "$tool" >/dev/null || { echo "build.sh: $tool not found" >&2; exit 1; }
done
VERSION=$(sed -n 's/.*"version" *: *"\([^"]*\)".*/\1/p' "$REPO_ROOT/package.json" | head -1)
[[ -n "$VERSION" ]] || { echo "build.sh: can't read the version from package.json" >&2; exit 1; }
NAME="$APP_NAME-$VERSION-win-x64"
STAGE="$OUT_DIR/$NAME"
echo "ProdDash $VERSION — building the Windows bundle"
rm -rf "$OUT_DIR"
mkdir -p "$STAGE/node" "$STAGE/config" "$CACHE_DIR"
# ── Node for Windows ─────────────────────────────────────────────────────
ZIPFILE="$NODE_DIST.zip"
if [[ ! -f "$CACHE_DIR/$ZIPFILE" ]]; then
echo " fetching Node $NODE_VERSION for Windows (~30 MB, cached for next time)"
curl -fsSL --retry 3 -o "$CACHE_DIR/$ZIPFILE.part" "https://nodejs.org/dist/$NODE_VERSION/$ZIPFILE"
mv "$CACHE_DIR/$ZIPFILE.part" "$CACHE_DIR/$ZIPFILE"
fi
SUMS="$CACHE_DIR/SHASUMS256-$NODE_VERSION.txt"
[[ -f "$SUMS" ]] || curl -fsSL --retry 3 -o "$SUMS" "https://nodejs.org/dist/$NODE_VERSION/SHASUMS256.txt"
# This binary ships under ProdDash's name — check it every build.
WANT=$(awk -v f="$ZIPFILE" '$2 == f {print $1}' "$SUMS")
GOT=$(shasum -a 256 "$CACHE_DIR/$ZIPFILE" | awk '{print $1}')
if [[ -z "$WANT" || "$WANT" != "$GOT" ]]; then
echo "build.sh: $ZIPFILE does not match nodejs.org's checksum — refusing to ship it" >&2
exit 1
fi
echo " embedding Node $NODE_VERSION (node.exe and its licence)"
unzip -q -j "$CACHE_DIR/$ZIPFILE" "$NODE_DIST/node.exe" "$NODE_DIST/LICENSE" -d "$STAGE/node"
# ── ProdDash ─────────────────────────────────────────────────────────────
echo " embedding ProdDash $VERSION (no modules — the catalog installs them)"
cp "$REPO_ROOT/server.js" "$REPO_ROOT/package.json" "$STAGE/"
cp -R "$REPO_ROOT/public" "$STAGE/"
# Only the checked-in defaults: config/ also holds this machine's own
# modules.json and layouts when ProdDash has ever run from the checkout.
cp "$REPO_ROOT/config/proddash.json" "$STAGE/config/"
find "$STAGE" -name '.DS_Store' -delete
# ── the start script and the read-me, with Windows line endings ──────────
crlf() { awk '{ printf "%s\r\n", $0 }' > "$1"; }
crlf "$STAGE/ProdDash.bat" <<'BAT'
@echo off
rem ProdDash - double-click to start. Keep this window open: closing it stops the dashboard.
title ProdDash
cd /d "%~dp0"
rem The server exits with code 75 after applying an update from the admin page; start it again.
set PRODDASH_LAUNCHER=1
:run
"%~dp0node\node.exe" server.js
set CODE=%ERRORLEVEL%
if %CODE% EQU 75 goto run
echo.
echo ProdDash stopped (exit code %CODE%). Press any key to close this window.
pause >nul
BAT
crlf "$STAGE/README.txt" <<README
ProdDash $VERSION for Windows (64-bit)
=======================================
Nothing to install. This folder holds the dashboard and its own copy of Node.
1. Move this folder somewhere you can write to, for example C:\ProdDash
(not Program Files: updates write into this folder).
2. Double-click ProdDash.bat. A window opens and stays open while the
dashboard runs; closing it stops the dashboard.
- If Windows warns about the file, choose More info, then Run anyway.
- When Windows Defender Firewall asks about node.exe, allow it on
private networks so other screens can reach the dashboard.
3. Open http://localhost:24500 on this PC. Other devices on the network use
http://<this PC's IP address>:24500. Settings are on the admin page,
http://localhost:24500/admin.
4. The bundle ships without modules. On the admin page, tick the ones this
PC needs under Available modules and press Install selected; each one
updates on its own from then on.
Your settings and layouts live in %APPDATA%\ProdDash, not in this folder,
so unzipping a new version or updating never loses them.
Start with Windows: press Win+R, type shell:startup, press Enter, and put a
shortcut to ProdDash.bat in the folder that opens.
Updates: the admin page's Update now fetches the new version into this
folder and restarts the dashboard; the window stays open. A copy of the
version it replaced is kept for a short while in case the new one will not
start.
README
# ── zip ──────────────────────────────────────────────────────────────────
echo " zipping"
(cd "$OUT_DIR" && zip -q -r -X "$NAME.zip" "$NAME")
echo "built $OUT_DIR/$NAME.zip ($(du -sh "$OUT_DIR/$NAME.zip" | awk '{print $1}'))"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "spl-meter",
"name": "SPL Meter",
"version": "0.4.3",
"version": "0.5.0",
"proddash": ">=1.5.0",
"description": "Calibrated sound level, Leq and RTA from an audio input on this machine",
"permissions": [
+48
View File
@@ -0,0 +1,48 @@
#!/bin/zsh
# Build this module's native helper for every macOS architecture it ships —
# ../bin/spl-capture-macos-arm64 and ../bin/spl-capture-macos-x86_64 —
# from spl-capture.swift beside this script. Run it on any Mac with the
# Xcode command line tools; both architectures cross-compile from either.
# The binaries are committed: a machine running ProdDash never builds
# anything (docs/MODULE-GUIDE.md, "Native helpers").
#
# Signed with a Developer ID Application certificate when one is installed
# (override with CODESIGN_IDENTITY) — hardened runtime plus the audio-input
# entitlement, which a helper that reads a microphone needs under the
# hardened runtime — and ad-hoc otherwise, which is fine on your own Macs.
#
# Windows is not built here: there is no Windows implementation of this
# tool yet, so on Windows the module reports measurement off.
set -e -u
cd "$(dirname "$0")"
NAME="spl-capture"
DEPLOYMENT_TARGET="13.0"
command -v swiftc >/dev/null || {
echo "build.sh: swiftc not found — install the Xcode command line tools: xcode-select --install" >&2
exit 1
}
IDENTITY="${CODESIGN_IDENTITY:-}"
if [[ -z "$IDENTITY" ]]; then
IDENTITY=$(security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/Developer ID Application/ {print $2; exit}')
fi
if [[ -n "$IDENTITY" ]]; then
echo "signing as $IDENTITY"
SIGN=(--force --sign "$IDENTITY" --options runtime --timestamp)
else
echo "signing ad-hoc (no Developer ID installed — fine on your own Macs)"
SIGN=(--force --sign - --options runtime)
fi
mkdir -p ../bin
for ARCH in arm64 x86_64; do
OUT="../bin/$NAME-macos-$ARCH"
echo " compiling $OUT"
swiftc -O -whole-module-optimization -target "$ARCH-apple-macos$DEPLOYMENT_TARGET" -o "$OUT" "$NAME.swift"
codesign "${SIGN[@]}" --entitlements helper.entitlements "$OUT"
codesign --verify --strict "$OUT"
echo " built $OUT ($(du -h "$OUT" | awk '{print $1}'))"
done
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The helper reads an audio input; under the hardened runtime that
needs saying. The permission itself comes from the launcher. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
@@ -8,10 +8,11 @@
// measurement is done here and only the answers cross the pipe.
//
// Everything below uses Accelerate, which ships with macOS. No third-party
// code, no package manager, ~2 s to build:
// code, no package manager. It ships prebuilt in the module's bin/ (one
// binary per platform, committed — a booth never builds anything). To
// rebuild after a change here, on any Mac with the Xcode command line tools:
//
// swiftc -O -o modules/spl-meter/spl-capture \
// modules/spl-meter/spl-capture.swift
// modules/spl-meter/native/build.sh (both architectures)
//
// spl-capture --list
// every input-capable device: name, input channels, sample rate
+10 -20
View File
@@ -20,7 +20,7 @@
*/
const path = require('path');
const { createSplListener, ensureCaptureBinary } = require('./spl-listener');
const { createSplListener, captureTool } = require('./spl-listener');
/** Reference level of a sound level calibrator. Effectively always 94 dB. */
const CALIBRATOR_DB = 94;
@@ -120,12 +120,9 @@ module.exports = {
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 });
// The capture tool ships prebuilt in the module (bin/); with none for
// this platform every reading is off and the health line says so.
state.toolMissing = captureTool(moduleDir).missing;
if (!config.enabled) {
state.error = 'Sound level measurement is off — turn it on in ProdDash settings';
@@ -185,9 +182,7 @@ module.exports = {
return {
stop() { teardown(); },
health() {
if (state.toolMissing && !fsExists(path.join(moduleDir, 'spl-capture'))) {
return { status: 'error', message: state.toolMissing };
}
if (state.toolMissing) return { status: 'error', message: state.toolMissing };
if (state.error) return { status: 'error', message: state.error };
if (!state.frame) return { status: 'connecting', message: 'waiting for the first reading' };
const quiet = silentMs();
@@ -311,11 +306,10 @@ module.exports = {
/** Device list for the admin page. Runs the capture tool's --list,
which is the only thing that knows what CoreAudio can see. */
'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: [] });
const tool = captureTool(__dirname);
if (tool.missing) {
log(`device list unavailable — ${tool.missing}`);
json(res, 200, { options: [], error: tool.missing });
return;
}
try {
@@ -447,7 +441,7 @@ function teardown() {
function listDevices() {
return new Promise((resolve, reject) => {
const { execFile } = require('child_process');
execFile(path.join(__dirname, 'spl-capture'), ['--list'], { timeout: 10000 },
execFile(captureTool(__dirname).bin, ['--list'], { timeout: 10000 },
(err, stdout) => {
if (err && !stdout) { reject(err); return; }
try { resolve(JSON.parse(stdout).devices || []); }
@@ -456,10 +450,6 @@ function listDevices() {
});
}
function fsExists(p) {
try { return require('fs').existsSync(p); } catch { return false; }
}
function json(res, code, body) {
const text = JSON.stringify(body);
res.writeHead(code, {
+35 -88
View File
@@ -3,7 +3,8 @@
/**
* Sound level listener — owns the native capture child.
*
* Spawns spl-capture (compiled from spl-capture.swift beside it) for the
* Spawns spl-capture (bin/spl-capture-macos-<arch>, shipped prebuilt;
* source in native/) for the
* admin-configured device and channels, reads its JSON lines, and hands the
* latest frame upward. All measurement happens in the child; this file is
* plumbing and supervision, nothing more.
@@ -13,12 +14,11 @@
* { frame: {...}, meta: {...} } measuring
* { error: '<what is wrong and how to fix it>' } not measuring
*
* The capture binary is per-machine and gitignored, exactly like the Timers
* 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, 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 capture tool ships prebuilt inside the module, one binary per
* platform under bin/ (module guide, "Native helpers"), exactly like the
* Timers module's ltc-capture: nothing is ever built on the machine running
* ProdDash. captureTool() picks the binary for this platform; when there is
* none, measurement reports itself off and the rest of the module carries on.
*
* 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
@@ -31,72 +31,31 @@ 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';
/* ── the capture tool: prebuilt, one binary per platform ───────────────── */
/** What the module ships in bin/, by platform and architecture. */
const HELPERS = {
darwin: { arm64: 'spl-capture-macos-arm64', x64: 'spl-capture-macos-x86_64' },
win32: { x64: 'spl-capture-win-x64.exe' },
};
/**
* Build the capture tool if it is missing, WITHOUT blocking, and shared by
* everything that needs it.
* The capture tool for this machine. It is never built here and never
* looked for anywhere else: the module ships it, or measurement is off on
* this platform and says so. `bin` is the usable binary; otherwise
* `missing` is what to tell the admin page and the tiles.
*
* 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.
* @returns {{bin:string, missing:string}}
*/
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;
function captureTool(moduleDir, platform = process.platform, arch = process.arch) {
const name = HELPERS[platform]?.[arch] || '';
const bin = name ? path.join(moduleDir, 'bin', name) : '';
if (bin && fs.existsSync(bin)) return { bin, missing: '' };
const here = `${platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : platform} ${arch}`;
return {
bin: '',
missing: `The module ships no sound level capture tool for ${here}${name ? ` (bin/${name} is missing)` : ''} — measurement is off on this machine`,
};
}
const RESTART_MS = 5000;
@@ -114,7 +73,6 @@ const STALL_MS = 3000;
*/
function createSplListener(opts) {
const { device, channels, weighting, bands, calibration, moduleDir, log, onUpdate } = opts;
const bin = path.join(moduleDir, 'spl-capture');
let child = null;
let stopped = false;
@@ -124,13 +82,6 @@ function createSplListener(opts) {
let meta = null;
let lastFrameWall = 0;
/** '' when the tool is ready, otherwise what to say about it. */
function ensureBinary() {
return ensureCaptureBinary({ moduleDir, log, onReady() {
if (!stopped) { clearTimeout(restartTimer); restartTimer = null; start(); }
} });
}
function scheduleRestart(ms) {
if (stopped || restartTimer) return;
restartTimer = setTimeout(() => { restartTimer = null; start(); }, ms);
@@ -150,15 +101,11 @@ function createSplListener(opts) {
function start() {
if (stopped) return;
const missing = ensureBinary();
if (missing) {
onUpdate({ error: missing });
// 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.
const tool = captureTool(moduleDir);
if (tool.missing) {
// No binary for this platform: say so and look again later — a module
// update remounts us anyway, this is only belt and braces.
onUpdate({ error: tool.missing });
scheduleRestart(BAD_CONFIG_RETRY_MS);
return;
}
@@ -175,7 +122,7 @@ function createSplListener(opts) {
let stderrTail = '';
let buf = '';
child = spawn(bin, args, { stdio: ['pipe', 'pipe', 'pipe'] });
child = spawn(tool.bin, args, { stdio: ['pipe', 'pipe', 'pipe'] });
child.stderr.setEncoding('utf8');
child.stderr.on('data', (text) => {
@@ -258,4 +205,4 @@ function createSplListener(opts) {
};
}
module.exports = { createSplListener, ensureCaptureBinary };
module.exports = { createSplListener, captureTool };