fix(process): Give child processes the host environment inside an AppImage (#118)

Fixes #117

Inside an AppImage, AppRun and the linuxdeploy GTK/GStreamer hooks point LD_LIBRARY_PATH, PATH, XDG_DATA_DIRS, GIO_MODULE_DIR, GTK_PATH, GST_PLUGIN_* … into the mounted image, and every child inherited them, so host programs loaded the image's older libraries (curl + bundled nghttp2 → symbol lookup error when installing Kotlin LSP). ide_core::process::prepare (formerly no_window) now gives children the host environment back: entries inside $APPDIR are dropped (a <NAME>_ORIG wins), emptied variables and the runtime's launch variables are removed, explicit call-site values are kept; the IDE process itself is unchanged. Covers all spawn sites that used no_window plus terminal/run PTYs, run configuration environments and stop commands, login-shell probes, attach ps/kill, docker PATH, tool search PATH and the Linux browser opener. No-op outside an AppImage. Not verified on a Linux AppImage.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
This commit is contained in:
l0ng-ai
2026-10-28 13:50:20 +08:00
committed by GitHub
parent 429e8ffc82
commit 96be40cd84
25 changed files with 287 additions and 42 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ pub(crate) fn run(
use std::os::unix::process::CommandExt;
command.process_group(0);
}
ide_core::process::no_window(&mut command);
ide_core::process::prepare(&mut command);
let mut child = command.spawn().map_err(|e| IdeError::unavailable(format!("cannot start {program}: {e}")))?;
let (tx, rx) = mpsc::channel::<(Stream, String)>();
let pump = |stream: Box<dyn Read + Send>, kind: Stream, tx: mpsc::Sender<(Stream, String)>| {
+209 -6
View File
@@ -5,22 +5,121 @@ use std::process::Command;
use std::sync::OnceLock;
/// Windows `CREATE_NO_WINDOW` creation flag, for call sites that combine it
/// with other flags. Prefer [`no_window`] otherwise.
/// with other flags (and then call [`host_env`] themselves). Prefer [`prepare`] otherwise.
pub const CREATE_NO_WINDOW: u32 = 0x0800_0000;
/// Keeps a console child (java, node, curl, a language server, ...) from
/// opening its own console window on Windows. Release builds use the GUI
/// subsystem, so without this every spawn flashes a `cmd` window. A no-op
/// elsewhere. For a `tokio::process::Command`, pass `cmd.as_std_mut()`.
pub fn no_window(cmd: &mut Command) -> &mut Command {
/// Readies `cmd` for spawning any child (java, node, curl, git, a language server, ...).
/// For a `tokio::process::Command`, pass `cmd.as_std_mut()`.
///
/// - Windows: release builds use the GUI subsystem, so without `CREATE_NO_WINDOW` every
/// console child flashes a `cmd` window.
/// - Linux AppImage: see [`host_env`].
///
/// Variables the call site has already set are kept; call it before `env_clear`.
pub fn prepare(cmd: &mut Command) -> &mut Command {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(CREATE_NO_WINDOW);
}
host_env(cmd)
}
/// Gives `cmd` the host's environment back when the IDE runs from an AppImage. `AppRun` and
/// its hooks point `LD_LIBRARY_PATH`, `PATH`, `XDG_DATA_DIRS`, `GIO_MODULE_DIR`, `GTK_PATH`,
/// `GST_PLUGIN_*`, ... into the mounted image for the IDE itself; a host program that
/// inherits them loads the image's (older) libraries and plugins next to its own, e.g.
/// `curl: symbol lookup error ... nghttp2_...`. The IDE process keeps its environment
/// (WebKit and GTK still need it). Variables the call site has already set are kept.
/// A no-op outside an AppImage.
pub fn host_env(cmd: &mut Command) -> &mut Command {
let fixes = appimage_fixes();
if fixes.is_empty() {
return cmd;
}
let explicit: Vec<OsString> = cmd.get_envs().map(|(k, _)| k.to_owned()).collect();
for (key, value) in fixes.into_iter().filter(|(k, _)| !explicit.contains(k)) {
match value {
Some(v) => cmd.env(key, v),
None => cmd.env_remove(key),
};
}
cmd
}
/// The [`host_env`] changes as `(name, value)` pairs, `None` meaning "remove", for spawners
/// that are not a `std::process::Command` (a PTY `CommandBuilder`). Apply before the call
/// site's own variables. Empty outside an AppImage.
pub fn host_env_fixes() -> Vec<(OsString, Option<OsString>)> {
appimage_fixes()
}
/// This process's environment as a child should inherit it (see [`host_env`]), for call
/// sites that build a child's environment from the IDE's own instead of inheriting it.
pub fn host_vars() -> impl Iterator<Item = (OsString, OsString)> {
let fixes = appimage_fixes();
std::env::vars_os().filter_map(move |(k, v)| match fixes.iter().find(|(f, _)| *f == k) {
Some((_, fixed)) => fixed.clone().map(|v| (k, v)),
None => Some((k, v)),
})
}
/// [`std::env::var_os`] as a child should see it (see [`host_env`]).
pub fn host_var_os(key: impl AsRef<OsStr>) -> Option<OsString> {
let key = key.as_ref();
match appimage_fixes().into_iter().find(|(k, _)| k == key) {
Some((_, fixed)) => fixed,
None => std::env::var_os(key),
}
}
/// Read on every call, not cached: startup rewrites `PATH` (login shell, managed tools).
fn appimage_fixes() -> Vec<(OsString, Option<OsString>)> {
if !cfg!(target_os = "linux") || std::env::var_os("APPDIR").is_none() {
return Vec::new();
}
appimage_env_fixes(&std::env::vars_os().collect::<Vec<_>>())
}
/// Variables the AppImage runtime sets to describe the IDE's own launch.
const APPIMAGE_LAUNCH_VARS: [&str; 4] = ["APPDIR", "APPIMAGE", "ARGV0", "OWD"];
/// What to change in `vars` (an environment) so a child sees the host's environment rather
/// than the AppImage's: every `:`-separated entry inside `$APPDIR` is dropped (a saved
/// `<NAME>_ORIG` wins when present), variables left empty are removed, and so are the
/// runtime's own launch variables. Nothing changes when `APPDIR` is not set.
fn appimage_env_fixes(vars: &[(OsString, OsString)]) -> Vec<(OsString, Option<OsString>)> {
let get = |name: &str| vars.iter().find(|(k, _)| k == name).map(|(_, v)| v);
let Some(appdir) =
get("APPDIR").and_then(|v| v.to_str()).map(|v| v.trim_end_matches('/')).filter(|v| !v.is_empty())
else {
return Vec::new();
};
let inside = |entry: &str| entry.strip_prefix(appdir).is_some_and(|rest| rest.is_empty() || rest.starts_with('/'));
let mut fixes = Vec::new();
for (key, value) in vars {
let (Some(name), Some(value)) = (key.to_str(), value.to_str()) else { continue };
let fixed = if APPIMAGE_LAUNCH_VARS.contains(&name) {
None
} else if value.split(':').any(inside) {
match get(&format!("{name}_ORIG")) {
Some(orig) => Some(orig.clone()),
None => {
let kept: Vec<&str> = value.split(':').filter(|e| !e.is_empty() && !inside(e)).collect();
Some(OsString::from(kept.join(":")))
}
}
} else if name == "GTK_THEME" && value.is_empty() {
// linuxdeploy-plugin-gtk exports `GTK_THEME="$APPIMAGE_GTK_THEME"`, usually empty.
None
} else {
continue;
};
fixes.push((key.clone(), fixed.filter(|v| !v.is_empty())));
}
fixes
}
static SHELL_ENV: OnceLock<Vec<(OsString, OsString)>> = OnceLock::new();
/// Records the login shell's environment, read once at startup off the main thread.
@@ -50,4 +149,108 @@ mod tests {
let vars: Vec<_> = shell_env().collect();
assert_eq!(vars, [(OsStr::new("OPENIDEA_SHELL_ONLY_VAR"), OsStr::new("1"))]);
}
fn env(pairs: &[(&str, &str)]) -> Vec<(OsString, OsString)> {
pairs.iter().map(|(k, v)| (OsString::from(k), OsString::from(v))).collect()
}
fn fixes(pairs: &[(&str, &str)]) -> Vec<(String, Option<String>)> {
appimage_env_fixes(&env(pairs))
.into_iter()
.map(|(k, v)| (k.into_string().unwrap(), v.map(|v| v.into_string().unwrap())))
.collect()
}
fn fix(name: &str, value: Option<&str>) -> (String, Option<String>) {
(name.to_owned(), value.map(str::to_owned))
}
#[test]
fn appimage_entries_are_dropped_and_host_entries_kept() {
let got = fixes(&[
("APPDIR", "/tmp/.mount_OpenIDabc"),
(
"LD_LIBRARY_PATH",
"/tmp/.mount_OpenIDabc/usr/lib/:/tmp/.mount_OpenIDabc/usr/lib/x86_64-linux-gnu/:/opt/cuda/lib64:/usr/local/lib",
),
("PATH", "/tmp/.mount_OpenIDabc/usr/bin/:/tmp/.mount_OpenIDabc/usr/sbin/:/usr/local/bin:/usr/bin"),
("XDG_DATA_DIRS", "/tmp/.mount_OpenIDabc/usr/share:/usr/share:/usr/local/share:/usr/share"),
("HOME", "/home/me"),
("JAVA_HOME", "/tmp/.mount_OpenIDabcdef/jdk"),
]);
assert_eq!(
got,
[
fix("APPDIR", None),
fix("LD_LIBRARY_PATH", Some("/opt/cuda/lib64:/usr/local/lib")),
fix("PATH", Some("/usr/local/bin:/usr/bin")),
fix("XDG_DATA_DIRS", Some("/usr/share:/usr/local/share:/usr/share")),
]
);
}
#[test]
fn variables_left_empty_are_removed() {
// AppRun prepends to an unset variable, leaving a trailing `:`; hooks set single paths.
let got = fixes(&[
("APPIMAGE", "/opt/openidea/OpenIDEA.AppImage"),
("APPDIR", "/tmp/.mount_x/"),
("ARGV0", "/opt/openidea/OpenIDEA.AppImage"),
("OWD", "/home/me"),
("LD_LIBRARY_PATH", "/tmp/.mount_x/usr/lib/:/tmp/.mount_x/lib/:"),
("GIO_MODULE_DIR", "/tmp/.mount_x/usr/lib/gio/modules"),
("GTK_DATA_PREFIX", "/tmp/.mount_x"),
("GDK_PIXBUF_MODULE_FILE", "/tmp/.mount_x/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"),
("GST_PLUGIN_SYSTEM_PATH_1_0", "/tmp/.mount_x/usr/lib/gstreamer-1.0"),
("GTK_THEME", ""),
]);
let names: Vec<_> = got.iter().filter(|(_, v)| v.is_none()).map(|(k, _)| k.as_str()).collect();
assert_eq!(
names,
[
"APPIMAGE",
"APPDIR",
"ARGV0",
"OWD",
"LD_LIBRARY_PATH",
"GIO_MODULE_DIR",
"GTK_DATA_PREFIX",
"GDK_PIXBUF_MODULE_FILE",
"GST_PLUGIN_SYSTEM_PATH_1_0",
"GTK_THEME",
]
);
assert_eq!(names.len(), got.len());
}
#[test]
fn a_saved_original_value_wins() {
let got = fixes(&[
("APPDIR", "/tmp/.mount_x"),
("LD_LIBRARY_PATH", "/tmp/.mount_x/usr/lib:/opt/lib"),
("LD_LIBRARY_PATH_ORIG", "/opt/lib:/home/me/lib"),
("PYTHONHOME", "/tmp/.mount_x/usr"),
("PYTHONHOME_ORIG", ""),
]);
assert_eq!(
got,
[fix("APPDIR", None), fix("LD_LIBRARY_PATH", Some("/opt/lib:/home/me/lib")), fix("PYTHONHOME", None)]
);
}
#[test]
fn outside_an_appimage_nothing_changes() {
let plain = [("LD_LIBRARY_PATH", "/opt/lib:"), ("PATH", "/usr/bin"), ("GTK_THEME", "")];
assert!(fixes(&plain).is_empty());
assert!(fixes(&[("APPDIR", ""), ("PATH", "/usr/bin")]).is_empty());
assert!(fixes(&[("APPDIR", "/"), ("PATH", "/usr/bin")]).is_empty());
}
#[test]
fn host_env_keeps_what_the_call_site_set() {
let mut cmd = Command::new("true");
cmd.env("PATH", "/custom");
prepare(&mut cmd);
assert_eq!(cmd.get_envs().find(|(k, _)| *k == "PATH").and_then(|(_, v)| v), Some(OsStr::new("/custom")));
}
}
+1 -1
View File
@@ -206,7 +206,7 @@ pub fn collect(spec: &CoverageReportSpec) -> IdeResult<CoverageSuite> {
CoverageReportSpec::Exported { runner, argv, cwd, env, lcov } => {
let (program, args) =
argv.split_first().ok_or_else(|| IdeError::invalid("empty coverage export command"))?;
let out = ide_core::process::no_window(&mut Command::new(program))
let out = ide_core::process::prepare(&mut Command::new(program))
.args(args)
.current_dir(cwd)
.envs(env)
+2 -2
View File
@@ -257,7 +257,7 @@ pub async fn prepare_debugpy(spec: &mut AdapterSpec) -> IdeResult<()> {
return Ok(()); // Reported as a missing adapter when it is started.
};
let mut probe = Command::new(&python);
ide_core::process::no_window(probe.as_std_mut());
ide_core::process::prepare(probe.as_std_mut());
let probe = probe
.args(["-c", "import debugpy.adapter"])
.current_dir(&spec.cwd)
@@ -396,7 +396,7 @@ impl AdapterLauncher for ProcessLauncher {
None => args,
};
let mut cmd = Command::new(&binary);
ide_core::process::no_window(cmd.as_std_mut());
ide_core::process::prepare(cmd.as_std_mut());
cmd.args(&args)
.envs(spec.env.iter().map(|(k, v)| (k, v)))
.current_dir(&spec.cwd)
+4 -2
View File
@@ -170,7 +170,7 @@ pub fn is_go_binary(exe: &str) -> bool {
pub fn list_processes() -> IdeResult<Vec<RawProcess>> {
use std::collections::HashMap;
let ps = |fields: &str| -> IdeResult<String> {
let out = std::process::Command::new("ps")
let out = ide_core::process::prepare(&mut std::process::Command::new("ps"))
.args(["-x", "-ww", "-o", fields])
.output()
.map_err(|e| IdeError::unavailable(format!("cannot list processes: {e}")))?;
@@ -212,7 +212,9 @@ pub const NODE_INSPECTOR_PORT: u16 = 9229;
/// picker does) and waits until it listens. Returns the inspector port.
#[cfg(unix)]
pub async fn enable_node_inspector(pid: u32) -> IdeResult<u16> {
let status = tokio::process::Command::new("kill")
let mut kill = tokio::process::Command::new("kill");
ide_core::process::prepare(kill.as_std_mut());
let status = kill
.args(["-USR1", &pid.to_string()])
.status()
.await
+1 -1
View File
@@ -55,7 +55,7 @@ pub async fn build(
) -> IdeResult<PathBuf> {
let cargo = find_binary("cargo", cwd).ok_or_else(|| IdeError::unavailable("`cargo` not found on PATH"))?;
let mut cmd = tokio::process::Command::new(cargo);
ide_core::process::no_window(cmd.as_std_mut());
ide_core::process::prepare(cmd.as_std_mut());
cmd.args(args)
.arg("--message-format=json-render-diagnostics")
.current_dir(cwd)
+1 -1
View File
@@ -421,7 +421,7 @@ pub(crate) fn run_in_terminal(s: &Arc<Session>, args: &Value) -> Result<Value, S
return Err("runInTerminal without args".into());
};
let mut cmd = tokio::process::Command::new(program);
ide_core::process::no_window(cmd.as_std_mut());
ide_core::process::prepare(cmd.as_std_mut());
cmd.args(rest)
.envs(ide_core::process::shell_env())
.stdin(Stdio::null())
+1 -1
View File
@@ -69,7 +69,7 @@ pub fn docker_executable() -> Option<PathBuf> {
pub fn run_env(ep: &DockerEndpoint) -> std::collections::BTreeMap<String, String> {
let mut env = ep.env_vars();
if let Some(dir) = docker_executable().as_deref().and_then(Path::parent) {
let current = std::env::var_os("PATH").unwrap_or_default();
let current = ide_core::process::host_var_os("PATH").unwrap_or_default();
let mut dirs: Vec<PathBuf> = vec![dir.to_path_buf()];
dirs.extend(std::env::split_paths(&current).filter(|d| d != dir));
if let Ok(path) = std::env::join_paths(dirs) {
+1 -1
View File
@@ -25,7 +25,7 @@ fn prepare(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
ide_core::process::no_window(cmd);
ide_core::process::prepare(cmd);
}
fn spawn(cmd: &mut Command) -> IdeResult<Child> {
+1 -1
View File
@@ -25,7 +25,7 @@ fn prepare(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
ide_core::process::no_window(cmd);
ide_core::process::prepare(cmd);
}
fn spawn(cmd: &mut Command) -> IdeResult<Child> {
+1 -1
View File
@@ -79,7 +79,7 @@ fn read_login_env(timeout: std::time::Duration) -> Option<Vec<(OsString, OsStrin
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".into());
const MARK: &str = "__OPENIDEA_ENV__";
let mut child = Command::new(&shell)
let mut child = ide_core::process::prepare(&mut Command::new(&shell))
.args(["-ilc", &format!("printf '{MARK}'; /usr/bin/env -0; printf '{MARK}'")])
.stdin(Stdio::null())
.stdout(Stdio::piped())
+1 -1
View File
@@ -355,7 +355,7 @@ impl Peer {
) -> std::io::Result<Self> {
use std::process::Stdio;
let mut cmd = tokio::process::Command::new(command);
ide_core::process::no_window(cmd.as_std_mut());
ide_core::process::prepare(cmd.as_std_mut());
cmd.args(args)
.current_dir(cwd)
.envs(ide_core::process::shell_env())
+2 -2
View File
@@ -116,7 +116,7 @@ fn resolve_rustup_proxy(command: &str, binary: PathBuf) -> Option<PathBuf> {
if !is_proxy || command.contains('/') {
return Some(binary);
}
let works = ide_core::process::no_window(&mut std::process::Command::new(&binary))
let works = ide_core::process::prepare(&mut std::process::Command::new(&binary))
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
@@ -190,7 +190,7 @@ fn login_shell_path() -> Option<OsString> {
use std::time::{Duration, Instant};
let shell = std::env::var_os("SHELL")?;
const MARK: &str = "__OPENIDEA_PATH__";
let mut child = Command::new(shell)
let mut child = ide_core::process::prepare(&mut Command::new(shell))
.args(["-l", "-c", &format!("printf '{MARK}%s{MARK}' \"$PATH\"")])
.stdin(Stdio::null())
.stdout(Stdio::piped())
+1 -1
View File
@@ -94,7 +94,7 @@ pub(crate) fn connect_or_start(
let args = substitute_args(&config.args, port, root);
let mut cmd = tokio::process::Command::new(binary);
ide_core::process::no_window(cmd.as_std_mut());
ide_core::process::prepare(cmd.as_std_mut());
cmd.args(&args)
.current_dir(root)
.envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
+1 -1
View File
@@ -161,7 +161,7 @@ pub fn collect(spec: &ProfileSpec) -> IdeResult<Profile> {
if !data.is_file() {
return Err(nothing_recorded("perf wrote no data"));
}
let out = ide_core::process::no_window(&mut Command::new(perf))
let out = ide_core::process::prepare(&mut Command::new(perf))
.args(["script", "-i"])
.arg(data.as_ref() as &Path)
.current_dir(cwd)
+1 -1
View File
@@ -209,7 +209,7 @@ pub fn save_templates(root: &Path, templates: &[RunConfig]) -> IdeResult<()> {
/// The environment runs inherit (unless "Include system environment variables" is off).
pub fn system_env() -> BTreeMap<String, String> {
std::env::vars().collect()
ide_core::process::host_vars().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?))).collect()
}
/// Path of the user run configuration file for `root`.
+9 -3
View File
@@ -407,6 +407,12 @@ impl ProcessManager {
if env.clear {
cmd.env_clear();
} else {
for (k, v) in ide_core::process::host_env_fixes() {
match v {
Some(v) => cmd.env(k, v),
None => cmd.env_remove(k),
}
}
for (k, v) in ide_core::process::shell_env() {
cmd.env(k, v);
}
@@ -602,7 +608,7 @@ fn launch_env(root: &Path, config: &RunConfig) -> IdeResult<LaunchEnv> {
BTreeMap::new()
} else {
let base: BTreeMap<String, String> =
if extras.include_system_env { std::env::vars().collect() } else { BTreeMap::new() };
if extras.include_system_env { crate::system_env() } else { BTreeMap::new() };
let paths: Vec<PathBuf> = extras.env_files.iter().map(|f| resolve_cwd(root, Some(Path::new(f)))).collect();
crate::envfile::load(&paths, &base).map_err(|e| IdeError::not_found(format!("cannot read .env file: {e}")))?
};
@@ -620,7 +626,7 @@ fn apply_env(cmd: &mut Command, env: &LaunchEnv) {
if env.clear {
cmd.env_clear();
} else {
cmd.envs(ide_core::process::shell_env());
ide_core::process::host_env(cmd).envs(ide_core::process::shell_env());
}
cmd.envs(&env.vars);
}
@@ -631,6 +637,7 @@ fn run_stop_command(stop: &StopCommand, output: &OutputFn) {
output(OutputChunk::System { text: format!("{}\n", stop.line) });
let inv = shell_invocation(&stop.line, cfg!(windows), std::env::var("COMSPEC").ok());
let mut cmd = Command::new(&inv.program);
ide_core::process::prepare(&mut cmd);
if inv.raw_args {
add_raw_args(&mut cmd, &inv.args);
} else {
@@ -638,7 +645,6 @@ fn run_stop_command(stop: &StopCommand, output: &OutputFn) {
}
cmd.current_dir(&stop.cwd).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::piped());
apply_env(&mut cmd, &stop.env);
ide_core::process::no_window(&mut cmd);
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
+1 -1
View File
@@ -93,7 +93,7 @@ pub fn find_tool(name: &str, config: &RunConfig, dir: &Path, root: &Path) -> Opt
pub fn probe(argv: &[String], dir: &Path, config: &RunConfig) -> bool {
let Some((program, args)) = argv.split_first() else { return false };
let program = resolve(program, config);
ide_core::process::no_window(&mut Command::new(program))
ide_core::process::prepare(&mut Command::new(program))
.args(args)
.current_dir(dir)
.envs(&config.env)
+2 -2
View File
@@ -59,7 +59,7 @@ pub(crate) fn query_in(
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
let mut child = ide_core::process::no_window(&mut cmd)
let mut child = ide_core::process::prepare(&mut cmd)
.args(args)
.env("PATH", search_path())
.envs(env.iter().copied())
@@ -112,7 +112,7 @@ pub(crate) fn stream(
on_line: &dyn Fn(String),
) -> IdeResult<String> {
let mut cmd = Command::new(program);
ide_core::process::no_window(&mut cmd);
ide_core::process::prepare(&mut cmd);
cmd.args(args).env("PATH", search_path()).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
cmd.envs(env.iter().map(|(k, v)| (k, v)));
if let Some(cwd) = cwd {
+6
View File
@@ -97,6 +97,12 @@ impl TerminalManager {
let mut cmd = CommandBuilder::new(&shell);
cmd.args(&args);
cmd.cwd(resolve_cwd(&options.cwd));
for (k, v) in ide_core::process::host_env_fixes() {
match v {
Some(v) => cmd.env(k, v),
None => cmd.env_remove(k),
}
}
for (k, v) in env {
cmd.env(k, v);
}
+1 -1
View File
@@ -113,7 +113,7 @@ fn extract_zip(archive: &Path, dest: &Path) -> IdeResult<()> {
/// `.tar.xz` goes through the system `tar` (bsdtar on macOS/Windows, GNU tar + xz on Linux).
fn extract_tar_xz(archive: &Path, dest: &Path) -> IdeResult<()> {
let tar = which::which("tar").map_err(|_| IdeError::unavailable("`tar` is required to unpack .tar.xz archives"))?;
let out = ide_core::process::no_window(&mut std::process::Command::new(tar))
let out = ide_core::process::prepare(&mut std::process::Command::new(tar))
.arg("-xJf")
.arg(archive)
.arg("-C")
+2 -2
View File
@@ -165,7 +165,7 @@ pub fn extend_process_path(app_data: &Path) {
/// `PATH` plus the usual per-user tool locations (GUI apps on macOS start with a minimal `PATH`).
pub fn default_search_path() -> OsString {
let mut dirs: Vec<PathBuf> =
std::env::var_os("PATH").map(|p| std::env::split_paths(&p).collect()).unwrap_or_default();
ide_core::process::host_var_os("PATH").map(|p| std::env::split_paths(&p).collect()).unwrap_or_default();
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).map(PathBuf::from) {
for rel in [".cargo/bin", ".local/bin", "go/bin", ".bun/bin", ".volta/bin", ".npm-global/bin", "Library/pnpm"] {
dirs.push(home.join(rel));
@@ -343,7 +343,7 @@ impl Tools {
fn command(&self, program: impl AsRef<std::ffi::OsStr>) -> Command {
let mut cmd = Command::new(program);
ide_core::process::no_window(&mut cmd).env("PATH", &self.child_path);
ide_core::process::prepare(&mut cmd).env("PATH", &self.child_path);
cmd
}
+3 -3
View File
@@ -48,7 +48,7 @@ pub fn run_streamed(cmd: Command, cancel: &CancellationToken, emit: Emit) -> Ide
/// Like [`run_streamed`] but returns `(success, output, status text)` instead of failing on a non-zero exit.
pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) -> IdeResult<(bool, String, String)> {
emit(InstallEvent::Line { text: format!("$ {}", describe(&cmd)) });
ide_core::process::no_window(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
ide_core::process::prepare(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| IdeError::unavailable(format!("cannot run {}: {e}", describe(&cmd))))?;
let (tx, rx) = mpsc::channel::<String>();
let pipes: [Option<Box<dyn Read + Send>>; 2] = [
@@ -94,7 +94,7 @@ pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) ->
/// Runs `cmd` to completion within `timeout`; returns (success, stdout + stderr).
pub fn run_quiet(mut cmd: Command, timeout: Duration) -> Option<(bool, String)> {
ide_core::process::no_window(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
ide_core::process::prepare(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = cmd.spawn().ok()?;
let mut stdout = child.stdout.take()?;
let mut stderr = child.stderr.take()?;
@@ -161,7 +161,7 @@ impl Http {
fn base(&self, url: &str) -> Command {
let mut cmd = Command::new(&self.curl);
ide_core::process::no_window(&mut cmd);
ide_core::process::prepare(&mut cmd);
cmd.args(["-fsSL", "--retry", "2", "--connect-timeout", "20", "-A", "openidea"]);
if url.starts_with("https://api.github.com/")
&& let Ok(token) = std::env::var("GITHUB_TOKEN").or_else(|_| std::env::var("GH_TOKEN"))
+2 -2
View File
@@ -72,7 +72,7 @@ pub fn git_version(executable: Option<&Path>) -> IdeResult<String> {
};
let mut cmd = Command::new(&exe);
cmd.arg("--version").stdin(Stdio::null()).env("LC_ALL", "C");
ide_core::process::no_window(&mut cmd);
ide_core::process::prepare(&mut cmd);
let out = cmd.output().map_err(|e| IdeError::unavailable(format!("cannot run {}: {e}", exe.display())))?;
let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !out.status.success() || !text.starts_with("git version") {
@@ -106,7 +106,7 @@ pub(crate) fn command(root: &Path) -> IdeResult<Command> {
for (k, v) in ASKPASS.read().unwrap_or_else(|e| e.into_inner()).iter() {
cmd.env(k, v);
}
ide_core::process::no_window(&mut cmd);
ide_core::process::prepare(&mut cmd);
Ok(cmd)
}
+32 -4
View File
@@ -96,18 +96,46 @@ pub fn open(url: &str, id: Option<&str>) -> IdeResult<()> {
if !(url.starts_with("http://") || url.starts_with("https://") || url.starts_with("file://")) {
return Err(IdeError::invalid(format!("not a web URL: {url}")));
}
let result = match id {
None => open::that_detached(url),
let app = match id {
None => None,
Some(id) => {
let known =
KNOWN.iter().find(|k| k.id == id).ok_or_else(|| IdeError::invalid(format!("unknown browser: {id}")))?;
let app = locate(known).ok_or_else(|| IdeError::unavailable(format!("{} is not installed", known.name)))?;
open::with_detached(url, app.to_string_lossy().as_ref())
Some(locate(known).ok_or_else(|| IdeError::unavailable(format!("{} is not installed", known.name)))?)
}
};
#[cfg(target_os = "linux")]
let result = spawn_with_host_env(match &app {
None => open::commands(url),
Some(app) => vec![open::with_command(url, app.to_string_lossy())],
});
#[cfg(not(target_os = "linux"))]
let result = match &app {
None => open::that_detached(url),
Some(app) => open::with_detached(url, app.to_string_lossy().as_ref()),
};
result.map_err(|e| IdeError::unavailable(format!("cannot open the browser: {e}")))
}
/// Starts the first launcher that runs (`xdg-open`, `gio`, ...), with the host's environment
/// rather than an AppImage's, and reaps it in the background.
#[cfg(target_os = "linux")]
fn spawn_with_host_env(commands: Vec<std::process::Command>) -> std::io::Result<()> {
use std::process::Stdio;
let mut last_err = None;
for mut cmd in commands {
ide_core::process::prepare(&mut cmd).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
match cmd.spawn() {
Ok(mut child) => {
std::thread::spawn(move || child.wait());
return Ok(());
}
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| std::io::Error::other("no launcher available")))
}
#[cfg(test)]
mod tests {
use super::*;