Skip to main content

snix_cli/
lib.rs

1use std::env::{join_paths, split_paths, var_os};
2use std::ffi::OsString;
3use std::io;
4use std::path::PathBuf;
5
6use tracing::debug;
7use which::which_in;
8
9pub type SnixCliResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
10
11pub const DEFAULT_LIBEXEC_PATH_VAR: &str = "SNIX_LIBEXEC_PATH";
12
13/// Make an os-specific search path.
14///
15/// This concatenates `SNIX_LIBEXEC_PATH` environment variable, `default_libexec_path`
16/// argument, `PATH` environment variable and the directory of the current executable
17/// into one string separated by the os-specific path separator.
18///
19/// It does this to crate one giant search path of places to look for a sub-command
20/// binary.
21pub fn make_search_path(default_libexec_path: Option<&str>) -> Option<OsString> {
22    let libexec_path = var_os(DEFAULT_LIBEXEC_PATH_VAR);
23    let libexec_paths = libexec_path.iter().flat_map(split_paths);
24
25    let default_libexec_paths = default_libexec_path.iter().flat_map(split_paths);
26
27    let path = var_os("PATH");
28    let paths = path.iter().flat_map(split_paths);
29
30    let current_exe = std::env::current_exe()
31        .ok()
32        .and_then(|p| p.parent().map(ToOwned::to_owned));
33    let paths = libexec_paths
34        .chain(default_libexec_paths)
35        .chain(paths)
36        .chain(current_exe);
37    join_paths(paths).ok()
38}
39
40/// Search for a snix sub-command.
41///
42/// This searches the paths in the `SNIX_LIBEXEC_PATH` environment variable,
43/// the `default_libexec_path` argument, the `PATH` environment variable and
44/// the directory of the current executable in that order for a binary called
45/// `snix-{sub_cmd}` and will return the absolute path to it if found.
46pub fn find_command(sub_cmd: &str, default_libexec_path: Option<&str>) -> SnixCliResult<PathBuf> {
47    let cwd = std::env::current_exe()
48        .ok()
49        .and_then(|c| c.parent().map(ToOwned::to_owned))
50        .or_else(|| std::env::current_dir().ok())
51        .ok_or_else(|| io::Error::other("Could not resolve current directory"))?;
52    let binary_name = format!("snix-{sub_cmd}");
53    let search_path: Option<OsString> = make_search_path(default_libexec_path);
54    debug!(?search_path, binary_name, "Searching for {binary_name}");
55    Ok(which_in(binary_name, search_path, cwd)?)
56}
57
58/// future that listens to both ctrl-c and sigterm.
59pub async fn shutdown_signal() {
60    let ctrl_c = async {
61        tokio::signal::ctrl_c()
62            .await
63            .expect("failed to install Ctrl+C handler");
64    };
65
66    #[cfg(unix)]
67    let terminate = async {
68        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
69            .expect("failed to install SIGTERM handler")
70            .recv()
71            .await;
72    };
73
74    #[cfg(not(unix))]
75    let terminate = std::future::pending::<()>();
76
77    tokio::select! {
78        _ = ctrl_c => {},
79        _ = terminate => {},
80    }
81
82    debug!("signal received, shutting down…");
83}
84
85/// Opens a given path, with special-casing for `-` as stdin.
86pub async fn reader_for_path(
87    path: impl AsRef<std::path::Path>,
88) -> std::io::Result<Box<dyn tokio::io::AsyncBufRead + Unpin + Send>> {
89    use std::os::unix::fs::FileTypeExt;
90    use tokio::io::BufReader;
91
92    let path = path.as_ref();
93    if path == "-" {
94        Ok(Box::new(BufReader::new(tokio::io::stdin())) as Box<_>)
95    } else {
96        let metadata = tokio::fs::metadata(path).await?;
97
98        if metadata.file_type().is_socket() {
99            let stream = tokio::net::UnixStream::connect(path).await?;
100            Ok(Box::new(BufReader::new(stream)))
101        } else {
102            let file = tokio::fs::File::open(path).await?;
103            Ok(Box::new(BufReader::new(file)))
104        }
105    }
106}