Skip to main content

linuxutils_system/
flock.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use rustix::{
7    fd::{AsFd, BorrowedFd, OwnedFd},
8    fs::{FlockOperation, OFlags, flock},
9};
10use std::{
11    os::unix::process::CommandExt,
12    process,
13    process::ExitCode,
14    time::{Duration, Instant},
15};
16
17#[derive(Parser)]
18#[command(
19    name = "flock",
20    about = "Manage locks from shell scripts",
21    override_usage = "flock [options] <file>|<directory> <command> [<argument>...]\n       \
22                      flock [options] <file>|<directory> -c <command>\n       \
23                      flock [options] <file descriptor number>"
24)]
25pub struct Args {
26    /// Get a shared lock
27    #[arg(short, long)]
28    shared: bool,
29
30    /// Get an exclusive lock (default)
31    #[arg(short = 'x', short_alias = 'e', long)]
32    exclusive: bool,
33
34    /// Remove a lock
35    #[arg(short, long)]
36    unlock: bool,
37
38    /// Fail rather than wait if the lock cannot be immediately acquired
39    #[arg(short, long, alias = "nb")]
40    nonblock: bool,
41
42    /// Wait at most this many seconds; decimal fractions allowed
43    #[arg(short = 'w', long, value_name = "secs", alias = "wait")]
44    timeout: Option<f64>,
45
46    /// Exit code when the lock cannot be acquired (default: 1)
47    #[arg(short = 'E', long, value_name = "number", default_value = "1")]
48    conflict_exit_code: u8,
49
50    /// Close the file descriptor before executing the command
51    #[arg(short = 'o', long)]
52    close: bool,
53
54    /// Execute command without forking
55    #[arg(short = 'F', long)]
56    no_fork: bool,
57
58    /// Run a single command string through the shell
59    #[arg(short = 'c', long, value_name = "command")]
60    command: Option<String>,
61
62    /// Increase verbosity
63    #[arg(long)]
64    verbose: bool,
65
66    /// File, directory, or open file descriptor number
67    #[arg(required = true)]
68    file: String,
69
70    /// Command and arguments to execute
71    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
72    args: Vec<String>,
73}
74
75fn acquire_lock(
76    fd: BorrowedFd,
77    op: FlockOperation,
78    nonblock: bool,
79    timeout: Option<f64>,
80    conflict_exit_code: u8,
81    verbose: bool,
82) -> Result<(), ExitCode> {
83    let effective_nonblock = nonblock || matches!(timeout, Some(t) if t == 0.0);
84
85    if effective_nonblock {
86        let nb_op = match op {
87            FlockOperation::LockShared => FlockOperation::NonBlockingLockShared,
88            FlockOperation::LockExclusive => {
89                FlockOperation::NonBlockingLockExclusive
90            }
91            other => other,
92        };
93        flock(fd, nb_op).map_err(|_| ExitCode::from(conflict_exit_code))?;
94        return Ok(());
95    }
96
97    if let Some(secs) = timeout {
98        let deadline = Instant::now() + Duration::from_secs_f64(secs);
99        let nb_op = match op {
100            FlockOperation::LockShared => FlockOperation::NonBlockingLockShared,
101            FlockOperation::LockExclusive => {
102                FlockOperation::NonBlockingLockExclusive
103            }
104            other => other,
105        };
106        loop {
107            match flock(fd, nb_op) {
108                Ok(()) => return Ok(()),
109                Err(_) => {
110                    let now = Instant::now();
111                    if now >= deadline {
112                        if verbose {
113                            eprintln!(
114                                "flock: timeout while waiting to get lock"
115                            );
116                        }
117                        return Err(ExitCode::from(conflict_exit_code));
118                    }
119                    let remaining = deadline - now;
120                    std::thread::sleep(
121                        remaining.min(Duration::from_millis(100)),
122                    );
123                }
124            }
125        }
126    }
127
128    // Blocking acquire.
129    flock(fd, op).map_err(|e| {
130        eprintln!("flock: {e}");
131        ExitCode::FAILURE
132    })
133}
134
135pub fn run(args: Args) -> ExitCode {
136    // Determine lock operation.
137    let lock_op = if args.unlock {
138        FlockOperation::Unlock
139    } else if args.shared {
140        FlockOperation::LockShared
141    } else {
142        FlockOperation::LockExclusive
143    };
144
145    // Determine if `file` is a bare file descriptor number or a path.
146    let is_fd_num = args.file.parse::<i32>().is_ok()
147        && args.args.is_empty()
148        && args.command.is_none();
149
150    let owned_fd: Option<OwnedFd>;
151    let borrowed: BorrowedFd;
152
153    if is_fd_num {
154        let n: i32 = args.file.parse().unwrap();
155        // SAFETY: the caller is responsible for passing a valid open fd.
156        owned_fd = None;
157        borrowed = unsafe { BorrowedFd::borrow_raw(n) };
158    } else {
159        let flags = OFlags::RDWR | OFlags::CREATE | OFlags::NOFOLLOW;
160        let perms = rustix::fs::Mode::RUSR
161            | rustix::fs::Mode::WUSR
162            | rustix::fs::Mode::RGRP
163            | rustix::fs::Mode::WGRP
164            | rustix::fs::Mode::ROTH
165            | rustix::fs::Mode::WOTH;
166
167        // Try RDWR first; fall back to RDONLY for read-only files/dirs.
168        let fd = rustix::fs::open(&args.file, flags, perms)
169            .or_else(|_| {
170                rustix::fs::open(
171                    &args.file,
172                    OFlags::RDONLY | OFlags::CREATE | OFlags::NOFOLLOW,
173                    perms,
174                )
175            })
176            .or_else(|_| {
177                // For directories, RDONLY without CREAT.
178                rustix::fs::open(
179                    &args.file,
180                    OFlags::RDONLY,
181                    rustix::fs::Mode::empty(),
182                )
183            });
184
185        match fd {
186            Ok(f) => {
187                owned_fd = Some(f);
188                borrowed = owned_fd.as_ref().unwrap().as_fd();
189            }
190            Err(e) => {
191                eprintln!("flock: {}: {e}", args.file);
192                return ExitCode::FAILURE;
193            }
194        }
195    };
196
197    if args.verbose {
198        eprintln!("flock: getting lock...");
199    }
200
201    if let Err(code) = acquire_lock(
202        borrowed,
203        lock_op,
204        args.nonblock,
205        args.timeout,
206        args.conflict_exit_code,
207        args.verbose,
208    ) {
209        return code;
210    }
211
212    if args.verbose {
213        eprintln!("flock: got lock");
214    }
215
216    // Build the command to run.
217    let cmd_parts: Vec<String> = if let Some(ref cmd) = args.command {
218        vec!["sh".to_string(), "-c".to_string(), cmd.clone()]
219    } else if !args.args.is_empty() {
220        args.args.clone()
221    } else {
222        // No command — just hold the lock until this process exits.
223        return ExitCode::SUCCESS;
224    };
225
226    let (prog, prog_args) = cmd_parts.split_first().unwrap();
227
228    if args.close {
229        drop(owned_fd);
230    }
231
232    if args.no_fork {
233        let err = process::Command::new(prog).args(prog_args).exec();
234        eprintln!("flock: {prog}: {err}");
235        return ExitCode::FAILURE;
236    }
237
238    match process::Command::new(prog).args(prog_args).status() {
239        Ok(status) => {
240            let code = status.code().unwrap_or(1) as u8;
241            ExitCode::from(code)
242        }
243        Err(e) => {
244            eprintln!("flock: {prog}: {e}");
245            ExitCode::FAILURE
246        }
247    }
248}