#![cfg(unix)] use crate::shell::Shell; use log::debug; use std::io::{Error, ErrorKind}; use thiserror::Error; #[derive(Debug)] struct ProcessInfo { parent_pid: Option, command: String, } const MAX_ITERATIONS: u8 = 10; pub fn infer_shell() -> Option> { let mut pid = Some(std::process::id()); let mut visited = 0; while let Some(current_pid) = pid { if visited > MAX_ITERATIONS { return None; } let process_info = get_process_info(current_pid) .map_err(|err| { debug!("{}", err); err }) .ok()?; let binary = process_info .command .trim_start_matches('-') .split('/') .last()?; if let Some(shell) = super::shell_from_string(binary) { return Some(shell); } pid = process_info.parent_pid; visited += 1; } None } fn get_process_info(pid: u32) -> Result { use std::io::{BufRead, BufReader}; use std::process::Command; let buffer = Command::new("ps") .arg("-o") .arg("ppid,comm") .arg(pid.to_string()) .stdout(std::process::Stdio::piped()) .spawn()? .stdout .ok_or_else(|| Error::from(ErrorKind::UnexpectedEof))?; let mut lines = BufReader::new(buffer).lines(); // skip header line lines .next() .ok_or_else(|| Error::from(ErrorKind::UnexpectedEof))??; let line = lines .next() .ok_or_else(|| Error::from(ErrorKind::NotFound))??; let mut parts = line.trim().split_whitespace(); let ppid = parts.next().ok_or_else(|| ProcessInfoError::Parse { expectation: "Can't read the ppid from ps, should be the first item in the table", got: line.to_string(), })?; let command = parts.next().ok_or_else(|| ProcessInfoError::Parse { expectation: "Can't read the command from ps, should be the second item in the table", got: line.to_string(), })?; Ok(ProcessInfo { parent_pid: ppid.parse().ok(), command: command.into(), }) } #[derive(Debug, Error)] enum ProcessInfoError { #[error("Can't read process info: {source}")] Io { #[source] #[from] source: std::io::Error, }, #[error("Can't parse process info output. {expectation}. Got: {got}")] Parse { got: String, expectation: &'static str, }, } #[cfg(all(test, unix))] mod tests { use super::*; use pretty_assertions::assert_eq; use std::process::{Command, Stdio}; #[test] fn test_get_process_info() -> anyhow::Result<()> { let subprocess = Command::new("bash") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let process_info = get_process_info(subprocess.id()); let parent_pid = process_info.ok().and_then(|x| x.parent_pid); assert_eq!(parent_pid, Some(std::process::id())); Ok(()) } }