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
311
312
313
314
315
316
317
318
//! Daemon-specific integration tests
//!
//! This module tests the `daemon()` function which creates a detached background process.
//! These tests verify:
//! - Process detachment and proper PID management
//! - Directory handling (chdir vs nochdir)
//! - Process group and session management
//! - File descriptor handling (noclose option)
//! - Command execution in daemon context
//! - Absence of controlling terminal
//!
//! Note: These tests fork twice (the daemon pattern) so they run in separate
//! processes to avoid terminating the test runner when `daemon()` calls `exit(0)`.
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
#![allow(clippy::match_wild_err_arm)]
#![allow(clippy::similar_names)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::indexing_slicing)]
mod common;
use std::{
env, fs,
process::{Command, exit},
};
use fork::{Fork, daemon, fork};
use common::{get_unique_test_dir, setup_test_dir, wait_for_file};
#[test]
fn test_daemon_creates_detached_process() {
// Tests that daemon() successfully creates a detached background process
// Expected behavior:
// 1. Parent process forks
// 2. First child creates new session and forks again
// 3. First child exits (daemon() calls exit(0))
// 4. Grandchild (daemon) is detached and writes its PID
// 5. Daemon changes to root directory (nochdir=false)
// 6. Daemon has valid PID > 0
let test_dir = setup_test_dir(get_unique_test_dir("daemon_creates_detached"));
let marker_file = test_dir.join("daemon.marker");
// Fork the test to avoid daemon() calling exit(0) on parent
match fork().expect("Failed to fork") {
Fork::Parent(_) => {
// Parent waits for marker file to be created
assert!(
wait_for_file(&marker_file, 500),
"Daemon should have created marker file"
);
// Read PID from marker file
let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
let daemon_pid: i32 = content.trim().parse().expect("Failed to parse PID");
assert!(daemon_pid > 0, "Daemon PID should be positive");
// Cleanup
let _ = fs::remove_dir_all(&test_dir);
}
Fork::Child => {
// Child calls daemon()
if let Ok(Fork::Child) = daemon(false, true) {
// This is the daemon process
// Write our PID to marker file
let pid = unsafe { libc::getpid() };
fs::write(&marker_file, format!("{}", pid)).expect("Failed to write marker file");
// Verify we're in root directory
let current = env::current_dir().expect("Failed to get current dir");
assert_eq!(current.to_str(), Some("/"));
exit(0);
}
// Parent of daemon exits (daemon() calls exit(0) for us)
}
}
}
#[test]
fn test_daemon_with_nochdir() {
// Tests that daemon(nochdir=true) preserves the current working directory
// Expected behavior:
// 1. Test changes to a specific directory before calling daemon()
// 2. daemon(true, true) is called (nochdir=true, noclose=true)
// 3. Daemon process should remain in the same directory (not /)
// 4. Daemon writes current directory to file for verification
let test_dir = setup_test_dir(get_unique_test_dir("daemon_nochdir"));
let marker_file = test_dir.join("nochdir.marker");
// Change to test directory
env::set_current_dir(&test_dir).expect("Failed to change directory");
match fork().expect("Failed to fork") {
Fork::Parent(_) => {
assert!(
wait_for_file(&marker_file, 500),
"Daemon should have created marker file"
);
// Cleanup
let _ = fs::remove_dir_all(&test_dir);
}
Fork::Child => {
if let Ok(Fork::Child) = daemon(true, true) {
// Daemon with nochdir=true should preserve directory
let current = env::current_dir().expect("Failed to get current dir");
// Write confirmation to marker file
fs::write(&marker_file, format!("{}", current.display()))
.expect("Failed to write marker file");
// Directory should still be test_dir, not root
assert_ne!(current.to_str(), Some("/"));
exit(0);
}
}
}
}
#[test]
fn test_daemon_process_group() {
// Tests that daemon creates proper process group structure
// Expected behavior:
// 1. daemon() performs double-fork pattern
// 2. After double-fork, daemon is NOT a session leader (PID != PGID)
// 3. This prevents daemon from acquiring a controlling terminal
// 4. Both PID and PGID are positive values
// 5. Daemon writes PID,PGID to file for verification
let test_dir = setup_test_dir(get_unique_test_dir("daemon_process_group"));
let marker_file = test_dir.join("pgid.marker");
match fork().expect("Failed to fork") {
Fork::Parent(_) => {
assert!(
wait_for_file(&marker_file, 500),
"Daemon should have created marker file"
);
// Read and verify process group info
let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
let parts: Vec<&str> = content.trim().split(',').collect();
assert_eq!(parts.len(), 2);
let pid: i32 = parts[0].parse().expect("Failed to parse PID");
let pgid: i32 = parts[1].parse().expect("Failed to parse PGID");
// Daemon (after double-fork) should NOT be session leader
// but should be in a new process group
assert!(pid > 0, "PID should be positive");
assert!(pgid > 0, "PGID should be positive");
assert_ne!(
pid, pgid,
"Daemon (after double-fork) should NOT be session leader"
);
// Cleanup
let _ = fs::remove_dir_all(&test_dir);
}
Fork::Child => {
if let Ok(Fork::Child) = daemon(false, true) {
let pid = unsafe { libc::getpid() };
let pgid = unsafe { libc::getpgrp() };
fs::write(&marker_file, format!("{},{}", pid, pgid))
.expect("Failed to write marker file");
exit(0);
}
}
}
}
#[test]
fn test_daemon_with_command_execution() {
// Tests that daemon can execute commands successfully
// Expected behavior:
// 1. Daemon process is created
// 2. Daemon executes a shell command
// 3. Command output is written to a file
// 4. Parent can verify command executed correctly
// 5. Tests real-world daemon usage pattern
let test_dir = setup_test_dir(get_unique_test_dir("daemon_command_exec"));
let output_file = test_dir.join("command.output");
match fork().expect("Failed to fork") {
Fork::Parent(_) => {
assert!(
wait_for_file(&output_file, 500),
"Command output file should exist"
);
let content = fs::read_to_string(&output_file).expect("Failed to read output file");
assert!(
content.contains("hello from daemon"),
"Output should contain expected text"
);
// Cleanup
let _ = fs::remove_dir_all(&test_dir);
}
Fork::Child => {
if let Ok(Fork::Child) = daemon(false, true) {
// Execute a command in the daemon
Command::new("sh")
.arg("-c")
.arg(format!(
"echo 'hello from daemon' > {}",
output_file.display()
))
.output()
.expect("Failed to execute command");
exit(0);
}
}
}
}
#[test]
fn test_daemon_no_controlling_terminal() {
// Tests that daemon has no controlling terminal
// Expected behavior:
// 1. Daemon process is created via double-fork + setsid()
// 2. Daemon tries to open /dev/tty (the POSIX controlling terminal device)
// 3. open() should fail because the daemon has no controlling terminal
// 4. This confirms daemon is properly detached
// 5. Critical for background service behavior
let test_dir = setup_test_dir(get_unique_test_dir("daemon_no_tty"));
let tty_file = test_dir.join("tty.info");
match fork().expect("Failed to fork") {
Fork::Parent(_) => {
assert!(wait_for_file(&tty_file, 500), "TTY info file should exist");
let content = fs::read_to_string(&tty_file).expect("Failed to read tty file");
assert_eq!(
content.trim(),
"no_ctty",
"Daemon should have no controlling terminal, got: {}",
content
);
// Cleanup
let _ = fs::remove_dir_all(&test_dir);
}
Fork::Child => {
if let Ok(Fork::Child) = daemon(false, true) {
// The POSIX way to check for a controlling terminal:
// opening /dev/tty fails when the process has none.
let fd =
unsafe { libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
let result = if fd == -1 {
"no_ctty"
} else {
unsafe { libc::close(fd) };
"has_ctty"
};
fs::write(&tty_file, result).expect("Failed to write tty file");
exit(0);
}
}
}
}
#[test]
fn test_daemon_never_returns_parent() {
// Tests that daemon() never returns Ok(Fork::Parent(_))
// Expected behavior:
// 1. daemon() performs double-fork internally
// 2. Both parent processes call _exit(0) and never return
// 3. Only Ok(Fork::Child) is ever returned to the caller
// 4. The daemon writes "child" to a marker file to confirm
// 5. If Fork::Parent were ever returned, "parent" would be written instead
let test_dir = setup_test_dir(get_unique_test_dir("daemon_never_returns_parent"));
let marker_file = test_dir.join("result.marker");
match fork().expect("Failed to fork") {
Fork::Parent(_) => {
assert!(
wait_for_file(&marker_file, 500),
"Result marker file should exist"
);
let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
assert_eq!(
content.trim(),
"child",
"daemon() should only return Fork::Child, never Fork::Parent"
);
// Cleanup
let _ = fs::remove_dir_all(&test_dir);
}
Fork::Child => {
match daemon(false, true) {
Ok(Fork::Child) => {
fs::write(&marker_file, "child").expect("Failed to write marker");
exit(0);
}
Ok(Fork::Parent(_)) => {
// This arm should be unreachable
fs::write(&marker_file, "parent").expect("Failed to write marker");
exit(1);
}
Err(_) => {
fs::write(&marker_file, "error").expect("Failed to write marker");
exit(2);
}
}
}
}
}