Skip to content

Fix thread unsafety due to internal use of set_var() #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,10 @@ You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fe
key from the current process. It will report error if the environment variable is not present, and it also
includes other checks to avoid silent failures.

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

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

#### Thread Safety

This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine.
The only known APIs not supported in multi-thread environment are the
[`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.
This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine.
That said, there are some limitations to be aware of:

- [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program
- [`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), and
[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which means
each thread will have its own independent version of the variable
- [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and
[`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads;
there is currently no way to change those settings without affecting other threads

[std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html
[std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html
[must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var


License: MIT OR Apache-2.0
26 changes: 18 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,10 @@
//! key from the current process. It will report error if the environment variable is not present, and it also
//! includes other checks to avoid silent failures.
//!
//! To set environment variables, you can use [std::env::set_var](https://doc.rust-lang.org/std/env/fn.set_var.html).
//! There are also other related APIs in the [std::env](https://doc.rust-lang.org/std/env/index.html) module.
//! To set environment variables in **single-threaded programs**, you can use [std::env::set_var] and
//! [std::env::remove_var]. While those functions **[must not be called]** if any other threads might be running, you can
//! always set environment variables for one command at a time, by putting the assignments before the command:
//!
//! To set environment variables for the command only, you can put the assignments before the command.
//! Like this:
//! ```no_run
//! # use cmd_lib::run_cmd;
//! run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?;
Expand All @@ -364,10 +363,21 @@
//!
//! ### Thread Safety
//!
//! This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine.
//! The only known APIs not supported in multi-thread environment are the
//! [`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.
//!
//! This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine.
//! That said, there are some limitations to be aware of:
//!
//! - [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program
//! - [`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), and
//! [`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which
//! means each thread will have its own independent version of the variable
//! - [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and
//! [`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads;
//! there is currently no way to change those settings without affecting other threads
//!
//! [std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html
//! [std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html
//! [must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var

pub use cmd_lib_macros::{
cmd_die, main, run_cmd, run_fun, spawn, spawn_with_output, use_custom_cmd,
Expand Down
28 changes: 21 additions & 7 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use std::fs::{File, OpenOptions};
use std::io::{Error, ErrorKind, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::{LazyLock, Mutex};
use std::thread;

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

static DEBUG_ENABLED: LazyLock<AtomicBool> =
LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_DEBUG") == Ok("1".into())));

static PIPEFAIL_ENABLED: LazyLock<AtomicBool> =
LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into())));

/// Set debug mode or not, false by default.
///
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect
/// This is **global**, and affects all threads.
///
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect, but the environment variable is only
/// checked once at an unspecified time, so the only reliable way to do that is when the program is first started.
pub fn set_debug(enable: bool) {
std::env::set_var("CMD_LIB_DEBUG", if enable { "1" } else { "0" });
DEBUG_ENABLED.store(enable, SeqCst);
}

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

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

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

#[doc(hidden)]
Expand Down