Add native ssh (#57)

* Bump to 0.15.0
* Add new distant-ssh2 subcrate to provide an alternate session as an ssh client
* Add rpassword & wezterm-ssh dependencies
* Rename core -> distant-core in project directory structure and move ssh2 feature into distant-ssh2 crate
* Upgrade tokio to 1.12,
* Update github actions to detect changes and apply testing for only those changes
* Add method parameter to support distant & ssh methods for action and lsp subcommands
* Add ssh-host, ssh-port, and ssh-user parameters to specify information for ssh method
pull/59/head
Chip Senkbeil 3 years ago committed by GitHub
parent 32639166bc
commit 0a11ec65a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,8 +9,8 @@ on:
- master
jobs:
test-core:
name: "[distant-core] Test Rust ${{ matrix.rust }} on ${{ matrix.os }}"
tests:
name: "Test Rust ${{ matrix.rust }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@ -32,34 +32,52 @@ jobs:
run: cargo --version
- uses: Vampire/setup-wsl@v1
if: ${{ matrix.os == 'windows-latest' }}
- run: cargo test --verbose -p distant-core
- run: cargo test --verbose --all-features -p distant-core
test-cli:
name: "[distant] Test Rust ${{ matrix.rust }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { rust: stable, os: ubuntu-latest }
- { rust: stable, os: macos-latest }
# - { rust: stable, os: windows-latest }
- { rust: 1.51.0, os: ubuntu-latest }
steps:
- uses: actions/checkout@v2
- name: Install Rust ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
- uses: dorny/paths-filter@v2
id: changes
with:
profile: minimal
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- uses: Vampire/setup-wsl@v1
if: ${{ matrix.os == 'windows-latest' }}
- run: cargo test --verbose
base: ${{ github.ref }}
filters: |
cli:
- 'src/**'
- 'Cargo.*'
core:
- 'distant-core/**'
ssh2:
- 'distant-ssh2/**'
- name: Run core tests (default features)
run: cargo test --verbose -p distant-core
if: steps.changes.outputs.core == 'true'
- name: Run core tests (all features)
run: cargo test --verbose --all-features -p distant-core
if: steps.changes.outputs.core == 'true'
- name: Ensure /run/sshd exists on Unix
run: mkdir -p /run/sshd
if: |
matrix.os != 'windows-latest' &&
matrix.os != 'macos-latest' &&
steps.changes.outputs.ssh2 == 'true'
- name: Run ssh2 tests (default features)
run: cargo test --verbose -p distant-ssh2
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.ssh2 == 'true'
- name: Run ssh2 tests (all features)
run: cargo test --verbose --all-features -p distant-ssh2
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.ssh2 == 'true'
- name: Run CLI tests
run: cargo test --verbose
shell: bash
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.cli == 'true'
- name: Run CLI tests (no default features)
run: cargo test --verbose --no-default-features
shell: bash
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.cli == 'true'
clippy:
name: Lint with clippy

5
.gitignore vendored

@ -1,3 +1,4 @@
/target
.DS_Store
/core/Cargo.lock
**/.DS_Store
/distant-core/Cargo.lock
/distant-ssh2/Cargo.lock

642
Cargo.lock generated

@ -29,13 +29,28 @@ dependencies = [
"winapi",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "assert_cmd"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54f002ce7d0c5e809ebb02be78fd503aeed4a511fd0fcaff6e6914cbdabbfa33"
dependencies = [
"bstr",
"bstr 0.2.16",
"doc-comment",
"predicates",
"predicates-core",
@ -57,6 +72,131 @@ dependencies = [
"tempfile",
]
[[package]]
name = "async-channel"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "async-compat"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b48b4ff0c2026db683dea961cd8ea874737f56cffca86fa84415eaddc51c00d"
dependencies = [
"futures-core",
"futures-io",
"once_cell",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-executor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"once_cell",
"slab",
]
[[package]]
name = "async-fs"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2"
dependencies = [
"async-lock",
"blocking",
"futures-lite",
]
[[package]]
name = "async-io"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b"
dependencies = [
"concurrent-queue",
"futures-lite",
"libc",
"log",
"once_cell",
"parking",
"polling",
"slab",
"socket2",
"waker-fn",
"winapi",
]
[[package]]
name = "async-lock"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b"
dependencies = [
"event-listener",
]
[[package]]
name = "async-net"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b21b63ab5a0db0369deb913540af2892750e42d949faacc7a61495ac418a1692"
dependencies = [
"async-io",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"libc",
"once_cell",
"signal-hook",
"winapi",
]
[[package]]
name = "async-task"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
[[package]]
name = "async_ossl"
version = "0.1.0"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
dependencies = [
"openssl",
]
[[package]]
name = "atomic-waker"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
[[package]]
name = "atty"
version = "0.2.14"
@ -74,12 +214,43 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blocking"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9"
dependencies = [
"async-channel",
"async-task",
"atomic-waker",
"fastrand",
"futures-lite",
"once_cell",
]
[[package]]
name = "bstr"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59604ece62a407dc9164732e5adea02467898954c3a5811fd2dc140af14ef15b"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
]
[[package]]
name = "bstr"
version = "0.2.16"
@ -103,6 +274,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cache-padded"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
[[package]]
name = "cc"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -162,7 +345,7 @@ version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"ansi_term 0.11.0",
"atty",
"bitflags",
"strsim",
@ -171,6 +354,15 @@ dependencies = [
"vec_map",
]
[[package]]
name = "concurrent-queue"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
dependencies = [
"cache-padded",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@ -214,19 +406,41 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "distant"
version = "0.14.2"
version = "0.15.0"
dependencies = [
"assert_cmd",
"assert_fs",
"derive_more",
"distant-core",
"flexi_logger",
"distant-ssh2",
"flexi_logger 0.18.1",
"fork",
"indoc",
"lazy_static",
"log",
"once_cell",
"predicates",
"rand",
"rstest",
@ -239,7 +453,7 @@ dependencies = [
[[package]]
name = "distant-core"
version = "0.14.2"
version = "0.15.0"
dependencies = [
"assert_fs",
"bytes",
@ -248,13 +462,15 @@ dependencies = [
"futures",
"hex",
"indoc",
"lazy_static",
"log",
"once_cell",
"predicates",
"rand",
"rpassword",
"serde",
"serde_cbor",
"serde_json",
"ssh2",
"structopt",
"strum",
"tokio",
@ -262,6 +478,29 @@ dependencies = [
"walkdir",
]
[[package]]
name = "distant-ssh2"
version = "0.15.0"
dependencies = [
"assert_cmd",
"assert_fs",
"async-compat",
"distant-core",
"flexi_logger 0.19.4",
"futures",
"indoc",
"log",
"once_cell",
"predicates",
"rand",
"rpassword",
"rstest",
"smol",
"tokio",
"wezterm-ssh",
"whoami",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
@ -274,6 +513,43 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "event-listener"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
[[package]]
name = "fastrand"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e"
dependencies = [
"instant",
]
[[package]]
name = "filedescriptor"
version = "0.8.1"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
dependencies = [
"libc",
"thiserror",
"winapi",
]
[[package]]
name = "filenamegen"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2da6e8ef70499318bc50abd003fd66dbf6d8a46c23f9e90158f388a788976a"
dependencies = [
"anyhow",
"bstr 0.1.4",
"regex",
"walkdir",
]
[[package]]
name = "flexi_logger"
version = "0.18.1"
@ -290,6 +566,23 @@ dependencies = [
"yansi",
]
[[package]]
name = "flexi_logger"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35627d78fcea84d52181369fe48fcfce56a2a18e29443dba23d4c24b650fb107"
dependencies = [
"ansi_term 0.12.1",
"atty",
"chrono",
"glob",
"lazy_static",
"log",
"regex",
"rustversion",
"thiserror",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
@ -305,6 +598,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "fork"
version = "0.1.18"
@ -362,6 +670,21 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
[[package]]
name = "futures-lite"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-macro"
version = "0.3.17"
@ -442,7 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
"bstr 0.2.16",
"fnv",
"log",
"regex",
@ -525,6 +848,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "ioctl-rs"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
dependencies = [
"libc",
]
[[package]]
name = "itertools"
version = "0.10.1"
@ -561,6 +893,31 @@ version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
[[package]]
name = "libssh2-sys"
version = "0.2.21"
source = "git+https://github.com/wez/ssh2-rs.git?branch=win32ssl#c65067040c97a0cf7f96c69d6fc87764a32c34ae"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "lock_api"
version = "0.4.5"
@ -663,6 +1020,49 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-sys",
]
[[package]]
name = "openssl-src"
version = "111.16.0+1.1.1l"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab2173f69416cf3ec12debb5823d244127d23a9b127d5a5189aa97c5fa2859f"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82"
dependencies = [
"autocfg",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -700,6 +1100,25 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "polling"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25"
dependencies = [
"cfg-if",
"libc",
"log",
"wepoll-ffi",
"winapi",
]
[[package]]
name = "poly1305"
version = "0.7.2"
@ -711,6 +1130,23 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "portable-pty"
version = "0.5.0"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
dependencies = [
"anyhow",
"bitflags",
"filedescriptor",
"lazy_static",
"libc",
"log",
"serial",
"shared_library",
"shell-words",
"winapi",
]
[[package]]
name = "ppv-lite86"
version = "0.2.10"
@ -850,6 +1286,16 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]]
name = "regex"
version = "1.5.4"
@ -882,6 +1328,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "rpassword"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "rstest"
version = "0.11.0"
@ -904,6 +1360,12 @@ dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "ryu"
version = "1.0.5"
@ -972,6 +1434,74 @@ dependencies = [
"serde",
]
[[package]]
name = "serial"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
dependencies = [
"serial-core",
"serial-unix",
"serial-windows",
]
[[package]]
name = "serial-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
dependencies = [
"libc",
]
[[package]]
name = "serial-unix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
dependencies = [
"ioctl-rs",
"libc",
"serial-core",
"termios",
]
[[package]]
name = "serial-windows"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
dependencies = [
"libc",
"serial-core",
]
[[package]]
name = "shared_library"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
dependencies = [
"lazy_static",
"libc",
]
[[package]]
name = "shell-words"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
[[package]]
name = "signal-hook"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -993,6 +1523,45 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "smol"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4"
dependencies = [
"async-channel",
"async-executor",
"async-fs",
"async-io",
"async-lock",
"async-net",
"async-process",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "socket2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "ssh2"
version = "0.9.1"
source = "git+https://github.com/wez/ssh2-rs.git?branch=win32ssl#c65067040c97a0cf7f96c69d6fc87764a32c34ae"
dependencies = [
"bitflags",
"libc",
"libssh2-sys",
"parking_lot",
]
[[package]]
name = "strsim"
version = "0.8.0"
@ -1075,6 +1644,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termios"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
dependencies = [
"libc",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -1125,9 +1703,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce"
checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
dependencies = [
"autocfg",
"bytes",
@ -1214,6 +1792,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
@ -1235,6 +1819,12 @@ dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.2"
@ -1316,11 +1906,39 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
dependencies = [
"cc",
]
[[package]]
name = "wezterm-ssh"
version = "0.2.0"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
dependencies = [
"anyhow",
"async_ossl",
"base64",
"dirs-next",
"filedescriptor",
"filenamegen",
"log",
"portable-pty",
"regex",
"smol",
"ssh2",
"thiserror",
]
[[package]]
name = "whoami"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7741161a40200a867c96dfa5574544efa4178cf4c8f770b62dd1cc0362d7ae1"
checksum = "cabfe22aa4936611957e0b5ad9ed0472ac52b2bfb9aedac4a3f3a91a03bd1ff0"
dependencies = [
"wasm-bindgen",
"web-sys",

@ -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.14.2"
version = "0.15.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
@ -12,27 +12,37 @@ readme = "README.md"
license = "MIT OR Apache-2.0"
[workspace]
members = ["core"]
members = ["distant-core", "distant-ssh2"]
[profile.release]
opt-level = 'z'
lto = true
codegen-units = 1
[patch.crates-io]
ssh2 = { git = "https://github.com/wez/ssh2-rs.git", branch="win32ssl" }
[features]
default = ["ssh2"]
ssh2 = ["distant-ssh2"]
[dependencies]
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] }
distant-core = { version = "=0.14.2", path = "core", features = ["structopt"] }
distant-core = { version = "=0.15.0", path = "distant-core", features = ["structopt"] }
flexi_logger = "0.18.0"
fork = "0.1.18"
lazy_static = "1.4.0"
log = "0.4.14"
once_cell = "1.8.0"
rand = { version = "0.8.4", features = ["getrandom"] }
tokio = { version = "1.9.0", features = ["full"] }
tokio = { version = "1.12.0", features = ["full"] }
serde_json = "1.0.64"
structopt = "0.3.22"
strum = { version = "0.21.0", features = ["derive"] }
whoami = "1.1.2"
# Optional native SSH functionality
distant-ssh2 = { version = "=0.15.0", path = "distant-ssh2", optional = true }
[dev-dependencies]
assert_cmd = "2.0.0"
assert_fs = "1.0.4"

@ -2,7 +2,7 @@
name = "distant-core"
description = "Core library for distant, enabling operation on a remote computer through file and process manipulation"
categories = ["network-programming"]
version = "0.14.2"
version = "0.15.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
@ -10,24 +10,29 @@ repository = "https://github.com/chipsenkbeil/distant"
readme = "README.md"
license = "MIT OR Apache-2.0"
[features]
native-ssh2 = ["rpassword", "ssh2"]
[dependencies]
bytes = "1.1.0"
chacha20poly1305 = "0.9.0"
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] }
futures = "0.3.16"
hex = "0.4.3"
lazy_static = "1.4.0"
log = "0.4.14"
once_cell = "1.8.0"
rand = { version = "0.8.4", features = ["getrandom"] }
serde = { version = "1.0.126", features = ["derive"] }
serde_cbor = "0.11.1"
serde_json = "1.0.64"
strum = { version = "0.21.0", features = ["derive"] }
tokio = { version = "1.9.0", features = ["full"] }
tokio = { version = "1.12.0", features = ["full"] }
tokio-util = { version = "0.6.7", features = ["codec"] }
walkdir = "2.3.2"
# Optional dependencies based on features
rpassword = { version = "5.0.1", optional = true }
ssh2 = { version = "0.9.1", features = ["vendored-openssl"], optional = true }
structopt = { version = "0.3.22", optional = true }
[dev-dependencies]

@ -591,6 +591,12 @@ where
error!("<Conn @ {}> Join on stdout task failed: {}", conn_id, x);
}
// Wait for the child after being killed to ensure that it has been cleaned
// up at the operating system level
if let Err(x) = child.wait().await {
error!("<Conn @ {}> Failed to wait on killed process {}: {}", conn_id, id, x);
}
state_2.lock().await.remove_process(conn_id, id);
let payload = vec![ResponseData::ProcDone { id, success: false, code: None }];
@ -669,61 +675,68 @@ async fn system_info() -> Result<Outgoing, ServerError> {
mod tests {
use super::*;
use assert_fs::prelude::*;
use once_cell::sync::Lazy;
use predicates::prelude::*;
use std::time::Duration;
lazy_static::lazy_static! {
static ref TEMP_SCRIPT_DIR: assert_fs::TempDir = assert_fs::TempDir::new().unwrap();
static ref SCRIPT_RUNNER: String = String::from("bash");
static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> =
Lazy::new(|| assert_fs::TempDir::new().unwrap());
static SCRIPT_RUNNER: Lazy<String> = Lazy::new(|| String::from("bash"));
static ref ECHO_ARGS_TO_STDOUT_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
script.write_str(indoc::indoc!(r#"
static ECHO_ARGS_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$@"
"#)).unwrap();
script
};
printf "%s" "$*"
"#
))
.unwrap();
script
});
static ref ECHO_ARGS_TO_STDERR_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
script.write_str(indoc::indoc!(r#"
static ECHO_ARGS_TO_STDERR_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$@" 1>&2
"#)).unwrap();
script
};
printf "%s" "$*" 1>&2
"#
))
.unwrap();
script
});
static ref ECHO_STDIN_TO_STDOUT_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
script.write_str(indoc::indoc!(r#"
static ECHO_STDIN_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
while IFS= read; do echo "$REPLY"; done
"#)).unwrap();
script
};
static ref EXIT_CODE_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("exit_code.sh");
script.write_str(indoc::indoc!(r#"
#!/usr/bin/env bash
exit "$1"
"#)).unwrap();
script
};
"#
))
.unwrap();
script
});
static ref SLEEP_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("sleep.sh");
script.write_str(indoc::indoc!(r#"
static SLEEP_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("sleep.sh");
script
.write_str(indoc::indoc!(
r#"
#!/usr/bin/env bash
sleep "$1"
"#)).unwrap();
script
};
"#
))
.unwrap();
script
});
static ref DOES_NOT_EXIST_BIN: assert_fs::fixture::ChildPath =
TEMP_SCRIPT_DIR.child("does_not_exist_bin");
}
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
fn setup(
buffer: usize,

@ -0,0 +1,32 @@
[package]
name = "distant-ssh2"
description = "Library to enable native ssh-2 protocol for use with distant sessions"
categories = ["network-programming"]
version = "0.15.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
repository = "https://github.com/chipsenkbeil/distant"
readme = "README.md"
license = "MIT OR Apache-2.0"
[dependencies]
async-compat = "0.2.1"
distant-core = { version = "=0.15.0", path = "../distant-core" }
futures = "0.3.16"
log = "0.4.14"
rand = { version = "0.8.4", features = ["getrandom"] }
rpassword = "5.0.1"
smol = "1.2"
tokio = { version = "1.12.0", features = ["full"] }
wezterm-ssh = { version = "0.2.0", features = ["vendored-openssl"], git = "https://github.com/chipsenkbeil/wezterm" }
[dev-dependencies]
assert_cmd = "2.0.0"
assert_fs = "1.0.4"
flexi_logger = "0.19.4"
indoc = "1.0.3"
once_cell = "1.8.0"
predicates = "2.0.2"
rstest = "0.11.0"
whoami = "1.1.4"

@ -0,0 +1,881 @@
use async_compat::CompatExt;
use distant_core::{
data::{DirEntry, Error as DistantError, FileType, RunningProcess},
Request, RequestData, Response, ResponseData,
};
use futures::future;
use log::*;
use std::{
collections::HashMap,
future::Future,
io::{self, Read, Write},
path::{Component, Path, PathBuf},
pin::Pin,
sync::Arc,
};
use tokio::sync::{mpsc, Mutex};
use wezterm_ssh::{Child, ExecResult, OpenFileType, OpenOptions, Session as WezSession, WriteMode};
const MAX_PIPE_CHUNK_SIZE: usize = 8192;
const READ_PAUSE_MILLIS: u64 = 50;
fn to_other_error<E>(err: E) -> io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::Other, err)
}
#[derive(Default)]
pub(crate) struct State {
processes: HashMap<usize, Process>,
}
struct Process {
id: usize,
cmd: String,
args: Vec<String>,
stdin_tx: mpsc::Sender<String>,
kill_tx: mpsc::Sender<()>,
}
type ReplyRet = Pin<Box<dyn Future<Output = bool> + Send + 'static>>;
type PostHook = Box<dyn FnOnce() + Send>;
struct Outgoing {
data: ResponseData,
post_hook: Option<PostHook>,
}
impl From<ResponseData> for Outgoing {
fn from(data: ResponseData) -> Self {
Self {
data,
post_hook: None,
}
}
}
/// Processes the provided request, sending replies using the given sender
pub(super) async fn process(
session: WezSession,
state: Arc<Mutex<State>>,
req: Request,
tx: mpsc::Sender<Response>,
) -> Result<(), mpsc::error::SendError<Response>> {
async fn inner<F>(
session: WezSession,
state: Arc<Mutex<State>>,
data: RequestData,
reply: F,
) -> io::Result<Outgoing>
where
F: FnMut(Vec<ResponseData>) -> ReplyRet + Clone + Send + 'static,
{
match data {
RequestData::FileRead { path } => file_read(session, path).await,
RequestData::FileReadText { path } => file_read_text(session, path).await,
RequestData::FileWrite { path, data } => file_write(session, path, data).await,
RequestData::FileWriteText { path, text } => file_write(session, path, text).await,
RequestData::FileAppend { path, data } => file_append(session, path, data).await,
RequestData::FileAppendText { path, text } => file_append(session, path, text).await,
RequestData::DirRead {
path,
depth,
absolute,
canonicalize,
include_root,
} => dir_read(session, path, depth, absolute, canonicalize, include_root).await,
RequestData::DirCreate { path, all } => dir_create(session, path, all).await,
RequestData::Remove { path, force } => remove(session, path, force).await,
RequestData::Copy { src, dst } => copy(session, src, dst).await,
RequestData::Rename { src, dst } => rename(session, src, dst).await,
RequestData::Exists { path } => exists(session, path).await,
RequestData::Metadata {
path,
canonicalize,
resolve_file_type,
} => metadata(session, path, canonicalize, resolve_file_type).await,
RequestData::ProcRun { cmd, args } => proc_run(session, state, reply, cmd, args).await,
RequestData::ProcKill { id } => proc_kill(session, state, id).await,
RequestData::ProcStdin { id, data } => proc_stdin(session, state, id, data).await,
RequestData::ProcList {} => proc_list(session, state).await,
RequestData::SystemInfo {} => system_info(session).await,
}
}
let reply = {
let origin_id = req.id;
let tenant = req.tenant.clone();
let tx_2 = tx.clone();
move |payload: Vec<ResponseData>| -> ReplyRet {
let tx = tx_2.clone();
let res = Response::new(tenant.to_string(), origin_id, payload);
Box::pin(async move { tx.send(res).await.is_ok() })
}
};
// Build up a collection of tasks to run independently
let mut payload_tasks = Vec::new();
for data in req.payload {
let state_2 = Arc::clone(&state);
let reply_2 = reply.clone();
let session = session.clone();
payload_tasks.push(tokio::spawn(async move {
match inner(session, state_2, data, reply_2).await {
Ok(outgoing) => outgoing,
Err(x) => Outgoing::from(ResponseData::from(x)),
}
}));
}
// Collect the results of our tasks into the payload entries
let mut outgoing: Vec<Outgoing> = future::join_all(payload_tasks)
.await
.into_iter()
.map(|x| match x {
Ok(outgoing) => outgoing,
Err(x) => Outgoing::from(ResponseData::from(x)),
})
.collect();
let post_hooks: Vec<PostHook> = outgoing
.iter_mut()
.filter_map(|x| x.post_hook.take())
.collect();
let payload = outgoing.into_iter().map(|x| x.data).collect();
let res = Response::new(req.tenant, req.id, payload);
// Send out our primary response from processing the request
let result = tx.send(res).await;
// Invoke all post hooks
for hook in post_hooks {
hook();
}
result
}
async fn file_read(session: WezSession, path: PathBuf) -> io::Result<Outgoing> {
use smol::io::AsyncReadExt;
let mut file = session
.sftp()
.open(path)
.compat()
.await
.map_err(to_other_error)?;
let mut contents = String::new();
file.read_to_string(&mut contents).compat().await?;
Ok(Outgoing::from(ResponseData::Blob {
data: contents.into_bytes(),
}))
}
async fn file_read_text(session: WezSession, path: PathBuf) -> io::Result<Outgoing> {
use smol::io::AsyncReadExt;
let mut file = session
.sftp()
.open(path)
.compat()
.await
.map_err(to_other_error)?;
let mut contents = String::new();
file.read_to_string(&mut contents).compat().await?;
Ok(Outgoing::from(ResponseData::Text { data: contents }))
}
async fn file_write(
session: WezSession,
path: PathBuf,
data: impl AsRef<[u8]>,
) -> io::Result<Outgoing> {
use smol::io::AsyncWriteExt;
let mut file = session
.sftp()
.create(path)
.compat()
.await
.map_err(to_other_error)?;
file.write_all(data.as_ref()).compat().await?;
Ok(Outgoing::from(ResponseData::Ok))
}
async fn file_append(
session: WezSession,
path: PathBuf,
data: impl AsRef<[u8]>,
) -> io::Result<Outgoing> {
use smol::io::AsyncWriteExt;
let mut file = session
.sftp()
.open_mode(
path,
OpenOptions {
read: false,
write: Some(WriteMode::Append),
// Using 644 as this mirrors "ssh <host> touch ..."
// 644: rw-r--r--
mode: 0o644,
ty: OpenFileType::File,
},
)
.compat()
.await
.map_err(to_other_error)?;
file.write_all(data.as_ref()).compat().await?;
Ok(Outgoing::from(ResponseData::Ok))
}
async fn dir_read(
session: WezSession,
path: PathBuf,
depth: usize,
absolute: bool,
canonicalize: bool,
include_root: bool,
) -> io::Result<Outgoing> {
let sftp = session.sftp();
// Canonicalize our provided path to ensure that it is exists, not a loop, and absolute
let root_path = sftp.realpath(path).compat().await.map_err(to_other_error)?;
// Build up our entry list
let mut entries = Vec::new();
let mut errors = Vec::new();
let mut to_traverse = vec![DirEntry {
path: root_path.to_path_buf(),
file_type: FileType::Dir,
depth: 0,
}];
while let Some(entry) = to_traverse.pop() {
let is_root = entry.depth == 0;
let next_depth = entry.depth + 1;
let ft = entry.file_type;
let path = if entry.path.is_relative() {
root_path.join(&entry.path)
} else {
entry.path.to_path_buf()
};
// Always include any non-root in our traverse list, but only include the
// root directory if flagged to do so
if !is_root || include_root {
entries.push(entry);
}
let is_dir = match ft {
FileType::Dir => true,
FileType::File => false,
FileType::Symlink => match sftp.stat(&path).await {
Ok(stat) => stat.is_dir(),
Err(x) => {
errors.push(DistantError::from(to_other_error(x)));
continue;
}
},
};
// Determine if we continue traversing or stop
if is_dir && (depth == 0 || next_depth <= depth) {
match sftp.readdir(&path).compat().await.map_err(to_other_error) {
Ok(entries) => {
for (mut path, stat) in entries {
// Canonicalize the path if specified, otherwise just return
// the path as is
path = if canonicalize {
match sftp.realpath(path).compat().await {
Ok(path) => path,
Err(x) => {
errors.push(DistantError::from(to_other_error(x)));
continue;
}
}
} else {
path
};
// Strip the path of its prefix based if not flagged as absolute
if !absolute {
// NOTE: In the situation where we canonicalized the path earlier,
// there is no guarantee that our root path is still the parent of
// the symlink's destination; so, in that case we MUST just return
// the path if the strip_prefix fails
path = path
.strip_prefix(root_path.as_path())
.map(Path::to_path_buf)
.unwrap_or(path);
};
let ft = stat.ty;
to_traverse.push(DirEntry {
path,
file_type: if ft.is_dir() {
FileType::Dir
} else if ft.is_file() {
FileType::File
} else {
FileType::Symlink
},
depth: next_depth,
});
}
}
Err(x) if is_root => return Err(io::Error::new(io::ErrorKind::Other, x)),
Err(x) => errors.push(DistantError::from(x)),
}
}
}
// Sort entries by filename
entries.sort_unstable_by_key(|e| e.path.to_path_buf());
Ok(Outgoing::from(ResponseData::DirEntries { entries, errors }))
}
async fn dir_create(session: WezSession, path: PathBuf, all: bool) -> io::Result<Outgoing> {
let sftp = session.sftp();
// Makes the immediate directory, failing if given a path with missing components
async fn mkdir(sftp: &wezterm_ssh::Sftp, path: PathBuf) -> io::Result<()> {
// Using 755 as this mirrors "ssh <host> mkdir ..."
// 755: rwxr-xr-x
sftp.mkdir(path, 0o755)
.compat()
.await
.map_err(to_other_error)
}
if all {
// Keep trying to create a directory, moving up to parent each time a failure happens
let mut failed_paths = Vec::new();
let mut cur_path = path.as_path();
loop {
let failed = mkdir(&sftp, cur_path.to_path_buf()).await.is_err();
if failed {
failed_paths.push(cur_path);
if let Some(path) = cur_path.parent() {
cur_path = path;
} else {
return Err(io::Error::from(io::ErrorKind::PermissionDenied));
}
} else {
break;
}
}
// Now that we've successfully created a parent component (or the directory), proceed
// to attempt to create each failed directory
while let Some(path) = failed_paths.pop() {
mkdir(&sftp, path.to_path_buf()).await?;
}
} else {
mkdir(&sftp, path).await?;
}
Ok(Outgoing::from(ResponseData::Ok))
}
async fn remove(session: WezSession, path: PathBuf, force: bool) -> io::Result<Outgoing> {
let sftp = session.sftp();
// Determine if we are dealing with a file or directory
let stat = sftp
.stat(path.to_path_buf())
.compat()
.await
.map_err(to_other_error)?;
// If a file or symlink, we just unlink (easy)
if stat.is_file() || stat.is_symlink() {
sftp.unlink(path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
// If directory and not forcing, we just rmdir (easy)
} else if !force {
sftp.rmdir(path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
// Otherwise, we need to find all files and directories, keep track of their depth, and
// then attempt to remove them all
} else {
let mut entries = Vec::new();
let mut to_traverse = vec![DirEntry {
path,
file_type: FileType::Dir,
depth: 0,
}];
// Collect all entries within directory
while let Some(entry) = to_traverse.pop() {
if entry.file_type == FileType::Dir {
let path = entry.path.to_path_buf();
let depth = entry.depth;
entries.push(entry);
for (path, stat) in sftp.readdir(path).await.map_err(to_other_error)? {
to_traverse.push(DirEntry {
path,
file_type: if stat.is_dir() {
FileType::Dir
} else if stat.is_file() {
FileType::File
} else {
FileType::Symlink
},
depth: depth + 1,
});
}
} else {
entries.push(entry);
}
}
// Sort by depth such that deepest are last as we will be popping
// off entries from end to remove first
entries.sort_unstable_by_key(|e| e.depth);
while let Some(entry) = entries.pop() {
if entry.file_type == FileType::Dir {
sftp.rmdir(entry.path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
} else {
sftp.unlink(entry.path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
}
}
}
Ok(Outgoing::from(ResponseData::Ok))
}
async fn copy(session: WezSession, src: PathBuf, dst: PathBuf) -> io::Result<Outgoing> {
// NOTE: SFTP does not provide a remote-to-remote copy method, so we instead execute
// a program and hope that it applies, starting with the Unix/BSD/GNU cp method
// and switch to Window's xcopy if the former fails
// Unix cp -R <src> <dst>
let unix_result = session
.exec(&format!("cp -R {:?} {:?}", src, dst), None)
.compat()
.await;
let failed = unix_result.is_err() || {
let exit_status = unix_result.unwrap().child.async_wait().compat().await;
exit_status.is_err() || !exit_status.unwrap().success()
};
// Windows xcopy <src> <dst> /s /e
if failed {
let exit_status = session
.exec(&format!("xcopy {:?} {:?} /s /e", src, dst), None)
.compat()
.await
.map_err(to_other_error)?
.child
.async_wait()
.compat()
.await
.map_err(to_other_error)?;
if !exit_status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Unix and windows copy commands failed",
));
}
}
Ok(Outgoing::from(ResponseData::Ok))
}
async fn rename(session: WezSession, src: PathBuf, dst: PathBuf) -> io::Result<Outgoing> {
session
.sftp()
.rename(src, dst, Default::default())
.compat()
.await
.map_err(to_other_error)?;
Ok(Outgoing::from(ResponseData::Ok))
}
async fn exists(session: WezSession, path: PathBuf) -> io::Result<Outgoing> {
// NOTE: SFTP does not provide a means to check if a path exists that can be performed
// separately from getting permission errors; so, we just assume any error means that the path
// does not exist
let exists = session.sftp().lstat(path).compat().await.is_ok();
Ok(Outgoing::from(ResponseData::Exists(exists)))
}
async fn metadata(
session: WezSession,
path: PathBuf,
canonicalize: bool,
resolve_file_type: bool,
) -> io::Result<Outgoing> {
let sftp = session.sftp();
let canonicalized_path = if canonicalize {
Some(
sftp.realpath(path.to_path_buf())
.compat()
.await
.map_err(to_other_error)?,
)
} else {
None
};
let stat = if resolve_file_type {
sftp.stat(path).compat().await.map_err(to_other_error)?
} else {
sftp.lstat(path).compat().await.map_err(to_other_error)?
};
let file_type = if stat.is_dir() {
FileType::Dir
} else if stat.is_file() {
FileType::File
} else {
FileType::Symlink
};
Ok(Outgoing::from(ResponseData::Metadata {
canonicalized_path,
file_type,
len: stat.len(),
// Check that owner, group, or other has write permission (if not, then readonly)
readonly: stat.is_readonly(),
accessed: stat.accessed.map(u128::from),
modified: stat.modified.map(u128::from),
created: None,
}))
}
async fn proc_run<F>(
session: WezSession,
state: Arc<Mutex<State>>,
reply: F,
cmd: String,
args: Vec<String>,
) -> io::Result<Outgoing>
where
F: FnMut(Vec<ResponseData>) -> ReplyRet + Clone + Send + 'static,
{
let id = rand::random();
let cmd_string = format!("{} {}", cmd, args.join(" "));
let ExecResult {
mut stdin,
mut stdout,
mut stderr,
mut child,
} = session
.exec(&cmd_string, None)
.compat()
.await
.map_err(to_other_error)?;
// Force stdin, stdout, and stderr to be nonblocking
stdin
.set_non_blocking(true)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
stdout
.set_non_blocking(true)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
stderr
.set_non_blocking(true)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
// Check if the process died immediately and report
// an error if that's the case
if let Ok(Some(exit_status)) = child.try_wait() {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
format!("Process exited early: {:?}", exit_status),
));
}
let (stdin_tx, mut stdin_rx) = mpsc::channel(1);
let (kill_tx, mut kill_rx) = mpsc::channel(1);
state.lock().await.processes.insert(
id,
Process {
id,
cmd,
args,
stdin_tx,
kill_tx,
},
);
let post_hook = Box::new(move || {
// Spawn a task that sends stdout as a response
let mut reply_2 = reply.clone();
let stdout_task = tokio::spawn(async move {
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match stdout.read(&mut buf) {
Ok(n) if n > 0 => match String::from_utf8(buf[..n].to_vec()) {
Ok(data) => {
let payload = vec![ResponseData::ProcStdout { id, data }];
if !reply_2(payload).await {
error!("<Ssh: Proc {}> Stdout channel closed", id);
break;
}
// Pause to allow buffer to fill up a little bit, avoiding
// spamming with a lot of smaller responses
tokio::time::sleep(tokio::time::Duration::from_millis(
READ_PAUSE_MILLIS,
))
.await;
}
Err(x) => {
error!(
"<Ssh: Proc {}> Invalid data read from stdout pipe: {}",
id, x
);
break;
}
},
Ok(_) => break,
Err(x) if x.kind() == io::ErrorKind::WouldBlock => {
// Pause to allow buffer to fill up a little bit, avoiding
// spamming with a lot of smaller responses
tokio::time::sleep(tokio::time::Duration::from_millis(READ_PAUSE_MILLIS))
.await;
}
Err(_) => break,
}
}
});
// Spawn a task that sends stderr as a response
let mut reply_2 = reply.clone();
let stderr_task = tokio::spawn(async move {
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match stderr.read(&mut buf) {
Ok(n) if n > 0 => match String::from_utf8(buf[..n].to_vec()) {
Ok(data) => {
let payload = vec![ResponseData::ProcStderr { id, data }];
if !reply_2(payload).await {
error!("<Ssh: Proc {}> Stderr channel closed", id);
break;
}
// Pause to allow buffer to fill up a little bit, avoiding
// spamming with a lot of smaller responses
tokio::time::sleep(tokio::time::Duration::from_millis(
READ_PAUSE_MILLIS,
))
.await;
}
Err(x) => {
error!(
"<Ssh: Proc {}> Invalid data read from stderr pipe: {}",
id, x
);
break;
}
},
Ok(_) => break,
Err(x) if x.kind() == io::ErrorKind::WouldBlock => {
// Pause to allow buffer to fill up a little bit, avoiding
// spamming with a lot of smaller responses
tokio::time::sleep(tokio::time::Duration::from_millis(READ_PAUSE_MILLIS))
.await;
}
Err(_) => break,
}
}
});
let stdin_task = tokio::spawn(async move {
while let Some(line) = stdin_rx.recv().await {
if let Err(x) = stdin.write_all(line.as_bytes()) {
error!("<Ssh: Proc {}> Failed to send stdin: {}", id, x);
break;
}
}
});
// Spawn a task that waits on the process to exit but can also
// kill the process when triggered
let state_2 = Arc::clone(&state);
let mut reply_2 = reply.clone();
tokio::spawn(async move {
let mut should_kill = false;
let mut success = false;
tokio::select! {
_ = kill_rx.recv() => {
should_kill = true;
}
result = child.async_wait().compat() => {
match result {
Ok(status) => {
success = status.success();
}
Err(x) => {
error!("<Ssh: Proc {}> Waiting on process failed: {}", id, x);
}
}
}
}
// Force stdin task to abort if it hasn't exited as there is no
// point to sending any more stdin
stdin_task.abort();
if should_kill {
debug!("<Ssh: Proc {}> Process killed", id);
if let Err(x) = child.kill() {
error!("<Ssh: Proc {}> Unable to kill process: {}", id, x);
}
// NOTE: At the moment, child.kill does nothing for wezterm_ssh::SshChildProcess;
// so, we need to manually run kill/taskkill to make sure that the
// process is sent a kill signal
if let Some(pid) = child.process_id() {
let _ = session
.exec(&format!("kill -9 {}", pid), None)
.compat()
.await;
let _ = session
.exec(&format!("taskkill /F /PID {}", pid), None)
.compat()
.await;
}
} else {
debug!("<Ssh: Proc {}> Process done", id);
}
if let Err(x) = stderr_task.await {
error!("<Ssh: Proc {}> Join on stderr task failed: {}", id, x);
}
if let Err(x) = stdout_task.await {
error!("<Ssh: Proc {}> Join on stdout task failed: {}", id, x);
}
state_2.lock().await.processes.remove(&id);
let payload = vec![ResponseData::ProcDone {
id,
success: !should_kill && success,
code: None,
}];
if !reply_2(payload).await {
error!("<Ssh: Proc {}> Failed to send done!", id,);
}
});
});
Ok(Outgoing {
data: ResponseData::ProcStart { id },
post_hook: Some(post_hook),
})
}
async fn proc_kill(
_session: WezSession,
state: Arc<Mutex<State>>,
id: usize,
) -> io::Result<Outgoing> {
if let Some(process) = state.lock().await.processes.remove(&id) {
if process.kill_tx.send(()).await.is_ok() {
return Ok(Outgoing::from(ResponseData::Ok));
}
}
Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Unable to send kill signal to process",
))
}
async fn proc_stdin(
_session: WezSession,
state: Arc<Mutex<State>>,
id: usize,
data: String,
) -> io::Result<Outgoing> {
if let Some(process) = state.lock().await.processes.get_mut(&id) {
if process.stdin_tx.send(data).await.is_ok() {
return Ok(Outgoing::from(ResponseData::Ok));
}
}
Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Unable to send stdin to process",
))
}
async fn proc_list(_session: WezSession, state: Arc<Mutex<State>>) -> io::Result<Outgoing> {
Ok(Outgoing::from(ResponseData::ProcEntries {
entries: state
.lock()
.await
.processes
.values()
.map(|p| RunningProcess {
cmd: p.cmd.to_string(),
args: p.args.clone(),
id: p.id,
})
.collect(),
}))
}
async fn system_info(session: WezSession) -> io::Result<Outgoing> {
let current_dir = session
.sftp()
.realpath(".")
.compat()
.await
.map_err(to_other_error)?;
let first_component = current_dir.components().next();
let is_windows =
first_component.is_some() && matches!(first_component.unwrap(), Component::Prefix(_));
let is_unix = current_dir.as_os_str().to_string_lossy().starts_with('/');
let family = if is_windows {
"windows"
} else if is_unix {
"unix"
} else {
""
}
.to_string();
Ok(Outgoing::from(ResponseData::SystemInfo {
family,
os: "".to_string(),
arch: "".to_string(),
current_dir,
main_separator: if is_windows { '\\' } else { '/' },
}))
}

@ -0,0 +1,255 @@
use async_compat::CompatExt;
use distant_core::{Request, Session, Transport};
use log::*;
use smol::channel::Receiver as SmolReceiver;
use std::{
io::{self, Write},
path::PathBuf,
sync::Arc,
};
use tokio::sync::{mpsc, Mutex};
use wezterm_ssh::{Config as WezConfig, Session as WezSession, SessionEvent as WezSessionEvent};
mod handler;
#[derive(Debug)]
pub struct Ssh2AuthPrompt {
/// The label to show when prompting the user
pub prompt: String,
/// If true, the response that the user inputs should be displayed as they type. If false then
/// treat it as a password entry and do not display what is typed in response to this prompt.
pub echo: bool,
}
#[derive(Debug)]
pub struct Ssh2AuthEvent {
/// Represents the name of the user to be authenticated. This may be empty!
pub username: String,
/// Informational text to be displayed to the user prior to the prompt
pub instructions: String,
/// Prompts to be conveyed to the user, each representing a single answer needed
pub prompts: Vec<Ssh2AuthPrompt>,
}
#[derive(Clone, Debug, Default)]
pub struct Ssh2SessionOpts {
pub identity_files: Vec<PathBuf>,
pub identities_only: Option<bool>,
pub port: Option<u16>,
pub proxy_command: Option<String>,
pub user: Option<String>,
pub user_known_hosts_files: Vec<PathBuf>,
}
pub struct Ssh2AuthHandler {
pub on_authenticate: Box<dyn FnMut(Ssh2AuthEvent) -> io::Result<Vec<String>>>,
pub on_banner: Box<dyn FnMut(&str)>,
pub on_host_verify: Box<dyn FnMut(&str) -> io::Result<bool>>,
pub on_error: Box<dyn FnMut(&str)>,
}
impl Default for Ssh2AuthHandler {
fn default() -> Self {
Self {
on_authenticate: Box::new(|ev| {
if !ev.username.is_empty() {
eprintln!("Authentication for {}", ev.username);
}
if !ev.instructions.is_empty() {
eprintln!("{}", ev.instructions);
}
let mut answers = Vec::new();
for prompt in &ev.prompts {
// Contains all prompt lines including same line
let mut prompt_lines = prompt.prompt.split('\n').collect::<Vec<_>>();
// Line that is prompt on same line as answer
let prompt_line = prompt_lines.pop().unwrap();
// Go ahead and display all other lines
for line in prompt_lines.into_iter() {
eprintln!("{}", line);
}
let answer = if prompt.echo {
eprint!("{}", prompt_line);
std::io::stderr().lock().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
answer
} else {
rpassword::prompt_password_stderr(prompt_line)?
};
answers.push(answer);
}
Ok(answers)
}),
on_banner: Box::new(|_| {}),
on_host_verify: Box::new(|message| {
eprintln!("{}", message);
match rpassword::prompt_password_stderr("Enter [y/N]> ")?.as_str() {
"y" | "Y" | "yes" | "YES" => Ok(true),
_ => Ok(false),
}
}),
on_error: Box::new(|_| {}),
}
}
}
pub struct Ssh2Session {
session: WezSession,
events: SmolReceiver<WezSessionEvent>,
}
impl Ssh2Session {
/// Connect to a remote TCP server using SSH
pub fn connect(host: impl AsRef<str>, opts: Ssh2SessionOpts) -> io::Result<Self> {
let mut config = WezConfig::new();
config.add_default_config_files();
// Grab the config for the specific host
let mut config = config.for_host(host.as_ref());
// Override config with any settings provided by session opts
if let Some(port) = opts.port.as_ref() {
config.insert("port".to_string(), port.to_string());
}
if let Some(user) = opts.user.as_ref() {
config.insert("user".to_string(), user.to_string());
}
if !opts.identity_files.is_empty() {
config.insert(
"identityfile".to_string(),
opts.identity_files
.iter()
.filter_map(|p| p.to_str())
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(" "),
);
}
if let Some(yes) = opts.identities_only.as_ref() {
let value = if *yes {
"yes".to_string()
} else {
"no".to_string()
};
config.insert("identitiesonly".to_string(), value);
}
if let Some(cmd) = opts.proxy_command.as_ref() {
config.insert("proxycommand".to_string(), cmd.to_string());
}
if !opts.user_known_hosts_files.is_empty() {
config.insert(
"userknownhostsfile".to_string(),
opts.user_known_hosts_files
.iter()
.filter_map(|p| p.to_str())
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(" "),
);
}
// Establish a connection
let (session, events) =
WezSession::connect(config).map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
Ok(Self { session, events })
}
/// Authenticates the [`Ssh2Session`] and produces a [`Session`]
pub async fn authenticate(self, mut handler: Ssh2AuthHandler) -> io::Result<Session> {
// Perform the authentication by listening for events and continuing to handle them
// until authenticated
while let Ok(event) = self.events.recv().await {
match event {
WezSessionEvent::Banner(banner) => {
if let Some(banner) = banner {
(handler.on_banner)(banner.as_ref());
}
}
WezSessionEvent::HostVerify(verify) => {
let verified = (handler.on_host_verify)(verify.message.as_str())?;
verify
.answer(verified)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
}
WezSessionEvent::Authenticate(mut auth) => {
let ev = Ssh2AuthEvent {
username: auth.username.clone(),
instructions: auth.instructions.clone(),
prompts: auth
.prompts
.drain(..)
.map(|p| Ssh2AuthPrompt {
prompt: p.prompt,
echo: p.echo,
})
.collect(),
};
let answers = (handler.on_authenticate)(ev)?;
auth.answer(answers)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
}
WezSessionEvent::Error(err) => {
(handler.on_error)(&err);
return Err(io::Error::new(io::ErrorKind::PermissionDenied, err));
}
WezSessionEvent::Authenticated => break,
}
}
// We are now authenticated, so convert into a distant session that wraps our ssh2 session
self.into_session()
}
/// Consume [`Ssh2Session`] and produce a distant [`Session`]
fn into_session(self) -> io::Result<Session> {
let (t1, t2) = Transport::pair(1);
let session = Session::initialize(t1)?;
// Spawn tasks that forward requests to the ssh session
// and send back responses from the ssh session
let (mut t_read, mut t_write) = t2.into_split();
let Self {
session: wez_session,
..
} = self;
let (tx, mut rx) = mpsc::channel(1);
tokio::spawn(async move {
let state = Arc::new(Mutex::new(handler::State::default()));
while let Ok(Some(req)) = t_read.receive::<Request>().await {
if let Err(x) =
handler::process(wez_session.clone(), Arc::clone(&state), req, tx.clone()).await
{
error!("{}", x);
}
}
});
tokio::spawn(async move {
while let Some(res) = rx.recv().await {
if t_write.send(res).await.is_err() {
break;
}
}
});
Ok(session)
}
}

@ -0,0 +1,2 @@
mod ssh2;
mod sshd;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,432 @@
use assert_fs::{prelude::*, TempDir};
use distant_core::Session;
use distant_ssh2::{Ssh2AuthHandler, Ssh2Session, Ssh2SessionOpts};
use once_cell::sync::{Lazy, OnceCell};
use rstest::*;
use std::{
collections::HashMap,
fmt, io,
path::Path,
process::{Child, Command},
sync::atomic::{AtomicU16, Ordering},
thread,
time::Duration,
};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
/// NOTE: OpenSSH's sshd requires absolute path
const BIN_PATH_STR: &str = "/usr/sbin/sshd";
/// Port range to use when finding a port to bind to (using IANA guidance)
const PORT_RANGE: (u16, u16) = (49152, 65535);
static USERNAME: Lazy<String> = Lazy::new(whoami::username);
pub struct SshKeygen;
impl SshKeygen {
// ssh-keygen -t rsa -f $ROOT/id_rsa -N "" -q
pub fn generate_rsa(path: impl AsRef<Path>, passphrase: impl AsRef<str>) -> io::Result<bool> {
let res = Command::new("ssh-keygen")
.args(&["-m", "PEM"])
.args(&["-t", "rsa"])
.arg("-f")
.arg(path.as_ref())
.arg("-N")
.arg(passphrase.as_ref())
.arg("-q")
.status()
.map(|status| status.success())?;
#[cfg(unix)]
if res {
// chmod 600 id_rsa* -> ida_rsa + ida_rsa.pub
std::fs::metadata(path.as_ref().with_extension("pub"))?
.permissions()
.set_mode(0o600);
std::fs::metadata(path)?.permissions().set_mode(0o600);
}
Ok(res)
}
}
pub struct SshAgent;
impl SshAgent {
pub fn generate_shell_env() -> io::Result<HashMap<String, String>> {
let output = Command::new("ssh-agent").arg("-s").output()?;
let stdout = String::from_utf8(output.stdout)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
Ok(stdout
.split(';')
.map(str::trim)
.filter(|s| s.contains('='))
.map(|s| {
let mut tokens = s.split('=');
let key = tokens.next().unwrap().trim().to_string();
let rest = tokens
.map(str::trim)
.map(ToString::to_string)
.collect::<Vec<String>>()
.join("=");
(key, rest)
})
.collect::<HashMap<String, String>>())
}
pub fn update_tests_with_shell_env() -> io::Result<()> {
let env_map = Self::generate_shell_env()?;
for (key, value) in env_map {
std::env::set_var(key, value);
}
Ok(())
}
}
#[derive(Debug)]
pub struct SshdConfig(HashMap<String, Vec<String>>);
impl Default for SshdConfig {
fn default() -> Self {
let mut config = Self::new();
config.set_authentication_methods(vec!["publickey".to_string()]);
config.set_use_privilege_separation(false);
config.set_subsystem(true, true);
config.set_use_pam(false);
config.set_x11_forwarding(true);
config.set_print_motd(true);
config.set_permit_tunnel(true);
config.set_kbd_interactive_authentication(true);
config.set_allow_tcp_forwarding(true);
config.set_max_startups(500, None);
config.set_strict_modes(false);
config
}
}
impl SshdConfig {
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn set_authentication_methods(&mut self, methods: Vec<String>) {
self.0.insert("AuthenticationMethods".to_string(), methods);
}
pub fn set_authorized_keys_file(&mut self, path: impl AsRef<Path>) {
self.0.insert(
"AuthorizedKeysFile".to_string(),
vec![path.as_ref().to_string_lossy().to_string()],
);
}
pub fn set_host_key(&mut self, path: impl AsRef<Path>) {
self.0.insert(
"HostKey".to_string(),
vec![path.as_ref().to_string_lossy().to_string()],
);
}
pub fn set_pid_file(&mut self, path: impl AsRef<Path>) {
self.0.insert(
"PidFile".to_string(),
vec![path.as_ref().to_string_lossy().to_string()],
);
}
pub fn set_subsystem(&mut self, sftp: bool, internal_sftp: bool) {
let mut values = Vec::new();
if sftp {
values.push("sftp".to_string());
}
if internal_sftp {
values.push("internal-sftp".to_string());
}
self.0.insert("Subsystem".to_string(), values);
}
pub fn set_use_pam(&mut self, yes: bool) {
self.0.insert("UsePAM".to_string(), Self::yes_value(yes));
}
pub fn set_x11_forwarding(&mut self, yes: bool) {
self.0
.insert("X11Forwarding".to_string(), Self::yes_value(yes));
}
pub fn set_use_privilege_separation(&mut self, yes: bool) {
self.0
.insert("UsePrivilegeSeparation".to_string(), Self::yes_value(yes));
}
pub fn set_print_motd(&mut self, yes: bool) {
self.0.insert("PrintMotd".to_string(), Self::yes_value(yes));
}
pub fn set_permit_tunnel(&mut self, yes: bool) {
self.0
.insert("PermitTunnel".to_string(), Self::yes_value(yes));
}
pub fn set_kbd_interactive_authentication(&mut self, yes: bool) {
self.0.insert(
"KbdInteractiveAuthentication".to_string(),
Self::yes_value(yes),
);
}
pub fn set_allow_tcp_forwarding(&mut self, yes: bool) {
self.0
.insert("AllowTcpForwarding".to_string(), Self::yes_value(yes));
}
pub fn set_max_startups(&mut self, start: u16, rate_full: Option<(u16, u16)>) {
let value = format!(
"{}{}",
start,
rate_full
.map(|(r, f)| format!(":{}:{}", r, f))
.unwrap_or_default(),
);
self.0.insert("MaxStartups".to_string(), vec![value]);
}
pub fn set_strict_modes(&mut self, yes: bool) {
self.0
.insert("StrictModes".to_string(), Self::yes_value(yes));
}
fn yes_value(yes: bool) -> Vec<String> {
vec![Self::yes_string(yes)]
}
fn yes_string(yes: bool) -> String {
Self::yes_str(yes).to_string()
}
const fn yes_str(yes: bool) -> &'static str {
if yes {
"yes"
} else {
"no"
}
}
}
impl fmt::Display for SshdConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (keyword, values) in self.0.iter() {
writeln!(
f,
"{} {}",
keyword,
values
.iter()
.map(|v| {
let v = v.trim();
if v.contains(|c: char| c.is_whitespace()) {
format!("\"{}\"", v)
} else {
v.to_string()
}
})
.collect::<Vec<String>>()
.join(" ")
)?;
}
Ok(())
}
}
/// Context for some sshd instance
pub struct Sshd {
child: Child,
/// Port that sshd is listening on
pub port: u16,
/// Temporary directory used to hold resources for sshd such as its config, keys, and log
pub tmp: TempDir,
}
impl Sshd {
pub fn spawn(mut config: SshdConfig) -> Result<Self, Box<dyn std::error::Error>> {
let tmp = TempDir::new()?;
// Ensure that everything needed for interacting with ssh-agent is set
SshAgent::update_tests_with_shell_env()?;
// ssh-keygen -t rsa -f $ROOT/id_rsa -N "" -q
let id_rsa_file = tmp.child("id_rsa");
assert!(
SshKeygen::generate_rsa(id_rsa_file.path(), "")?,
"Failed to ssh-keygen id_rsa"
);
// cp $ROOT/id_rsa.pub $ROOT/authorized_keys
let authorized_keys_file = tmp.child("authorized_keys");
std::fs::copy(
id_rsa_file.path().with_extension("pub"),
authorized_keys_file.path(),
)?;
// ssh-keygen -t rsa -f $ROOT/ssh_host_rsa_key -N "" -q
let ssh_host_rsa_key_file = tmp.child("ssh_host_rsa_key");
assert!(
SshKeygen::generate_rsa(ssh_host_rsa_key_file.path(), "")?,
"Failed to ssh-keygen ssh_host_rsa_key"
);
config.set_authorized_keys_file(id_rsa_file.path().with_extension("pub"));
config.set_host_key(ssh_host_rsa_key_file.path());
let sshd_pid_file = tmp.child("sshd.pid");
config.set_pid_file(sshd_pid_file.path());
// Generate $ROOT/sshd_config based on config
let sshd_config_file = tmp.child("sshd_config");
sshd_config_file.write_str(&config.to_string())?;
let sshd_log_file = tmp.child("sshd.log");
let (child, port) = Self::try_spawn_next(sshd_config_file.path(), sshd_log_file.path())
.expect("No open port available for sshd");
Ok(Self { child, port, tmp })
}
fn try_spawn_next(
config_path: impl AsRef<Path>,
log_path: impl AsRef<Path>,
) -> io::Result<(Child, u16)> {
static PORT: AtomicU16 = AtomicU16::new(PORT_RANGE.0);
loop {
let port = PORT.fetch_add(1, Ordering::Relaxed);
match Self::try_spawn(port, config_path.as_ref(), log_path.as_ref()) {
// If successful, return our spawned server child process
Ok(Ok(child)) => break Ok((child, port)),
// If the server died when spawned and we reached the final port, we want to exit
Ok(Err((code, msg))) if port == PORT_RANGE.1 => {
break Err(io::Error::new(
io::ErrorKind::Other,
format!(
"{} failed [{}]: {}",
BIN_PATH_STR,
code.map(|x| x.to_string())
.unwrap_or_else(|| String::from("???")),
msg
),
))
}
// If we've reached the final port in our range to try, we want to exit
Err(x) if port == PORT_RANGE.1 => break Err(x),
// Otherwise, try next port
Err(_) | Ok(Err(_)) => continue,
}
}
}
fn try_spawn(
port: u16,
config_path: impl AsRef<Path>,
log_path: impl AsRef<Path>,
) -> io::Result<Result<Child, (Option<i32>, String)>> {
let mut child = Command::new(BIN_PATH_STR)
.arg("-D")
.arg("-p")
.arg(port.to_string())
.arg("-f")
.arg(config_path.as_ref())
.arg("-E")
.arg(log_path.as_ref())
.spawn()?;
// Pause for a little bit to make sure that the server didn't die due to an error
thread::sleep(Duration::from_millis(100));
if let Some(exit_status) = child.try_wait()? {
let output = child.wait_with_output()?;
Ok(Err((
exit_status.code(),
format!(
"{}\n{}",
String::from_utf8(output.stdout).unwrap(),
String::from_utf8(output.stderr).unwrap(),
),
)))
} else {
Ok(Ok(child))
}
}
}
impl Drop for Sshd {
/// Kills server upon drop
fn drop(&mut self) {
let _ = self.child.kill();
}
}
#[fixture]
pub fn logger() -> &'static flexi_logger::LoggerHandle {
static LOGGER: OnceCell<flexi_logger::LoggerHandle> = OnceCell::new();
LOGGER.get_or_init(|| {
// flexi_logger::Logger::try_with_str("off, distant_core=trace, distant_ssh2=trace")
flexi_logger::Logger::try_with_str("off, distant_core=warn, distant_ssh2=warn")
.expect("Failed to load env")
.start()
.expect("Failed to start logger")
})
}
#[fixture]
pub fn sshd() -> &'static Sshd {
static SSHD: OnceCell<Sshd> = OnceCell::new();
SSHD.get_or_init(|| Sshd::spawn(Default::default()).unwrap())
}
#[fixture]
pub async fn session(sshd: &'_ Sshd, _logger: &'_ flexi_logger::LoggerHandle) -> Session {
let port = sshd.port;
Ssh2Session::connect(
"127.0.0.1",
Ssh2SessionOpts {
port: Some(port),
identity_files: vec![sshd.tmp.child("id_rsa").path().to_path_buf()],
identities_only: Some(true),
user: Some(USERNAME.to_string()),
user_known_hosts_files: vec![sshd.tmp.child("known_hosts").path().to_path_buf()],
..Default::default()
},
)
.unwrap()
.authenticate(Ssh2AuthHandler {
on_authenticate: Box::new(|ev| {
println!("on_authenticate: {:?}", ev);
Ok(vec![String::new(); ev.prompts.len()])
}),
on_host_verify: Box::new(|host| {
println!("on_host_verify: {}", host);
Ok(true)
}),
..Default::default()
})
.await
.unwrap()
}

@ -1,3 +1,4 @@
use once_cell::sync::Lazy;
use std::{env, path::PathBuf};
/// Represents the maximum size (in bytes) that data will be read from pipes
@ -14,15 +15,16 @@ pub const SERVER_CONN_MSG_CAPACITY: usize = 10000;
/// before failing (0 meaning indefinitely)
pub const TIMEOUT: usize = 15000;
lazy_static::lazy_static! {
pub static ref TIMEOUT_STR: String = TIMEOUT.to_string();
pub static ref SERVER_CONN_MSG_CAPACITY_STR: String = SERVER_CONN_MSG_CAPACITY.to_string();
pub static TIMEOUT_STR: Lazy<String> = Lazy::new(|| TIMEOUT.to_string());
pub static SERVER_CONN_MSG_CAPACITY_STR: Lazy<String> =
Lazy::new(|| SERVER_CONN_MSG_CAPACITY.to_string());
/// Represents the path to the global session file
pub static ref SESSION_FILE_PATH: PathBuf = env::temp_dir().join("distant.session");
pub static ref SESSION_FILE_PATH_STR: String = SESSION_FILE_PATH.to_string_lossy().to_string();
/// Represents the path to the global session file
pub static SESSION_FILE_PATH: Lazy<PathBuf> = Lazy::new(|| env::temp_dir().join("distant.session"));
pub static SESSION_FILE_PATH_STR: Lazy<String> =
Lazy::new(|| SESSION_FILE_PATH.to_string_lossy().to_string());
/// Represents the path to a socket to communicate instead of a session file
pub static ref SESSION_SOCKET_PATH: PathBuf = env::temp_dir().join("distant.sock");
pub static ref SESSION_SOCKET_PATH_STR: String = SESSION_SOCKET_PATH.to_string_lossy().to_string();
}
/// Represents the path to a socket to communicate instead of a session file
pub static SESSION_SOCKET_PATH: Lazy<PathBuf> = Lazy::new(|| env::temp_dir().join("distant.sock"));
pub static SESSION_SOCKET_PATH_STR: Lazy<String> =
Lazy::new(|| SESSION_SOCKET_PATH.to_string_lossy().to_string());

@ -7,7 +7,7 @@ use crate::{
};
use derive_more::{Display, Error, From, IsVariant};
use distant_core::{PortRange, RequestData};
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::{
env,
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr},
@ -18,9 +18,7 @@ use std::{
use structopt::StructOpt;
use strum::{EnumString, EnumVariantNames, IntoStaticStr, VariantNames};
lazy_static! {
static ref USERNAME: String = whoami::username();
}
static USERNAME: Lazy<String> = Lazy::new(whoami::username);
/// Options and commands to apply to binary
#[derive(Debug, StructOpt)]
@ -123,6 +121,22 @@ pub struct SessionOpt {
pub session_socket: PathBuf,
}
/// Contains options related ssh
#[derive(Clone, Debug, StructOpt)]
pub struct SshConnectionOpts {
/// Host to use for connection to when using SSH method
#[structopt(name = "ssh-host", long, default_value = "localhost")]
pub host: String,
/// Port to use for connection when using SSH method
#[structopt(name = "ssh-port", long, default_value = "22")]
pub port: u16,
/// Alternative user for connection when using SSH method
#[structopt(name = "ssh-user", long)]
pub user: Option<String>,
}
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Performs some action on a remote machine
@ -165,6 +179,35 @@ impl Subcommand {
}
}
/// Represents the method to use in communicating with a remote machine
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
IsVariant,
IntoStaticStr,
EnumString,
EnumVariantNames,
)]
#[strum(serialize_all = "snake_case")]
pub enum Method {
/// Launch/connect to a distant server running on a remote machine
Distant,
/// Connect to an SSH server running on a remote machine
#[cfg(feature = "ssh2")]
Ssh,
}
impl Default for Method {
fn default() -> Self {
Self::Distant
}
}
/// Represents the format for data communicated to & from the client
#[derive(
Copy,
@ -208,9 +251,20 @@ pub struct ActionSubcommand {
)]
pub format: Format,
/// Method to communicate with a remote machine
#[structopt(
short,
long,
case_insensitive = true,
default_value = Method::default().into(),
possible_values = Method::VARIANTS
)]
pub method: Method,
/// Represents the medium for retrieving a session for use in performing the action
#[structopt(
long,
case_insensitive = true,
default_value = SessionInput::default().into(),
possible_values = SessionInput::VARIANTS
)]
@ -220,6 +274,10 @@ pub struct ActionSubcommand {
#[structopt(flatten)]
pub session_data: SessionOpt,
/// SSH connection settings when method is ssh
#[structopt(flatten)]
pub ssh_connection: SshConnectionOpts,
/// If specified, commands to send are sent over stdin and responses are received
/// over stdout (and stderr if mode is shell)
#[structopt(short, long)]
@ -574,9 +632,20 @@ pub struct LspSubcommand {
)]
pub format: Format,
/// Method to communicate with a remote machine
#[structopt(
short,
long,
case_insensitive = true,
default_value = Method::default().into(),
possible_values = Method::VARIANTS
)]
pub method: Method,
/// Represents the medium for retrieving a session to use when running a remote LSP server
#[structopt(
long,
case_insensitive = true,
default_value = SessionInput::default().into(),
possible_values = SessionInput::VARIANTS
)]
@ -586,6 +655,10 @@ pub struct LspSubcommand {
#[structopt(flatten)]
pub session_data: SessionOpt,
/// SSH connection settings when method is ssh
#[structopt(flatten)]
pub ssh_connection: SshConnectionOpts,
/// Command to run on the remote machine that represents an LSP server
pub cmd: String,

@ -1,9 +1,10 @@
use crate::{
exit::{ExitCode, ExitCodeError},
link::RemoteProcessLink,
opt::{ActionSubcommand, CommonOpt, Format, SessionInput},
opt::{ActionSubcommand, CommonOpt, Format},
output::ResponseOut,
session::CliSession,
subcommand::CommandRunner,
utils,
};
use derive_more::{Display, Error, From};
@ -52,17 +53,26 @@ pub fn run(cmd: ActionSubcommand, opt: CommonOpt) -> Result<(), Error> {
}
async fn run_async(cmd: ActionSubcommand, opt: CommonOpt) -> Result<(), Error> {
let method = cmd.method;
let ssh_connection = cmd.ssh_connection.clone();
let session_input = cmd.session;
let timeout = opt.to_timeout_duration();
let session_file = cmd.session_data.session_file.clone();
let session_socket = cmd.session_data.session_socket.clone();
extract_session_and_start!(
cmd,
cmd.session,
CommandRunner {
method,
ssh_connection,
session_input,
session_file,
session_socket,
timeout,
|cmd, session, timeout, lsp_data| start(cmd, session, timeout, lsp_data)
}
.run(
|session, timeout, lsp_data| Box::pin(start(cmd, session, timeout, lsp_data)),
Error::Io,
)
.await
}
async fn start(

@ -1,7 +1,8 @@
use crate::{
exit::{ExitCode, ExitCodeError},
link::RemoteProcessLink,
opt::{CommonOpt, LspSubcommand, SessionInput},
opt::{CommonOpt, LspSubcommand},
subcommand::CommandRunner,
utils,
};
use derive_more::{Display, Error, From};
@ -40,17 +41,26 @@ pub fn run(cmd: LspSubcommand, opt: CommonOpt) -> Result<(), Error> {
}
async fn run_async(cmd: LspSubcommand, opt: CommonOpt) -> Result<(), Error> {
let method = cmd.method;
let timeout = opt.to_timeout_duration();
let ssh_connection = cmd.ssh_connection.clone();
let session_input = cmd.session;
let session_file = cmd.session_data.session_file.clone();
let session_socket = cmd.session_data.session_socket.clone();
extract_session_and_start!(
cmd,
cmd.session,
CommandRunner {
method,
ssh_connection,
session_input,
session_file,
session_socket,
timeout,
|cmd, session, _, lsp_data| start(cmd, session, lsp_data)
}
.run(
|session, _, lsp_data| Box::pin(start(cmd, session, lsp_data)),
Error::Io,
)
.await
}
async fn start(

@ -1,80 +1,166 @@
macro_rules! extract_session_and_start {
(
$cmd:expr,
$session_ty:expr,
$session_file:expr,
$session_socket:expr,
$timeout:expr,
$start:expr
) => {{
use distant_core::{PlainCodec, SessionInfo, SessionInfoFile, XChaCha20Poly1305Codec};
match $session_ty {
SessionInput::Environment => {
let info = SessionInfo::from_environment()?;
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
$start(
$cmd,
Session::tcp_connect_timeout(addr, codec, $timeout).await?,
$timeout,
None,
use crate::opt::{Method, SessionInput, SshConnectionOpts};
use distant_core::{
LspData, PlainCodec, Session, SessionInfo, SessionInfoFile, XChaCha20Poly1305Codec,
};
use std::{
future::Future,
io,
net::SocketAddr,
path::{Path, PathBuf},
pin::Pin,
time::Duration,
};
pub mod action;
pub mod launch;
pub mod listen;
pub mod lsp;
struct CommandRunner {
method: Method,
ssh_connection: SshConnectionOpts,
session_input: SessionInput,
session_file: PathBuf,
session_socket: PathBuf,
timeout: Duration,
}
impl CommandRunner {
async fn run<F1, F2, E>(self, start: F1, wrap_err: F2) -> Result<(), E>
where
F1: FnOnce(
Session,
Duration,
Option<LspData>,
) -> Pin<Box<dyn Future<Output = Result<(), E>>>>,
F2: Fn(io::Error) -> E + Copy,
E: std::error::Error,
{
let CommandRunner {
method,
ssh_connection,
session_input,
session_file,
session_socket,
timeout,
} = self;
let (session, lsp_data) = match method {
#[cfg(feature = "ssh2")]
Method::Ssh => {
use distant_ssh2::{Ssh2Session, Ssh2SessionOpts};
let SshConnectionOpts { host, port, user } = ssh_connection;
let session = Ssh2Session::connect(
host,
Ssh2SessionOpts {
port: Some(port),
user,
..Default::default()
},
)
.map_err(wrap_err)?
.authenticate(Default::default())
.await
.map_err(wrap_err)?;
(session, None)
}
SessionInput::File => {
let info: SessionInfo = SessionInfoFile::load_from($session_file).await?.into();
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
$start(
$cmd,
Session::tcp_connect_timeout(addr, codec, $timeout).await?,
$timeout,
None,
)
.await
Method::Distant => {
let params = retrieve_session_params(session_input, session_file, session_socket)
.await
.map_err(wrap_err)?;
match params {
SessionParams::Tcp {
addr,
codec,
lsp_data,
} => {
let session = Session::tcp_connect_timeout(addr, codec, timeout)
.await
.map_err(wrap_err)?;
(session, lsp_data)
}
SessionParams::Socket { path, codec } => {
let session = Session::unix_connect_timeout(path, codec, timeout)
.await
.map_err(wrap_err)?;
(session, None)
}
}
}
SessionInput::Pipe => {
let info = SessionInfo::from_stdin()?;
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
$start(
$cmd,
Session::tcp_connect_timeout(addr, codec, $timeout).await?,
$timeout,
None,
)
.await
};
start(session, timeout, lsp_data).await
}
}
enum SessionParams {
Tcp {
addr: SocketAddr,
codec: XChaCha20Poly1305Codec,
lsp_data: Option<LspData>,
},
Socket {
path: PathBuf,
codec: PlainCodec,
},
}
async fn retrieve_session_params(
session_input: SessionInput,
session_file: impl AsRef<Path>,
session_socket: impl AsRef<Path>,
) -> io::Result<SessionParams> {
Ok(match session_input {
SessionInput::Environment => {
let info = SessionInfo::from_environment()?;
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
SessionParams::Tcp {
addr,
codec,
lsp_data: None,
}
SessionInput::Lsp => {
let mut data = LspData::from_buf_reader(&mut std::io::stdin().lock())
.map_err(io::Error::from)?;
let info = data.take_session_info().map_err(io::Error::from)?;
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
$start(
$cmd,
Session::tcp_connect_timeout(addr, codec, $timeout).await?,
$timeout,
Some(data),
)
.await
}
SessionInput::File => {
let info: SessionInfo = SessionInfoFile::load_from(session_file).await?.into();
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
SessionParams::Tcp {
addr,
codec,
lsp_data: None,
}
#[cfg(unix)]
SessionInput::Socket => {
$start(
$cmd,
Session::unix_connect_timeout($session_socket, PlainCodec::new(), $timeout)
.await?,
$timeout,
None,
)
.await
}
SessionInput::Pipe => {
let info = SessionInfo::from_stdin()?;
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
SessionParams::Tcp {
addr,
codec,
lsp_data: None,
}
}
}};
SessionInput::Lsp => {
let mut data =
LspData::from_buf_reader(&mut io::stdin().lock()).map_err(io::Error::from)?;
let info = data.take_session_info().map_err(io::Error::from)?;
let addr = info.to_socket_addr().await?;
let codec = XChaCha20Poly1305Codec::from(info.key);
SessionParams::Tcp {
addr,
codec,
lsp_data: Some(data),
}
}
#[cfg(unix)]
SessionInput::Socket => {
let path = session_socket.as_ref().to_path_buf();
let codec = PlainCodec::new();
SessionParams::Socket { path, codec }
}
})
}
pub mod action;
pub mod launch;
pub mod listen;
pub mod lsp;

@ -9,52 +9,67 @@ use distant_core::{
data::{Error, ErrorKind},
Request, RequestData, Response, ResponseData,
};
use once_cell::sync::Lazy;
use rstest::*;
use std::{io::Write, time::Duration};
lazy_static::lazy_static! {
static ref TEMP_SCRIPT_DIR: assert_fs::TempDir = assert_fs::TempDir::new().unwrap();
static ref SCRIPT_RUNNER: String = String::from("bash");
static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> = Lazy::new(|| assert_fs::TempDir::new().unwrap());
static SCRIPT_RUNNER: Lazy<String> = Lazy::new(|| String::from("bash"));
static ref ECHO_ARGS_TO_STDOUT_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
script.write_str(indoc::indoc!(r#"
static ECHO_ARGS_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$@"
"#)).unwrap();
script
};
static ref ECHO_ARGS_TO_STDERR_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
script.write_str(indoc::indoc!(r#"
printf "%s" "$*"
"#
))
.unwrap();
script
});
static ECHO_ARGS_TO_STDERR_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$@" 1>&2
"#)).unwrap();
script
};
static ref ECHO_STDIN_TO_STDOUT_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
script.write_str(indoc::indoc!(r#"
printf "%s" "$*" 1>&2
"#
))
.unwrap();
script
});
static ECHO_STDIN_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
while IFS= read; do echo "$REPLY"; done
"#)).unwrap();
script
};
static ref EXIT_CODE_SH: assert_fs::fixture::ChildPath = {
let script = TEMP_SCRIPT_DIR.child("exit_code.sh");
script.write_str(indoc::indoc!(r#"
"#
))
.unwrap();
script
});
static EXIT_CODE_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("exit_code.sh");
script
.write_str(indoc::indoc!(
r#"
#!/usr/bin/env bash
exit "$1"
"#)).unwrap();
script
};
"#
))
.unwrap();
script
});
static ref DOES_NOT_EXIST_BIN: assert_fs::fixture::ChildPath =
TEMP_SCRIPT_DIR.child("does_not_exist_bin");
}
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
macro_rules! next_two_msgs {
($rx:expr) => {{

@ -1,6 +1,7 @@
use crate::cli::utils;
use assert_cmd::Command;
use distant_core::*;
use once_cell::sync::OnceCell;
use rstest::*;
use std::{ffi::OsStr, net::SocketAddr, thread};
use tokio::{runtime::Runtime, sync::mpsc};
@ -86,11 +87,9 @@ impl Drop for DistantServerCtx {
#[fixture]
pub fn ctx() -> &'static DistantServerCtx {
lazy_static::lazy_static! {
static ref CTX: DistantServerCtx = DistantServerCtx::initialize();
}
static CTX: OnceCell<DistantServerCtx> = OnceCell::new();
&CTX
CTX.get_or_init(DistantServerCtx::initialize)
}
#[fixture]

@ -1,4 +1,5 @@
use crate::cli::fixtures::DistantServerCtx;
use once_cell::sync::Lazy;
use predicates::prelude::*;
use std::{
env, io,
@ -8,11 +9,9 @@ use std::{
time::{Duration, Instant},
};
lazy_static::lazy_static! {
/// Predicate that checks for a single line that is a failure
pub static ref FAILURE_LINE: predicates::str::RegexPredicate =
regex_pred(r"^Failed \(.*\): '.*'\.\n$");
}
/// Predicate that checks for a single line that is a failure
pub static FAILURE_LINE: Lazy<predicates::str::RegexPredicate> =
Lazy::new(|| regex_pred(r"^Failed \(.*\): '.*'\.\n$"));
/// Produces a regex predicate using the given string
pub fn regex_pred(s: &str) -> predicates::str::RegexPredicate {

Loading…
Cancel
Save