use std::ffi::OsStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvKind {
OwnedFlag,
ForeignFlag,
OptOutFlag,
ExactValue(&'static str),
Path,
Text,
Number { zero_selects_default: bool },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Owner {
Crate,
Foreign,
}
#[derive(Debug, Clone, Copy)]
pub struct EnvVar {
pub name: &'static str,
pub kind: EnvKind,
pub owner: Owner,
pub default: &'static str,
pub summary: &'static str,
}
impl EnvVar {
pub fn is_set(&self) -> bool {
match self.kind {
EnvKind::OwnedFlag => flag_owned(self.name),
EnvKind::ForeignFlag => flag_foreign(self.name),
EnvKind::OptOutFlag => flag_opt_out(self.name),
EnvKind::ExactValue(expected) => {
std::env::var_os(self.name).is_some_and(|value| value == OsStr::new(expected))
}
other => panic!("{} is declared as {other:?}, not a flag", self.name),
}
}
}
impl EnvVar {
pub fn count_or(&self, default: usize) -> usize {
self.parsed::<usize>().unwrap_or(default)
}
pub fn millis_or(&self, default: std::time::Duration) -> std::time::Duration {
self.parsed::<u64>()
.map(std::time::Duration::from_millis)
.unwrap_or(default)
}
pub fn port(&self) -> Option<u16> {
self.parsed::<u16>()
}
pub fn text(&self) -> Option<String> {
std::env::var(self.name)
.ok()
.filter(|value| !value.is_empty())
}
pub fn path(&self) -> Option<std::path::PathBuf> {
std::env::var_os(self.name)
.filter(|value| !value.is_empty())
.map(std::path::PathBuf::from)
}
fn parsed<T>(&self) -> Option<T>
where
T: std::str::FromStr + Default + PartialEq,
{
let EnvKind::Number {
zero_selects_default,
} = self.kind
else {
panic!("{} is declared as {:?}, not a number", self.name, self.kind);
};
let parsed: T = std::env::var(self.name)
.ok()?
.trim()
.parse()
.ok()
.filter(|value: &T| !(zero_selects_default && *value == T::default()))?;
Some(parsed)
}
}
const AFFIRMATIVE: &[&str] = &["1", "true", "yes", "on"];
const NEGATIVE: &[&str] = &["", "0", "false", "no", "off"];
pub fn flag_owned(name: &str) -> bool {
match std::env::var_os(name) {
Some(value) => AFFIRMATIVE.contains(&normalize(&value).as_str()),
None => false,
}
}
pub fn flag_foreign(name: &str) -> bool {
match std::env::var_os(name) {
Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
None => false,
}
}
pub fn flag_opt_out(name: &str) -> bool {
match std::env::var_os(name) {
Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
None => true,
}
}
pub fn value_is_affirmative_foreign(value: &str) -> bool {
!NEGATIVE.contains(&value.trim().to_ascii_lowercase().as_str())
}
fn normalize(value: &OsStr) -> String {
value.to_string_lossy().trim().to_ascii_lowercase()
}
macro_rules! declare {
($($ident:ident => $name:literal, $kind:expr, $owner:expr, $default:literal, $summary:literal;)*) => {
$(
#[doc = $summary]
#[doc = concat!("Environment variable `", $name, "`. Unset: ", $default, ".")]
pub const $ident: EnvVar = EnvVar {
name: $name,
kind: $kind,
owner: $owner,
default: $default,
summary: $summary,
};
)*
pub const DECLARED: &[EnvVar] = &[$($ident),*];
};
}
declare! {
GITHUB_ACTIONS => "GITHUB_ACTIONS",
EnvKind::ForeignFlag, Owner::Foreign, "not running under GitHub Actions",
"Set by GitHub Actions; tests wait longer for a shared runner.";
INVOCATION_ID => "INVOCATION_ID",
EnvKind::Text, Owner::Foreign, "not started by systemd",
"Set by systemd for a unit invocation; identifies the launching unit.";
LOCALAPPDATA => "LOCALAPPDATA",
EnvKind::Path, Owner::Foreign, "the platform default is derived",
"Windows per-user application data root.";
PATH => "PATH",
EnvKind::Text, Owner::Foreign, "the child inherits no explicit PATH",
"Executable search path, forwarded to the symbolization worker.";
BROKER_ALLOW_PRIVILEGED => "RUNNING_PROCESS_BROKER_ALLOW_PRIVILEGED",
EnvKind::ExactValue("1"), Owner::Crate, "privileged startup is refused",
"Opt out of the broker's refusal to start as root or LocalSystem.";
BROKER_CLIENT_TIMEOUT_MS => "RUNNING_PROCESS_BROKER_CLIENT_TIMEOUT_MS",
EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in client timeout",
"Broker client request timeout, in milliseconds.";
BROKER_CRASH_DUMP_DIR => "RUNNING_PROCESS_BROKER_CRASH_DUMP_DIR",
EnvKind::Path, Owner::Crate, "the standard diagnostic-artifact location",
"Where broker crash dumps are written.";
BROKER_HELLO_PERF_GUARD => "RUNNING_PROCESS_BROKER_HELLO_PERF_GUARD",
EnvKind::OwnedFlag, Owner::Crate, "the guard does not run",
"Run the broker Hello latency guard.";
BROKER_HELLO_TIMEOUT_MS => "RUNNING_PROCESS_BROKER_HELLO_TIMEOUT_MS",
EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in Hello timeout",
"Broker Hello handshake timeout, in milliseconds.";
BROKER_HTTP_BIND => "RUNNING_PROCESS_BROKER_HTTP_BIND",
EnvKind::Text, Owner::Crate, "the loopback bind address",
"Bind address for the broker HTTP aggregator.";
BROKER_HTTP_PORT => "RUNNING_PROCESS_BROKER_HTTP_PORT",
EnvKind::Number { zero_selects_default: false }, Owner::Crate, "an ephemeral port",
"Port for the broker HTTP aggregator.";
BROKER_LISTENER_FD => "RUNNING_PROCESS_BROKER_LISTENER_FD",
EnvKind::Number { zero_selects_default: false }, Owner::Foreign, "the daemon binds its own endpoint",
"Descriptor of a listening socket the broker already bound and passed.";
BROKER_MAX_INFLIGHT_HANDLERS => "RUNNING_PROCESS_BROKER_MAX_INFLIGHT_HANDLERS",
EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in concurrency cap",
"Maximum broker request handlers running at once.";
BROKER_OWNED_BIND => "RUNNING_PROCESS_BROKER_OWNED_BIND",
EnvKind::OptOutFlag, Owner::Crate, "broker-owned bind is used",
"Escape hatch: set falsy to fall back to spawn-then-probe.";
BROKER_V1_BACKEND_NAMESPACE => "RUNNING_PROCESS_BROKER_V1_BACKEND_NAMESPACE",
EnvKind::Text, Owner::Foreign, "no namespace is applied",
"Backend namespace handed to a v1 broker backend.";
BROKER_V1_BACKEND_PIPE => "RUNNING_PROCESS_BROKER_V1_BACKEND_PIPE",
EnvKind::Text, Owner::Foreign, "the backend derives its own endpoint",
"Endpoint a v1 broker backend should serve on.";
BROKER_V1_INSTANCE => "RUNNING_PROCESS_BROKER_V1_INSTANCE",
EnvKind::Text, Owner::Foreign, "the default instance",
"Instance identifier for a v1 broker backend.";
BROKER_V1_SERVICE_NAME => "RUNNING_PROCESS_BROKER_V1_SERVICE_NAME",
EnvKind::Text, Owner::Foreign, "the backend supplies its own name",
"Service name a v1 broker backend registers under.";
BROKER_V1_SERVICE_VERSION => "RUNNING_PROCESS_BROKER_V1_SERVICE_VERSION",
EnvKind::Text, Owner::Foreign, "the backend supplies its own version",
"Service version a v1 broker backend reports.";
BROKER_V1_SESSION_TOKEN => "RUNNING_PROCESS_BROKER_V1_SESSION_TOKEN",
EnvKind::Text, Owner::Foreign, "no session token is presented",
"Session token a v1 broker backend presents to the broker.";
BROKER_V1_SOCKET => "RUNNING_PROCESS_BROKER_V1_SOCKET",
EnvKind::Text, Owner::Foreign, "the standard broker endpoint",
"Broker endpoint a v1 backend dials.";
BROKER_V1_TRACEPARENT => "RUNNING_PROCESS_BROKER_V1_TRACEPARENT",
EnvKind::Text, Owner::Foreign, "no trace context is propagated",
"W3C traceparent propagated into a v1 broker backend.";
BROKER_V1_TRACESTATE => "RUNNING_PROCESS_BROKER_V1_TRACESTATE",
EnvKind::Text, Owner::Foreign, "no trace state is propagated",
"W3C tracestate propagated into a v1 broker backend.";
CHILD_PID_LOG_PATH => "RUNNING_PROCESS_CHILD_PID_LOG_PATH",
EnvKind::Path, Owner::Foreign, "spawned child PIDs are not logged",
"Append each spawned child PID to this file (test harness seam).";
CLIENT_CONNECT_TIMEOUT_MS => "RUNNING_PROCESS_CLIENT_CONNECT_TIMEOUT_MS",
EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in connect timeout",
"Daemon client connect timeout, in milliseconds.";
CLIENT_RPC_TIMEOUT_MS => "RUNNING_PROCESS_CLIENT_RPC_TIMEOUT_MS",
EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in RPC timeout",
"Daemon client RPC timeout, in milliseconds.";
DAEMON_SCOPE => "RUNNING_PROCESS_DAEMON_SCOPE",
EnvKind::Text, Owner::Crate, "the user-wide scope",
"Daemon scope selector; `dev` gives a CWD-scoped daemon for tests.";
DAEMON_SHADOWED => "RUNNING_PROCESS_DAEMON_SHADOWED",
EnvKind::OwnedFlag, Owner::Crate, "a dev-build daemon relocates itself",
"Marks a daemon already running from its shadow copy.";
DAEMON_START_TIMEOUT_MS => "RUNNING_PROCESS_DAEMON_START_TIMEOUT_MS",
EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in 750ms budget",
"How long a client waits for a freshly spawned daemon to bind its socket, in milliseconds.";
DISABLE => "RUNNING_PROCESS_DISABLE",
EnvKind::ExactValue("1"), Owner::Crate, "the broker is used",
"Canonical escape hatch: bypass the broker entirely.";
FAKE_BACKEND => "RUNNING_PROCESS_FAKE_BACKEND",
EnvKind::Path, Owner::Foreign, "backends are reached through the broker",
"TEST-ONLY: dial this endpoint directly, skipping broker negotiation.";
IS_DAEMON => "RUNNING_PROCESS_IS_DAEMON",
EnvKind::ForeignFlag, Owner::Crate, "the process is not a daemon",
"Marks a process spawned as a daemon, for originator reaping.";
KILL_DRAIN_TIMEOUT_MS => "RUNNING_PROCESS_KILL_DRAIN_TIMEOUT_MS",
EnvKind::Number { zero_selects_default: false }, Owner::Crate, "two seconds",
"How long `kill()` waits for output capture to drain, in milliseconds.";
MANIFEST_DIR => "RUNNING_PROCESS_MANIFEST_DIR",
EnvKind::Path, Owner::Foreign, "the standard manifest location",
"Where broker cache manifests are read and written.";
NO_TRACKING => "RUNNING_PROCESS_NO_TRACKING",
EnvKind::OwnedFlag, Owner::Crate, "processes are tracked",
"Disable daemon IPC and process tracking.";
ORIGINATOR => "RUNNING_PROCESS_ORIGINATOR",
EnvKind::Text, Owner::Foreign, "the originator is inferred",
"Identifies the process that originated a spawn tree.";
SERVICE_DEF_DIR => "RUNNING_PROCESS_SERVICE_DEF_DIR",
EnvKind::Path, Owner::Foreign, "the standard service-definition location",
"Where service definitions are read from.";
TMPDIR => "TMPDIR",
EnvKind::Path, Owner::Foreign, "the platform temporary directory",
"macOS per-session temporary directory; a broker endpoint root.";
USERNAME => "USERNAME",
EnvKind::Text, Owner::Foreign, "the endpoint is named `unknown`",
"Windows account name, mixed into the daemon pipe name.";
XDG_CONFIG_HOME => "XDG_CONFIG_HOME",
EnvKind::Path, Owner::Foreign, "`~/.config` is used",
"XDG per-user configuration root; where service definitions are read.";
XDG_DATA_HOME => "XDG_DATA_HOME",
EnvKind::Path, Owner::Foreign, "the platform default is derived",
"XDG per-user data root, used by the daemon runtime collector.";
XDG_RUNTIME_DIR => "XDG_RUNTIME_DIR",
EnvKind::Path, Owner::Foreign, "a per-user directory under /tmp",
"XDG per-user runtime root; where broker sockets are placed.";
}
#[cfg(test)]
#[path = "tests/env_vars.rs"]
mod tests;