use std::{
ffi::OsStr,
io,
os::unix::fs::PermissionsExt,
path::Path,
process::{ExitStatus, Stdio},
time::Duration,
};
pub(crate) const USES_PATHEXT: bool = false;
const EXECUTE_BITS: u32 = 0o111;
pub(crate) fn is_executable(path: &Path) -> bool {
path.metadata().is_ok_and(|metadata| {
metadata.is_file() && metadata.permissions().mode() & EXECUTE_BITS != 0
})
}
pub(crate) struct Tree {
pub(crate) child: tokio::process::Child,
group: Option<GroupId>,
}
struct GroupId(rustix::process::Pid);
pub(crate) fn spawn_tree(command: &mut tokio::process::Command) -> io::Result<Tree> {
command.process_group(0);
let child = command.spawn()?;
let group = child.id().map(|pid| {
let pid = i32::try_from(pid).expect("Unix process IDs fit pid_t");
GroupId(rustix::process::Pid::from_raw(pid).expect("a child's process ID is positive"))
});
Ok(Tree { child, group })
}
impl Tree {
pub(crate) fn kill(&mut self) {
if let Some(group) = self.group.take() {
let _ = rustix::process::kill_process_group(group.0, rustix::process::Signal::KILL);
}
}
}
impl Drop for Tree {
fn drop(&mut self) {
self.kill();
}
}
const OPEN_TIMEOUT: Duration = Duration::from_secs(3);
#[cfg(target_os = "macos")]
const OPENER: &str = "open";
#[cfg(not(target_os = "macos"))]
const OPENER: &str = "xdg-open";
pub(crate) async fn open_desktop(target: &OsStr) -> io::Result<()> {
let cwd = std::env::current_dir()?;
let program = crate::program::resolve(OsStr::new(OPENER), &cwd)?;
let mut command = tokio::process::Command::new(program);
command
.arg(target)
.env_clear()
.envs(crate::environment::user())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true);
let status = tokio::time::timeout(OPEN_TIMEOUT, command.status())
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "Desktop opener timed out."))??;
if !opened(&status, is_wsl()) {
return Err(io::Error::other(format!(
"Desktop opener exited with {status}."
)));
}
Ok(())
}
const WSL_EXPLORER_STATUS: i32 = 4;
fn opened(status: &ExitStatus, wsl: bool) -> bool {
status.success() || (wsl && status.code() == Some(WSL_EXPLORER_STATUS))
}
fn is_wsl() -> bool {
cfg!(target_os = "linux")
&& std::fs::read_to_string("/proc/version")
.is_ok_and(|version| version.contains("microsoft"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::process::ExitStatusExt;
fn exited(code: i32) -> ExitStatus {
ExitStatus::from_raw(code << 8)
}
#[test]
fn wsl_explorer_status_counts_as_opened_only_on_wsl() {
assert!(opened(&exited(0), false));
assert!(opened(&exited(0), true));
assert!(opened(&exited(WSL_EXPLORER_STATUS), true));
assert!(!opened(&exited(WSL_EXPLORER_STATUS), false));
for code in [1, 2, 3] {
assert!(!opened(&exited(code), true));
}
}
}