Skip to content

Commit 7ccdfdc

Browse files
authored
v1.5.23 — capture fd state on spawn failure (#89 diagnostic)
1 parent 7a79190 commit 7ccdfdc

3 files changed

Lines changed: 50 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.5.23 — 2026-07-03
4+
5+
### Diagnostics
6+
- **`spawn EBADF` instrumentation** (#89): when a child-process spawn throws (the crash a reporter hits during ffmpeg capability detection, not reproducible on any environment we can build), the shim now logs the file-descriptor state at the point of failure, which of fd 0/1/2 is invalid, the requested stdio, and the process's open fds, then re-throws unchanged. Fires only on the error path; silent in normal operation. This is to capture the root cause on the affected machine.
7+
38
## 1.5.22 — 2026-07-02
49

510
### Fixed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.22
1+
1.5.23

immich_accelerator/hooks/pg_dump_shim.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,44 @@ function rewriteSpawn(command, args) {
100100
return rewriteGzip(command, args);
101101
}
102102

103+
// Diagnostic (issue #89): a `spawn EBADF` crash in fluent-ffmpeg's capability
104+
// detection reproduces on a reporter's machine but on no environment we can
105+
// build here (matched Node version, shims, launchd stdio, concurrency, fd
106+
// limits). Since `spawn` throws synchronously, we capture the file-descriptor
107+
// state at the point of failure before re-throwing: which of fd 0/1/2 is
108+
// invalid, the requested stdio, and the process's open fds. Fires only on the
109+
// error path, so it is silent in normal operation and never alters behavior
110+
// (the original error is always re-thrown).
111+
function dumpSpawnFailure(command, options, err) {
112+
try {
113+
const fs = require('fs');
114+
const out = ['[immich-accelerator][spawn-debug] spawn threw: '
115+
+ ((err && err.code) || (err && err.message) || err)];
116+
out.push(' command=' + command);
117+
out.push(' stdio=' + JSON.stringify(options && options.stdio));
118+
for (const fd of [0, 1, 2]) {
119+
try {
120+
fs.fstatSync(fd);
121+
out.push(' fd' + fd + '=ok');
122+
} catch (e) {
123+
out.push(' fd' + fd + '=BAD(' + ((e && e.code) || e) + ')');
124+
}
125+
}
126+
let open;
127+
try {
128+
open = fs.readdirSync('/dev/fd')
129+
.map(Number).filter(function (n) { return !Number.isNaN(n); })
130+
.sort(function (x, y) { return x - y; });
131+
} catch (e) {
132+
open = '/dev/fd unreadable: ' + ((e && e.code) || e);
133+
}
134+
out.push(' open_fds=' + JSON.stringify(open));
135+
process.stderr.write(out.join('\n') + '\n');
136+
} catch (_e) {
137+
// diagnostics must never mask or replace the original failure
138+
}
139+
}
140+
103141
// Node caches modules by specifier, so `require('child_process')` and
104142
// `require('node:child_process')` may or may not be the same object
105143
// depending on Node version. Patch both to be safe.
@@ -124,7 +162,12 @@ function install(moduleSpecifier) {
124162
const origSpawn = cp.spawn;
125163
cp.spawn = function (command, args, options) {
126164
const [c, a] = rewriteSpawn(command, args);
127-
return origSpawn.call(this, c, a, options);
165+
try {
166+
return origSpawn.call(this, c, a, options);
167+
} catch (err) {
168+
dumpSpawnFailure(c, options, err);
169+
throw err;
170+
}
128171
};
129172

130173
const origSpawnSync = cp.spawnSync;

0 commit comments

Comments
 (0)