Skip to content

diag: trigger on this branch, since workflow_dispatch needs the defau… #1

diag: trigger on this branch, since workflow_dispatch needs the defau…

diag: trigger on this branch, since workflow_dispatch needs the defau… #1

name: Diag pipe holder
# THROWAWAY. This exists to answer one question and then be deleted: when the
# conformance run finishes but its output pipe does not close, WHICH process is
# holding the write end?
#
# It runs only on its own throwaway branch. A workflow_dispatch alone is not
# dispatchable until the file exists on the default branch, which is exactly
# what this must not do.
on:
push:
branches: [diag/pipe-holder]
workflow_dispatch:
jobs:
diag:
runs-on: ubuntu-latest
# The work finishes in under three minutes. Anything past that is the hang,
# and there is no reason to buy 45 minutes of it again.
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/determinate-nix-action@v3
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build the Node pty at the pinned commit
run: |
ref=$(cat crates/pty-conformance/node-ref)
git clone --filter=blob:none https://github.com/compoundingtech/pty /tmp/node-pty
git -C /tmp/node-pty checkout --detach "$ref"
(cd /tmp/node-pty && npm ci --silent && npm run build --silent)
- name: Build
run: nix develop --command cargo build --workspace --release
- name: Catch the pipe holder
env:
PTY_NODE_CHECKOUT: /tmp/node-pty
run: |
set -u
mkdir -p target/diag
DIAG=target/diag/holders.txt
PIPE=/tmp/diagpipe
# Every finding is appended to a FILE, as it happens.
#
# Last night's instrumentation lost its evidence twice: `tail`
# buffers until a command ends and a killed command never ends, and
# `tee` could not open its output file. A diagnostic whose whole
# subject is "something is holding a pipe open" must not itself
# depend on a pipe closing, or on surviving to the end.
say() { echo "$(date -u +%H:%M:%S) $*" | tee -a "$DIAG"; }
# A NAMED pipe, so a holder is identifiable by the symlink target of
# its file descriptor rather than by matching inodes.
rm -f "$PIPE"; mkfifo "$PIPE"
cat "$PIPE" > /dev/null &
reader=$!
say "reader pid $reader on $PIPE"
files=$(ls crates/pty-conformance/tests/*.rs \
| xargs -n1 basename | sed 's/\.rs$//' \
| awk 'NR % 4 == 2' | tr '\n' ' ')
say "files: $files"
nix develop --command ./scripts/conformance-both.sh \
--node /tmp/node-pty/bin/pty \
--rust "$PWD/target/release/pty" \
--out "$PWD/target/conformance" $files > "$PIPE" 2>&1 &
script=$!
say "script pid $script"
wait "$script" || say "script exited non-zero ($?)"
say "SCRIPT HAS EXITED. The question is whether the pipe closes now."
# Give it five minutes, looking every fifteen seconds. If the reader
# is still alive the write end is still held, and the holders are
# named right here.
for i in $(seq 1 20); do
if ! kill -0 "$reader" 2>/dev/null; then
say "reader exited after ${i} checks — pipe closed, NOT reproduced"
exit 0
fi
say "--- check $i: reader still alive, scanning for holders ---"
for fd in /proc/[0-9]*/fd/*; do
tgt=$(readlink "$fd" 2>/dev/null) || continue
case "$tgt" in
"$PIPE")
pid=$(echo "$fd" | cut -d/ -f3)
[ "$pid" = "$reader" ] && continue
exe=$(readlink "/proc/$pid/exe" 2>/dev/null || echo '?')
cl=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | cut -c1-140)
st=$(awk '/^PPid:|^State:/{printf "%s ", $2}' "/proc/$pid/status" 2>/dev/null)
say " HOLDER pid=$pid fd=${fd##*/} state/ppid=$st exe=$exe"
say " cmdline=$cl"
;;
esac
done
sleep 15
done
say "REPRODUCED: pipe still open after 5 minutes. Holders are listed above."
kill "$reader" 2>/dev/null || true
exit 1
- name: Keep the findings
if: always()
uses: actions/upload-artifact@v4
with:
name: pipe-holders
path: target/diag/
if-no-files-found: error