Environment
Summary
Each recs invocation spawns a background helper via spawn(process.execPath, [...,
"--check-update-internal"], ...). That helper detaches (SESS=0, reparents to launchd), never
terminates, and burns CPU. On a machine where recs is called from a hot path (e.g. from a wrapper
script used many times a day), the orphans accumulate. I ended up with 20+ orphaned (recs) processes
consuming a combined ~150% CPU and dropping idle to 0.09% — noticeable enough to cook the battery
within a work session.
Reproduction
$ for i in 1 2 3 4 5; do echo '[{"a":1}]' | jq -c '.[]' | recs totable >/dev/null; done
$ sleep 3
$ ps -Ao pid,pcpu,etime,command | grep -E '\brecs\b.*check-update-internal' | grep -v grep
Each invocation leaves at least one orphaned --check-update-internal process behind. Over a longer
session they stack up and never exit.
Kernel log also shows each child triggering:
kernel: recs[N] triggered unnest of range 0x1e8000000->0x1ec000000 of DYLD shared region ...
so the leak has a real memory footprint per orphan on top of the CPU cost.
Expected
The update-check helper should:
- Have a hard timeout (a few seconds).
- Exit cleanly whether or not the check succeeds.
- Be suppressible from a normal invocation.
Root cause hint
The binary already has the plumbing for opt-out — from strings recs:
var noUpdateCheck = args.includes("--no-update-check");
if (!noUpdateCheck && shouldCheck(getConfigDir())) { ... }
But --no-update-check isn't stripped from argv before subcommand dispatch, so:
- recs --no-update-check totable → "Unknown command: --no-update-check"
- recs totable --no-update-check → "Unknown option: --no-update-check"
So the flag effectively can't be used from the CLI today. It suppresses the daemon (I verified
args.includes runs before dispatch, so the guard works), but the invocation still fails.
Suggested fix (small)
- Filter --no-update-check (and any other top-level global flags) out of args before handing off to the
subcommand parser.
- Wrap the update-check child in a short (~3s) hard timeout, and ensure it always exit()s — either on
success, HTTP error, or timeout.
- Optionally: also honor an env var (e.g. RECS_NO_UPDATE_CHECK=1) so it can be set once in a shell rc.
Impact
Anyone piping recs from a script (e.g. ... | recs totable) in a loop will silently accumulate CPU-hungry
orphans. It's not visible until you check ps.
Workaround for other users (until fixed)
Rename the binary and drop in a tiny wrapper that reaps the leak:
mv ~/.local/bin/recs ~/.local/bin/recs-bin
cat > ~/.local/bin/recs <<'SH'
#!/bin/bash
"$(dirname "$0")/recs-bin" "$@"; rc=$?
pkill -u "$(id -u)" -f 'recs.*--check-update-internal' 2>/dev/null || true
exit $rc
SH
chmod +x ~/.local/bin/recs
Environment
8d295582a19a5dd31758c3b1602c680f09e1f43f4ccc53241dc552cbb4939db4)
https://raw.githubusercontent.com/benbernard/RecordStream/master/install.sh | bash
Summary
Each recs invocation spawns a background helper via spawn(process.execPath, [...,
"--check-update-internal"], ...). That helper detaches (SESS=0, reparents to launchd), never
terminates, and burns CPU. On a machine where recs is called from a hot path (e.g. from a wrapper
script used many times a day), the orphans accumulate. I ended up with 20+ orphaned (recs) processes
consuming a combined ~150% CPU and dropping idle to 0.09% — noticeable enough to cook the battery
within a work session.
Reproduction
$ for i in 1 2 3 4 5; do echo '[{"a":1}]' | jq -c '.[]' | recs totable >/dev/null; done
$ sleep 3
$ ps -Ao pid,pcpu,etime,command | grep -E '\brecs\b.*check-update-internal' | grep -v grep
Each invocation leaves at least one orphaned --check-update-internal process behind. Over a longer
session they stack up and never exit.
Kernel log also shows each child triggering:
kernel: recs[N] triggered unnest of range 0x1e8000000->0x1ec000000 of DYLD shared region ...
so the leak has a real memory footprint per orphan on top of the CPU cost.
Expected
The update-check helper should:
Root cause hint
The binary already has the plumbing for opt-out — from strings recs:
var noUpdateCheck = args.includes("--no-update-check");
if (!noUpdateCheck && shouldCheck(getConfigDir())) { ... }
But --no-update-check isn't stripped from argv before subcommand dispatch, so:
So the flag effectively can't be used from the CLI today. It suppresses the daemon (I verified
args.includes runs before dispatch, so the guard works), but the invocation still fails.
Suggested fix (small)
subcommand parser.
success, HTTP error, or timeout.
Impact
Anyone piping recs from a script (e.g. ... | recs totable) in a loop will silently accumulate CPU-hungry
orphans. It's not visible until you check ps.
Workaround for other users (until fixed)
Rename the binary and drop in a tiny wrapper that reaps the leak:
mv ~/.local/bin/recs ~/.local/bin/recs-bin
cat > ~/.local/bin/recs <<'SH'
#!/bin/bash
"$(dirname "$0")/recs-bin" "$@"; rc=$?
pkill -u "$(id -u)" -f 'recs.*--check-update-internal' 2>/dev/null || true
exit $rc
SH
chmod +x ~/.local/bin/recs