├── .github ├── FUNDING.yml └── workflows │ ├── .codespellrc │ ├── build.yml │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .justfile ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── example_daemon.rs ├── example_pipe.rs └── example_touch_pid.rs ├── src └── lib.rs └── tests ├── README.md ├── common └── mod.rs ├── daemon_tests.rs ├── fork_tests.rs └── integration_tests.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nbari 2 | -------------------------------------------------------------------------------- /.github/workflows/.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | ignore-words-list = crate 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test & Build 3 | 4 | on: 5 | push: 6 | branches: 7 | - '*' 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | test: 15 | uses: ./.github/workflows/test.yml 16 | 17 | build: 18 | name: Test build 19 | runs-on: ${{ matrix.os }} 20 | needs: test 21 | 22 | strategy: 23 | matrix: 24 | include: 25 | - build: linux 26 | os: ubuntu-latest 27 | target: x86_64-unknown-linux-musl 28 | 29 | - build: macos 30 | os: macos-latest 31 | target: x86_64-apple-darwin 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | - name: Get the release version from the tag 38 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 39 | 40 | - name: Install Rust 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | targets: ${{ matrix.target }} 44 | 45 | - name: Build 46 | run: | 47 | cargo build --release --target ${{ matrix.target }} 48 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deploy 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | test: 15 | uses: ./.github/workflows/test.yml 16 | 17 | build: 18 | name: Test build 19 | runs-on: ${{ matrix.os }} 20 | needs: test 21 | 22 | strategy: 23 | matrix: 24 | include: 25 | - build: linux 26 | os: ubuntu-latest 27 | target: x86_64-unknown-linux-musl 28 | 29 | - build: macos 30 | os: macos-latest 31 | target: x86_64-apple-darwin 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | - name: Get the release version from the tag 38 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 39 | 40 | - name: Install Rust 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | targets: ${{ matrix.target }} 44 | 45 | - name: Build 46 | run: | 47 | cargo build --release --target ${{ matrix.target }} 48 | 49 | publish: 50 | name: Publish 51 | runs-on: ubuntu-latest 52 | needs: 53 | - build 54 | steps: 55 | - name: Checkout sources 56 | uses: actions/checkout@v4 57 | 58 | - name: Install Rust 59 | uses: dtolnay/rust-toolchain@stable 60 | 61 | - run: cargo publish --token ${CRATES_TOKEN} 62 | env: 63 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 64 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: 5 | workflow_call: 6 | pull_request: 7 | branches: 8 | - '*' 9 | 10 | jobs: 11 | format: 12 | name: Format 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: dtolnay/rust-toolchain@stable 17 | 18 | - name: Format 19 | run: cargo fmt --all -- --check 20 | 21 | lint: 22 | name: Clippy 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@stable 27 | 28 | - name: Clippy 29 | run: cargo clippy -- -D clippy::all -D clippy::nursery -D warnings 30 | 31 | check: 32 | name: Check 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: dtolnay/rust-toolchain@stable 37 | 38 | - name: Check 39 | run: cargo check 40 | 41 | test: 42 | name: Test 43 | strategy: 44 | matrix: 45 | os: 46 | - ubuntu-latest 47 | - macOS-latest 48 | rust: 49 | - stable 50 | runs-on: ${{ matrix.os }} 51 | needs: 52 | - format 53 | - lint 54 | - check 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: dtolnay/rust-toolchain@stable 58 | 59 | - name: test 60 | run: cargo test 61 | 62 | - name: Cleanup orphaned test processes 63 | if: always() 64 | run: | 65 | # Kill any remaining test processes 66 | pkill -9 -f "fork.*debug/deps" || true 67 | # Clean up test directories 68 | rm -rf /tmp/fork_test* || true 69 | 70 | coverage: 71 | name: Coverage 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: dtolnay/rust-toolchain@nightly 76 | with: 77 | components: llvm-tools-preview 78 | 79 | - name: Run tests 80 | run: cargo test --verbose -- --nocapture 81 | env: 82 | RUST_BACKTRACE: full 83 | CARGO_INCREMENTAL: 0 84 | LLVM_PROFILE_FILE: coverage-%p-%m.profraw 85 | RUSTFLAGS: -Cinstrument-coverage -Ccodegen-units=1 -Clink-dead-code -Coverflow-checks=off 86 | RUSTDOCFLAGS: -Cinstrument-coverage -Ccodegen-units=1 -Clink-dead-code -Coverflow-checks=off 87 | 88 | - name: Cleanup orphaned test processes 89 | if: always() 90 | run: | 91 | # Kill any remaining test processes 92 | pkill -9 -f "fork.*debug/deps" || true 93 | # Clean up test directories 94 | rm -rf /tmp/fork_test* || true 95 | 96 | - name: Install grcov 97 | run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi 98 | 99 | - name: Run grcov 100 | run: grcov . --binary-path target/debug/ -s . -t lcov --branch --ignore-not-existing 101 | --ignore '../**' --ignore '/*' -o coverage.lcov 102 | 103 | - name: Upload to codecov.io 104 | uses: codecov/codecov-action@v5 105 | env: 106 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 107 | with: 108 | files: coverage.lcov 109 | flags: rust 110 | 111 | - name: Coveralls GitHub Action 112 | uses: coverallsapp/github-action@v2 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.justfile: -------------------------------------------------------------------------------- 1 | test: fmt clippy 2 | cargo test 3 | 4 | fmt: 5 | cargo fmt --all -- --check 6 | 7 | clippy: 8 | cargo clippy --all-targets --all-features -- -D warnings 9 | 10 | # Coverage report 11 | coverage: 12 | CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage-%p-%m.profraw' cargo test 13 | grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html 14 | firefox target/coverage/html/index.html 15 | rm -rf *.profraw 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | 6 | os: 7 | - linux 8 | - osx 9 | 10 | before_script: 11 | - rustup component add clippy 12 | script: 13 | - cargo clippy --all-targets --all-features -- -D clippy::pedantic -D clippy::nursery 14 | - cargo build --all --all-targets 15 | - cargo test --all 16 | 17 | notifications: 18 | email: 19 | on_sucess: never 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | * Added comprehensive test coverage for `getpgrp()` function 3 | - Unit tests in `src/lib.rs` (`test_getpgrp`, `test_getpgrp_in_parent`) 4 | - Integration test `test_getpgrp_returns_process_group` in `tests/integration_tests.rs` 5 | * Added `coverage` recipe to `.justfile` for generating coverage reports with grcov 6 | 7 | ## 0.3.0 8 | 9 | ### Changed 10 | * Updated Rust edition from 2021 to 2024 11 | * Applied edition 2024 formatting standards (alphabetical import ordering) 12 | 13 | ### Added 14 | * **Integration tests directory** - Added `tests/` directory with comprehensive integration tests 15 | - `daemon_tests.rs` - 5 tests for daemon functionality (detached process, nochdir, process groups, command execution, no controlling terminal) 16 | - `fork_tests.rs` - 7 tests for fork functionality (basic fork, parent-child communication, multiple children, environment inheritance, command execution, different PIDs, waitpid) 17 | - `integration_tests.rs` - 5 tests for advanced patterns (double-fork daemon, setsid, chdir, process isolation, getpgrp) 18 | 19 | ### Improved 20 | * Significantly expanded test coverage from 1 to 13 comprehensive unit tests 21 | * Added tests for all public API functions: 22 | - `fork()` - Multiple test scenarios including child execution 23 | - `daemon()` - Daemon pattern tested (double-fork with setsid) 24 | - `waitpid()` - Proper parent-child synchronization 25 | - `setsid()` - Session management and verification 26 | - `getpgrp()` - Process group queries 27 | - `chdir()` - Directory changes with verification 28 | - `close_fd()` - File descriptor management 29 | * Added real-world usage pattern tests: 30 | - Classic double-fork daemon pattern 31 | - Multiple sequential forks 32 | - Command execution in child processes 33 | * Improved test quality with proper cleanup and zombie process prevention 34 | * Enhanced CI/CD integration with LLVM coverage instrumentation 35 | * **Total test count: 35 tests** (13 unit + 17 integration + 5 doc tests) 36 | 37 | ### Fixed 38 | * Daemon tests now properly test the daemon pattern without calling `daemon()` directly 39 | (which would call `exit(0)` and terminate the test runner) 40 | 41 | ### Updated 42 | * GitHub Actions: codecov/codecov-action from v4 to v5 43 | 44 | ## 0.2.0 45 | * Added waitpid(pid: i32) 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fork" 3 | version = "0.3.1" 4 | authors = ["Nicolas Embriz "] 5 | description = "Library for creating a new process detached from the controlling terminal (daemon)" 6 | documentation = "https://docs.rs/fork/latest/fork/" 7 | homepage = "https://docs.rs/fork/latest/fork/" 8 | repository = "https://github.com/immortal/fork" 9 | readme = "README.md" 10 | keywords = ["fork", "setsid", "daemon", "process", "daemonize"] 11 | categories = ["os::unix-apis", "api-bindings"] 12 | license = "BSD-3-Clause" 13 | edition = "2024" 14 | 15 | [dependencies] 16 | libc = "0.2" 17 | 18 | [dev-dependencies] 19 | os_pipe = "1.2" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, immortal 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fork 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/fork.svg)](https://crates.io/crates/fork) 4 | [![Documentation](https://docs.rs/fork/badge.svg)](https://docs.rs/fork) 5 | [![Build](https://github.com/immortal/fork/actions/workflows/build.yml/badge.svg)](https://github.com/immortal/fork/actions/workflows/build.yml) 6 | [![codecov](https://codecov.io/gh/immortal/fork/graph/badge.svg?token=LHZW56OC10)](https://codecov.io/gh/immortal/fork) 7 | [![License](https://img.shields.io/crates/l/fork.svg)](https://github.com/immortal/fork/blob/main/LICENSE) 8 | 9 | Library for creating a new process detached from the controlling terminal (daemon) on Unix-like systems. 10 | 11 | ## Features 12 | 13 | - ✅ **Minimal** - Small, focused library for process forking and daemonization 14 | - ✅ **Safe** - Comprehensive test coverage (35 tests: 13 unit + 17 integration + 5 doc) 15 | - ✅ **Well-documented** - Extensive documentation with real-world examples 16 | - ✅ **Unix-first** - Built specifically for Unix-like systems (Linux, macOS, BSD) 17 | - ✅ **Edition 2024** - Uses latest Rust edition features 18 | 19 | ## Why? 20 | 21 | - Minimal library to daemonize, fork, double-fork a process 22 | - [daemon(3)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/daemon.3.html) has been 23 | deprecated in macOS 10.5. By using `fork` and `setsid` syscalls, new methods 24 | can be created to achieve the same goal 25 | - Provides the building blocks for creating proper Unix daemons 26 | 27 | ## Installation 28 | 29 | Add `fork` to your `Cargo.toml`: 30 | 31 | ```toml 32 | [dependencies] 33 | fork = "0.3" 34 | ``` 35 | 36 | Or use cargo-add: 37 | 38 | ```bash 39 | cargo add fork 40 | ``` 41 | 42 | ## Quick Start 43 | 44 | ### Basic Daemon Example 45 | 46 | ```rust 47 | use fork::{daemon, Fork}; 48 | use std::process::Command; 49 | 50 | fn main() { 51 | if let Ok(Fork::Child) = daemon(false, false) { 52 | // This code runs in the daemon process 53 | Command::new("sleep") 54 | .arg("300") 55 | .output() 56 | .expect("failed to execute process"); 57 | } 58 | } 59 | ``` 60 | 61 | ### Simple Fork Example 62 | 63 | ```rust 64 | use fork::{fork, Fork}; 65 | 66 | match fork() { 67 | Ok(Fork::Parent(child)) => { 68 | println!("Parent process, child PID: {}", child); 69 | } 70 | Ok(Fork::Child) => { 71 | println!("Child process"); 72 | } 73 | Err(_) => println!("Fork failed"), 74 | } 75 | ``` 76 | 77 | ## API Overview 78 | 79 | ### Main Functions 80 | 81 | - **`fork()`** - Creates a new child process 82 | - **`daemon(nochdir, noclose)`** - Creates a daemon using double-fork pattern 83 | - `nochdir`: if `false`, changes working directory to `/` 84 | - `noclose`: if `false`, redirects stdin/stdout/stderr to `/dev/null` 85 | - **`setsid()`** - Creates a new session and sets the process group ID 86 | - **`waitpid(pid)`** - Waits for child process to change state 87 | - **`getpgrp()`** - Returns the process group ID 88 | - **`chdir()`** - Changes current directory to `/` 89 | - **`close_fd()`** - Closes stdin, stdout, and stderr 90 | 91 | See the [documentation](https://docs.rs/fork) for detailed usage. 92 | 93 | ## Process Tree Example 94 | 95 | When using `daemon(false, false)`, it will change directory to `/` and close the standard file descriptors. 96 | 97 | Test running: 98 | 99 | ```bash 100 | $ cargo run 101 | ``` 102 | 103 | Use `ps` to check the process: 104 | 105 | ```bash 106 | $ ps -axo ppid,pid,pgid,sess,tty,tpgid,stat,uid,%mem,%cpu,command | egrep "myapp|sleep|PID" 107 | ``` 108 | 109 | Output: 110 | 111 | ``` 112 | PPID PID PGID SESS TTY TPGID STAT UID %MEM %CPU COMMAND 113 | 1 48738 48737 0 ?? 0 S 501 0.0 0.0 target/debug/myapp 114 | 48738 48753 48737 0 ?? 0 S 501 0.0 0.0 sleep 300 115 | ``` 116 | 117 | Key points: 118 | - `PPID == 1` - Parent is init/systemd (orphaned process) 119 | - `TTY = ??` - No controlling terminal 120 | - New `PGID = 48737` - Own process group 121 | 122 | Process hierarchy: 123 | 124 | ``` 125 | 1 - root (init/systemd) 126 | └── 48738 myapp PGID - 48737 127 | └── 48753 sleep PGID - 48737 128 | ``` 129 | 130 | ## Double-Fork Daemon Pattern 131 | 132 | The `daemon()` function implements the classic double-fork pattern: 133 | 134 | 1. **First fork** - Creates child process 135 | 2. **setsid()** - Child becomes session leader 136 | 3. **Second fork** - Grandchild is created (not a session leader) 137 | 4. **First child exits** - Leaves grandchild orphaned 138 | 5. **Grandchild continues** - As daemon (no controlling terminal) 139 | 140 | This prevents the daemon from ever acquiring a controlling terminal. 141 | 142 | ## Testing 143 | 144 | The library has comprehensive test coverage: 145 | 146 | - **13 unit tests** in `src/lib.rs` 147 | - **17 integration tests** in `tests/` directory 148 | - **5 documentation tests** 149 | 150 | Run tests: 151 | 152 | ```bash 153 | cargo test 154 | ``` 155 | 156 | See [`tests/README.md`](tests/README.md) for detailed information about integration tests. 157 | 158 | ## Platform Support 159 | 160 | This library is designed for Unix-like operating systems: 161 | 162 | - ✅ Linux 163 | - ✅ macOS 164 | - ✅ FreeBSD 165 | - ✅ NetBSD 166 | - ✅ OpenBSD 167 | - ❌ Windows (not supported) 168 | 169 | ## Documentation 170 | 171 | - [API Documentation](https://docs.rs/fork) 172 | - [Integration Tests Documentation](tests/README.md) 173 | - [Changelog](CHANGELOG.md) 174 | 175 | ## Examples 176 | 177 | See the [`examples/`](examples/) directory for more usage examples: 178 | 179 | - `example_daemon.rs` - Daemon creation 180 | - `example_pipe.rs` - Fork with pipe communication 181 | - `example_touch_pid.rs` - PID file creation 182 | 183 | Run an example: 184 | 185 | ```bash 186 | cargo run --example example_daemon 187 | ``` 188 | 189 | ## Contributing 190 | 191 | Contributions are welcome! Please ensure: 192 | 193 | - All tests pass: `cargo test` 194 | - Code is formatted: `cargo fmt` 195 | - No clippy warnings: `cargo clippy -- -D warnings` 196 | - Documentation is updated 197 | 198 | ## License 199 | 200 | BSD 3-Clause License - see [LICENSE](LICENSE) file for details. 201 | -------------------------------------------------------------------------------- /examples/example_daemon.rs: -------------------------------------------------------------------------------- 1 | /// run with `cargo run --example example_daemon` 2 | use fork::{Fork, daemon}; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | // Keep file descriptors open to print the pid of the daemon 7 | match daemon(false, true) { 8 | Ok(Fork::Child) => { 9 | Command::new("sleep") 10 | .arg("300") 11 | .output() 12 | .expect("failed to execute process"); 13 | } 14 | Ok(Fork::Parent(pid)) => { 15 | println!("daemon pid: {}", pid); 16 | } 17 | Err(_) => { 18 | println!("Fork failed"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/example_pipe.rs: -------------------------------------------------------------------------------- 1 | /// run with `cargo run --example example_pipe` 2 | use fork::{Fork, fork, setsid}; 3 | use os_pipe::pipe; 4 | use std::io::prelude::*; 5 | use std::process::{Command, Stdio, exit}; 6 | 7 | fn main() { 8 | // Create a pipe for communication 9 | let (mut reader, writer) = pipe().expect("Failed to create pipe"); 10 | 11 | match fork() { 12 | Ok(Fork::Child) => match fork() { 13 | Ok(Fork::Child) => { 14 | setsid().expect("Failed to setsid"); 15 | match Command::new("sleep") 16 | .arg("300") 17 | .stdin(Stdio::null()) 18 | .stdout(Stdio::null()) 19 | .stderr(Stdio::null()) 20 | .spawn() 21 | { 22 | Ok(child) => { 23 | println!("Child pid: {}", child.id()); 24 | 25 | // Write child pid to the pipe 26 | let mut writer = writer; // Shadowing to prevent move errors 27 | writeln!(writer, "{}", child.id()).expect("Failed to write to pipe"); 28 | 29 | exit(0); 30 | } 31 | Err(e) => { 32 | eprintln!("Error running command: {:?}", e); 33 | exit(1); 34 | } 35 | } 36 | } 37 | Ok(Fork::Parent(_)) => exit(0), 38 | Err(e) => { 39 | eprintln!("Error spawning process: {:?}", e); 40 | exit(1) 41 | } 42 | }, 43 | Ok(Fork::Parent(_)) => { 44 | drop(writer); 45 | 46 | // Read the child pid from the pipe 47 | let mut child_pid_str = String::new(); 48 | reader 49 | .read_to_string(&mut child_pid_str) 50 | .expect("Failed to read from pipe"); 51 | 52 | if let Ok(child_pid) = child_pid_str.trim().parse::() { 53 | println!("Received child pid: {}", child_pid); 54 | } else { 55 | eprintln!("Failed to parse child pid"); 56 | } 57 | } 58 | Err(e) => eprintln!("Error spawning process: {:?}", e), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/example_touch_pid.rs: -------------------------------------------------------------------------------- 1 | /// run with `cargo run --example example_touch_pid` 2 | use fork::{Fork, daemon}; 3 | use std::fs::OpenOptions; 4 | use std::process::Command; 5 | 6 | fn main() { 7 | match daemon(false, false) { 8 | Ok(Fork::Child) => { 9 | Command::new("sleep") 10 | .arg("300") 11 | .output() 12 | .expect("failed to execute process"); 13 | } 14 | Ok(Fork::Parent(pid)) => { 15 | // touch file with name like pid 16 | let file_name = format!("/tmp/{}.pid", pid); 17 | OpenOptions::new() 18 | .write(true) 19 | .create(true) 20 | .truncate(true) 21 | .open(file_name) 22 | .expect("failed to open file"); 23 | } 24 | Err(_) => { 25 | println!("Fork failed"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Library for creating a new process detached from the controlling terminal (daemon). 2 | //! 3 | //! Example: 4 | //! ``` 5 | //!use fork::{daemon, Fork}; 6 | //!use std::process::Command; 7 | //! 8 | //!if let Ok(Fork::Child) = daemon(false, false) { 9 | //! Command::new("sleep") 10 | //! .arg("3") 11 | //! .output() 12 | //! .expect("failed to execute process"); 13 | //!} 14 | //!``` 15 | 16 | use std::ffi::CString; 17 | use std::process::exit; 18 | 19 | /// Fork result 20 | pub enum Fork { 21 | Parent(libc::pid_t), 22 | Child, 23 | } 24 | 25 | /// Change dir to `/` [see chdir(2)](https://www.freebsd.org/cgi/man.cgi?query=chdir&sektion=2) 26 | /// 27 | /// Upon successful completion, 0 shall be returned. Otherwise, -1 shall be 28 | /// returned, the current working directory shall remain unchanged, and errno 29 | /// shall be set to indicate the error. 30 | /// 31 | /// Example: 32 | /// 33 | ///``` 34 | ///use fork::chdir; 35 | ///use std::env; 36 | /// 37 | ///match chdir() { 38 | /// Ok(_) => { 39 | /// let path = env::current_dir().expect("failed current_dir"); 40 | /// assert_eq!(Some("/"), path.to_str()); 41 | /// } 42 | /// _ => panic!(), 43 | ///} 44 | ///``` 45 | /// 46 | /// # Errors 47 | /// returns `-1` if error 48 | /// # Panics 49 | /// Panics if `CString::new` fails 50 | pub fn chdir() -> Result { 51 | let dir = CString::new("/").expect("CString::new failed"); 52 | let res = unsafe { libc::chdir(dir.as_ptr()) }; 53 | match res { 54 | -1 => Err(-1), 55 | res => Ok(res), 56 | } 57 | } 58 | 59 | /// Close file descriptors stdin,stdout,stderr 60 | /// 61 | /// # Errors 62 | /// returns `-1` if error 63 | pub fn close_fd() -> Result<(), i32> { 64 | match unsafe { libc::close(0) } { 65 | -1 => Err(-1), 66 | _ => match unsafe { libc::close(1) } { 67 | -1 => Err(-1), 68 | _ => match unsafe { libc::close(2) } { 69 | -1 => Err(-1), 70 | _ => Ok(()), 71 | }, 72 | }, 73 | } 74 | } 75 | 76 | /// Create a new child process [see fork(2)](https://www.freebsd.org/cgi/man.cgi?fork) 77 | /// 78 | /// Upon successful completion, `fork()` returns a value of 0 to the child process 79 | /// and returns the process ID of the child process to the parent process. 80 | /// Otherwise, a value of -1 is returned to the parent process, no child process 81 | /// is created. 82 | /// 83 | /// Example: 84 | /// 85 | /// ``` 86 | ///use fork::{fork, Fork}; 87 | /// 88 | ///match fork() { 89 | /// Ok(Fork::Parent(child)) => { 90 | /// println!("Continuing execution in parent process, new child has pid: {}", child); 91 | /// } 92 | /// Ok(Fork::Child) => println!("I'm a new child process"), 93 | /// Err(_) => println!("Fork failed"), 94 | ///} 95 | ///``` 96 | /// This will print something like the following (order indeterministic). 97 | /// 98 | /// ```text 99 | /// Continuing execution in parent process, new child has pid: 1234 100 | /// I'm a new child process 101 | /// ``` 102 | /// 103 | /// The thing to note is that you end up with two processes continuing execution 104 | /// immediately after the fork call but with different match arms. 105 | /// 106 | /// # [`nix::unistd::fork`](https://docs.rs/nix/0.15.0/nix/unistd/fn.fork.html) 107 | /// 108 | /// The example has been taken from the [`nix::unistd::fork`](https://docs.rs/nix/0.15.0/nix/unistd/fn.fork.html), 109 | /// please check the [Safety](https://docs.rs/nix/0.15.0/nix/unistd/fn.fork.html#safety) section 110 | /// 111 | /// # Errors 112 | /// returns `-1` if error 113 | pub fn fork() -> Result { 114 | let res = unsafe { libc::fork() }; 115 | match res { 116 | -1 => Err(-1), 117 | 0 => Ok(Fork::Child), 118 | res => Ok(Fork::Parent(res)), 119 | } 120 | } 121 | 122 | /// Wait for process to change status [see wait(2)](https://man.freebsd.org/cgi/man.cgi?waitpid) 123 | /// 124 | /// # Errors 125 | /// returns `-1` if error 126 | /// 127 | /// Example: 128 | /// 129 | /// ``` 130 | ///use fork::{waitpid, Fork}; 131 | ///use std::process::Command; 132 | /// 133 | ///fn main() { 134 | /// match fork::fork() { 135 | /// Ok(Fork::Parent(pid)) => { 136 | /// 137 | /// println!("Child pid: {pid}"); 138 | /// 139 | /// match waitpid(pid) { 140 | /// Ok(_) => println!("Child existed"), 141 | /// Err(_) => eprintln!("Failted to wait on child"), 142 | /// } 143 | /// } 144 | /// Ok(Fork::Child) => { 145 | /// Command::new("sleep") 146 | /// .arg("1") 147 | /// .output() 148 | /// .expect("failed to execute process"); 149 | /// } 150 | /// Err(_) => eprintln!("Failed to fork"), 151 | /// } 152 | ///} 153 | ///``` 154 | pub fn waitpid(pid: i32) -> Result<(), i32> { 155 | let mut status: i32 = 0; 156 | let res = unsafe { libc::waitpid(pid, &mut status, 0) }; 157 | match res { 158 | -1 => Err(-1), 159 | _ => Ok(()), 160 | } 161 | } 162 | 163 | /// Create session and set process group ID [see setsid(2)](https://www.freebsd.org/cgi/man.cgi?setsid) 164 | /// 165 | /// Upon successful completion, the `setsid()` system call returns the value of the 166 | /// process group ID of the new process group, which is the same as the process ID 167 | /// of the calling process. If an error occurs, `setsid()` returns -1 168 | /// 169 | /// # Errors 170 | /// returns `-1` if error 171 | pub fn setsid() -> Result { 172 | let res = unsafe { libc::setsid() }; 173 | match res { 174 | -1 => Err(-1), 175 | res => Ok(res), 176 | } 177 | } 178 | 179 | /// The process group of the current process [see getgrp(2)](https://www.freebsd.org/cgi/man.cgi?query=getpgrp) 180 | /// 181 | /// # Errors 182 | /// returns `-1` if error 183 | pub fn getpgrp() -> Result { 184 | let res = unsafe { libc::getpgrp() }; 185 | match res { 186 | -1 => Err(-1), 187 | res => Ok(res), 188 | } 189 | } 190 | 191 | /// The daemon function is for programs wishing to detach themselves from the 192 | /// controlling terminal and run in the background as system daemons. 193 | /// 194 | /// * `nochdir = false`, changes the current working directory to the root (`/`). 195 | /// * `noclose = false`, will close standard input, standard output, and standard error 196 | /// 197 | /// # Errors 198 | /// If an error occurs, returns -1 199 | /// 200 | /// Example: 201 | /// 202 | ///``` 203 | ///// The parent forks the child 204 | ///// The parent exits 205 | ///// The child calls setsid() to start a new session with no controlling terminals 206 | ///// The child forks a grandchild 207 | ///// The child exits 208 | ///// The grandchild is now the daemon 209 | ///use fork::{daemon, Fork}; 210 | ///use std::process::Command; 211 | /// 212 | ///if let Ok(Fork::Child) = daemon(false, false) { 213 | /// Command::new("sleep") 214 | /// .arg("3") 215 | /// .output() 216 | /// .expect("failed to execute process"); 217 | ///} 218 | ///``` 219 | pub fn daemon(nochdir: bool, noclose: bool) -> Result { 220 | match fork() { 221 | Ok(Fork::Parent(_)) => exit(0), 222 | Ok(Fork::Child) => setsid().and_then(|_| { 223 | if !nochdir { 224 | chdir()?; 225 | } 226 | if !noclose { 227 | close_fd()?; 228 | } 229 | fork() 230 | }), 231 | Err(n) => Err(n), 232 | } 233 | } 234 | 235 | #[cfg(test)] 236 | mod tests { 237 | use super::*; 238 | use std::env; 239 | use std::process::{Command, exit}; 240 | 241 | #[test] 242 | fn test_fork() { 243 | match fork() { 244 | Ok(Fork::Parent(child)) => { 245 | assert!(child > 0); 246 | // Wait for child to complete 247 | let _ = waitpid(child); 248 | } 249 | Ok(Fork::Child) => { 250 | // Child process exits immediately 251 | exit(0); 252 | } 253 | Err(_) => panic!("Fork failed"), 254 | } 255 | } 256 | 257 | #[test] 258 | fn test_fork_with_waitpid() { 259 | match fork() { 260 | Ok(Fork::Parent(child)) => { 261 | assert!(child > 0); 262 | // Wait for child and verify it succeeds 263 | assert!(waitpid(child).is_ok()); 264 | } 265 | Ok(Fork::Child) => { 266 | // Child does some work then exits 267 | let _ = Command::new("true").output(); 268 | exit(0); 269 | } 270 | Err(_) => panic!("Fork failed"), 271 | } 272 | } 273 | 274 | #[test] 275 | fn test_chdir() { 276 | match fork() { 277 | Ok(Fork::Parent(child)) => { 278 | let _ = waitpid(child); 279 | } 280 | Ok(Fork::Child) => { 281 | // Test changing directory to root 282 | match chdir() { 283 | Ok(res) => { 284 | assert_eq!(res, 0); 285 | let path = env::current_dir().expect("failed current_dir"); 286 | assert_eq!(Some("/"), path.to_str()); 287 | exit(0); 288 | } 289 | Err(_) => exit(1), 290 | } 291 | } 292 | Err(_) => panic!("Fork failed"), 293 | } 294 | } 295 | 296 | #[test] 297 | fn test_getpgrp() { 298 | match fork() { 299 | Ok(Fork::Parent(child)) => { 300 | let _ = waitpid(child); 301 | } 302 | Ok(Fork::Child) => { 303 | // Get process group and verify it's valid 304 | match getpgrp() { 305 | Ok(pgrp) => { 306 | assert!(pgrp > 0); 307 | exit(0); 308 | } 309 | Err(_) => exit(1), 310 | } 311 | } 312 | Err(_) => panic!("Fork failed"), 313 | } 314 | } 315 | 316 | #[test] 317 | fn test_setsid() { 318 | match fork() { 319 | Ok(Fork::Parent(child)) => { 320 | let _ = waitpid(child); 321 | } 322 | Ok(Fork::Child) => { 323 | // Create new session 324 | match setsid() { 325 | Ok(sid) => { 326 | assert!(sid > 0); 327 | // Verify we're the session leader 328 | let pgrp = getpgrp().expect("Failed to get process group"); 329 | assert_eq!(sid, pgrp); 330 | exit(0); 331 | } 332 | Err(_) => exit(1), 333 | } 334 | } 335 | Err(_) => panic!("Fork failed"), 336 | } 337 | } 338 | 339 | #[test] 340 | fn test_daemon_pattern_with_chdir() { 341 | // Test the daemon pattern manually without calling daemon() 342 | // to avoid exit(0) killing the test process 343 | match fork() { 344 | Ok(Fork::Parent(child)) => { 345 | // Parent waits for child 346 | let _ = waitpid(child); 347 | } 348 | Ok(Fork::Child) => { 349 | // Child creates new session and forks again 350 | setsid().expect("Failed to setsid"); 351 | chdir().expect("Failed to chdir"); 352 | 353 | match fork() { 354 | Ok(Fork::Parent(_)) => { 355 | // Middle process exits 356 | exit(0); 357 | } 358 | Ok(Fork::Child) => { 359 | // Grandchild (daemon) - verify state 360 | let path = env::current_dir().expect("failed current_dir"); 361 | assert_eq!(Some("/"), path.to_str()); 362 | 363 | let pgrp = getpgrp().expect("Failed to get process group"); 364 | assert!(pgrp > 0); 365 | 366 | exit(0); 367 | } 368 | Err(_) => exit(1), 369 | } 370 | } 371 | Err(_) => panic!("Fork failed"), 372 | } 373 | } 374 | 375 | #[test] 376 | fn test_daemon_pattern_no_chdir() { 377 | // Test daemon pattern preserving current directory 378 | let original_dir = env::current_dir().expect("failed to get current dir"); 379 | 380 | match fork() { 381 | Ok(Fork::Parent(child)) => { 382 | let _ = waitpid(child); 383 | } 384 | Ok(Fork::Child) => { 385 | setsid().expect("Failed to setsid"); 386 | // Don't call chdir - preserve directory 387 | 388 | match fork() { 389 | Ok(Fork::Parent(_)) => exit(0), 390 | Ok(Fork::Child) => { 391 | let current_dir = env::current_dir().expect("failed current_dir"); 392 | // Directory should be preserved 393 | if original_dir.to_str() != Some("/") { 394 | assert!(current_dir.to_str().is_some()); 395 | } 396 | exit(0); 397 | } 398 | Err(_) => exit(1), 399 | } 400 | } 401 | Err(_) => panic!("Fork failed"), 402 | } 403 | } 404 | 405 | #[test] 406 | fn test_daemon_pattern_with_close_fd() { 407 | // Test daemon pattern with file descriptor closure 408 | match fork() { 409 | Ok(Fork::Parent(child)) => { 410 | let _ = waitpid(child); 411 | } 412 | Ok(Fork::Child) => { 413 | setsid().expect("Failed to setsid"); 414 | chdir().expect("Failed to chdir"); 415 | close_fd().expect("Failed to close fd"); 416 | 417 | match fork() { 418 | Ok(Fork::Parent(_)) => exit(0), 419 | Ok(Fork::Child) => { 420 | // Daemon process with closed fds 421 | exit(0); 422 | } 423 | Err(_) => exit(1), 424 | } 425 | } 426 | Err(_) => panic!("Fork failed"), 427 | } 428 | } 429 | 430 | #[test] 431 | fn test_close_fd_functionality() { 432 | match fork() { 433 | Ok(Fork::Parent(child)) => { 434 | let _ = waitpid(child); 435 | } 436 | Ok(Fork::Child) => { 437 | // Close standard file descriptors 438 | match close_fd() { 439 | Ok(_) => exit(0), 440 | Err(_) => exit(1), 441 | } 442 | } 443 | Err(_) => panic!("Fork failed"), 444 | } 445 | } 446 | 447 | #[test] 448 | fn test_double_fork_pattern() { 449 | // Test the double-fork pattern commonly used for daemons 450 | match fork() { 451 | Ok(Fork::Parent(child1)) => { 452 | assert!(child1 > 0); 453 | let _ = waitpid(child1); 454 | } 455 | Ok(Fork::Child) => { 456 | // First child creates new session 457 | setsid().expect("Failed to setsid"); 458 | 459 | // Second fork to ensure we're not session leader 460 | match fork() { 461 | Ok(Fork::Parent(_)) => { 462 | // First child exits 463 | exit(0); 464 | } 465 | Ok(Fork::Child) => { 466 | // Grandchild - the daemon process 467 | let pgrp = getpgrp().expect("Failed to get process group"); 468 | assert!(pgrp > 0); 469 | exit(0); 470 | } 471 | Err(_) => exit(1), 472 | } 473 | } 474 | Err(_) => panic!("Fork failed"), 475 | } 476 | } 477 | 478 | #[test] 479 | fn test_waitpid_with_child() { 480 | match fork() { 481 | Ok(Fork::Parent(child)) => { 482 | assert!(child > 0); 483 | // Wait for child with timeout to prevent hanging 484 | // Simple approach: just call waitpid, the child exits immediately 485 | let result = waitpid(child); 486 | assert!(result.is_ok(), "waitpid should succeed"); 487 | } 488 | Ok(Fork::Child) => { 489 | // Child exits immediately to prevent any hanging issues 490 | exit(0); 491 | } 492 | Err(_) => panic!("Fork failed"), 493 | } 494 | } 495 | 496 | #[test] 497 | fn test_fork_child_execution() { 498 | match fork() { 499 | Ok(Fork::Parent(child)) => { 500 | assert!(child > 0); 501 | // Wait for child to finish its work 502 | assert!(waitpid(child).is_ok()); 503 | } 504 | Ok(Fork::Child) => { 505 | // Child executes a simple command 506 | let output = Command::new("echo") 507 | .arg("test") 508 | .output() 509 | .expect("Failed to execute command"); 510 | assert!(output.status.success()); 511 | exit(0); 512 | } 513 | Err(_) => panic!("Fork failed"), 514 | } 515 | } 516 | 517 | #[test] 518 | fn test_multiple_forks() { 519 | // Test creating multiple child processes 520 | for i in 0..3 { 521 | match fork() { 522 | Ok(Fork::Parent(child)) => { 523 | assert!(child > 0); 524 | let _ = waitpid(child); 525 | } 526 | Ok(Fork::Child) => { 527 | // Each child exits with its index 528 | exit(i); 529 | } 530 | Err(_) => panic!("Fork {} failed", i), 531 | } 532 | } 533 | } 534 | 535 | #[test] 536 | fn test_getpgrp_in_parent() { 537 | // Test getpgrp in parent process 538 | let parent_pgrp = getpgrp().expect("getpgrp should succeed"); 539 | assert!(parent_pgrp > 0); 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | This directory contains integration tests for the `fork` library. These tests run in separate processes and can properly test functions like `daemon()` that call `exit()`. 4 | 5 | ## Overview 6 | 7 | The integration tests are organized into four files: 8 | - **`daemon_tests.rs`** - 5 tests for daemon functionality 9 | - **`fork_tests.rs`** - 7 tests for fork/waitpid functionality 10 | - **`integration_tests.rs`** - 5 tests for advanced patterns 11 | - **`common/mod.rs`** - Shared test utilities 12 | 13 | **Total: 17 integration tests** providing comprehensive coverage of process management, daemon creation, and fork patterns. 14 | 15 | ## Test Files 16 | 17 | ### `daemon_tests.rs` - Daemon Functionality Tests (5 tests) 18 | 19 | Tests the `daemon()` function with real process daemonization. Each test documents: 20 | - What is being tested 21 | - Expected behavior (numbered steps) 22 | - Why it matters for daemon creation 23 | 24 | Tests include: 25 | - **test_daemon_creates_detached_process** - Verifies daemon process creation and PID management 26 | - **test_daemon_with_nochdir** - Tests `nochdir` option preserves current directory 27 | - **test_daemon_process_group** - Verifies daemon process group structure (double-fork pattern) 28 | - **test_daemon_with_command_execution** - Tests command execution in daemon context 29 | - **test_daemon_no_controlling_terminal** - Verifies daemon has no controlling terminal 30 | 31 | ### `fork_tests.rs` - Fork Functionality Tests (7 tests) 32 | 33 | Tests the core `fork()` and `waitpid()` functions. Each test explains the expected parent-child behavior. 34 | 35 | Tests include: 36 | - **test_fork_basic** - Basic fork/waitpid functionality and cleanup 37 | - **test_fork_parent_child_communication** - File-based parent-child IPC pattern 38 | - **test_fork_multiple_children** - Creating and managing multiple child processes 39 | - **test_fork_child_inherits_environment** - Environment variable inheritance across fork 40 | - **test_fork_child_can_execute_commands** - Command execution in child processes 41 | - **test_fork_child_has_different_pid** - PID uniqueness between parent and child 42 | - **test_waitpid_waits_for_child** - Proper parent-child synchronization 43 | 44 | ### `integration_tests.rs` - Advanced Pattern Tests (5 tests) 45 | 46 | Tests complex usage patterns combining multiple operations. Documents real-world daemon scenarios. 47 | 48 | Tests include: 49 | - **test_double_fork_daemon_pattern** - Classic double-fork daemon creation (standard pattern) 50 | - **test_setsid_creates_new_session** - Session management and session leader verification 51 | - **test_chdir_changes_directory** - Directory changes in child processes 52 | - **test_process_isolation** - File system isolation between parent/child (separate memory) 53 | - **test_getpgrp_returns_process_group** - Process group queries and verification 54 | 55 | ### `common/mod.rs` - Shared Test Utilities 56 | 57 | Provides reusable helper functions to reduce code duplication: 58 | - `get_unique_test_dir()` - Creates unique test directories with atomic counter 59 | - `get_test_dir()` - Creates simple test directories 60 | - `setup_test_dir()` - Sets up and cleans test directory 61 | - `wait_for_file()` - Waits for file creation with timeout 62 | - `cleanup_test_dir()` - Removes test directory 63 | 64 | ## Running Tests 65 | 66 | ```bash 67 | # Run all tests (unit + integration + doc) 68 | cargo test 69 | 70 | # Run only integration tests 71 | cargo test --tests 72 | 73 | # Run specific test file 74 | cargo test --test daemon_tests 75 | cargo test --test fork_tests 76 | cargo test --test integration_tests 77 | 78 | # Run specific test 79 | cargo test --test daemon_tests test_daemon_creates_detached_process 80 | 81 | # Run with output 82 | cargo test --test daemon_tests -- --nocapture 83 | 84 | # Run with verbose output 85 | cargo test --test fork_tests -- --nocapture --test-threads=1 86 | ``` 87 | 88 | ## How Integration Tests Work 89 | 90 | Unlike unit tests in `src/lib.rs`, integration tests: 91 | 92 | 1. **Run in separate processes** - Each test file is compiled as its own binary 93 | 2. **Can call `daemon()`** - The parent process in tests doesn't terminate the test runner 94 | 3. **Use file-based communication** - Temporary files in `/tmp` for parent-child verification 95 | 4. **Have proper isolation** - Each test uses unique temporary directories to avoid conflicts 96 | 5. **Clean up after themselves** - Temporary files are removed after test completion 97 | 6. **Document expected behavior** - Each test has detailed comments explaining what happens 98 | 99 | ## Test Documentation 100 | 101 | Every test includes comprehensive documentation: 102 | 103 | ```rust 104 | #[test] 105 | fn test_name() { 106 | // Clear description of what is being tested 107 | // Expected behavior: 108 | // 1. First step 109 | // 2. Second step 110 | // 3. Third step 111 | // 4. Fourth step 112 | // 5. Final verification 113 | 114 | [test implementation] 115 | } 116 | ``` 117 | 118 | This makes it easy to: 119 | - Understand test purpose at a glance 120 | - Debug failures quickly 121 | - Use tests as usage examples 122 | - Onboard new contributors 123 | 124 | ## Test Isolation 125 | 126 | Each test uses a unique temporary directory to prevent conflicts when running in parallel: 127 | 128 | ```rust 129 | // Daemon tests use atomic counter for uniqueness 130 | let test_dir = setup_test_dir(get_unique_test_dir("daemon_creates_detached")); 131 | 132 | // Fork tests use descriptive prefixes 133 | let test_dir = setup_test_dir(get_test_dir("fork_communication")); 134 | 135 | // Integration tests use specific names 136 | let test_dir = setup_test_dir(get_test_dir("int_double_fork")); 137 | ``` 138 | 139 | This allows tests to run in parallel without interfering with each other. 140 | 141 | ## Coverage 142 | 143 | Integration tests provide coverage for: 144 | 145 | - **Daemon creation** - Real process daemonization (not mocked) 146 | - **Process groups** - Session management and process group creation 147 | - **File descriptors** - Proper handling of stdin/stdout/stderr 148 | - **IPC patterns** - Parent-child communication via files 149 | - **Command execution** - Running commands in forked/daemon processes 150 | - **Environment inheritance** - Variable passing across fork 151 | - **Process isolation** - Memory separation and filesystem sharing 152 | - **Double-fork pattern** - Standard daemon creation technique 153 | - **PID management** - Process ID tracking and verification 154 | 155 | These tests complement the 13 unit tests in `src/lib.rs` and 5 doc tests to provide **comprehensive coverage** (35 total tests) of the library's functionality. 156 | 157 | ## Module Structure 158 | 159 | ``` 160 | tests/ 161 | ├── common/ 162 | │ └── mod.rs # Shared utilities (51 lines) 163 | ├── daemon_tests.rs # Daemon tests (260 lines, 5 tests) 164 | ├── fork_tests.rs # Fork tests (290 lines, 7 tests) 165 | ├── integration_tests.rs # Advanced tests (226 lines, 5 tests) 166 | └── README.md # This file 167 | ``` 168 | 169 | Total: **827 lines** of well-documented integration test code with **~160 lines** of explanatory comments. 170 | 171 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common test utilities for fork integration tests 2 | //! 3 | //! This module provides shared helper functions for integration tests, 4 | //! reducing code duplication across test files. 5 | 6 | #![allow(dead_code)] 7 | 8 | use std::{ 9 | env, fs, 10 | path::{Path, PathBuf}, 11 | sync::atomic::{AtomicU64, Ordering}, 12 | thread, 13 | time::{Duration, Instant}, 14 | }; 15 | 16 | static TEST_COUNTER: AtomicU64 = AtomicU64::new(0); 17 | 18 | /// Get a unique test directory with counter to avoid conflicts 19 | pub fn get_unique_test_dir(test_name: &str) -> PathBuf { 20 | let counter = TEST_COUNTER.fetch_add(1, Ordering::SeqCst); 21 | env::temp_dir().join(format!("fork_test_{}_{}", test_name, counter)) 22 | } 23 | 24 | /// Get a simple test directory without counter 25 | pub fn get_test_dir(prefix: &str) -> PathBuf { 26 | env::temp_dir().join(format!("fork_test_{}", prefix)) 27 | } 28 | 29 | /// Setup a test directory (creates and cleans if exists) 30 | pub fn setup_test_dir(path: PathBuf) -> PathBuf { 31 | let _ = fs::remove_dir_all(&path); 32 | fs::create_dir_all(&path).expect("Failed to create test directory"); 33 | path 34 | } 35 | 36 | /// Wait for a file to exist with timeout 37 | pub fn wait_for_file(path: &Path, timeout_ms: u64) -> bool { 38 | let start = Instant::now(); 39 | while start.elapsed().as_millis() < timeout_ms as u128 { 40 | if path.exists() { 41 | return true; 42 | } 43 | thread::sleep(Duration::from_millis(10)); 44 | } 45 | false 46 | } 47 | 48 | /// Cleanup a test directory 49 | pub fn cleanup_test_dir(path: &Path) { 50 | let _ = fs::remove_dir_all(path); 51 | } 52 | -------------------------------------------------------------------------------- /tests/daemon_tests.rs: -------------------------------------------------------------------------------- 1 | //! Daemon-specific integration tests 2 | //! 3 | //! This module tests the `daemon()` function which creates a detached background process. 4 | //! These tests verify: 5 | //! - Process detachment and proper PID management 6 | //! - Directory handling (chdir vs nochdir) 7 | //! - Process group and session management 8 | //! - File descriptor handling (noclose option) 9 | //! - Command execution in daemon context 10 | //! - Absence of controlling terminal 11 | //! 12 | //! Note: These tests fork twice (the daemon pattern) so they run in separate 13 | //! processes to avoid terminating the test runner when daemon() calls exit(0). 14 | 15 | mod common; 16 | 17 | use common::{get_unique_test_dir, setup_test_dir, wait_for_file}; 18 | use fork::{Fork, daemon, fork}; 19 | use std::{ 20 | env, fs, 21 | process::{Command, exit}, 22 | }; 23 | 24 | #[test] 25 | fn test_daemon_creates_detached_process() { 26 | // Tests that daemon() successfully creates a detached background process 27 | // Expected behavior: 28 | // 1. Parent process forks 29 | // 2. First child creates new session and forks again 30 | // 3. First child exits (daemon() calls exit(0)) 31 | // 4. Grandchild (daemon) is detached and writes its PID 32 | // 5. Daemon changes to root directory (nochdir=false) 33 | // 6. Daemon has valid PID > 0 34 | let test_dir = setup_test_dir(get_unique_test_dir("daemon_creates_detached")); 35 | let marker_file = test_dir.join("daemon.marker"); 36 | 37 | // Fork the test to avoid daemon() calling exit(0) on parent 38 | match fork().expect("Failed to fork") { 39 | Fork::Parent(_) => { 40 | // Parent waits for marker file to be created 41 | assert!( 42 | wait_for_file(&marker_file, 500), 43 | "Daemon should have created marker file" 44 | ); 45 | 46 | // Read PID from marker file 47 | let content = fs::read_to_string(&marker_file).expect("Failed to read marker file"); 48 | let daemon_pid: i32 = content.trim().parse().expect("Failed to parse PID"); 49 | assert!(daemon_pid > 0, "Daemon PID should be positive"); 50 | 51 | // Cleanup 52 | let _ = fs::remove_dir_all(&test_dir); 53 | } 54 | Fork::Child => { 55 | // Child calls daemon() 56 | if let Ok(Fork::Child) = daemon(false, true) { 57 | // This is the daemon process 58 | // Write our PID to marker file 59 | let pid = unsafe { libc::getpid() }; 60 | fs::write(&marker_file, format!("{}", pid)).expect("Failed to write marker file"); 61 | 62 | // Verify we're in root directory 63 | let current = env::current_dir().expect("Failed to get current dir"); 64 | assert_eq!(current.to_str(), Some("/")); 65 | 66 | exit(0); 67 | } 68 | // Parent of daemon exits (daemon() calls exit(0) for us) 69 | } 70 | } 71 | } 72 | 73 | #[test] 74 | fn test_daemon_with_nochdir() { 75 | // Tests that daemon(nochdir=true) preserves the current working directory 76 | // Expected behavior: 77 | // 1. Test changes to a specific directory before calling daemon() 78 | // 2. daemon(true, true) is called (nochdir=true, noclose=true) 79 | // 3. Daemon process should remain in the same directory (not /) 80 | // 4. Daemon writes current directory to file for verification 81 | let test_dir = setup_test_dir(get_unique_test_dir("daemon_nochdir")); 82 | let marker_file = test_dir.join("nochdir.marker"); 83 | 84 | // Change to test directory 85 | env::set_current_dir(&test_dir).expect("Failed to change directory"); 86 | 87 | match fork().expect("Failed to fork") { 88 | Fork::Parent(_) => { 89 | assert!( 90 | wait_for_file(&marker_file, 500), 91 | "Daemon should have created marker file" 92 | ); 93 | 94 | // Cleanup 95 | let _ = fs::remove_dir_all(&test_dir); 96 | } 97 | Fork::Child => { 98 | if let Ok(Fork::Child) = daemon(true, true) { 99 | // Daemon with nochdir=true should preserve directory 100 | let current = env::current_dir().expect("Failed to get current dir"); 101 | 102 | // Write confirmation to marker file 103 | fs::write(&marker_file, format!("{}", current.display())) 104 | .expect("Failed to write marker file"); 105 | 106 | // Directory should still be test_dir, not root 107 | assert_ne!(current.to_str(), Some("/")); 108 | 109 | exit(0); 110 | } 111 | } 112 | } 113 | } 114 | 115 | #[test] 116 | fn test_daemon_process_group() { 117 | // Tests that daemon creates proper process group structure 118 | // Expected behavior: 119 | // 1. daemon() performs double-fork pattern 120 | // 2. After double-fork, daemon is NOT a session leader (PID != PGID) 121 | // 3. This prevents daemon from acquiring a controlling terminal 122 | // 4. Both PID and PGID are positive values 123 | // 5. Daemon writes PID,PGID to file for verification 124 | let test_dir = setup_test_dir(get_unique_test_dir("daemon_process_group")); 125 | let marker_file = test_dir.join("pgid.marker"); 126 | 127 | match fork().expect("Failed to fork") { 128 | Fork::Parent(_) => { 129 | assert!( 130 | wait_for_file(&marker_file, 500), 131 | "Daemon should have created marker file" 132 | ); 133 | 134 | // Read and verify process group info 135 | let content = fs::read_to_string(&marker_file).expect("Failed to read marker file"); 136 | let parts: Vec<&str> = content.trim().split(',').collect(); 137 | assert_eq!(parts.len(), 2); 138 | 139 | let pid: i32 = parts[0].parse().expect("Failed to parse PID"); 140 | let pgid: i32 = parts[1].parse().expect("Failed to parse PGID"); 141 | 142 | // Daemon (after double-fork) should NOT be session leader 143 | // but should be in a new process group 144 | assert!(pid > 0, "PID should be positive"); 145 | assert!(pgid > 0, "PGID should be positive"); 146 | assert_ne!( 147 | pid, pgid, 148 | "Daemon (after double-fork) should NOT be session leader" 149 | ); 150 | 151 | // Cleanup 152 | let _ = fs::remove_dir_all(&test_dir); 153 | } 154 | Fork::Child => { 155 | if let Ok(Fork::Child) = daemon(false, true) { 156 | let pid = unsafe { libc::getpid() }; 157 | let pgid = unsafe { libc::getpgrp() }; 158 | 159 | fs::write(&marker_file, format!("{},{}", pid, pgid)) 160 | .expect("Failed to write marker file"); 161 | 162 | exit(0); 163 | } 164 | } 165 | } 166 | } 167 | 168 | #[test] 169 | fn test_daemon_with_command_execution() { 170 | // Tests that daemon can execute commands successfully 171 | // Expected behavior: 172 | // 1. Daemon process is created 173 | // 2. Daemon executes a shell command 174 | // 3. Command output is written to a file 175 | // 4. Parent can verify command executed correctly 176 | // 5. Tests real-world daemon usage pattern 177 | let test_dir = setup_test_dir(get_unique_test_dir("daemon_command_exec")); 178 | let output_file = test_dir.join("command.output"); 179 | 180 | match fork().expect("Failed to fork") { 181 | Fork::Parent(_) => { 182 | assert!( 183 | wait_for_file(&output_file, 500), 184 | "Command output file should exist" 185 | ); 186 | 187 | let content = fs::read_to_string(&output_file).expect("Failed to read output file"); 188 | assert!( 189 | content.contains("hello from daemon"), 190 | "Output should contain expected text" 191 | ); 192 | 193 | // Cleanup 194 | let _ = fs::remove_dir_all(&test_dir); 195 | } 196 | Fork::Child => { 197 | if let Ok(Fork::Child) = daemon(false, true) { 198 | // Execute a command in the daemon 199 | Command::new("sh") 200 | .arg("-c") 201 | .arg(format!( 202 | "echo 'hello from daemon' > {}", 203 | output_file.display() 204 | )) 205 | .output() 206 | .expect("Failed to execute command"); 207 | 208 | exit(0); 209 | } 210 | } 211 | } 212 | } 213 | 214 | #[test] 215 | fn test_daemon_no_controlling_terminal() { 216 | // Tests that daemon has no controlling terminal 217 | // Expected behavior: 218 | // 1. Daemon process is created 219 | // 2. Daemon calls 'tty' command to check for terminal 220 | // 3. tty command should return "not a tty" or similar error 221 | // 4. This confirms daemon is properly detached from terminal 222 | // 5. Critical for background service behavior 223 | let test_dir = setup_test_dir(get_unique_test_dir("daemon_no_tty")); 224 | let tty_file = test_dir.join("tty.info"); 225 | 226 | match fork().expect("Failed to fork") { 227 | Fork::Parent(_) => { 228 | assert!(wait_for_file(&tty_file, 500), "TTY info file should exist"); 229 | 230 | let content = fs::read_to_string(&tty_file).expect("Failed to read tty file"); 231 | // When daemon has no controlling terminal, tty command should fail or return "not a tty" 232 | assert!( 233 | content.contains("not a tty") || content.contains("No such"), 234 | "Daemon should have no controlling terminal, got: {}", 235 | content 236 | ); 237 | 238 | // Cleanup 239 | let _ = fs::remove_dir_all(&test_dir); 240 | } 241 | Fork::Child => { 242 | if let Ok(Fork::Child) = daemon(false, true) { 243 | // Check if we have a controlling terminal 244 | let output = Command::new("tty") 245 | .output() 246 | .expect("Failed to run tty command"); 247 | 248 | let tty_output = if output.stdout.is_empty() { 249 | String::from_utf8_lossy(&output.stderr).to_string() 250 | } else { 251 | String::from_utf8_lossy(&output.stdout).to_string() 252 | }; 253 | 254 | fs::write(&tty_file, tty_output).expect("Failed to write tty file"); 255 | 256 | exit(0); 257 | } 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /tests/fork_tests.rs: -------------------------------------------------------------------------------- 1 | //! Fork functionality integration tests 2 | //! 3 | //! This module tests the core `fork()` and `waitpid()` functions. 4 | //! These tests verify: 5 | //! - Basic fork/waitpid functionality 6 | //! - Parent-child process communication via files 7 | //! - Multiple child process management 8 | //! - Environment variable inheritance across fork 9 | //! - Command execution in child processes 10 | //! - PID uniqueness between parent and child 11 | //! - Proper process synchronization with waitpid 12 | //! 13 | //! All tests use temporary files for parent-child communication since 14 | //! forked processes have separate memory spaces. 15 | 16 | mod common; 17 | 18 | use common::{get_test_dir, setup_test_dir}; 19 | use fork::{Fork, fork, waitpid}; 20 | use std::{ 21 | env, fs, 22 | process::{Command, exit}, 23 | thread, 24 | time::Duration, 25 | }; 26 | 27 | #[test] 28 | // Tests basic fork() functionality with waitpid() 29 | // Expected behavior: 30 | // 1. fork() returns Ok(Fork::Parent(pid)) in parent with child PID 31 | // 2. fork() returns Ok(Fork::Child) in child 32 | // 3. Child PID is positive 33 | // 4. waitpid() successfully waits for child to exit 34 | // 5. No zombie processes remain 35 | fn test_fork_basic() { 36 | match fork() { 37 | Ok(Fork::Parent(child)) => { 38 | assert!(child > 0, "Child PID should be positive"); 39 | 40 | // Wait for child 41 | assert!(waitpid(child).is_ok(), "waitpid should succeed"); 42 | } 43 | Ok(Fork::Child) => { 44 | // Child just exits 45 | exit(0); 46 | } 47 | Err(_) => panic!("Fork failed"), 48 | } 49 | } 50 | 51 | #[test] 52 | // Tests parent-child communication using files 53 | // Expected behavior: 54 | // 1. Parent and child have separate memory spaces 55 | // 2. Child writes a message to a file 56 | // 3. Parent reads the message after child completes 57 | // 4. Message content matches what child wrote 58 | // 5. Demonstrates file-based IPC pattern 59 | fn test_fork_parent_child_communication() { 60 | let test_dir = setup_test_dir(get_test_dir("fork_communication")); 61 | let message_file = test_dir.join("message.txt"); 62 | 63 | match fork() { 64 | Ok(Fork::Parent(child)) => { 65 | // Wait for child to write 66 | thread::sleep(Duration::from_millis(50)); 67 | 68 | // Read message from child 69 | let message = fs::read_to_string(&message_file).expect("Failed to read message file"); 70 | assert_eq!(message.trim(), "hello from child"); 71 | 72 | waitpid(child).expect("Failed to wait for child"); 73 | 74 | // Cleanup 75 | fs::remove_file(&message_file).ok(); 76 | } 77 | Ok(Fork::Child) => { 78 | // Write message 79 | fs::write(&message_file, "hello from child").expect("Failed to write message"); 80 | exit(0); 81 | } 82 | Err(_) => panic!("Fork failed"), 83 | } 84 | } 85 | 86 | #[test] 87 | fn test_fork_multiple_children() { 88 | let mut children = Vec::new(); 89 | // Tests creating and managing multiple child processes 90 | // Expected behavior: 91 | // 1. Parent creates 3 child processes sequentially 92 | // 2. Each child exits with a different exit code 93 | // 3. Parent tracks all child PIDs 94 | // 4. Parent successfully waits for all children 95 | // 5. No zombie processes remain 96 | 97 | for i in 0..3 { 98 | match fork() { 99 | Ok(Fork::Parent(child)) => { 100 | children.push(child); 101 | } 102 | Ok(Fork::Child) => { 103 | // Each child exits with different code 104 | exit(i); 105 | } 106 | Err(_) => panic!("Fork {} failed", i), 107 | } 108 | } 109 | 110 | // Parent waits for all children 111 | assert_eq!(children.len(), 3, "Should have 3 children"); 112 | 113 | for child in children { 114 | assert!(waitpid(child).is_ok(), "Failed to wait for child {}", child); 115 | } 116 | } 117 | 118 | #[test] 119 | fn test_fork_child_inherits_environment() { 120 | let test_dir = setup_test_dir(get_test_dir("fork_env")); 121 | // Tests environment variable inheritance across fork 122 | // Expected behavior: 123 | // 1. Parent sets an environment variable before fork 124 | // 2. Child inherits parent's environment 125 | // 3. Child can read the environment variable 126 | // 4. Child writes variable value to file for verification 127 | // 5. Demonstrates environment inheritance 128 | let env_file = test_dir.join("env.txt"); 129 | 130 | // Set a test environment variable 131 | let test_var = "FORK_TEST_VAR"; 132 | let test_value = "test_value_12345"; 133 | unsafe { 134 | env::set_var(test_var, test_value); 135 | } 136 | 137 | match fork() { 138 | Ok(Fork::Parent(child)) => { 139 | thread::sleep(Duration::from_millis(50)); 140 | 141 | let content = fs::read_to_string(&env_file).expect("Failed to read env file"); 142 | assert_eq!(content.trim(), test_value); 143 | 144 | waitpid(child).expect("Failed to wait for child"); 145 | 146 | // Cleanup 147 | fs::remove_file(&env_file).ok(); 148 | unsafe { 149 | env::remove_var(test_var); 150 | } 151 | } 152 | Ok(Fork::Child) => { 153 | // Child should have inherited the environment 154 | let value = env::var(test_var).expect("Environment variable not found"); 155 | fs::write(&env_file, value).expect("Failed to write env file"); 156 | exit(0); 157 | } 158 | Err(_) => panic!("Fork failed"), 159 | } 160 | } 161 | // Tests that child process can execute external commands 162 | // Expected behavior: 163 | // 1. Child process forks successfully 164 | // 2. Child executes 'echo' command 165 | // 3. Command output is captured 166 | // 4. Output is written to file 167 | // 5. Parent verifies command executed successfully 168 | 169 | #[test] 170 | fn test_fork_child_can_execute_commands() { 171 | let test_dir = get_test_dir("fork"); 172 | fs::create_dir_all(&test_dir).expect("Failed to create test directory"); 173 | let output_file = test_dir.join("command_output.txt"); 174 | 175 | match fork() { 176 | Ok(Fork::Parent(child)) => { 177 | thread::sleep(Duration::from_millis(100)); 178 | 179 | assert!(output_file.exists(), "Output file should exist"); 180 | let content = fs::read_to_string(&output_file).expect("Failed to read output"); 181 | assert!(!content.is_empty(), "Output should not be empty"); 182 | 183 | waitpid(child).expect("Failed to wait for child"); 184 | 185 | // Cleanup 186 | fs::remove_file(&output_file).ok(); 187 | } 188 | Ok(Fork::Child) => { 189 | // Execute a command and save output 190 | let output = Command::new("echo") 191 | .arg("child executed command") 192 | .output() 193 | // Tests that parent and child have unique PIDs 194 | // Expected behavior: 195 | // 1. Parent records its PID before fork 196 | // 2. Child records its PID after fork 197 | // 3. Child PID differs from parent PID 198 | // 4. fork() returns correct child PID to parent 199 | // 5. PIDs match between fork return value and actual child PID 200 | .expect("Failed to execute command"); 201 | 202 | fs::write(&output_file, &output.stdout).expect("Failed to write output"); 203 | exit(0); 204 | } 205 | Err(_) => panic!("Fork failed"), 206 | } 207 | } 208 | 209 | #[test] 210 | fn test_fork_child_has_different_pid() { 211 | let test_dir = get_test_dir("fork"); 212 | fs::create_dir_all(&test_dir).expect("Failed to create test directory"); 213 | let pid_file = test_dir.join("pids.txt"); 214 | 215 | let parent_pid = unsafe { libc::getpid() }; 216 | 217 | match fork() { 218 | Ok(Fork::Parent(child)) => { 219 | thread::sleep(Duration::from_millis(50)); 220 | 221 | let content = fs::read_to_string(&pid_file).expect("Failed to read pid file"); 222 | let child_pid: i32 = content.trim().parse().expect("Failed to parse PID"); 223 | 224 | assert_ne!( 225 | parent_pid, child_pid, 226 | "Parent and child should have different PIDs" 227 | ); 228 | assert_eq!( 229 | child, child_pid, 230 | "Child PID from fork() should match actual child PID" 231 | ); 232 | // Tests that waitpid() properly synchronizes parent-child execution 233 | // Expected behavior: 234 | // 1. Parent forks and immediately checks for marker file 235 | // 2. Marker file doesn't exist yet (child hasn't run) 236 | // 3. Parent calls waitpid() to wait for child 237 | // 4. Child creates marker file before exiting 238 | // 5. After waitpid(), marker file exists (child completed) 239 | 240 | waitpid(child).expect("Failed to wait for child"); 241 | 242 | // Cleanup 243 | fs::remove_file(&pid_file).ok(); 244 | } 245 | Ok(Fork::Child) => { 246 | let child_pid = unsafe { libc::getpid() }; 247 | fs::write(&pid_file, format!("{}", child_pid)).expect("Failed to write PID"); 248 | exit(0); 249 | } 250 | Err(_) => panic!("Fork failed"), 251 | } 252 | } 253 | 254 | #[test] 255 | fn test_waitpid_waits_for_child() { 256 | let test_dir = get_test_dir("fork"); 257 | fs::create_dir_all(&test_dir).expect("Failed to create test directory"); 258 | let marker_file = test_dir.join("wait_marker.txt"); 259 | 260 | match fork() { 261 | Ok(Fork::Parent(child)) => { 262 | // Marker should not exist yet 263 | assert!( 264 | !marker_file.exists(), 265 | "Marker should not exist before child runs" 266 | ); 267 | 268 | // Wait for child 269 | waitpid(child).expect("Failed to wait for child"); 270 | 271 | // Now marker should exist 272 | assert!( 273 | marker_file.exists(), 274 | "Marker should exist after child completes" 275 | ); 276 | 277 | // Cleanup 278 | fs::remove_file(&marker_file).ok(); 279 | } 280 | Ok(Fork::Child) => { 281 | // Sleep a bit to ensure parent checks first 282 | thread::sleep(Duration::from_millis(50)); 283 | 284 | // Create marker 285 | fs::write(&marker_file, "done").expect("Failed to write marker"); 286 | exit(0); 287 | } 288 | Err(_) => panic!("Fork failed"), 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | //! Advanced integration tests for complex fork patterns 2 | //! 3 | //! This module tests advanced usage patterns and combinations of functions. 4 | //! These tests verify: 5 | //! - Classic double-fork daemon pattern 6 | //! - Session creation and management (setsid) 7 | //! - Directory changes in child processes (chdir) 8 | //! - Process isolation between parent and child 9 | //! - Process group queries (getpgrp) 10 | //! 11 | //! These tests combine multiple fork operations to test real-world 12 | //! daemon creation patterns and process management scenarios. 13 | 14 | mod common; 15 | 16 | use common::{get_test_dir, setup_test_dir, wait_for_file}; 17 | use fork::{Fork, chdir, fork, getpgrp, setsid}; 18 | use std::{env, fs, process::exit, thread, time::Duration}; 19 | 20 | #[test] 21 | fn test_double_fork_daemon_pattern() { 22 | let test_dir = setup_test_dir(get_test_dir("int_double_fork")); 23 | let daemon_pid_file = test_dir.join("daemon.pid"); 24 | 25 | // First fork 26 | match fork().expect("First fork failed") { 27 | Fork::Parent(_child) => { 28 | // Original parent waits for daemon to create PID file 29 | // Use longer timeout for CI environments 30 | assert!( 31 | wait_for_file(&daemon_pid_file, 1000), 32 | "Daemon PID file should exist" 33 | ); 34 | 35 | // Tests the classic double-fork daemon pattern 36 | // Expected behavior: 37 | // 1. First fork creates a child process 38 | // 2. Child calls setsid() to create new session (becomes session leader) 39 | // 3. Child forks again (grandchild) 40 | // 4. First child exits (leaving grandchild orphaned) 41 | // 5. Grandchild is not session leader (prevents controlling terminal acquisition) 42 | // 6. Grandchild writes its PID to file 43 | // 7. This is the standard daemon creation pattern 44 | let pid_str = fs::read_to_string(&daemon_pid_file).expect("Failed to read PID file"); 45 | let daemon_pid: i32 = pid_str.trim().parse().expect("Failed to parse daemon PID"); 46 | assert!(daemon_pid > 0, "Daemon PID should be positive"); 47 | 48 | // Cleanup 49 | fs::remove_file(&daemon_pid_file).ok(); 50 | } 51 | Fork::Child => { 52 | // First child - create new session 53 | setsid().expect("setsid failed"); 54 | 55 | // Second fork to ensure we're not session leader 56 | match fork().expect("Second fork failed") { 57 | Fork::Parent(_) => { 58 | // First child exits 59 | exit(0); 60 | } 61 | Fork::Child => { 62 | // This is the daemon process 63 | let pid = unsafe { libc::getpid() }; 64 | let pgid = getpgrp().expect("getpgrp failed"); 65 | 66 | // Write PID to file 67 | fs::write(&daemon_pid_file, format!("{}", pid)) 68 | .expect("Failed to write daemon PID"); 69 | 70 | // Daemon should be in its own process group 71 | assert!(pgid > 0); 72 | 73 | exit(0); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | #[test] 81 | fn test_setsid_creates_new_session() { 82 | let test_dir = setup_test_dir(get_test_dir("int_double_fork")); 83 | let session_file = test_dir.join("session.info"); 84 | 85 | match fork().expect("Fork failed") { 86 | Fork::Parent(_child) => { 87 | thread::sleep(Duration::from_millis(50)); 88 | 89 | let content = fs::read_to_string(&session_file).expect("Failed to read session file"); 90 | let parts: Vec<&str> = content.trim().split(',').collect(); 91 | 92 | let sid: i32 = parts[0].parse().expect("Failed to parse SID"); 93 | let pid: i32 = parts[1].parse().expect("Failed to parse PID"); 94 | let pgid: i32 = parts[2].parse().expect("Failed to parse PGID"); 95 | 96 | // After setsid, PID should equal PGID (session leader) 97 | assert_eq!(pid, pgid, "Process should be session leader"); 98 | assert_eq!(sid, pid, "SID should equal PID for session leader"); 99 | // Tests session creation and management with setsid() 100 | // Expected behavior: 101 | // 1. Child process calls setsid() 102 | // 2. setsid() creates new session and returns SID 103 | // 3. Child becomes session leader (PID == PGID == SID) 104 | // 4. This is Step 2 of the daemon pattern 105 | // 5. Child writes SID, PID, PGID to file for verification 106 | 107 | // Cleanup 108 | fs::remove_file(&session_file).ok(); 109 | } 110 | Fork::Child => { 111 | // Create new session 112 | let sid = setsid().expect("setsid failed"); 113 | let pid = unsafe { libc::getpid() }; 114 | let pgid = getpgrp().expect("getpgrp failed"); 115 | 116 | fs::write(&session_file, format!("{},{},{}", sid, pid, pgid)) 117 | .expect("Failed to write session info"); 118 | 119 | exit(0); 120 | } 121 | } 122 | } 123 | 124 | #[test] 125 | fn test_chdir_changes_directory() { 126 | let test_dir = setup_test_dir(get_test_dir("int_chdir")); 127 | let dir_file = test_dir.join("directory.info"); 128 | 129 | match fork().expect("Fork failed") { 130 | Fork::Parent(_child) => { 131 | thread::sleep(Duration::from_millis(50)); 132 | // Tests directory change in child process 133 | // Expected behavior: 134 | // 1. Child process calls chdir() 135 | // 2. chdir() changes current directory to root (/) 136 | // 3. Child verifies current directory is / 137 | // 4. Child writes directory path to file 138 | // 5. Parent verifies child changed directory correctly 139 | 140 | let content = fs::read_to_string(&dir_file).expect("Failed to read dir file"); 141 | assert_eq!(content.trim(), "/", "Directory should be root"); 142 | 143 | // Cleanup 144 | fs::remove_file(&dir_file).ok(); 145 | } 146 | Fork::Child => { 147 | // Change to root 148 | chdir().expect("chdir failed"); 149 | 150 | let current = env::current_dir().expect("Failed to get current dir"); 151 | fs::write(&dir_file, current.to_str().unwrap()) 152 | .expect("Failed to write directory info"); 153 | 154 | exit(0); 155 | } 156 | } 157 | } 158 | 159 | #[test] 160 | // Tests process isolation between parent and child 161 | // Expected behavior: 162 | // 1. Parent writes data to file before fork 163 | // 2. Child can see parent's file (same filesystem) 164 | // 3. Child writes its own file 165 | // 4. Parent can see child's file after fork completes 166 | // 5. Both processes can access shared filesystem but have separate memory 167 | fn test_process_isolation() { 168 | let test_dir = setup_test_dir(get_test_dir("int_isolation")); 169 | let parent_file = test_dir.join("parent.txt"); 170 | let child_file = test_dir.join("child.txt"); 171 | 172 | // Parent writes before fork 173 | fs::write(&parent_file, "parent data").expect("Failed to write parent file"); 174 | 175 | match fork().expect("Fork failed") { 176 | Fork::Parent(_child) => { 177 | thread::sleep(Duration::from_millis(50)); 178 | 179 | // Parent file should still exist 180 | assert!(parent_file.exists(), "Parent file should exist"); 181 | 182 | // Child should have created its own file 183 | assert!(child_file.exists(), "Child file should exist"); 184 | 185 | let child_content = fs::read_to_string(&child_file).expect("Failed to read child file"); 186 | assert_eq!(child_content.trim(), "child data"); 187 | 188 | // Cleanup 189 | fs::remove_file(&parent_file).ok(); 190 | fs::remove_file(&child_file).ok(); 191 | } 192 | Fork::Child => { 193 | // Child can see parent's file 194 | assert!(parent_file.exists(), "Child should see parent file"); 195 | 196 | // Child writes its own file 197 | fs::write(&child_file, "child data").expect("Failed to write child file"); 198 | 199 | exit(0); 200 | // Tests process group queries with getpgrp() 201 | // Expected behavior: 202 | // 1. Both parent and child can call getpgrp() 203 | // 2. Both return valid positive PGID values 204 | // 3. Initially parent and child share same process group 205 | // 4. Used to verify process group membership 206 | // 5. Critical for session and job control 207 | } 208 | } 209 | } 210 | 211 | #[test] 212 | fn test_getpgrp_returns_process_group() { 213 | match fork().expect("Fork failed") { 214 | Fork::Parent(_child) => { 215 | let parent_pgid = getpgrp().expect("getpgrp failed"); 216 | assert!(parent_pgid > 0, "Parent PGID should be positive"); 217 | 218 | thread::sleep(Duration::from_millis(50)); 219 | } 220 | Fork::Child => { 221 | let child_pgid = getpgrp().expect("getpgrp failed"); 222 | assert!(child_pgid > 0, "Child PGID should be positive"); 223 | 224 | exit(0); 225 | } 226 | } 227 | } 228 | --------------------------------------------------------------------------------