Timers 1.5.0: the LTC capture tool installs itself; tiles warn in one line
A catalog install ships source only, and a catalog update deletes the module folder — a previously built ltc-capture binary with it — so every install or update left LTC dead until someone compiled the tool by hand. The build also still used the blocking spawnSync that once froze the whole server (the bug spl-meter fixed in 0.4.0), and only ran when a listener started: no tool meant no device list, and no device meant no listener. ensureCaptureBinary() (module scope in ltc-listener.js, the spl-meter 0.4.2 pattern) now builds the tool in the background as part of coming up: kicked when the LTC source starts and by the admin device picker, single-flight, built to a temp name and renamed so a killed build can't leave a truncated binary, and rebuilt behind a still-serving binary when a module update leaves the source newer. The waiting listener starts itself the moment the build lands. Tile-side warnings are now one line — 'Not configured — see Admin', 'Unreachable', 'Capture tool building…' — with the client's source-label prefix no longer repeated. The full diagnostics (host:port, errno, the capture tool's own words, build instructions) move to the admin health line and the log, where the person who can act on them is looking. Verified: build from the exact catalog-install state (source only) lands and lists 12 real devices; touching the source triggers an atomic rebuild while the old binary keeps serving; /ltc/devices kicks the build and answers with the device list; live server shows terse source messages on the tile and the detailed story in /api/admin/state health. ltc and phase selftests still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5f60cd563c
commit
8843339f11
@@ -10,13 +10,23 @@
|
|||||||
* onUpdate roughly 4×/s:
|
* onUpdate roughly 4×/s:
|
||||||
*
|
*
|
||||||
* { state: 'running'|'stopped'|'nosignal', time, fps, df } healthy
|
* { state: 'running'|'stopped'|'nosignal', time, fps, df } healthy
|
||||||
* { error: '<what is wrong and how to fix it>' } not capturing
|
* { error: '<short, for the tiles>', detail: '<for admin/log>' } not capturing
|
||||||
*
|
*
|
||||||
* The capture binary is per-machine (gitignored): if it is missing this
|
* Error strings are two-tier on purpose: `error` is what a dashboard tile
|
||||||
* tries one swiftc compile of the source next to it, and otherwise says to
|
* shows (short — "Not configured — see Admin"), `detail` is the full story
|
||||||
* build it on any same-architecture Mac and copy it over. A dead child is
|
* for the admin health line and the log.
|
||||||
* restarted with backoff — fast for a crash, slow for config-shaped
|
*
|
||||||
* failures (unknown device, ambiguous name) that never fix themselves.
|
* 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.
|
||||||
*
|
*
|
||||||
* macOS gotcha this file can't fix: microphone permission. A ProdDash
|
* macOS gotcha this file can't fix: microphone permission. A ProdDash
|
||||||
* server launched over SSH or from a bare launchd job gets SILENT ZEROS
|
* server launched over SSH or from a bare launchd job gets SILENT ZEROS
|
||||||
@@ -27,7 +37,7 @@
|
|||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { spawn, spawnSync } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const { LtcDecoder, ChannelExtractor, fmtTc } = require('./ltc');
|
const { LtcDecoder, ChannelExtractor, fmtTc } = require('./ltc');
|
||||||
|
|
||||||
const RESTART_MS = 5000;
|
const RESTART_MS = 5000;
|
||||||
@@ -41,6 +51,100 @@ const RUNNING_WINDOW_MS = 400;
|
|||||||
/** stderr line the capture prints once the device is open. */
|
/** stderr line the capture prints once the device is open. */
|
||||||
const CAPTURING_RE = /capturing "(.+)": (\d+) channels @ (\d+) Hz/;
|
const CAPTURING_RE = /capturing "(.+)": (\d+) channels @ (\d+) Hz/;
|
||||||
|
|
||||||
|
/* ── the capture tool: build it when the install didn't bring one ──────── */
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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 : {
|
||||||
|
ready: false,
|
||||||
|
error: building || !buildFailed ? TOOL_BUILDING : TOOL_MISSING,
|
||||||
|
detail: building || !buildFailed ? 'Building the LTC capture tool — ready in a moment' : BUILD_HELP,
|
||||||
|
};
|
||||||
|
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,
|
* @param {{device:string, channel:number, moduleDir:string, log:Function,
|
||||||
* onUpdate:(u:object)=>void}} opts
|
* onUpdate:(u:object)=>void}} opts
|
||||||
@@ -75,26 +179,21 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) {
|
|||||||
return 'nosignal';
|
return 'nosignal';
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBinary() {
|
|
||||||
if (fs.existsSync(bin)) return '';
|
|
||||||
const src = bin + '.swift';
|
|
||||||
log('ltc-capture binary missing — trying a one-time swiftc build');
|
|
||||||
const built = spawnSync('swiftc', ['-O', '-o', bin, src], { timeout: 120000 });
|
|
||||||
if (built.status === 0 && fs.existsSync(bin)) {
|
|
||||||
log('ltc-capture compiled');
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return 'LTC capture tool missing — build it on any Apple Silicon Mac with '
|
|
||||||
+ '`swiftc -O -o modules/propresenter-timers/ltc-capture modules/propresenter-timers/ltc-capture.swift` '
|
|
||||||
+ 'and copy it here';
|
|
||||||
}
|
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
const missing = ensureBinary();
|
const tool = ensureCaptureBinary({
|
||||||
if (missing) {
|
moduleDir,
|
||||||
report({ error: missing });
|
log,
|
||||||
scheduleRestart(BAD_CONFIG_RETRY_MS);
|
onReady() {
|
||||||
|
// The build this start() was waiting on landed — go now.
|
||||||
|
if (!stopped) { clearTimeout(restartTimer); restartTimer = null; start(); }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!tool.ready) {
|
||||||
|
report({ error: tool.error, detail: tool.detail });
|
||||||
|
// A build in flight restarts us itself when it lands; anything else is
|
||||||
|
// config-shaped and should back off hard rather than spin.
|
||||||
|
if (tool.error !== TOOL_BUILDING) scheduleRestart(BAD_CONFIG_RETRY_MS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +212,10 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) {
|
|||||||
const channels = Number(m[2]);
|
const channels = Number(m[2]);
|
||||||
const rate = Number(m[3]);
|
const rate = Number(m[3]);
|
||||||
if (channel > channels) {
|
if (channel > channels) {
|
||||||
report({ error: `"${m[1]}" has ${channels} input channels — channel ${channel} is configured. Fix it in Admin.` });
|
report({
|
||||||
|
error: 'Channel not on this device — see Admin',
|
||||||
|
detail: `"${m[1]}" has ${channels} input channels — channel ${channel} is configured. Fix it in Admin.`,
|
||||||
|
});
|
||||||
try { child.kill(); } catch { /* exiting anyway */ }
|
try { child.kill(); } catch { /* exiting anyway */ }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,17 +237,21 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (err) => {
|
child.on('error', (err) => {
|
||||||
report({ error: `LTC capture failed to start: ${err.message}` });
|
report({ error: TOOL_MISSING, detail: `LTC capture failed to start: ${err.message}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
child.on('exit', (code) => {
|
||||||
child = null;
|
child = null;
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
// The capture's own failure text (unknown device, ambiguous name,
|
// The capture's own failure text (unknown device, ambiguous name,
|
||||||
// permission hint) is the most useful thing we can show.
|
// permission hint) is the most useful DETAIL we have; the tile just
|
||||||
|
// needs to know the input is down and where to fix it.
|
||||||
const said = stderrTail.split('\n').reverse().find((l) => l.includes('ltc-capture:') && !CAPTURING_RE.test(l));
|
const said = stderrTail.split('\n').reverse().find((l) => l.includes('ltc-capture:') && !CAPTURING_RE.test(l));
|
||||||
const quickDeath = Date.now() - startedAt < 3000;
|
const quickDeath = Date.now() - startedAt < 3000;
|
||||||
report({ error: (said ? said.replace(/^.*ltc-capture:\s*/, '') : `LTC capture exited (code ${code})`) + (quickDeath ? '' : ' — restarting') });
|
report({
|
||||||
|
error: quickDeath ? 'Audio device unavailable — see Admin' : 'Audio capture restarting…',
|
||||||
|
detail: (said ? said.replace(/^.*ltc-capture:\s*/, '') : `LTC capture exited (code ${code})`) + (quickDeath ? '' : ' — restarting'),
|
||||||
|
});
|
||||||
scheduleRestart(quickDeath ? BAD_CONFIG_RETRY_MS : RESTART_MS);
|
scheduleRestart(quickDeath ? BAD_CONFIG_RETRY_MS : RESTART_MS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -188,4 +294,4 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createLtcListener };
|
module.exports = { createLtcListener, ensureCaptureBinary };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "propresenter-timers",
|
"id": "propresenter-timers",
|
||||||
"name": "Timers",
|
"name": "Timers",
|
||||||
"version": "1.4.2",
|
"version": "1.5.0",
|
||||||
"proddash": ">=1.4.0",
|
"proddash": ">=1.4.0",
|
||||||
"description": "Timers, Clocks, and LTC From configured Modules",
|
"description": "Timers, Clocks, and LTC From configured Modules",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const { createLtcListener } = require('./ltc-listener');
|
const { createLtcListener, ensureCaptureBinary } = require('./ltc-listener');
|
||||||
const { createTimerPhase } = require('./phase');
|
const { createTimerPhase } = require('./phase');
|
||||||
const manifest = require('./module.json');
|
const manifest = require('./module.json');
|
||||||
|
|
||||||
@@ -396,7 +396,16 @@ function createProPresenterSource({ config, log }) {
|
|||||||
return lastError ? 'error' : 'connecting';
|
return lastError ? 'error' : 'connecting';
|
||||||
}
|
}
|
||||||
|
|
||||||
function message() {
|
/** Short, for the tiles — a dashboard warns, the admin page explains.
|
||||||
|
The client prefixes the source label, so no "ProPresenter" here. */
|
||||||
|
function tileMessage() {
|
||||||
|
if (!configured) return 'Not configured — see Admin';
|
||||||
|
if (reachable) return `${timers.length} timer${timers.length === 1 ? '' : 's'}`;
|
||||||
|
return 'Unreachable';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The full story, for the admin health line. */
|
||||||
|
function adminMessage() {
|
||||||
if (!configured) return 'No ProPresenter host configured — set one in Admin, or switch ProPresenter timers off';
|
if (!configured) return 'No ProPresenter host configured — set one in Admin, or switch ProPresenter timers off';
|
||||||
if (reachable) return `Polling ${host}:${port}, ${timers.length} timer${timers.length === 1 ? '' : 's'}`;
|
if (reachable) return `Polling ${host}:${port}, ${timers.length} timer${timers.length === 1 ? '' : 's'}`;
|
||||||
return lastError ? `Unreachable at ${host}:${port} — ${lastError}` : `Reaching ${host}:${port}…`;
|
return lastError ? `Unreachable at ${host}:${port} — ${lastError}` : `Reaching ${host}:${port}…`;
|
||||||
@@ -404,7 +413,7 @@ function createProPresenterSource({ config, log }) {
|
|||||||
|
|
||||||
function publish() {
|
function publish() {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
emit({ status: status(), message: message(), timers, updatedAt });
|
emit({ status: status(), message: tileMessage(), timers, updatedAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollTick() {
|
async function pollTick() {
|
||||||
@@ -450,7 +459,7 @@ function createProPresenterSource({ config, log }) {
|
|||||||
clearTimeout(pollTimer);
|
clearTimeout(pollTimer);
|
||||||
},
|
},
|
||||||
health() {
|
health() {
|
||||||
return { status: status(), message: message() };
|
return { status: status(), message: adminMessage() };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -464,7 +473,9 @@ function createLtcSource({ config, log, moduleDir }) {
|
|||||||
let emit = () => {};
|
let emit = () => {};
|
||||||
let listener = null;
|
let listener = null;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
/** The listener's latest word: { state, time, fps, df, ageMs } | { error } | null while starting. */
|
/** The listener's latest word: { state, time, fps, df, ageMs } |
|
||||||
|
{ error, detail } (error = short tile text, detail = the admin/log
|
||||||
|
story) | null while starting. */
|
||||||
let word = null;
|
let word = null;
|
||||||
|
|
||||||
function rateText() {
|
function rateText() {
|
||||||
@@ -516,8 +527,16 @@ function createLtcSource({ config, log, moduleDir }) {
|
|||||||
kind: 'ltc',
|
kind: 'ltc',
|
||||||
start(e) {
|
start(e) {
|
||||||
emit = 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 });
|
||||||
if (!device) {
|
if (!device) {
|
||||||
word = { error: 'LTC timecode is on but no audio device is selected — pick one in Admin' };
|
word = {
|
||||||
|
error: 'Not configured — see Admin',
|
||||||
|
detail: 'LTC timecode is on but no audio device is selected — pick one in Admin',
|
||||||
|
};
|
||||||
publish();
|
publish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -539,7 +558,8 @@ function createLtcSource({ config, log, moduleDir }) {
|
|||||||
listener = null;
|
listener = null;
|
||||||
},
|
},
|
||||||
health() {
|
health() {
|
||||||
return { status: status(), message: message() };
|
// The admin page gets the full story; the tiles got the short one.
|
||||||
|
return { status: status(), message: (word && word.error && word.detail) || message() };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -588,7 +608,15 @@ function createModuleSource({ moduleId, name, shellPort, log }) {
|
|||||||
let silenceTimer = null;
|
let silenceTimer = null;
|
||||||
let backoff = BACKOFF_MIN_MS;
|
let backoff = BACKOFF_MIN_MS;
|
||||||
|
|
||||||
function message() {
|
/** Short, for the tiles; the lastError detail belongs to the admin line.
|
||||||
|
The client prefixes the source label, so it isn't repeated here. */
|
||||||
|
function tileMessage() {
|
||||||
|
if (state === 'ok') return `${timers.length} timer${timers.length === 1 ? '' : 's'}`;
|
||||||
|
if (state === 'connecting') return 'Connecting…';
|
||||||
|
return 'Unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminMessage() {
|
||||||
if (state === 'ok') return `${timers.length} timer${timers.length === 1 ? '' : 's'}${via ? ` (${via})` : ''}`;
|
if (state === 'ok') return `${timers.length} timer${timers.length === 1 ? '' : 's'}${via ? ` (${via})` : ''}`;
|
||||||
if (state === 'connecting') return lastError ? `reconnecting — ${lastError}` : 'connecting…';
|
if (state === 'connecting') return lastError ? `reconnecting — ${lastError}` : 'connecting…';
|
||||||
return `unreachable — ${lastError}`;
|
return `unreachable — ${lastError}`;
|
||||||
@@ -596,7 +624,7 @@ function createModuleSource({ moduleId, name, shellPort, log }) {
|
|||||||
|
|
||||||
function publish() {
|
function publish() {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
emit({ label, status: state, message: message(), timers, updatedAt });
|
emit({ label, status: state, message: tileMessage(), timers, updatedAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
function accept(raw) {
|
function accept(raw) {
|
||||||
@@ -774,7 +802,7 @@ function createModuleSource({ moduleId, name, shellPort, log }) {
|
|||||||
if (!stopped && !stream && via === 'poll') connectStream();
|
if (!stopped && !stream && via === 'poll') connectStream();
|
||||||
},
|
},
|
||||||
health() {
|
health() {
|
||||||
return { status: state, message: message() };
|
return { status: state, message: adminMessage() };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1172,7 +1200,7 @@ module.exports = {
|
|||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
routes() {
|
routes({ log }) {
|
||||||
return {
|
return {
|
||||||
'GET /state': (req, res) => {
|
'GET /state': (req, res) => {
|
||||||
sendJson(res, 200, { state: current ? current.getState() : emptyState() });
|
sendJson(res, 200, { state: current ? current.getState() : emptyState() });
|
||||||
@@ -1195,7 +1223,15 @@ module.exports = {
|
|||||||
|
|
||||||
// The admin device picker asks this machine what it can hear. The
|
// The admin device picker asks this machine what it can hear. The
|
||||||
// options shape matches the admin select's optionsRoute contract.
|
// 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.
|
||||||
'GET /ltc/devices': (req, res) => {
|
'GET /ltc/devices': (req, res) => {
|
||||||
|
const tool = ensureCaptureBinary({ moduleDir: __dirname, log });
|
||||||
|
if (!tool.ready) {
|
||||||
|
sendJson(res, 200, { options: [], error: tool.detail || tool.error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const bin = path.join(__dirname, 'ltc-capture');
|
const bin = path.join(__dirname, 'ltc-capture');
|
||||||
const child = spawn(bin, ['--list'], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 4000 });
|
const child = spawn(bin, ['--list'], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 4000 });
|
||||||
let err = '';
|
let err = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user