1use std::ffi::OsStr;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum EnvKind {
44 OwnedFlag,
46 ForeignFlag,
49 OptOutFlag,
52 ExactValue(&'static str),
56 Path,
58 Text,
60 Number { zero_selects_default: bool },
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum Owner {
75 Crate,
77 Foreign,
79}
80
81#[derive(Debug, Clone, Copy)]
83pub struct EnvVar {
84 pub name: &'static str,
86 pub kind: EnvKind,
88 pub owner: Owner,
90 pub default: &'static str,
92 pub summary: &'static str,
94}
95
96impl EnvVar {
97 pub fn is_set(&self) -> bool {
104 match self.kind {
105 EnvKind::OwnedFlag => flag_owned(self.name),
106 EnvKind::ForeignFlag => flag_foreign(self.name),
107 EnvKind::OptOutFlag => flag_opt_out(self.name),
108 EnvKind::ExactValue(expected) => {
109 std::env::var_os(self.name).is_some_and(|value| value == OsStr::new(expected))
110 }
111 other => panic!("{} is declared as {other:?}, not a flag", self.name),
112 }
113 }
114}
115
116impl EnvVar {
117 pub fn count_or(&self, default: usize) -> usize {
122 self.parsed::<usize>().unwrap_or(default)
123 }
124
125 pub fn millis_or(&self, default: std::time::Duration) -> std::time::Duration {
127 self.parsed::<u64>()
128 .map(std::time::Duration::from_millis)
129 .unwrap_or(default)
130 }
131
132 pub fn port(&self) -> Option<u16> {
134 self.parsed::<u16>()
135 }
136
137 pub fn text(&self) -> Option<String> {
139 std::env::var(self.name)
140 .ok()
141 .filter(|value| !value.is_empty())
142 }
143
144 pub fn path(&self) -> Option<std::path::PathBuf> {
149 std::env::var_os(self.name)
150 .filter(|value| !value.is_empty())
151 .map(std::path::PathBuf::from)
152 }
153
154 fn parsed<T>(&self) -> Option<T>
160 where
161 T: std::str::FromStr + Default + PartialEq,
162 {
163 let EnvKind::Number {
164 zero_selects_default,
165 } = self.kind
166 else {
167 panic!("{} is declared as {:?}, not a number", self.name, self.kind);
168 };
169 let parsed: T = std::env::var(self.name)
170 .ok()?
171 .trim()
172 .parse()
173 .ok()
174 .filter(|value: &T| !(zero_selects_default && *value == T::default()))?;
175 Some(parsed)
176 }
177}
178
179const AFFIRMATIVE: &[&str] = &["1", "true", "yes", "on"];
182
183const NEGATIVE: &[&str] = &["", "0", "false", "no", "off"];
186
187pub fn flag_owned(name: &str) -> bool {
189 match std::env::var_os(name) {
190 Some(value) => AFFIRMATIVE.contains(&normalize(&value).as_str()),
191 None => false,
192 }
193}
194
195pub fn flag_foreign(name: &str) -> bool {
200 match std::env::var_os(name) {
201 Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
202 None => false,
203 }
204}
205
206pub fn flag_opt_out(name: &str) -> bool {
214 match std::env::var_os(name) {
215 Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
216 None => true,
217 }
218}
219
220pub fn value_is_affirmative_foreign(value: &str) -> bool {
225 !NEGATIVE.contains(&value.trim().to_ascii_lowercase().as_str())
226}
227
228fn normalize(value: &OsStr) -> String {
229 value.to_string_lossy().trim().to_ascii_lowercase()
230}
231
232macro_rules! declare {
233 ($($ident:ident => $name:literal, $kind:expr, $owner:expr, $default:literal, $summary:literal;)*) => {
234 $(
235 #[doc = $summary]
236 #[doc = concat!("Environment variable `", $name, "`. Unset: ", $default, ".")]
238 pub const $ident: EnvVar = EnvVar {
239 name: $name,
240 kind: $kind,
241 owner: $owner,
242 default: $default,
243 summary: $summary,
244 };
245 )*
246
247 pub const DECLARED: &[EnvVar] = &[$($ident),*];
253 };
254}
255
256declare! {
257 GITHUB_ACTIONS => "GITHUB_ACTIONS",
258 EnvKind::ForeignFlag, Owner::Foreign, "not running under GitHub Actions",
259 "Set by GitHub Actions; tests wait longer for a shared runner.";
260 INVOCATION_ID => "INVOCATION_ID",
261 EnvKind::Text, Owner::Foreign, "not started by systemd",
262 "Set by systemd for a unit invocation; identifies the launching unit.";
263 LOCALAPPDATA => "LOCALAPPDATA",
264 EnvKind::Path, Owner::Foreign, "the platform default is derived",
265 "Windows per-user application data root.";
266 PATH => "PATH",
267 EnvKind::Text, Owner::Foreign, "the child inherits no explicit PATH",
268 "Executable search path, forwarded to the symbolization worker.";
269 BROKER_ALLOW_PRIVILEGED => "RUNNING_PROCESS_BROKER_ALLOW_PRIVILEGED",
270 EnvKind::ExactValue("1"), Owner::Crate, "privileged startup is refused",
271 "Opt out of the broker's refusal to start as root or LocalSystem.";
272 BROKER_CLIENT_TIMEOUT_MS => "RUNNING_PROCESS_BROKER_CLIENT_TIMEOUT_MS",
273 EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in client timeout",
274 "Broker client request timeout, in milliseconds.";
275 BROKER_CRASH_DUMP_DIR => "RUNNING_PROCESS_BROKER_CRASH_DUMP_DIR",
276 EnvKind::Path, Owner::Crate, "the standard diagnostic-artifact location",
277 "Where broker crash dumps are written.";
278 BROKER_HELLO_PERF_GUARD => "RUNNING_PROCESS_BROKER_HELLO_PERF_GUARD",
279 EnvKind::OwnedFlag, Owner::Crate, "the guard does not run",
280 "Run the broker Hello latency guard.";
281 BROKER_HELLO_TIMEOUT_MS => "RUNNING_PROCESS_BROKER_HELLO_TIMEOUT_MS",
282 EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in Hello timeout",
283 "Broker Hello handshake timeout, in milliseconds.";
284 BROKER_HTTP_BIND => "RUNNING_PROCESS_BROKER_HTTP_BIND",
285 EnvKind::Text, Owner::Crate, "the loopback bind address",
286 "Bind address for the broker HTTP aggregator.";
287 BROKER_HTTP_PORT => "RUNNING_PROCESS_BROKER_HTTP_PORT",
288 EnvKind::Number { zero_selects_default: false }, Owner::Crate, "an ephemeral port",
289 "Port for the broker HTTP aggregator.";
290 BROKER_LISTENER_FD => "RUNNING_PROCESS_BROKER_LISTENER_FD",
291 EnvKind::Number { zero_selects_default: false }, Owner::Foreign, "the daemon binds its own endpoint",
292 "Descriptor of a listening socket the broker already bound and passed.";
293 BROKER_MAX_INFLIGHT_HANDLERS => "RUNNING_PROCESS_BROKER_MAX_INFLIGHT_HANDLERS",
294 EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in concurrency cap",
295 "Maximum broker request handlers running at once.";
296 BROKER_OWNED_BIND => "RUNNING_PROCESS_BROKER_OWNED_BIND",
297 EnvKind::OptOutFlag, Owner::Crate, "broker-owned bind is used",
298 "Escape hatch: set falsy to fall back to spawn-then-probe.";
299 BROKER_V1_BACKEND_NAMESPACE => "RUNNING_PROCESS_BROKER_V1_BACKEND_NAMESPACE",
300 EnvKind::Text, Owner::Foreign, "no namespace is applied",
301 "Backend namespace handed to a v1 broker backend.";
302 BROKER_V1_BACKEND_PIPE => "RUNNING_PROCESS_BROKER_V1_BACKEND_PIPE",
303 EnvKind::Text, Owner::Foreign, "the backend derives its own endpoint",
304 "Endpoint a v1 broker backend should serve on.";
305 BROKER_V1_INSTANCE => "RUNNING_PROCESS_BROKER_V1_INSTANCE",
306 EnvKind::Text, Owner::Foreign, "the default instance",
307 "Instance identifier for a v1 broker backend.";
308 BROKER_V1_SERVICE_NAME => "RUNNING_PROCESS_BROKER_V1_SERVICE_NAME",
309 EnvKind::Text, Owner::Foreign, "the backend supplies its own name",
310 "Service name a v1 broker backend registers under.";
311 BROKER_V1_SERVICE_VERSION => "RUNNING_PROCESS_BROKER_V1_SERVICE_VERSION",
312 EnvKind::Text, Owner::Foreign, "the backend supplies its own version",
313 "Service version a v1 broker backend reports.";
314 BROKER_V1_SESSION_TOKEN => "RUNNING_PROCESS_BROKER_V1_SESSION_TOKEN",
315 EnvKind::Text, Owner::Foreign, "no session token is presented",
316 "Session token a v1 broker backend presents to the broker.";
317 BROKER_V1_SOCKET => "RUNNING_PROCESS_BROKER_V1_SOCKET",
318 EnvKind::Text, Owner::Foreign, "the standard broker endpoint",
319 "Broker endpoint a v1 backend dials.";
320 BROKER_V1_TRACEPARENT => "RUNNING_PROCESS_BROKER_V1_TRACEPARENT",
321 EnvKind::Text, Owner::Foreign, "no trace context is propagated",
322 "W3C traceparent propagated into a v1 broker backend.";
323 BROKER_V1_TRACESTATE => "RUNNING_PROCESS_BROKER_V1_TRACESTATE",
324 EnvKind::Text, Owner::Foreign, "no trace state is propagated",
325 "W3C tracestate propagated into a v1 broker backend.";
326 CHILD_PID_LOG_PATH => "RUNNING_PROCESS_CHILD_PID_LOG_PATH",
327 EnvKind::Path, Owner::Foreign, "spawned child PIDs are not logged",
328 "Append each spawned child PID to this file (test harness seam).";
329 CLIENT_CONNECT_TIMEOUT_MS => "RUNNING_PROCESS_CLIENT_CONNECT_TIMEOUT_MS",
330 EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in connect timeout",
331 "Daemon client connect timeout, in milliseconds.";
332 CLIENT_RPC_TIMEOUT_MS => "RUNNING_PROCESS_CLIENT_RPC_TIMEOUT_MS",
333 EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in RPC timeout",
334 "Daemon client RPC timeout, in milliseconds.";
335 DAEMON_SCOPE => "RUNNING_PROCESS_DAEMON_SCOPE",
336 EnvKind::Text, Owner::Crate, "the user-wide scope",
337 "Daemon scope selector; `dev` gives a CWD-scoped daemon for tests.";
338 DAEMON_SHADOWED => "RUNNING_PROCESS_DAEMON_SHADOWED",
339 EnvKind::OwnedFlag, Owner::Crate, "a dev-build daemon relocates itself",
340 "Marks a daemon already running from its shadow copy.";
341 DAEMON_START_TIMEOUT_MS => "RUNNING_PROCESS_DAEMON_START_TIMEOUT_MS",
342 EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in 750ms budget",
343 "How long a client waits for a freshly spawned daemon to bind its socket, in milliseconds.";
344 DISABLE => "RUNNING_PROCESS_DISABLE",
345 EnvKind::ExactValue("1"), Owner::Crate, "the broker is used",
346 "Canonical escape hatch: bypass the broker entirely.";
347 FAKE_BACKEND => "RUNNING_PROCESS_FAKE_BACKEND",
348 EnvKind::Path, Owner::Foreign, "backends are reached through the broker",
349 "TEST-ONLY: dial this endpoint directly, skipping broker negotiation.";
350 IS_DAEMON => "RUNNING_PROCESS_IS_DAEMON",
351 EnvKind::ForeignFlag, Owner::Crate, "the process is not a daemon",
352 "Marks a process spawned as a daemon, for originator reaping.";
353 KILL_DRAIN_TIMEOUT_MS => "RUNNING_PROCESS_KILL_DRAIN_TIMEOUT_MS",
354 EnvKind::Number { zero_selects_default: false }, Owner::Crate, "two seconds",
355 "How long `kill()` waits for output capture to drain, in milliseconds.";
356 MANIFEST_DIR => "RUNNING_PROCESS_MANIFEST_DIR",
357 EnvKind::Path, Owner::Foreign, "the standard manifest location",
358 "Where broker cache manifests are read and written.";
359 NO_TRACKING => "RUNNING_PROCESS_NO_TRACKING",
360 EnvKind::OwnedFlag, Owner::Crate, "processes are tracked",
361 "Disable daemon IPC and process tracking.";
362 ORIGINATOR => "RUNNING_PROCESS_ORIGINATOR",
363 EnvKind::Text, Owner::Foreign, "the originator is inferred",
364 "Identifies the process that originated a spawn tree.";
365 SERVICE_DEF_DIR => "RUNNING_PROCESS_SERVICE_DEF_DIR",
366 EnvKind::Path, Owner::Foreign, "the standard service-definition location",
367 "Where service definitions are read from.";
368 TMPDIR => "TMPDIR",
369 EnvKind::Path, Owner::Foreign, "the platform temporary directory",
370 "macOS per-session temporary directory; a broker endpoint root.";
371 USERNAME => "USERNAME",
372 EnvKind::Text, Owner::Foreign, "the endpoint is named `unknown`",
373 "Windows account name, mixed into the daemon pipe name.";
374 XDG_CONFIG_HOME => "XDG_CONFIG_HOME",
375 EnvKind::Path, Owner::Foreign, "`~/.config` is used",
376 "XDG per-user configuration root; where service definitions are read.";
377 XDG_DATA_HOME => "XDG_DATA_HOME",
378 EnvKind::Path, Owner::Foreign, "the platform default is derived",
379 "XDG per-user data root, used by the daemon runtime collector.";
380 XDG_RUNTIME_DIR => "XDG_RUNTIME_DIR",
381 EnvKind::Path, Owner::Foreign, "a per-user directory under /tmp",
382 "XDG per-user runtime root; where broker sockets are placed.";
383}
384
385#[cfg(test)]
386#[path = "tests/env_vars.rs"]
387mod tests;