Skip to main content

running_process_platform_internal/
env_vars.rs

1//! Every environment variable this crate reads in production, declared in one
2//! place (#1101).
3//!
4//! [`crate::env`] is the mechanism; this is the policy for
5//! `running-process-platform-internal`. Each reader goes through one of these
6//! constants rather than a string literal, so an embedder can learn what the
7//! crate reads from [`DECLARED_PLATFORM`] instead of grepping call sites.
8//!
9//! Many of these are read with [`EnvVar::os`](crate::env::EnvVar::os) rather
10//! than [`EnvVar::path`](crate::env::EnvVar::path): the original readers
11//! treated `VAR=` (empty) as set, and moving them onto a table is not a reason
12//! to change what they do.
13//!
14//! # One owner per name
15//!
16//! Some of these are also read by `running-process`. Each name is declared
17//! once, here, by the lowest crate that reads it; `running_process::env_vars`
18//! refers to the same constant rather than repeating it, and its
19//! `all_declared()` is the one combined inventory. The shared declarations
20//! keep the wording `running_process::env_vars::DECLARED` has always
21//! published, so that table did not change when they moved.
22//!
23//! Not covered, deliberately:
24//! - Reads keyed by a caller-supplied name (`ipc` descriptor keys on Linux and
25//!   macOS). The name is data there, not a variable this crate chooses, so
26//!   there is nothing to declare; they go through
27//!   [`os_named`](crate::env::os_named).
28//! - Save/restore and fixture plumbing inside `#[cfg(test)]` code.
29
30use crate::env::{EnvKind, Owner};
31
32crate::declare_env_vars! {
33    /// Every environment variable this crate reads in production, sorted by
34    /// name; `declarations_are_sorted_unique_and_documented` holds that.
35    pub const DECLARED_PLATFORM;
36    COMPUTERNAME => "COMPUTERNAME",
37        EnvKind::Text, Owner::Foreign, "hostname is unknown",
38        "Windows machine name, reported as the host name.";
39    DISPLAY => "DISPLAY",
40        EnvKind::Text, Owner::Foreign, "no X11 display; window icons unsupported",
41        "X11 display; its presence is what makes a Linux window icon possible.";
42    HOME => "HOME",
43        EnvKind::Path, Owner::Foreign, "autostart paths cannot be resolved",
44        "Home directory; roots the autostart entries and the fallback APE loader cache directory.";
45    LOCALAPPDATA => "LOCALAPPDATA",
46        EnvKind::Path, Owner::Foreign, "the platform default is derived",
47        "Windows per-user application data root.";
48    PATH => "PATH",
49        EnvKind::Text, Owner::Foreign, "the child inherits no explicit PATH",
50        "Executable search path, forwarded to the symbolization worker and searched for an APE image or loader.";
51    APE_CACHE_DIR => "RUNNING_PROCESS_APE_CACHE_DIR",
52        EnvKind::Path, Owner::Crate, "the XDG cache, runtime and temporary directories",
53        "Preferred directory for loaders extracted from APE images; used only when private and exec-capable.";
54    APE_LOADER => "RUNNING_PROCESS_APE_LOADER",
55        EnvKind::Path, Owner::Crate, "the image's embedded loader, then `ape`, then `/bin/sh`",
56        "Explicit loader (an `ape` binary or a POSIX shell) for APE images.";
57    CONPTY_CACHE => "RUNNING_PROCESS_CONPTY_CACHE",
58        EnvKind::Path, Owner::Crate, "the platform cache directory",
59        "Root under which the ConPTY sidecar is cached on Windows.";
60    CONPTY_DIAGNOSTICS => "RUNNING_PROCESS_CONPTY_DIAGNOSTICS",
61        EnvKind::Text, Owner::Crate, "ConPTY resolution is silent",
62        "Print how the ConPTY implementation was chosen and fetched, to stderr.";
63    CONPTY_OFFLINE => "RUNNING_PROCESS_CONPTY_OFFLINE",
64        EnvKind::Text, Owner::Crate, "the ConPTY sidecar may be fetched",
65        "Forbid fetching the ConPTY sidecar over the network.";
66    CONPTY_SIDECAR_FETCH_TIMEOUT_MS => "RUNNING_PROCESS_CONPTY_SIDECAR_FETCH_TIMEOUT_MS",
67        EnvKind::Number { zero_selects_default: true }, Owner::Crate, "the built-in fetch timeout",
68        "Upper bound on the ConPTY sidecar download, in milliseconds.";
69    KILL_DRAIN_TIMEOUT_MS => "RUNNING_PROCESS_KILL_DRAIN_TIMEOUT_MS",
70        EnvKind::Number { zero_selects_default: false }, Owner::Crate, "two seconds",
71        "How long `kill()` waits for output capture to drain, in milliseconds.";
72    NATIVE_TERMINAL_INPUT_TRACE_PATH => "RUNNING_PROCESS_NATIVE_TERMINAL_INPUT_TRACE_PATH",
73        EnvKind::Text, Owner::Crate, "terminal input is not traced",
74        "Where Windows native terminal input events are traced.";
75    USE_SYSTEM_CONPTY => "RUNNING_PROCESS_USE_SYSTEM_CONPTY",
76        EnvKind::Text, Owner::Crate, "the bundled ConPTY sidecar is preferred",
77        "Use the system ConPTY instead of the bundled sidecar on Windows.";
78    TMPDIR => "TMPDIR",
79        EnvKind::Path, Owner::Foreign, "the platform temporary directory",
80        "macOS per-session temporary directory; a broker endpoint root.";
81    WAYLAND_DISPLAY => "WAYLAND_DISPLAY",
82        EnvKind::Text, Owner::Foreign, "not a Wayland session",
83        "Wayland session marker; window icons are unsupported under Wayland.";
84    WINDOWID => "WINDOWID",
85        EnvKind::Text, Owner::Foreign, "the X11 window is unknown",
86        "X11 window id exported by the terminal emulator.";
87    WT_SESSION => "WT_SESSION",
88        EnvKind::Text, Owner::Foreign, "not inside Windows Terminal",
89        "Windows Terminal session marker; runtime window icons are degraded there.";
90    XDG_CACHE_HOME => "XDG_CACHE_HOME",
91        EnvKind::Path, Owner::Foreign, "`~/.cache` is used",
92        "XDG per-user cache root; where extracted APE loaders are installed.";
93    XDG_CONFIG_HOME => "XDG_CONFIG_HOME",
94        EnvKind::Path, Owner::Foreign, "`~/.config` is used",
95        "XDG per-user configuration root; where service definitions are read.";
96    XDG_DATA_HOME => "XDG_DATA_HOME",
97        EnvKind::Path, Owner::Foreign, "the platform default is derived",
98        "XDG per-user data root, used by the daemon runtime collector.";
99    XDG_RUNTIME_DIR => "XDG_RUNTIME_DIR",
100        EnvKind::Path, Owner::Foreign, "a per-user directory under /tmp",
101        "XDG per-user runtime root; where broker sockets and fallback APE loaders are placed.";
102    XDG_STATE_HOME => "XDG_STATE_HOME",
103        EnvKind::Path, Owner::Foreign, "~/.local/state",
104        "XDG state root.";
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::env::Owner;
111
112    #[test]
113    fn declarations_are_sorted_unique_and_documented() {
114        let names: Vec<&str> = DECLARED_PLATFORM.iter().map(|var| var.name).collect();
115        let mut sorted = names.clone();
116        sorted.sort_unstable();
117        sorted.dedup();
118        assert_eq!(names, sorted, "DECLARED_PLATFORM must be sorted and unique");
119        for var in DECLARED_PLATFORM {
120            assert!(
121                !var.summary.trim().is_empty(),
122                "{} has no summary",
123                var.name
124            );
125            assert!(
126                !var.default.trim().is_empty(),
127                "{} has no default",
128                var.name
129            );
130        }
131    }
132
133    #[test]
134    fn owner_follows_the_name() {
135        for var in DECLARED_PLATFORM {
136            let ours = var.name.starts_with("RUNNING_PROCESS_");
137            assert_eq!(
138                var.owner == Owner::Crate,
139                ours,
140                "{} is declared {:?}",
141                var.name,
142                var.owner
143            );
144        }
145    }
146
147    /// Direct calls exempted by their argument text. None remain: reads keyed
148    /// by a caller-supplied name go through [`crate::env::os_named`] instead,
149    /// which keeps the exemption out of this list and inside the mechanism.
150    const DYNAMIC_KEYS: &[&str] = &[];
151
152    /// Production code reaches the environment only through [`crate::env`]:
153    /// any direct `std::env` variable call outside test code is drift. The
154    /// name keeps its history; there are no dynamic-key exemptions left (see
155    /// [`DYNAMIC_KEYS`]).
156    ///
157    /// Test code is recognised by layout: a `#[cfg(test)]` (or
158    /// `#[cfg(all(test, ..))]`) inline `mod name {` runs to the end of its file
159    /// in this crate, as do `*_tests.rs` files and `tests/` directories.
160    #[test]
161    fn production_code_calls_std_env_only_for_dynamic_keys() {
162        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
163        let mut offenders = Vec::new();
164        visit(&root, &mut offenders);
165        assert!(
166            offenders.is_empty(),
167            "undeclared environment reads: {offenders:#?}"
168        );
169    }
170
171    fn visit(dir: &std::path::Path, offenders: &mut Vec<String>) {
172        for entry in std::fs::read_dir(dir).expect("read src") {
173            let path = entry.expect("dir entry").path();
174            if path.is_dir() {
175                if path.file_name().is_some_and(|name| name == "tests") {
176                    continue;
177                }
178                visit(&path, offenders);
179                continue;
180            }
181            if path.extension().is_none_or(|ext| ext != "rs")
182                || path.file_name().is_some_and(|name| {
183                    let name = name.to_string_lossy();
184                    name == "env.rs" || name == "env_vars.rs" || name.ends_with("_tests.rs")
185                })
186            {
187                continue;
188            }
189            let text = std::fs::read_to_string(&path).expect("read source");
190            let lines: Vec<&str> = text.lines().collect();
191            let cut = lines
192                .iter()
193                .enumerate()
194                .position(|(index, line)| {
195                    let line = line.trim_start();
196                    (line.starts_with("#[cfg(test)]") || line.starts_with("#[cfg(all(test"))
197                        && lines[index + 1..]
198                            .iter()
199                            .map(|next| next.trim())
200                            .find(|next| !next.is_empty() && !next.starts_with("#["))
201                            .is_some_and(|next| next.starts_with("mod ") && next.ends_with('{'))
202                })
203                .unwrap_or(lines.len());
204            for (index, line) in lines[..cut].iter().enumerate() {
205                let direct = [
206                    "env::var(",
207                    "env::var_os(",
208                    "env::set_var(",
209                    "env::remove_var(",
210                ]
211                .iter()
212                .find_map(|call| line.find(call).map(|at| &line[at + call.len()..]));
213                if let Some(arg) = direct {
214                    if !DYNAMIC_KEYS.iter().any(|key| arg.starts_with(key)) {
215                        offenders.push(format!("{}:{}", path.display(), index + 1));
216                    }
217                }
218            }
219        }
220    }
221}