From 4eaae55d53e1d816e2f891adad529f960d7cdeed Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Mon, 5 Jun 2023 18:01:16 -0500 Subject: [PATCH] Refactor to use debouncer for file watching and support configuration (#195) --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 16 ++ Cargo.lock | 23 ++ README.md | 6 +- distant-auth/README.md | 6 +- distant-core/README.md | 6 +- distant-local/Cargo.toml | 3 +- distant-local/README.md | 10 +- distant-local/src/api.rs | 32 ++- distant-local/src/api/state.rs | 8 +- distant-local/src/api/state/search.rs | 4 +- distant-local/src/api/state/watcher.rs | 151 +++++++++---- distant-local/src/config.rs | 28 +++ distant-local/src/lib.rs | 17 +- distant-local/tests/stress/fixtures.rs | 4 +- distant-net/README.md | 6 +- distant-protocol/README.md | 6 +- distant-ssh2/README.md | 6 +- src/cli/commands/client.rs | 2 +- src/cli/commands/server.rs | 14 +- src/cli/common/spawner.rs | 2 +- src/options.rs | 125 ++++++++++- src/options/common.rs | 2 + src/options/common/time.rs | 284 +++++++++++++++++++++++++ src/options/config.rs | 27 ++- src/options/config.toml | 24 +++ src/options/config/client/api.rs | 4 +- src/options/config/server.rs | 4 + src/options/config/server/watch.rs | 24 +++ 29 files changed, 737 insertions(+), 109 deletions(-) create mode 100644 distant-local/src/config.rs create mode 100644 src/options/common/time.rs create mode 100644 src/options/config/server/watch.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c05a8e6..7654638 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,7 @@ jobs: - { rust: stable, os: windows-latest, target: x86_64-pc-windows-msvc } - { rust: stable, os: macos-latest } - { rust: stable, os: ubuntu-latest } - - { rust: 1.64.0, os: ubuntu-latest } + - { rust: 1.68.0, os: ubuntu-latest } steps: - uses: actions/checkout@v3 - name: Install Rust ${{ matrix.rust }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 93dd356..82dabc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `distant-local` now has two features: `macos-fsevent` and `macos-kqueue`. These are used to indicate what kind of file watching to support (for MacOS). The default is `macos-fsevent`. +- `[server.watch]` configuration is now available with the following + settings: + - `native = ` to specify whether to use native watching or polling + (default true) + - `poll_interval = ` to specify seconds to wait between polling + attempts (only for polling watcher) + - `compare_contents = ` to specify how polling watcher will evaluate a + file change (default false) + - `debounce_timeout = ` to specify how long to wait before sending a + change notification (will aggregate and merge changes) + - `debounce_tick_rate = ` to specify how long to wait between event + aggregation loops + +### Changed + +- Bump minimum Rust version to 1.68.0 ### Removed diff --git a/Cargo.lock b/Cargo.lock index fd4b8d4..d806f44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,6 +902,7 @@ dependencies = [ "indoc", "log", "notify", + "notify-debouncer-full", "num_cpus", "once_cell", "portable-pty 0.8.1", @@ -1142,6 +1143,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "file-id" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13be71e6ca82e91bc0cb862bebaac0b2d1924a5a1d970c822b2f98b63fda8c3" +dependencies = [ + "winapi-util", +] + [[package]] name = "file-mode" version = "0.1.2" @@ -1965,6 +1975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2" dependencies = [ "bitflags 1.3.2", + "crossbeam-channel", "filetime", "fsevent-sys", "inotify", @@ -1975,6 +1986,18 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "notify-debouncer-full" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4812c1eb49be776fb8df4961623bdc01ec9dfdc1abe8211ceb09150a2e64219" +dependencies = [ + "file-id", + "notify", + "parking_lot 0.12.1", + "walkdir", +] + [[package]] name = "ntapi" version = "0.4.1" diff --git a/README.md b/README.md index 853322b..6ffbdb5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # distant - remotely edit files and run programs -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![CI][distant_ci_img]][distant_ci_lnk] [![RustC 1.64+][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![CI][distant_ci_img]][distant_ci_lnk] [![RustC 1.68+][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant.svg [distant_crates_lnk]: https://crates.io/crates/distant @@ -8,8 +8,8 @@ [distant_doc_lnk]: https://docs.rs/distant [distant_ci_img]: https://github.com/chipsenkbeil/distant/actions/workflows/ci.yml/badge.svg [distant_ci_lnk]: https://github.com/chipsenkbeil/distant/actions/workflows/ci.yml -[distant_rustc_img]: https://img.shields.io/badge/distant-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html 🚧 **(Alpha stage software) This program is in rapid development and may break or change frequently!** 🚧 diff --git a/distant-auth/README.md b/distant-auth/README.md index 4f79e68..08eb082 100644 --- a/distant-auth/README.md +++ b/distant-auth/README.md @@ -1,13 +1,13 @@ # distant auth -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.64.0][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.68.0][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant-auth.svg [distant_crates_lnk]: https://crates.io/crates/distant-auth [distant_doc_img]: https://docs.rs/distant-auth/badge.svg [distant_doc_lnk]: https://docs.rs/distant-auth -[distant_rustc_img]: https://img.shields.io/badge/distant_auth-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant_auth-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html ## Details diff --git a/distant-core/README.md b/distant-core/README.md index 9b8c01f..aeaf594 100644 --- a/distant-core/README.md +++ b/distant-core/README.md @@ -1,13 +1,13 @@ # distant core -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.64.0][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.68.0][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant-core.svg [distant_crates_lnk]: https://crates.io/crates/distant-core [distant_doc_img]: https://docs.rs/distant-core/badge.svg [distant_doc_lnk]: https://docs.rs/distant-core -[distant_rustc_img]: https://img.shields.io/badge/distant_core-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant_core-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html ## Details diff --git a/distant-local/Cargo.toml b/distant-local/Cargo.toml index 1b427f1..5a54905 100644 --- a/distant-local/Cargo.toml +++ b/distant-local/Cargo.toml @@ -25,7 +25,8 @@ distant-core = { version = "=0.20.0-alpha.7", path = "../distant-core" } grep = "0.2.12" ignore = "0.4.20" log = "0.4.18" -notify = { version = "6.0.0", default-features = false } +notify = { version = "6.0.0", default-features = false, features = ["macos_fsevent"] } +notify-debouncer-full = { version = "0.1.0", default-features = false } num_cpus = "1.15.0" portable-pty = "0.8.1" rand = { version = "0.8.5", features = ["getrandom"] } diff --git a/distant-local/README.md b/distant-local/README.md index 2f2c9d8..cd6a251 100644 --- a/distant-local/README.md +++ b/distant-local/README.md @@ -1,13 +1,13 @@ # distant local -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.64.0][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.68.0][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant-local.svg [distant_crates_lnk]: https://crates.io/crates/distant-local [distant_doc_img]: https://docs.rs/distant-local/badge.svg [distant_doc_lnk]: https://docs.rs/distant-local -[distant_rustc_img]: https://img.shields.io/badge/distant_local-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant_local-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html ## Details @@ -27,8 +27,10 @@ distant-local = "0.20" ## Examples ```rust,no_run +use distant_local::{Config, new_handler}; + // Create a server API handler to be used with the server -let handler = distant_local::initialize_handler().unwrap(); +let handler = new_handler(Config::default()).unwrap(); ``` ## License diff --git a/distant-local/src/api.rs b/distant-local/src/api.rs index 2b125d3..eb731f3 100644 --- a/distant-local/src/api.rs +++ b/distant-local/src/api.rs @@ -14,8 +14,9 @@ use log::*; use tokio::io::AsyncWriteExt; use walkdir::WalkDir; -mod process; +use crate::config::Config; +mod process; mod state; use state::*; @@ -23,21 +24,21 @@ use state::*; /// where the server using this api is running. In other words, this is a direct /// impementation of the API instead of a proxy to another machine as seen with /// implementations on top of SSH and other protocol. -pub struct LocalDistantApi { +pub struct Api { state: GlobalState, } -impl LocalDistantApi { +impl Api { /// Initialize the api instance - pub fn initialize() -> io::Result { + pub fn initialize(config: Config) -> io::Result { Ok(Self { - state: GlobalState::initialize()?, + state: GlobalState::initialize(config)?, }) } } #[async_trait] -impl DistantApi for LocalDistantApi { +impl DistantApi for Api { type LocalData = (); async fn read_file( @@ -152,7 +153,7 @@ impl DistantApi for LocalDistantApi { // Traverse, but don't include root directory in entries (hence min depth 1), unless indicated // to do so (min depth 0) let dir = WalkDir::new(root_path.as_path()) - .min_depth(if include_root { 0 } else { 1 }) + .min_depth(usize::from(!include_root)) .sort_by_file_name(); // If depth > 0, will recursively traverse to specified max depth, otherwise @@ -709,6 +710,7 @@ mod tests { use tokio::sync::mpsc; use super::*; + use crate::config::WatchConfig; static TEMP_SCRIPT_DIR: Lazy = Lazy::new(|| assert_fs::TempDir::new().unwrap()); @@ -769,8 +771,16 @@ mod tests { static DOES_NOT_EXIST_BIN: Lazy = Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin")); - async fn setup(buffer: usize) -> (LocalDistantApi, DistantCtx<()>, mpsc::Receiver) { - let api = LocalDistantApi::initialize().unwrap(); + const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); + + async fn setup(buffer: usize) -> (Api, DistantCtx<()>, mpsc::Receiver) { + let api = Api::initialize(Config { + watch: WatchConfig { + debounce_timeout: DEBOUNCE_TIMEOUT, + ..Default::default() + }, + }) + .unwrap(); let (reply, rx) = make_reply(buffer); let connection_id = rand::random(); @@ -1613,7 +1623,7 @@ mod tests { api.watch( ctx, - file.path().to_path_buf(), + temp.path().to_path_buf(), /* recursive */ true, /* only */ Default::default(), /* except */ Default::default(), @@ -1630,7 +1640,7 @@ mod tests { // Sleep a bit to give time to get all changes happening // TODO: Can we slim down this sleep? Or redesign test in some other way? - tokio::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(DEBOUNCE_TIMEOUT + Duration::from_millis(100)).await; // Collect all responses, as we may get multiple for interactions within a directory let mut responses = Vec::new(); diff --git a/distant-local/src/api/state.rs b/distant-local/src/api/state.rs index f95e1c8..7a78943 100644 --- a/distant-local/src/api/state.rs +++ b/distant-local/src/api/state.rs @@ -1,5 +1,7 @@ use std::io; +use crate::config::Config; + mod process; pub use process::*; @@ -22,11 +24,13 @@ pub struct GlobalState { } impl GlobalState { - pub fn initialize() -> io::Result { + pub fn initialize(config: Config) -> io::Result { Ok(Self { process: ProcessState::new(), search: SearchState::new(), - watcher: WatcherState::initialize()?, + watcher: WatcherBuilder::new() + .with_config(config.watch) + .initialize()?, }) } } diff --git a/distant-local/src/api/state/search.rs b/distant-local/src/api/state/search.rs index 0e07cbb..7d9f33e 100644 --- a/distant-local/src/api/state/search.rs +++ b/distant-local/src/api/state/search.rs @@ -812,10 +812,10 @@ mod tests { use super::*; fn make_path(path: &str) -> PathBuf { - use std::path::MAIN_SEPARATOR; + use std::path::MAIN_SEPARATOR_STR; // Ensure that our path is compliant with the current platform - let path = path.replace('/', &MAIN_SEPARATOR.to_string()); + let path = path.replace('/', MAIN_SEPARATOR_STR); PathBuf::from(path) } diff --git a/distant-local/src/api/state/watcher.rs b/distant-local/src/api/state/watcher.rs index fcb00cb..1d4c222 100644 --- a/distant-local/src/api/state/watcher.rs +++ b/distant-local/src/api/state/watcher.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::io; use std::ops::Deref; use std::path::{Path, PathBuf}; +use std::time::Duration; use distant_core::net::common::ConnectionId; use distant_core::protocol::ChangeKind; @@ -9,49 +10,49 @@ use log::*; use notify::event::{AccessKind, AccessMode, ModifyKind}; use notify::{ Config as WatcherConfig, Error as WatcherError, ErrorKind as WatcherErrorKind, - Event as WatcherEvent, EventKind, PollWatcher, RecursiveMode, Watcher, + Event as WatcherEvent, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher, }; +use notify_debouncer_full::{new_debouncer_opt, DebounceEventResult, Debouncer, FileIdMap}; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinHandle; +use crate::config::WatchConfig; use crate::constants::SERVER_WATCHER_CAPACITY; mod path; pub use path::*; -/// Holds information related to watched paths on the server -pub struct WatcherState { - channel: WatcherChannel, - task: JoinHandle<()>, +/// Builder for a watcher. +#[derive(Default)] +pub struct WatcherBuilder { + config: WatchConfig, } -impl Drop for WatcherState { - /// Aborts the task that handles watcher path operations and management - fn drop(&mut self) { - self.abort(); +impl WatcherBuilder { + /// Creates a new builder configured to use the native watcher using default configuration. + pub fn new() -> Self { + Self::default() + } + + /// Swaps the configuration with the provided one. + pub fn with_config(self, config: WatchConfig) -> Self { + Self { config } } -} -impl WatcherState { /// Will create a watcher and initialize watched paths to be empty - pub fn initialize() -> io::Result { + pub fn initialize(self) -> io::Result { // NOTE: Cannot be something small like 1 as this seems to cause a deadlock sometimes // with a large volume of watch requests let (tx, rx) = mpsc::channel(SERVER_WATCHER_CAPACITY); - macro_rules! spawn_watcher { - ($watcher:ident) => {{ - Self { - channel: WatcherChannel { tx }, - task: tokio::spawn(watcher_task($watcher, rx)), - } - }}; - } + let watcher_config = WatcherConfig::default() + .with_compare_contents(self.config.compare_contents) + .with_poll_interval(self.config.poll_interval.unwrap_or(Duration::from_secs(30))); - macro_rules! event_handler { - ($tx:ident) => { - move |res| match $tx.try_send(match res { + macro_rules! process_event { + ($tx:ident, $evt:expr) => { + match $tx.try_send(match $evt { Ok(x) => InnerWatcherMsg::Event { ev: x }, Err(x) => InnerWatcherMsg::Error { err: x }, }) { @@ -69,30 +70,83 @@ impl WatcherState { }; } + macro_rules! new_debouncer { + ($watcher:ident, $tx:ident) => {{ + new_debouncer_opt::<_, $watcher, FileIdMap>( + self.config.debounce_timeout, + self.config.debounce_tick_rate, + move |result: DebounceEventResult| match result { + Ok(events) => { + for x in events { + process_event!($tx, Ok(x)); + } + } + Err(errors) => { + for x in errors { + process_event!($tx, Err(x)); + } + } + }, + FileIdMap::new(), + watcher_config, + ) + }}; + } + + macro_rules! spawn_task { + ($debouncer:expr) => {{ + WatcherState { + channel: WatcherChannel { tx }, + task: tokio::spawn(watcher_task($debouncer, rx)), + } + }}; + } + let tx = tx.clone(); - let result = { - let tx = tx.clone(); - notify::recommended_watcher(event_handler!(tx)) - }; - - match result { - Ok(watcher) => Ok(spawn_watcher!(watcher)), - Err(x) => match x.kind { - // notify-rs has a bug on Mac M1 with Docker and Linux, so we detect that error - // and fall back to the poll watcher if this occurs - // - // https://github.com/notify-rs/notify/issues/423 - WatcherErrorKind::Io(x) if x.raw_os_error() == Some(38) => { - warn!("Recommended watcher is unsupported! Falling back to polling watcher!"); - let watcher = PollWatcher::new(event_handler!(tx), WatcherConfig::default()) - .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; - Ok(spawn_watcher!(watcher)) + if self.config.native { + let result = { + let tx = tx.clone(); + new_debouncer!(RecommendedWatcher, tx) + }; + + match result { + Ok(debouncer) => Ok(spawn_task!(debouncer)), + Err(x) => { + match x.kind { + // notify-rs has a bug on Mac M1 with Docker and Linux, so we detect that error + // and fall back to the poll watcher if this occurs + // + // https://github.com/notify-rs/notify/issues/423 + WatcherErrorKind::Io(x) if x.raw_os_error() == Some(38) => { + warn!("Recommended watcher is unsupported! Falling back to polling watcher!"); + Ok(spawn_task!(new_debouncer!(PollWatcher, tx) + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?)) + } + _ => Err(io::Error::new(io::ErrorKind::Other, x)), + } } - _ => Err(io::Error::new(io::ErrorKind::Other, x)), - }, + } + } else { + Ok(spawn_task!(new_debouncer!(PollWatcher, tx) + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?)) } } +} +/// Holds information related to watched paths on the server +pub struct WatcherState { + channel: WatcherChannel, + task: JoinHandle<()>, +} + +impl Drop for WatcherState { + /// Aborts the task that handles watcher path operations and management + fn drop(&mut self) { + self.abort(); + } +} + +impl WatcherState { /// Aborts the watcher task pub fn abort(&self) { self.task.abort(); @@ -169,7 +223,12 @@ enum InnerWatcherMsg { }, } -async fn watcher_task(mut watcher: impl Watcher, mut rx: mpsc::Receiver) { +async fn watcher_task( + mut debouncer: Debouncer, + mut rx: mpsc::Receiver, +) where + W: Watcher, +{ // TODO: Optimize this in some way to be more performant than // checking every path whenever an event comes in let mut registered_paths: Vec = Vec::new(); @@ -193,7 +252,8 @@ async fn watcher_task(mut watcher: impl Watcher, mut rx: mpsc::Receiver, + pub compare_contents: bool, + pub debounce_timeout: Duration, + pub debounce_tick_rate: Option, +} + +impl Default for WatchConfig { + fn default() -> Self { + Self { + native: true, + poll_interval: None, + compare_contents: false, + debounce_timeout: Duration::from_millis(500), + debounce_tick_rate: None, + } + } +} diff --git a/distant-local/src/lib.rs b/distant-local/src/lib.rs index dc0bbc7..e17f39e 100644 --- a/distant-local/src/lib.rs +++ b/distant-local/src/lib.rs @@ -5,17 +5,16 @@ pub struct ReadmeDoctests; mod api; +mod config; mod constants; -pub use api::LocalDistantApi; +pub use api::Api; +pub use config::*; use distant_core::{DistantApi, DistantApiServerHandler}; -/// Implementation of [`DistantApiServerHandler`] using [`LocalDistantApi`]. -pub type LocalDistantApiServerHandler = - DistantApiServerHandler::LocalData>; +/// Implementation of [`DistantApiServerHandler`] using [`Api`]. +pub type Handler = DistantApiServerHandler::LocalData>; -/// Initializes a new [`LocalDistantApiServerHandler`]. -pub fn initialize_handler() -> std::io::Result { - Ok(LocalDistantApiServerHandler::new( - LocalDistantApi::initialize()?, - )) +/// Initializes a new [`Handler`]. +pub fn new_handler(config: Config) -> std::io::Result { + Ok(Handler::new(Api::initialize(config)?)) } diff --git a/distant-local/tests/stress/fixtures.rs b/distant-local/tests/stress/fixtures.rs index 63ed8bf..2bd1672 100644 --- a/distant-local/tests/stress/fixtures.rs +++ b/distant-local/tests/stress/fixtures.rs @@ -6,7 +6,7 @@ use distant_core::net::client::{Client, TcpConnector}; use distant_core::net::common::PortRange; use distant_core::net::server::Server; use distant_core::{DistantApiServerHandler, DistantClient}; -use distant_local::LocalDistantApi; +use distant_local::Api; use rstest::*; use tokio::sync::mpsc; @@ -22,7 +22,7 @@ impl DistantClientCtx { let (started_tx, mut started_rx) = mpsc::channel::(1); tokio::spawn(async move { - if let Ok(api) = LocalDistantApi::initialize() { + if let Ok(api) = Api::initialize(Default::default()) { let port: PortRange = "0".parse().unwrap(); let port = { let handler = DistantApiServerHandler::new(api); diff --git a/distant-net/README.md b/distant-net/README.md index f31c71b..c9e66af 100644 --- a/distant-net/README.md +++ b/distant-net/README.md @@ -1,13 +1,13 @@ # distant net -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.64.0][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.68.0][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant-net.svg [distant_crates_lnk]: https://crates.io/crates/distant-net [distant_doc_img]: https://docs.rs/distant-net/badge.svg [distant_doc_lnk]: https://docs.rs/distant-net -[distant_rustc_img]: https://img.shields.io/badge/distant_net-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant_net-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html ## Details diff --git a/distant-protocol/README.md b/distant-protocol/README.md index c56a918..d2db26a 100644 --- a/distant-protocol/README.md +++ b/distant-protocol/README.md @@ -1,13 +1,13 @@ # distant protocol -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.64.0][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.68.0][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant-protocol.svg [distant_crates_lnk]: https://crates.io/crates/distant-protocol [distant_doc_img]: https://docs.rs/distant-protocol/badge.svg [distant_doc_lnk]: https://docs.rs/distant-protocol -[distant_rustc_img]: https://img.shields.io/badge/distant_protocol-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant_protocol-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html ## Details diff --git a/distant-ssh2/README.md b/distant-ssh2/README.md index a7b42dd..974874c 100644 --- a/distant-ssh2/README.md +++ b/distant-ssh2/README.md @@ -1,13 +1,13 @@ # distant ssh2 -[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.64.0][distant_rustc_img]][distant_rustc_lnk] +[![Crates.io][distant_crates_img]][distant_crates_lnk] [![Docs.rs][distant_doc_img]][distant_doc_lnk] [![Rustc 1.68.0][distant_rustc_img]][distant_rustc_lnk] [distant_crates_img]: https://img.shields.io/crates/v/distant-ssh2.svg [distant_crates_lnk]: https://crates.io/crates/distant-ssh2 [distant_doc_img]: https://docs.rs/distant-ssh2/badge.svg [distant_doc_lnk]: https://docs.rs/distant-ssh2 -[distant_rustc_img]: https://img.shields.io/badge/distant_ssh2-rustc_1.64+-lightgray.svg -[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html +[distant_rustc_img]: https://img.shields.io/badge/distant_ssh2-rustc_1.68+-lightgray.svg +[distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html Library provides native ssh integration into the [`distant`](https://github.com/chipsenkbeil/distant) binary. diff --git a/src/cli/commands/client.rs b/src/cli/commands/client.rs index 82fc7e1..b6a9f28 100644 --- a/src/cli/commands/client.rs +++ b/src/cli/commands/client.rs @@ -205,7 +205,7 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult { use_or_lookup_connection_id(&mut cache, connection, &mut client).await?; let timeout = match timeout { - Some(timeout) if timeout >= f32::EPSILON => Some(timeout), + Some(timeout) if timeout.as_secs_f64() >= f64::EPSILON => Some(timeout), _ => None, }; diff --git a/src/cli/commands/server.rs b/src/cli/commands/server.rs index 3bd719b..f385f6d 100644 --- a/src/cli/commands/server.rs +++ b/src/cli/commands/server.rs @@ -5,6 +5,7 @@ use distant_core::net::auth::Verifier; use distant_core::net::common::{Host, SecretKey32}; use distant_core::net::server::{Server, ServerConfig as NetServerConfig, ServerRef}; use distant_core::DistantSingleKeyCredentials; +use distant_local::{Config as LocalConfig, WatchConfig as LocalWatchConfig}; use log::*; use crate::options::ServerSubcommand; @@ -104,6 +105,7 @@ async fn async_run(cmd: ServerSubcommand, _is_forked: bool) -> CliResult { use_ipv6, shutdown, current_dir, + watch, daemon: _, key_from_stdin, output_to_local_pipe, @@ -140,8 +142,16 @@ async fn async_run(cmd: ServerSubcommand, _is_forked: bool) -> CliResult { "using an ephemeral port".to_string() } ); - let handler = distant_local::initialize_handler() - .context("Failed to create local distant api")?; + let handler = distant_local::new_handler(LocalConfig { + watch: LocalWatchConfig { + native: !watch.watch_polling, + poll_interval: watch.watch_poll_interval.map(Into::into), + compare_contents: watch.watch_compare_contents, + debounce_timeout: watch.watch_debounce_timeout.into_inner().into(), + debounce_tick_rate: watch.watch_debounce_tick_rate.map(Into::into), + }, + }) + .context("Failed to create local distant api")?; let server = Server::tcp() .config(NetServerConfig { shutdown: shutdown.into_inner(), diff --git a/src/cli/common/spawner.rs b/src/cli/common/spawner.rs index 70b8bb4..b2eefeb 100644 --- a/src/cli/common/spawner.rs +++ b/src/cli/common/spawner.rs @@ -181,7 +181,7 @@ impl Spawner { let mut process_id = None; let mut return_value = None; - for line in stdout.lines().filter_map(|l| l.ok()) { + for line in stdout.lines().map_while(Result::ok) { let line = line.trim(); if line.starts_with("ProcessId") { if let Some((_, id)) = line.split_once(':') { diff --git a/src/options.rs b/src/options.rs index e1367dd..a5eab23 100644 --- a/src/options.rs +++ b/src/options.rs @@ -2,7 +2,7 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use clap::builder::TypedValueParser as _; -use clap::{Parser, Subcommand, ValueEnum, ValueHint}; +use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint}; use clap_complete::Shell as ClapCompleteShell; use derive_more::IsVariant; use distant_core::net::common::{ConnectionId, Destination, Map, PortRange}; @@ -194,8 +194,13 @@ impl Options { port, shutdown, use_ipv6, + watch, .. } => { + // + // GENERAL SETTINGS + // + *current_dir = current_dir.take().or(config.server.listen.current_dir); if host.is_default() && config.server.listen.host.is_some() { *host = Value::Explicit(config.server.listen.host.unwrap()); @@ -209,6 +214,35 @@ impl Options { if !*use_ipv6 && config.server.listen.use_ipv6 { *use_ipv6 = true; } + + // + // WATCH-SPECIFIC SETTINGS + // + + if !watch.watch_polling && !config.server.watch.native { + watch.watch_polling = true; + } + + watch.watch_poll_interval = watch + .watch_poll_interval + .take() + .or(config.server.watch.poll_interval); + + if !watch.watch_compare_contents && config.server.watch.compare_contents { + watch.watch_compare_contents = true; + } + + if watch.watch_debounce_timeout.is_default() + && config.server.watch.debounce_timeout.is_some() + { + watch.watch_debounce_timeout = + Value::Explicit(config.server.watch.debounce_timeout.unwrap()); + } + + watch.watch_debounce_tick_rate = watch + .watch_debounce_tick_rate + .take() + .or(config.server.watch.debounce_tick_rate); } } } @@ -253,7 +287,7 @@ pub enum ClientSubcommand { /// Represents the maximum time (in seconds) to wait for a network request before timing out. #[clap(long)] - timeout: Option, + timeout: Option, /// Specify a connection being managed #[clap(long)] @@ -1103,6 +1137,9 @@ pub enum ServerSubcommand { #[clap(long)] daemon: bool, + #[clap(flatten)] + watch: ServerListenWatchOptions, + /// If specified, the server will not generate a key but instead listen on stdin for the next /// 32 bytes that it will use as the key instead. Receiving less than 32 bytes before stdin /// is closed is considered an error and any bytes after the first 32 are not used for the key @@ -1115,6 +1152,34 @@ pub enum ServerSubcommand { }, } +#[derive(Args, Debug, PartialEq)] +pub struct ServerListenWatchOptions { + /// If specified, will use the polling-based watcher for filesystem changes + #[clap(long)] + pub watch_polling: bool, + + /// If specified, represents the time (in seconds) between polls of files being watched, + /// only relevant when using the polling watcher implementation + #[clap(long)] + pub watch_poll_interval: Option, + + /// If true, will attempt to load a file and compare its contents to detect file changes, + /// only relevant when using the polling watcher implementation (VERY SLOW) + #[clap(long)] + pub watch_compare_contents: bool, + + /// Represents the maximum time (in seconds) to wait for filesystem changes before + /// reporting them, which is useful to avoid noisy changes as well as serves to consolidate + /// different events that represent the same action + #[clap(long, default_value_t = Value::Default(Seconds::try_from(0.5).unwrap()))] + pub watch_debounce_timeout: Value, + + /// Represents how often (in seconds) to check for new events before the debounce timeout + /// occurs. Defaults to 1/4 the debounce timeout if not set. + #[clap(long)] + pub watch_debounce_tick_rate: Option, +} + /// Represents the format to use for output from a command. #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] #[clap(rename_all = "snake_case")] @@ -1178,7 +1243,9 @@ mod tests { unix_socket: Some(PathBuf::from("config-unix-socket")), windows_pipe: Some(String::from("config-windows-pipe")), }, - api: ClientApiConfig { timeout: Some(5.0) }, + api: ClientApiConfig { + timeout: Some(Seconds::from(5u32)), + }, ..Default::default() }, ..Default::default() @@ -1199,7 +1266,7 @@ mod tests { unix_socket: Some(PathBuf::from("config-unix-socket")), windows_pipe: Some(String::from("config-windows-pipe")), }, - timeout: Some(5.0), + timeout: Some(Seconds::from(5u32)), }), } ); @@ -1220,7 +1287,7 @@ mod tests { unix_socket: Some(PathBuf::from("cli-unix-socket")), windows_pipe: Some(String::from("cli-windows-pipe")), }, - timeout: Some(99.0), + timeout: Some(Seconds::from(99u32)), }), }; @@ -1234,7 +1301,9 @@ mod tests { unix_socket: Some(PathBuf::from("config-unix-socket")), windows_pipe: Some(String::from("config-windows-pipe")), }, - api: ClientApiConfig { timeout: Some(5.0) }, + api: ClientApiConfig { + timeout: Some(Seconds::from(5u32)), + }, ..Default::default() }, ..Default::default() @@ -1255,7 +1324,7 @@ mod tests { unix_socket: Some(PathBuf::from("cli-unix-socket")), windows_pipe: Some(String::from("cli-windows-pipe")), }, - timeout: Some(99.0), + timeout: Some(Seconds::from(99u32)), }), } ); @@ -4077,6 +4146,13 @@ mod tests { use_ipv6: false, shutdown: Value::Default(Shutdown::After(Duration::from_secs(123))), current_dir: None, + watch: ServerListenWatchOptions { + watch_polling: false, + watch_poll_interval: None, + watch_compare_contents: false, + watch_debounce_timeout: Value::Default(Seconds::try_from(0.5).unwrap()), + watch_debounce_tick_rate: None, + }, daemon: false, key_from_stdin: false, output_to_local_pipe: None, @@ -4096,6 +4172,13 @@ mod tests { shutdown: Some(Shutdown::Lonely(Duration::from_secs(456))), current_dir: Some(PathBuf::from("config-dir")), }, + watch: ServerWatchConfig { + native: false, + poll_interval: Some(Seconds::from(100u32)), + compare_contents: true, + debounce_timeout: Some(Seconds::from(200u32)), + debounce_tick_rate: Some(Seconds::from(300u32)), + }, }, ..Default::default() }); @@ -4114,6 +4197,13 @@ mod tests { use_ipv6: true, shutdown: Value::Explicit(Shutdown::Lonely(Duration::from_secs(456))), current_dir: Some(PathBuf::from("config-dir")), + watch: ServerListenWatchOptions { + watch_polling: true, + watch_poll_interval: Some(Seconds::from(100u32)), + watch_compare_contents: true, + watch_debounce_timeout: Value::Explicit(Seconds::from(200u32)), + watch_debounce_tick_rate: Some(Seconds::from(300u32)), + }, daemon: false, key_from_stdin: false, output_to_local_pipe: None, @@ -4136,6 +4226,13 @@ mod tests { use_ipv6: true, shutdown: Value::Explicit(Shutdown::After(Duration::from_secs(123))), current_dir: Some(PathBuf::from("cli-dir")), + watch: ServerListenWatchOptions { + watch_polling: true, + watch_poll_interval: Some(Seconds::from(10u32)), + watch_compare_contents: true, + watch_debounce_timeout: Value::Explicit(Seconds::from(20u32)), + watch_debounce_tick_rate: Some(Seconds::from(30u32)), + }, daemon: false, key_from_stdin: false, output_to_local_pipe: None, @@ -4155,6 +4252,13 @@ mod tests { shutdown: Some(Shutdown::Lonely(Duration::from_secs(456))), current_dir: Some(PathBuf::from("config-dir")), }, + watch: ServerWatchConfig { + native: true, + poll_interval: Some(Seconds::from(100u32)), + compare_contents: false, + debounce_timeout: Some(Seconds::from(200u32)), + debounce_tick_rate: Some(Seconds::from(300u32)), + }, }, ..Default::default() }); @@ -4173,6 +4277,13 @@ mod tests { use_ipv6: true, shutdown: Value::Explicit(Shutdown::After(Duration::from_secs(123))), current_dir: Some(PathBuf::from("cli-dir")), + watch: ServerListenWatchOptions { + watch_polling: true, + watch_poll_interval: Some(Seconds::from(10u32)), + watch_compare_contents: true, + watch_debounce_timeout: Value::Explicit(Seconds::from(20u32)), + watch_debounce_tick_rate: Some(Seconds::from(30u32)), + }, daemon: false, key_from_stdin: false, output_to_local_pipe: None, diff --git a/src/options/common.rs b/src/options/common.rs index 2251fbf..6f1efb9 100644 --- a/src/options/common.rs +++ b/src/options/common.rs @@ -3,6 +3,7 @@ mod cmd; mod logging; mod network; mod search; +mod time; mod value; pub use address::*; @@ -10,4 +11,5 @@ pub use cmd::*; pub use logging::*; pub use network::*; pub use search::*; +pub use time::*; pub use value::*; diff --git a/src/options/common/time.rs b/src/options/common/time.rs new file mode 100644 index 0000000..6f1a8bf --- /dev/null +++ b/src/options/common/time.rs @@ -0,0 +1,284 @@ +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; +use std::time::Duration; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +/// Represents a time in seconds. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Seconds(Duration); + +impl FromStr for Seconds { + type Err = ParseSecondsError; + + fn from_str(s: &str) -> Result { + match f64::from_str(s) { + Ok(secs) => Ok(Self::try_from(secs)?), + Err(_) => Err(ParseSecondsError::NotANumber), + } + } +} + +impl fmt::Display for Seconds { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0.as_secs_f32()) + } +} + +impl Deref for Seconds { + type Target = Duration; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Seconds { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl TryFrom for Seconds { + type Error = std::num::TryFromIntError; + + fn try_from(secs: i8) -> Result { + Ok(Self(Duration::from_secs(u64::try_from(secs)?))) + } +} + +impl TryFrom for Seconds { + type Error = std::num::TryFromIntError; + + fn try_from(secs: i16) -> Result { + Ok(Self(Duration::from_secs(u64::try_from(secs)?))) + } +} + +impl TryFrom for Seconds { + type Error = std::num::TryFromIntError; + + fn try_from(secs: i32) -> Result { + Ok(Self(Duration::from_secs(u64::try_from(secs)?))) + } +} + +impl TryFrom for Seconds { + type Error = std::num::TryFromIntError; + + fn try_from(secs: i64) -> Result { + Ok(Self(Duration::from_secs(u64::try_from(secs)?))) + } +} + +impl From for Seconds { + fn from(secs: u8) -> Self { + Self(Duration::from_secs(u64::from(secs))) + } +} + +impl From for Seconds { + fn from(secs: u16) -> Self { + Self(Duration::from_secs(u64::from(secs))) + } +} + +impl From for Seconds { + fn from(secs: u32) -> Self { + Self(Duration::from_secs(u64::from(secs))) + } +} + +impl From for Seconds { + fn from(secs: u64) -> Self { + Self(Duration::from_secs(secs)) + } +} + +impl TryFrom for Seconds { + type Error = NegativeSeconds; + + fn try_from(secs: f32) -> Result { + if secs.is_sign_negative() { + Err(NegativeSeconds) + } else { + Ok(Self(Duration::from_secs_f32(secs))) + } + } +} + +impl TryFrom for Seconds { + type Error = NegativeSeconds; + + fn try_from(secs: f64) -> Result { + if secs.is_sign_negative() { + Err(NegativeSeconds) + } else { + Ok(Self(Duration::from_secs_f64(secs))) + } + } +} + +impl From for Seconds { + fn from(d: Duration) -> Self { + Self(d) + } +} + +impl From for Duration { + fn from(secs: Seconds) -> Self { + secs.0 + } +} + +pub use self::errors::{NegativeSeconds, ParseSecondsError}; + +mod errors { + use super::*; + + /// Represents errors that can occur when parsing seconds. + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] + pub enum ParseSecondsError { + NegativeSeconds, + NotANumber, + } + + impl std::error::Error for ParseSecondsError {} + + impl fmt::Display for ParseSecondsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::NegativeSeconds => write!(f, "seconds cannot be negative"), + Self::NotANumber => write!(f, "seconds must be a number"), + } + } + } + + impl From for ParseSecondsError { + fn from(_: NegativeSeconds) -> Self { + Self::NegativeSeconds + } + } + + /// Error type when provided seconds is negative. + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] + pub struct NegativeSeconds; + + impl std::error::Error for NegativeSeconds {} + + impl fmt::Display for NegativeSeconds { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "seconds cannot be negative") + } + } +} + +mod ser { + use super::*; + + impl Serialize for Seconds { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_f32(self.as_secs_f32()) + } + } + + impl<'de> Deserialize<'de> for Seconds { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_f32(SecondsVisitor) + } + } + + struct SecondsVisitor; + + impl<'de> de::Visitor<'de> for SecondsVisitor { + type Value = Seconds; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid amount of seconds") + } + + fn visit_i8(self, value: i8) -> Result + where + E: de::Error, + { + Seconds::try_from(value).map_err(de::Error::custom) + } + + fn visit_i16(self, value: i16) -> Result + where + E: de::Error, + { + Seconds::try_from(value).map_err(de::Error::custom) + } + + fn visit_i32(self, value: i32) -> Result + where + E: de::Error, + { + Seconds::try_from(value).map_err(de::Error::custom) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Seconds::try_from(value).map_err(de::Error::custom) + } + + fn visit_u8(self, value: u8) -> Result + where + E: de::Error, + { + Ok(Seconds::from(value)) + } + + fn visit_u16(self, value: u16) -> Result + where + E: de::Error, + { + Ok(Seconds::from(value)) + } + + fn visit_u32(self, value: u32) -> Result + where + E: de::Error, + { + Ok(Seconds::from(value)) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(Seconds::from(value)) + } + + fn visit_f32(self, value: f32) -> Result + where + E: de::Error, + { + Seconds::try_from(value).map_err(de::Error::custom) + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + Seconds::try_from(value).map_err(de::Error::custom) + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + s.parse().map_err(de::Error::custom) + } + } +} diff --git a/src/options/config.rs b/src/options/config.rs index e857792..1700a84 100644 --- a/src/options/config.rs +++ b/src/options/config.rs @@ -112,7 +112,9 @@ mod tests { config, Config { client: ClientConfig { - api: ClientApiConfig { timeout: Some(0.) }, + api: ClientApiConfig { + timeout: Some(Seconds::from(0u32)) + }, connect: ClientConnectConfig { options: Map::new() }, @@ -162,6 +164,13 @@ mod tests { log_level: Some(LogLevel::Info), log_file: None }, + watch: ServerWatchConfig { + native: true, + poll_interval: None, + compare_contents: false, + debounce_timeout: None, + debounce_tick_rate: None, + }, }, } ); @@ -213,6 +222,13 @@ port = "8080:8089" use_ipv6 = true shutdown = "after=123" current_dir = "server-current-dir" + +[server.watch] +native = false +poll_interval = 12.5 +compare_contents = true +debounce_timeout = 10.5 +debounce_tick_rate = 0.678 "#, ) .unwrap(); @@ -223,7 +239,7 @@ current_dir = "server-current-dir" Config { client: ClientConfig { api: ClientApiConfig { - timeout: Some(456.) + timeout: Some(Seconds::from(456u32)) }, connect: ClientConnectConfig { options: map!("key" -> "value", "key2" -> "value2"), @@ -277,6 +293,13 @@ current_dir = "server-current-dir" log_level: Some(LogLevel::Error), log_file: Some(PathBuf::from("server-log-file")), }, + watch: ServerWatchConfig { + native: false, + poll_interval: Some(Seconds::try_from(12.5).unwrap()), + compare_contents: true, + debounce_timeout: Some(Seconds::try_from(10.5).unwrap()), + debounce_tick_rate: Some(Seconds::try_from(0.678).unwrap()) + }, }, } ); diff --git a/src/options/config.toml b/src/options/config.toml index 2fb8d46..f906abd 100644 --- a/src/options/config.toml +++ b/src/options/config.toml @@ -169,3 +169,27 @@ shutdown = "never" # Changes the current working directory (cwd) to the specified directory. # current_dir = "path/to/dir" + +# Configuration related to filesystem watching done by the server +[server.watch] + +# If true, will attempt to use native filesystem watching (more efficient), +# otherwise will leverage polling of watched files and directories to detect changes +native = true + +# If specified, represents the time (in seconds) between polls of files being watched, +# only relevant when using the polling watcher implementation +#poll_interval = 30 + +# If true, will attempt to load a file and compare its contents to detect file changes, +# only relevant when using the polling watcher implementation (VERY SLOW) +compare_contents = false + +# Represents the maximum time (in seconds) to wait for filesystem changes before +# reporting them, which is useful to avoid noisy changes as well as serves to consolidate +# different events that represent the same action +# debounce_timeout = 0.5 + +# Represents how often (in seconds) to check for new events before the debounce timeout +# occurs. Defaults to 1/4 the debounce timeout if not set. +# debounce_tick_rate = 0.125 diff --git a/src/options/config/client/api.rs b/src/options/config/client/api.rs index 34c1566..bcb91c1 100644 --- a/src/options/config/client/api.rs +++ b/src/options/config/client/api.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; +use crate::options::common::Seconds; + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct ClientApiConfig { - pub timeout: Option, + pub timeout: Option, } diff --git a/src/options/config/server.rs b/src/options/config/server.rs index a2bc437..6571bbf 100644 --- a/src/options/config/server.rs +++ b/src/options/config/server.rs @@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize}; use super::common::LoggingSettings; mod listen; +mod watch; + pub use listen::*; +pub use watch::*; /// Represents configuration settings for the distant server #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -12,4 +15,5 @@ pub struct ServerConfig { pub logging: LoggingSettings, pub listen: ServerListenConfig, + pub watch: ServerWatchConfig, } diff --git a/src/options/config/server/watch.rs b/src/options/config/server/watch.rs new file mode 100644 index 0000000..7a659f4 --- /dev/null +++ b/src/options/config/server/watch.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +use crate::options::common::Seconds; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerWatchConfig { + pub native: bool, + pub poll_interval: Option, + pub compare_contents: bool, + pub debounce_timeout: Option, + pub debounce_tick_rate: Option, +} + +impl Default for ServerWatchConfig { + fn default() -> Self { + Self { + native: true, + poll_interval: None, + compare_contents: false, + debounce_timeout: None, + debounce_tick_rate: None, + } + } +}