linuxutils_system/
flock.rs1use 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 #[arg(short, long)]
28 shared: bool,
29
30 #[arg(short = 'x', short_alias = 'e', long)]
32 exclusive: bool,
33
34 #[arg(short, long)]
36 unlock: bool,
37
38 #[arg(short, long, alias = "nb")]
40 nonblock: bool,
41
42 #[arg(short = 'w', long, value_name = "secs", alias = "wait")]
44 timeout: Option<f64>,
45
46 #[arg(short = 'E', long, value_name = "number", default_value = "1")]
48 conflict_exit_code: u8,
49
50 #[arg(short = 'o', long)]
52 close: bool,
53
54 #[arg(short = 'F', long)]
56 no_fork: bool,
57
58 #[arg(short = 'c', long, value_name = "command")]
60 command: Option<String>,
61
62 #[arg(long)]
64 verbose: bool,
65
66 #[arg(required = true)]
68 file: String,
69
70 #[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 flock(fd, op).map_err(|e| {
130 eprintln!("flock: {e}");
131 ExitCode::FAILURE
132 })
133}
134
135pub fn run(args: Args) -> ExitCode {
136 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 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 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 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 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 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 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}