@seekrit/cli 0.23.3 → 0.23.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +87 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1976,7 +1976,7 @@ function isServiceToken(value) {
1976
1976
  }
1977
1977
  //#endregion
1978
1978
  //#region package.json
1979
- var version = "0.23.3";
1979
+ var version = "0.23.4";
1980
1980
  //#endregion
1981
1981
  //#region ../../packages/api-client/src/index.ts
1982
1982
  var SeekritApiError = class extends Error {
@@ -4575,45 +4575,70 @@ async function materializeForRun(options) {
4575
4575
  }
4576
4576
  }
4577
4577
  /**
4578
- * Every live descendant of `root`, from a single `ps(1)` snapshot of the
4579
- * process table. We follow the parent chain rather than the process group
4580
- * because a process that calls setsid(2)/setpgid(2) leaves the group nodemon
4581
- * does exactly that for the script it watches — while the parent chain still
4582
- * leads to it. Returns nothing if `ps` is unavailable; callers fall back to
4583
- * signalling the immediate child.
4578
+ * Every live process `seekrit run` is responsible for tearing down, from a
4579
+ * single `ps(1)` snapshot: each descendant of `root`, plus when we lead our
4580
+ * own process group every other member of that group.
4581
+ *
4582
+ * Two mechanisms, because each covers the other's blind spot:
4583
+ *
4584
+ * - The parent-chain walk reaches a process that left our group via
4585
+ * setsid(2)/setpgid(2), which no group-wide kill and no tty-generated signal
4586
+ * can touch. But it loses anything whose chain up to `root` has already
4587
+ * broken — teardown kills the middle layers first, and an orphan reparents to
4588
+ * init, out of reach of any walk down from `root`.
4589
+ * - Group membership survives exactly that: a pgid outlives the parent that
4590
+ * passed it down. But it misses the ones that left the group.
4591
+ *
4592
+ * The group half applies only when our pid *is* our pgid, which is the shell-job
4593
+ * case: the shell put this command in a new group, so every other member of it
4594
+ * descends from us. Nested inside a shell script or a CI runner we are not the
4595
+ * leader and the group holds processes we did not start, so there we use the
4596
+ * walk alone.
4597
+ *
4598
+ * Zombies are skipped: they are already dead and waiting to be reaped, so
4599
+ * counting them would stall teardown for the full grace period and then report
4600
+ * a kill that did nothing. Returns nothing if `ps` is unavailable; callers fall
4601
+ * back to signalling the immediate child.
4584
4602
  */
4585
- function descendantsOf(root) {
4603
+ function teardownTargets(root) {
4586
4604
  const ps = spawnSync("ps", [
4587
4605
  "-A",
4588
4606
  "-o",
4589
- "pid=,ppid=,pgid="
4607
+ "pid=,ppid=,pgid=,stat="
4590
4608
  ], { encoding: "utf8" });
4591
4609
  if (ps.status !== 0 || typeof ps.stdout !== "string") return [];
4592
4610
  const childrenOf = /* @__PURE__ */ new Map();
4593
4611
  const groupOf = /* @__PURE__ */ new Map();
4594
4612
  const isId = (value) => value !== void 0 && Number.isInteger(value);
4595
4613
  for (const line of ps.stdout.split("\n")) {
4596
- const [pid, ppid, pgid] = line.trim().split(/\s+/, 3).map(Number);
4597
- if (!isId(pid) || !isId(ppid) || !isId(pgid)) continue;
4598
- const siblings = childrenOf.get(ppid);
4599
- if (siblings) siblings.push(pid);
4600
- else childrenOf.set(ppid, [pid]);
4601
- groupOf.set(pid, pgid);
4602
- }
4603
- const ourGroup = groupOf.get(process.pid);
4604
- const found = [];
4614
+ const [pid, ppid, pgid, stat] = line.trim().split(/\s+/, 4);
4615
+ const [id, parent, group] = [
4616
+ pid,
4617
+ ppid,
4618
+ pgid
4619
+ ].map(Number);
4620
+ if (!isId(id) || !isId(parent) || !isId(group)) continue;
4621
+ if (stat?.startsWith("Z")) continue;
4622
+ const siblings = childrenOf.get(parent);
4623
+ if (siblings) siblings.push(id);
4624
+ else childrenOf.set(parent, [id]);
4625
+ groupOf.set(id, group);
4626
+ }
4627
+ const found = /* @__PURE__ */ new Set();
4605
4628
  const queue = [root];
4606
4629
  const seen = new Set(queue);
4607
4630
  for (let pid = queue.shift(); pid !== void 0; pid = queue.shift()) for (const kid of childrenOf.get(pid) ?? []) {
4608
4631
  if (seen.has(kid)) continue;
4609
4632
  seen.add(kid);
4610
- found.push({
4611
- pid: kid,
4612
- escaped: ourGroup === void 0 || groupOf.get(kid) !== ourGroup
4613
- });
4633
+ found.add(kid);
4614
4634
  queue.push(kid);
4615
4635
  }
4616
- return found;
4636
+ if (groupOf.get(process.pid) === process.pid) {
4637
+ for (const [pid, group] of groupOf) if (group === process.pid) found.add(pid);
4638
+ }
4639
+ found.delete(process.pid);
4640
+ found.delete(root);
4641
+ return [...found];
4617
4642
  }
4618
4643
  /** Whether `pid` still exists — signal 0 tests reachability, delivers nothing. */
4619
4644
  function isAlive(pid) {
@@ -4624,25 +4649,37 @@ function isAlive(pid) {
4624
4649
  return false;
4625
4650
  }
4626
4651
  }
4627
- /** How long a signalled escapee gets to exit on its own before SIGKILL. */
4628
- const ESCAPEE_GRACE_MS = 2e3;
4652
+ /** How long a straggler gets to finish shutting down before SIGKILL. */
4653
+ const STRAGGLER_GRACE_MS = 2e3;
4629
4654
  /**
4630
- * Give already-signalled processes that left our group a moment to finish
4631
- * shutting down, then SIGKILL the holdouts. We are the last process that knows
4632
- * their pids: the tty won't hang them up, and once their parent exits they
4633
- * reparent to init, out of reach of any walk down from our own child — which is
4634
- * how one survives for days, invisible to `ps -a` for want of a tty. Only
4635
- * processes that already ignored a termination signal are killed; the command
4636
- * itself and anything still in our group shut down at their own pace.
4655
+ * Once the command has exited, make sure nothing it started outlives us.
4656
+ *
4657
+ * Anything still alive here has ignored the signal we forwarded *and* lost the
4658
+ * parent that launched it, and we are the last process that knows its pid:
4659
+ * the tty won't hang it up (it is no longer in the foreground group, and the
4660
+ * shell has moved on), so it survives until the machine reboots. That is how
4661
+ * `tsx` and the server under it stacked up for days.
4662
+ *
4663
+ * So escalate rather than trust. SIGTERM first — it costs no extra time and is
4664
+ * a rung the SIGINT-only handlers common in dev servers don't catch — then
4665
+ * SIGKILL whatever is left after the grace period. Nothing is killed unless it
4666
+ * was asked to stop first and declined, and this runs only when we forwarded a
4667
+ * termination signal: a command that exits on its own having deliberately left a
4668
+ * daemon behind keeps working.
4637
4669
  */
4638
- async function reapEscapees(pids) {
4639
- if (pids.size === 0) return;
4640
- const deadline = Date.now() + ESCAPEE_GRACE_MS;
4670
+ async function reapStragglers(pids, signal) {
4641
4671
  let live = [...pids].filter(isAlive);
4672
+ if (live.length === 0) return;
4673
+ for (const pid of live) try {
4674
+ process.kill(pid, "SIGTERM");
4675
+ } catch {}
4676
+ const deadline = Date.now() + STRAGGLER_GRACE_MS;
4642
4677
  while (live.length > 0 && Date.now() < deadline) {
4643
4678
  await new Promise((resolve) => setTimeout(resolve, 50));
4644
4679
  live = live.filter(isAlive);
4645
4680
  }
4681
+ if (live.length === 0) return;
4682
+ console.error(`seekrit: force-killed ${live.length} leftover process(es) that ignored ${signal} and SIGTERM (${live.join(", ")})`);
4646
4683
  for (const pid of live) try {
4647
4684
  process.kill(pid, "SIGKILL");
4648
4685
  } catch {}
@@ -4669,21 +4706,27 @@ program.command("run").description("run a command with decrypted secrets injecte
4669
4706
  "SIGHUP",
4670
4707
  "SIGQUIT"
4671
4708
  ];
4672
- const escapees = /* @__PURE__ */ new Set();
4709
+ const stragglers = /* @__PURE__ */ new Set();
4710
+ let forwarded;
4711
+ const collect = () => {
4712
+ if (!posix || !child.pid) return;
4713
+ for (const pid of teardownTargets(child.pid)) stragglers.add(pid);
4714
+ };
4673
4715
  const forward = (signal) => {
4674
4716
  if (child.exitCode !== null || child.signalCode !== null) return;
4717
+ forwarded ??= signal;
4718
+ collect();
4675
4719
  child.kill(signal);
4676
- if (!posix || !child.pid) return;
4677
- for (const { pid, escaped } of descendantsOf(child.pid)) {
4678
- if (escaped) escapees.add(pid);
4679
- try {
4680
- process.kill(pid, signal);
4681
- } catch {}
4682
- }
4720
+ for (const pid of stragglers) try {
4721
+ process.kill(pid, signal);
4722
+ } catch {}
4683
4723
  };
4684
4724
  for (const signal of signals) process.on(signal, forward);
4685
4725
  child.on("exit", async (code, signal) => {
4686
- await reapEscapees(escapees);
4726
+ if (forwarded) {
4727
+ collect();
4728
+ await reapStragglers(stragglers, forwarded);
4729
+ }
4687
4730
  for (const s of signals) process.off(s, forward);
4688
4731
  if (signal) process.kill(process.pid, signal);
4689
4732
  else process.exit(code ?? 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.23.3",
3
+ "version": "0.23.4",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {