Skip to content

Commit 171690e

Browse files
authored
Merge pull request #84 from delan/fix-thread-unsafety
Fix thread unsafety due to internal use of set_var()
2 parents c5c50e2 + b5ab2f2 commit 171690e

File tree

3 files changed

+57
-22
lines changed

3 files changed

+57
-22
lines changed

README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,10 @@ You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fe
304304
key from the current process. It will report error if the environment variable is not present, and it also
305305
includes other checks to avoid silent failures.
306306

307-
To set environment variables, you can use [std::env::set_var](https://doc.rust-lang.org/std/env/fn.set_var.html).
308-
There are also other related APIs in the [std::env](https://doc.rust-lang.org/std/env/index.html) module.
307+
To set environment variables in **single-threaded programs**, you can use [std::env::set_var] and
308+
[std::env::remove_var]. While those functions **[must not be called]** if any other threads might be running, you can
309+
always set environment variables for one command at a time, by putting the assignments before the command:
309310

310-
To set environment variables for the command only, you can put the assignments before the command.
311-
Like this:
312311
```rust
313312
run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?;
314313
```
@@ -330,9 +329,21 @@ You can use the [glob](https://github.com/rust-lang-nursery/glob) package instea
330329

331330
#### Thread Safety
332331

333-
This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine.
334-
The only known APIs not supported in multi-thread environment are the
335-
[`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html)/[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html)/[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) macros, and you should only use them for *thread local* variables.
332+
This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine.
333+
That said, there are some limitations to be aware of:
334+
335+
- [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program
336+
- [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html),
337+
[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html), and
338+
[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which means
339+
each thread will have its own independent version of the variable
340+
- [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and
341+
[`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads;
342+
there is currently no way to change those settings without affecting other threads
343+
344+
[std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html
345+
[std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html
346+
[must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var
336347

337348

338349
License: MIT OR Apache-2.0

src/lib.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -334,11 +334,10 @@
334334
//! key from the current process. It will report error if the environment variable is not present, and it also
335335
//! includes other checks to avoid silent failures.
336336
//!
337-
//! To set environment variables, you can use [std::env::set_var](https://doc.rust-lang.org/std/env/fn.set_var.html).
338-
//! There are also other related APIs in the [std::env](https://doc.rust-lang.org/std/env/index.html) module.
337+
//! To set environment variables in **single-threaded programs**, you can use [std::env::set_var] and
338+
//! [std::env::remove_var]. While those functions **[must not be called]** if any other threads might be running, you can
339+
//! always set environment variables for one command at a time, by putting the assignments before the command:
339340
//!
340-
//! To set environment variables for the command only, you can put the assignments before the command.
341-
//! Like this:
342341
//! ```no_run
343342
//! # use cmd_lib::run_cmd;
344343
//! run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?;
@@ -364,10 +363,21 @@
364363
//!
365364
//! ### Thread Safety
366365
//!
367-
//! This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine.
368-
//! The only known APIs not supported in multi-thread environment are the
369-
//! [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html)/[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html)/[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) macros, and you should only use them for *thread local* variables.
370-
//!
366+
//! This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine.
367+
//! That said, there are some limitations to be aware of:
368+
//!
369+
//! - [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program
370+
//! - [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html),
371+
//! [`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html), and
372+
//! [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which
373+
//! means each thread will have its own independent version of the variable
374+
//! - [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and
375+
//! [`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads;
376+
//! there is currently no way to change those settings without affecting other threads
377+
//!
378+
//! [std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html
379+
//! [std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html
380+
//! [must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var
371381
372382
pub use cmd_lib_macros::{
373383
cmd_die, main, run_cmd, run_fun, spawn, spawn_with_output, use_custom_cmd,

src/process.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ use std::io::{Error, ErrorKind, Result};
1414
use std::mem::take;
1515
use std::path::{Path, PathBuf};
1616
use std::process::Command;
17-
use std::sync::Mutex;
17+
use std::sync::atomic::AtomicBool;
18+
use std::sync::atomic::Ordering::SeqCst;
19+
use std::sync::{LazyLock, Mutex};
1820
use std::thread;
1921

2022
const CD_CMD: &str = "cd";
@@ -89,26 +91,38 @@ pub fn register_cmd(cmd: &'static str, func: FnFun) {
8991
CMD_MAP.lock().unwrap().insert(OsString::from(cmd), func);
9092
}
9193

94+
static DEBUG_ENABLED: LazyLock<AtomicBool> =
95+
LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_DEBUG") == Ok("1".into())));
96+
97+
static PIPEFAIL_ENABLED: LazyLock<AtomicBool> =
98+
LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into())));
99+
92100
/// Set debug mode or not, false by default.
93101
///
94-
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect
102+
/// This is **global**, and affects all threads.
103+
///
104+
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect, but the environment variable is only
105+
/// checked once at an unspecified time, so the only reliable way to do that is when the program is first started.
95106
pub fn set_debug(enable: bool) {
96-
std::env::set_var("CMD_LIB_DEBUG", if enable { "1" } else { "0" });
107+
DEBUG_ENABLED.store(enable, SeqCst);
97108
}
98109

99110
/// Set pipefail or not, true by default.
100111
///
101-
/// Setting environment variable CMD_LIB_PIPEFAIL=0|1 has the same effect
112+
/// This is **global**, and affects all threads.
113+
///
114+
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect, but the environment variable is only
115+
/// checked once at an unspecified time, so the only reliable way to do that is when the program is first started.
102116
pub fn set_pipefail(enable: bool) {
103-
std::env::set_var("CMD_LIB_PIPEFAIL", if enable { "1" } else { "0" });
117+
PIPEFAIL_ENABLED.store(enable, SeqCst);
104118
}
105119

106120
pub(crate) fn debug_enabled() -> bool {
107-
std::env::var("CMD_LIB_DEBUG") == Ok("1".into())
121+
DEBUG_ENABLED.load(SeqCst)
108122
}
109123

110124
pub(crate) fn pipefail_enabled() -> bool {
111-
std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into())
125+
PIPEFAIL_ENABLED.load(SeqCst)
112126
}
113127

114128
#[doc(hidden)]

0 commit comments

Comments
 (0)