Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions native/lifeline.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include <errno.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#define GROUP_POLL_TIMEOUT_MS 100

static int parse_pgid(const char *value, pid_t *pgid) {
errno = 0;
char *end = NULL;
long parsed = strtol(value, &end, 10);
if (errno == ERANGE || end == value || *end != '\0' || parsed <= 1) {
return -1;
}

pid_t narrowed = (pid_t)parsed;
if ((long)narrowed != parsed) {
return -1;
}

*pgid = narrowed;
return 0;
}

static int process_group_alive(pid_t pgid) {
if (kill(-pgid, 0) == 0) {
return 1;
}
if (errno == EPERM) {
return 1;
}
if (errno == ESRCH) {
return 0;
}
return 1;
}

static void reap_group(pid_t pgid) {
kill(-pgid, SIGTERM);
usleep(200000);
/*
* The process group can outlive its leader. Probe the group itself so
* TERM-resistant descendants still get the SIGKILL fallback.
*/
if (kill(-pgid, 0) == 0 || errno == EPERM) {
kill(-pgid, SIGKILL);
}
}

static int should_reap_after_owner_eof(pid_t bridge_pgid) {
return process_group_alive(bridge_pgid) ? 1 : 0;
}

static int wait_for_owner_pipe(pid_t bridge_pgid) {
struct pollfd pfd = {
.fd = STDIN_FILENO,
.events = POLLIN,
};
char buffer[32];

for (;;) {
int ready = poll(&pfd, 1, GROUP_POLL_TIMEOUT_MS);
if (ready == -1) {
if (errno == EINTR) {
continue;
}
perror("poll");
return -1;
}

if (ready == 0) {
if (!process_group_alive(bridge_pgid)) {
return 0;
}
continue;
}

if ((pfd.revents & POLLIN) != 0) {
ssize_t nread = read(STDIN_FILENO, buffer, sizeof(buffer));
if (nread == -1) {
if (errno == EINTR) {
continue;
}
perror("read");
return -1;
}
if (nread == 0) {
return should_reap_after_owner_eof(bridge_pgid);
}
if (memchr(buffer, 'R', (size_t)nread) != NULL) {
return 0;
}
}

if ((pfd.revents & POLLHUP) != 0) {
return should_reap_after_owner_eof(bridge_pgid);
}

if ((pfd.revents & (POLLERR | POLLNVAL)) != 0) {
/*
* The lifeline fd itself is broken: we can no longer observe the owner.
* Degrade to pre-lifeline behavior (exit without reaping) rather than
* risk killing a live session, and avoid a POLLERR busy-loop.
*/
return -1;
}

if (!process_group_alive(bridge_pgid)) {
return 0;
}
}
}

int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: lifeline <bridgePgid>\n");
return 2;
}

pid_t bridge_pgid = 0;
if (parse_pgid(argv[1], &bridge_pgid) != 0) {
fprintf(stderr, "invalid bridge pgid\n");
return 2;
}

int result = wait_for_owner_pipe(bridge_pgid);
if (result == 1) {
reap_group(bridge_pgid);
return 0;
}
return result == 0 ? 0 : 1;
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"files": [
"dist",
"native",
"skills",
"README.md",
"LICENSE"
Expand All @@ -35,8 +36,9 @@
"./package.json": "./package.json"
},
"scripts": {
"build": "tsdown src/cli.ts src/flows.ts src/runtime.ts --format esm --dts --clean --platform node --target node22 --no-fixedExtension",
"build:quiet": "tsdown --logLevel silent src/cli.ts src/flows.ts src/runtime.ts --format esm --dts --clean --platform node --target node22 --no-fixedExtension",
"build": "tsdown src/cli.ts src/flows.ts src/runtime.ts --format esm --dts --clean --platform node --target node22 --no-fixedExtension && pnpm run build:native",
"build:native": "node scripts/build-native-lifeline.mjs",
"build:quiet": "tsdown --logLevel silent src/cli.ts src/flows.ts src/runtime.ts --format esm --dts --clean --platform node --target node22 --no-fixedExtension && pnpm run build:native",
"build:test": "node -e \"require('node:fs').rmSync('dist-test',{recursive:true,force:true})\" && tsgo -p tsconfig.test.json",
"check": "pnpm run format:check && pnpm run typecheck && pnpm run lint && pnpm run build && pnpm run viewer:typecheck && pnpm run viewer:build && pnpm run test:coverage",
"check:changed": "pnpm run check",
Expand All @@ -62,7 +64,7 @@
"mutate": "stryker run",
"perf:report": "tsx scripts/perf-report.ts",
"precommit": "pnpm exec lint-staged && pnpm run -s build",
"prepack": "tsdown --logLevel silent src/cli.ts src/flows.ts src/runtime.ts --format esm --dts --clean --platform node --target node22 --no-fixedExtension",
"prepack": "tsdown --logLevel silent src/cli.ts src/flows.ts src/runtime.ts --format esm --dts --clean --platform node --target node22 --no-fixedExtension && node -e \"require('node:fs').rmSync('dist/native',{recursive:true,force:true})\"",
"prepare": "husky",
"test": "pnpm run build && pnpm run build:test && node --test dist-test/test/*.test.js",
"test:changed": "pnpm run test:coverage",
Expand Down
48 changes: 48 additions & 0 deletions scripts/build-native-lifeline.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { spawnSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const targetPlatform = process.env.npm_config_platform ?? process.platform;
const targetArch = process.env.npm_config_arch ?? process.arch;

if (targetPlatform !== "darwin" && targetPlatform !== "linux") {
process.exit(0);
}

if (targetPlatform !== process.platform) {
console.warn(
`[acpx] skipping ${targetPlatform} lifeline helper build on ${process.platform} host`,
);
process.exit(0);
}

if (targetArch !== process.arch) {
console.warn(
`[acpx] skipping ${targetPlatform}-${targetArch} lifeline helper build on ${process.arch} host`,
);
process.exit(0);
}

const cc = process.env.CC ?? "cc";
const source = path.join(repoRoot, "native", "lifeline.c");
const outDir = path.join(repoRoot, "dist", "native");
const output = path.join(outDir, `lifeline-${targetPlatform}-${targetArch}`);

mkdirSync(outDir, { recursive: true });

const result = spawnSync(cc, ["-O2", source, "-o", output], {
stdio: "inherit",
});

if (result.error) {
const code = "code" in result.error ? result.error.code : undefined;
if (code === "ENOENT") {
console.warn(`[acpx] skipping ${targetPlatform} lifeline helper build: cc not found`);
process.exit(0);
}
throw result.error;
}

process.exit(result.status ?? 1);
Loading