From ee50eaf9b39488ef3292d584644b7268b40b2dd7 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Sun, 27 Nov 2022 12:38:53 -0600 Subject: [PATCH] Add heartbeat support (#153) * Update to support zero-size frame items * Add heartbeat functionality with client reconnecting logic * Fix connection reauthentication failures preventing future reauthentication * More logging * Remove persist * Update connection logic to have server take on client id rather than having client take on server id during reconnect * Bump minimum rust version to 1.64.0 * Bump to v0.20.0-alpha.3 and fix clippy warnings * Update cargo.lock --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 23 + Cargo.lock | 593 +++++++++-------- Cargo.toml | 6 +- distant-core/Cargo.toml | 4 +- distant-core/src/api.rs | 6 +- distant-core/src/api/local.rs | 34 +- distant-core/src/api/local/state.rs | 58 +- distant-core/src/api/local/state/process.rs | 36 +- .../src/api/local/state/process/instance.rs | 3 - distant-core/src/api/local/state/watcher.rs | 4 - distant-core/src/client/ext.rs | 7 - distant-core/src/client/lsp.rs | 15 +- distant-core/src/client/process.rs | 15 +- distant-core/src/client/searcher.rs | 4 +- distant-core/src/client/watcher.rs | 4 +- distant-core/src/data.rs | 6 - distant-core/tests/stress/fixtures.rs | 2 +- distant-net/Cargo.toml | 2 +- distant-net/src/client.rs | 167 +++-- distant-net/src/client/builder.rs | 53 +- distant-net/src/client/config.rs | 34 + distant-net/src/client/reconnect.rs | 8 +- distant-net/src/common/connection.rs | 141 ++-- distant-net/src/common/transport.rs | 4 +- distant-net/src/common/transport/framed.rs | 46 +- .../src/common/transport/framed/backup.rs | 5 + .../src/common/transport/framed/frame.rs | 127 ++-- distant-net/src/common/transport/test.rs | 8 +- distant-net/src/manager/client.rs | 4 +- distant-net/src/manager/client/channel.rs | 12 +- distant-net/src/manager/server.rs | 4 +- distant-net/src/server.rs | 58 +- distant-net/src/server/config.rs | 5 + distant-net/src/server/connection.rs | 608 ++++++++++++++---- distant-net/src/server/ref.rs | 26 +- distant-net/src/server/ref/tcp.rs | 4 +- distant-net/src/server/ref/unix.rs | 4 +- distant-net/src/server/ref/windows.rs | 4 +- distant-net/src/server/state.rs | 65 +- distant-net/tests/manager_tests.rs | 4 +- distant-net/tests/typed_tests.rs | 3 +- distant-net/tests/untyped_tests.rs | 3 +- distant-ssh2/Cargo.toml | 4 +- distant-ssh2/src/api.rs | 11 +- distant-ssh2/src/lib.rs | 8 +- distant-ssh2/tests/ssh2/client.rs | 9 - distant-ssh2/tests/ssh2/launched.rs | 9 - src/cli/client.rs | 32 +- src/cli/commands/client.rs | 26 +- src/cli/commands/client/lsp.rs | 3 +- src/cli/commands/client/shell.rs | 2 - src/cli/commands/manager/handlers.rs | 19 +- tests/cli/repl/proc_spawn.rs | 5 - 54 files changed, 1418 insertions(+), 931 deletions(-) create mode 100644 distant-net/src/client/config.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 500b48c..f85af94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,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.61.0, os: ubuntu-latest } + - { rust: 1.64.0, os: ubuntu-latest } steps: - uses: actions/checkout@v3 - name: Install Rust ${{ matrix.rust }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7483d56..ebea231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.20.0-alpha.3] + +### Added + +- `Frame::empty` method as convenience for `Frame::new(&[])` +- `ClientConfig` to support `ReconnectStrategy` and a duration serving as the + maximum time to wait between server activity before attempting to reconnect + from the client +- Server sends empty frames periodically to act as heartbeats to let the client + know if the connection is still established +- Client now tracks length of time since last server activity and will attempt + a reconnect if no activity beyond that point + +### Changed + +- `Frame` methods `read` and `write` no longer return an `io::Result<...>` + and instead return `Option>` and nothing respectively +- `Frame::read` method now supports zero-size items +- `Client::inmemory_spawn` and `UntypedClient::inmemory_spawn` now take a + `ClientConfig` as the second argument instead of `ReconnectStrategy` +- Persist option now removed from `ProcSpawn` message and CLI +- Bump minimum Rust version to 1.64.0 + ## [0.20.0-alpha.2] - 2022-11-20 ### Added diff --git a/Cargo.lock b/Cargo.lock index 4e09b72..e2b89ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae06cea71059b6b79d879afcdd237a33ac61afc052fdd605815e6f3916254abf" +checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" dependencies = [ "crypto-common", "generic-array", @@ -20,9 +20,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -81,11 +81,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.4", "event-listener", "futures-core", ] @@ -105,64 +105,68 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", "async-task", - "concurrent-queue", + "concurrent-queue 2.0.0", "fastrand", "futures-lite", - "once_cell", "slab", ] [[package]] name = "async-fs" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" dependencies = [ "async-lock", + "autocfg", "blocking", "futures-lite", ] [[package]] name = "async-io" -version = "1.7.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ - "concurrent-queue", + "async-lock", + "autocfg", + "concurrent-queue 2.0.0", "futures-lite", "libc", "log", - "once_cell", "parking", "polling", "slab", "socket2", "waker-fn", - "winapi", + "windows-sys 0.42.0", ] [[package]] name = "async-lock" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" dependencies = [ "event-listener", + "futures-lite", ] [[package]] name = "async-net" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df" +checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" dependencies = [ "async-io", + "autocfg", "blocking", "futures-lite", ] @@ -175,26 +179,27 @@ checksum = "f61305cacf1d0c5c9d3ee283d22f8f1f8c743a18ceb44a1b102bd53476c141de" [[package]] name = "async-process" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", + "autocfg", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook 0.3.14", - "winapi", + "windows-sys 0.42.0", ] [[package]] name = "async-task" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" @@ -219,7 +224,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -238,15 +243,15 @@ checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64ct" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" [[package]] name = "bitflags" @@ -265,25 +270,25 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -322,9 +327,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytecount" @@ -340,9 +345,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cache-padded" @@ -352,15 +357,15 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "camino" -version = "1.0.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" +checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -423,14 +428,14 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.26" +version = "4.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e" +checksum = "0acbd8d28a0a60d7108d7ae850af6ba34cf2d1257fc646980e5f97ce14275966" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex", + "is-terminal", "once_cell", "strsim", "termcolor", @@ -438,9 +443,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.0.5" +version = "4.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0fba905b035a30d25c1b585bf1171690712fbb0ad3ac47214963aa4acc36c" +checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da" dependencies = [ "clap", ] @@ -489,13 +494,22 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" dependencies = [ "cache-padded", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.13.2" @@ -512,14 +526,13 @@ dependencies = [ [[package]] name = "console" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", - "regex", "terminal_size 0.1.17", "unicode-width", "winapi", @@ -527,9 +540,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661" +checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" [[package]] name = "convert_case" @@ -545,9 +558,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -563,9 +576,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -573,9 +586,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", @@ -584,36 +597,34 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.8" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg", "cfg-if 1.0.0", "crossbeam-utils", - "lazy_static", - "memoffset", + "memoffset 0.7.1", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if 1.0.0", - "lazy_static", ] [[package]] name = "crypto-bigint" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac961631d66e80ac7ac2ac01320628ce214ad2b5ef0a88ceb86eae459069e2b4" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -625,7 +636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.3", + "rand_core 0.6.4", "typenum", ] @@ -641,9 +652,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" dependencies = [ "cc", "cxxbridge-flags", @@ -653,9 +664,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" dependencies = [ "cc", "codespan-reporting", @@ -668,15 +679,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" [[package]] name = "cxxbridge-macro" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" dependencies = [ "proc-macro2", "quote", @@ -742,7 +753,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle", ] @@ -809,7 +820,7 @@ dependencies = [ [[package]] name = "distant" -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" dependencies = [ "anyhow", "assert_cmd", @@ -852,7 +863,7 @@ dependencies = [ [[package]] name = "distant-core" -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" dependencies = [ "assert_fs", "async-trait", @@ -892,7 +903,7 @@ dependencies = [ [[package]] name = "distant-net" -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" dependencies = [ "async-trait", "bytes", @@ -920,7 +931,7 @@ dependencies = [ [[package]] name = "distant-ssh2" -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" dependencies = [ "anyhow", "assert_fs", @@ -972,9 +983,9 @@ checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" [[package]] name = "ecdsa" -version = "0.14.1" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e737f9eebb44576f3ee654141a789464071eb369d02c4397b32b6a79790112" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ "der", "elliptic-curve", @@ -984,15 +995,15 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "elliptic-curve" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f6664c6a37892ed55da8dda26a99e6ccc783f0c72fa3c2eeaa00ed30d8f4d9a" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ "base16ct", "crypto-bigint", @@ -1004,7 +1015,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core 0.6.3", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1084,26 +1095,26 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] [[package]] name = "ff" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", ] @@ -1132,14 +1143,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -1156,9 +1167,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", "miniz_oxide", @@ -1326,9 +1337,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -1347,13 +1358,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -1364,9 +1375,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "globset" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" dependencies = [ "aho-corasick", "bstr 0.2.17", @@ -1472,20 +1483,20 @@ dependencies = [ [[package]] name = "group" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", ] [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -1502,6 +1513,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -1630,9 +1650,19 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "0.7.2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074" + +[[package]] +name = "io-lifetimes" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c3f4eff5495aee4c0399d7b6a0dc2b6e81be84242ffbfcf253ebacccc1d0cb" +checksum = "e394faa0efb47f9f227f1cd89978f854542b318a6f64fa695489c9c993056656" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] [[package]] name = "ioctl-rs" @@ -1643,6 +1673,18 @@ dependencies = [ "libc", ] +[[package]] +name = "is-terminal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae5bc6e2eb41c9def29a3e0f1306382807764b9b53112030eff57435667352d" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes 1.0.2", + "rustix 0.36.3", + "windows-sys 0.42.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1654,24 +1696,24 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] [[package]] name = "kqueue" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" dependencies = [ "kqueue-sys", "libc", @@ -1769,11 +1811,17 @@ version = "0.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" +[[package]] +name = "linux-raw-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" + [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -1796,9 +1844,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95af15f345b17af2efc8ead6080fb8bc376f8cec1b35277b935637595fe77498" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" dependencies = [ "libc", ] @@ -1818,6 +1866,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1826,9 +1883,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] @@ -1854,7 +1911,7 @@ dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -1947,7 +2004,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", ] @@ -1965,9 +2022,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl-src" -version = "300.0.7+3.0.3" +version = "300.0.11+3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0e85632b936bdfd145e98385e90da446b0574485d53be5627ebb9eeece0fa2" +checksum = "181e2429088818099075f6ccf30e6d096a513c46e7e0b946a4dde055c35cb17a" dependencies = [ "cc", ] @@ -1988,18 +2045,18 @@ dependencies = [ [[package]] name = "ordered-float" -version = "3.0.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bcbab4bfea7a59c2c0fe47211a1ac4e3e96bea6eb446d704f310bc5c732ae2" +checksum = "d84eb1409416d254e4a9c8fa56cc24701755025b458f0fcd8e59e1f5f40c23bf" dependencies = [ "num-traits", ] [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "p256" @@ -2047,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -2066,15 +2123,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -2100,9 +2157,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69486e2b8c2d2aeb9762db7b4e00b0331156393555cff467f4163ff06821eef8" +checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a" dependencies = [ "thiserror", "ucd-trie", @@ -2110,9 +2167,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13570633aff33c6d22ce47dd566b10a3b9122c2fe9d8e7501895905be532b91" +checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63" dependencies = [ "pest", "pest_generator", @@ -2120,9 +2177,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c567e5702efdc79fb18859ea74c3eb36e14c43da7b8c1f098a4ed6514ec7a0" +checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1" dependencies = [ "pest", "pest_meta", @@ -2133,13 +2190,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb32be5ee3bbdafa8c7a18b0a8a8d962b66cfa2ceee4037f49267a50ee821fe" +checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" dependencies = [ "once_cell", "pest", - "sha-1", + "sha1", ] [[package]] @@ -2264,21 +2321,22 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "polling" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "9f7d73f1eaed1ca1fb37b54dcc9b38e3b17d6c7b8ecb7abfffcac8d0351f17d4" dependencies = [ + "autocfg", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -2312,9 +2370,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" @@ -2332,15 +2390,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" [[package]] name = "predicates-tree" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" dependencies = [ "predicates-core", "termtree", @@ -2381,9 +2439,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -2410,7 +2468,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2430,7 +2488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2444,11 +2502,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.8", ] [[package]] @@ -2471,11 +2529,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" dependencies = [ - "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -2483,9 +2540,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -2495,9 +2552,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -2508,7 +2565,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] @@ -2547,9 +2604,9 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c0788437d5ee113c49af91d3594ebc4fcdcc962f8b6df5aa1c3eeafd8ad95de" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ "crypto-bigint", "hmac", @@ -2580,11 +2637,12 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" dependencies = [ "libc", + "rtoolbox", "winapi", ] @@ -2613,40 +2671,64 @@ dependencies = [ "syn", ] +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.9", + "semver 1.0.14", ] [[package]] name = "rustix" -version = "0.35.7" +version = "0.35.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51cc38aa10f6bbb377ed28197aa052aa4e2b762c22be9d3153d01822587e787" +checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9" dependencies = [ "bitflags", "errno", - "io-lifetimes", + "io-lifetimes 0.7.5", "libc", - "linux-raw-sys", - "windows-sys 0.36.1", + "linux-raw-sys 0.0.46", + "windows-sys 0.42.0", +] + +[[package]] +name = "rustix" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1fbb4dfc4eb1d390c02df47760bb19a84bb80b301ecc947ab5406394d8223e" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes 1.0.2", + "libc", + "linux-raw-sys 0.1.3", + "windows-sys 0.42.0", ] [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" @@ -2718,9 +2800,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "semver-parser" @@ -2773,9 +2855,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -2837,10 +2919,10 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -2918,12 +3000,12 @@ dependencies = [ [[package]] name = "signature" -version = "1.5.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ "digest 0.10.6", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2934,15 +3016,18 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smol" @@ -2964,9 +3049,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -3030,9 +3115,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.96" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", @@ -3053,9 +3138,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c375d5fd899e32847b8566e10598d6e9f1d9b55ec6de3cdf9e7da4bdc51371bc" +checksum = "29ddf41e393a9133c81d5f0974195366bd57082deac6e0eb02ed39b8341c2bb6" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -3129,7 +3214,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ca90c434fd12083d1a6bdcbe9f92a14f96c8a1ba600ba451734ac334521f7a" dependencies = [ - "rustix", + "rustix 0.35.13", "windows-sys 0.42.0", ] @@ -3166,9 +3251,9 @@ dependencies = [ [[package]] name = "termtree" -version = "0.2.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" [[package]] name = "termwiz" @@ -3224,18 +3309,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -3329,9 +3414,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -3340,9 +3425,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", ] @@ -3361,33 +3446,33 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" @@ -3458,12 +3543,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3472,9 +3551,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3482,13 +3561,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -3497,9 +3576,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3507,9 +3586,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -3520,15 +3599,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3791,6 +3870,6 @@ checksum = "3ab703352da6a72f35c39a533526393725640575bb211f61987a2748323ad956" [[package]] name = "zeroize" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index a22bb2b..37d755c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "distant" description = "Operate on a remote computer through file and process manipulation" categories = ["command-line-utilities"] keywords = ["cli"] -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -32,7 +32,7 @@ clap_complete = "4.0.5" config = { version = "0.13.2", default-features = false, features = ["toml"] } derive_more = { version = "0.99.17", default-features = false, features = ["display", "from", "error", "is_variant"] } dialoguer = { version = "0.10.2", default-features = false } -distant-core = { version = "=0.20.0-alpha.2", path = "distant-core", features = ["clap", "schemars"] } +distant-core = { version = "=0.20.0-alpha.3", path = "distant-core", features = ["clap", "schemars"] } directories = "4.0.1" flexi_logger = "0.24.1" indoc = "1.0.7" @@ -54,7 +54,7 @@ winsplit = "0.1.0" whoami = "1.2.3" # Optional native SSH functionality -distant-ssh2 = { version = "=0.20.0-alpha.2", path = "distant-ssh2", default-features = false, features = ["serde"], optional = true } +distant-ssh2 = { version = "=0.20.0-alpha.3", path = "distant-ssh2", default-features = false, features = ["serde"], optional = true } [target.'cfg(unix)'.dependencies] fork = "0.1.20" diff --git a/distant-core/Cargo.toml b/distant-core/Cargo.toml index 3472ef9..fa53c0b 100644 --- a/distant-core/Cargo.toml +++ b/distant-core/Cargo.toml @@ -3,7 +3,7 @@ name = "distant-core" description = "Core library for distant, enabling operation on a remote computer through file and process manipulation" categories = ["network-programming"] keywords = ["api", "async"] -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -19,7 +19,7 @@ async-trait = "0.1.58" bitflags = "1.3.2" bytes = "1.2.1" derive_more = { version = "0.99.17", default-features = false, features = ["as_mut", "as_ref", "deref", "deref_mut", "display", "from", "error", "into", "into_iterator", "is_variant", "try_into"] } -distant-net = { version = "=0.20.0-alpha.2", path = "../distant-net" } +distant-net = { version = "=0.20.0-alpha.3", path = "../distant-net" } futures = "0.3.25" grep = "0.2.10" hex = "0.4.3" diff --git a/distant-core/src/api.rs b/distant-core/src/api.rs index 8b76c36..7d90120 100644 --- a/distant-core/src/api.rs +++ b/distant-core/src/api.rs @@ -348,8 +348,6 @@ pub trait DistantApi { /// * `cmd` - the full command to run as a new process (including arguments) /// * `environment` - the environment variables to associate with the process /// * `current_dir` - the alternative current directory to use with the process - /// * `persist` - if true, the process will continue running even after the connection that - /// spawned the process has terminated /// * `pty` - if provided, will run the process within a PTY of the given size /// /// *Override this, otherwise it will return "unsupported" as an error.* @@ -360,7 +358,6 @@ pub trait DistantApi { cmd: String, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> io::Result { unsupported("proc_spawn") @@ -650,11 +647,10 @@ where cmd, environment, current_dir, - persist, pty, } => server .api - .proc_spawn(ctx, cmd.into(), environment, current_dir, persist, pty) + .proc_spawn(ctx, cmd.into(), environment, current_dir, pty) .await .map(|id| DistantResponseData::ProcSpawned { id }) .unwrap_or_else(DistantResponseData::from), diff --git a/distant-core/src/api/local.rs b/distant-core/src/api/local.rs index 411bb28..1cc8fce 100644 --- a/distant-core/src/api/local.rs +++ b/distant-core/src/api/local.rs @@ -6,7 +6,6 @@ use crate::{ DistantApi, DistantCtx, }; use async_trait::async_trait; -use distant_net::server::ConnectionCtx; use log::*; use std::{ io, @@ -18,7 +17,6 @@ use walkdir::WalkDir; mod process; mod state; -pub use state::ConnectionState; use state::*; /// Represents an implementation of [`DistantApi`] that works with the local machine @@ -40,14 +38,7 @@ impl LocalDistantApi { #[async_trait] impl DistantApi for LocalDistantApi { - type LocalData = ConnectionState; - - /// Injects the global channels into the local connection - async fn on_accept(&self, ctx: ConnectionCtx<'_, Self::LocalData>) -> io::Result<()> { - ctx.local_data.process_channel = self.state.process.clone_channel(); - ctx.local_data.watcher_channel = self.state.watcher.clone_channel(); - Ok(()) - } + type LocalData = (); async fn capabilities(&self, ctx: DistantCtx) -> io::Result { debug!("[Conn {}] Querying capabilities", ctx.connection_id); @@ -451,16 +442,15 @@ impl DistantApi for LocalDistantApi { cmd: String, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> io::Result { debug!( - "[Conn {}] Spawning {} {{environment: {:?}, current_dir: {:?}, persist: {}, pty: {:?}}}", - ctx.connection_id, cmd, environment, current_dir, persist, pty + "[Conn {}] Spawning {} {{environment: {:?}, current_dir: {:?}, pty: {:?}}}", + ctx.connection_id, cmd, environment, current_dir, pty ); self.state .process - .spawn(cmd, environment, current_dir, persist, pty, ctx.reply) + .spawn(cmd, environment, current_dir, pty, ctx.reply) .await } @@ -504,6 +494,7 @@ impl DistantApi for LocalDistantApi { #[cfg(test)] mod tests { use super::*; + use crate::api::ConnectionCtx; use crate::data::DistantResponseData; use assert_fs::prelude::*; use distant_net::server::Reply; @@ -576,18 +567,18 @@ mod tests { buffer: usize, ) -> ( LocalDistantApi, - DistantCtx, + DistantCtx<()>, mpsc::Receiver, ) { let api = LocalDistantApi::initialize().unwrap(); let (reply, rx) = make_reply(buffer); let connection_id = rand::random(); - let mut local_data = ConnectionState::default(); + DistantApi::on_accept( &api, ConnectionCtx { connection_id, - local_data: &mut local_data, + local_data: &mut (), }, ) .await @@ -595,7 +586,7 @@ mod tests { let ctx = DistantCtx { connection_id, reply, - local_data: Arc::new(local_data), + local_data: Arc::new(()), }; (api, ctx, rx) } @@ -1842,7 +1833,6 @@ mod tests { /* cmd */ DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -1867,7 +1857,6 @@ mod tests { ), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -1893,7 +1882,6 @@ mod tests { ), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -1958,7 +1946,6 @@ mod tests { ), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -2019,7 +2006,6 @@ mod tests { format!("{} {} 0.1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -2059,7 +2045,6 @@ mod tests { format!("{} {} 1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -2126,7 +2111,6 @@ mod tests { ), Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await diff --git a/distant-core/src/api/local/state.rs b/distant-core/src/api/local/state.rs index 4bccae5..f95e1c8 100644 --- a/distant-core/src/api/local/state.rs +++ b/distant-core/src/api/local/state.rs @@ -1,6 +1,4 @@ -use crate::data::{ProcessId, SearchId}; -use distant_net::common::ConnectionId; -use std::{io, path::PathBuf}; +use std::io; mod process; pub use process::*; @@ -32,57 +30,3 @@ impl GlobalState { }) } } - -/// Holds connection-specific state managed by the server -#[derive(Default)] -pub struct ConnectionState { - /// Unique id associated with connection - id: ConnectionId, - - /// Channel connected to global process state - pub(crate) process_channel: ProcessChannel, - - /// Channel connected to global search state - pub(crate) search_channel: SearchChannel, - - /// Channel connected to global watcher state - pub(crate) watcher_channel: WatcherChannel, - - /// Contains ids of processes that will be terminated when the connection is closed - processes: Vec, - - /// Contains paths being watched that will be unwatched when the connection is closed - paths: Vec, - - /// Contains ids of searches that will be terminated when the connection is closed - searches: Vec, -} - -impl Drop for ConnectionState { - fn drop(&mut self) { - let id = self.id; - let processes: Vec = self.processes.drain(..).collect(); - let paths: Vec = self.paths.drain(..).collect(); - let searches: Vec = self.searches.drain(..).collect(); - - let process_channel = self.process_channel.clone(); - let search_channel = self.search_channel.clone(); - let watcher_channel = self.watcher_channel.clone(); - - // NOTE: We cannot (and should not) block during drop to perform cleanup, - // instead spawning a task that will do the cleanup async - tokio::spawn(async move { - for id in processes { - let _ = process_channel.kill(id).await; - } - - for id in searches { - let _ = search_channel.cancel(id).await; - } - - for path in paths { - let _ = watcher_channel.unwatch(id, path).await; - } - }); - } -} diff --git a/distant-core/src/api/local/state/process.rs b/distant-core/src/api/local/state/process.rs index 3638177..65efabc 100644 --- a/distant-core/src/api/local/state/process.rs +++ b/distant-core/src/api/local/state/process.rs @@ -9,14 +9,14 @@ use tokio::{ mod instance; pub use instance::*; -/// Holds information related to spawned processes on the server +/// Holds information related to spawned processes on the server. pub struct ProcessState { channel: ProcessChannel, task: JoinHandle<()>, } impl Drop for ProcessState { - /// Aborts the task that handles process operations and management + /// Aborts the task that handles process operations and management. fn drop(&mut self) { self.abort(); } @@ -33,10 +33,6 @@ impl ProcessState { } } - pub fn clone_channel(&self) -> ProcessChannel { - self.channel.clone() - } - /// Aborts the process task pub fn abort(&self) { self.task.abort(); @@ -57,7 +53,7 @@ pub struct ProcessChannel { } impl Default for ProcessChannel { - /// Creates a new channel that is closed by default + /// Creates a new channel that is closed by default. fn default() -> Self { let (tx, _) = mpsc::channel(1); Self { tx } @@ -65,13 +61,12 @@ impl Default for ProcessChannel { } impl ProcessChannel { - /// Spawns a new process, returning the id associated with it + /// Spawns a new process, returning the id associated with it. pub async fn spawn( &self, cmd: String, environment: Environment, current_dir: Option, - persist: bool, pty: Option, reply: Box>, ) -> io::Result { @@ -81,7 +76,6 @@ impl ProcessChannel { cmd, environment, current_dir, - persist, pty, reply, cb, @@ -92,7 +86,7 @@ impl ProcessChannel { .map_err(|_| io::Error::new(io::ErrorKind::Other, "Response to spawn dropped"))? } - /// Resizes the pty of a running process + /// Resizes the pty of a running process. pub async fn resize_pty(&self, id: ProcessId, size: PtySize) -> io::Result<()> { let (cb, rx) = oneshot::channel(); self.tx @@ -103,7 +97,7 @@ impl ProcessChannel { .map_err(|_| io::Error::new(io::ErrorKind::Other, "Response to resize dropped"))? } - /// Send stdin to a running process + /// Send stdin to a running process. pub async fn send_stdin(&self, id: ProcessId, data: Vec) -> io::Result<()> { let (cb, rx) = oneshot::channel(); self.tx @@ -114,7 +108,8 @@ impl ProcessChannel { .map_err(|_| io::Error::new(io::ErrorKind::Other, "Response to stdin dropped"))? } - /// Kills a running process + /// Kills a running process, including persistent processes if `force` is true. Will fail if + /// unable to kill the process or `force` is false when the process is persistent. pub async fn kill(&self, id: ProcessId) -> io::Result<()> { let (cb, rx) = oneshot::channel(); self.tx @@ -126,13 +121,12 @@ impl ProcessChannel { } } -/// Internal message to pass to our task below to perform some action +/// Internal message to pass to our task below to perform some action. enum InnerProcessMsg { Spawn { cmd: String, environment: Environment, current_dir: Option, - persist: bool, pty: Option, reply: Box>, cb: oneshot::Sender>, @@ -165,14 +159,12 @@ async fn process_task(tx: mpsc::Sender, mut rx: mpsc::Receiver< cmd, environment, current_dir, - persist, pty, reply, cb, } => { let _ = cb.send( - match ProcessInstance::spawn(cmd, environment, current_dir, persist, pty, reply) - { + match ProcessInstance::spawn(cmd, environment, current_dir, pty, reply) { Ok(mut process) => { let id = process.id; @@ -195,7 +187,7 @@ async fn process_task(tx: mpsc::Sender, mut rx: mpsc::Receiver< Some(process) => process.pty.resize_pty(size), None => Err(io::Error::new( io::ErrorKind::Other, - format!("No process found with id {}", id), + format!("No process found with id {id}"), )), }); } @@ -205,12 +197,12 @@ async fn process_task(tx: mpsc::Sender, mut rx: mpsc::Receiver< Some(stdin) => stdin.send(&data).await, None => Err(io::Error::new( io::ErrorKind::Other, - format!("Process {} stdin is closed", id), + format!("Process {id} stdin is closed"), )), }, None => Err(io::Error::new( io::ErrorKind::Other, - format!("No process found with id {}", id), + format!("No process found with id {id}"), )), }); } @@ -219,7 +211,7 @@ async fn process_task(tx: mpsc::Sender, mut rx: mpsc::Receiver< Some(process) => process.killer.kill().await, None => Err(io::Error::new( io::ErrorKind::Other, - format!("No process found with id {}", id), + format!("No process found with id {id}"), )), }); } diff --git a/distant-core/src/api/local/state/process/instance.rs b/distant-core/src/api/local/state/process/instance.rs index 38b7aaf..c78d4d3 100644 --- a/distant-core/src/api/local/state/process/instance.rs +++ b/distant-core/src/api/local/state/process/instance.rs @@ -13,7 +13,6 @@ use tokio::task::JoinHandle; pub struct ProcessInstance { pub cmd: String, pub args: Vec, - pub persist: bool, pub id: ProcessId, pub stdin: Option>, @@ -63,7 +62,6 @@ impl ProcessInstance { cmd: String, environment: Environment, current_dir: Option, - persist: bool, pty: Option, reply: Box>, ) -> io::Result { @@ -135,7 +133,6 @@ impl ProcessInstance { Ok(ProcessInstance { cmd, args, - persist, id, stdin, killer, diff --git a/distant-core/src/api/local/state/watcher.rs b/distant-core/src/api/local/state/watcher.rs index 79a3287..18f2133 100644 --- a/distant-core/src/api/local/state/watcher.rs +++ b/distant-core/src/api/local/state/watcher.rs @@ -95,10 +95,6 @@ impl WatcherState { } } - pub fn clone_channel(&self) -> WatcherChannel { - self.channel.clone() - } - /// Aborts the watcher task pub fn abort(&self) { self.task.abort(); diff --git a/distant-core/src/client/ext.rs b/distant-core/src/client/ext.rs index 2053e1a..9532ca0 100644 --- a/distant-core/src/client/ext.rs +++ b/distant-core/src/client/ext.rs @@ -101,7 +101,6 @@ pub trait DistantChannelExt { cmd: impl Into, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> AsyncReturn<'_, RemoteProcess>; @@ -111,7 +110,6 @@ pub trait DistantChannelExt { cmd: impl Into, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> AsyncReturn<'_, RemoteLspProcess>; @@ -369,7 +367,6 @@ impl DistantChannelExt cmd: impl Into, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> AsyncReturn<'_, RemoteProcess> { let cmd = cmd.into(); @@ -377,7 +374,6 @@ impl DistantChannelExt RemoteCommand::new() .environment(environment) .current_dir(current_dir) - .persist(persist) .pty(pty) .spawn(self.clone(), cmd) .await @@ -389,7 +385,6 @@ impl DistantChannelExt cmd: impl Into, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> AsyncReturn<'_, RemoteLspProcess> { let cmd = cmd.into(); @@ -397,7 +392,6 @@ impl DistantChannelExt RemoteLspCommand::new() .environment(environment) .current_dir(current_dir) - .persist(persist) .pty(pty) .spawn(self.clone(), cmd) .await @@ -416,7 +410,6 @@ impl DistantChannelExt RemoteCommand::new() .environment(environment) .current_dir(current_dir) - .persist(false) .pty(pty) .spawn(self.clone(), cmd) .await? diff --git a/distant-core/src/client/lsp.rs b/distant-core/src/client/lsp.rs index 2240c09..09bf85d 100644 --- a/distant-core/src/client/lsp.rs +++ b/distant-core/src/client/lsp.rs @@ -22,7 +22,6 @@ pub use msg::*; /// A [`RemoteLspProcess`] builder providing support to configure /// before spawning the process on a remote machine pub struct RemoteLspCommand { - persist: bool, pty: Option, environment: Environment, current_dir: Option, @@ -38,21 +37,12 @@ impl RemoteLspCommand { /// Creates a new set of options for a remote LSP process pub fn new() -> Self { Self { - persist: false, pty: None, environment: Environment::new(), current_dir: None, } } - /// Sets whether or not the process will be persistent, - /// meaning that it will not be terminated when the - /// connection to the remote machine is terminated - pub fn persist(&mut self, persist: bool) -> &mut Self { - self.persist = persist; - self - } - /// Configures the process to leverage a PTY with the specified size pub fn pty(&mut self, pty: Option) -> &mut Self { self.pty = pty; @@ -81,7 +71,6 @@ impl RemoteLspCommand { let mut command = RemoteCommand::new(); command.environment(self.environment.clone()); command.current_dir(self.current_dir.clone()); - command.persist(self.persist); command.pty(self.pty); let mut inner = command.spawn(channel, cmd).await?; @@ -412,7 +401,7 @@ mod tests { use crate::data::{DistantRequestData, DistantResponseData}; use distant_net::{ common::{FramedTransport, InmemoryTransport, Request, Response}, - Client, ReconnectStrategy, + Client, }; use std::{future::Future, time::Duration}; use test_log::test; @@ -423,7 +412,7 @@ mod tests { // Configures an lsp process with a means to send & receive data from outside async fn spawn_lsp_process() -> (FramedTransport, RemoteLspProcess) { let (mut t1, t2) = FramedTransport::pair(100); - let client = Client::spawn_inmemory(t2, ReconnectStrategy::Fail); + let client = Client::spawn_inmemory(t2, Default::default()); let spawn_task = tokio::spawn({ let channel = client.clone_channel(); async move { diff --git a/distant-core/src/client/process.rs b/distant-core/src/client/process.rs index e502b04..6defd7d 100644 --- a/distant-core/src/client/process.rs +++ b/distant-core/src/client/process.rs @@ -47,7 +47,6 @@ type StatusResult = io::Result; /// A [`RemoteProcess`] builder providing support to configure /// before spawning the process on a remote machine pub struct RemoteCommand { - persist: bool, pty: Option, environment: Environment, current_dir: Option, @@ -63,21 +62,12 @@ impl RemoteCommand { /// Creates a new set of options for a remote process pub fn new() -> Self { Self { - persist: false, pty: None, environment: Environment::new(), current_dir: None, } } - /// Sets whether or not the process will be persistent, - /// meaning that it will not be terminated when the - /// connection to the remote machine is terminated - pub fn persist(&mut self, persist: bool) -> &mut Self { - self.persist = persist; - self - } - /// Configures the process to leverage a PTY with the specified size pub fn pty(&mut self, pty: Option) -> &mut Self { self.pty = pty; @@ -109,7 +99,6 @@ impl RemoteCommand { .mail(Request::new(DistantMsg::Single( DistantRequestData::ProcSpawn { cmd: Cmd::from(cmd), - persist: self.persist, pty: self.pty, environment: self.environment.clone(), current_dir: self.current_dir.clone(), @@ -613,14 +602,14 @@ mod tests { }; use distant_net::{ common::{FramedTransport, InmemoryTransport, Response}, - Client, ReconnectStrategy, + Client, }; use std::time::Duration; use test_log::test; fn make_session() -> (FramedTransport, DistantClient) { let (t1, t2) = FramedTransport::pair(100); - (t1, Client::spawn_inmemory(t2, ReconnectStrategy::Fail)) + (t1, Client::spawn_inmemory(t2, Default::default())) } #[test(tokio::test)] diff --git a/distant-core/src/client/searcher.rs b/distant-core/src/client/searcher.rs index d3fb364..a81cbd9 100644 --- a/distant-core/src/client/searcher.rs +++ b/distant-core/src/client/searcher.rs @@ -198,7 +198,7 @@ mod tests { use crate::DistantClient; use distant_net::{ common::{FramedTransport, InmemoryTransport, Response}, - Client, ReconnectStrategy, + Client, }; use std::{path::PathBuf, sync::Arc}; use test_log::test; @@ -206,7 +206,7 @@ mod tests { fn make_session() -> (FramedTransport, DistantClient) { let (t1, t2) = FramedTransport::pair(100); - (t1, Client::spawn_inmemory(t2, ReconnectStrategy::Fail)) + (t1, Client::spawn_inmemory(t2, Default::default())) } #[test(tokio::test)] diff --git a/distant-core/src/client/watcher.rs b/distant-core/src/client/watcher.rs index a1f98a3..cb366bd 100644 --- a/distant-core/src/client/watcher.rs +++ b/distant-core/src/client/watcher.rs @@ -186,7 +186,7 @@ mod tests { use crate::DistantClient; use distant_net::{ common::{FramedTransport, InmemoryTransport, Response}, - Client, ReconnectStrategy, + Client, }; use std::sync::Arc; use test_log::test; @@ -194,7 +194,7 @@ mod tests { fn make_session() -> (FramedTransport, DistantClient) { let (t1, t2) = FramedTransport::pair(100); - (t1, Client::spawn_inmemory(t2, ReconnectStrategy::Fail)) + (t1, Client::spawn_inmemory(t2, Default::default())) } #[test(tokio::test)] diff --git a/distant-core/src/data.rs b/distant-core/src/data.rs index 1a135d6..5089632 100644 --- a/distant-core/src/data.rs +++ b/distant-core/src/data.rs @@ -417,12 +417,6 @@ pub enum DistantRequestData { #[cfg_attr(feature = "clap", clap(long))] current_dir: Option, - /// Whether or not the process should be persistent, meaning that the process will not be - /// killed when the associated client disconnects - #[serde(default)] - #[cfg_attr(feature = "clap", clap(long))] - persist: bool, - /// If provided, will spawn process in a pty, otherwise spawns directly #[serde(default)] #[cfg_attr(feature = "clap", clap(long))] diff --git a/distant-core/tests/stress/fixtures.rs b/distant-core/tests/stress/fixtures.rs index ae06018..2e75f6c 100644 --- a/distant-core/tests/stress/fixtures.rs +++ b/distant-core/tests/stress/fixtures.rs @@ -45,7 +45,7 @@ impl DistantClientCtx { // Now initialize our client let client: DistantClient = Client::build() .auth_handler(DummyAuthHandler) - .timeout(Duration::from_secs(1)) + .connect_timeout(Duration::from_secs(1)) .connector(TcpConnector::new( format!("{}:{}", ip_addr, port) .parse::() diff --git a/distant-net/Cargo.toml b/distant-net/Cargo.toml index 50f55b5..747b913 100644 --- a/distant-net/Cargo.toml +++ b/distant-net/Cargo.toml @@ -3,7 +3,7 @@ name = "distant-net" description = "Network library for distant, providing implementations to support client/server architecture" categories = ["network-programming"] keywords = ["api", "async"] -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" diff --git a/distant-net/src/client.rs b/distant-net/src/client.rs index aaa2e22..5c324c1 100644 --- a/distant-net/src/client.rs +++ b/distant-net/src/client.rs @@ -8,7 +8,7 @@ use std::{ fmt, io, ops::{Deref, DerefMut}, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use tokio::{ sync::{mpsc, oneshot, watch}, @@ -21,6 +21,9 @@ pub use builder::*; mod channel; pub use channel::*; +mod config; +pub use config::*; + mod reconnect; pub use reconnect::*; @@ -135,18 +138,18 @@ impl UntypedClient { /// within a program. pub fn spawn_inmemory( transport: FramedTransport, - strategy: ReconnectStrategy, + config: ClientConfig, ) -> Self { let connection = Connection::Client { id: rand::random(), reauth_otp: HeapSecretKey::generate(32).unwrap(), transport, }; - Self::spawn(connection, strategy) + Self::spawn(connection, config) } /// Spawns a client using the provided [`Connection`]. - pub(crate) fn spawn(mut connection: Connection, mut strategy: ReconnectStrategy) -> Self + pub(crate) fn spawn(mut connection: Connection, config: ClientConfig) -> Self where V: Transport + 'static, { @@ -164,6 +167,7 @@ impl UntypedClient { let (watcher_tx, watcher_rx) = watch::channel(ConnectionState::Connected); let task = tokio::spawn(async move { let mut needs_reconnect = false; + let mut last_read_frame_time = Instant::now(); // NOTE: We hold onto a copy of the shutdown sender, even though we will never use it, // to prevent the channel from being closed. This is because we do a check to @@ -171,19 +175,24 @@ impl UntypedClient { // would cause recv() to resolve immediately and result in the task shutting // down. let _shutdown_tx = shutdown_tx_2; + let ClientConfig { + mut reconnect_strategy, + silence_duration, + } = config; loop { // If we have flagged that a reconnect is needed, attempt to do so if needs_reconnect { info!("Client encountered issue, attempting to reconnect"); if log::log_enabled!(log::Level::Debug) { - debug!("Using strategy {strategy:?}"); + debug!("Using strategy {reconnect_strategy:?}"); } - match strategy.reconnect(&mut connection).await { - Ok(x) => { + match reconnect_strategy.reconnect(&mut connection).await { + Ok(()) => { + info!("Client successfully reconnected!"); needs_reconnect = false; + last_read_frame_time = Instant::now(); watcher_tx.send_replace(ConnectionState::Connected); - x } Err(x) => { error!("Unable to re-establish connection: {x}"); @@ -193,6 +202,29 @@ impl UntypedClient { } } + macro_rules! silence_needs_reconnect { + () => {{ + debug!( + "Client exceeded {}s without server activity, so attempting to reconnect", + silence_duration.as_secs_f32(), + ); + needs_reconnect = true; + watcher_tx.send_replace(ConnectionState::Reconnecting); + continue; + }}; + } + + let silence_time_remaining = silence_duration + .checked_sub(last_read_frame_time.elapsed()) + .unwrap_or_default(); + + // NOTE: sleep will not trigger if duration is zero/nanosecond scale, so we + // instead will do an early check here in the case that we need to reconnect + // prior to a sleep while polling the ready status + if silence_time_remaining.as_millis() == 0 { + silence_needs_reconnect!(); + } + let ready = tokio::select! { // NOTE: This should NEVER return None as we never allow the channel to close. cb = shutdown_rx.recv() => { @@ -202,6 +234,9 @@ impl UntypedClient { watcher_tx.send_replace(ConnectionState::Disconnected); break Ok(()); } + _ = tokio::time::sleep(silence_time_remaining) => { + silence_needs_reconnect!(); + } result = connection.ready(Interest::READABLE | Interest::WRITABLE) => { match result { Ok(result) => result, @@ -220,7 +255,16 @@ impl UntypedClient { if ready.is_readable() { match connection.try_read_frame() { + // If we get an empty frame, we consider this a heartbeat and want + // to adjust our frame read time and discard it from our backup + Ok(Some(frame)) if frame.is_empty() => { + trace!("Client received heartbeat"); + last_read_frame_time = Instant::now(); + } + + // Otherwise, we attempt to parse a frame as a response Ok(Some(frame)) => { + last_read_frame_time = Instant::now(); match UntypedResponse::from_slice(frame.as_item()) { Ok(response) => { if log_enabled!(Level::Trace) { @@ -242,6 +286,7 @@ impl UntypedClient { } } } + Ok(None) => { debug!("Connection closed"); needs_reconnect = true; @@ -391,9 +436,9 @@ where /// within a program. pub fn spawn_inmemory( transport: FramedTransport, - strategy: ReconnectStrategy, + config: ClientConfig, ) -> Self { - UntypedClient::spawn_inmemory(transport, strategy).into_typed_client() + UntypedClient::spawn_inmemory(transport, config).into_typed_client() } } @@ -515,6 +560,7 @@ impl From> for Channel { #[cfg(test)] mod tests { use super::*; + use crate::client::ClientConfig; use crate::common::{Ready, Request, Response, TestTransport}; mod typed { @@ -524,12 +570,19 @@ mod tests { fn spawn_test_client( connection: Connection, - strategy: ReconnectStrategy, + reconnect_strategy: ReconnectStrategy, ) -> TestClient where T: Transport + 'static, { - UntypedClient::spawn(connection, strategy).into_typed_client() + UntypedClient::spawn( + connection, + ClientConfig { + reconnect_strategy, + ..Default::default() + }, + ) + .into_typed_client() } /// Creates a new test transport whose operations do not panic, but do nothing. @@ -848,7 +901,7 @@ mod tests { async fn should_write_queued_requests_as_outgoing_frames() { let (client, mut server) = Connection::pair(100); - let mut client = TestClient::spawn(client, ReconnectStrategy::Fail); + let mut client = TestClient::spawn(client, Default::default()); client .fire(Request::new(1u8).to_untyped_request().unwrap()) .await @@ -908,7 +961,7 @@ mod tests { .unwrap(); }); - let mut client = TestClient::spawn(client, ReconnectStrategy::Fail); + let mut client = TestClient::spawn(client, Default::default()); assert_eq!( client .send(Request::new(1u8).to_untyped_request().unwrap()) @@ -938,10 +991,13 @@ mod tests { transport }), - ReconnectStrategy::FixedInterval { - interval: Duration::from_millis(50), - max_retries: None, - timeout: None, + ClientConfig { + reconnect_strategy: ReconnectStrategy::FixedInterval { + interval: Duration::from_millis(50), + max_retries: None, + timeout: None, + }, + ..Default::default() }, ); @@ -969,10 +1025,13 @@ mod tests { transport }), - ReconnectStrategy::FixedInterval { - interval: Duration::from_millis(50), - max_retries: None, - timeout: None, + ClientConfig { + reconnect_strategy: ReconnectStrategy::FixedInterval { + interval: Duration::from_millis(50), + max_retries: None, + timeout: None, + }, + ..Default::default() }, ); @@ -1000,10 +1059,13 @@ mod tests { transport }), - ReconnectStrategy::FixedInterval { - interval: Duration::from_millis(50), - max_retries: None, - timeout: None, + ClientConfig { + reconnect_strategy: ReconnectStrategy::FixedInterval { + interval: Duration::from_millis(50), + max_retries: None, + timeout: None, + }, + ..Default::default() }, ); @@ -1031,10 +1093,13 @@ mod tests { transport }), - ReconnectStrategy::FixedInterval { - interval: Duration::from_millis(50), - max_retries: None, - timeout: None, + ClientConfig { + reconnect_strategy: ReconnectStrategy::FixedInterval { + interval: Duration::from_millis(50), + max_retries: None, + timeout: None, + }, + ..Default::default() }, ); @@ -1079,10 +1144,13 @@ mod tests { transport }), - ReconnectStrategy::FixedInterval { - interval: Duration::from_millis(50), - max_retries: None, - timeout: None, + ClientConfig { + reconnect_strategy: ReconnectStrategy::FixedInterval { + interval: Duration::from_millis(50), + max_retries: None, + timeout: None, + }, + ..Default::default() }, ); @@ -1101,7 +1169,7 @@ mod tests { // Spawn the client, verify the task is running, kill our server, and verify that the // client does not block trying to reconnect - let client = TestClient::spawn(client, ReconnectStrategy::Fail); + let client = TestClient::spawn(client, Default::default()); assert!(!client.is_finished(), "Client unexpectedly died"); drop(server); assert_eq!( @@ -1114,7 +1182,7 @@ mod tests { async fn should_exit_if_shutdown_signal_detected() { let (client, _server) = Connection::pair(100); - let client = TestClient::spawn(client, ReconnectStrategy::Fail); + let client = TestClient::spawn(client, Default::default()); client.shutdown().await.unwrap(); // NOTE: We wait for the client's task to conclude by using `wait` to ensure we do not @@ -1142,7 +1210,7 @@ mod tests { // NOTE: We consume the client to produce a channel without maintaining the shutdown // channel in order to ensure that dropping the client does not kill the task. - let mut channel = TestClient::spawn(client, ReconnectStrategy::Fail).into_channel(); + let mut channel = TestClient::spawn(client, Default::default()).into_channel(); assert_eq!( channel .send(Request::new(1u8).to_untyped_request().unwrap()) @@ -1154,5 +1222,30 @@ mod tests { 2 ); } + + #[test(tokio::test)] + async fn should_attempt_to_reconnect_if_no_activity_from_server_within_silence_duration() { + let (client, _) = Connection::pair(100); + + // NOTE: We consume the client to produce a channel without maintaining the shutdown + // channel in order to ensure that dropping the client does not kill the task. + let client = TestClient::spawn( + client, + ClientConfig { + silence_duration: Duration::from_millis(100), + reconnect_strategy: ReconnectStrategy::FixedInterval { + interval: Duration::from_millis(50), + max_retries: Some(3), + timeout: None, + }, + }, + ); + + let (tx, mut rx) = mpsc::unbounded_channel(); + client.on_connection_change(move |state| tx.send(state).unwrap()); + assert_eq!(rx.recv().await, Some(ConnectionState::Reconnecting)); + assert_eq!(rx.recv().await, Some(ConnectionState::Disconnected)); + assert_eq!(rx.recv().await, None); + } } } diff --git a/distant-net/src/client/builder.rs b/distant-net/src/client/builder.rs index 13a15f6..199c0d3 100644 --- a/distant-net/src/client/builder.rs +++ b/distant-net/src/client/builder.rs @@ -13,7 +13,8 @@ mod windows; #[cfg(windows)] pub use windows::*; -use crate::client::{Client, ReconnectStrategy, UntypedClient}; +use super::ClientConfig; +use crate::client::{Client, UntypedClient}; use crate::common::{authentication::AuthHandler, Connection, Transport}; use async_trait::async_trait; use std::{convert, io, time::Duration}; @@ -40,44 +41,48 @@ impl Connector for T { pub struct ClientBuilder { auth_handler: H, connector: C, - reconnect_strategy: ReconnectStrategy, - timeout: Option, + config: ClientConfig, + connect_timeout: Option, } impl ClientBuilder { + /// Configure the authentication handler to use when connecting to a server. pub fn auth_handler(self, auth_handler: U) -> ClientBuilder { ClientBuilder { auth_handler, + config: self.config, connector: self.connector, - reconnect_strategy: self.reconnect_strategy, - timeout: self.timeout, + connect_timeout: self.connect_timeout, } } - pub fn connector(self, connector: U) -> ClientBuilder { - ClientBuilder { + /// Configure the client-local configuration details. + pub fn config(self, config: ClientConfig) -> Self { + Self { auth_handler: self.auth_handler, - connector, - reconnect_strategy: self.reconnect_strategy, - timeout: self.timeout, + config, + connector: self.connector, + connect_timeout: self.connect_timeout, } } - pub fn reconnect_strategy(self, reconnect_strategy: ReconnectStrategy) -> ClientBuilder { + /// Configure the connector to use to facilitate connecting to a server. + pub fn connector(self, connector: U) -> ClientBuilder { ClientBuilder { auth_handler: self.auth_handler, - connector: self.connector, - reconnect_strategy, - timeout: self.timeout, + config: self.config, + connector, + connect_timeout: self.connect_timeout, } } - pub fn timeout(self, timeout: impl Into>) -> Self { + /// Configure a maximum duration to wait for a connection to a server to complete. + pub fn connect_timeout(self, connect_timeout: impl Into>) -> Self { Self { auth_handler: self.auth_handler, + config: self.config, connector: self.connector, - reconnect_strategy: self.reconnect_strategy, - timeout: timeout.into(), + connect_timeout: connect_timeout.into(), } } } @@ -86,9 +91,9 @@ impl ClientBuilder<(), ()> { pub fn new() -> Self { Self { auth_handler: (), - reconnect_strategy: ReconnectStrategy::default(), + config: Default::default(), connector: (), - timeout: None, + connect_timeout: None, } } } @@ -109,11 +114,11 @@ where /// is fully established and authenticated. pub async fn connect_untyped(self) -> io::Result { let auth_handler = self.auth_handler; - let retry_strategy = self.reconnect_strategy; - let timeout = self.timeout; + let config = self.config; + let connect_timeout = self.connect_timeout; let f = async move { - let transport = match timeout { + let transport = match connect_timeout { Some(duration) => tokio::time::timeout(duration, self.connector.connect()) .await .map_err(|x| io::Error::new(io::ErrorKind::TimedOut, x)) @@ -121,10 +126,10 @@ where None => self.connector.connect().await?, }; let connection = Connection::client(transport, auth_handler).await?; - Ok(UntypedClient::spawn(connection, retry_strategy)) + Ok(UntypedClient::spawn(connection, config)) }; - match timeout { + match connect_timeout { Some(duration) => tokio::time::timeout(duration, f) .await .map_err(|x| io::Error::new(io::ErrorKind::TimedOut, x)) diff --git a/distant-net/src/client/config.rs b/distant-net/src/client/config.rs new file mode 100644 index 0000000..ad6c13b --- /dev/null +++ b/distant-net/src/client/config.rs @@ -0,0 +1,34 @@ +use super::ReconnectStrategy; +use std::time::Duration; + +const DEFAULT_SILENCE_DURATION: Duration = Duration::from_secs(20); +const MAXIMUM_SILENCE_DURATION: Duration = Duration::from_millis(68719476734); + +/// Represents a general-purpose set of properties tied with a client instance. +#[derive(Clone, Debug)] +pub struct ClientConfig { + /// Strategy to use when reconnecting to a server. + pub reconnect_strategy: ReconnectStrategy, + + /// A maximum duration to not receive any response/heartbeat from a server before deeming the + /// server as lost and triggering a reconnect. + pub silence_duration: Duration, +} + +impl ClientConfig { + pub fn with_maximum_silence_duration(self) -> Self { + Self { + reconnect_strategy: self.reconnect_strategy, + silence_duration: MAXIMUM_SILENCE_DURATION, + } + } +} + +impl Default for ClientConfig { + fn default() -> Self { + Self { + reconnect_strategy: ReconnectStrategy::Fail, + silence_duration: DEFAULT_SILENCE_DURATION, + } + } +} diff --git a/distant-net/src/client/reconnect.rs b/distant-net/src/client/reconnect.rs index 2162826..98e5670 100644 --- a/distant-net/src/client/reconnect.rs +++ b/distant-net/src/client/reconnect.rs @@ -1,4 +1,5 @@ use super::Reconnectable; +use log::*; use std::io; use std::time::Duration; use strum::Display; @@ -170,8 +171,11 @@ impl ReconnectStrategy { }; // If reconnect was successful, we're done and we can exit - if result.is_ok() { - return Ok(()); + match &result { + Ok(()) => return Ok(()), + Err(x) => { + error!("Failed to reconnect: {x}"); + } } // Decrement remaining retries if we have a limit diff --git a/distant-net/src/common/connection.rs b/distant-net/src/common/connection.rs index 64833dd..ff67daf 100644 --- a/distant-net/src/common/connection.rs +++ b/distant-net/src/common/connection.rs @@ -91,8 +91,8 @@ where /// /// For a client, this means performing an actual [`reconnect`] on the underlying /// [`Transport`], re-establishing an encrypted codec, submitting a request to the server to - /// reauthenticate using a previously-derived OTP, and refreshing the connection id and OTP for - /// use in a future reauthentication. + /// reauthenticate using a previously-derived OTP, and refreshing the OTP for use in a future + /// reauthentication. /// /// ### Server /// @@ -101,10 +101,10 @@ where /// [`reconnect`]: Reconnectable::reconnect async fn reconnect(&mut self) -> io::Result<()> { async fn reconnect_client( - id: &mut ConnectionId, - reauth_otp: &mut HeapSecretKey, + id: ConnectionId, + reauth_otp: HeapSecretKey, transport: &mut FramedTransport, - ) -> io::Result<()> { + ) -> io::Result<(ConnectionId, HeapSecretKey)> { // Re-establish a raw connection debug!("[Conn {id}] Re-establishing connection"); Reconnectable::reconnect(transport).await?; @@ -117,29 +117,16 @@ where debug!("[Conn {id}] Performing re-authentication"); transport .write_frame_for(&ConnectType::Reconnect { - id: *id, - otp: reauth_otp.unprotected_as_bytes().to_vec(), + id, + otp: reauth_otp.unprotected_into_bytes(), }) .await?; - // Receive the new id for the connection - // NOTE: If we fail re-authentication above, - // this will fail as the connection is dropped - debug!("[Conn {id}] Receiving new connection id"); - let new_id = transport - .read_frame_as::() - .await? - .ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Missing connection id frame") - })?; - debug!("[Conn {id}] Resetting id to {new_id}"); - *id = new_id; - // Derive an OTP for reauthentication debug!("[Conn {id}] Deriving future OTP for reauthentication"); - *reauth_otp = transport.exchange_keys().await?.into_heap_secret_key(); + let reauth_otp = transport.exchange_keys().await?.into_heap_secret_key(); - Ok(()) + Ok((id, reauth_otp)) } match self { @@ -148,19 +135,24 @@ where transport, reauth_otp, } => { - // Freeze our backup as we don't want the connection logic to alter it - transport.backup.freeze(); - - // Attempt to perform the reconnection and unfreeze our backup regardless of the - // result - let result = reconnect_client(id, reauth_otp, transport).await; - transport.backup.unfreeze(); - result?; + // Freeze our backup as we don't want the connection logic to alter it, attempt to + // perform the reconnection, and unfreeze our backup regardless of the result + let (new_id, new_reauth_otp) = { + transport.backup.freeze(); + let result = reconnect_client(*id, reauth_otp.clone(), transport).await; + transport.backup.unfreeze(); + result? + }; // Perform synchronization debug!("[Conn {id}] Synchronizing frame state"); transport.synchronize().await?; + // Everything has succeeded, so we now will update our id and reauth otp + info!("[Conn {id}] Reconnect completed successfully! Assigning new id {new_id}"); + *id = new_id; + *reauth_otp = new_reauth_otp; + Ok(()) } @@ -234,6 +226,7 @@ where debug!("[Conn {id}] Deriving future OTP for reauthentication"); let reauth_otp = transport.exchange_keys().await?.into_heap_secret_key(); + info!("[Conn {id}] Connect completed successfully!"); Ok(Self::Client { id, reauth_otp, @@ -283,7 +276,7 @@ where // Based on the connection type, we either try to find and validate an existing connection // or we perform normal verification - match connection_type { + let id = match connection_type { ConnectType::Connect => { // Communicate the connection id debug!("[Conn {id}] Telling other side to change connection id"); @@ -298,42 +291,69 @@ where let reauth_otp = transport.exchange_keys().await?.into_heap_secret_key(); // Store the id, OTP, and backup retrieval in our database + info!("[Conn {id}] Connect completed successfully!"); keychain.insert(id.to_string(), reauth_otp, rx).await; + + id } ConnectType::Reconnect { id: other_id, otp } => { let reauth_otp = HeapSecretKey::from(otp); debug!("[Conn {id}] Checking if {other_id} exists and has matching OTP"); match keychain - .remove_if_has_key(other_id.to_string(), reauth_otp) + .remove_if_has_key(other_id.to_string(), reauth_otp.clone()) .await { KeychainResult::Ok(x) => { - // Communicate the connection id - debug!("[Conn {id}] Telling other side to change connection id"); - transport.write_frame_for(&id).await?; - - // Derive an OTP for reauthentication - debug!("[Conn {id}] Deriving future OTP for reauthentication"); - let reauth_otp = transport.exchange_keys().await?.into_heap_secret_key(); + // Match found, so we want ot update our id to be the pre-existing id + debug!("[Conn {id}] Reassigning to {other_id}"); + let id = other_id; - // Grab the old backup and swap it into our transport + // Grab the old backup debug!("[Conn {id}] Acquiring backup for existing connection"); - match x.await { - Ok(backup) => { - transport.backup = backup; - } + let backup = match x.await { + Ok(backup) => backup, Err(_) => { - warn!("[Conn {id}] Missing backup"); + warn!("[Conn {id}] Missing backup, will use fresh copy"); + Backup::new() } + }; + + macro_rules! unwrap_or_fail { + ($action:expr) => { + unwrap_or_fail!(backup, $action) + }; + ($backup:expr, $action:expr) => {{ + match $action { + Ok(x) => x, + Err(x) => { + error!("[Conn {id}] Encountered error, restoring with old backup"); + let _ = tx.send($backup); + keychain.insert(id.to_string(), reauth_otp, rx).await; + return Err(x); + } + } + }}; } + // Derive an OTP for reauthentication + debug!("[Conn {id}] Deriving future OTP for reauthentication"); + let new_reauth_otp = + unwrap_or_fail!(transport.exchange_keys().await).into_heap_secret_key(); + + // Replace our backup with the old one + debug!("[Conn {id}] Restoring backup"); + transport.backup = backup; + // Synchronize using the provided backup debug!("[Conn {id}] Synchronizing frame state"); - transport.synchronize().await?; + unwrap_or_fail!(transport.backup, transport.synchronize().await); // Store the id, OTP, and backup retrieval in our database - keychain.insert(id.to_string(), reauth_otp, rx).await; + info!("[Conn {id}] Reconnect restoration completed successfully!"); + keychain.insert(id.to_string(), new_reauth_otp, rx).await; + + id } KeychainResult::InvalidPassword => { return Err(io::Error::new( @@ -349,7 +369,7 @@ where } } } - } + }; Ok(Self::Server { id, tx, transport }) } @@ -384,7 +404,6 @@ impl Connection { } } -#[cfg(test)] impl Connection { /// Returns the id of the connection. pub fn id(&self) -> ConnectionId { @@ -393,7 +412,10 @@ impl Connection { Self::Server { id, .. } => *id, } } +} +#[cfg(test)] +impl Connection { /// Returns the OTP associated with the connection, or none if connection is server-side. pub fn otp(&self) -> Option<&HeapSecretKey> { match self { @@ -821,9 +843,6 @@ mod tests { .await .unwrap(); - // Receive a new client id - let _id = t1.read_frame_as::().await.unwrap().unwrap(); - // Send garbage to fail the otp exchange t1.write_frame(Frame::new(b"hello")).await.unwrap(); @@ -862,9 +881,6 @@ mod tests { .await .unwrap(); - // Receive a new client id - let _id = t1.read_frame_as::().await.unwrap().unwrap(); - // Perform otp exchange let _otp = t1.exchange_keys().await.unwrap(); @@ -928,9 +944,10 @@ mod tests { let verifier = Verifier::none(); let keychain = Keychain::new(); let key = HeapSecretKey::generate(32).unwrap(); + let id = 1234; keychain - .insert(1234.to_string(), key.clone(), { + .insert(id.to_string(), key.clone(), { // Create a custom backup we'll use to replay frames from the server-side let mut backup = Backup::new(); @@ -968,9 +985,6 @@ mod tests { .await .unwrap(); - // Receive a new client id - let id = t1.read_frame_as::().await.unwrap().unwrap(); - // Perform otp exchange let otp = t1.exchange_keys().await.unwrap(); @@ -996,9 +1010,6 @@ mod tests { // Validate the connection ids match assert_eq!(server.id(), id); - // Check that our old connection id is no longer contained in the keychain - assert!(!keychain.has_id("1234").await, "Old OTP still exists"); - // Validate the OTP was stored in our keychain assert!( keychain @@ -1210,12 +1221,6 @@ mod tests { .await .expect("Failed to retrieve backup"); - // Send a new id back to the client connection - transport - .write_frame_for(&rand::random::()) - .await - .unwrap(); - // Perform key exchange let otp = transport.exchange_keys().await.unwrap(); diff --git a/distant-net/src/common/transport.rs b/distant-net/src/common/transport.rs index 41432c8..a1a1b6d 100644 --- a/distant-net/src/common/transport.rs +++ b/distant-net/src/common/transport.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use std::{io, time::Duration}; +use std::{fmt, io, time::Duration}; mod framed; pub use framed::*; @@ -42,7 +42,7 @@ pub trait Reconnectable { /// Interface representing a transport of raw bytes into and out of the system. #[async_trait] -pub trait Transport: Reconnectable + Send + Sync { +pub trait Transport: Reconnectable + fmt::Debug + Send + Sync { /// Tries to read data from the transport into the provided buffer, returning how many bytes /// were read. /// diff --git a/distant-net/src/common/transport/framed.rs b/distant-net/src/common/transport/framed.rs index fb57710..e173b22 100644 --- a/distant-net/src/common/transport/framed.rs +++ b/distant-net/src/common/transport/framed.rs @@ -254,12 +254,13 @@ impl FramedTransport { macro_rules! read_next_frame { () => {{ match Frame::read(&mut self.incoming) { - Ok(None) => (), - Ok(Some(frame)) => { - self.backup.increment_received_cnt(); + None => (), + Some(frame) => { + if frame.is_nonempty() { + self.backup.increment_received_cnt(); + } return Ok(Some(self.codec.decode(frame)?.into_owned())); } - Err(x) => return Err(x), } }}; } @@ -363,14 +364,17 @@ impl FramedTransport { // Encode the frame and store it in our outgoing queue self.codec .encode(frame.as_borrowed())? - .write(&mut self.outgoing)?; + .write(&mut self.outgoing); - // Once the frame enters our queue, we count it as written, even if it isn't fully flushed - self.backup.increment_sent_cnt(); + // Update tracking stats and more of backup if frame is nonempty + if frame.is_nonempty() { + // Once the frame enters our queue, we count it as written, even if it isn't fully flushed + self.backup.increment_sent_cnt(); - // Then we store the raw frame (non-encoded) for the future in case we need to retry - // sending it later (possibly with a different codec) - self.backup.push_frame(frame); + // Then we store the raw frame (non-encoded) for the future in case we need to retry + // sending it later (possibly with a different codec) + self.backup.push_frame(frame); + } // Attempt to write everything in our queue self.try_flush()?; @@ -535,7 +539,7 @@ impl FramedTransport { // Encode our frame and write it to be queued in our incoming data // NOTE: We have to do encoding here as incoming bytes are expected to be encoded - this.codec.encode(frame)?.write(&mut this.incoming)?; + this.codec.encode(frame)?.write(&mut this.incoming); } // Catch up our read count as we can have the case where the other side has a higher @@ -701,7 +705,7 @@ impl FramedTransport { })?; // Choose a compression and encryption option from the options - debug!("[{log_label}] Selecting from options: {options:#?}"); + debug!("[{log_label}] Selecting from options: {options:?}"); let choice = Choice { // Use preferred compression if available, otherwise default to no compression // to avoid choosing something poor @@ -725,7 +729,7 @@ impl FramedTransport { }; // Report back to the server the choice - debug!("[{log_label}] Reporting choice: {choice:#?}"); + debug!("[{log_label}] Reporting choice: {choice:?}"); self.write_frame_for(&choice).await?; choice @@ -740,7 +744,7 @@ impl FramedTransport { }; // Send options to the client - debug!("[{log_label}] Sending options: {options:#?}"); + debug!("[{log_label}] Sending options: {options:?}"); self.write_frame_for(&options).await?; // Get client's response with selected compression and encryption @@ -754,7 +758,7 @@ impl FramedTransport { } }; - debug!("[{log_label}] Building compression & encryption codecs based on {choice:#?}"); + debug!("[{log_label}] Building compression & encryption codecs based on {choice:?}"); let compression_level = choice.compression_level.unwrap_or_default(); // Acquire a codec for the compression type @@ -968,7 +972,7 @@ mod tests { let mut buf = BytesMut::new(); for frame in frames { - frame.write(&mut buf).unwrap(); + frame.write(&mut buf); } buf.to_vec() @@ -1059,7 +1063,7 @@ mod tests { fn try_read_frame_should_return_next_available_frame() { let data = { let mut data = BytesMut::new(); - Frame::new(b"hello world").write(&mut data).unwrap(); + Frame::new(b"hello world").write(&mut data); data.freeze() }; @@ -1082,8 +1086,8 @@ mod tests { // Store two frames in our data to transmit let data = { let mut data = BytesMut::new(); - Frame::new(b"hello world").write(&mut data).unwrap(); - Frame::new(b"hello again").write(&mut data).unwrap(); + Frame::new(b"hello world").write(&mut data); + Frame::new(b"hello again").write(&mut data); data.freeze() }; @@ -1746,8 +1750,8 @@ mod tests { let (mut t1, mut t2) = FramedTransport::pair(100); // Put some frames into the incoming and outgoing of our transport - Frame::new(b"bad incoming").write(&mut t2.incoming).unwrap(); - Frame::new(b"bad outgoing").write(&mut t2.outgoing).unwrap(); + Frame::new(b"bad incoming").write(&mut t2.incoming); + Frame::new(b"bad outgoing").write(&mut t2.outgoing); // Configure the backup such that we have sent two frames t2.backup.push_frame(Frame::new(b"hello")); diff --git a/distant-net/src/common/transport/framed/backup.rs b/distant-net/src/common/transport/framed/backup.rs index 4a09068..3888c96 100644 --- a/distant-net/src/common/transport/framed/backup.rs +++ b/distant-net/src/common/transport/framed/backup.rs @@ -5,6 +5,11 @@ use std::collections::VecDeque; const MAX_BACKUP_SIZE: usize = 256 * 1024 * 1024; /// Stores [`Frame`]s for reuse later. +/// +/// ### Note +/// +/// Empty [`Frame`]s are an exception and are not stored within the backup nor +/// are they tracked in terms of sent/received counts. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Backup { /// Maximum size (in bytes) to save frames in case we need to backup them diff --git a/distant-net/src/common/transport/framed/frame.rs b/distant-net/src/common/transport/framed/frame.rs index 47ffb7f..b92b96b 100644 --- a/distant-net/src/common/transport/framed/frame.rs +++ b/distant-net/src/common/transport/framed/frame.rs @@ -1,5 +1,5 @@ use bytes::{Buf, BufMut, BytesMut}; -use std::{borrow::Cow, io}; +use std::borrow::Cow; /// Represents a frame whose lifetime is static pub type OwnedFrame = Frame<'static>; @@ -13,7 +13,7 @@ pub struct Frame<'a> { } impl<'a> Frame<'a> { - /// Creates a new frame wrapping the `item` that will be shipped across the network + /// Creates a new frame wrapping the `item` that will be shipped across the network. pub fn new(item: &'a [u8]) -> Self { Self { item: Cow::Borrowed(item), @@ -27,75 +27,66 @@ impl<'a> Frame<'a> { } impl Frame<'_> { - /// Total bytes to use as the header field denoting a frame's size + /// Total bytes to use as the header field denoting a frame's size. pub const HEADER_SIZE: usize = 8; - /// Returns the len (in bytes) of the item wrapped by the frame + /// Creates a new frame with no item. + pub fn empty() -> Self { + Self::new(&[]) + } + + /// Returns the len (in bytes) of the item wrapped by the frame. pub fn len(&self) -> usize { self.item.len() } - /// Returns true if the frame is comprised of zero bytes + /// Returns true if the frame is comprised of zero bytes. pub fn is_empty(&self) -> bool { self.item.is_empty() } - /// Returns a reference to the bytes of the frame's item + /// Returns true if the frame is comprised of some bytes. + #[inline] + pub fn is_nonempty(&self) -> bool { + !self.is_empty() + } + + /// Returns a reference to the bytes of the frame's item. pub fn as_item(&self) -> &[u8] { &self.item } - /// Writes the frame to a new [`Vec`] of bytes, returning them on success - pub fn try_to_bytes(&self) -> io::Result> { + /// Writes the frame to a new [`Vec`] of bytes, returning them on success. + pub fn to_bytes(&self) -> Vec { let mut bytes = BytesMut::new(); - self.write(&mut bytes)?; - Ok(bytes.to_vec()) + self.write(&mut bytes); + bytes.to_vec() } /// Writes the frame to the end of `dst`, including the header representing the length of the - /// item as part of the written bytes - pub fn write(&self, dst: &mut BytesMut) -> io::Result<()> { - if self.item.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Empty item provided", - )); - } - + /// item as part of the written bytes. + pub fn write(&self, dst: &mut BytesMut) { dst.reserve(Self::HEADER_SIZE + self.item.len()); // Add data in form of {LEN}{ITEM} dst.put_u64((self.item.len()) as u64); dst.put_slice(&self.item); - - Ok(()) } /// Attempts to read a frame from `src`, returning `Some(Frame)` if a frame was found - /// (including the header) or `None` if the current `src` does not contain a frame - pub fn read(src: &mut BytesMut) -> io::Result> { + /// (including the header) or `None` if the current `src` does not contain a frame. + pub fn read(src: &mut BytesMut) -> Option { // First, check if we have more data than just our frame's message length if src.len() <= Self::HEADER_SIZE { - return Ok(None); + return None; } // Second, retrieve total size of our frame's message let item_len = u64::from_be_bytes(src[..Self::HEADER_SIZE].try_into().unwrap()) as usize; - // In the case that our item len is 0, we skip over the invalid frame - if item_len == 0 { - // Ensure we advance to remove the frame - src.advance(Self::HEADER_SIZE); - - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Frame's msg cannot have length of 0", - )); - } - // Third, check if we have all data for our frame; if not, exit early if src.len() < item_len + Self::HEADER_SIZE { - return Ok(None); + return None; } // Fourth, get and return our item @@ -104,13 +95,13 @@ impl Frame<'_> { // Fifth, advance so frame is no longer kept around src.advance(Self::HEADER_SIZE + item_len); - Ok(Some(Frame::from(item))) + Some(Frame::from(item)) } /// Checks if a full frame is available from `src`, returning true if a frame was found false /// if the current `src` does not contain a frame. Does not consume the frame. pub fn available(src: &BytesMut) -> bool { - matches!(Frame::read(&mut src.clone()), Ok(Some(_))) + matches!(Frame::read(&mut src.clone()), Some(_)) } /// Returns a new frame which is identical but has a lifetime tied to this frame. @@ -239,16 +230,15 @@ mod tests { use test_log::test; #[test] - fn write_should_fail_when_item_is_zero_bytes() { + fn write_should_succeed_when_item_is_zero_bytes() { let frame = Frame::new(&[]); let mut buf = BytesMut::new(); - let result = frame.write(&mut buf); + frame.write(&mut buf); - match result { - Err(x) if x.kind() == io::ErrorKind::InvalidInput => {} - x => panic!("Unexpected result: {:?}", x), - } + // Writing a frame of zero bytes means that the header is all zeros and there is + // no item that follows the header + assert_eq!(buf.as_ref(), &[0, 0, 0, 0, 0, 0, 0, 0]); } #[test] @@ -256,7 +246,7 @@ mod tests { let frame = Frame::new(b"hello, world"); let mut buf = BytesMut::new(); - frame.write(&mut buf).expect("Failed to write"); + frame.write(&mut buf); let len = buf.get_u64() as usize; assert_eq!(len, 12, "Wrong length writed"); @@ -269,11 +259,7 @@ mod tests { buf.put_bytes(0, Frame::HEADER_SIZE); let result = Frame::read(&mut buf); - assert!( - matches!(result, Ok(None)), - "Unexpected result: {:?}", - result - ); + assert!(matches!(result, None), "Unexpected result: {:?}", result); } #[test] @@ -282,24 +268,21 @@ mod tests { buf.put_u64(0); let result = Frame::read(&mut buf); - assert!( - matches!(result, Ok(None)), - "Unexpected result: {:?}", - result - ); + assert!(matches!(result, None), "Unexpected result: {:?}", result); } #[test] - fn read_should_fail_if_writed_item_length_is_zero() { + fn read_should_succeed_if_written_item_length_is_zero() { let mut buf = BytesMut::new(); buf.put_u64(0); buf.put_u8(255); - let result = Frame::read(&mut buf); - match result { - Err(x) if x.kind() == io::ErrorKind::InvalidData => {} - x => panic!("Unexpected result: {:?}", x), - } + // Reading will result in a frame of zero bytes + let frame = Frame::read(&mut buf).expect("missing frame"); + assert_eq!(frame, Frame::empty()); + + // Nothing following the frame header should have been extracted + assert_eq!(buf.as_ref(), &[255]); } #[test] @@ -308,10 +291,7 @@ mod tests { buf.put_u64(0); buf.put_bytes(0, 3); - assert!( - Frame::read(&mut buf).is_err(), - "read unexpectedly succeeded" - ); + assert_eq!(Frame::read(&mut buf).unwrap(), Frame::empty()); assert_eq!(buf.len(), 3, "Advanced an unexpected amount in src buf"); } @@ -319,25 +299,22 @@ mod tests { fn read_should_advance_src_by_frame_size_when_successful() { // Add 3 extra bytes after a full frame let mut buf = BytesMut::new(); - Frame::new(b"hello, world") - .write(&mut buf) - .expect("Failed to write"); + Frame::new(b"hello, world").write(&mut buf); buf.put_bytes(0, 3); - assert!(Frame::read(&mut buf).is_ok(), "read unexpectedly failed"); + assert!( + Frame::read(&mut buf).is_some(), + "read unexpectedly missing frame" + ); assert_eq!(buf.len(), 3, "Advanced an unexpected amount in src buf"); } #[test] fn read_should_return_some_byte_vec_when_successful() { let mut buf = BytesMut::new(); - Frame::new(b"hello, world") - .write(&mut buf) - .expect("Failed to write"); + Frame::new(b"hello, world").write(&mut buf); - let item = Frame::read(&mut buf) - .expect("Failed to read") - .expect("Item not properly captured"); + let item = Frame::read(&mut buf).expect("missing frame"); assert_eq!(item, b"hello, world"); } } diff --git a/distant-net/src/common/transport/test.rs b/distant-net/src/common/transport/test.rs index d5ac10a..0b28bf2 100644 --- a/distant-net/src/common/transport/test.rs +++ b/distant-net/src/common/transport/test.rs @@ -1,6 +1,6 @@ use super::{Interest, Ready, Reconnectable, Transport}; use async_trait::async_trait; -use std::io; +use std::{fmt, io}; pub type TryReadFn = Box io::Result + Send + Sync>; pub type TryWriteFn = Box io::Result + Send + Sync>; @@ -25,6 +25,12 @@ impl Default for TestTransport { } } +impl fmt::Debug for TestTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestTransport").finish() + } +} + #[async_trait] impl Reconnectable for TestTransport { async fn reconnect(&mut self) -> io::Result<()> { diff --git a/distant-net/src/manager/client.rs b/distant-net/src/manager/client.rs index 7142d86..30d9bbe 100644 --- a/distant-net/src/manager/client.rs +++ b/distant-net/src/manager/client.rs @@ -303,13 +303,13 @@ impl ManagerClient { #[cfg(test)] mod tests { use super::*; - use crate::client::{ReconnectStrategy, UntypedClient}; + use crate::client::UntypedClient; use crate::common::authentication::DummyAuthHandler; use crate::common::{Connection, InmemoryTransport, Request, Response}; fn setup() -> (ManagerClient, Connection) { let (client, server) = Connection::pair(100); - let client = UntypedClient::spawn(client, ReconnectStrategy::Fail).into_typed_client(); + let client = UntypedClient::spawn(client, Default::default()).into_typed_client(); (client, server) } diff --git a/distant-net/src/manager/client/channel.rs b/distant-net/src/manager/client/channel.rs index 0d01308..7271542 100644 --- a/distant-net/src/manager/client/channel.rs +++ b/distant-net/src/manager/client/channel.rs @@ -1,5 +1,5 @@ use crate::{ - client::{Client, ReconnectStrategy, UntypedClient}, + client::{Client, ClientConfig, UntypedClient}, common::{ConnectionId, FramedTransport, InmemoryTransport, UntypedRequest}, manager::data::{ManagerRequest, ManagerResponse}, }; @@ -35,7 +35,10 @@ impl RawChannel { T: Send + Sync + Serialize + 'static, U: Send + Sync + DeserializeOwned + 'static, { - Client::spawn_inmemory(self.transport, ReconnectStrategy::Fail) + Client::spawn_inmemory( + self.transport, + ClientConfig::default().with_maximum_silence_duration(), + ) } /// Consumes this channel, returning an untyped client wrapping the transport. @@ -46,7 +49,10 @@ impl RawChannel { /// performed during separate connection and this merely wraps an inmemory transport that maps /// to the primary connection. pub fn into_untyped_client(self) -> UntypedClient { - UntypedClient::spawn_inmemory(self.transport, ReconnectStrategy::Fail) + UntypedClient::spawn_inmemory( + self.transport, + ClientConfig::default().with_maximum_silence_duration(), + ) } /// Returns reference to the underlying framed transport. diff --git a/distant-net/src/manager/server.rs b/distant-net/src/manager/server.rs index 1dc2286..e2f3658 100644 --- a/distant-net/src/manager/server.rs +++ b/distant-net/src/manager/server.rs @@ -316,7 +316,7 @@ impl ServerHandler for ManagerServer { #[cfg(test)] mod tests { use super::*; - use crate::client::{ReconnectStrategy, UntypedClient}; + use crate::client::UntypedClient; use crate::common::FramedTransport; use crate::server::ServerReply; use crate::{boxed_connect_handler, boxed_launch_handler}; @@ -335,7 +335,7 @@ mod tests { /// Create an untyped client that is detached such that reads and writes will fail fn detached_untyped_client() -> UntypedClient { - UntypedClient::spawn_inmemory(FramedTransport::pair(1).0, ReconnectStrategy::Fail) + UntypedClient::spawn_inmemory(FramedTransport::pair(1).0, Default::default()) } /// Create a new server and authenticator diff --git a/distant-net/src/server.rs b/distant-net/src/server.rs index ea5fdae..4b1fbfd 100644 --- a/distant-net/src/server.rs +++ b/distant-net/src/server.rs @@ -1,9 +1,9 @@ -use crate::common::{authentication::Verifier, Listener, Transport}; +use crate::common::{authentication::Verifier, Listener, Response, Transport}; use async_trait::async_trait; use log::*; use serde::{de::DeserializeOwned, Serialize}; use std::{io, sync::Arc, time::Duration}; -use tokio::sync::RwLock; +use tokio::sync::{broadcast, RwLock}; mod builder; pub use builder::*; @@ -148,14 +148,20 @@ where L::Output: Transport + 'static, { let state = Arc::new(ServerState::new()); - let task = tokio::spawn(self.task(Arc::clone(&state), listener)); + let (tx, rx) = broadcast::channel(1); + let task = tokio::spawn(self.task(Arc::clone(&state), listener, tx.clone(), rx)); - Ok(Box::new(GenericServerRef { state, task })) + Ok(Box::new(GenericServerRef { shutdown: tx, task })) } /// Internal task that is run to receive connections and spawn connection tasks - async fn task(self, state: Arc, mut listener: L) - where + async fn task( + self, + state: Arc>>, + mut listener: L, + shutdown_tx: broadcast::Sender<()>, + shutdown_rx: broadcast::Receiver<()>, + ) where L: Listener + 'static, L::Output: Transport + 'static, { @@ -171,6 +177,7 @@ where let timer = Arc::new(RwLock::new(timer)); let verifier = Arc::new(verifier); + let mut connection_tasks = Vec::new(); loop { // Receive a new connection, exiting if no longer accepting connections or if the shutdown // signal has been received @@ -191,10 +198,7 @@ where config.shutdown.duration().unwrap_or_default().as_secs_f32(), ); - for (id, task) in state.connections.write().await.drain() { - info!("Terminating task {id}"); - task.abort(); - } + let _ = shutdown_tx.send(()); break; } @@ -203,26 +207,28 @@ where // Ensure that the shutdown timer is cancelled now that we have a connection timer.read().await.stop(); - let connection = ConnectionTask::build() - .handler(Arc::downgrade(&handler)) - .state(Arc::downgrade(&state)) - .keychain(state.keychain.clone()) - .transport(transport) - .shutdown_timer(Arc::downgrade(&timer)) - .sleep_duration(config.connection_sleep) - .verifier(Arc::downgrade(&verifier)) - .spawn(); - - state - .connections - .write() - .await - .insert(connection.id(), connection); + connection_tasks.push( + ConnectionTask::build() + .handler(Arc::downgrade(&handler)) + .state(Arc::downgrade(&state)) + .keychain(state.keychain.clone()) + .transport(transport) + .shutdown(shutdown_rx.resubscribe()) + .shutdown_timer(Arc::downgrade(&timer)) + .sleep_duration(config.connection_sleep) + .heartbeat_duration(config.connection_heartbeat) + .verifier(Arc::downgrade(&verifier)) + .spawn(), + ); } // Once we stop listening, we still want to wait until all connections have terminated info!("Server waiting for active connections to terminate"); - while state.has_active_connections().await { + loop { + connection_tasks.retain(|task| !task.is_finished()); + if connection_tasks.is_empty() { + break; + } tokio::time::sleep(Duration::from_millis(50)).await; } info!("Server task terminated"); diff --git a/distant-net/src/server/config.rs b/distant-net/src/server/config.rs index 5bdb098..b4a4975 100644 --- a/distant-net/src/server/config.rs +++ b/distant-net/src/server/config.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::{num::ParseFloatError, str::FromStr, time::Duration}; const DEFAULT_CONNECTION_SLEEP: Duration = Duration::from_millis(1); +const DEFAULT_HEARTBEAT_DURATION: Duration = Duration::from_secs(5); /// Represents a general-purpose set of properties tied with a server instance #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -10,6 +11,9 @@ pub struct ServerConfig { /// Time to wait inbetween connection read/write when nothing was read or written on last pass pub connection_sleep: Duration, + /// Minimum time to wait inbetween sending heartbeat messages + pub connection_heartbeat: Duration, + /// Rules for how a server will shutdown automatically pub shutdown: Shutdown, } @@ -18,6 +22,7 @@ impl Default for ServerConfig { fn default() -> Self { Self { connection_sleep: DEFAULT_CONNECTION_SLEEP, + connection_heartbeat: DEFAULT_HEARTBEAT_DURATION, shutdown: Default::default(), } } diff --git a/distant-net/src/server/connection.rs b/distant-net/src/server/connection.rs index 8899ad9..c525267 100644 --- a/distant-net/src/server/connection.rs +++ b/distant-net/src/server/connection.rs @@ -1,7 +1,10 @@ -use super::{ConnectionCtx, ServerCtx, ServerHandler, ServerReply, ServerState, ShutdownTimer}; +use super::{ + ConnectionCtx, ConnectionState, ServerCtx, ServerHandler, ServerReply, ServerState, + ShutdownTimer, +}; use crate::common::{ authentication::{Keychain, Verifier}, - Backup, Connection, ConnectionId, Interest, Response, Transport, UntypedRequest, + Backup, Connection, Frame, Interest, Response, Transport, UntypedRequest, }; use log::*; use serde::{de::DeserializeOwned, Serialize}; @@ -11,56 +14,33 @@ use std::{ pin::Pin, sync::{Arc, Weak}, task::{Context, Poll}, - time::Duration, + time::{Duration, Instant}, }; use tokio::{ - sync::{mpsc, oneshot, RwLock}, + sync::{broadcast, mpsc, oneshot, RwLock}, task::JoinHandle, }; pub type ServerKeychain = Keychain>; -/// Time to wait inbetween connection read/write when nothing was read or written on last pass +/// Time to wait inbetween connection read/write when nothing was read or written on last pass. const SLEEP_DURATION: Duration = Duration::from_millis(1); -/// Represents an individual connection on the server -pub struct ConnectionTask { - /// Unique identifier tied to the connection - id: ConnectionId, +/// Minimum time between heartbeats to communicate to the client connection. +const MINIMUM_HEARTBEAT_DURATION: Duration = Duration::from_secs(5); - /// Task that is processing requests and responses - task: JoinHandle>, -} +/// Represents an individual connection on the server. +pub(super) struct ConnectionTask(JoinHandle>); impl ConnectionTask { /// Starts building a new connection - pub fn build() -> ConnectionTaskBuilder<(), ()> { - let id: ConnectionId = rand::random(); - ConnectionTaskBuilder { - id, - handler: Weak::new(), - state: Weak::new(), - keychain: Keychain::new(), - transport: (), - shutdown_timer: Weak::new(), - sleep_duration: SLEEP_DURATION, - verifier: Weak::new(), - } - } - - /// Returns the id associated with the connection - pub fn id(&self) -> ConnectionId { - self.id + pub fn build() -> ConnectionTaskBuilder<(), (), ()> { + ConnectionTaskBuilder::new() } /// Returns true if the task has finished pub fn is_finished(&self) -> bool { - self.task.is_finished() - } - - /// Aborts the connection - pub fn abort(&self) { - self.task.abort(); + self.0.is_finished() } } @@ -68,7 +48,7 @@ impl Future for ConnectionTask { type Output = io::Result<()>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match Future::poll(Pin::new(&mut self.task), cx) { + match Future::poll(Pin::new(&mut self.0), cx) { Poll::Pending => Poll::Pending, Poll::Ready(x) => match x { Ok(x) => Poll::Ready(x), @@ -78,114 +58,171 @@ impl Future for ConnectionTask { } } -pub struct ConnectionTaskBuilder { - id: ConnectionId, +/// Represents a builder for a new connection task. +pub(super) struct ConnectionTaskBuilder { handler: Weak, - state: Weak, + state: Weak>, keychain: Keychain>, transport: T, + shutdown: broadcast::Receiver<()>, shutdown_timer: Weak>, sleep_duration: Duration, + heartbeat_duration: Duration, verifier: Weak, } -impl ConnectionTaskBuilder { - pub fn handler(self, handler: Weak) -> ConnectionTaskBuilder { +impl ConnectionTaskBuilder<(), (), ()> { + /// Starts building a new connection. + pub fn new() -> Self { + Self { + handler: Weak::new(), + state: Weak::new(), + keychain: Keychain::new(), + transport: (), + shutdown: broadcast::channel(1).1, + shutdown_timer: Weak::new(), + sleep_duration: SLEEP_DURATION, + heartbeat_duration: MINIMUM_HEARTBEAT_DURATION, + verifier: Weak::new(), + } + } +} + +impl ConnectionTaskBuilder { + pub fn handler(self, handler: Weak) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler, state: self.state, keychain: self.keychain, transport: self.transport, + shutdown: self.shutdown, shutdown_timer: self.shutdown_timer, sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, } } - pub fn state(self, state: Weak) -> ConnectionTaskBuilder { + pub fn state(self, state: Weak>) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler: self.handler, state, keychain: self.keychain, transport: self.transport, + shutdown: self.shutdown, shutdown_timer: self.shutdown_timer, sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, } } - pub fn keychain(self, keychain: ServerKeychain) -> ConnectionTaskBuilder { + pub fn keychain(self, keychain: ServerKeychain) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler: self.handler, state: self.state, keychain, transport: self.transport, + shutdown: self.shutdown, shutdown_timer: self.shutdown_timer, sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, } } - pub fn transport(self, transport: U) -> ConnectionTaskBuilder { + pub fn transport(self, transport: U) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler: self.handler, keychain: self.keychain, state: self.state, transport, + shutdown: self.shutdown, + shutdown_timer: self.shutdown_timer, + sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, + verifier: self.verifier, + } + } + + pub fn shutdown(self, shutdown: broadcast::Receiver<()>) -> ConnectionTaskBuilder { + ConnectionTaskBuilder { + handler: self.handler, + state: self.state, + keychain: self.keychain, + transport: self.transport, + shutdown, shutdown_timer: self.shutdown_timer, sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, } } - pub(crate) fn shutdown_timer( + pub fn shutdown_timer( self, shutdown_timer: Weak>, - ) -> ConnectionTaskBuilder { + ) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler: self.handler, state: self.state, keychain: self.keychain, transport: self.transport, + shutdown: self.shutdown, shutdown_timer, sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, } } - pub fn sleep_duration(self, sleep_duration: Duration) -> ConnectionTaskBuilder { + pub fn sleep_duration(self, sleep_duration: Duration) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler: self.handler, state: self.state, keychain: self.keychain, transport: self.transport, + shutdown: self.shutdown, shutdown_timer: self.shutdown_timer, sleep_duration, + heartbeat_duration: self.heartbeat_duration, + verifier: self.verifier, + } + } + + pub fn heartbeat_duration( + self, + heartbeat_duration: Duration, + ) -> ConnectionTaskBuilder { + ConnectionTaskBuilder { + handler: self.handler, + state: self.state, + keychain: self.keychain, + transport: self.transport, + shutdown: self.shutdown, + shutdown_timer: self.shutdown_timer, + sleep_duration: self.sleep_duration, + heartbeat_duration, verifier: self.verifier, } } - pub fn verifier(self, verifier: Weak) -> ConnectionTaskBuilder { + pub fn verifier(self, verifier: Weak) -> ConnectionTaskBuilder { ConnectionTaskBuilder { - id: self.id, handler: self.handler, state: self.state, keychain: self.keychain, transport: self.transport, + shutdown: self.shutdown, shutdown_timer: self.shutdown_timer, sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, verifier, } } } -impl ConnectionTaskBuilder +impl ConnectionTaskBuilder, T> where H: ServerHandler + Sync + 'static, H::Request: DeserializeOwned + Send + Sync + 'static, @@ -194,52 +231,86 @@ where T: Transport + 'static, { pub fn spawn(self) -> ConnectionTask { - let id = self.id; - - ConnectionTask { - id, - task: tokio::spawn(self.run()), - } + ConnectionTask(tokio::spawn(self.run())) } async fn run(self) -> io::Result<()> { let ConnectionTaskBuilder { - id, handler, state, keychain, transport, + mut shutdown, shutdown_timer, sleep_duration, + heartbeat_duration, verifier, } = self; + // NOTE: This exists purely to make the compiler happy for macro_rules declaration order. + let (mut local_shutdown, channel_tx, connection_state) = ConnectionState::channel(); + // Will check if no more connections and restart timer if that's the case macro_rules! terminate_connection { - // Prints an error message before terminating the connection by panicking - (@error $($msg:tt)+) => { + // Prints an error message and does not store state + (@fatal $($msg:tt)+) => { error!($($msg)+); terminate_connection!(); return Err(io::Error::new(io::ErrorKind::Other, format!($($msg)+))); }; - // Prints a debug message before terminating the connection by cleanly returning - (@debug $($msg:tt)+) => { + // Prints an error message and stores state before terminating + (@error($tx:ident, $rx:ident) $($msg:tt)+) => { + error!($($msg)+); + terminate_connection!($tx, $rx); + return Err(io::Error::new(io::ErrorKind::Other, format!($($msg)+))); + }; + + // Prints a debug message and stores state before terminating + (@debug($tx:ident, $rx:ident) $($msg:tt)+) => { debug!($($msg)+); + terminate_connection!($tx, $rx); + return Ok(()); + }; + + // Prints a shutdown message with no connection id and exit without sending state + (@shutdown) => { + debug!("Shutdown triggered before a connection could be fully established"); + terminate_connection!(); + return Ok(()); + }; + + // Prints a shutdown message with no connection id and stores state before terminating + (@shutdown) => { + debug!("Shutdown triggered before a connection could be fully established"); terminate_connection!(); return Ok(()); }; + // Prints a shutdown message and stores state before terminating + (@shutdown($id:ident, $tx:ident, $rx:ident)) => {{ + debug!("[Conn {}] Shutdown triggered", $id); + terminate_connection!($tx, $rx); + return Ok(()); + }}; + + // Performs the connection termination by removing it from server state and + // restarting the shutdown timer if it was the last connection + ($tx:ident, $rx:ident) => { + // Send the channels back + let _ = channel_tx.send(($tx, $rx)); + + terminate_connection!(); + }; + // Performs the connection termination by removing it from server state and // restarting the shutdown timer if it was the last connection () => { - // Remove the connection from our state if it has closed + // Restart our shutdown timer if this is the last connection if let Some(state) = Weak::upgrade(&state) { - state.connections.write().await.remove(&self.id); - - // If we have no more connections, start the timer if let Some(timer) = Weak::upgrade(&shutdown_timer) { - if state.connections.read().await.is_empty() { + if state.connections.read().await.values().filter(|conn| !conn.is_finished()).count() <= 1 { + debug!("Last connection terminating, so restarting shutdown timer"); timer.write().await.restart(); } } @@ -247,58 +318,160 @@ where }; } + /// Awaits a future to complete, or detects if a signal was received by either the global + /// or local shutdown channel. Shutdown only occurs if a signal was received, and any + /// errors received by either shutdown channel are ignored. + macro_rules! await_or_shutdown { + ($(@save($id:ident, $tx:ident, $rx:ident))? $future:expr) => {{ + let mut f = $future; + + loop { + let use_shutdown = match shutdown.try_recv() { + Ok(_) => { + terminate_connection!(@shutdown $(($id, $tx, $rx))?); + } + Err(broadcast::error::TryRecvError::Empty) => true, + Err(broadcast::error::TryRecvError::Lagged(_)) => true, + Err(broadcast::error::TryRecvError::Closed) => false, + }; + + let use_local_shutdown = match local_shutdown.try_recv() { + Ok(_) => { + terminate_connection!(@shutdown $(($id, $tx, $rx))?); + } + Err(oneshot::error::TryRecvError::Empty) => true, + Err(oneshot::error::TryRecvError::Closed) => false, + }; + + if use_shutdown && use_local_shutdown { + tokio::select! { + x = shutdown.recv() => { + if x.is_err() { + continue; + } + + terminate_connection!(@shutdown $(($id, $tx, $rx))?); + } + x = &mut local_shutdown => { + if x.is_err() { + continue; + } + + terminate_connection!(@shutdown $(($id, $tx, $rx))?); + } + x = &mut f => { break x; } + } + } else if use_shutdown { + tokio::select! { + x = shutdown.recv() => { + if x.is_err() { + continue; + } + + terminate_connection!(@shutdown $(($id, $tx, $rx))?); + } + x = &mut f => { break x; } + } + } else if use_local_shutdown { + tokio::select! { + x = &mut local_shutdown => { + if x.is_err() { + continue; + } + + terminate_connection!(@shutdown $(($id, $tx, $rx))?); + } + x = &mut f => { break x; } + } + } else { + break f.await; + } + } + }}; + } + + // Attempt to upgrade our handler for use with the connection going forward + let handler = match Weak::upgrade(&handler) { + Some(handler) => handler, + None => { + terminate_connection!(@fatal "Failed to setup connection because handler dropped"); + } + }; + + // Attempt to upgrade our state for use with the connection going forward + let state = match Weak::upgrade(&state) { + Some(state) => state, + None => { + terminate_connection!(@fatal "Failed to setup connection because state dropped"); + } + }; + // Properly establish the connection's transport - debug!("[Conn {id}] Establishing full connection"); + debug!("Establishing full connection using {transport:?}"); let mut connection = match Weak::upgrade(&verifier) { Some(verifier) => { - match Connection::server(transport, verifier.as_ref(), keychain).await { + match await_or_shutdown!(Box::pin(Connection::server( + transport, + verifier.as_ref(), + keychain + ))) { Ok(connection) => connection, Err(x) => { - terminate_connection!(@error "[Conn {id}] Failed to setup connection: {x}"); + terminate_connection!(@fatal "Failed to setup connection: {x}"); } } } None => { - terminate_connection!(@error "[Conn {id}] Verifier has been dropped"); - } - }; - - // Attempt to upgrade our handler for use with the connection going forward - debug!("[Conn {id}] Preparing connection handler"); - let handler = match Weak::upgrade(&handler) { - Some(handler) => handler, - None => { - terminate_connection!(@error "[Conn {id}] Handler has been dropped"); + terminate_connection!(@fatal "Verifier has been dropped"); } }; - // Construct a queue of outgoing responses - let (tx, mut rx) = mpsc::channel::>(1); + // Update our id to be the connection id + let id = connection.id(); // Create local data for the connection and then process it debug!("[Conn {id}] Officially accepting connection"); let mut local_data = H::LocalData::default(); - if let Err(x) = handler - .on_accept(ConnectionCtx { - connection_id: id, - local_data: &mut local_data, - }) - .await - { - terminate_connection!(@error "[Conn {id}] Accepting connection failed: {x}"); + if let Err(x) = await_or_shutdown!(handler.on_accept(ConnectionCtx { + connection_id: id, + local_data: &mut local_data + })) { + terminate_connection!(@fatal "[Conn {id}] Accepting connection failed: {x}"); } let local_data = Arc::new(local_data); + let mut last_heartbeat = Instant::now(); + + // Restore our connection's channels if we have them, otherwise make new ones + let (tx, mut rx) = match state.connections.write().await.remove(&id) { + Some(conn) => match conn.shutdown_and_wait().await { + Some(x) => { + debug!("[Conn {id}] Marked as existing connection"); + x + } + None => { + warn!("[Conn {id}] Existing connection with id, but channels not saved"); + mpsc::channel::>(1) + } + }, + None => { + debug!("[Conn {id}] Marked as new connection"); + mpsc::channel::>(1) + } + }; + + // Store our connection details + state.connections.write().await.insert(id, connection_state); debug!("[Conn {id}] Beginning read/write loop"); loop { - let ready = match connection - .ready(Interest::READABLE | Interest::WRITABLE) - .await - { + let ready = match await_or_shutdown!( + @save(id, tx, rx) + Box::pin(connection.ready(Interest::READABLE | Interest::WRITABLE)) + ) { Ok(ready) => ready, Err(x) => { - terminate_connection!(@error "[Conn {id}] Failed to examine ready state: {x}"); + terminate_connection!(@error(tx, rx) "[Conn {id}] Failed to examine ready state: {x}"); } }; @@ -311,15 +484,14 @@ where Ok(Some(frame)) => match UntypedRequest::from_slice(frame.as_item()) { Ok(request) => match request.to_typed_request() { Ok(request) => { - let reply = ServerReply { - origin_id: request.id.clone(), - tx: tx.clone(), - }; - + let origin_id = request.id.clone(); let ctx = ServerCtx { connection_id: id, request, - reply: reply.clone(), + reply: ServerReply { + origin_id, + tx: tx.clone(), + }, local_data: Arc::clone(&local_data), }; @@ -344,11 +516,11 @@ where } }, Ok(None) => { - terminate_connection!(@debug "[Conn {id}] Connection closed"); + terminate_connection!(@debug(tx, rx) "[Conn {id}] Connection closed"); } Err(x) if x.kind() == io::ErrorKind::WouldBlock => read_blocked = true, Err(x) => { - terminate_connection!(@error "[Conn {id}] {x}"); + terminate_connection!(@error(tx, rx) "[Conn {id}] {x}"); } } } @@ -356,10 +528,20 @@ where // If our socket is ready to be written to, we try to get the next item from // the queue and process it if ready.is_writable() { + // Send a heartbeat if we have exceeded our last time + if last_heartbeat.elapsed() >= heartbeat_duration { + trace!("[Conn {id}] Sending heartbeat via empty frame"); + match connection.try_write_frame(Frame::empty()) { + Ok(()) => (), + Err(x) if x.kind() == io::ErrorKind::WouldBlock => write_blocked = true, + Err(x) => error!("[Conn {id}] Send failed: {x}"), + } + last_heartbeat = Instant::now(); + } // If we get more data to write, attempt to write it, which will result in writing // any queued bytes as well. Othewise, we attempt to flush any pending outgoing // bytes that weren't sent earlier. - if let Ok(response) = rx.try_recv() { + else if let Ok(response) = rx.try_recv() { // Log our message as a string, which can be expensive if log_enabled!(Level::Trace) { trace!( @@ -541,7 +723,7 @@ mod tests { let err = task.await.unwrap_err(); assert!( - err.to_string().contains("Handler has been dropped"), + err.to_string().contains("handler dropped"), "Unexpected error: {err}" ); } @@ -610,6 +792,7 @@ mod tests { let shutdown_timer = Arc::new(RwLock::new(ShutdownTimer::start(Shutdown::Never))); let verifier = Arc::new(Verifier::none()); + #[derive(Debug)] struct FakeTransport { inner: InmemoryTransport, fail_ready: Arc, @@ -678,7 +861,7 @@ mod tests { let err = task.await.unwrap_err(); assert!( - err.to_string().contains("Failed to examine ready state"), + err.to_string().contains("targeted ready failure"), "Unexpected error: {err}" ); } @@ -722,7 +905,7 @@ mod tests { let shutdown_timer = Arc::new(RwLock::new(ShutdownTimer::start(Shutdown::Never))); let verifier = Arc::new(Verifier::none()); - ConnectionTask::build() + let _conn = ConnectionTask::build() .handler(Arc::downgrade(&handler)) .state(Arc::downgrade(&state)) .keychain(keychain) @@ -748,4 +931,205 @@ mod tests { let response = task.await.unwrap(); assert_eq!(response.payload, "hello"); } + + #[test(tokio::test)] + async fn should_send_heartbeat_via_empty_frame_every_minimum_duration() { + let handler = Arc::new(TestServerHandler); + let state = Arc::new(ServerState::default()); + let keychain = ServerKeychain::new(); + let (t1, t2) = InmemoryTransport::pair(100); + let shutdown_timer = Arc::new(RwLock::new(ShutdownTimer::start(Shutdown::Never))); + let verifier = Arc::new(Verifier::none()); + + let _conn = ConnectionTask::build() + .handler(Arc::downgrade(&handler)) + .state(Arc::downgrade(&state)) + .keychain(keychain) + .transport(t1) + .shutdown_timer(Arc::downgrade(&shutdown_timer)) + .heartbeat_duration(Duration::from_millis(200)) + .verifier(Arc::downgrade(&verifier)) + .spawn(); + + // Spawn a task to handle establishing connection from client-side + let task = tokio::spawn(async move { + let mut client = Connection::client(t2, DummyAuthHandler) + .await + .expect("Fail to establish client-side connection"); + + // Verify we don't get a frame immediately + assert_eq!( + client.try_read_frame().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "got a frame early" + ); + + // Sleep more than our minimum heartbeat duration to ensure we get one + tokio::time::sleep(Duration::from_millis(250)).await; + assert_eq!( + client.read_frame().await.unwrap().unwrap(), + Frame::empty(), + "non-empty frame" + ); + + // Verify we don't get a frame immediately + assert_eq!( + client.try_read_frame().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "got a frame early" + ); + + // Sleep more than our minimum heartbeat duration to ensure we get one + tokio::time::sleep(Duration::from_millis(250)).await; + assert_eq!( + client.read_frame().await.unwrap().unwrap(), + Frame::empty(), + "non-empty frame" + ); + }); + + task.await.unwrap(); + } + + #[test(tokio::test)] + async fn should_be_able_to_shutdown_while_establishing_connection() { + let handler = Arc::new(TestServerHandler); + let state = Arc::new(ServerState::default()); + let keychain = ServerKeychain::new(); + let (t1, _t2) = InmemoryTransport::pair(100); + let shutdown_timer = Arc::new(RwLock::new(ShutdownTimer::start(Shutdown::Never))); + let verifier = Arc::new(Verifier::none()); + + let (shutdown_tx, shutdown_rx) = broadcast::channel(1); + let conn = ConnectionTask::build() + .handler(Arc::downgrade(&handler)) + .state(Arc::downgrade(&state)) + .keychain(keychain) + .transport(t1) + .shutdown(shutdown_rx) + .shutdown_timer(Arc::downgrade(&shutdown_timer)) + .heartbeat_duration(Duration::from_millis(200)) + .verifier(Arc::downgrade(&verifier)) + .spawn(); + + // Shutdown server connection task while it is establishing a full connection with the + // client, verifying that we do not get an error in return + shutdown_tx + .send(()) + .expect("Failed to send shutdown signal"); + conn.await.unwrap(); + } + + #[test(tokio::test)] + async fn should_be_able_to_shutdown_while_accepting_connection() { + struct HangingAcceptServerHandler; + + #[async_trait] + impl ServerHandler for HangingAcceptServerHandler { + type Request = (); + type Response = (); + type LocalData = (); + + async fn on_accept(&self, _: ConnectionCtx<'_, Self::LocalData>) -> io::Result<()> { + // Wait "forever" so we can ensure that we fail at this step + tokio::time::sleep(Duration::MAX).await; + Err(io::Error::new(io::ErrorKind::Other, "bad accept")) + } + + async fn on_request( + &self, + _: ServerCtx, + ) { + unreachable!(); + } + } + + let handler = Arc::new(HangingAcceptServerHandler); + let state = Arc::new(ServerState::default()); + let keychain = ServerKeychain::new(); + let (t1, t2) = InmemoryTransport::pair(100); + let shutdown_timer = Arc::new(RwLock::new(ShutdownTimer::start(Shutdown::Never))); + let verifier = Arc::new(Verifier::none()); + + let (shutdown_tx, shutdown_rx) = broadcast::channel(1); + let conn = ConnectionTask::build() + .handler(Arc::downgrade(&handler)) + .state(Arc::downgrade(&state)) + .keychain(keychain) + .transport(t1) + .shutdown(shutdown_rx) + .shutdown_timer(Arc::downgrade(&shutdown_timer)) + .heartbeat_duration(Duration::from_millis(200)) + .verifier(Arc::downgrade(&verifier)) + .spawn(); + + // Spawn a task to handle the client-side establishment of a full connection + let _client_task = tokio::spawn(Connection::client(t2, DummyAuthHandler)); + + // Shutdown server connection task while it is accepting the connection, verifying that we + // do not get an error in return + shutdown_tx + .send(()) + .expect("Failed to send shutdown signal"); + conn.await.unwrap(); + } + + #[test(tokio::test)] + async fn should_be_able_to_shutdown_while_waiting_for_connection_to_be_ready() { + struct AcceptServerHandler { + tx: mpsc::Sender<()>, + } + + #[async_trait] + impl ServerHandler for AcceptServerHandler { + type Request = (); + type Response = (); + type LocalData = (); + + async fn on_accept(&self, _: ConnectionCtx<'_, Self::LocalData>) -> io::Result<()> { + self.tx.send(()).await.unwrap(); + Ok(()) + } + + async fn on_request( + &self, + _: ServerCtx, + ) { + unreachable!(); + } + } + + let (tx, mut rx) = mpsc::channel(100); + let handler = Arc::new(AcceptServerHandler { tx }); + let state = Arc::new(ServerState::default()); + let keychain = ServerKeychain::new(); + let (t1, t2) = InmemoryTransport::pair(100); + let shutdown_timer = Arc::new(RwLock::new(ShutdownTimer::start(Shutdown::Never))); + let verifier = Arc::new(Verifier::none()); + + let (shutdown_tx, shutdown_rx) = broadcast::channel(1); + let conn = ConnectionTask::build() + .handler(Arc::downgrade(&handler)) + .state(Arc::downgrade(&state)) + .keychain(keychain) + .transport(t1) + .shutdown(shutdown_rx) + .shutdown_timer(Arc::downgrade(&shutdown_timer)) + .heartbeat_duration(Duration::from_millis(200)) + .verifier(Arc::downgrade(&verifier)) + .spawn(); + + // Spawn a task to handle the client-side establishment of a full connection + let _client_task = tokio::spawn(Connection::client(t2, DummyAuthHandler)); + + // Wait to ensure we complete the accept call first + let _ = rx.recv().await; + + // Shutdown server connection task while it is accepting the connection, verifying that we + // do not get an error in return + shutdown_tx + .send(()) + .expect("Failed to send shutdown signal"); + conn.await.unwrap(); + } } diff --git a/distant-net/src/server/ref.rs b/distant-net/src/server/ref.rs index a693d19..f8f0fc5 100644 --- a/distant-net/src/server/ref.rs +++ b/distant-net/src/server/ref.rs @@ -1,23 +1,21 @@ -use super::ServerState; use crate::common::AsAny; -use log::*; use std::{ future::Future, io, pin::Pin, - sync::Arc, task::{Context, Poll}, time::Duration, }; +use tokio::sync::broadcast; use tokio::task::{JoinError, JoinHandle}; -/// Interface to engage with a server instance +/// Interface to engage with a server instance. pub trait ServerRef: AsAny + Send { - /// Returns true if the server is no longer running + /// Returns true if the server is no longer running. fn is_finished(&self) -> bool; - /// Kills the internal task processing new inbound requests - fn abort(&self); + /// Sends a shutdown signal to the server. + fn shutdown(&self); fn wait(self) -> Pin>>> where @@ -64,7 +62,7 @@ impl dyn ServerRef { /// Represents a generic reference to a server pub struct GenericServerRef { - pub(crate) state: Arc, + pub(crate) shutdown: broadcast::Sender<()>, pub(crate) task: JoinHandle<()>, } @@ -74,16 +72,8 @@ impl ServerRef for GenericServerRef { self.task.is_finished() } - fn abort(&self) { - self.task.abort(); - - let state = Arc::clone(&self.state); - tokio::spawn(async move { - for (id, connection) in state.connections.read().await.iter() { - debug!("Aborting connection {}", id); - connection.abort(); - } - }); + fn shutdown(&self) { + let _ = self.shutdown.send(()); } fn wait(self) -> Pin>>> diff --git a/distant-net/src/server/ref/tcp.rs b/distant-net/src/server/ref/tcp.rs index 80f4552..c6bc81f 100644 --- a/distant-net/src/server/ref/tcp.rs +++ b/distant-net/src/server/ref/tcp.rs @@ -29,7 +29,7 @@ impl ServerRef for TcpServerRef { self.inner.is_finished() } - fn abort(&self) { - self.inner.abort(); + fn shutdown(&self) { + self.inner.shutdown(); } } diff --git a/distant-net/src/server/ref/unix.rs b/distant-net/src/server/ref/unix.rs index 8642cea..120ff31 100644 --- a/distant-net/src/server/ref/unix.rs +++ b/distant-net/src/server/ref/unix.rs @@ -28,7 +28,7 @@ impl ServerRef for UnixSocketServerRef { self.inner.is_finished() } - fn abort(&self) { - self.inner.abort(); + fn shutdown(&self) { + self.inner.shutdown(); } } diff --git a/distant-net/src/server/ref/windows.rs b/distant-net/src/server/ref/windows.rs index 4c29762..df59ced 100644 --- a/distant-net/src/server/ref/windows.rs +++ b/distant-net/src/server/ref/windows.rs @@ -28,7 +28,7 @@ impl ServerRef for WindowsPipeServerRef { self.inner.is_finished() } - fn abort(&self) { - self.inner.abort(); + fn shutdown(&self) { + self.inner.shutdown(); } } diff --git a/distant-net/src/server/state.rs b/distant-net/src/server/state.rs index bbd1a07..cba27be 100644 --- a/distant-net/src/server/state.rs +++ b/distant-net/src/server/state.rs @@ -1,37 +1,70 @@ -use super::ConnectionTask; use crate::common::{authentication::Keychain, Backup, ConnectionId}; use std::collections::HashMap; -use tokio::sync::{oneshot, RwLock}; +use tokio::sync::{mpsc, oneshot, RwLock}; +use tokio::task::JoinHandle; /// Contains all top-level state for the server -pub struct ServerState { - /// Mapping of connection ids to their transports - pub connections: RwLock>, +pub struct ServerState { + /// Mapping of connection ids to their tasks. + pub connections: RwLock>>, /// Mapping of connection ids to (OTP, backup) pub keychain: Keychain>, } -impl ServerState { +impl ServerState { pub fn new() -> Self { Self { connections: RwLock::new(HashMap::new()), keychain: Keychain::new(), } } - - /// Returns true if there is at least one active connection - pub async fn has_active_connections(&self) -> bool { - self.connections - .read() - .await - .values() - .any(|task| !task.is_finished()) - } } -impl Default for ServerState { +impl Default for ServerState { fn default() -> Self { Self::new() } } + +pub struct ConnectionState { + shutdown_tx: oneshot::Sender<()>, + task: JoinHandle, mpsc::Receiver)>>, +} + +impl ConnectionState { + /// Creates new state with appropriate channels, returning + /// (shutdown receiver, channel sender, state). + #[allow(clippy::type_complexity)] + pub fn channel() -> ( + oneshot::Receiver<()>, + oneshot::Sender<(mpsc::Sender, mpsc::Receiver)>, + Self, + ) { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let (channel_tx, channel_rx) = oneshot::channel(); + + ( + shutdown_rx, + channel_tx, + Self { + shutdown_tx, + task: tokio::spawn(async move { + match channel_rx.await { + Ok(x) => Some(x), + Err(_) => None, + } + }), + }, + ) + } + + pub fn is_finished(&self) -> bool { + self.task.is_finished() + } + + pub async fn shutdown_and_wait(self) -> Option<(mpsc::Sender, mpsc::Receiver)> { + let _ = self.shutdown_tx.send(()); + self.task.await.unwrap() + } +} diff --git a/distant-net/tests/manager_tests.rs b/distant-net/tests/manager_tests.rs index e45f74d..783f68b 100644 --- a/distant-net/tests/manager_tests.rs +++ b/distant-net/tests/manager_tests.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use distant_net::boxed_connect_handler; -use distant_net::client::{Client, ReconnectStrategy}; +use distant_net::client::Client; use distant_net::common::authentication::{DummyAuthHandler, Verifier}; use distant_net::common::{Destination, InmemoryTransport, Map, OneshotListener}; use distant_net::manager::{Config, ManagerClient, ManagerServer}; @@ -43,7 +43,6 @@ async fn should_be_able_to_establish_a_single_connection_and_communicate_with_a_ let client = Client::build() .auth_handler(DummyAuthHandler) - .reconnect_strategy(ReconnectStrategy::Fail) .connector(t1) .connect_untyped() .await?; @@ -61,7 +60,6 @@ async fn should_be_able_to_establish_a_single_connection_and_communicate_with_a_ info!("Connecting to manager"); let mut client: ManagerClient = Client::build() .auth_handler(DummyAuthHandler) - .reconnect_strategy(ReconnectStrategy::Fail) .connector(t1) .connect() .await diff --git a/distant-net/tests/typed_tests.rs b/distant-net/tests/typed_tests.rs index e2503ff..1bb9eaa 100644 --- a/distant-net/tests/typed_tests.rs +++ b/distant-net/tests/typed_tests.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use distant_net::client::{Client, ReconnectStrategy}; +use distant_net::client::Client; use distant_net::common::authentication::{DummyAuthHandler, Verifier}; use distant_net::common::{InmemoryTransport, OneshotListener}; use distant_net::server::{Server, ServerCtx, ServerHandler}; @@ -38,7 +38,6 @@ async fn should_be_able_to_send_and_receive_typed_payloads_between_client_and_se let mut client: Client<(u8, String), String> = Client::build() .auth_handler(DummyAuthHandler) - .reconnect_strategy(ReconnectStrategy::Fail) .connector(t1) .connect() .await diff --git a/distant-net/tests/untyped_tests.rs b/distant-net/tests/untyped_tests.rs index e8526f7..12b775d 100644 --- a/distant-net/tests/untyped_tests.rs +++ b/distant-net/tests/untyped_tests.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use distant_net::client::{Client, ReconnectStrategy}; +use distant_net::client::Client; use distant_net::common::authentication::{DummyAuthHandler, Verifier}; use distant_net::common::{InmemoryTransport, OneshotListener, Request}; use distant_net::server::{Server, ServerCtx, ServerHandler}; @@ -38,7 +38,6 @@ async fn should_be_able_to_send_and_receive_untyped_payloads_between_client_and_ let mut client = Client::build() .auth_handler(DummyAuthHandler) - .reconnect_strategy(ReconnectStrategy::Fail) .connector(t1) .connect_untyped() .await diff --git a/distant-ssh2/Cargo.toml b/distant-ssh2/Cargo.toml index d7d7023..7958f02 100644 --- a/distant-ssh2/Cargo.toml +++ b/distant-ssh2/Cargo.toml @@ -2,7 +2,7 @@ name = "distant-ssh2" description = "Library to enable native ssh-2 protocol for use with distant sessions" categories = ["network-programming"] -version = "0.20.0-alpha.2" +version = "0.20.0-alpha.3" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -20,7 +20,7 @@ async-compat = "0.2.1" async-once-cell = "0.4.2" async-trait = "0.1.58" derive_more = { version = "0.99.17", default-features = false, features = ["display", "error"] } -distant-core = { version = "=0.20.0-alpha.2", path = "../distant-core" } +distant-core = { version = "=0.20.0-alpha.3", path = "../distant-core" } futures = "0.3.25" hex = "0.4.3" log = "0.4.17" diff --git a/distant-ssh2/src/api.rs b/distant-ssh2/src/api.rs index 409b3ed..fb73c1a 100644 --- a/distant-ssh2/src/api.rs +++ b/distant-ssh2/src/api.rs @@ -694,12 +694,11 @@ impl DistantApi for SshDistantApi { cmd: String, environment: Environment, current_dir: Option, - persist: bool, pty: Option, ) -> io::Result { debug!( - "[Conn {}] Spawning {} {{environment: {:?}, current_dir: {:?}, persist: {}, pty: {:?}}}", - ctx.connection_id, cmd, environment, current_dir, persist, pty + "[Conn {}] Spawning {} {{environment: {:?}, current_dir: {:?}, pty: {:?}}}", + ctx.connection_id, cmd, environment, current_dir, pty ); let global_processes = Arc::downgrade(&self.processes); @@ -744,12 +743,6 @@ impl DistantApi for SshDistantApi { } }; - // If the process will be killed when the connection ends, we want to add it - // to our local data - if !persist { - ctx.local_data.processes.write().await.insert(id); - } - self.processes.write().await.insert( id, Process { diff --git a/distant-ssh2/src/lib.rs b/distant-ssh2/src/lib.rs index 9124dcc..9967003 100644 --- a/distant-ssh2/src/lib.rs +++ b/distant-ssh2/src/lib.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use distant_core::{ data::Environment, net::{ - client::{Client, ReconnectStrategy}, + client::{Client, ClientConfig}, common::authentication::{AuthHandlerMap, DummyAuthHandler, Verifier}, common::{InmemoryTransport, OneshotListener}, server::{Server, ServerRef}, @@ -574,7 +574,7 @@ impl Ssh { debug!("Attempting to connect to distant server @ {}", addr); match Client::tcp(addr) .auth_handler(AuthHandlerMap::new().with_static_key(key.clone())) - .timeout(timeout) + .connect_timeout(timeout) .connect() .await { @@ -646,7 +646,7 @@ impl Ssh { ); // Close out ssh client by killing the internal server and client - server.abort(); + server.shutdown(); client.abort(); let _ = client.wait().await; @@ -718,8 +718,8 @@ impl Ssh { .start(OneshotListener::from_value(t2))?; let client = Client::build() .auth_handler(DummyAuthHandler) + .config(ClientConfig::default().with_maximum_silence_duration()) .connector(t1) - .reconnect_strategy(ReconnectStrategy::Fail) .connect() .await?; Ok((client, server)) diff --git a/distant-ssh2/tests/ssh2/client.rs b/distant-ssh2/tests/ssh2/client.rs index 6d91a3c..338ad79 100644 --- a/distant-ssh2/tests/ssh2/client.rs +++ b/distant-ssh2/tests/ssh2/client.rs @@ -1217,7 +1217,6 @@ async fn proc_spawn_should_not_fail_even_if_process_not_found( /* cmd */ DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(), /* environment */ Environment::new(), /* current_dir */ None, - /* persist */ false, /* pty */ None, ) .await @@ -1239,7 +1238,6 @@ async fn proc_spawn_should_return_id_of_spawned_process(#[future] client: Ctx Client { for path in self.network.to_unix_socket_path_candidates() { match NetClient::unix_socket(path) .auth_handler(self.auth_handler.clone()) - .reconnect_strategy(ReconnectStrategy::ExponentialBackoff { - base: Duration::from_secs(1), - factor: 2.0, - max_duration: None, - max_retries: None, - timeout: None, + .config(ClientConfig { + reconnect_strategy: ReconnectStrategy::ExponentialBackoff { + base: Duration::from_secs(1), + factor: 2.0, + max_duration: Some(Duration::from_secs(10)), + max_retries: None, + timeout: None, + }, + ..Default::default() }) .connect() .await @@ -100,12 +103,15 @@ impl Client { for name in self.network.to_windows_pipe_name_candidates() { match NetClient::local_windows_pipe(name) .auth_handler(self.auth_handler.clone()) - .reconnect_strategy(ReconnectStrategy::ExponentialBackoff { - base: Duration::from_secs(1), - factor: 2.0, - max_duration: None, - max_retries: None, - timeout: None, + .config(ClientConfig { + reconnect_strategy: ReconnectStrategy::ExponentialBackoff { + base: Duration::from_secs(1), + factor: 2.0, + max_duration: Some(Duration::from_secs(10)), + max_retries: None, + timeout: None, + }, + ..Default::default() }) .connect() .await diff --git a/src/cli/commands/client.rs b/src/cli/commands/client.rs index 324c0d1..ffdd3b1 100644 --- a/src/cli/commands/client.rs +++ b/src/cli/commands/client.rs @@ -134,11 +134,6 @@ pub enum ClientSubcommand { #[clap(flatten)] network: NetworkConfig, - /// If provided, will run in persist mode, meaning that the process will not be killed if the - /// client disconnects from the server - #[clap(long)] - persist: bool, - /// If provided, will run LSP in a pty #[clap(long)] pty: bool, @@ -215,11 +210,6 @@ pub enum ClientSubcommand { #[clap(long, default_value_t)] environment: Environment, - /// If provided, will run in persist mode, meaning that the process will not be killed if the - /// client disconnects from the server - #[clap(long)] - persist: bool, - /// Optional command to run instead of $SHELL cmd: Option, }, @@ -294,14 +284,12 @@ impl ClientSubcommand { cmd, environment, current_dir, - persist, pty, } => { debug!("Special request spawning {:?}", cmd); let mut proc = RemoteCommand::new() .environment(environment) .current_dir(current_dir) - .persist(persist) .pty(pty) .spawn(channel.into_client().into_channel(), cmd.as_str()) .await @@ -568,7 +556,6 @@ impl ClientSubcommand { Self::Lsp { connection, network, - persist, pty, cmd, .. @@ -592,12 +579,9 @@ impl ClientSubcommand { format!("Failed to open channel to connection {connection_id}") })?; - debug!( - "Spawning LSP server (persist = {}, pty = {}): {}", - persist, pty, cmd - ); + debug!("Spawning LSP server (pty = {}): {}", pty, cmd); Lsp::new(channel.into_client().into_channel()) - .spawn(cmd, persist, pty) + .spawn(cmd, pty) .await?; } Self::Repl { @@ -869,7 +853,6 @@ impl ClientSubcommand { connection, network, environment, - persist, cmd, .. } => { @@ -893,13 +876,12 @@ impl ClientSubcommand { })?; debug!( - "Spawning shell (environment = {:?}, persist = {}): {}", + "Spawning shell (environment = {:?}): {}", environment, - persist, cmd.as_deref().unwrap_or(r"$SHELL") ); Shell::new(channel.into_client().into_channel()) - .spawn(cmd, environment, persist) + .spawn(cmd, environment) .await?; } } diff --git a/src/cli/commands/client/lsp.rs b/src/cli/commands/client/lsp.rs index 9dea374..4cbe163 100644 --- a/src/cli/commands/client/lsp.rs +++ b/src/cli/commands/client/lsp.rs @@ -11,10 +11,9 @@ impl Lsp { Self(channel) } - pub async fn spawn(self, cmd: impl Into, persist: bool, pty: bool) -> CliResult { + pub async fn spawn(self, cmd: impl Into, pty: bool) -> CliResult { let cmd = cmd.into(); let mut proc = RemoteLspCommand::new() - .persist(persist) .pty(if pty { terminal_size().map(|(Width(width), Height(height))| { PtySize::from_rows_and_cols(height, width) diff --git a/src/cli/commands/client/shell.rs b/src/cli/commands/client/shell.rs index c7dd225..247fa72 100644 --- a/src/cli/commands/client/shell.rs +++ b/src/cli/commands/client/shell.rs @@ -25,7 +25,6 @@ impl Shell { mut self, cmd: impl Into>, mut environment: Environment, - persist: bool, ) -> CliResult { // Automatically add TERM=xterm-256color if not specified if !environment.contains_key("TERM") { @@ -55,7 +54,6 @@ impl Shell { }; let mut proc = RemoteCommand::new() - .persist(persist) .environment(environment) .pty( terminal_size() diff --git a/src/cli/commands/manager/handlers.rs b/src/cli/commands/manager/handlers.rs index 2092c55..42cee96 100644 --- a/src/cli/commands/manager/handlers.rs +++ b/src/cli/commands/manager/handlers.rs @@ -1,6 +1,6 @@ use crate::config::ClientLaunchConfig; use async_trait::async_trait; -use distant_core::net::client::{Client, ReconnectStrategy, UntypedClient}; +use distant_core::net::client::{Client, ClientConfig, ReconnectStrategy, UntypedClient}; use distant_core::net::common::authentication::msg::*; use distant_core::net::common::authentication::{ AuthHandler, Authenticator, DynAuthHandler, ProxyAuthHandler, SingleAuthHandler, @@ -210,14 +210,17 @@ impl DistantConnectHandler { match Client::tcp(addr) .auth_handler(DynAuthHandler::from(&mut auth_handler)) - .reconnect_strategy(ReconnectStrategy::ExponentialBackoff { - base: Duration::from_secs(1), - factor: 2.0, - max_duration: None, - max_retries: None, - timeout: None, + .config(ClientConfig { + reconnect_strategy: ReconnectStrategy::ExponentialBackoff { + base: Duration::from_secs(1), + factor: 2.0, + max_duration: Some(Duration::from_secs(10)), + max_retries: None, + timeout: None, + }, + ..Default::default() }) - .timeout(Duration::from_secs(180)) + .connect_timeout(Duration::from_secs(180)) .connect_untyped() .await { diff --git a/tests/cli/repl/proc_spawn.rs b/tests/cli/repl/proc_spawn.rs index 4934629..0834eae 100644 --- a/tests/cli/repl/proc_spawn.rs +++ b/tests/cli/repl/proc_spawn.rs @@ -84,7 +84,6 @@ async fn should_support_json_to_execute_program_and_return_exit_status( "payload": { "type": "proc_spawn", "cmd": cmd, - "persist": false, "pty": null, }, }); @@ -109,7 +108,6 @@ async fn should_support_json_to_capture_and_print_stdout(mut json_repl: CtxComma "payload": { "type": "proc_spawn", "cmd": cmd, - "persist": false, "pty": null, }, }); @@ -148,7 +146,6 @@ async fn should_support_json_to_capture_and_print_stderr(mut json_repl: CtxComma "payload": { "type": "proc_spawn", "cmd": cmd, - "persist": false, "pty": null, }, }); @@ -187,7 +184,6 @@ async fn should_support_json_to_forward_stdin_to_remote_process(mut json_repl: C "payload": { "type": "proc_spawn", "cmd": cmd, - "persist": false, "pty": null, }, }); @@ -271,7 +267,6 @@ async fn should_support_json_output_for_error(mut json_repl: CtxCommand) { "payload": { "type": "proc_spawn", "cmd": DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(), - "persist": false, "pty": null, }, });