From 2817a444c31754c8af8ce6af94be22c9a4177b38 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Wed, 30 Sep 2026 10:11:08 -0400 Subject: [PATCH] Timers 1.7.0: the LTC capture tool ships prebuilt, for Apple silicon and Intel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool used to be compiled by the macOS app build, or by the module itself on the booth with swiftc — and a catalog install got source only, which the unbundled 1.10.0 shell made every fresh install. Under the module guide's rule it now ships built: bin/ltc-capture-macos-arm64 and bin/ltc-capture-macos-x86_64, signed with the Developer ID (hardened runtime, audio-input entitlement), from native/ltc-capture.swift via native/build.sh. Nothing is built or looked for on the machine any more: captureTool() picks the binary for this platform, and where there is none — Windows, until a Windows implementation exists — timecode reports itself off in the tile and the health line while the rest of the module runs. The runtime build, its "building…" states and the device picker's build kick are gone with it. The old top-level binary is no longer gitignored (it is no longer written). Co-Authored-By: Claude Fable 5.1 --- .gitignore | 3 - .../bin/ltc-capture-macos-arm64 | Bin 0 -> 100864 bytes .../bin/ltc-capture-macos-x86_64 | Bin 0 -> 63536 bytes modules/propresenter-timers/ltc-listener.js | 147 ++++-------------- modules/propresenter-timers/module.json | 2 +- modules/propresenter-timers/native/build.sh | 48 ++++++ .../native/helper.entitlements | 10 ++ .../{ => native}/ltc-capture.swift | 10 +- modules/propresenter-timers/server.js | 27 ++-- 9 files changed, 109 insertions(+), 138 deletions(-) create mode 100755 modules/propresenter-timers/bin/ltc-capture-macos-arm64 create mode 100755 modules/propresenter-timers/bin/ltc-capture-macos-x86_64 create mode 100755 modules/propresenter-timers/native/build.sh create mode 100644 modules/propresenter-timers/native/helper.entitlements rename modules/propresenter-timers/{ => native}/ltc-capture.swift (96%) diff --git a/.gitignore b/.gitignore index b553dd6..7acc9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ dist/ config/modules.json config/layouts/ -# compiled per-machine, source sits next to it -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/ diff --git a/modules/propresenter-timers/bin/ltc-capture-macos-arm64 b/modules/propresenter-timers/bin/ltc-capture-macos-arm64 new file mode 100755 index 0000000000000000000000000000000000000000..55b38772ca57cf60149e200ee99bd880b9768e31 GIT binary patch literal 100864 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 WKtMo1KtMo1KtMo1K;VBL0{;iBXbUy~ literal 0 HcmV?d00001 diff --git a/modules/propresenter-timers/bin/ltc-capture-macos-x86_64 b/modules/propresenter-timers/bin/ltc-capture-macos-x86_64 new file mode 100755 index 0000000000000000000000000000000000000000..a30ffd63cf745c72a78f536d97879d77aeec69e0 GIT binary patch literal 63536 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 literal 0 HcmV?d00001 diff --git a/modules/propresenter-timers/ltc-listener.js b/modules/propresenter-timers/ltc-listener.js index d98a1b8..2274aaa 100644 --- a/modules/propresenter-timers/ltc-listener.js +++ b/modules/propresenter-timers/ltc-listener.js @@ -3,8 +3,8 @@ /** * Built-in LTC listener — capture child + in-process decode. * - * Spawns the module's native capture tool (ltc-capture, compiled from - * ltc-capture.swift beside it) for the admin-configured audio device, reads + * Spawns the module's native capture tool (bin/ltc-capture-macos-, + * shipped prebuilt; source in native/) for the admin-configured audio device, reads * the interleaved s16le stream, extracts the configured channel, and decodes * SMPTE LTC with ltc.js — all inside the module server. Reports upward via * onUpdate roughly 4×/s: @@ -16,17 +16,13 @@ * shows (short — "Not configured — see Admin"), `detail` is the full story * for the admin health line and the log. * - * The capture binary is per-machine (gitignored), and a catalog install or - * update ships source only — the update even wipes a previously built - * binary. ensureCaptureBinary() therefore rebuilds it with swiftc in the - * background as part of coming up: kicked when the LTC source starts and by - * the admin device picker, never on the event loop (a blocking build once - * froze the whole server — see spl-meter's history), built to a temp name - * and renamed so a killed build can't leave a half-written "binary", and - * rebuilt when a module update leaves the source newer than the binary. - * A dead capture child is restarted with backoff — fast for a crash, slow - * for config-shaped failures (unknown device, ambiguity) that never fix - * themselves. + * The capture tool ships prebuilt inside the module, one binary per + * platform under bin/ (module guide, "Native helpers"): nothing is ever + * built on the machine running ProdDash. captureTool() picks the binary for + * this platform; when there is none, timecode reports itself off and the + * rest of the module carries on. A dead capture child is restarted with + * backoff — fast for a crash, slow for config-shaped failures (unknown + * device, ambiguity) that never fix themselves. * * macOS gotcha this file can't fix: microphone permission. A ProdDash * server launched over SSH or from a bare launchd job gets SILENT ZEROS @@ -51,107 +47,43 @@ const RUNNING_WINDOW_MS = 400; /** stderr line the capture prints once the device is open. */ const CAPTURING_RE = /capturing "(.+)": (\d+) channels @ (\d+) Hz/; -/* ── the capture tool: build it when the install didn't bring one ──────── */ +/* ── the capture tool: prebuilt, one binary per platform ───────────────── */ /** Tile-facing words (short); the detail goes to admin health and the log. */ -const TOOL_BUILDING = 'Capture tool building…'; const TOOL_MISSING = 'Capture tool unavailable — see Admin'; -const BUILD_HELP = 'LTC capture tool could not be built here — build it on any Mac of the ' - + 'same architecture with `swiftc -O -o ltc-capture ltc-capture.swift` (both files are in ' - + 'the module folder) and copy the binary next to ltc-capture.swift'; -let building = false; -let buildFailed = false; +/** What the module ships in bin/, by platform and architecture. */ +const HELPERS = { + darwin: { arm64: 'ltc-capture-macos-arm64', x64: 'ltc-capture-macos-x86_64' }, + win32: { x64: 'ltc-capture-win-x64.exe' }, +}; /** - * Make sure the capture tool exists, building it in the background when it - * does not — module scope, so the LTC source can kick it the moment it - * starts and the admin device picker can kick it before any device is - * configured (a listener-only build would deadlock: no tool → no device - * list → no device → no listener). A catalog install ships source only and - * a catalog update deletes the module folder (old binary included), so this - * IS the install step for the tool. + * The capture tool for this machine. It is never built here and never + * looked for anywhere else: the module ships it, or timecode is off on this + * platform and says so. `ready` means a usable binary at `bin`; otherwise + * `error` is the short tile text and `detail` names the platform. * - * Never blocks (a synchronous build once held the whole server's event loop - * — every module — for the duration). Builds to a temp name and renames, so - * a killed or crashed build can't leave a truncated file that looks like a - * ready binary. A binary older than the source (a module update over an - * existing folder) keeps serving while a replacement builds behind it. - * - * @returns {{ready:boolean, error:string, detail:string}} `ready` = a binary - * is usable right now; otherwise `error` is the short tile text and - * `detail` the full one. + * @returns {{ready:boolean, bin:string, error:string, detail:string}} */ -function ensureCaptureBinary({ moduleDir, log, onReady }) { - const bin = path.join(moduleDir, 'ltc-capture'); - const src = bin + '.swift'; - const ok = { ready: true, error: '', detail: '' }; - let haveBin = false; - let stale = false; - try { - haveBin = fs.existsSync(bin); - if (haveBin && fs.existsSync(src)) stale = fs.statSync(src).mtimeMs > fs.statSync(bin).mtimeMs; - } catch { /* treat as missing */ } - if (haveBin && !stale) return ok; - if (!fs.existsSync(src)) { - return haveBin ? ok : { ready: false, error: TOOL_MISSING, detail: 'ltc-capture.swift is missing from the module folder' }; - } - const notYet = haveBin ? ok : { +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 { ready: true, bin, error: '', detail: '' }; + const here = `${platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : platform} ${arch}`; + return { ready: false, - error: building || !buildFailed ? TOOL_BUILDING : TOOL_MISSING, - detail: building || !buildFailed ? 'Building the LTC capture tool — ready in a moment' : BUILD_HELP, + bin: '', + error: TOOL_MISSING, + detail: `The module ships no LTC capture tool for ${here}${name ? ` (bin/${name} is missing)` : ''} — timecode is off on this machine`, }; - if (building || buildFailed) return notYet; - - building = true; - const tmp = bin + '.building'; - log(stale ? 'ltc-capture is older than its source — rebuilding it in the background' - : 'ltc-capture binary missing — building it in the background'); - const build = spawn('swiftc', ['-O', '-o', tmp, 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); - giveUp.unref?.(); - build.on('error', (err) => { - clearTimeout(giveUp); - building = false; - buildFailed = true; - try { fs.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ } - log(`swiftc could not be run: ${err.message}`); - }); - build.on('exit', (code) => { - clearTimeout(giveUp); - building = false; - if (code === 0 && fs.existsSync(tmp)) { - try { - fs.renameSync(tmp, bin); - } catch (err) { - buildFailed = true; - log(`ltc-capture built but could not be moved into place: ${err.message}`); - return; - } - buildFailed = false; - log('ltc-capture built'); - if (typeof onReady === 'function') onReady(); - return; - } - buildFailed = true; - try { fs.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ } - log(`ltc-capture build failed (exit ${code})` + (buildErr ? `: ${buildErr.trim().split('\n').pop()}` : '')); - }); - return notYet; } - /** * @param {{device:string, channel:number, moduleDir:string, log:Function, * onUpdate:(u:object)=>void}} opts * @returns {{stop:()=>void}} */ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) { - const bin = path.join(moduleDir, 'ltc-capture'); let child = null; let stopped = false; let restartTimer = null; @@ -181,22 +113,11 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) { function start() { if (stopped) return; - const tool = ensureCaptureBinary({ - moduleDir, - log, - onReady() { - // The build this start() was waiting on landed — go now. - if (!stopped) { clearTimeout(restartTimer); restartTimer = null; start(); } - }, - }); + const tool = captureTool(moduleDir); if (!tool.ready) { + // No binary for this platform: say so and look again later — a module + // update remounts us anyway, this is only belt and braces. report({ error: tool.error, detail: tool.detail }); - // 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 production: - // "Capture tool building…" forever, with a working binary on disk. scheduleRestart(BAD_CONFIG_RETRY_MS); return; } @@ -206,7 +127,7 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) { let decoder = null; let stderrTail = ''; - child = spawn(bin, ['--device', device], { stdio: ['ignore', 'pipe', 'pipe'] }); + child = spawn(tool.bin, ['--device', device], { stdio: ['ignore', 'pipe', 'pipe'] }); child.stderr.setEncoding('utf8'); child.stderr.on('data', (text) => { @@ -298,4 +219,4 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) { }; } -module.exports = { createLtcListener, ensureCaptureBinary }; +module.exports = { createLtcListener, captureTool }; diff --git a/modules/propresenter-timers/module.json b/modules/propresenter-timers/module.json index a2b9d43..f85f541 100644 --- a/modules/propresenter-timers/module.json +++ b/modules/propresenter-timers/module.json @@ -1,7 +1,7 @@ { "id": "propresenter-timers", "name": "Timers", - "version": "1.6.0", + "version": "1.7.0", "proddash": ">=1.4.0", "description": "Timers, Clocks, and LTC From configured Modules", "provides": [ diff --git a/modules/propresenter-timers/native/build.sh b/modules/propresenter-timers/native/build.sh new file mode 100755 index 0000000..f957c96 --- /dev/null +++ b/modules/propresenter-timers/native/build.sh @@ -0,0 +1,48 @@ +#!/bin/zsh +# Build this module's native helper for every macOS architecture it ships — +# ../bin/ltc-capture-macos-arm64 and ../bin/ltc-capture-macos-x86_64 — +# from ltc-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 timecode off. +set -e -u +cd "$(dirname "$0")" + +NAME="ltc-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 diff --git a/modules/propresenter-timers/native/helper.entitlements b/modules/propresenter-timers/native/helper.entitlements new file mode 100644 index 0000000..6d11e71 --- /dev/null +++ b/modules/propresenter-timers/native/helper.entitlements @@ -0,0 +1,10 @@ + + + + + + com.apple.security.device.audio-input + + + diff --git a/modules/propresenter-timers/ltc-capture.swift b/modules/propresenter-timers/native/ltc-capture.swift similarity index 96% rename from modules/propresenter-timers/ltc-capture.swift rename to modules/propresenter-timers/native/ltc-capture.swift index ff1f2d6..824d447 100644 --- a/modules/propresenter-timers/ltc-capture.swift +++ b/modules/propresenter-timers/native/ltc-capture.swift @@ -4,12 +4,12 @@ // The module decodes LTC in-process (ltc.js) and spawns this to move // samples from an input device to a pipe. Production machines rarely have // ffmpeg/sox, and shipping a third-party binary for that one job is -// overkill — this is the whole job, natively. The module compiles it on -// first use when swiftc is available; otherwise build it once on any -// same-architecture Mac and copy the binary next to this source: +// overkill — this is the whole job, natively. 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/propresenter-timers/ltc-capture \ -// modules/propresenter-timers/ltc-capture.swift (once, ~5 s) +// modules/propresenter-timers/native/build.sh (both architectures, ~10 s) // // ltc-capture --list // every input-capable device: name, input channels, sample rate diff --git a/modules/propresenter-timers/server.js b/modules/propresenter-timers/server.js index 4235b14..8822544 100644 --- a/modules/propresenter-timers/server.js +++ b/modules/propresenter-timers/server.js @@ -81,7 +81,7 @@ const http = require('http'); const path = require('path'); const { spawn } = require('child_process'); -const { createLtcListener, ensureCaptureBinary } = require('./ltc-listener'); +const { createLtcListener, captureTool } = require('./ltc-listener'); const { createTimerPhase } = require('./phase'); const manifest = require('./module.json'); @@ -660,11 +660,8 @@ function createLtcSource({ config, log, moduleDir }) { kind: 'ltc', start(e) { emit = e; - // Kick the capture-tool build right away: a catalog install ships - // source only and a catalog update wipes any previously built binary, - // so coming up after an install/update must rebuild it without - // waiting for a device to be picked or a listener to need it. - ensureCaptureBinary({ moduleDir, log }); + // The capture tool ships prebuilt in the module; the listener picks + // it for this platform when it starts and says so when there is none. if (!device) { word = { error: 'Not configured — see Admin', @@ -1359,23 +1356,21 @@ module.exports = { }, // The admin device picker asks this machine what it can hear. The - // options shape matches the admin select's optionsRoute contract. - // Opening the picker also kicks the capture-tool build when a catalog - // install/update left only the source — without this, no tool meant - // no device list, and no device meant nothing would ever build it. + // options shape matches the admin select's optionsRoute contract. The + // capture tool ships prebuilt in the module (bin/); with none for this + // platform the list is empty and the error says so. 'GET /ltc/devices': (req, res) => { - const tool = ensureCaptureBinary({ moduleDir: __dirname, log }); + const tool = captureTool(__dirname); if (!tool.ready) { - sendJson(res, 200, { options: [], error: tool.detail || tool.error }); + sendJson(res, 200, { options: [], error: tool.detail }); return; } - const bin = path.join(__dirname, 'ltc-capture'); - const child = spawn(bin, ['--list'], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 4000 }); + const child = spawn(tool.bin, ['--list'], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 4000 }); let err = ''; child.stderr.setEncoding('utf8'); child.stderr.on('data', (t) => { err += t; }); - child.on('error', () => { - sendJson(res, 200, { options: [], error: 'LTC capture tool missing — see modules/propresenter-timers/ltc-capture.swift' }); + child.on('error', (e) => { + sendJson(res, 200, { options: [], error: `LTC capture tool could not be run: ${e.message}` }); }); child.on('exit', () => { if (res.writableEnded) return;