feat(envc): improve root help and add stdin hang repro script
Root usage documents commands; help/-h/--help and unknown-command paths print the same text. test_hang.sh reproduces blocking reads on stdin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+25
-2
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -13,15 +14,36 @@ var (
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
// printUsage writes the root help text to w.
|
||||
func printUsage(w io.Writer) {
|
||||
_, _ = io.WriteString(w, `envc — convert, query, and merge configuration across env, YAML, JSON, and INI.
|
||||
|
||||
Usage:
|
||||
envc <command> [arguments]
|
||||
|
||||
Commands:
|
||||
convert Read one source, normalize keys, write another format (--from, --to)
|
||||
get Read one source and print the value at a dot path (--from, --path)
|
||||
merge Deep-merge multiple sources in order, then write one output (--from, --to)
|
||||
version Print version and build metadata (also -v, --version)
|
||||
|
||||
Each command accepts its own flags; run:
|
||||
envc <command> -h
|
||||
`)
|
||||
}
|
||||
|
||||
func main() {
|
||||
stdin, stdout, stderr := os.Stdin, os.Stdout, os.Stderr
|
||||
args := os.Args[1:]
|
||||
if len(args) < 1 {
|
||||
_, _ = fmt.Fprintln(stderr, "usage: envc <convert|get|merge|version> [flags]")
|
||||
printUsage(stderr)
|
||||
os.Exit(2)
|
||||
}
|
||||
var code int
|
||||
switch args[0] {
|
||||
case "help", "-h", "--help":
|
||||
printUsage(stdout)
|
||||
code = 0
|
||||
case "convert":
|
||||
code = RunConvert(args[1:], stdin, stdout, stderr)
|
||||
case "get":
|
||||
@@ -31,7 +53,8 @@ func main() {
|
||||
case "version", "--version", "-v":
|
||||
_, _ = fmt.Fprintf(stdout, "envc %s (commit %s, built %s)\n", version, commit, date)
|
||||
default:
|
||||
_, _ = fmt.Fprintf(stderr, "unknown command %q\n", args[0])
|
||||
_, _ = fmt.Fprintf(stderr, "unknown command %q\n\n", args[0])
|
||||
printUsage(stderr)
|
||||
code = 2
|
||||
}
|
||||
os.Exit(code)
|
||||
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce and explain envc "hangs": default input is stdin (-); io.ReadAll
|
||||
# blocks until EOF. A TTY waits for Ctrl-D; /dev/zero never EOF (infinite read).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ENVC="${TMPDIR:-/tmp}/envc-hang-test.$$"
|
||||
cleanup() { rm -f "$ENVC"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "== build envc -> $ENVC"
|
||||
go build -o "$ENVC" ./cmd/envc
|
||||
|
||||
echo
|
||||
echo "== 1) expected to finish quickly (YAML on stdin, explicit -)"
|
||||
printf 'k: 1\n' | "$ENVC" convert --from yaml --to json --input - --output - | head -c 200
|
||||
echo
|
||||
echo " (ok)"
|
||||
|
||||
echo
|
||||
echo "== 2) empty stdin should finish quickly (EOF immediately)"
|
||||
if ! out="$( "$ENVC" convert --from yaml --to json --input - --output - </dev/null 2>&1 )"; then
|
||||
echo " exit non-zero (often empty YAML parse); stderr/out:"
|
||||
echo "$out" | sed 's/^/ | /'
|
||||
echo " (ok — no hang, process terminated)"
|
||||
else
|
||||
echo "$out" | sed 's/^/ /'
|
||||
echo " (ok — no hang)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== 3) infinite stdin: should hit wall-clock limit (GNU timeout -> 124)"
|
||||
if ! command -v timeout >/dev/null; then
|
||||
echo " skip: no 'timeout' in PATH"
|
||||
else
|
||||
set +e
|
||||
timeout -v 2s "$ENVC" convert --from yaml --to json --input - --output - </dev/zero >/dev/null 2>&1
|
||||
ec=$?
|
||||
set -e
|
||||
if [[ "$ec" -eq 124 ]]; then
|
||||
echo " timeout fired (exit 124) — binary was blocked in read (no EOF on stdin)."
|
||||
elif [[ "$ec" -eq 137 ]] || [[ "$ec" -eq 143 ]]; then
|
||||
echo " killed by signal (exit $ec) — still indicates blocked read until SIGKILL/SIGTERM."
|
||||
else
|
||||
echo " unexpected exit $ec (expected 124 from GNU timeout on hang)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== why it happens"
|
||||
cat <<'EOF'
|
||||
convert/get default to reading the input source once with io.ReadAll.
|
||||
For --input - (convert) or positional "-" (get), that is os.Stdin until EOF.
|
||||
|
||||
- Interactive shell, no pipe: stdin is a TTY — blocked until you press Ctrl-D
|
||||
or pipe data in.
|
||||
- /dev/zero (or any endless reader): never EOF — blocked until killed or OOM.
|
||||
|
||||
merge uses the same pattern for non-"-" inputs via bytesutil; URL fetches use
|
||||
a 30s HTTP client timeout (see internal/source.URL).
|
||||
|
||||
Fix for users: pass a file or URL (--input path), or pipe finite data into stdin.
|
||||
EOF
|
||||
|
||||
echo
|
||||
echo "== done"
|
||||
Reference in New Issue
Block a user