From 16d4fbb1b6d7d429c4508c6bd85b50a4bdd89c35 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Mon, 22 Jun 2026 14:38:39 -0500 Subject: [PATCH] Add retry sleep support to disag.js --- disag.js | 71 ++++++++++++++++++++++++++++++++--- tests/disag-js.rc | 11 +++++- tests/fixtures/diag-helper.js | 14 +++++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/disag.js b/disag.js index 721eb7c..18f18f0 100755 --- a/disag.js +++ b/disag.js @@ -15,14 +15,14 @@ const signalCodes = { function usage() { process.stderr.write( - "usage: diag [-z|--no-color] command [-f|--file file] [-n|--name name] [-r|--retry] [-s|--shell] ...\n", + "usage: diag [-z|--no-color] command [-f|--file file] [-n|--name name] [-r|--retry] [--sleep duration] [-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] [-r|--retry] [-s|--shell] ... + process.stdout.write(`usage: diag [-z|--no-color] command [-f|--file file] [-n|--name name] [-r|--retry] [--sleep duration] [-s|--shell] ... diag -h|--help Run one or more commands at the same time and mirror their output. @@ -39,12 +39,17 @@ Options: command with [name]; not written to log files -r, --retry retry the immediately preceding command until it exits with code 0 + --sleep duration + sleep before retrying the immediately preceding command; + bare numbers are seconds, ms and s suffixes are accepted, + defaults to 1s, and 0 disables sleeping -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, -r, and -s always apply to the command immediately before them. + -f, -n, -r, --sleep, 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. @@ -62,6 +67,8 @@ Examples: diag 'flaky_command' -r -n FLAKY + diag 'flaky_command' -r --sleep 250ms -n FLAKY + diag \\ 'while true; do echo hello; sleep 1; done' -s -f ./hello.log -n HELLO \\ 'some_command' -f ./some_command.log @@ -74,6 +81,18 @@ Examples: process.exit(0); } +function parseSleepDuration(value) { + if (value.trim() === "") { + usage(); + } + const match = value.match(/^((?:\d+(?:\.\d+)?)|(?:\.\d+))(ms|s)?$/); + if (match === null) { + usage(); + } + const amount = Number(match[1]); + return Math.round(amount * (match[2] === "ms" ? 1 : 1000)); +} + function parseArgs(args) { if (args.length > 0 && (args[0] === "-h" || args[0] === "--help")) { help(); @@ -116,6 +135,13 @@ function parseArgs(args) { current.shell = true; continue; } + if (arg === "--sleep") { + if (current === null || i + 1 >= args.length) { + usage(); + } + current.sleepMs = parseSleepDuration(args[++i]); + continue; + } if (arg === "-r" || arg === "--retry") { if (current === null) { usage(); @@ -124,7 +150,14 @@ function parseArgs(args) { continue; } finishCurrent(); - current = { command: arg, file: "", name: "", retry: false, shell: false }; + current = { + command: arg, + file: "", + name: "", + retry: false, + shell: false, + sleepMs: 1000, + }; } finishCurrent(); @@ -210,10 +243,17 @@ function colorize(line, color) { return `\x1b[${color}m${line}\x1b[0m`; } +function colorizeSystemLineWithLabel(prefix, label, suffix, spec) { + if (spec.systemColor === "" && spec.color === "") { + return `${prefix}${label}${suffix}`; + } + return `${colorize(prefix, spec.systemColor)}${colorize(label, spec.color)}${colorize(suffix, spec.systemColor)}`; +} + function reportExit(spec, status) { const label = spec.name !== "" ? spec.name : spec.command; process.stderr.write( - `${colorize(`[DISAG] ${label} exited with exit code ${status}`, spec.systemColor)}\n`, + `${colorizeSystemLineWithLabel("[DISAG] ", label, ` exited with exit code ${status}`, spec)}\n`, ); } @@ -224,6 +264,26 @@ function reportRetry(spec, status) { ); } +function sleep(ms, stopping) { + if (ms <= 0 || stopping()) { + return Promise.resolve(); + } + return new Promise((resolve) => { + let timeout = null; + const interval = setInterval(() => { + if (stopping()) { + clearTimeout(timeout); + clearInterval(interval); + resolve(); + } + }, Math.min(ms, 50)); + timeout = setTimeout(() => { + clearInterval(interval); + resolve(); + }, ms); + }); +} + function attachLineStream(readable, display, log, spec) { let pending = ""; @@ -331,6 +391,7 @@ function runCommand(spec) { return status; } reportRetry(spec, status); + await sleep(spec.sleepMs, spec.stopping); } })(); diff --git a/tests/disag-js.rc b/tests/disag-js.rc index b6f45a3..3d368d0 100755 --- a/tests/disag-js.rc +++ b/tests/disag-js.rc @@ -87,6 +87,7 @@ script -q $tmp/color.tty $diag 'echo color-test' -n COLOR >/dev/null || fail col awk 'BEGIN { ok = 0; pat = sprintf("%c[31m[DISAG]", 27) } index($0, pat) { ok = 1 } END { exit ok ? 0 : 1 }' $tmp/color.tty || fail color-system-red awk 'BEGIN { ok = 0; pat = sprintf("%c[32m[COLOR]", 27) } index($0, pat) { ok = 1 } END { exit ok ? 0 : 1 }' $tmp/color.tty || fail color-command-non-red awk 'BEGIN { bad = 0; pat = sprintf("%c[31m[COLOR]", 27) } index($0, pat) { bad = 1 } END { exit bad ? 1 : 0 }' $tmp/color.tty || fail color-command-red +awk 'BEGIN { ok = 0; pat = sprintf("%c[31m[DISAG] %c[0m%c[32mCOLOR%c[0m%c[31m exited with exit code 0", 27, 27, 27, 27, 27) } index($0, pat) { ok = 1 } END { exit ok ? 0 : 1 }' $tmp/color.tty || fail color-exit-label-command-color script -q $tmp/color-rotation.tty $diag 'echo one' -n C1 'echo two' -n C2 'echo three' -n C3 'echo four' -n C4 'echo five' -n C5 'echo six' -n C6 >/dev/null || fail color-rotation-run awk 'BEGIN { bad = 0; red = sprintf("%c[31m[C", 27); brightred = sprintf("%c[91m[C", 27) } index($0, red) || index($0, brightred) { bad = 1 } END { exit bad ? 1 : 0 }' $tmp/color-rotation.tty || fail color-rotation-red-command @@ -99,7 +100,7 @@ s=$status if(~ $s 0) fail status-zero -$diag 'node tests/fixtures/diag-helper.js flaky '^$tmp^'/retry.count' -r -n RETRY -f $tmp/retry.log > $tmp/retry.out >[2]$tmp/retry.err || fail retry-run +$diag 'node tests/fixtures/diag-helper.js flaky '^$tmp^'/retry.count' -r --sleep 0 -n RETRY -f $tmp/retry.log > $tmp/retry.out >[2]$tmp/retry.err || fail retry-run printf '%s\n' '[RETRY] attempt 1' '[RETRY] attempt 2' > $tmp/retry.out.want printf '%s\n' '[DISAG] RETRY exited with exit code 7' '[DISAG] RETRY retrying after exit code 7' '[DISAG] RETRY exited with exit code 0' > $tmp/retry.err.want printf '%s\n' 'attempt 2' > $tmp/retry.log.want @@ -109,7 +110,13 @@ assert_file $tmp/retry.err $tmp/retry.err.want retry-stderr assert_file $tmp/retry.log $tmp/retry.log.want retry-log-last-attempt assert_file $tmp/retry.count $tmp/retry.count.want retry-count -script -q $tmp/retry-color.tty $diag 'node tests/fixtures/diag-helper.js flaky '^$tmp^'/retry-color.count' -r -n RETRYCOLOR >/dev/null || fail retry-color-run +$diag 'node tests/fixtures/diag-helper.js flaky-time '^$tmp^'/sleep-default.count '^$tmp^'/sleep-default.times' -r -n SLEEPDEF > $tmp/sleep-default.out >[2]$tmp/sleep-default.err || fail sleep-default-run +awk 'NR == 1 { first = $1 } NR == 2 { second = $1 } END { exit second - first >= 900 ? 0 : 1 }' $tmp/sleep-default.times || fail sleep-default-duration + +$diag 'node tests/fixtures/diag-helper.js flaky-time '^$tmp^'/sleep-zero.count '^$tmp^'/sleep-zero.times' -r --sleep 0 -n SLEEPZERO > $tmp/sleep-zero.out >[2]$tmp/sleep-zero.err || fail sleep-zero-run +awk 'NR == 1 { first = $1 } NR == 2 { second = $1 } END { exit second - first < 900 ? 0 : 1 }' $tmp/sleep-zero.times || fail sleep-zero-duration + +script -q $tmp/retry-color.tty $diag 'node tests/fixtures/diag-helper.js flaky '^$tmp^'/retry-color.count' -r --sleep 0 -n RETRYCOLOR >/dev/null || fail retry-color-run awk 'BEGIN { ok = 0; pat = sprintf("%c[31m[DISAG] RETRYCOLOR retrying after exit code 7", 27) } index($0, pat) { ok = 1 } END { exit ok ? 0 : 1 }' $tmp/retry-color.tty || fail retry-color-red sigcmd='node tests/fixtures/diag-helper.js signal '^$tmp diff --git a/tests/fixtures/diag-helper.js b/tests/fixtures/diag-helper.js index 964f973..4d4e941 100644 --- a/tests/fixtures/diag-helper.js +++ b/tests/fixtures/diag-helper.js @@ -21,6 +21,20 @@ if (mode === "emit") { fs.writeFileSync(counter, `${attempts}\n`); process.stdout.write(`attempt ${attempts}\n`); process.exit(attempts < 2 ? 7 : 0); +} else if (mode === "flaky-time") { + const counter = args[0]; + const timings = args[1]; + let attempts = 0; + try { + attempts = Number(fs.readFileSync(counter, "utf8")); + } catch { + attempts = 0; + } + attempts += 1; + fs.writeFileSync(counter, `${attempts}\n`); + fs.appendFileSync(timings, `${Date.now()}\n`); + process.stdout.write(`attempt ${attempts}\n`); + process.exit(attempts < 2 ? 7 : 0); } else if (mode === "signal") { const dir = args[0]; process.on("SIGTERM", () => { -- 2.51.2