Skip to content

Commit b5ab2f2

Browse files
committed
Fix thread unsafety due to internal use of set_var()
1 parent f0c102c commit b5ab2f2

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
@@ -13,7 +13,9 @@ use std::fs::{File, OpenOptions};
1313
use std::io::{Error, ErrorKind, Result};
1414
use std::path::{Path, PathBuf};
1515
use std::process::Command;
16-
use std::sync::Mutex;
16+
use std::sync::atomic::AtomicBool;
17+
use std::sync::atomic::Ordering::SeqCst;
18+
use std::sync::{LazyLock, Mutex};
1719
use std::thread;
1820

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

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

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

105119
pub(crate) fn debug_enabled() -> bool {
106-
std::env::var("CMD_LIB_DEBUG") == Ok("1".into())
120+
DEBUG_ENABLED.load(SeqCst)
107121
}
108122

109123
pub(crate) fn pipefail_enabled() -> bool {
110-
std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into())
124+
PIPEFAIL_ENABLED.load(SeqCst)
111125
}
112126

113127
#[doc(hidden)]

0 commit comments

Comments
 (0)