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

pull/196/head
Chip Senkbeil 11 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: 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 }}

@ -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 = <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

23
Cargo.lock generated

@ -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"

@ -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!** 🚧

@ -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

@ -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

@ -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"] }

@ -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

@ -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<Self> {
pub fn initialize(config: Config) -> io::Result<Self> {
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<assert_fs::TempDir> =
Lazy::new(|| assert_fs::TempDir::new().unwrap());
@ -769,8 +771,16 @@ mod tests {
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
async fn setup(buffer: usize) -> (LocalDistantApi, DistantCtx<()>, mpsc::Receiver<Response>) {
let api = LocalDistantApi::initialize().unwrap();
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
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 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();

@ -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<Self> {
pub fn initialize(config: Config) -> io::Result<Self> {
Ok(Self {
process: ProcessState::new(),
search: SearchState::new(),
watcher: WatcherState::initialize()?,
watcher: WatcherBuilder::new()
.with_config(config.watch)
.initialize()?,
})
}
}

@ -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)
}

@ -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<Self> {
pub fn initialize(self) -> io::Result<WatcherState> {
// 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<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
// checking every path whenever an event comes in
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
let _ = cb.send(Ok(()));
} else {
let res = watcher
let res = debouncer
.watcher()
.watch(
registered_path.path(),
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
if *cnt <= removed_cnt {
let _ = cb.send(
watcher
debouncer
.watcher()
.unwatch(&path)
.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;
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<LocalDistantApi, <LocalDistantApi as DistantApi>::LocalData>;
/// Implementation of [`DistantApiServerHandler`] using [`Api`].
pub type Handler = DistantApiServerHandler<Api, <Api as DistantApi>::LocalData>;
/// Initializes a new [`LocalDistantApiServerHandler`].
pub fn initialize_handler() -> std::io::Result<LocalDistantApiServerHandler> {
Ok(LocalDistantApiServerHandler::new(
LocalDistantApi::initialize()?,
))
/// Initializes a new [`Handler`].
pub fn new_handler(config: Config) -> std::io::Result<Handler> {
Ok(Handler::new(Api::initialize(config)?))
}

@ -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::<u16>(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);

@ -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

@ -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

@ -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.

@ -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,
};

@ -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(),

@ -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(':') {

@ -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<f32>,
timeout: Option<Seconds>,
/// 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<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.
#[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,

@ -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::*;

@ -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 {
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())
},
},
}
);

@ -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

@ -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<f32>,
pub timeout: Option<Seconds>,
}

@ -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,
}

@ -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