#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(
rust_2018_idioms,
deprecated_in_future,
macro_use_extern_crate,
missing_debug_implementations,
unused_qualifications
)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
mod browser_command;
#[cfg(all(feature = "reveal", target_os = "linux"))]
mod freedesktop;
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
mod linux_and_more;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
use crate::linux_and_more as sys;
#[cfg(target_os = "macos")]
use crate::macos as sys;
#[cfg(target_os = "windows")]
use crate::windows as sys;
use std::error::Error;
use std::ffi::{OsStr, OsString};
use std::fmt::{self, Display, Formatter};
use std::process::{Command, ExitStatus, Stdio};
use std::{env, io};
pub fn open<P>(path: P) -> Result<(), OpenError>
where
P: AsRef<OsStr>,
{
sys::open(path.as_ref())
}
pub fn open_browser<P>(path: P) -> Result<(), OpenError>
where
P: AsRef<OsStr>,
{
let mut path = path.as_ref();
if let Ok(browser_var) = env::var("BROWSER") {
let windows_path;
if is_wsl() && browser_var.ends_with(".exe") {
if let Some(windows_path_2) = wsl_to_windows_browser_argument(path) {
windows_path = windows_path_2;
path = &windows_path;
}
};
spawn_detached(
Command::new(&browser_var)
.arg(path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null()),
)
.map_err(|err| OpenError::Spawn {
cmds: browser_var,
source: err,
})
} else {
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
sys::open_browser(path)
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
{
sys::open(path)
}
}
}
#[cfg(feature = "reveal")]
pub fn reveal<P>(path: P) -> Result<(), OpenError>
where
P: AsRef<std::path::Path>,
{
sys::reveal(path.as_ref())
}
#[non_exhaustive]
#[derive(Debug)]
pub enum OpenError {
Io(io::Error),
Spawn {
cmds: String,
source: io::Error,
},
ExitStatus {
cmd: &'static str,
status: ExitStatus,
stderr: String,
},
}
impl Display for OpenError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
OpenError::Io(inner) => {
write!(f, "{inner}")?;
}
OpenError::Spawn { cmds, source: _ } => {
write!(f, "error spawning command(s) '{cmds}'")?;
}
OpenError::ExitStatus {
cmd,
status,
stderr,
} => {
write!(f, "command '{cmd}' did not execute successfully; {status}")?;
let stderr = stderr.trim();
if !stderr.is_empty() {
write!(f, "\ncommand stderr:\n{stderr}")?;
}
}
}
Ok(())
}
}
impl Error for OpenError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
OpenError::Io(inner) => inner.source(),
OpenError::Spawn { cmds: _, source } => Some(source),
OpenError::ExitStatus { .. } => None,
}
}
}
#[cfg(unix)]
fn spawn_detached(command: &mut Command) -> io::Result<()> {
let mut child = command.spawn()?;
let _ = std::thread::Builder::new()
.name("opener-reaper".into())
.stack_size(64 * 1024)
.spawn(move || child.wait());
Ok(())
}
#[cfg(not(unix))]
fn spawn_detached(command: &mut Command) -> io::Result<()> {
command.spawn().map(drop)
}
#[cfg(target_os = "linux")]
fn is_wsl() -> bool {
sys::is_wsl()
}
#[cfg(not(target_os = "linux"))]
fn is_wsl() -> bool {
false
}
#[cfg(target_os = "linux")]
fn wsl_to_windows_browser_argument(path: &OsStr) -> Option<OsString> {
sys::wsl_to_windows_browser_argument(path)
}
#[cfg(not(target_os = "linux"))]
fn wsl_to_windows_browser_argument(_path: &OsStr) -> Option<OsString> {
unreachable!()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn io_errors_display_the_underlying_error_once() {
let error = OpenError::Io(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid file URL path",
));
assert_eq!(error.to_string(), "invalid file URL path");
assert!(error.source().is_none());
}
#[cfg(target_os = "linux")]
#[test]
fn detached_children_are_reaped() {
use std::time::Duration;
let pid_file = env::temp_dir().join(format!("opener-reap-{}", std::process::id()));
spawn_detached(
Command::new("sh")
.args(["-c", r#"echo $$ > "$0""#])
.arg(&pid_file),
)
.unwrap();
let process = (0..250)
.find_map(|_| {
std::thread::sleep(Duration::from_millis(20));
let pid: u32 = std::fs::read_to_string(&pid_file)
.ok()?
.trim()
.parse()
.ok()?;
Some(std::path::PathBuf::from(format!("/proc/{pid}")))
})
.unwrap();
let _ = std::fs::remove_file(&pid_file);
assert!((0..250).any(|_| {
std::thread::sleep(Duration::from_millis(20));
!process.exists()
}));
}
}