@@ -148,27 +148,28 @@ pub enum Fork {
148148 Child ,
149149}
150150
151- /// Close a file descriptor, retrying on `EINTR` and treating `EBADF` as success.
151+ /// Close a file descriptor without retrying on `EINTR`, treating `EBADF` as success.
152+ ///
153+ /// On Linux, `close()` always releases the fd before returning `EINTR`, so retrying
154+ /// would risk closing an unrelated fd opened by another thread. On FreeBSD, macOS,
155+ /// and other Unixes the fd state after `EINTR` is unspecified (POSIX 2008+, Austin
156+ /// Group defect 529), making retry equally unsafe. The safe portable behavior is to
157+ /// call `close()` exactly once and treat `EINTR` as success — the same approach used
158+ /// by Rust's stdlib, Go's runtime, and glibc internals.
152159#[ inline]
153- fn close_retry ( fd : libc:: c_int ) -> io:: Result < ( ) > {
154- loop {
155- let res = unsafe { libc:: close ( fd) } ;
156- if res == 0 {
157- return Ok ( ( ) ) ;
158- }
159-
160- let err = io:: Error :: last_os_error ( ) ;
161-
162- if err. kind ( ) == io:: ErrorKind :: Interrupted {
163- continue ;
164- }
160+ fn close_once ( fd : libc:: c_int ) -> io:: Result < ( ) > {
161+ let res = unsafe { libc:: close ( fd) } ;
162+ if res == 0 {
163+ return Ok ( ( ) ) ;
164+ }
165165
166- if err. raw_os_error ( ) == Some ( libc:: EBADF ) {
167- return Ok ( ( ) ) ;
168- }
166+ let err = io:: Error :: last_os_error ( ) ;
169167
170- return Err ( err) ;
168+ if err. kind ( ) == io:: ErrorKind :: Interrupted || err. raw_os_error ( ) == Some ( libc:: EBADF ) {
169+ return Ok ( ( ) ) ;
171170 }
171+
172+ Err ( err)
172173}
173174
174175impl Fork {
@@ -307,7 +308,7 @@ pub fn chdir() -> io::Result<()> {
307308/// ```
308309pub fn close_fd ( ) -> io:: Result < ( ) > {
309310 for fd in 0 ..=2 {
310- close_retry ( fd) ?;
311+ close_once ( fd) ?;
311312 }
312313
313314 Ok ( ( ) )
@@ -377,7 +378,7 @@ pub fn redirect_stdio() -> io::Result<()> {
377378 // Only close null_fd if it's > 2 (not one of the stdio fds we're duplicating to)
378379 // If null_fd was 0, 1, or 2, we're in the process of duping it, so don't close
379380 if null_fd > 2 {
380- let _ = close_retry ( null_fd) ;
381+ let _ = close_once ( null_fd) ;
381382 }
382383 return Err ( err) ;
383384 }
@@ -388,7 +389,7 @@ pub fn redirect_stdio() -> io::Result<()> {
388389 // Close the extra fd if it's > 2
389390 // (if null_fd was 0, 1, or 2, it's now dup'd to all three, so don't close)
390391 if null_fd > 2 {
391- close_retry ( null_fd) ?;
392+ close_once ( null_fd) ?;
392393 }
393394
394395 Ok ( ( ) )
@@ -710,12 +711,51 @@ pub fn getppid() -> libc::pid_t {
710711/// * `nochdir = false`, changes the current working directory to the root (`/`).
711712/// * `noclose = false`, redirects stdin, stdout, and stderr to `/dev/null`
712713///
714+ /// # Return Value
715+ ///
716+ /// This function only ever returns in the **daemon (grandchild) process**:
717+ ///
718+ /// - `Ok(Fork::Child)` — You are the daemon. The original process and the
719+ /// intermediate child have already exited via `_exit(0)`.
720+ /// - `Err(...)` — A system call failed before the daemon could be created.
721+ ///
722+ /// **`Ok(Fork::Parent(_))` is never returned** because both parent processes
723+ /// call `_exit(0)` internally. You do not need to match on it:
724+ ///
725+ /// ```no_run
726+ /// use fork::{daemon, Fork};
727+ ///
728+ /// // Recommended: use `if let` — no dead Parent arm needed
729+ /// if let Ok(Fork::Child) = daemon(false, false) {
730+ /// // Only the daemon reaches here
731+ /// loop {
732+ /// // daemon work…
733+ /// std::thread::sleep(std::time::Duration::from_secs(60));
734+ /// }
735+ /// }
736+ /// ```
737+ ///
738+ /// If you prefer `match` for explicit error handling, mark the parent arm
739+ /// unreachable:
740+ ///
741+ /// ```no_run
742+ /// use fork::{daemon, Fork};
743+ ///
744+ /// match daemon(false, false) {
745+ /// Ok(Fork::Child) => {
746+ /// // daemon work…
747+ /// }
748+ /// Ok(Fork::Parent(_)) => unreachable!("daemon() exits both parent processes"),
749+ /// Err(err) => eprintln!("daemon failed: {err}"),
750+ /// }
751+ /// ```
752+ ///
713753/// # Implementation (double-fork)
714754///
715- /// 1. **First fork** - Parent exits immediately.
716- /// 2. **Session setup** - Child calls `setsid()`, optionally `chdir("/")`, and optionally redirects stdio.
717- /// 3. **Second (double) fork** - Session-leader child exits immediately.
718- /// 4. **Daemon continues** - Grandchild (daemon) runs with no controlling terminal.
755+ /// 1. **First fork** — Parent calls `_exit(0)` immediately.
756+ /// 2. **Session setup** — Child calls `setsid()`, optionally `chdir("/")`, and optionally redirects stdio.
757+ /// 3. **Second (double) fork** — Session-leader child calls `_exit(0)` immediately.
758+ /// 4. **Daemon continues** — Grandchild (daemon) runs with no controlling terminal.
719759///
720760/// # Behavior Change in v0.4.0
721761///
@@ -750,7 +790,7 @@ pub fn getppid() -> libc::pid_t {
750790/// .expect("failed to execute process");
751791///}
752792///```
753- #[ must_use = "daemon result must be checked to determine if this is the daemon process " ]
793+ #[ must_use = "daemon() only returns Ok(Fork::Child) in the daemon process; check the result " ]
754794pub fn daemon ( nochdir : bool , noclose : bool ) -> io:: Result < Fork > {
755795 // 1. First fork: detach from original parent; parent exits immediately
756796 match fork ( ) ? {
@@ -1119,19 +1159,19 @@ mod tests {
11191159 }
11201160
11211161 #[ test]
1122- fn test_close_retry_ok_and_ebadf ( ) {
1162+ fn test_close_once_ok_and_ebadf ( ) {
11231163 // Create a pipe to obtain valid fds
11241164 let mut fds = [ 0 ; 2 ] ;
11251165 assert_eq ! ( unsafe { libc:: pipe( & raw mut fds[ 0 ] ) } , 0 ) ;
11261166
1127- // Close write end via close_retry (should succeed)
1128- close_retry ( fds[ 1 ] ) . expect ( "close_retry should close valid fd" ) ;
1167+ // Close write end via close_once (should succeed)
1168+ close_once ( fds[ 1 ] ) . expect ( "close_once should close valid fd" ) ;
11291169
11301170 // Wrap read end in File to close it once; drop immediately
11311171 let read_fd = fds[ 0 ] ;
11321172 unsafe { std:: fs:: File :: from_raw_fd ( read_fd) } ;
11331173
11341174 // Second close should be treated as success (EBADF path)
1135- close_retry ( read_fd) . expect ( "EBADF should be treated as success" ) ;
1175+ close_once ( read_fd) . expect ( "EBADF should be treated as success" ) ;
11361176 }
11371177}
0 commit comments