diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..faf8a55 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,51 @@ +--- +name: use-disag +description: Use the disag command runner in this repository to run one or more commands concurrently, mirror stdout/stderr, write optional raw log files, and test or modify the JavaScript implementation. +--- + +# Disag + +Use this skill when an agent needs to run, test, or modify this repository's `disag` command runner. + +## Implementations + +- `./disag` is the original rc implementation. Keep it unless the user explicitly asks to replace it. +- `./disag.js` is the JavaScript implementation. It is designed for Bun via its shebang, but uses Node-compatible APIs and can also be run with `node ./disag.js`. + +## Usage + +Run one or more commands concurrently: + +```sh +./disag.js 'echo hello' +./disag.js 'node tests/fixtures/diag-helper.js emit out err' -f ./command.log -n CMD +./disag.js 'echo out; echo err >&2' -s -f ./command.log -n SHELL +``` + +Options apply to the immediately preceding command: + +- `-f, --file ` writes raw stdout and stderr for that command to the named log file. If repeated, the last file wins. +- `-n, --name ` prefixes displayed lines with `[name]`; prefixes are not written to log files. +- `-s, --shell` runs that command through `sh -c`. Use it only when shell syntax is required. +- `-z, --no-color` disables ANSI color on displayed stdout. +- `-h, --help` prints usage. +- Each command prints a display-only stderr line of the form `[DISAG] exited with exit code ` when it exits. Use `-n` as the short name in that line when available; otherwise use the command text. + +## Constraints + +- By default, `./disag.js` tokenizes the command string into argv and invokes the executable directly. Shell syntax such as redirection, pipes, variables, and compound commands requires `-s/--shell`. +- Do not introduce implicit shell wrappers, named temporary files, fifos, or filesystem writes outside the paths explicitly passed with `-f`. +- For signal forwarding, prefer direct process APIs and `kill` semantics. The JavaScript implementation uses detached child process groups and forwards signals to the group and child PID. +- Preserve separate stdout and stderr display streams. Log files intentionally contain raw lines from both streams without prefixes or color. +- The `[DISAG]` exit-code line goes to stderr only, is not written to `-f` logs, and should use the same ANSI color as that command's displayed output when color is enabled. + +## Validation + +Run these checks after changing the tool: + +```sh +./tests/disag-js.rc +node ./disag.js 'echo node-ok' -n NODE +``` + +Run `./tests/disag.rc` only when changing or validating the original rc implementation. diff --git a/disag b/disag index 072b752..920b6f1 100755 --- a/disag +++ b/disag @@ -2,6 +2,9 @@ prog=$0 +if(! ~ $1 --diag-run-one) + echo 'DEPRECATED use disag.js' >[1=2] + fn usage { echo 'usage: diag [-z|--no-color] command [-f|--file file] [-n|--name name] ...' >[1=2] echo 'try: diag --help' >[1=2] @@ -32,6 +35,7 @@ Notes: If no -f is specified for a command, its output is only displayed. Display prefixes and ANSI colors are never written to log files. Colors are used only when stdout is a terminal. + Each command prints a [DISAG] exit-code line to stderr when it exits. diag uses anonymous rc pipes internally and only writes paths named with -f. Signals are forwarded to child processes with kill. @@ -64,6 +68,22 @@ fn runone { if(! ~ $file '') > $file + fn reportexit { + status=$1 + label=$cmd + if(! ~ $name '') + label=$name + awk -v label=$label -v status=$status -v color=$color ' + BEGIN { + line = "[DISAG] " label " exited with exit code " status + if(color != "") + line = sprintf("%c[%sm%s%c[0m", 27, color, line, 27) + print line > "/dev/stderr" + fflush("/dev/stderr") + } + ' + } + fn stoprunone { sig=$1 for(pid in $runpids) @@ -71,6 +91,11 @@ fn runone { for(pid in $runpids) kill -^$sig $pid >[2]/dev/null wait + for(pid in $runpids) { + while(kill -0 -^$pid >[2]/dev/null) + sleep 0.1 + } + reportexit $status exit sig^$sig } @@ -126,6 +151,9 @@ fn runone { } & runpids=$apid wait $runpids + st=$status + reportexit $st + exit $st } if(! ~ $#* 0 && ~ $1 --diag-run-one) { @@ -136,11 +164,15 @@ if(! ~ $#* 0 && ~ $1 --diag-run-one) { fn stopall { sig=$1 - for(pid in $apids) + for(pid in $writers) kill -^$sig -^$pid >[2]/dev/null - for(pid in $apids) + for(pid in $writers) kill -^$sig $pid >[2]/dev/null wait + for(pid in $writers) { + while(kill -0 -^$pid >[2]/dev/null) + sleep 0.1 + } exit sig^$sig } diff --git a/disag.js b/disag.js new file mode 100755 index 0000000..2050613 --- /dev/null +++ b/disag.js @@ -0,0 +1,343 @@ +#!/usr/bin/env bun + +import fs from "node:fs"; +import { spawn } from "node:child_process"; +import process from "node:process"; + +const colors = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96]; +const signalCodes = { + SIGINT: 130, + SIGTERM: 143, + SIGQUIT: 131, + SIGHUP: 129, +}; + +function usage() { + process.stderr.write( + "usage: diag [-z|--no-color] command [-f|--file file] [-n|--name name] [-s|--shell] ...\n", + ); + process.stderr.write("try: diag --help\n"); + process.exit(1); +} + +function help() { + process.stdout.write(`usage: diag [-z|--no-color] command [-f|--file file] [-n|--name name] [-s|--shell] ... + diag -h|--help + +Run one or more commands at the same time and mirror their output. + +By default, each command argument is split into argv and executed directly. +Its stdout is printed to diag's stdout and its stderr is printed to diag's +stderr. If -f is set for a command, both streams are also written to that file +as raw command output. + +Options: + -f, --file file write the immediately preceding command's stdout/stderr + to file; if repeated for that command, the last value wins + -n, --name name prefix displayed lines for the immediately preceding + command with [name]; not written to log files + -s, --shell execute the immediately preceding command with sh -c + -z, --no-color disable ANSI colors even when stdout is a terminal + -h, --help show this help text + +Notes: + -f, -n, and -s always apply to the command immediately before them. + If no -f is specified for a command, its output is only displayed. + Display prefixes and ANSI colors are never written to log files. + Colors are used only when stdout is a terminal. + Each command prints a [DISAG] exit-code line to stderr when it exits. + diag-js only writes paths named with -f. + Signals are forwarded to child processes with kill. + +Examples: + diag 'echo hello' + + diag 'printf "hello world\n"' -n CMD + + diag 'echo out; echo err >&2' -s -f ./command.log -n CMD + + diag \\ + 'while true; do echo hello; sleep 1; done' -s -f ./hello.log -n HELLO \\ + 'some_command' -f ./some_command.log + + diag -z 'echo no color' -n PLAIN + + diag 'echo first' -f old.log -f new.log -n OLD -n NEW + # new.log is used and displayed lines are prefixed with [NEW]. +`); + process.exit(0); +} + +function parseArgs(args) { + if (args.length > 0 && (args[0] === "-h" || args[0] === "--help")) { + help(); + } + + const noColor = args.some((arg) => arg === "-z" || arg === "--no-color"); + const commands = []; + let current = null; + + function finishCurrent() { + if (current !== null) { + commands.push(current); + current = null; + } + } + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === "-z" || arg === "--no-color") { + continue; + } + if (arg === "-f" || arg === "--file") { + if (current === null || i + 1 >= args.length) { + usage(); + } + current.file = args[++i]; + continue; + } + if (arg === "-n" || arg === "--name") { + if (current === null || i + 1 >= args.length) { + usage(); + } + current.name = args[++i]; + continue; + } + if (arg === "-s" || arg === "--shell") { + if (current === null) { + usage(); + } + current.shell = true; + continue; + } + finishCurrent(); + current = { command: arg, file: "", name: "", shell: false }; + } + + finishCurrent(); + if (commands.length === 0) { + usage(); + } + + return { commands, noColor }; +} + +function splitCommand(command) { + const argv = []; + let current = ""; + let quote = null; + let escaped = false; + let hadToken = false; + + for (const ch of command) { + if (escaped) { + current += ch; + escaped = false; + hadToken = true; + continue; + } + if (ch === "\\" && quote !== "'") { + escaped = true; + hadToken = true; + continue; + } + if (quote !== null) { + if (ch === quote) { + quote = null; + } else { + current += ch; + } + hadToken = true; + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + hadToken = true; + continue; + } + if (ch === " " || ch === "\t" || ch === "\n") { + if (hadToken) { + argv.push(current); + current = ""; + hadToken = false; + } + continue; + } + current += ch; + hadToken = true; + } + + if (escaped || quote !== null) { + throw new Error(`unterminated quote or escape in command: ${command}`); + } + if (hadToken) { + argv.push(current); + } + if (argv.length === 0) { + throw new Error("empty command"); + } + return argv; +} + +function decorate(line, spec) { + let out = line; + if (spec.name !== "") { + out = `[${spec.name}] ${out}`; + } + if (spec.color !== "") { + out = `\x1b[${spec.color}m${out}\x1b[0m`; + } + return out; +} + +function colorize(line, spec) { + if (spec.color === "") { + return line; + } + return `\x1b[${spec.color}m${line}\x1b[0m`; +} + +function reportExit(spec, status) { + const label = spec.name !== "" ? spec.name : spec.command; + process.stderr.write( + `${colorize(`[DISAG] ${label} exited with exit code ${status}`, spec)}\n`, + ); +} + +function attachLineStream(readable, display, log, spec) { + let pending = ""; + + function emit(line) { + if (log !== null) { + fs.writeSync(log, `${line}\n`); + } + display.write(`${decorate(line, spec)}\n`); + } + + readable.setEncoding("utf8"); + readable.on("data", (chunk) => { + pending += chunk; + for (;;) { + const newline = pending.indexOf("\n"); + if (newline === -1) { + break; + } + let line = pending.slice(0, newline); + if (line.endsWith("\r")) { + line = line.slice(0, -1); + } + pending = pending.slice(newline + 1); + emit(line); + } + }); + + readable.on("end", () => { + if (pending !== "") { + emit(pending); + pending = ""; + } + }); +} + +function killChild(child, signal) { + if (child.exitCode !== null || child.signalCode !== null) { + return; + } + try { + process.kill(-child.pid, signal); + } catch { + // The child may not have its own process group on some runtimes/platforms. + } + try { + child.kill(signal); + } catch { + // It may already be gone. + } +} + +function runCommand(spec) { + const argv = spec.shell ? ["sh", "-c", spec.command] : splitCommand(spec.command); + let log = null; + if (spec.file !== "") { + log = fs.openSync(spec.file, "w"); + } + + const child = spawn(argv[0], argv.slice(1), { + detached: true, + stdio: ["ignore", "pipe", "pipe"], + }); + + if (child.stdout !== null) { + attachLineStream(child.stdout, process.stdout, log, spec); + } + if (child.stderr !== null) { + attachLineStream(child.stderr, process.stderr, log, spec); + } + + const done = new Promise((resolve) => { + child.on("error", (err) => { + if (log !== null) { + fs.closeSync(log); + } + process.stderr.write(`diag-js: ${err.message}\n`); + resolve(1); + }); + + child.on("close", (code, signal) => { + if (log !== null) { + fs.closeSync(log); + } + if (code !== null) { + reportExit(spec, code); + resolve(code); + } else if (signal !== null && signal in signalCodes) { + const status = signalCodes[signal]; + reportExit(spec, status); + resolve(status); + } else { + reportExit(spec, 1); + resolve(1); + } + }); + }); + + return { child, done }; +} + +async function main() { + const { commands, noColor } = parseArgs(process.argv.slice(2)); + const useColor = !noColor && process.stdout.isTTY; + const runs = commands.map((command, index) => + runCommand({ + ...command, + color: useColor ? String(colors[index % colors.length]) : "", + }), + ); + + let stopping = false; + let stopCode = null; + for (const signal of Object.keys(signalCodes)) { + process.on(signal, () => { + if (stopping) { + return; + } + stopping = true; + stopCode = signalCodes[signal]; + for (const run of runs) { + killChild(run.child, signal); + } + }); + } + + const statuses = await Promise.all(runs.map((run) => run.done)); + if (stopCode !== null) { + process.exit(stopCode); + } + const failed = statuses.find((status) => status !== 0); + process.exit(failed === undefined ? 0 : failed); +} + +main().catch((err) => { + process.stderr.write(`diag-js: ${err.stack || err.message}\n`); + process.exit(1); +}); diff --git a/tests/disag-js.rc b/tests/disag-js.rc new file mode 100755 index 0000000..1c239cb --- /dev/null +++ b/tests/disag-js.rc @@ -0,0 +1,124 @@ +#!/usr/bin/env rc + +diag=./disag.js +tmp=/tmp/diag-js-tests.$pid +mkdir $tmp || exit $status + +fn cleanup { + rm -rf $tmp +} + +fn fail { + echo test failed: $* >[1=2] + cleanup + exit fail +} + +fn assert_file { + if(! cmp -s $1 $2) + fail $3 +} + +$diag --help > $tmp/help.out >[2]$tmp/help.err || fail help-run +grep -s '^usage: diag' $tmp/help.out >/dev/null || fail help-usage +grep -s '^Examples:' $tmp/help.out >/dev/null || fail help-examples +grep -s 'only writes paths named with -f' $tmp/help.out >/dev/null || fail help-files +if(! ~ `{wc -c < $tmp/help.err} 0) + fail help-stderr + +if(grep -s 'mkfifo\|/tmp/diag\|pgrep -P\|perl\|rc -c\|setsid' $diag >/dev/null) + fail named-temp-or-shell-mechanism + +ls -la > $tmp/workspace.before +$diag 'echo no-files' -n NF > $tmp/no-files.out >[2=1] || fail no-files-run +ls -la > $tmp/workspace.after +assert_file $tmp/workspace.before $tmp/workspace.after no-files-mutated-workspace + +$diag 'echo direct; echo hidden' -n DIRECT > $tmp/direct.out >[2=1] || fail direct-run +printf '%s\n' '[DIRECT] direct; echo hidden' '[DISAG] DIRECT exited with exit code 0' > $tmp/direct.out.want +assert_file $tmp/direct.out $tmp/direct.out.want direct-no-shell + +$diag 'node tests/fixtures/diag-helper.js emit out err' -f $tmp/raw.log -n RAW > $tmp/raw.out >[2]$tmp/raw.err || fail raw-run +printf '%s\n' '[RAW] out' > $tmp/raw.out.want +printf '%s\n' '[RAW] err' '[DISAG] RAW exited with exit code 0' > $tmp/raw.err.want +printf '%s\n' err out > $tmp/raw.log.want +assert_file $tmp/raw.out $tmp/raw.out.want raw-output +assert_file $tmp/raw.err $tmp/raw.err.want raw-error +sort $tmp/raw.log > $tmp/raw.log.sorted +assert_file $tmp/raw.log.sorted $tmp/raw.log.want raw-log + +$diag 'echo first' -f $tmp/old.log -f $tmp/new.log -n OLD -n NEW > $tmp/last.out >[2=1] || fail last-run +printf '%s\n' '[NEW] first' '[DISAG] NEW exited with exit code 0' > $tmp/last.out.want +printf '%s\n' first > $tmp/new.log.want +assert_file $tmp/last.out $tmp/last.out.want last-output +assert_file $tmp/new.log $tmp/new.log.want last-log +if(test -e $tmp/old.log) + fail old-log-created + +$diag 'echo shell-out; echo shell-err >&2' -s -n SH > $tmp/shell.out >[2]$tmp/shell.err || fail shell-run +printf '%s\n' '[SH] shell-out' > $tmp/shell.out.want +printf '%s\n' '[SH] shell-err' '[DISAG] SH exited with exit code 0' > $tmp/shell.err.want +assert_file $tmp/shell.out $tmp/shell.out.want shell-stdout +assert_file $tmp/shell.err $tmp/shell.err.want shell-stderr + +$diag 'echo display-only' -n DISP > $tmp/display.out >[2=1] || fail display-run +printf '%s\n' '[DISP] display-only' '[DISAG] DISP exited with exit code 0' > $tmp/display.out.want +assert_file $tmp/display.out $tmp/display.out.want display-output + +$diag 'node tests/fixtures/diag-helper.js emit out err' -f $tmp/streams.log -n S > $tmp/streams.out >[2]$tmp/streams.err || fail streams-run +printf '%s\n' '[S] out' > $tmp/streams.out.want +printf '%s\n' '[S] err' '[DISAG] S exited with exit code 0' > $tmp/streams.err.want +assert_file $tmp/streams.out $tmp/streams.out.want streams-stdout +assert_file $tmp/streams.err $tmp/streams.err.want streams-stderr + +$diag 'echo spaced' -f $tmp/'space log.txt' -n 'SPACE NAME' > $tmp/spaced.out >[2=1] || fail spaced-run +printf '%s\n' '[SPACE NAME] spaced' '[DISAG] SPACE NAME exited with exit code 0' > $tmp/spaced.out.want +printf '%s\n' spaced > $tmp/'space log.txt.want' +assert_file $tmp/spaced.out $tmp/spaced.out.want spaced-output +assert_file $tmp/'space log.txt' $tmp/'space log.txt.want' spaced-log + +$diag 'echo command-label' > $tmp/command-label.out >[2]$tmp/command-label.err || fail command-label-run +printf '%s\n' command-label > $tmp/command-label.out.want +printf '%s\n' '[DISAG] echo command-label exited with exit code 0' > $tmp/command-label.err.want +assert_file $tmp/command-label.out $tmp/command-label.out.want command-label-stdout +assert_file $tmp/command-label.err $tmp/command-label.err.want command-label-stderr + +$diag 'node tests/fixtures/diag-helper.js exit 7' > $tmp/status.out >[2=1] +s=$status +if(~ $s 0) + fail status-zero + +sigcmd='node tests/fixtures/diag-helper.js signal '^$tmp +$diag $sigcmd -f $tmp/signal.log > $tmp/signal.out >[2=1] & +pid=$apid +i=0 +while(! test -f $tmp/ready.seen && ! ~ $i 50) { + sleep 0.1 + i=`{expr $i + 1} +} +kill -TERM $pid +wait $pid +i=0 +while(! test -f $tmp/term.seen && ! ~ $i 50) { + sleep 0.1 + i=`{expr $i + 1} +} +if(! test -f $tmp/term.seen) + fail signal-not-propagated + +rm -f $tmp/ready.seen $tmp/term.seen $tmp/done.seen +sigcmd='node tests/fixtures/diag-helper.js signal-delay '^$tmp +$diag $sigcmd -f $tmp/signal-delay.log > $tmp/signal-delay.out >[2=1] & +pid=$apid +i=0 +while(! test -f $tmp/ready.seen && ! ~ $i 50) { + sleep 0.1 + i=`{expr $i + 1} +} +kill -TERM $pid +wait $pid +if(! test -f $tmp/done.seen) + fail signal-delay-did-not-wait + +cleanup +echo ok diff --git a/tests/disag.rc b/tests/disag.rc index 5f65c1f..d633ec4 100755 --- a/tests/disag.rc +++ b/tests/disag.rc @@ -23,8 +23,8 @@ $diag --help > $tmp/help.out >[2]$tmp/help.err || fail help-run grep -s '^usage: diag' $tmp/help.out >/dev/null || fail help-usage grep -s '^Examples:' $tmp/help.out >/dev/null || fail help-examples grep -s 'anonymous rc pipes' $tmp/help.out >/dev/null || fail help-anonymous-pipes -if(! ~ `{wc -c < $tmp/help.err} 0) - fail help-stderr +printf '%s\n' 'DEPRECATED use disag.js' > $tmp/help.err.want +assert_file $tmp/help.err $tmp/help.err.want help-stderr if(grep -s 'mkfifo\|/tmp/diag\|pgrep -P\|perl' $diag >/dev/null) fail named-temp-mechanism @@ -36,7 +36,7 @@ assert_file $tmp/workspace.before $tmp/workspace.after no-files-mutated-workspac $diag 'echo out; echo err >[1=2]' -f $tmp/raw.log -n RAW > $tmp/raw.out >[2]$tmp/raw.err || fail raw-run printf '%s\n' '[RAW] out' > $tmp/raw.out.want -printf '%s\n' '[RAW] err' > $tmp/raw.err.want +printf '%s\n' 'DEPRECATED use disag.js' '[RAW] err' '[DISAG] RAW exited with exit code 0' > $tmp/raw.err.want printf '%s\n' err out > $tmp/raw.log.want assert_file $tmp/raw.out $tmp/raw.out.want raw-output assert_file $tmp/raw.err $tmp/raw.err.want raw-error @@ -44,7 +44,7 @@ sort $tmp/raw.log > $tmp/raw.log.sorted assert_file $tmp/raw.log.sorted $tmp/raw.log.want raw-log $diag 'echo first' -f $tmp/old.log -f $tmp/new.log -n OLD -n NEW > $tmp/last.out >[2=1] || fail last-run -printf '%s\n' '[NEW] first' > $tmp/last.out.want +printf '%s\n' 'DEPRECATED use disag.js' '[NEW] first' '[DISAG] NEW exited with exit code 0' > $tmp/last.out.want printf '%s\n' first > $tmp/new.log.want assert_file $tmp/last.out $tmp/last.out.want last-output assert_file $tmp/new.log $tmp/new.log.want last-log @@ -52,17 +52,17 @@ if(test -e $tmp/old.log) fail old-log-created $diag 'echo display-only' -n DISP > $tmp/display.out >[2=1] || fail display-run -printf '%s\n' '[DISP] display-only' > $tmp/display.out.want +printf '%s\n' 'DEPRECATED use disag.js' '[DISP] display-only' '[DISAG] DISP exited with exit code 0' > $tmp/display.out.want assert_file $tmp/display.out $tmp/display.out.want display-output $diag 'echo out; echo err >[1=2]' -f $tmp/streams.log -n S > $tmp/streams.out >[2]$tmp/streams.err || fail streams-run printf '%s\n' '[S] out' > $tmp/streams.out.want -printf '%s\n' '[S] err' > $tmp/streams.err.want +printf '%s\n' 'DEPRECATED use disag.js' '[S] err' '[DISAG] S exited with exit code 0' > $tmp/streams.err.want assert_file $tmp/streams.out $tmp/streams.out.want streams-stdout assert_file $tmp/streams.err $tmp/streams.err.want streams-stderr $diag 'echo spaced' -f $tmp/'space log.txt' -n 'SPACE NAME' > $tmp/spaced.out >[2=1] || fail spaced-run -printf '%s\n' '[SPACE NAME] spaced' > $tmp/spaced.out.want +printf '%s\n' 'DEPRECATED use disag.js' '[SPACE NAME] spaced' '[DISAG] SPACE NAME exited with exit code 0' > $tmp/spaced.out.want printf '%s\n' spaced > $tmp/'space log.txt.want' assert_file $tmp/spaced.out $tmp/spaced.out.want spaced-output assert_file $tmp/'space log.txt' $tmp/'space log.txt.want' spaced-log @@ -72,7 +72,7 @@ s=$status if(~ $s 0) fail status-zero -sigcmd='fn sigterm { echo term > '^$tmp^'/term.seen; exit 0 }; echo ready > '^$tmp^'/ready.seen; while(~ 1 1) sleep 1' +sigcmd='node tests/fixtures/diag-helper.js signal '^$tmp $diag $sigcmd -f $tmp/signal.log > $tmp/signal.out >[2=1] & pid=$apid i=0 @@ -90,5 +90,19 @@ while(! test -f $tmp/term.seen && ! ~ $i 50) { if(! test -f $tmp/term.seen) fail signal-not-propagated +rm -f $tmp/ready.seen $tmp/term.seen $tmp/done.seen +sigcmd='node tests/fixtures/diag-helper.js signal-delay '^$tmp +$diag $sigcmd -f $tmp/signal-delay.log > $tmp/signal-delay.out >[2=1] & +pid=$apid +i=0 +while(! test -f $tmp/ready.seen && ! ~ $i 50) { + sleep 0.1 + i=`{expr $i + 1} +} +kill -TERM $pid +wait $pid +if(! test -f $tmp/done.seen) + fail signal-delay-did-not-wait + cleanup echo ok diff --git a/tests/fixtures/diag-helper.js b/tests/fixtures/diag-helper.js new file mode 100644 index 0000000..5ac8e7f --- /dev/null +++ b/tests/fixtures/diag-helper.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import fs from "node:fs"; + +const [mode, ...args] = process.argv.slice(2); + +if (mode === "emit") { + process.stdout.write(`${args[0]}\n`); + process.stderr.write(`${args[1]}\n`); +} else if (mode === "exit") { + process.exit(Number(args[0])); +} else if (mode === "signal") { + const dir = args[0]; + process.on("SIGTERM", () => { + fs.writeFileSync(`${dir}/term.seen`, "term\n"); + process.exit(0); + }); + fs.writeFileSync(`${dir}/ready.seen`, "ready\n"); + setInterval(() => {}, 1000); +} else if (mode === "signal-delay") { + const dir = args[0]; + process.on("SIGTERM", () => { + fs.writeFileSync(`${dir}/term.seen`, "term\n"); + setTimeout(() => { + fs.writeFileSync(`${dir}/done.seen`, "done\n"); + process.exit(0); + }, 1200); + }); + fs.writeFileSync(`${dir}/ready.seen`, "ready\n"); + setInterval(() => {}, 1000); +} else { + process.stderr.write(`unknown mode: ${mode}\n`); + process.exit(2); +}