use std::{
ffi::OsStr, io, os::unix::fs::PermissionsExt, path::Path, process::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)
.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 !status.success() {
return Err(io::Error::other(format!(
"Desktop opener exited with {status}."
)));
}
Ok(())
}