1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/// Define a private namespace for all its items.
#[ allow( clippy ::std_instead_of_alloc, clippy ::std_instead_of_core ) ]
mod private
{
use std ::io ::{ self, Write };
use std ::path ::{ Path, PathBuf };
use former ::Former;
///
/// Writes a PID to a file.
///
/// # Arguments
/// - `path` — Destination file path.
/// - `pid` — Process ID to persist.
///
/// # Errors
///
/// Returns `Err` if the file cannot be created or written.
///
/// # Examples
///
/// ```rust,no_run
/// use process_tools ::lifecycle ::daemon;
///
/// daemon ::write_pidfile( std ::path ::Path ::new( "/tmp/test.pid" ), 12345 ).unwrap();
/// ```
///
pub fn write_pidfile( path : &Path, pid : u32 ) -> io ::Result< () >
{
std ::fs ::write( path, pid.to_string() )
}
///
/// Reads a PID from a file.
///
/// The file must contain a decimal integer optionally surrounded by whitespace.
///
/// # Arguments
/// - `path` — Path to the PID file.
///
/// # Errors
///
/// Returns `Err` if the file cannot be read or does not contain a valid integer.
///
/// # Examples
///
/// ```rust,no_run
/// use process_tools ::lifecycle ::daemon;
///
/// let pid = daemon ::read_pidfile( std ::path ::Path ::new( "/tmp/test.pid" ) ).unwrap();
/// ```
///
pub fn read_pidfile( path : &Path ) -> io ::Result< u32 >
{
let content = std ::fs ::read_to_string( path )?;
content.trim().parse().map_err( | e |
{
io ::Error ::new( io ::ErrorKind ::InvalidData, format!( "invalid PID in file: {e}" ) )
})
}
///
/// Removes a PID file.
///
/// # Arguments
/// - `path` — Path to the PID file to delete.
///
/// # Errors
///
/// Returns `Err` if the file does not exist or cannot be removed.
///
/// # Examples
///
/// ```rust,no_run
/// use process_tools ::lifecycle ::daemon;
///
/// daemon ::remove_pidfile( std ::path ::Path ::new( "/tmp/test.pid" ) ).unwrap();
/// ```
///
pub fn remove_pidfile( path : &Path ) -> io ::Result< () >
{
std ::fs ::remove_file( path )
}
///
/// Configuration for the [`daemonize`] function.
///
#[ derive( Debug, Former ) ]
pub struct DaemonizeOptions
{
/// Path to write the daemon PID file. Skipped when `None`.
pid_file : Option< PathBuf >,
/// Working directory after daemonization. Defaults to `/`.
#[ former( default = PathBuf ::from( "/" ) ) ]
working_dir : PathBuf,
}
///
/// Daemonizes the current process using the POSIX double-fork pattern.
///
/// After a successful return the calling code runs in a fully detached
/// daemon process. The original (parent) process has already exited.
///
/// # Known Pitfalls (from wplan source audit)
///
/// ## Pitfall 1 — TOCTOU race in singleton check
/// Multiple daemon instances can start simultaneously if the PID file
/// check and write are not atomic. This implementation uses
/// `flock(LOCK_EX | LOCK_NB)` before any file mutation.
///
/// ## Pitfall 2 — Truncate-before-lock
/// Truncating the PID file before acquiring the lock allows concurrent
/// children to both see an empty file. This implementation acquires the
/// lock first, then truncates and writes.
///
/// ## Pitfall 3 — PID verification after IPC
/// The parent may observe a socket created by a *different* daemon child.
/// Callers must verify the PID file contains the expected child PID after
/// observing readiness signals.
///
/// ## Pitfall 4 — FD closure vs redirection
/// Closing stderr (fd 2) allows client sockets to reuse that descriptor,
/// causing `eprintln!()` to write to sockets. This implementation
/// redirects stdin / stdout / stderr to `/dev/null` instead.
///
/// ## Pitfall 5 — Inherited FD leak
/// Inherited pipe FDs from the parent's `Command ::output()` are never
/// closed in the daemon child, causing the parent to hang in nextest.
/// This implementation closes all FDs from 3 to `sysconf(_SC_OPEN_MAX)`.
///
/// # Errors
///
/// Returns `Err` if `fork`, `setsid`, or file-descriptor operations fail,
/// or if another daemon instance already holds the PID file lock.
///
/// # Examples
///
/// ```rust,no_run
/// # #[ cfg( unix ) ]
/// # {
/// use process_tools ::lifecycle ::daemon;
///
/// let opts = daemon ::DaemonizeOptions ::former()
/// .pid_file( "/var/run/mydaemon.pid" )
/// .form();
/// daemon ::daemonize( &opts ).expect( "daemonization failed" );
/// // — running in daemon process now —
/// # }
/// ```
///
#[ cfg( unix ) ]
#[ allow( unsafe_code ) ]
pub fn daemonize( options : &DaemonizeOptions ) -> io ::Result< () >
{
// --- First fork: detach from controlling terminal ---
// SAFETY: fork() is a standard POSIX call. The process state is
// well-defined at this point (single-threaded recommended).
match unsafe { libc ::fork() }
{
-1 => return Err( io ::Error ::last_os_error() ),
0 => {} // child continues
_ =>
{
// SAFETY: _exit() terminates without running atexit handlers,
// preventing double-flush of stdio buffers shared with the child.
unsafe { libc ::_exit( 0 ) };
}
}
// --- New session: become session leader ---
// SAFETY: setsid() is a standard POSIX call, valid after fork.
if unsafe { libc ::setsid() } == -1
{
return Err( io ::Error ::last_os_error() );
}
// --- Second fork: prevent reacquiring a controlling terminal ---
// SAFETY: Same as the first fork.
match unsafe { libc ::fork() }
{
-1 => return Err( io ::Error ::last_os_error() ),
0 => {} // grandchild continues (this is the daemon)
_ =>
{
// SAFETY: Same _exit rationale.
unsafe { libc ::_exit( 0 ) };
}
}
// --- Change working directory ---
std ::env ::set_current_dir( &options.working_dir )?;
// --- Reset umask ---
// SAFETY: umask() is a trivial POSIX call with no failure mode.
unsafe { libc ::umask( 0 ) };
// --- Pitfall 4: Redirect stdin/stdout/stderr to /dev/null ---
redirect_std_fds()?;
// --- Pitfall 5: Close inherited FDs ---
close_inherited_fds();
// --- Pitfall 1 & 2: Write PID file with flock ---
if let Some( ref pid_file ) = options.pid_file
{
write_pidfile_locked( pid_file )?;
}
Ok( () )
}
/// Redirects stdin, stdout, stderr to `/dev/null`.
///
/// Pitfall 4 fix: redirect instead of close — prevents fd reuse by
/// sockets, which would corrupt application I/O.
#[ cfg( unix ) ]
#[ allow( unsafe_code ) ]
fn redirect_std_fds() -> io ::Result< () >
{
use std ::os ::unix ::io ::AsRawFd;
let dev_null = std ::fs ::OpenOptions ::new()
.read( true )
.write( true )
.open( "/dev/null" )?;
let fd = dev_null.as_raw_fd();
// SAFETY: dup2 duplicates a valid open fd to the target descriptor.
// fds 0, 1, 2 are well-known standard descriptors.
unsafe
{
libc ::dup2( fd, libc ::STDIN_FILENO );
libc ::dup2( fd, libc ::STDOUT_FILENO );
libc ::dup2( fd, libc ::STDERR_FILENO );
}
// `dev_null` is dropped here, closing the original fd — the dup'd
// descriptors 0/1/2 remain valid as independent duplicates.
Ok( () )
}
/// Closes all file descriptors from 3 to `sysconf(_SC_OPEN_MAX)`.
///
/// Pitfall 5 fix: prevents inherited pipe FDs from keeping parent
/// processes blocked on read.
#[ cfg( unix ) ]
#[ allow( unsafe_code ) ]
fn close_inherited_fds()
{
// SAFETY: sysconf(_SC_OPEN_MAX) is a read-only query.
let max_fd = unsafe { libc ::sysconf( libc ::_SC_OPEN_MAX ) };
let max_fd = if max_fd <= 0 { 1024_i64 } else { max_fd };
for fd in 3 ..i32 ::try_from( max_fd ).unwrap_or( 1024 )
{
// SAFETY: close() on an already-closed fd returns -1 but is harmless.
unsafe { libc ::close( fd ) };
}
}
/// Writes the current PID to a file with an exclusive non-blocking lock.
///
/// Pitfall 1 fix: `flock(LOCK_EX | LOCK_NB)` prevents TOCTOU races.
/// Pitfall 2 fix: lock acquired before truncation.
#[ cfg( unix ) ]
#[ allow( unsafe_code ) ]
fn write_pidfile_locked( path : &Path ) -> io ::Result< () >
{
use std ::os ::unix ::io ::AsRawFd;
let file = std ::fs ::OpenOptions ::new()
.create( true )
.write( true )
.truncate( false ) // Pitfall 2: never truncate before locking
.open( path )?;
// SAFETY: flock() is a standard POSIX call on a valid fd.
let ret = unsafe
{
libc ::flock( file.as_raw_fd(), libc ::LOCK_EX | libc ::LOCK_NB )
};
if ret == -1
{
return Err( io ::Error ::new
(
io ::ErrorKind ::AlreadyExists,
"another daemon instance holds the PID file lock",
));
}
// Now safe to truncate and write while holding the lock.
file.set_len( 0 )?;
write!( &file, "{}", std ::process ::id() )?;
// Intentionally leak the File handle so the flock is held for the
// daemon's entire lifetime. The OS releases it on process exit.
std ::mem ::forget( file );
Ok( () )
}
}
#[ cfg( unix ) ]
crate ::mod_interface!
{
own use write_pidfile;
own use read_pidfile;
own use remove_pidfile;
own use DaemonizeOptions;
own use daemonize;
}