Add fallback parent process detection to uv tool update-shell (#15356)

## Summary

Closes #15355

This PR adds a fallback mechanism to `Shell::from_env()` that inspects
the parent process when shell environment variables are not available on
Unix-like systems.

Currently, `uv tool update-shell` fails with "the current shell could
not be determined" when environment variables like `ZSH_VERSION`,
`BASH_VERSION`, or `SHELL` are not exported. This commonly occurs in
automated environments such as GitHub Actions runners.

The fallback approach:
1. Uses `nix::unistd::getppid()` to get the parent process ID
2. Reads `/proc/<ppid>/exe` to determine the parent executable path
3. Falls back to `/proc/<ppid>/comm` if the exe symlink fails  
4. Uses existing `parse_shell_from_path()` to identify the shell type

This maintains full backward compatibility - the fallback only activates
when environment variables are unavailable and an error would otherwise
occur.

## Test Plan

Tested locally with:

```bash
env -u ZSH_VERSION -u SHELL PATH="/usr/bin:/bin" $(which cargo) run -- tool update-shell --verbose
```
```text
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/uv tool update-shell --verbose`
DEBUG uv 0.8.11
DEBUG Ensuring that the executable directory is in PATH: /home/user/.local/bin
DEBUG Detected parent process ID: 4147396
DEBUG Parent process executable: /usr/bin/zsh
Updated configuration file: /home/user/.zshenv
Restart your shell to apply changes
```
This commit is contained in:
Ed Rogers 2025-08-18 13:48:34 -05:00 committed by GitHub
parent 242214c546
commit 4b88b1379a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 0 deletions

1
Cargo.lock generated
View file

@ -6272,6 +6272,7 @@ dependencies = [
"anyhow",
"fs-err",
"home",
"nix 0.30.1",
"same-file",
"tempfile",
"tracing",

View file

@ -15,10 +15,14 @@ uv-fs = { workspace = true }
uv-static = { workspace = true }
anyhow = { workspace = true }
fs-err = { workspace = true }
home = { workspace = true }
same-file = { workspace = true }
tracing = { workspace = true }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows-registry = { workspace = true }
windows-result = { workspace = true }

View file

@ -8,6 +8,9 @@ use std::path::{Path, PathBuf};
use uv_fs::Simplified;
use uv_static::EnvVars;
#[cfg(unix)]
use tracing::debug;
/// Shells for which virtualenv activation scripts are available.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[allow(clippy::doc_markdown)]
@ -64,6 +67,51 @@ impl Shell {
Some(Self::Powershell)
}
} else {
// Fallback to detecting the shell from the parent process
Self::from_parent_process()
}
}
/// Attempt to determine the shell from the parent process.
///
/// This is a fallback method for when environment variables don't provide
/// enough information about the current shell. It looks at the parent process
/// to try to identify which shell is running.
///
/// This method currently only works on Unix-like systems. On other platforms,
/// it returns `None`.
fn from_parent_process() -> Option<Self> {
#[cfg(unix)]
{
// Get the parent process ID
let ppid = nix::unistd::getppid();
debug!("Detected parent process ID: {ppid}");
// Try to read the parent process executable path
let proc_exe_path = format!("/proc/{ppid}/exe");
if let Ok(exe_path) = fs_err::read_link(&proc_exe_path) {
debug!("Parent process executable: {}", exe_path.display());
if let Some(shell) = Self::from_shell_path(&exe_path) {
return Some(shell);
}
}
// If reading exe fails, try reading the comm file
let proc_comm_path = format!("/proc/{ppid}/comm");
if let Ok(comm) = fs_err::read_to_string(&proc_comm_path) {
let comm = comm.trim();
debug!("Parent process comm: {comm}");
if let Some(shell) = parse_shell_from_path(Path::new(comm)) {
return Some(shell);
}
}
debug!("Could not determine shell from parent process");
None
}
#[cfg(not(unix))]
{
None
}
}