use std::io::Read;
use std::os::fd::AsFd;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use nix::fcntl::{FcntlArg, FdFlag, fcntl};
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::unistd::pipe;
#[test]
fn dbg_start_releases_parent_stdout_pipe() {
let (read_end, write_end) = pipe().expect("pipe");
use std::os::fd::AsRawFd;
fcntl(read_end.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC)).expect("cloexec r");
fcntl(write_end.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC)).expect("cloexec w");
let mut child = Command::new(env!("CARGO_BIN_EXE_dbg"))
.env("DBG_DETACH_SELF_TEST", "1")
.stdin(Stdio::null())
.stdout(Stdio::from(write_end))
.stderr(Stdio::null())
.spawn()
.expect("spawn dbg");
let wait_started = Instant::now();
let status = loop {
match child.try_wait().expect("try_wait") {
Some(s) => break s,
None if wait_started.elapsed() > Duration::from_secs(2) => {
let _ = child.kill();
panic!("dbg fork-parent did not exit within 2s");
}
None => std::thread::sleep(Duration::from_millis(25)),
}
};
assert!(status.success(), "dbg exited non-zero: {status:?}");
let fd = read_end.as_fd();
let mut fds = [PollFd::new(fd, PollFlags::POLLIN | PollFlags::POLLHUP)];
let ready = poll(&mut fds, PollTimeout::from(1000u16)).expect("poll");
assert!(
ready > 0,
"pipe did not EOF within 1s after dbg exited — \
the grandchild daemon still holds fd 1, which is exactly the \
bug that makes Claude Code's Bash tool hang or reap the daemon"
);
let mut f = std::fs::File::from(read_end);
let mut buf = [0u8; 64];
let n = f.read(&mut buf).expect("read");
assert_eq!(
n, 0,
"expected EOF, got {n} bytes: {:?}",
&buf[..n.min(buf.len())]
);
}