Repository Public
1/**2 * Static helper programs for durable tasks. Both texts are constants: the task command, host,3 * shell and paths reach them through files / CR_TASK_* environment variables, never by4 * interpolation into the script text.5 */67/**8 * Runner (POSIX sh), copied as `tasks/<id>/run.sh` (0700) and executed by tmux in `crt-<id>`.9 *10 * command ← `$D/command` (file) log → `$D/log` via the capped sink11 * started → `$D/started` (mtime = start time) exit → `$D/exit` (atomic; mtime = end time)12 * rusage → `$D/time` (`/usr/bin/time -l`, local tasks, when available)13 *14 * TERM/HUP/INT are *trapped* (not ignored) so children keep the default disposition and die15 * on cancel, while the runner survives long enough to record the exit code.16 */17export const TASK_RUNNER_SH = `#!/bin/sh18# Generated by claude-remote runtime (static text; parameters come from files and CR_TASK_* env).19D=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || exit 9720trap ':' TERM HUP INT21CMD=$(cat "$D/command") || exit 9722rm -f "$D/pipe"23mkfifo -m 600 "$D/pipe" || exit 9724# Job control puts the sink in its own process group: a cancel (TERM/KILL sent to this25# script's group) never reaches it, so it always drains the log to the end.26set -m27"$CR_TASK_NODE" "$CR_TASK_SINK" "$D/log" "$CR_TASK_LOG_CAP" <"$D/pipe" &28SINK=$!29set +m30: >"$D/started"31if [ "$CR_TASK_MODE" = ssh ]; then32 ssh -o BatchMode=yes -o ConnectTimeout=10 "$CR_TASK_HOST" -- "$CMD" >"$D/pipe" 2>&1 </dev/null33elif /usr/bin/time -l -o "$D/time" true >/dev/null 2>&1; then34 /usr/bin/time -l -o "$D/time" "$CR_TASK_SHELL" -lc "$CMD" >"$D/pipe" 2>&1 </dev/null35else36 rm -f "$D/time"37 "$CR_TASK_SHELL" -lc "$CMD" >"$D/pipe" 2>&1 </dev/null38fi39CODE=$?40while kill -0 "$SINK" 2>/dev/null; do wait "$SINK"; done41printf '%s\\n' "$CODE" >"$D/exit.tmp" && mv -f "$D/exit.tmp" "$D/exit"42rm -f "$D/pipe"43exit "$CODE"44`;4546/**47 * Log sink (CommonJS, run with the runtime's own node binary): appends stdin to the log file48 * until `cap` bytes, then keeps draining stdin to nowhere so the task is never blocked or49 * killed by a full log. Independent of the runtime process (survives its restarts).50 */51export const TASK_LOG_SINK_CJS = `'use strict';52const fs = require('fs');53const file = process.argv[2];54const cap = Number(process.argv[3]);55for (const s of ['SIGTERM', 'SIGHUP', 'SIGINT']) process.on(s, () => {});56const fd = fs.openSync(file, 'a', 0o600);57let size = 0;58try { size = fs.fstatSync(fd).size; } catch (e) {}59const buf = Buffer.allocUnsafe(65536);60for (;;) {61 let n;62 try {63 n = fs.readSync(0, buf, 0, buf.length, null);64 } catch (e) {65 if (e && e.code === 'EAGAIN') { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20); continue; }66 if (e && e.code === 'EINTR') continue;67 break;68 }69 if (n === 0) break;70 if (size < cap) {71 const w = Math.min(n, cap - size);72 fs.writeSync(fd, buf, 0, w);73 size += w;74 }75}76`;7778export interface TaskRusage {79 cpuSec: number | null;80 maxRssMb: number | null;81}8283/**84 * Parses the trailer written by BSD/macOS `/usr/bin/time -l` (decimal separator may be `.`85 * or `,` depending on the locale). Anything not recognised yields null — never a guess.86 */87export function parseTimeTrailer(text: string): TaskRusage {88 const num = (s: string): number => Number(s.replace(',', '.'));89 let cpuSec: number | null = null;90 let maxRssMb: number | null = null;91 const t = /([0-9]+[.,][0-9]+)\s+real\s+([0-9]+[.,][0-9]+)\s+user\s+([0-9]+[.,][0-9]+)\s+sys/.exec(text);92 if (t?.[2] !== undefined && t[3] !== undefined) {93 const v = num(t[2]) + num(t[3]);94 if (Number.isFinite(v)) cpuSec = Math.round(v * 1000) / 1000;95 }96 const r = /^\s*([0-9]+)\s+maximum resident set size/m.exec(text);97 if (r?.[1] !== undefined) {98 const bytes = Number(r[1]); // bytes on macOS99 if (Number.isFinite(bytes)) maxRssMb = Math.round((bytes / (1024 * 1024)) * 10) / 10;100 }101 return { cpuSec, maxRssMb };102}103