Refactor to use debouncer for file watching and support configuration (#195)

pull/196/head
Chip Senkbeil 12 months ago committed by GitHub
parent 9da7679081
commit 4eaae55d53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -85,7 +85,7 @@ jobs:
- { rust: stable, os: windows-latest, target: x86_64-pc-windows-msvc } - { rust: stable, os: windows-latest, target: x86_64-pc-windows-msvc }
- { rust: stable, os: macos-latest } - { rust: stable, os: macos-latest }
- { rust: stable, os: ubuntu-latest } - { rust: stable, os: ubuntu-latest }
- { rust: 1.64.0, os: ubuntu-latest } - { rust: 1.68.0, os: ubuntu-latest }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install Rust ${{ matrix.rust }} - name: Install Rust ${{ matrix.rust }}

@ -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`. - `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). These are used to indicate what kind of file watching to support (for MacOS).
The default is `macos-fsevent`. The default is `macos-fsevent`.
- `[server.watch]` configuration is now available with the following
settings:
- `native = <bool>` to specify whether to use native watching or polling
(default true)
- `poll_interval = <secs>` to specify seconds to wait between polling
attempts (only for polling watcher)
- `compare_contents = <bool>` to specify how polling watcher will evaluate a
file change (default false)
- `debounce_timeout = <secs>` to specify how long to wait before sending a
change notification (will aggregate and merge changes)
- `debounce_tick_rate = <secs>` to specify how long to wait between event
aggregation loops
### Changed
- Bump minimum Rust version to 1.68.0
### Removed ### Removed

23
Cargo.lock generated

@ -902,6 +902,7 @@ dependencies = [
"indoc", "indoc",
"log", "log",
"notify", "notify",
"notify-debouncer-full",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"portable-pty 0.8.1", "portable-pty 0.8.1",
@ -1142,6 +1143,15 @@ dependencies = [
"subtle", "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]] [[package]]
name = "file-mode" name = "file-mode"
version = "0.1.2" version = "0.1.2"
@ -1965,6 +1975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2" checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"crossbeam-channel",
"filetime", "filetime",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
@ -1975,6 +1986,18 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.1" version = "0.4.1"

@ -1,6 +1,6 @@
# distant - remotely edit files and run programs # 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_img]: https://img.shields.io/crates/v/distant.svg
[distant_crates_lnk]: https://crates.io/crates/distant [distant_crates_lnk]: https://crates.io/crates/distant
@ -8,8 +8,8 @@
[distant_doc_lnk]: https://docs.rs/distant [distant_doc_lnk]: https://docs.rs/distant
[distant_ci_img]: https://github.com/chipsenkbeil/distant/actions/workflows/ci.yml/badge.svg [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_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_img]: https://img.shields.io/badge/distant-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [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!** 🚧 🚧 **(Alpha stage software) This program is in rapid development and may break or change frequently!** 🚧

@ -1,13 +1,13 @@
# distant auth # 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_img]: https://img.shields.io/crates/v/distant-auth.svg
[distant_crates_lnk]: https://crates.io/crates/distant-auth [distant_crates_lnk]: https://crates.io/crates/distant-auth
[distant_doc_img]: https://docs.rs/distant-auth/badge.svg [distant_doc_img]: https://docs.rs/distant-auth/badge.svg
[distant_doc_lnk]: https://docs.rs/distant-auth [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_img]: https://img.shields.io/badge/distant_auth-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
## Details ## Details

@ -1,13 +1,13 @@
# distant core # 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_img]: https://img.shields.io/crates/v/distant-core.svg
[distant_crates_lnk]: https://crates.io/crates/distant-core [distant_crates_lnk]: https://crates.io/crates/distant-core
[distant_doc_img]: https://docs.rs/distant-core/badge.svg [distant_doc_img]: https://docs.rs/distant-core/badge.svg
[distant_doc_lnk]: https://docs.rs/distant-core [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_img]: https://img.shields.io/badge/distant_core-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
## Details ## Details

@ -25,7 +25,8 @@ distant-core = { version = "=0.20.0-alpha.7", path = "../distant-core" }
grep = "0.2.12" grep = "0.2.12"
ignore = "0.4.20" ignore = "0.4.20"
log = "0.4.18" 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" num_cpus = "1.15.0"
portable-pty = "0.8.1" portable-pty = "0.8.1"
rand = { version = "0.8.5", features = ["getrandom"] } rand = { version = "0.8.5", features = ["getrandom"] }

@ -1,13 +1,13 @@
# distant local # 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_img]: https://img.shields.io/crates/v/distant-local.svg
[distant_crates_lnk]: https://crates.io/crates/distant-local [distant_crates_lnk]: https://crates.io/crates/distant-local
[distant_doc_img]: https://docs.rs/distant-local/badge.svg [distant_doc_img]: https://docs.rs/distant-local/badge.svg
[distant_doc_lnk]: https://docs.rs/distant-local [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_img]: https://img.shields.io/badge/distant_local-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
## Details ## Details
@ -27,8 +27,10 @@ distant-local = "0.20"
## Examples ## Examples
```rust,no_run ```rust,no_run
use distant_local::{Config, new_handler};
// Create a server API handler to be used with the server // 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 ## License

@ -14,8 +14,9 @@ use log::*;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use walkdir::WalkDir; use walkdir::WalkDir;
mod process; use crate::config::Config;
mod process;
mod state; mod state;
use state::*; use state::*;
@ -23,21 +24,21 @@ use state::*;
/// where the server using this api is running. In other words, this is a direct /// 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 /// impementation of the API instead of a proxy to another machine as seen with
/// implementations on top of SSH and other protocol. /// implementations on top of SSH and other protocol.
pub struct LocalDistantApi { pub struct Api {
state: GlobalState, state: GlobalState,
} }
impl LocalDistantApi { impl Api {
/// Initialize the api instance /// Initialize the api instance
pub fn initialize() -> io::Result<Self> { pub fn initialize(config: Config) -> io::Result<Self> {
Ok(Self { Ok(Self {
state: GlobalState::initialize()?, state: GlobalState::initialize(config)?,
}) })
} }
} }
#[async_trait] #[async_trait]
impl DistantApi for LocalDistantApi { impl DistantApi for Api {
type LocalData = (); type LocalData = ();
async fn read_file( 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 // Traverse, but don't include root directory in entries (hence min depth 1), unless indicated
// to do so (min depth 0) // to do so (min depth 0)
let dir = WalkDir::new(root_path.as_path()) 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(); .sort_by_file_name();
// If depth > 0, will recursively traverse to specified max depth, otherwise // If depth > 0, will recursively traverse to specified max depth, otherwise
@ -709,6 +710,7 @@ mod tests {
use tokio::sync::mpsc; use tokio::sync::mpsc;
use super::*; use super::*;
use crate::config::WatchConfig;
static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> = static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> =
Lazy::new(|| assert_fs::TempDir::new().unwrap()); Lazy::new(|| assert_fs::TempDir::new().unwrap());
@ -769,8 +771,16 @@ mod tests {
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> = static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin")); Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
async fn setup(buffer: usize) -> (LocalDistantApi, DistantCtx<()>, mpsc::Receiver<Response>) { const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
let api = LocalDistantApi::initialize().unwrap();
async fn setup(buffer: usize) -> (Api, DistantCtx<()>, mpsc::Receiver<Response>) {
let api = Api::initialize(Config {
watch: WatchConfig {
debounce_timeout: DEBOUNCE_TIMEOUT,
..Default::default()
},
})
.unwrap();
let (reply, rx) = make_reply(buffer); let (reply, rx) = make_reply(buffer);
let connection_id = rand::random(); let connection_id = rand::random();
@ -1613,7 +1623,7 @@ mod tests {
api.watch( api.watch(
ctx, ctx,
file.path().to_path_buf(), temp.path().to_path_buf(),
/* recursive */ true, /* recursive */ true,
/* only */ Default::default(), /* only */ Default::default(),
/* except */ Default::default(), /* except */ Default::default(),
@ -1630,7 +1640,7 @@ mod tests {
// Sleep a bit to give time to get all changes happening // 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? // 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 // Collect all responses, as we may get multiple for interactions within a directory
let mut responses = Vec::new(); let mut responses = Vec::new();

@ -1,5 +1,7 @@
use std::io; use std::io;
use crate::config::Config;
mod process; mod process;
pub use process::*; pub use process::*;
@ -22,11 +24,13 @@ pub struct GlobalState {
} }
impl GlobalState { impl GlobalState {
pub fn initialize() -> io::Result<Self> { pub fn initialize(config: Config) -> io::Result<Self> {
Ok(Self { Ok(Self {
process: ProcessState::new(), process: ProcessState::new(),
search: SearchState::new(), search: SearchState::new(),
watcher: WatcherState::initialize()?, watcher: WatcherBuilder::new()
.with_config(config.watch)
.initialize()?,
}) })
} }
} }

@ -812,10 +812,10 @@ mod tests {
use super::*; use super::*;
fn make_path(path: &str) -> PathBuf { 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 // 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) PathBuf::from(path)
} }

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::io; use std::io;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration;
use distant_core::net::common::ConnectionId; use distant_core::net::common::ConnectionId;
use distant_core::protocol::ChangeKind; use distant_core::protocol::ChangeKind;
@ -9,49 +10,49 @@ use log::*;
use notify::event::{AccessKind, AccessMode, ModifyKind}; use notify::event::{AccessKind, AccessMode, ModifyKind};
use notify::{ use notify::{
Config as WatcherConfig, Error as WatcherError, ErrorKind as WatcherErrorKind, 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::error::TrySendError;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use crate::config::WatchConfig;
use crate::constants::SERVER_WATCHER_CAPACITY; use crate::constants::SERVER_WATCHER_CAPACITY;
mod path; mod path;
pub use path::*; pub use path::*;
/// Holds information related to watched paths on the server /// Builder for a watcher.
pub struct WatcherState { #[derive(Default)]
channel: WatcherChannel, pub struct WatcherBuilder {
task: JoinHandle<()>, config: WatchConfig,
} }
impl Drop for WatcherState { impl WatcherBuilder {
/// Aborts the task that handles watcher path operations and management /// Creates a new builder configured to use the native watcher using default configuration.
fn drop(&mut self) { pub fn new() -> Self {
self.abort(); 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 /// Will create a watcher and initialize watched paths to be empty
pub fn initialize() -> io::Result<Self> { pub fn initialize(self) -> io::Result<WatcherState> {
// NOTE: Cannot be something small like 1 as this seems to cause a deadlock sometimes // NOTE: Cannot be something small like 1 as this seems to cause a deadlock sometimes
// with a large volume of watch requests // with a large volume of watch requests
let (tx, rx) = mpsc::channel(SERVER_WATCHER_CAPACITY); let (tx, rx) = mpsc::channel(SERVER_WATCHER_CAPACITY);
macro_rules! spawn_watcher { let watcher_config = WatcherConfig::default()
($watcher:ident) => {{ .with_compare_contents(self.config.compare_contents)
Self { .with_poll_interval(self.config.poll_interval.unwrap_or(Duration::from_secs(30)));
channel: WatcherChannel { tx },
task: tokio::spawn(watcher_task($watcher, rx)),
}
}};
}
macro_rules! event_handler { macro_rules! process_event {
($tx:ident) => { ($tx:ident, $evt:expr) => {
move |res| match $tx.try_send(match res { match $tx.try_send(match $evt {
Ok(x) => InnerWatcherMsg::Event { ev: x }, Ok(x) => InnerWatcherMsg::Event { ev: x },
Err(x) => InnerWatcherMsg::Error { err: 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 tx = tx.clone();
let result = { if self.config.native {
let tx = tx.clone(); let result = {
notify::recommended_watcher(event_handler!(tx)) let tx = tx.clone();
}; new_debouncer!(RecommendedWatcher, tx)
};
match result {
Ok(watcher) => Ok(spawn_watcher!(watcher)), match result {
Err(x) => match x.kind { Ok(debouncer) => Ok(spawn_task!(debouncer)),
// notify-rs has a bug on Mac M1 with Docker and Linux, so we detect that error Err(x) => {
// and fall back to the poll watcher if this occurs match x.kind {
// // notify-rs has a bug on Mac M1 with Docker and Linux, so we detect that error
// https://github.com/notify-rs/notify/issues/423 // and fall back to the poll watcher if this occurs
WatcherErrorKind::Io(x) if x.raw_os_error() == Some(38) => { //
warn!("Recommended watcher is unsupported! Falling back to polling watcher!"); // https://github.com/notify-rs/notify/issues/423
let watcher = PollWatcher::new(event_handler!(tx), WatcherConfig::default()) WatcherErrorKind::Io(x) if x.raw_os_error() == Some(38) => {
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; warn!("Recommended watcher is unsupported! Falling back to polling watcher!");
Ok(spawn_watcher!(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 /// Aborts the watcher task
pub fn abort(&self) { pub fn abort(&self) {
self.task.abort(); self.task.abort();
@ -169,7 +223,12 @@ enum InnerWatcherMsg {
}, },
} }
async fn watcher_task(mut watcher: impl Watcher, mut rx: mpsc::Receiver<InnerWatcherMsg>) { async fn watcher_task<W>(
mut debouncer: Debouncer<W, FileIdMap>,
mut rx: mpsc::Receiver<InnerWatcherMsg>,
) where
W: Watcher,
{
// TODO: Optimize this in some way to be more performant than // TODO: Optimize this in some way to be more performant than
// checking every path whenever an event comes in // checking every path whenever an event comes in
let mut registered_paths: Vec<RegisteredPath> = Vec::new(); let mut registered_paths: Vec<RegisteredPath> = Vec::new();
@ -193,7 +252,8 @@ async fn watcher_task(mut watcher: impl Watcher, mut rx: mpsc::Receiver<InnerWat
// Send an okay because we always succeed in this case // Send an okay because we always succeed in this case
let _ = cb.send(Ok(())); let _ = cb.send(Ok(()));
} else { } else {
let res = watcher let res = debouncer
.watcher()
.watch( .watch(
registered_path.path(), registered_path.path(),
if registered_path.is_recursive() { if registered_path.is_recursive() {
@ -233,7 +293,8 @@ async fn watcher_task(mut watcher: impl Watcher, mut rx: mpsc::Receiver<InnerWat
// 3. Otherwise, we return okay because we succeeded // 3. Otherwise, we return okay because we succeeded
if *cnt <= removed_cnt { if *cnt <= removed_cnt {
let _ = cb.send( let _ = cb.send(
watcher debouncer
.watcher()
.unwatch(&path) .unwatch(&path)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x)), .map_err(|x| io::Error::new(io::ErrorKind::Other, x)),
); );

@ -0,0 +1,28 @@
use std::time::Duration;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Config {
pub watch: WatchConfig,
}
/// Configuration specifically for watching files and directories.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WatchConfig {
pub native: bool,
pub poll_interval: Option<Duration>,
pub compare_contents: bool,
pub debounce_timeout: Duration,
pub debounce_tick_rate: Option<Duration>,
}
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,
}
}
}

@ -5,17 +5,16 @@
pub struct ReadmeDoctests; pub struct ReadmeDoctests;
mod api; mod api;
mod config;
mod constants; mod constants;
pub use api::LocalDistantApi; pub use api::Api;
pub use config::*;
use distant_core::{DistantApi, DistantApiServerHandler}; use distant_core::{DistantApi, DistantApiServerHandler};
/// Implementation of [`DistantApiServerHandler`] using [`LocalDistantApi`]. /// Implementation of [`DistantApiServerHandler`] using [`Api`].
pub type LocalDistantApiServerHandler = pub type Handler = DistantApiServerHandler<Api, <Api as DistantApi>::LocalData>;
DistantApiServerHandler<LocalDistantApi, <LocalDistantApi as DistantApi>::LocalData>;
/// Initializes a new [`LocalDistantApiServerHandler`]. /// Initializes a new [`Handler`].
pub fn initialize_handler() -> std::io::Result<LocalDistantApiServerHandler> { pub fn new_handler(config: Config) -> std::io::Result<Handler> {
Ok(LocalDistantApiServerHandler::new( Ok(Handler::new(Api::initialize(config)?))
LocalDistantApi::initialize()?,
))
} }

@ -6,7 +6,7 @@ use distant_core::net::client::{Client, TcpConnector};
use distant_core::net::common::PortRange; use distant_core::net::common::PortRange;
use distant_core::net::server::Server; use distant_core::net::server::Server;
use distant_core::{DistantApiServerHandler, DistantClient}; use distant_core::{DistantApiServerHandler, DistantClient};
use distant_local::LocalDistantApi; use distant_local::Api;
use rstest::*; use rstest::*;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -22,7 +22,7 @@ impl DistantClientCtx {
let (started_tx, mut started_rx) = mpsc::channel::<u16>(1); let (started_tx, mut started_rx) = mpsc::channel::<u16>(1);
tokio::spawn(async move { 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: PortRange = "0".parse().unwrap();
let port = { let port = {
let handler = DistantApiServerHandler::new(api); let handler = DistantApiServerHandler::new(api);

@ -1,13 +1,13 @@
# distant net # 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_img]: https://img.shields.io/crates/v/distant-net.svg
[distant_crates_lnk]: https://crates.io/crates/distant-net [distant_crates_lnk]: https://crates.io/crates/distant-net
[distant_doc_img]: https://docs.rs/distant-net/badge.svg [distant_doc_img]: https://docs.rs/distant-net/badge.svg
[distant_doc_lnk]: https://docs.rs/distant-net [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_img]: https://img.shields.io/badge/distant_net-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
## Details ## Details

@ -1,13 +1,13 @@
# distant protocol # 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_img]: https://img.shields.io/crates/v/distant-protocol.svg
[distant_crates_lnk]: https://crates.io/crates/distant-protocol [distant_crates_lnk]: https://crates.io/crates/distant-protocol
[distant_doc_img]: https://docs.rs/distant-protocol/badge.svg [distant_doc_img]: https://docs.rs/distant-protocol/badge.svg
[distant_doc_lnk]: https://docs.rs/distant-protocol [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_img]: https://img.shields.io/badge/distant_protocol-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
## Details ## Details

@ -1,13 +1,13 @@
# distant ssh2 # 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_img]: https://img.shields.io/crates/v/distant-ssh2.svg
[distant_crates_lnk]: https://crates.io/crates/distant-ssh2 [distant_crates_lnk]: https://crates.io/crates/distant-ssh2
[distant_doc_img]: https://docs.rs/distant-ssh2/badge.svg [distant_doc_img]: https://docs.rs/distant-ssh2/badge.svg
[distant_doc_lnk]: https://docs.rs/distant-ssh2 [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_img]: https://img.shields.io/badge/distant_ssh2-rustc_1.68+-lightgray.svg
[distant_rustc_lnk]: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html [distant_rustc_lnk]: https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
Library provides native ssh integration into the Library provides native ssh integration into the
[`distant`](https://github.com/chipsenkbeil/distant) binary. [`distant`](https://github.com/chipsenkbeil/distant) binary.

@ -205,7 +205,7 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?; use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
let timeout = match timeout { let timeout = match timeout {
Some(timeout) if timeout >= f32::EPSILON => Some(timeout), Some(timeout) if timeout.as_secs_f64() >= f64::EPSILON => Some(timeout),
_ => None, _ => None,
}; };

@ -5,6 +5,7 @@ use distant_core::net::auth::Verifier;
use distant_core::net::common::{Host, SecretKey32}; use distant_core::net::common::{Host, SecretKey32};
use distant_core::net::server::{Server, ServerConfig as NetServerConfig, ServerRef}; use distant_core::net::server::{Server, ServerConfig as NetServerConfig, ServerRef};
use distant_core::DistantSingleKeyCredentials; use distant_core::DistantSingleKeyCredentials;
use distant_local::{Config as LocalConfig, WatchConfig as LocalWatchConfig};
use log::*; use log::*;
use crate::options::ServerSubcommand; use crate::options::ServerSubcommand;
@ -104,6 +105,7 @@ async fn async_run(cmd: ServerSubcommand, _is_forked: bool) -> CliResult {
use_ipv6, use_ipv6,
shutdown, shutdown,
current_dir, current_dir,
watch,
daemon: _, daemon: _,
key_from_stdin, key_from_stdin,
output_to_local_pipe, output_to_local_pipe,
@ -140,8 +142,16 @@ async fn async_run(cmd: ServerSubcommand, _is_forked: bool) -> CliResult {
"using an ephemeral port".to_string() "using an ephemeral port".to_string()
} }
); );
let handler = distant_local::initialize_handler() let handler = distant_local::new_handler(LocalConfig {
.context("Failed to create local distant api")?; 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() let server = Server::tcp()
.config(NetServerConfig { .config(NetServerConfig {
shutdown: shutdown.into_inner(), shutdown: shutdown.into_inner(),

@ -181,7 +181,7 @@ impl Spawner {
let mut process_id = None; let mut process_id = None;
let mut return_value = 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(); let line = line.trim();
if line.starts_with("ProcessId") { if line.starts_with("ProcessId") {
if let Some((_, id)) = line.split_once(':') { if let Some((_, id)) = line.split_once(':') {

@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::builder::TypedValueParser as _; 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 clap_complete::Shell as ClapCompleteShell;
use derive_more::IsVariant; use derive_more::IsVariant;
use distant_core::net::common::{ConnectionId, Destination, Map, PortRange}; use distant_core::net::common::{ConnectionId, Destination, Map, PortRange};
@ -194,8 +194,13 @@ impl Options {
port, port,
shutdown, shutdown,
use_ipv6, use_ipv6,
watch,
.. ..
} => { } => {
//
// GENERAL SETTINGS
//
*current_dir = current_dir.take().or(config.server.listen.current_dir); *current_dir = current_dir.take().or(config.server.listen.current_dir);
if host.is_default() && config.server.listen.host.is_some() { if host.is_default() && config.server.listen.host.is_some() {
*host = Value::Explicit(config.server.listen.host.unwrap()); *host = Value::Explicit(config.server.listen.host.unwrap());
@ -209,6 +214,35 @@ impl Options {
if !*use_ipv6 && config.server.listen.use_ipv6 { if !*use_ipv6 && config.server.listen.use_ipv6 {
*use_ipv6 = true; *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. /// Represents the maximum time (in seconds) to wait for a network request before timing out.
#[clap(long)] #[clap(long)]
timeout: Option<f32>, timeout: Option<Seconds>,
/// Specify a connection being managed /// Specify a connection being managed
#[clap(long)] #[clap(long)]
@ -1103,6 +1137,9 @@ pub enum ServerSubcommand {
#[clap(long)] #[clap(long)]
daemon: bool, daemon: bool,
#[clap(flatten)]
watch: ServerListenWatchOptions,
/// If specified, the server will not generate a key but instead listen on stdin for the next /// 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 /// 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 /// 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<Seconds>,
/// 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<Seconds>,
/// 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<Seconds>,
}
/// Represents the format to use for output from a command. /// Represents the format to use for output from a command.
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "snake_case")] #[clap(rename_all = "snake_case")]
@ -1178,7 +1243,9 @@ mod tests {
unix_socket: Some(PathBuf::from("config-unix-socket")), unix_socket: Some(PathBuf::from("config-unix-socket")),
windows_pipe: Some(String::from("config-windows-pipe")), 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()
}, },
..Default::default() ..Default::default()
@ -1199,7 +1266,7 @@ mod tests {
unix_socket: Some(PathBuf::from("config-unix-socket")), unix_socket: Some(PathBuf::from("config-unix-socket")),
windows_pipe: Some(String::from("config-windows-pipe")), 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")), unix_socket: Some(PathBuf::from("cli-unix-socket")),
windows_pipe: Some(String::from("cli-windows-pipe")), 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")), unix_socket: Some(PathBuf::from("config-unix-socket")),
windows_pipe: Some(String::from("config-windows-pipe")), 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()
}, },
..Default::default() ..Default::default()
@ -1255,7 +1324,7 @@ mod tests {
unix_socket: Some(PathBuf::from("cli-unix-socket")), unix_socket: Some(PathBuf::from("cli-unix-socket")),
windows_pipe: Some(String::from("cli-windows-pipe")), 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, use_ipv6: false,
shutdown: Value::Default(Shutdown::After(Duration::from_secs(123))), shutdown: Value::Default(Shutdown::After(Duration::from_secs(123))),
current_dir: None, 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, daemon: false,
key_from_stdin: false, key_from_stdin: false,
output_to_local_pipe: None, output_to_local_pipe: None,
@ -4096,6 +4172,13 @@ mod tests {
shutdown: Some(Shutdown::Lonely(Duration::from_secs(456))), shutdown: Some(Shutdown::Lonely(Duration::from_secs(456))),
current_dir: Some(PathBuf::from("config-dir")), 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() ..Default::default()
}); });
@ -4114,6 +4197,13 @@ mod tests {
use_ipv6: true, use_ipv6: true,
shutdown: Value::Explicit(Shutdown::Lonely(Duration::from_secs(456))), shutdown: Value::Explicit(Shutdown::Lonely(Duration::from_secs(456))),
current_dir: Some(PathBuf::from("config-dir")), 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, daemon: false,
key_from_stdin: false, key_from_stdin: false,
output_to_local_pipe: None, output_to_local_pipe: None,
@ -4136,6 +4226,13 @@ mod tests {
use_ipv6: true, use_ipv6: true,
shutdown: Value::Explicit(Shutdown::After(Duration::from_secs(123))), shutdown: Value::Explicit(Shutdown::After(Duration::from_secs(123))),
current_dir: Some(PathBuf::from("cli-dir")), 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, daemon: false,
key_from_stdin: false, key_from_stdin: false,
output_to_local_pipe: None, output_to_local_pipe: None,
@ -4155,6 +4252,13 @@ mod tests {
shutdown: Some(Shutdown::Lonely(Duration::from_secs(456))), shutdown: Some(Shutdown::Lonely(Duration::from_secs(456))),
current_dir: Some(PathBuf::from("config-dir")), 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() ..Default::default()
}); });
@ -4173,6 +4277,13 @@ mod tests {
use_ipv6: true, use_ipv6: true,
shutdown: Value::Explicit(Shutdown::After(Duration::from_secs(123))), shutdown: Value::Explicit(Shutdown::After(Duration::from_secs(123))),
current_dir: Some(PathBuf::from("cli-dir")), 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, daemon: false,
key_from_stdin: false, key_from_stdin: false,
output_to_local_pipe: None, output_to_local_pipe: None,

@ -3,6 +3,7 @@ mod cmd;
mod logging; mod logging;
mod network; mod network;
mod search; mod search;
mod time;
mod value; mod value;
pub use address::*; pub use address::*;
@ -10,4 +11,5 @@ pub use cmd::*;
pub use logging::*; pub use logging::*;
pub use network::*; pub use network::*;
pub use search::*; pub use search::*;
pub use time::*;
pub use value::*; pub use value::*;

@ -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<Self, Self::Err> {
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<i8> for Seconds {
type Error = std::num::TryFromIntError;
fn try_from(secs: i8) -> Result<Self, Self::Error> {
Ok(Self(Duration::from_secs(u64::try_from(secs)?)))
}
}
impl TryFrom<i16> for Seconds {
type Error = std::num::TryFromIntError;
fn try_from(secs: i16) -> Result<Self, Self::Error> {
Ok(Self(Duration::from_secs(u64::try_from(secs)?)))
}
}
impl TryFrom<i32> for Seconds {
type Error = std::num::TryFromIntError;
fn try_from(secs: i32) -> Result<Self, Self::Error> {
Ok(Self(Duration::from_secs(u64::try_from(secs)?)))
}
}
impl TryFrom<i64> for Seconds {
type Error = std::num::TryFromIntError;
fn try_from(secs: i64) -> Result<Self, Self::Error> {
Ok(Self(Duration::from_secs(u64::try_from(secs)?)))
}
}
impl From<u8> for Seconds {
fn from(secs: u8) -> Self {
Self(Duration::from_secs(u64::from(secs)))
}
}
impl From<u16> for Seconds {
fn from(secs: u16) -> Self {
Self(Duration::from_secs(u64::from(secs)))
}
}
impl From<u32> for Seconds {
fn from(secs: u32) -> Self {
Self(Duration::from_secs(u64::from(secs)))
}
}
impl From<u64> for Seconds {
fn from(secs: u64) -> Self {
Self(Duration::from_secs(secs))
}
}
impl TryFrom<f32> for Seconds {
type Error = NegativeSeconds;
fn try_from(secs: f32) -> Result<Self, Self::Error> {
if secs.is_sign_negative() {
Err(NegativeSeconds)
} else {
Ok(Self(Duration::from_secs_f32(secs)))
}
}
}
impl TryFrom<f64> for Seconds {
type Error = NegativeSeconds;
fn try_from(secs: f64) -> Result<Self, Self::Error> {
if secs.is_sign_negative() {
Err(NegativeSeconds)
} else {
Ok(Self(Duration::from_secs_f64(secs)))
}
}
}
impl From<Duration> for Seconds {
fn from(d: Duration) -> Self {
Self(d)
}
}
impl From<Seconds> 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<NegativeSeconds> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_f32(self.as_secs_f32())
}
}
impl<'de> Deserialize<'de> for Seconds {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: i8) -> Result<Self::Value, E>
where
E: de::Error,
{
Seconds::try_from(value).map_err(de::Error::custom)
}
fn visit_i16<E>(self, value: i16) -> Result<Self::Value, E>
where
E: de::Error,
{
Seconds::try_from(value).map_err(de::Error::custom)
}
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
Seconds::try_from(value).map_err(de::Error::custom)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Seconds::try_from(value).map_err(de::Error::custom)
}
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Seconds::from(value))
}
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Seconds::from(value))
}
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Seconds::from(value))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Seconds::from(value))
}
fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
where
E: de::Error,
{
Seconds::try_from(value).map_err(de::Error::custom)
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
Seconds::try_from(value).map_err(de::Error::custom)
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
s.parse().map_err(de::Error::custom)
}
}
}

@ -112,7 +112,9 @@ mod tests {
config, config,
Config { Config {
client: ClientConfig { client: ClientConfig {
api: ClientApiConfig { timeout: Some(0.) }, api: ClientApiConfig {
timeout: Some(Seconds::from(0u32))
},
connect: ClientConnectConfig { connect: ClientConnectConfig {
options: Map::new() options: Map::new()
}, },
@ -162,6 +164,13 @@ mod tests {
log_level: Some(LogLevel::Info), log_level: Some(LogLevel::Info),
log_file: None 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 use_ipv6 = true
shutdown = "after=123" shutdown = "after=123"
current_dir = "server-current-dir" 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(); .unwrap();
@ -223,7 +239,7 @@ current_dir = "server-current-dir"
Config { Config {
client: ClientConfig { client: ClientConfig {
api: ClientApiConfig { api: ClientApiConfig {
timeout: Some(456.) timeout: Some(Seconds::from(456u32))
}, },
connect: ClientConnectConfig { connect: ClientConnectConfig {
options: map!("key" -> "value", "key2" -> "value2"), options: map!("key" -> "value", "key2" -> "value2"),
@ -277,6 +293,13 @@ current_dir = "server-current-dir"
log_level: Some(LogLevel::Error), log_level: Some(LogLevel::Error),
log_file: Some(PathBuf::from("server-log-file")), 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())
},
}, },
} }
); );

@ -169,3 +169,27 @@ shutdown = "never"
# Changes the current working directory (cwd) to the specified directory. # Changes the current working directory (cwd) to the specified directory.
# current_dir = "path/to/dir" # 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

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::options::common::Seconds;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ClientApiConfig { pub struct ClientApiConfig {
pub timeout: Option<f32>, pub timeout: Option<Seconds>,
} }

@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize};
use super::common::LoggingSettings; use super::common::LoggingSettings;
mod listen; mod listen;
mod watch;
pub use listen::*; pub use listen::*;
pub use watch::*;
/// Represents configuration settings for the distant server /// Represents configuration settings for the distant server
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
@ -12,4 +15,5 @@ pub struct ServerConfig {
pub logging: LoggingSettings, pub logging: LoggingSettings,
pub listen: ServerListenConfig, pub listen: ServerListenConfig,
pub watch: ServerWatchConfig,
} }

@ -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<Seconds>,
pub compare_contents: bool,
pub debounce_timeout: Option<Seconds>,
pub debounce_tick_rate: Option<Seconds>,
}
impl Default for ServerWatchConfig {
fn default() -> Self {
Self {
native: true,
poll_interval: None,
compare_contents: false,
debounce_timeout: None,
debounce_tick_rate: None,
}
}
}
Loading…
Cancel
Save