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/// Builds a listener. We want to always set sleep_on_errors = true
14/// to avoid server errors when running low on file handles.
15/// sleep_on_errors became the default in hyper, but is not yet flipped in
16/// tokio-listener.
17#[cfg(feature = "listener")]
18pub async fn make_listener(
19    address: &tokio_listener::ListenerAddress,
20    user_options: &tokio_listener::UserOptions,
21) -> std::io::Result<tokio_listener::Listener> {
22    tokio_listener::Listener::bind(
23        address,
24        &{
25            let mut system_options = tokio_listener::SystemOptions::default();
26            system_options.sleep_on_errors = true;
27            system_options
28        },
29        user_options,
30    )
31    .await
32}
33
34/// Make an os-specific search path.
35///
36/// This concatenates `SNIX_LIBEXEC_PATH` environment variable, `default_libexec_path`
37/// argument, `PATH` environment variable and the directory of the current executable
38/// into one string separated by the os-specific path separator.
39///
40/// It does this to crate one giant search path of places to look for a sub-command
41/// binary.
42pub fn make_search_path(default_libexec_path: Option<&str>) -> Option<OsString> {
43    let libexec_path = var_os(DEFAULT_LIBEXEC_PATH_VAR);
44    let libexec_paths = libexec_path.iter().flat_map(split_paths);
45
46    let default_libexec_paths = default_libexec_path.iter().flat_map(split_paths);
47
48    let path = var_os("PATH");
49    let paths = path.iter().flat_map(split_paths);
50
51    let current_exe = std::env::current_exe()
52        .ok()
53        .and_then(|p| p.parent().map(ToOwned::to_owned));
54    let paths = libexec_paths
55        .chain(default_libexec_paths)
56        .chain(paths)
57        .chain(current_exe);
58    join_paths(paths).ok()
59}
60
61/// Search for a snix sub-command.
62///
63/// This searches the paths in the `SNIX_LIBEXEC_PATH` environment variable,
64/// the `default_libexec_path` argument, the `PATH` environment variable and
65/// the directory of the current executable in that order for a binary called
66/// `snix-{sub_cmd}` and will return the absolute path to it if found.
67pub fn find_command(sub_cmd: &str, default_libexec_path: Option<&str>) -> SnixCliResult<PathBuf> {
68    let cwd = std::env::current_exe()
69        .ok()
70        .and_then(|c| c.parent().map(ToOwned::to_owned))
71        .or_else(|| std::env::current_dir().ok())
72        .ok_or_else(|| io::Error::other("Could not resolve current directory"))?;
73    let binary_name = format!("snix-{sub_cmd}");
74    let search_path: Option<OsString> = make_search_path(default_libexec_path);
75    debug!(?search_path, binary_name, "Searching for {binary_name}");
76    Ok(which_in(binary_name, search_path, cwd)?)
77}
78
79/// future that listens to both ctrl-c and sigterm.
80pub async fn shutdown_signal() {
81    let ctrl_c = async {
82        tokio::signal::ctrl_c()
83            .await
84            .expect("failed to install Ctrl+C handler");
85    };
86
87    #[cfg(unix)]
88    let terminate = async {
89        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
90            .expect("failed to install SIGTERM handler")
91            .recv()
92            .await;
93    };
94
95    #[cfg(not(unix))]
96    let terminate = std::future::pending::<()>();
97
98    tokio::select! {
99        _ = ctrl_c => {},
100        _ = terminate => {},
101    }
102
103    debug!("signal received, shutting down…");
104}
105
106/// Opens a given path, with special-casing for `-` as stdin.
107pub async fn reader_for_path(
108    path: impl AsRef<std::path::Path>,
109) -> std::io::Result<Box<dyn tokio::io::AsyncBufRead + Unpin + Send>> {
110    use std::os::unix::fs::FileTypeExt;
111    use tokio::io::BufReader;
112
113    let path = path.as_ref();
114    if path == "-" {
115        Ok(Box::new(BufReader::new(tokio::io::stdin())) as Box<_>)
116    } else {
117        let metadata = tokio::fs::metadata(path).await?;
118
119        if metadata.file_type().is_socket() {
120            let stream = tokio::net::UnixStream::connect(path).await?;
121            Ok(Box::new(BufReader::new(stream)))
122        } else {
123            let file = tokio::fs::File::open(path).await?;
124            Ok(Box::new(BufReader::new(file)))
125        }
126    }
127}