Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4383c9e62 |
@@ -162,7 +162,14 @@ function createLtcListener({ device, channel, moduleDir, log, onUpdate }) {
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
// A spawn that fails (EACCES on a helper that lost its execute bit,
|
||||
// ENOENT) emits 'error' with no 'exit' to follow. Leaving `child` set
|
||||
// would let the eval loop report "No signal" over a dead child forever
|
||||
// and nothing would ever retry — what parked LTC on AB on 2026-10-04.
|
||||
// Null it and retry on the slow timer, the way spl-listener does.
|
||||
report({ error: TOOL_MISSING, detail: `LTC capture failed to start: ${err.message}` });
|
||||
child = null;
|
||||
scheduleRestart(BAD_CONFIG_RETRY_MS);
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "propresenter-timers",
|
||||
"name": "Timers",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"proddash": ">=1.4.0",
|
||||
"description": "Timers, Clocks, and LTC From configured Modules",
|
||||
"provides": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proddash",
|
||||
"version": "1.12.1",
|
||||
"version": "1.12.0",
|
||||
"private": true,
|
||||
"description": "Modular production dashboard for Waters Church \u2014 zero-dependency Node server + vanilla client",
|
||||
"license": "UNLICENSED",
|
||||
|
||||
@@ -1121,11 +1121,6 @@ function parseTar(buf) {
|
||||
while (off + 512 <= buf.length) {
|
||||
const h = buf.subarray(off, off + 512);
|
||||
if (h.every((b) => b === 0)) break; // end-of-archive blocks
|
||||
// The mode matters: modules ship prebuilt helpers under bin/, and writing
|
||||
// one out without its execute bits leaves spawn() with EACCES and the
|
||||
// feature silently dead. Permission bits only — setuid/sticky from an
|
||||
// archive are not wanted here.
|
||||
const mode = (parseInt(cstr(h, 100, 8).trim() || '0', 8) & 0o777) || 0o644;
|
||||
const size = parseInt(cstr(h, 124, 12).trim() || '0', 8) || 0;
|
||||
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156]);
|
||||
const prefix = cstr(h, 257, 6).startsWith('ustar') ? cstr(h, 345, 155) : '';
|
||||
@@ -1141,7 +1136,7 @@ function parseTar(buf) {
|
||||
}
|
||||
const name = pendingName || shortName;
|
||||
pendingName = null;
|
||||
if (type === '0') files.set(name, { data: Buffer.from(data), mode });
|
||||
if (type === '0') files.set(name, Buffer.from(data));
|
||||
}
|
||||
return { files, comment };
|
||||
}
|
||||
@@ -1170,9 +1165,9 @@ async function fetchArchive() {
|
||||
}
|
||||
const { files, comment } = parseTar(zlib.gunzipSync(gz));
|
||||
const stripped = new Map(); // drop the archive's single top-level folder
|
||||
for (const [p, entry] of files) {
|
||||
for (const [p, data] of files) {
|
||||
const i = p.indexOf('/');
|
||||
if (i > 0 && i < p.length - 1) stripped.set(p.slice(i + 1), entry);
|
||||
if (i > 0 && i < p.length - 1) stripped.set(p.slice(i + 1), data);
|
||||
}
|
||||
return { files: stripped, sha: /^[0-9a-f]{7,40}$/.test(comment) ? comment : '' };
|
||||
}
|
||||
@@ -1187,17 +1182,17 @@ async function refreshRemote(force = false) {
|
||||
const job = (async () => {
|
||||
try {
|
||||
const { files, sha } = await fetchArchive();
|
||||
const pkg = safeJson(files.get('package.json')?.data);
|
||||
const pkg = safeJson(files.get('package.json'));
|
||||
const modules = new Map();
|
||||
for (const [p, file] of files) {
|
||||
for (const [p, data] of files) {
|
||||
const m = p.match(/^modules\/([^/]+)\/(.+)$/);
|
||||
if (!m) continue;
|
||||
const entry = modules.get(m[1]) || { id: m[1], manifest: null, files: new Map() };
|
||||
entry.files.set(m[2], file);
|
||||
entry.files.set(m[2], data);
|
||||
modules.set(m[1], entry);
|
||||
}
|
||||
for (const [id, entry] of modules) {
|
||||
const man = safeJson(entry.files.get('module.json')?.data);
|
||||
const man = safeJson(entry.files.get('module.json'));
|
||||
if (!man || typeof man !== 'object' || (man.id && man.id !== id) || !man.client) {
|
||||
modules.delete(id);
|
||||
continue;
|
||||
@@ -1327,14 +1322,12 @@ async function installModule(id) {
|
||||
const staging = path.join(INSTALLED_MODULES_DIR, `.${id}.installing`);
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
const shipped = new Set();
|
||||
for (const [rel, { data, mode }] of remoteEntry.files) {
|
||||
for (const [rel, data] of remoteEntry.files) {
|
||||
if (!safeRelPath(rel)) continue;
|
||||
shipped.add(rel);
|
||||
const target = path.join(staging, rel);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
// The tarball's mode, or a shipped bin/ helper lands 0644 and spawns
|
||||
// EACCES (what broke LTC and SPL on the 1.10.2 rollout, 2026-10-04).
|
||||
fs.writeFileSync(target, data, { mode });
|
||||
fs.writeFileSync(target, data);
|
||||
}
|
||||
const dest = path.join(INSTALLED_MODULES_DIR, id);
|
||||
// Per-machine executables — a capture tool compiled here, or copied in
|
||||
@@ -1446,11 +1439,11 @@ async function updateShell() {
|
||||
} else {
|
||||
const staging = path.join(ROOT, '.proddash-update');
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
for (const [rel, { data, mode }] of remote.files) {
|
||||
for (const [rel, data] of remote.files) {
|
||||
if (!safeRelPath(rel) || isKeptOnUpdate(rel)) continue;
|
||||
const target = path.join(staging, rel);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, data, { mode });
|
||||
fs.writeFileSync(target, data);
|
||||
}
|
||||
const newServer = path.join(staging, 'server.js');
|
||||
if (!fs.existsSync(newServer)) {
|
||||
|
||||
+1
-22
@@ -12,12 +12,7 @@
|
||||
* 2. a shell update from an archive that carries modules/ never writes
|
||||
* them into the app folder — they show up in the catalog instead;
|
||||
* 3. a fresh install lists everything the repo offers, and installing
|
||||
* several at once lands them in the data directory and mounts them;
|
||||
* 4. a module downloaded from the catalog keeps the tarball's modes — a
|
||||
* shipped bin/ helper is still executable (0644 means spawn EACCES,
|
||||
* which is what broke LTC and SPL on the 1.10.2 rollout) — while
|
||||
* per-machine executables the download does not include are still
|
||||
* carried over from the copy being replaced.
|
||||
* several at once lands them in the data directory and mounts them.
|
||||
*
|
||||
* node tools/unbundled.test.js
|
||||
*
|
||||
@@ -75,11 +70,6 @@ function buildArchive(dir) {
|
||||
fs.copyFileSync(path.join(REPO, 'server.js'), path.join(top, 'server.js'));
|
||||
fs.writeFileSync(path.join(top, 'package.json'), JSON.stringify({ name: 'proddash', version: '9.9.9' }));
|
||||
writeModule(path.join(top, 'modules', 'demo'), 'demo', '2.0.0');
|
||||
// The repo ships this one prebuilt (the way Timers and SPL ship their
|
||||
// capture helpers): it must come out of the download still executable.
|
||||
fs.mkdirSync(path.join(top, 'modules', 'demo', 'bin'));
|
||||
fs.writeFileSync(path.join(top, 'modules', 'demo', 'bin', 'demo-capture'), '#!/bin/sh\necho capture\n');
|
||||
fs.chmodSync(path.join(top, 'modules', 'demo', 'bin', 'demo-capture'), 0o755);
|
||||
writeModule(path.join(top, 'modules', 'other'), 'other', '1.0.0');
|
||||
writeModule(path.join(top, 'modules', 'third'), 'third', '1.0.0');
|
||||
const out = path.join(dir, 'main.tar.gz');
|
||||
@@ -198,17 +188,6 @@ function buildPackagedApp(app) {
|
||||
const empty = await api('/api/admin/modules/install', { method: 'POST', body: '{}' });
|
||||
assert.equal(empty.status, 400, 'nothing to install is said');
|
||||
|
||||
// ── 4. a download keeps executable modes, and carry-over still works ──
|
||||
const upd = await api('/api/admin/modules/demo/install', { method: 'POST', body: '{}' });
|
||||
assert.equal(upd.status, 200, 'demo update: ' + JSON.stringify(upd.body));
|
||||
assert.equal(upd.body.version, '2.0.0', 'demo was updated from the repo');
|
||||
const shipped = path.join(dataDir, 'modules', 'demo', 'bin', 'demo-capture');
|
||||
assert.ok(fs.existsSync(shipped), 'the downloaded helper is there');
|
||||
assert.ok(fs.statSync(shipped).mode & 0o111, 'and the tarball\'s execute bit survived the install');
|
||||
const carried = path.join(dataDir, 'modules', 'demo', 'bin', 'demo-helper');
|
||||
assert.ok(fs.existsSync(carried), 'the per-machine executable the download lacks was carried over');
|
||||
assert.ok(fs.statSync(carried).mode & 0o111, 'still executable');
|
||||
|
||||
console.log('unbundled shell: every check passed');
|
||||
} catch (err) {
|
||||
failed = true;
|
||||
|
||||
Reference in New Issue
Block a user