Complete shell support (#89)

pull/96/head
Chip Senkbeil 2 years ago committed by GitHub
parent c6c07c5c2c
commit 050bb3496a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

466
Cargo.lock generated

@ -168,12 +168,12 @@ checksum = "b21b63ab5a0db0369deb913540af2892750e42d949faacc7a61495ac418a1692"
dependencies = [
"async-io",
"blocking",
"cfg-if",
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
"libc",
"once_cell",
"signal-hook",
"signal-hook 0.3.10",
"winapi",
]
@ -218,6 +218,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
name = "blocking"
version = "1.0.2"
@ -284,6 +293,12 @@ version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -296,7 +311,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"cipher",
"cpufeatures",
"zeroize",
@ -409,7 +424,7 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"lazy_static",
]
@ -431,16 +446,46 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "dirs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
"cfg-if 0.1.10",
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@ -454,7 +499,7 @@ dependencies = [
[[package]]
name = "distant"
version = "0.15.1"
version = "0.16.0"
dependencies = [
"assert_cmd",
"assert_fs",
@ -467,19 +512,21 @@ dependencies = [
"log",
"once_cell",
"predicates",
"rand",
"rand 0.8.4",
"rstest",
"serde",
"serde_json",
"structopt",
"strum",
"terminal_size",
"termwiz",
"tokio",
"whoami",
]
[[package]]
name = "distant-core"
version = "0.15.1"
version = "0.16.0"
dependencies = [
"assert_fs",
"bytes",
@ -491,9 +538,9 @@ dependencies = [
"indoc",
"log",
"once_cell",
"portable-pty 0.7.0",
"portable-pty",
"predicates",
"rand",
"rand 0.8.4",
"serde",
"serde_json",
"structopt",
@ -505,7 +552,7 @@ dependencies = [
[[package]]
name = "distant-lua"
version = "0.15.1"
version = "0.16.0"
dependencies = [
"distant-core",
"distant-ssh2",
@ -539,7 +586,7 @@ dependencies = [
[[package]]
name = "distant-ssh2"
version = "0.15.1"
version = "0.16.0"
dependencies = [
"assert_cmd",
"assert_fs",
@ -551,7 +598,7 @@ dependencies = [
"log",
"once_cell",
"predicates",
"rand",
"rand 0.8.4",
"rpassword",
"rstest",
"serde",
@ -797,15 +844,26 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi",
"wasi 0.10.2+wasi-snapshot-preview1",
]
[[package]]
@ -901,7 +959,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@ -949,6 +1007,29 @@ version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "libssh-rs"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a24b7857c3bd6f52e22f1ca5445fad27f65719126de518abec5c1648f232e42"
dependencies = [
"bitflags",
"libssh-rs-sys",
"thiserror",
]
[[package]]
name = "libssh-rs-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d592a55d4efe34f3e437e3f74e32b6d60d54aa3270fe2925840173c7d8648a42"
dependencies = [
"cc",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libssh2-sys"
version = "0.2.23"
@ -990,7 +1071,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@ -1004,9 +1085,9 @@ dependencies = [
[[package]]
name = "luajit-src"
version = "210.2.0+resty5f13855"
version = "210.3.2+resty1085a4d"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f85722ea9e022305a077b916c9271011a195ee8dc9b2b764fc78b0378e3b72"
checksum = "b1e27456f513225a9edd22fc0a5f526323f6adb3099c4de87a84ceb842d93ba4"
dependencies = [
"cc",
]
@ -1017,6 +1098,12 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmem"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
[[package]]
name = "mio"
version = "0.7.13"
@ -1041,9 +1128,9 @@ dependencies = [
[[package]]
name = "mlua"
version = "0.6.6"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4235d7e740d73d7429df6f176c81b248f05c39d67264d45a7d8cecb67c227f6f"
checksum = "7d4c93ad12064932ae8f0667ecd09ca714ff44813fa1d1965ae4279108b67f21"
dependencies = [
"bstr 0.2.17",
"cc",
@ -1057,6 +1144,7 @@ dependencies = [
"num-traits",
"once_cell",
"pkg-config",
"rustc-hash",
"serde",
]
@ -1075,6 +1163,16 @@ dependencies = [
"syn",
]
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"memchr",
"version_check",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
@ -1090,6 +1188,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.44"
@ -1139,18 +1248,18 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl-src"
version = "111.16.0+1.1.1l"
version = "300.0.4+3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab2173f69416cf3ec12debb5823d244127d23a9b127d5a5189aa97c5fa2859f"
checksum = "216e1c6b4549e24182b9d7aa268f645414888a69daf44c7b2d8118da8e7b23e7"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.67"
version = "0.9.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
dependencies = [
"autocfg",
"cc",
@ -1160,6 +1269,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ordered-float"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
dependencies = [
"num-traits",
]
[[package]]
name = "parking"
version = "2.0.0"
@ -1183,7 +1301,7 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall",
@ -1197,6 +1315,53 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
"phf_shared",
"rand 0.7.3",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.7"
@ -1221,7 +1386,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"log",
"wepoll-ffi",
@ -1239,24 +1404,6 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "portable-pty"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b8383c3934bd6da733223ad1b22f8102c46e8bbced07800b2346cc34326ff83"
dependencies = [
"anyhow",
"bitflags",
"filedescriptor",
"lazy_static",
"libc",
"log",
"serial",
"shared_library",
"shell-words",
"winapi",
]
[[package]]
name = "portable-pty"
version = "0.7.0"
@ -1365,6 +1512,20 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc 0.2.0",
"rand_pcg",
]
[[package]]
name = "rand"
version = "0.8.4"
@ -1372,9 +1533,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rand_hc 0.3.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
@ -1384,7 +1555,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
@ -1393,7 +1573,16 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
"getrandom 0.2.3",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@ -1402,7 +1591,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
"rand_core 0.6.3",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@ -1420,7 +1618,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"getrandom 0.2.3",
"redox_syscall",
]
@ -1472,20 +1670,26 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2288c66aeafe3b2ed227c981f364f9968fa952ef0b30e84ada4486e7ee24d00a"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
"semver 1.0.4",
]
[[package]]
@ -1515,12 +1719,30 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "serde"
version = "1.0.130"
@ -1581,7 +1803,7 @@ dependencies = [
"ioctl-rs",
"libc",
"serial-core",
"termios",
"termios 0.2.2",
]
[[package]]
@ -1594,6 +1816,19 @@ dependencies = [
"serial-core",
]
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "shared_library"
version = "0.1.9"
@ -1610,6 +1845,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
[[package]]
name = "signal-hook"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook"
version = "0.3.10"
@ -1640,6 +1885,12 @@ dependencies = [
"termcolor",
]
[[package]]
name = "siphasher"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e"
[[package]]
name = "slab"
version = "0.4.4"
@ -1766,9 +2017,9 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"rand",
"rand 0.8.4",
"redox_syscall",
"remove_dir_all",
"winapi",
@ -1783,6 +2034,29 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "terminfo"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
dependencies = [
"dirs",
"fnv",
"nom",
"phf",
"phf_codegen",
]
[[package]]
name = "termios"
version = "0.2.2"
@ -1792,12 +2066,53 @@ dependencies = [
"libc",
]
[[package]]
name = "termios"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
dependencies = [
"libc",
]
[[package]]
name = "termtree"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78fbf2dd23e79c28ccfa2472d3e6b3b189866ffef1aeb91f17c2d968b6586378"
[[package]]
name = "termwiz"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31ef6892cc0348a9b3b8c377addba91e0f6365863d92354bf27559dca81ee8c5"
dependencies = [
"anyhow",
"base64",
"bitflags",
"cfg-if 1.0.0",
"filedescriptor",
"hex",
"lazy_static",
"libc",
"log",
"memmem",
"num-derive",
"num-traits",
"ordered-float",
"regex",
"semver 0.11.0",
"sha2",
"signal-hook 0.1.17",
"terminfo",
"termios 0.3.3",
"thiserror",
"ucd-trie",
"unicode-segmentation",
"vtparse",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -1897,6 +2212,12 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
@ -1931,6 +2252,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -1949,6 +2276,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "vtparse"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f41c9314c4dde1f43dd0c46c67bb5ae73850ce11eebaf7d8b912e178bda5401"
dependencies = [
"utf8parse",
]
[[package]]
name = "wait-timeout"
version = "0.2.0"
@ -1975,6 +2311,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
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"
@ -1987,7 +2329,7 @@ version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
@ -2056,9 +2398,9 @@ dependencies = [
[[package]]
name = "wezterm-ssh"
version = "0.2.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e233b00aaa22b6ef9b573534e529e9672da8662a8355c37acacedd88741ce1ec"
checksum = "21b5be360161357d5504d2b773d51fb23c4f9e92507720ef51821444d60cb5bb"
dependencies = [
"anyhow",
"base64",
@ -2067,8 +2409,10 @@ dependencies = [
"dirs-next",
"filedescriptor",
"filenamegen",
"libc",
"libssh-rs",
"log",
"portable-pty 0.5.0",
"portable-pty",
"regex",
"smol",
"ssh2",

@ -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.15.1"
version = "0.16.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
@ -25,20 +25,22 @@ ssh2 = ["distant-ssh2"]
[dependencies]
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] }
distant-core = { version = "=0.15.1", path = "distant-core", features = ["structopt"] }
distant-core = { version = "=0.16.0", path = "distant-core", features = ["structopt"] }
flexi_logger = "0.18.0"
log = "0.4.14"
once_cell = "1.8.0"
rand = { version = "0.8.4", features = ["getrandom"] }
tokio = { version = "1.12.0", features = ["full"] }
serde = { version = "1.0.126", features = ["derive"] }
serde_json = "1.0.64"
structopt = "0.3.22"
strum = { version = "0.21.0", features = ["derive"] }
tokio = { version = "1.12.0", features = ["full"] }
terminal_size = "0.1.17"
termwiz = "0.15.0"
whoami = "1.1.2"
# Optional native SSH functionality
distant-ssh2 = { version = "=0.15.1", path = "distant-ssh2", features = ["serde"], optional = true }
distant-ssh2 = { version = "=0.16.0", path = "distant-ssh2", features = ["serde"], optional = true }
[target.'cfg(unix)'.dependencies]
fork = "0.1.18"

@ -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.15.1"
version = "0.16.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"

@ -61,8 +61,11 @@ pub struct RemoteProcess {
/// Receiver for stderr
pub stderr: Option<RemoteStderr>,
/// Sender for resize events
resizer: RemoteProcessResizer,
/// Sender for kill events
kill: mpsc::Sender<()>,
killer: RemoteProcessKiller,
/// Task that waits for the process to complete
wait_task: JoinHandle<()>,
@ -134,6 +137,7 @@ impl RemoteProcess {
let (stdin_tx, stdin_rx) = mpsc::channel(CLIENT_PIPE_CAPACITY);
let (stdout_tx, stdout_rx) = mpsc::channel(CLIENT_PIPE_CAPACITY);
let (stderr_tx, stderr_rx) = mpsc::channel(CLIENT_PIPE_CAPACITY);
let (resize_tx, resize_rx) = mpsc::channel(1);
// Used to terminate request task, either explicitly by the process or internally
// by the response task when it terminates
@ -161,7 +165,7 @@ impl RemoteProcess {
_ = abort_req_task_rx.recv() => {
panic!("killed");
}
res = process_outgoing_requests(tenant, id, channel, stdin_rx, kill_rx) => {
res = process_outgoing_requests(tenant, id, channel, stdin_rx, resize_rx, kill_rx) => {
res
}
}
@ -185,7 +189,8 @@ impl RemoteProcess {
stdin: Some(RemoteStdin(stdin_tx)),
stdout: Some(RemoteStdout(stdout_rx)),
stderr: Some(RemoteStderr(stderr_rx)),
kill: kill_tx,
resizer: RemoteProcessResizer(resize_tx),
killer: RemoteProcessKiller(kill_tx),
wait_task,
status,
})
@ -225,6 +230,26 @@ impl RemoteProcess {
.unwrap_or(Err(RemoteProcessError::UnexpectedEof))
}
/// Resizes the pty of the remote process if it is attached to one
pub async fn resize(&self, size: PtySize) -> Result<(), RemoteProcessError> {
self.resizer.resize(size).await
}
/// Clones a copy of the remote process pty resizer
pub fn clone_resizer(&self) -> RemoteProcessResizer {
self.resizer.clone()
}
/// Submits a kill request for the running process
pub async fn kill(&mut self) -> Result<(), RemoteProcessError> {
self.killer.kill().await
}
/// Clones a copy of the remote process killer
pub fn clone_killer(&self) -> RemoteProcessKiller {
self.killer.clone()
}
/// Aborts the process by forcing its response task to shutdown, which means that a call
/// to `wait` will return an error. Note that this does **not** send a kill request, so if
/// you want to be nice you should send the request before aborting.
@ -232,10 +257,31 @@ impl RemoteProcess {
let _ = self.abort_req_task_tx.try_send(());
let _ = self.abort_res_task_tx.try_send(());
}
}
/// A handle to the channel to kill a remote process
#[derive(Clone, Debug)]
pub struct RemoteProcessResizer(mpsc::Sender<PtySize>);
impl RemoteProcessResizer {
/// Resizes the pty of the remote process if it is attached to one
pub async fn resize(&self, size: PtySize) -> Result<(), RemoteProcessError> {
self.0
.send(size)
.await
.map_err(|_| RemoteProcessError::ChannelDead)?;
Ok(())
}
}
/// A handle to the channel to kill a remote process
#[derive(Clone, Debug)]
pub struct RemoteProcessKiller(mpsc::Sender<()>);
impl RemoteProcessKiller {
/// Submits a kill request for the running process
pub async fn kill(&mut self) -> Result<(), RemoteProcessError> {
self.kill
self.0
.send(())
.await
.map_err(|_| RemoteProcessError::ChannelDead)?;
@ -244,10 +290,15 @@ impl RemoteProcess {
}
/// A handle to a remote process' standard input (stdin)
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct RemoteStdin(mpsc::Sender<Vec<u8>>);
impl RemoteStdin {
/// Creates a disconnected remote stdin
pub fn disconnected() -> Self {
Self(mpsc::channel(1).0)
}
/// Tries to write to the stdin of the remote process, returning ok if immediately
/// successful, `WouldBlock` if would need to wait to send data, and `BrokenPipe`
/// if stdin has been closed
@ -374,6 +425,7 @@ async fn process_outgoing_requests(
id: usize,
mut channel: SessionChannel,
mut stdin_rx: mpsc::Receiver<Vec<u8>>,
mut resize_rx: mpsc::Receiver<PtySize>,
mut kill_rx: mpsc::Receiver<()>,
) -> Result<(), RemoteProcessError> {
let result = loop {
@ -389,6 +441,17 @@ async fn process_outgoing_requests(
None => break Err(RemoteProcessError::ChannelDead),
}
}
size = resize_rx.recv() => {
match size {
Some(size) => channel.fire(
Request::new(
tenant.as_str(),
vec![RequestData::ProcResizePty { id, size }]
)
).await?,
None => break Err(RemoteProcessError::ChannelDead),
}
}
msg = kill_rx.recv() => {
if msg.is_some() {
channel.fire(Request::new(

@ -1,4 +1,5 @@
use derive_more::{Display, Error, IsVariant};
use portable_pty::PtySize as PortablePtySize;
use serde::{Deserialize, Serialize};
use std::{io, num::ParseIntError, path::PathBuf, str::FromStr};
use strum::AsRefStr;
@ -210,7 +211,7 @@ pub enum RequestData {
},
/// Spawns a new process on the remote machine
#[cfg_attr(feature = "structopt", structopt(visible_aliases = &["run"]))]
#[cfg_attr(feature = "structopt", structopt(visible_aliases = &["spawn", "run"]))]
ProcSpawn {
/// Name of the command to run
cmd: String,
@ -396,9 +397,11 @@ pub struct PtySize {
pub cols: u16,
/// Width of a cell in pixels. Note that some systems never fill this value and ignore it.
#[serde(default)]
pub pixel_width: u16,
/// Height of a cell in pixels. Note that some systems never fill this value and ignore it.
#[serde(default)]
pub pixel_height: u16,
}
@ -408,6 +411,38 @@ impl PtySize {
Self {
rows,
cols,
..Default::default()
}
}
}
impl From<PortablePtySize> for PtySize {
fn from(size: PortablePtySize) -> Self {
Self {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
}
}
}
impl From<PtySize> for PortablePtySize {
fn from(size: PtySize) -> Self {
Self {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
}
}
}
impl Default for PtySize {
fn default() -> Self {
PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
}

@ -1,10 +1,12 @@
use crate::{
constants::{MAX_PIPE_CHUNK_SIZE, READ_PAUSE_MILLIS},
data::{
self, DirEntry, FileType, Metadata, PtySize, Request, RequestData, Response, ResponseData,
RunningProcess, SystemInfo,
},
server::distant::state::{Process, State},
server::distant::{
process::{Process, PtyProcess, SimpleProcess},
state::{ProcessState, State},
},
};
use derive_more::{Display, Error, From};
use futures::future;
@ -14,14 +16,12 @@ use std::{
future::Future,
path::{Path, PathBuf},
pin::Pin,
process::Stdio,
sync::Arc,
time::SystemTime,
};
use tokio::{
io::{self, AsyncReadExt, AsyncWriteExt},
process::Command,
sync::{mpsc, oneshot, Mutex, MutexGuard},
io::{self, AsyncWriteExt},
sync::{mpsc, Mutex},
};
use walkdir::WalkDir;
@ -43,7 +43,7 @@ impl From<ServerError> for ResponseData {
}
}
type PostHook = Box<dyn FnOnce(MutexGuard<'_, State>) + Send>;
type PostHook = Box<dyn FnOnce() + Send>;
struct Outgoing {
data: ResponseData,
post_hook: Option<PostHook>,
@ -161,7 +161,7 @@ pub(super) async fn process(
// Invoke all post hooks
for hook in post_hooks {
hook(state.lock().await);
hook();
}
result
@ -439,212 +439,119 @@ async fn proc_spawn<F>(
where
F: FnMut(Vec<ResponseData>) -> ReplyRet + Clone + Send + 'static,
{
let id = rand::random();
debug!("<Conn @ {}> Spawning {} {}", conn_id, cmd, args.join(" "));
let mut child: Box<dyn Process> = match pty {
Some(size) => Box::new(PtyProcess::spawn(cmd.clone(), args.clone(), size)?),
None => Box::new(SimpleProcess::spawn(cmd.clone(), args.clone())?),
};
debug!(
"<Conn @ {} | Proc {}> Spawning {} {}",
conn_id,
id,
cmd,
args.join(" ")
);
let mut child = Command::new(cmd.to_string())
.args(args.clone())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
state
.lock()
.await
.push_process(conn_id, Process::new(id, cmd, args, detached, pty));
let id = child.id();
let stdin = child.take_stdin();
let stdout = child.take_stdout();
let stderr = child.take_stderr();
let killer = child.clone_killer();
let pty = child.clone_pty();
let post_hook = Box::new(move |mut state_lock: MutexGuard<'_, State>| {
let state_2 = Arc::clone(&state);
let post_hook = Box::new(move || {
// Spawn a task that sends stdout as a response
let mut stdout = child.stdout.take().unwrap();
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).await {
Ok(n) if n > 0 => {
let payload = vec![ResponseData::ProcStdout {
id,
data: buf[..n].to_vec(),
}];
if !reply_2(payload).await {
error!("<Conn @ {} | Proc {}> Stdout channel closed", conn_id, id);
if let Some(mut stdout) = stdout {
let mut reply_2 = reply.clone();
let _ = tokio::spawn(async move {
loop {
match stdout.recv().await {
Ok(Some(data)) => {
let payload = vec![ResponseData::ProcStdout { id, data }];
if !reply_2(payload).await {
error!("<Conn @ {} | Proc {}> Stdout channel closed", conn_id, id);
break;
}
}
Ok(None) => break,
Err(x) => {
error!(
"<Conn @ {} | Proc {}> Reading stdout failed: {}",
conn_id, id, x
);
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;
}
Ok(_) => break,
Err(x) => {
error!(
"<Conn @ {} | Proc {}> Reading stdout failed: {}",
conn_id, id, x
);
break;
}
}
}
});
});
}
// Spawn a task that sends stderr as a response
let mut stderr = child.stderr.take().unwrap();
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).await {
Ok(n) if n > 0 => {
let payload = vec![ResponseData::ProcStderr {
id,
data: buf[..n].to_vec(),
}];
if !reply_2(payload).await {
error!("<Conn @ {} | Proc {}> Stderr channel closed", conn_id, id);
if let Some(mut stderr) = stderr {
let mut reply_2 = reply.clone();
let _ = tokio::spawn(async move {
loop {
match stderr.recv().await {
Ok(Some(data)) => {
let payload = vec![ResponseData::ProcStderr { id, data }];
if !reply_2(payload).await {
error!("<Conn @ {} | Proc {}> Stderr channel closed", conn_id, id);
break;
}
}
Ok(None) => break,
Err(x) => {
error!(
"<Conn @ {} | Proc {}> Reading stderr failed: {}",
conn_id, id, x
);
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;
}
Ok(_) => break,
Err(x) => {
error!(
"<Conn @ {} | Proc {}> Reading stderr failed: {}",
conn_id, id, x
);
break;
}
}
}
});
// Spawn a task that sends stdin to the process
let mut stdin = child.stdin.take().unwrap();
let (stdin_tx, mut stdin_rx) = mpsc::channel::<Vec<u8>>(1);
let stdin_task = tokio::spawn(async move {
while let Some(line) = stdin_rx.recv().await {
if let Err(x) = stdin.write_all(&line).await {
error!(
"<Conn @ {} | Proc {}> Failed to send stdin: {}",
conn_id, 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 (kill_tx, kill_rx) = oneshot::channel();
let mut reply_2 = reply.clone();
let wait_task = tokio::spawn(async move {
tokio::select! {
status = child.wait() => {
debug!(
"<Conn @ {} | Proc {}> Completed and waiting on stdout & stderr tasks",
conn_id,
id,
);
// Force stdin task to abort if it hasn't exited as there is no
// point to sending any more stdin
stdin_task.abort();
if let Err(x) = stderr_task.await {
error!("<Conn @ {} | Proc {}> Join on stderr task failed: {}", conn_id, id, x);
}
if let Err(x) = stdout_task.await {
error!("<Conn @ {} | Proc {}> Join on stdout task failed: {}", conn_id, id, x);
}
state_2.lock().await.remove_process(conn_id, id);
match status {
Ok(status) => {
let success = status.success();
let mut code = status.code();
// If we succeeded and have no exit code, automatically populate
// with success exit code
if success && code.is_none() {
code = Some(0);
}
let payload = vec![ResponseData::ProcDone { id, success, code }];
if !reply_2(payload).await {
error!(
"<Conn @ {} | Proc {}> Failed to send done",
conn_id,
id,
);
}
}
Err(x) => {
let payload = vec![ResponseData::from(x)];
if !reply_2(payload).await {
error!(
"<Conn @ {} | Proc {}> Failed to send error for waiting",
conn_id,
id,
);
}
}
}
},
_ = kill_rx => {
debug!("<Conn @ {} | Proc {}> Killing", conn_id, id);
if let Err(x) = child.kill().await {
error!("<Conn @ {} | Proc {}> Unable to kill: {}", conn_id, id, x);
}
let _ = tokio::spawn(async move {
let status = child.wait().await;
debug!("<Conn @ {} | Proc {}> Completed {:?}", conn_id, id, status);
// Force stdin task to abort if it hasn't exited as there is no
// point to sending any more stdin
stdin_task.abort();
state_2.lock().await.remove_process(conn_id, id);
if let Err(x) = stderr_task.await {
error!("<Conn @ {} | Proc {}> Join on stderr task failed: {}", conn_id, id, x);
}
if let Err(x) = stdout_task.await {
error!("<Conn @ {} | Proc {}> Join on stdout task failed: {}", conn_id, 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 @ {} | Proc {}> Failed to wait after killed: {}", conn_id, id, x);
match status {
Ok(status) => {
let payload = vec![ResponseData::ProcDone {
id,
success: status.success,
code: status.code,
}];
if !reply_2(payload).await {
error!("<Conn @ {} | Proc {}> Failed to send done", conn_id, id,);
}
state_2.lock().await.remove_process(conn_id, id);
let payload = vec![ResponseData::ProcDone { id, success: false, code: None }];
}
Err(x) => {
let payload = vec![ResponseData::from(x)];
if !reply_2(payload).await {
error!("<Conn @ {} | Proc {}> Failed to send done", conn_id, id);
error!(
"<Conn @ {} | Proc {}> Failed to send error for waiting",
conn_id, id,
);
}
}
}
});
// Update our state with the new process
if let Some(proc) = state_lock.mut_process(id) {
proc.initialize(stdin_tx, kill_tx, wait_task);
}
});
state.lock().await.push_process_state(
conn_id,
ProcessState {
cmd,
args,
detached,
id,
stdin,
killer,
pty,
},
);
debug!(
"<Conn @ {} | Proc {}> Spawned successfully! Will enter post hook later",
conn_id, id
@ -656,8 +563,8 @@ where
}
async fn proc_kill(conn_id: usize, state: HState, id: usize) -> Result<Outgoing, ServerError> {
if let Some(process) = state.lock().await.processes.remove(&id) {
if process.kill() {
if let Some(mut process) = state.lock().await.processes.remove(&id) {
if process.killer.kill().await.is_ok() {
return Ok(Outgoing::from(ResponseData::Ok));
}
}
@ -677,9 +584,11 @@ async fn proc_stdin(
id: usize,
data: Vec<u8>,
) -> Result<Outgoing, ServerError> {
if let Some(process) = state.lock().await.processes.get(&id) {
if process.send_stdin(data).await {
return Ok(Outgoing::from(ResponseData::Ok));
if let Some(process) = state.lock().await.processes.get_mut(&id) {
if let Some(stdin) = process.stdin.as_mut() {
if stdin.send(&data).await.is_ok() {
return Ok(Outgoing::from(ResponseData::Ok));
}
}
}
@ -698,7 +607,19 @@ async fn proc_resize_pty(
id: usize,
size: PtySize,
) -> Result<Outgoing, ServerError> {
todo!();
if let Some(process) = state.lock().await.processes.get(&id) {
let _ = process.pty.resize_pty(size)?;
return Ok(Outgoing::from(ResponseData::Ok));
}
Err(ServerError::IoError(io::Error::new(
io::ErrorKind::BrokenPipe,
format!(
"<Conn @ {} | Proc {}> Unable to resize pty to {:?}",
conn_id, id, size,
),
)))
}
async fn proc_list(state: HState) -> Result<Outgoing, ServerError> {
@ -712,7 +633,8 @@ async fn proc_list(state: HState) -> Result<Outgoing, ServerError> {
cmd: p.cmd.to_string(),
args: p.args.clone(),
detached: p.detached,
pty: p.pty.clone(),
// TODO: Support retrieving current pty size
pty: None,
id: p.id,
})
.collect(),
@ -2213,7 +2135,7 @@ mod tests {
}
#[tokio::test]
async fn proc_run_should_send_error_on_failure() {
async fn proc_spawn_should_send_error_on_failure() {
let (conn_id, state, tx, mut rx) = setup(1);
let req = Request::new(
@ -2240,7 +2162,7 @@ mod tests {
}
#[tokio::test]
async fn proc_run_should_send_back_proc_start_on_success() {
async fn proc_spawn_should_send_back_proc_start_on_success() {
let (conn_id, state, tx, mut rx) = setup(1);
let req = Request::new(
@ -2270,7 +2192,7 @@ mod tests {
// with / but thinks it's on windows and is providing \
#[tokio::test]
#[cfg_attr(windows, ignore)]
async fn proc_run_should_send_back_stdout_periodically_when_available() {
async fn proc_spawn_should_send_back_stdout_periodically_when_available() {
let (conn_id, state, tx, mut rx) = setup(1);
// Run a program that echoes to stdout
@ -2337,7 +2259,7 @@ mod tests {
// with / but thinks it's on windows and is providing \
#[tokio::test]
#[cfg_attr(windows, ignore)]
async fn proc_run_should_send_back_stderr_periodically_when_available() {
async fn proc_spawn_should_send_back_stderr_periodically_when_available() {
let (conn_id, state, tx, mut rx) = setup(1);
// Run a program that echoes to stderr
@ -2404,7 +2326,7 @@ mod tests {
// with / but thinks it's on windows and is providing \
#[tokio::test]
#[cfg_attr(windows, ignore)]
async fn proc_run_should_clear_process_from_state_when_done() {
async fn proc_spawn_should_clear_process_from_state_when_done() {
let (conn_id, state, tx, mut rx) = setup(1);
// Run a program that ends after a little bit
@ -2448,7 +2370,7 @@ mod tests {
}
#[tokio::test]
async fn proc_run_should_clear_process_from_state_when_killed() {
async fn proc_spawn_should_clear_process_from_state_when_killed() {
let (conn_id, state, tx, mut rx) = setup(1);
// Run a program that ends slowly

@ -1,6 +1,8 @@
mod handler;
mod process;
mod state;
pub(crate) use process::{InputChannel, ProcessKiller, ProcessPty};
use state::State;
use crate::{

@ -0,0 +1,151 @@
use crate::data::PtySize;
use std::{future::Future, pin::Pin};
use tokio::{io, sync::mpsc};
mod pty;
pub use pty::*;
mod simple;
pub use simple::*;
mod wait;
pub use wait::{ExitStatus, WaitRx, WaitTx};
/// Alias to the return type of an async function (for use with traits)
pub type FutureReturn<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
/// Represents a process on the remote server
pub trait Process: ProcessKiller + ProcessPty {
/// Represents the id of the process
fn id(&self) -> usize;
/// Waits for the process to exit, returning the exit status
///
/// If the process has already exited, the status is returned immediately.
fn wait(&mut self) -> FutureReturn<'_, io::Result<ExitStatus>>;
/// Returns a reference to stdin channel if the process still has it associated
fn stdin(&self) -> Option<&dyn InputChannel>;
/// Returns a mutable reference to the stdin channel if the process still has it associated
fn mut_stdin(&mut self) -> Option<&mut (dyn InputChannel + 'static)>;
/// Takes the stdin channel from the process if it is still associated
fn take_stdin(&mut self) -> Option<Box<dyn InputChannel>>;
/// Returns a reference to stdout channel if the process still has it associated
fn stdout(&self) -> Option<&dyn OutputChannel>;
/// Returns a mutable reference to the stdout channel if the process still has it associated
fn mut_stdout(&mut self) -> Option<&mut (dyn OutputChannel + 'static)>;
/// Takes the stdout channel from the process if it is still associated
fn take_stdout(&mut self) -> Option<Box<dyn OutputChannel>>;
/// Returns a reference to stderr channel if the process still has it associated
fn stderr(&self) -> Option<&dyn OutputChannel>;
/// Returns a mutable reference to the stderr channel if the process still has it associated
fn mut_stderr(&mut self) -> Option<&mut (dyn OutputChannel + 'static)>;
/// Takes the stderr channel from the process if it is still associated
fn take_stderr(&mut self) -> Option<Box<dyn OutputChannel>>;
}
/// Represents interface that can be used to work with a pty associated with a process
pub trait ProcessPty: Send + Sync {
/// Returns the current size of the process' pty if it has one
fn pty_size(&self) -> Option<PtySize>;
/// Resize the pty associated with the process; returns an error if fails or if the
/// process does not leverage a pty
fn resize_pty(&self, size: PtySize) -> io::Result<()>;
/// Clone a process pty to support reading and updating pty independently
fn clone_pty(&self) -> Box<dyn ProcessPty>;
}
/// Trait that can be implemented to mark a process as not having a pty
pub trait NoProcessPty: Send + Sync {}
/// Internal type so we can create a dummy instance that implements trait
struct NoProcessPtyImpl {}
impl NoProcessPty for NoProcessPtyImpl {}
impl<T: NoProcessPty> ProcessPty for T {
fn pty_size(&self) -> Option<PtySize> {
None
}
fn resize_pty(&self, _size: PtySize) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
"Process does not use pty",
))
}
fn clone_pty(&self) -> Box<dyn ProcessPty> {
Box::new(NoProcessPtyImpl {})
}
}
/// Represents interface that can be used to kill a remote process
pub trait ProcessKiller: Send + Sync {
/// Kill the process
///
/// If the process is dead or has already been killed, this will return
/// an error.
fn kill(&mut self) -> FutureReturn<'_, io::Result<()>>;
/// Clone a process killer to support sending signals independently
fn clone_killer(&self) -> Box<dyn ProcessKiller>;
}
impl ProcessKiller for mpsc::Sender<()> {
fn kill(&mut self) -> FutureReturn<'_, io::Result<()>> {
async fn inner(this: &mut mpsc::Sender<()>) -> io::Result<()> {
this.send(())
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))
}
Box::pin(inner(self))
}
fn clone_killer(&self) -> Box<dyn ProcessKiller> {
Box::new(self.clone())
}
}
/// Represents an input channel of a process such as stdin
pub trait InputChannel: Send + Sync {
/// Sends input through channel, returning unit if succeeds or an error if fails
fn send<'a>(&'a mut self, data: &[u8]) -> FutureReturn<'a, io::Result<()>>;
}
impl InputChannel for mpsc::Sender<Vec<u8>> {
fn send<'a>(&'a mut self, data: &[u8]) -> FutureReturn<'a, io::Result<()>> {
let data = data.to_vec();
Box::pin(async move {
match mpsc::Sender::send(self, data).await {
Ok(_) => Ok(()),
Err(_) => Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Input channel closed",
)),
}
})
}
}
/// Represents an output channel of a process such as stdout or stderr
pub trait OutputChannel: Send + Sync {
/// Waits for next output from channel, returning Some(data) if there is output, None if
/// the channel has been closed, or bubbles up an error if encountered
fn recv(&mut self) -> FutureReturn<'_, io::Result<Option<Vec<u8>>>>;
}
impl OutputChannel for mpsc::Receiver<Vec<u8>> {
fn recv(&mut self) -> FutureReturn<'_, io::Result<Option<Vec<u8>>>> {
Box::pin(async move { Ok(mpsc::Receiver::recv(self).await) })
}
}

@ -0,0 +1,278 @@
use super::{
wait, ExitStatus, FutureReturn, InputChannel, OutputChannel, Process, ProcessKiller,
ProcessPty, PtySize, WaitRx,
};
use crate::constants::{MAX_PIPE_CHUNK_SIZE, READ_PAUSE_MILLIS};
use portable_pty::{CommandBuilder, MasterPty, PtySize as PortablePtySize};
use std::{
ffi::OsStr,
io::{self, Read, Write},
sync::{Arc, Mutex},
};
use tokio::{sync::mpsc, task::JoinHandle};
/// Represents a process that is associated with a pty
pub struct PtyProcess {
id: usize,
pty_master: PtyProcessMaster,
stdin: Option<Box<dyn InputChannel>>,
stdout: Option<Box<dyn OutputChannel>>,
stdin_task: Option<JoinHandle<()>>,
stdout_task: Option<JoinHandle<io::Result<()>>>,
kill_tx: mpsc::Sender<()>,
wait: WaitRx,
}
impl PtyProcess {
/// Spawns a new simple process
pub fn spawn<S, I, S2>(program: S, args: I, size: PtySize) -> io::Result<Self>
where
S: AsRef<OsStr>,
I: IntoIterator<Item = S2>,
S2: AsRef<OsStr>,
{
// Establish our new pty for the given size
let pty_system = portable_pty::native_pty_system();
let pty_pair = pty_system
.openpty(PortablePtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
})
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let pty_master = pty_pair.master;
let pty_slave = pty_pair.slave;
// Spawn our process within the pty
let mut cmd = CommandBuilder::new(program);
cmd.args(args);
let mut child = pty_slave
.spawn_command(cmd)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
// NOTE: Need to drop slave to close out file handles and avoid deadlock when waiting on
// the child
drop(pty_slave);
// Spawn a blocking task to process submitting stdin async
let (stdin_tx, mut stdin_rx) = mpsc::channel::<Vec<u8>>(1);
let mut stdin_writer = pty_master
.try_clone_writer()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let stdin_task = tokio::task::spawn_blocking(move || {
while let Some(input) = stdin_rx.blocking_recv() {
if stdin_writer.write_all(&input).is_err() {
break;
}
}
});
// Spawn a blocking task to process receiving stdout async
let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(1);
let mut stdout_reader = pty_master
.try_clone_reader()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let stdout_task = tokio::task::spawn_blocking(move || {
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match stdout_reader.read(&mut buf) {
Ok(n) if n > 0 => {
let _ = stdout_tx.blocking_send(buf[..n].to_vec()).map_err(|_| {
io::Error::new(io::ErrorKind::BrokenPipe, "Output channel closed")
})?;
}
Ok(_) => return Ok(()),
Err(x) => return Err(x),
}
}
});
let (kill_tx, mut kill_rx) = mpsc::channel(1);
let (mut wait_tx, wait_rx) = wait::channel();
tokio::spawn(async move {
loop {
match (child.try_wait(), kill_rx.try_recv()) {
(Ok(Some(status)), _) => {
// TODO: Keep track of io error
let _ = wait_tx
.send(ExitStatus {
success: status.success(),
code: None,
})
.await;
break;
}
(_, Ok(_)) => {
// TODO: Keep track of io error
let _ = wait_tx.kill().await;
break;
}
(Err(x), _) => {
// TODO: Keep track of io error
let _ = wait_tx.send(x).await;
break;
}
_ => {
tokio::time::sleep(tokio::time::Duration::from_millis(READ_PAUSE_MILLIS))
.await;
continue;
}
}
}
});
Ok(Self {
id: rand::random(),
pty_master: PtyProcessMaster(Arc::new(Mutex::new(pty_master))),
stdin: Some(Box::new(stdin_tx)),
stdout: Some(Box::new(stdout_rx)),
stdin_task: Some(stdin_task),
stdout_task: Some(stdout_task),
kill_tx,
wait: wait_rx,
})
}
}
impl Process for PtyProcess {
fn id(&self) -> usize {
self.id
}
fn wait(&mut self) -> FutureReturn<'_, io::Result<ExitStatus>> {
async fn inner(this: &mut PtyProcess) -> io::Result<ExitStatus> {
let mut status = this.wait.recv().await?;
if let Some(task) = this.stdin_task.take() {
task.abort();
}
if let Some(task) = this.stdout_task.take() {
let _ = task.await;
}
if status.success && status.code.is_none() {
status.code = Some(0);
}
Ok(status)
}
Box::pin(inner(self))
}
fn stdin(&self) -> Option<&dyn InputChannel> {
self.stdin.as_deref()
}
fn mut_stdin(&mut self) -> Option<&mut (dyn InputChannel + 'static)> {
self.stdin.as_deref_mut()
}
fn take_stdin(&mut self) -> Option<Box<dyn InputChannel>> {
self.stdin.take()
}
fn stdout(&self) -> Option<&dyn OutputChannel> {
self.stdout.as_deref()
}
fn mut_stdout(&mut self) -> Option<&mut (dyn OutputChannel + 'static)> {
self.stdout.as_deref_mut()
}
fn take_stdout(&mut self) -> Option<Box<dyn OutputChannel>> {
self.stdout.take()
}
fn stderr(&self) -> Option<&dyn OutputChannel> {
None
}
fn mut_stderr(&mut self) -> Option<&mut (dyn OutputChannel + 'static)> {
None
}
fn take_stderr(&mut self) -> Option<Box<dyn OutputChannel>> {
None
}
}
impl ProcessKiller for PtyProcess {
fn kill(&mut self) -> FutureReturn<'_, io::Result<()>> {
async fn inner(this: &mut PtyProcess) -> io::Result<()> {
this.kill_tx
.send(())
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))
}
Box::pin(inner(self))
}
fn clone_killer(&self) -> Box<dyn ProcessKiller> {
Box::new(self.kill_tx.clone())
}
}
#[derive(Clone)]
pub struct PtyProcessKiller(mpsc::Sender<()>);
impl ProcessKiller for PtyProcessKiller {
fn kill(&mut self) -> FutureReturn<'_, io::Result<()>> {
async fn inner(this: &mut PtyProcessKiller) -> io::Result<()> {
this.0
.send(())
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))
}
Box::pin(inner(self))
}
fn clone_killer(&self) -> Box<dyn ProcessKiller> {
Box::new(self.clone())
}
}
impl ProcessPty for PtyProcess {
fn pty_size(&self) -> Option<PtySize> {
self.pty_master.pty_size()
}
fn resize_pty(&self, size: PtySize) -> io::Result<()> {
self.pty_master.resize_pty(size)
}
fn clone_pty(&self) -> Box<dyn ProcessPty> {
self.pty_master.clone_pty()
}
}
#[derive(Clone)]
pub struct PtyProcessMaster(Arc<Mutex<Box<dyn MasterPty + Send>>>);
impl ProcessPty for PtyProcessMaster {
fn pty_size(&self) -> Option<PtySize> {
self.0.lock().unwrap().get_size().ok().map(|size| PtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
})
}
fn resize_pty(&self, size: PtySize) -> io::Result<()> {
self.0
.lock()
.unwrap()
.resize(PortablePtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
})
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))
}
fn clone_pty(&self) -> Box<dyn ProcessPty> {
Box::new(self.clone())
}
}

@ -0,0 +1,181 @@
use super::{
wait, ExitStatus, FutureReturn, InputChannel, NoProcessPty, OutputChannel, Process,
ProcessKiller, WaitRx,
};
use std::{ffi::OsStr, process::Stdio};
use tokio::{io, process::Command, sync::mpsc, task::JoinHandle};
mod tasks;
/// Represents a simple process that does not have a pty
pub struct SimpleProcess {
id: usize,
stdin: Option<Box<dyn InputChannel>>,
stdout: Option<Box<dyn OutputChannel>>,
stderr: Option<Box<dyn OutputChannel>>,
stdin_task: Option<JoinHandle<io::Result<()>>>,
stdout_task: Option<JoinHandle<io::Result<()>>>,
stderr_task: Option<JoinHandle<io::Result<()>>>,
kill_tx: mpsc::Sender<()>,
wait: WaitRx,
}
impl SimpleProcess {
/// Spawns a new simple process
pub fn spawn<S, I, S2>(program: S, args: I) -> io::Result<Self>
where
S: AsRef<OsStr>,
I: IntoIterator<Item = S2>,
S2: AsRef<OsStr>,
{
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take().unwrap();
let (stdout_task, stdout_ch) = tasks::spawn_read_task(stdout, 1);
let stderr = child.stderr.take().unwrap();
let (stderr_task, stderr_ch) = tasks::spawn_read_task(stderr, 1);
let stdin = child.stdin.take().unwrap();
let (stdin_task, stdin_ch) = tasks::spawn_write_task(stdin, 1);
let (kill_tx, mut kill_rx) = mpsc::channel(1);
let (mut wait_tx, wait_rx) = wait::channel();
tokio::spawn(async move {
tokio::select! {
_ = kill_rx.recv() => {
let status = match child.kill().await {
Ok(_) => ExitStatus::killed(),
Err(x) => ExitStatus::from(x),
};
// TODO: Keep track of io error
let _ = wait_tx.send(status).await;
}
status = child.wait() => {
// TODO: Keep track of io error
let _ = wait_tx.send(status).await;
}
}
});
Ok(Self {
id: rand::random(),
stdin: Some(Box::new(stdin_ch)),
stdout: Some(Box::new(stdout_ch)),
stderr: Some(Box::new(stderr_ch)),
stdin_task: Some(stdin_task),
stdout_task: Some(stdout_task),
stderr_task: Some(stderr_task),
kill_tx,
wait: wait_rx,
})
}
}
impl Process for SimpleProcess {
fn id(&self) -> usize {
self.id
}
fn wait(&mut self) -> FutureReturn<'_, io::Result<ExitStatus>> {
async fn inner(this: &mut SimpleProcess) -> io::Result<ExitStatus> {
let mut status = this.wait.recv().await?;
if let Some(task) = this.stdin_task.take() {
task.abort();
}
if let Some(task) = this.stdout_task.take() {
let _ = task.await;
}
if let Some(task) = this.stderr_task.take() {
let _ = task.await;
}
if status.success && status.code.is_none() {
status.code = Some(0);
}
Ok(status)
}
Box::pin(inner(self))
}
fn stdin(&self) -> Option<&dyn InputChannel> {
self.stdin.as_deref()
}
fn mut_stdin(&mut self) -> Option<&mut (dyn InputChannel + 'static)> {
self.stdin.as_deref_mut()
}
fn take_stdin(&mut self) -> Option<Box<dyn InputChannel>> {
self.stdin.take()
}
fn stdout(&self) -> Option<&dyn OutputChannel> {
self.stdout.as_deref()
}
fn mut_stdout(&mut self) -> Option<&mut (dyn OutputChannel + 'static)> {
self.stdout.as_deref_mut()
}
fn take_stdout(&mut self) -> Option<Box<dyn OutputChannel>> {
self.stdout.take()
}
fn stderr(&self) -> Option<&dyn OutputChannel> {
self.stderr.as_deref()
}
fn mut_stderr(&mut self) -> Option<&mut (dyn OutputChannel + 'static)> {
self.stderr.as_deref_mut()
}
fn take_stderr(&mut self) -> Option<Box<dyn OutputChannel>> {
self.stderr.take()
}
}
impl NoProcessPty for SimpleProcess {}
impl ProcessKiller for SimpleProcess {
fn kill(&mut self) -> FutureReturn<'_, io::Result<()>> {
async fn inner(this: &mut SimpleProcess) -> io::Result<()> {
this.kill_tx
.send(())
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))
}
Box::pin(inner(self))
}
fn clone_killer(&self) -> Box<dyn ProcessKiller> {
Box::new(self.kill_tx.clone())
}
}
#[derive(Clone)]
pub struct SimpleProcessKiller(mpsc::Sender<()>);
impl ProcessKiller for SimpleProcessKiller {
fn kill(&mut self) -> FutureReturn<'_, io::Result<()>> {
async fn inner(this: &mut SimpleProcessKiller) -> io::Result<()> {
this.0
.send(())
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))
}
Box::pin(inner(self))
}
fn clone_killer(&self) -> Box<dyn ProcessKiller> {
Box::new(self.clone())
}
}

@ -0,0 +1,71 @@
use crate::constants::{MAX_PIPE_CHUNK_SIZE, READ_PAUSE_MILLIS};
use tokio::{
io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
sync::mpsc,
task::JoinHandle,
};
pub fn spawn_read_task<R>(
reader: R,
buf: usize,
) -> (JoinHandle<io::Result<()>>, mpsc::Receiver<Vec<u8>>)
where
R: AsyncRead + Send + Unpin + 'static,
{
let (tx, rx) = mpsc::channel(buf);
let task = tokio::spawn(read_handler(reader, tx));
(task, rx)
}
/// Continually reads from some reader and fowards to the provided sender until the reader
/// or channel is closed
async fn read_handler<R>(mut reader: R, channel: mpsc::Sender<Vec<u8>>) -> io::Result<()>
where
R: AsyncRead + Unpin,
{
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match reader.read(&mut buf).await {
Ok(n) if n > 0 => {
let _ = channel.send(buf[..n].to_vec()).await.map_err(|_| {
io::Error::new(io::ErrorKind::BrokenPipe, "Output channel closed")
})?;
// 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;
}
Ok(_) => return Ok(()),
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(x) => return Err(x),
}
}
}
pub fn spawn_write_task<W>(
writer: W,
buf: usize,
) -> (JoinHandle<io::Result<()>>, mpsc::Sender<Vec<u8>>)
where
W: AsyncWrite + Send + Unpin + 'static,
{
let (tx, rx) = mpsc::channel(buf);
let task = tokio::spawn(write_handler(writer, rx));
(task, tx)
}
/// Continually writes to some writer by reading data from a provided receiver until the receiver
/// or writer is closed
async fn write_handler<W>(mut writer: W, mut channel: mpsc::Receiver<Vec<u8>>) -> io::Result<()>
where
W: AsyncWrite + Unpin,
{
while let Some(data) = channel.recv().await {
let _ = writer.write_all(&data).await?;
}
Ok(())
}

@ -0,0 +1,136 @@
use tokio::{io, sync::mpsc};
/// Exit status of a remote process
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ExitStatus {
pub success: bool,
pub code: Option<i32>,
}
impl ExitStatus {
/// Produces a new exit status representing a killed process
pub fn killed() -> Self {
Self {
success: false,
code: None,
}
}
}
impl<T, E> From<Result<T, E>> for ExitStatus
where
T: Into<ExitStatus>,
E: Into<ExitStatus>,
{
fn from(res: Result<T, E>) -> Self {
match res {
Ok(x) => x.into(),
Err(x) => x.into(),
}
}
}
impl From<io::Error> for ExitStatus {
fn from(err: io::Error) -> Self {
Self {
success: false,
code: err.raw_os_error(),
}
}
}
impl From<std::process::ExitStatus> for ExitStatus {
fn from(status: std::process::ExitStatus) -> Self {
Self {
success: status.success(),
code: status.code(),
}
}
}
/// Creates a new channel for when the exit status will be ready
pub fn channel() -> (WaitTx, WaitRx) {
let (tx, rx) = mpsc::channel(1);
(WaitTx::Pending(tx), WaitRx::Pending(rx))
}
/// Represents a notifier for a specific waiting state
#[derive(Debug)]
pub enum WaitTx {
/// Notification has been sent
Done,
/// Notification has not been sent
Pending(mpsc::Sender<ExitStatus>),
}
impl WaitTx {
/// Send exit status to receiving-side of wait
pub async fn send<S>(&mut self, status: S) -> io::Result<()>
where
S: Into<ExitStatus>,
{
let status = status.into();
match self {
Self::Done => Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Notifier is closed",
)),
Self::Pending(tx) => {
let res = tx.send(status).await;
*self = Self::Done;
match res {
Ok(_) => Ok(()),
Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
}
}
}
}
/// Mark wait as completed using killed status
pub async fn kill(&mut self) -> io::Result<()> {
self.send(ExitStatus::killed()).await
}
}
/// Represents the state of waiting for an exit status
#[derive(Debug)]
pub enum WaitRx {
/// Exit status is ready
Ready(ExitStatus),
/// If receiver for an exit status has been dropped without receiving the status
Dropped,
/// Exit status is not ready and has a "oneshot" to be invoked when available
Pending(mpsc::Receiver<ExitStatus>),
}
impl WaitRx {
/// Waits until the exit status is resolved; can be called repeatedly after being
/// resolved to immediately return the exit status again
pub async fn recv(&mut self) -> io::Result<ExitStatus> {
match self {
Self::Ready(status) => Ok(*status),
Self::Dropped => Err(io::Error::new(
io::ErrorKind::Other,
"Internal resolver dropped",
)),
Self::Pending(rx) => match rx.recv().await {
Some(status) => {
*self = Self::Ready(status);
Ok(status)
}
None => {
*self = Self::Dropped;
Err(io::Error::new(
io::ErrorKind::Other,
"Internal resolver dropped",
))
}
},
}
}
}

@ -1,38 +1,37 @@
use crate::data::PtySize;
use super::{InputChannel, ProcessKiller, ProcessPty};
use log::*;
use std::{
collections::HashMap,
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tokio::{
sync::{mpsc, oneshot},
task::{JoinError, JoinHandle},
};
use std::collections::HashMap;
/// Holds state related to multiple connections managed by a server
#[derive(Default)]
pub struct State {
/// Map of all processes running on the server
pub processes: HashMap<usize, Process>,
pub processes: HashMap<usize, ProcessState>,
/// List of processes that will be killed when a connection drops
client_processes: HashMap<usize, Vec<usize>>,
}
/// Holds information related to a spawned process on the server
pub struct ProcessState {
pub cmd: String,
pub args: Vec<String>,
pub detached: bool,
pub id: usize,
pub stdin: Option<Box<dyn InputChannel>>,
pub killer: Box<dyn ProcessKiller>,
pub pty: Box<dyn ProcessPty>,
}
impl State {
/// Pushes a new process associated with a connection
pub fn push_process(&mut self, conn_id: usize, process: Process) {
pub fn push_process_state(&mut self, conn_id: usize, process_state: ProcessState) {
self.client_processes
.entry(conn_id)
.or_insert_with(Vec::new)
.push(process.id);
self.processes.insert(process.id, process);
}
pub fn mut_process(&mut self, proc_id: usize) -> Option<&mut Process> {
self.processes.get_mut(&proc_id)
.push(process_state.id);
self.processes.insert(process_state.id, process_state);
}
/// Removes a process associated with a connection
@ -57,7 +56,7 @@ impl State {
process.id
);
process.close_stdin();
let _ = process.stdin.take();
}
}
}
@ -68,7 +67,7 @@ impl State {
debug!("<Conn @ {:?}> Cleaning up state", conn_id);
if let Some(ids) = self.client_processes.remove(&conn_id) {
for id in ids {
if let Some(process) = self.processes.remove(&id) {
if let Some(mut process) = self.processes.remove(&id) {
if !process.detached {
trace!(
"<Conn @ {:?}> Requesting proc {} be killed",
@ -76,8 +75,11 @@ impl State {
process.id
);
let pid = process.id;
if !process.kill() {
error!("Conn {} failed to send process {} kill signal", id, pid);
if let Err(x) = process.killer.kill().await {
error!(
"Conn {} failed to send process {} kill signal: {}",
id, pid, x
);
}
} else {
trace!(
@ -91,97 +93,3 @@ impl State {
}
}
}
/// Represents an actively-running process
pub struct Process {
/// Id of the process
pub id: usize,
/// Command used to start the process
pub cmd: String,
/// Arguments associated with the process
pub args: Vec<String>,
/// Whether or not this process was run detached
pub detached: bool,
/// Dimensions of pty associated with process, if it has one
pub pty: Option<PtySize>,
/// Transport channel to send new input to the stdin of the process,
/// one line at a time
stdin_tx: Option<mpsc::Sender<Vec<u8>>>,
/// Transport channel to report that the process should be killed
kill_tx: Option<oneshot::Sender<()>>,
/// Task used to wait on the process to complete or be killed
wait_task: Option<JoinHandle<()>>,
}
impl Process {
pub fn new(
id: usize,
cmd: String,
args: Vec<String>,
detached: bool,
pty: Option<PtySize>,
) -> Self {
Self {
id,
cmd,
args,
detached,
pty,
stdin_tx: None,
kill_tx: None,
wait_task: None,
}
}
/// Lazy initialization of process state
pub(crate) fn initialize(
&mut self,
stdin_tx: mpsc::Sender<Vec<u8>>,
kill_tx: oneshot::Sender<()>,
wait_task: JoinHandle<()>,
) {
self.stdin_tx = Some(stdin_tx);
self.kill_tx = Some(kill_tx);
self.wait_task = Some(wait_task);
}
pub async fn send_stdin(&self, input: impl Into<Vec<u8>>) -> bool {
if let Some(stdin) = self.stdin_tx.as_ref() {
if stdin.send(input.into()).await.is_ok() {
return true;
}
}
false
}
pub fn close_stdin(&mut self) {
self.stdin_tx.take();
}
pub fn kill(self) -> bool {
self.kill_tx
.map(|tx| tx.send(()).is_ok())
.unwrap_or_default()
}
}
impl Future for Process {
type Output = Result<(), JoinError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(task) = self.wait_task.as_mut() {
Pin::new(task).poll(cx)
} else {
// TODO: Does this work?
Poll::Pending
}
}
}

@ -20,7 +20,7 @@ assert_fs = "1.0.4"
distant-core = { path = "../distant-core" }
futures = "0.3.17"
indoc = "1.0.3"
mlua = { version = "0.6.6", features = ["async", "macros", "serialize"] }
mlua = { version = "0.7.3", features = ["async", "macros", "serialize"] }
once_cell = "1.8.0"
predicates = "2.0.2"
rstest = "0.11.0"

@ -10,6 +10,7 @@ mod read_file_text;
mod remove;
mod rename;
mod spawn;
mod spawn_pty;
mod spawn_wait;
mod system_info;
mod write_file;

@ -174,6 +174,8 @@ fn should_return_process_that_can_retrieve_stdout(ctx: &'_ DistantServerCtx) {
end
end)
assert(not err, "Unexpectedly failed reading stdout: " .. tostring(err))
stdout = string.char(unpack(stdout))
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
})
.exec();
@ -232,7 +234,9 @@ fn should_return_process_that_can_retrieve_stderr(ctx: &'_ DistantServerCtx) {
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout: " .. tostring(err))
assert(not err, "Unexpectedly failed reading stderr: " .. tostring(err))
stderr = string.char(unpack(stderr))
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
@ -430,6 +434,8 @@ fn should_support_sending_stdin_to_spawned_process(ctx: &'_ DistantServerCtx) {
end
end)
assert(not err, "Unexpectedly failed reading stdout: " .. tostring(err))
stdout = string.char(unpack(stdout))
assert(stdout == "some text\n", "Unexpected stdout received: " .. stdout)
})
.exec();

@ -0,0 +1,519 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use once_cell::sync::Lazy;
use rstest::*;
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 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 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 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 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
});
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = DOES_NOT_EXIST_BIN.to_str().unwrap().to_string();
let args: Vec<String> = Vec::new();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_back_process_on_success(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()];
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc.id >= 0, "Invalid process returned")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stdout(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string(),
String::from("some stdout"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc, "Missing proc")
// Wait briefly to ensure the process sends stdout
$wait_fn()
local f = distant.utils.wrap_async(proc.read_stdout_async, $schedule_fn)
local err, stdout
f(proc, function(success, res)
if success then
stdout = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout: " .. tostring(err))
stdout = string.char(unpack(stdout))
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stderr_as_part_of_stdout(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDERR_SH.to_str().unwrap().to_string(),
String::from("some stderr"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc, "Missing proc")
// Wait briefly to ensure the process sends stdout
$wait_fn()
// stderr is a broken pipe as it does not exist for pty
local f = distant.utils.wrap_async(proc.read_stderr_async, $schedule_fn)
local err, stderr
f(proc, function(success, res)
if success then
stderr = res
else
err = res
end
end)
assert(err)
// in a pty process, stderr is part of stdout
local f = distant.utils.wrap_async(proc.read_stdout_async, $schedule_fn)
local err, stdout
f(proc, function(success, res)
if success then
stdout = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout: " .. tostring(err))
// stdout should match what we'd normally expect from stderr
stdout = string.char(unpack(stdout))
assert(stdout == "some stderr", "Unexpected stdout: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_when_killing_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc, "Missing proc")
// Wait briefly to ensure the process dies
$wait_fn()
local f = distant.utils.wrap_async(proc.kill_async, $schedule_fn)
local err
f(proc, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded in killing dead process")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_killing_processing(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")];
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc, "Missing proc")
local f = distant.utils.wrap_async(proc.kill_async, $schedule_fn)
local err
f(proc, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed to kill process: " .. tostring(err))
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_if_sending_stdin_to_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc, "Missing proc")
// Wait briefly to ensure the process dies
$wait_fn()
local f = distant.utils.wrap_async(proc.write_stdin_async, $schedule_fn)
local err
f(proc, "some text\n", function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_support_sending_stdin_to_spawned_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed spawning process: " .. tostring(err))
assert(proc, "Missing proc")
local f = distant.utils.wrap_async(proc.write_stdin_async, $schedule_fn)
local err
f(proc, "some text\n", function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed writing stdin: " .. tostring(err))
// Wait briefly to ensure that pty reflects everything
$wait_fn()
local f = distant.utils.wrap_async(proc.read_stdout_async, $schedule_fn)
local err, stdout
f(proc, function(success, res)
if success then
stdout = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout: " .. tostring(err))
// NOTE: We're removing whitespace as there's some issue with properly comparing
// due to something else being captured from pty
stdout = string.gsub(string.char(unpack(stdout)), "%s+", "")
// TODO: Sometimes this comes back as "sometextsometext" (double) and I'm assuming
// this is part of pty output, but the tests seem to have a race condition
// to produce it, so we're just checking for either right now
assert(
stdout == "sometext" or stdout == "sometextsometext",
"Unexpected stdout received: " .. stdout
)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_support_resizing_pty(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args: Vec<String> = Vec::new();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed spawning process: " .. tostring(err))
assert(proc, "Missing proc")
local f = distant.utils.wrap_async(proc.resize_async, $schedule_fn)
local err
f(proc, { rows = 16, cols = 40 }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed resizing proc: " .. tostring(err))
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -10,6 +10,7 @@ mod read_file_text;
mod remove;
mod rename;
mod spawn;
mod spawn_pty;
mod spawn_wait;
mod system_info;
mod write_file;

@ -131,6 +131,7 @@ fn should_return_process_that_can_retrieve_stdout(ctx: &'_ DistantServerCtx) {
$wait_fn()
local stdout = proc:read_stdout()
stdout = string.char(unpack(stdout))
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
})
.exec();
@ -167,6 +168,7 @@ fn should_return_process_that_can_retrieve_stderr(ctx: &'_ DistantServerCtx) {
$wait_fn()
local stderr = proc:read_stderr()
stderr = string.char(unpack(stderr))
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
@ -281,6 +283,7 @@ fn should_support_sending_stdin_to_spawned_process(ctx: &'_ DistantServerCtx) {
$wait_fn()
local stdout = proc:read_stdout()
stdout = string.char(unpack(stdout))
assert(stdout == "some text\n", "Unexpected stdin sent: " .. stdout)
})
.exec();

@ -0,0 +1,324 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use once_cell::sync::Lazy;
use rstest::*;
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 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 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 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 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
});
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = DOES_NOT_EXIST_BIN.to_str().unwrap().to_string();
let args: Vec<String> = Vec::new();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.spawn, session, {
cmd = $cmd,
args = $args,
pty = { rows = 24, cols = 80 }
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_back_process_on_success(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()];
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } })
assert(proc.id >= 0, "Invalid process returned")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stdout(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string(),
String::from("some stdout"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
// Wait briefly to ensure the process sends stdout
$wait_fn()
local stdout = proc:read_stdout()
stdout = string.char(unpack(stdout))
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stderr(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDERR_SH.to_str().unwrap().to_string(),
String::from("some stderr"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
// Wait briefly to ensure the process sends stdout
$wait_fn()
local stderr = proc:read_stderr()
stderr = string.char(unpack(stderr))
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_when_killing_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
// Wait briefly to ensure the process dies
$wait_fn()
local status, _ = pcall(proc.kill, proc)
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_killing_processing(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")];
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
proc:kill()
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_if_sending_stdin_to_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
// Wait briefly to ensure the process dies
$wait_fn()
local status, _ = pcall(proc.write_stdin, proc, "some text")
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_support_sending_stdin_to_spawned_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
proc:write_stdin("some text\n")
// Wait briefly to ensure the process echoes stdin
$wait_fn()
local stdout = proc:read_stdout()
stdout = string.char(unpack(stdout))
assert(stdout == "some text\n", "Unexpected stdin sent: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_support_resizing_pty(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args: Vec<String> = Vec::new();
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args, rows = 24, cols = 80 })
// Wait briefly to ensure the process starts
$wait_fn()
proc:resize({ rows = 16, cols = 40 })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -96,8 +96,13 @@ fn should_capture_all_stdout(ctx: &'_ DistantServerCtx) {
local output = session:spawn_wait({ cmd = $cmd, args = $args })
assert(output, "Missing process output")
assert(output.success, "Process unexpectedly failed")
assert(output.stdout == "some stdout", "Unexpected stdout: " .. output.stdout)
assert(output.stderr == "", "Unexpected stderr: " .. output.stderr)
local stdout, stderr
stdout = string.char(unpack(output.stdout))
stderr = string.char(unpack(output.stderr))
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
assert(stderr == "", "Unexpected stderr: " .. stderr)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
@ -123,8 +128,13 @@ fn should_capture_all_stderr(ctx: &'_ DistantServerCtx) {
local output = session:spawn_wait({ cmd = $cmd, args = $args })
assert(output, "Missing process output")
assert(output.success, "Process unexpectedly failed")
assert(output.stdout == "", "Unexpected stdout: " .. output.stdout)
assert(output.stderr == "some stderr", "Unexpected stderr: " .. output.stderr)
local stdout, stderr
stdout = string.char(unpack(output.stdout))
stderr = string.char(unpack(output.stderr))
assert(stdout == "", "Unexpected stdout: " .. stdout)
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());

@ -3,7 +3,7 @@ name = "distant-lua"
description = "Lua bindings to the distant Rust crates"
categories = ["api-bindings", "network-programming"]
keywords = ["api", "async"]
version = "0.15.1"
version = "0.16.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
@ -24,11 +24,11 @@ luajit = ["mlua/luajit"]
vendored = ["mlua/vendored"]
[dependencies]
distant-core = { version = "=0.15.1", path = "../distant-core" }
distant-ssh2 = { version = "=0.15.1", features = ["serde"], path = "../distant-ssh2" }
distant-core = { version = "=0.16.0", path = "../distant-core" }
distant-ssh2 = { version = "=0.16.0", features = ["serde"], path = "../distant-ssh2" }
futures = "0.3.17"
log = "0.4.14"
mlua = { version = "0.6.6", features = ["async", "macros", "module", "serialize"] }
mlua = { version = "0.7.3", features = ["async", "macros", "module", "serialize"] }
once_cell = "1.8.0"
oorandom = "11.1.3"
paste = "1.0.5"

@ -3,8 +3,8 @@ use crate::{
session::proc::{Output, RemoteProcess as LuaRemoteProcess},
};
use distant_core::{
DirEntry, Error as Failure, Metadata, RemoteLspProcess, RemoteProcess, SessionChannel,
SessionChannelExt, SystemInfo,
data::PtySize, DirEntry, Error as Failure, Metadata, RemoteLspProcess, RemoteProcess,
SessionChannel, SessionChannelExt, SystemInfo,
};
use mlua::prelude::*;
use once_cell::sync::Lazy;
@ -164,22 +164,23 @@ make_api!(
make_api!(
spawn,
RemoteProcess,
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] detached: bool },
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] pty: Option<PtySize>, #[serde(default)] detached: bool },
|channel, tenant, params| {
channel.spawn(tenant, params.cmd, params.args, params.detached).await
channel.spawn(tenant, params.cmd, params.args, params.detached, params.pty).await
}
);
make_api!(
spawn_wait,
Output,
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] detached: bool },
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] pty: Option<PtySize>, #[serde(default)] detached: bool },
|channel, tenant, params| {
let proc = channel.spawn(
tenant,
params.cmd,
params.args,
params.detached,
params.pty,
).await.to_lua_err()?;
let id = LuaRemoteProcess::from_distant_async(proc).await?.id;
LuaRemoteProcess::output_async(id).await
@ -189,9 +190,9 @@ make_api!(
make_api!(
spawn_lsp,
RemoteLspProcess,
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] detached: bool },
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] pty: Option<PtySize>, #[serde(default)] detached: bool },
|channel, tenant, params| {
channel.spawn_lsp(tenant, params.cmd, params.args, params.detached).await
channel.spawn_lsp(tenant, params.cmd, params.args, params.detached, params.pty).await
}
);

@ -1,6 +1,7 @@
use crate::{constants::PROC_POLL_TIMEOUT, runtime};
use distant_core::{
RemoteLspProcess as DistantRemoteLspProcess, RemoteProcess as DistantRemoteProcess,
data::PtySize, RemoteLspProcess as DistantRemoteLspProcess,
RemoteProcess as DistantRemoteProcess,
};
use mlua::{prelude::*, UserData, UserDataFields, UserDataMethods};
use once_cell::sync::Lazy;
@ -86,7 +87,7 @@ macro_rules! impl_process {
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stdin closed").to_lua_err()
})?
.try_write(data.as_str());
.try_write(data.as_bytes());
match res {
Ok(_) => Ok(true),
Err(x) if x.kind() == io::ErrorKind::WouldBlock => Ok(false),
@ -112,7 +113,7 @@ macro_rules! impl_process {
})
}
fn read_stdout(id: usize) -> LuaResult<Option<String>> {
fn read_stdout(id: usize) -> LuaResult<Option<Vec<u8>>> {
with_proc!($map_name, id, proc -> {
proc.stdout
.as_mut()
@ -124,7 +125,7 @@ macro_rules! impl_process {
})
}
async fn read_stdout_async(id: usize) -> LuaResult<String> {
async fn read_stdout_async(id: usize) -> LuaResult<Vec<u8>> {
// NOTE: We must spawn a task that continually tries to read stdout as
// if we wait until successful then we hold the lock the entire time
runtime::spawn(async move {
@ -146,7 +147,7 @@ macro_rules! impl_process {
}).await
}
fn read_stderr(id: usize) -> LuaResult<Option<String>> {
fn read_stderr(id: usize) -> LuaResult<Option<Vec<u8>>> {
with_proc!($map_name, id, proc -> {
proc.stderr
.as_mut()
@ -158,7 +159,7 @@ macro_rules! impl_process {
})
}
async fn read_stderr_async(id: usize) -> LuaResult<String> {
async fn read_stderr_async(id: usize) -> LuaResult<Vec<u8>> {
// NOTE: We must spawn a task that continually tries to read stdout as
// if we wait until successful then we hold the lock the entire time
runtime::spawn(async move {
@ -180,6 +181,16 @@ macro_rules! impl_process {
}).await
}
fn resize(id: usize, size: PtySize) -> LuaResult<()> {
runtime::block_on(Self::resize_async(id, size))
}
async fn resize_async(id: usize, size: PtySize) -> LuaResult<()> {
with_proc_async!($map_name, id, proc -> {
proc.resize(size).await.to_lua_err()
})
}
fn kill(id: usize) -> LuaResult<()> {
runtime::block_on(Self::kill_async(id))
}
@ -245,14 +256,14 @@ macro_rules! impl_process {
// Gather stdout and stderr after process completes
let (success, exit_code) = proc.wait().await.to_lua_err()?;
let mut stdout_buf = String::new();
let mut stdout_buf = Vec::new();
while let Ok(Some(data)) = stdout.try_read() {
stdout_buf.push_str(&data);
stdout_buf.extend(data);
}
let mut stderr_buf = String::new();
let mut stderr_buf = Vec::new();
while let Ok(Some(data)) = stderr.try_read() {
stderr_buf.push_str(&data);
stderr_buf.extend(data);
}
Ok(Output {
@ -304,6 +315,17 @@ macro_rules! impl_process {
methods.add_async_method("output_async", |_, this, ()| {
runtime::spawn(Self::output_async(this.id))
});
methods.add_method("resize", |lua, this, value: LuaValue| {
let size: PtySize = lua.from_value(value)?;
Self::resize(this.id, size)
});
methods.add_async_method("resize_async", |lua, this, value: LuaValue| {
let size: LuaResult<PtySize> = lua.from_value(value);
runtime::spawn(async move {
let size = size?;
Self::resize_async(this.id, size).await
})
});
methods.add_method("kill", |_, this, ()| Self::kill(this.id));
methods.add_async_method("kill_async", |_, this, ()| {
runtime::spawn(Self::kill_async(this.id))
@ -342,16 +364,16 @@ impl UserData for Status {
pub struct Output {
pub success: bool,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl UserData for Output {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("success", |_, this| Ok(this.success));
fields.add_field_method_get("exit_code", |_, this| Ok(this.exit_code));
fields.add_field_method_get("stdout", |_, this| Ok(this.stdout.to_string()));
fields.add_field_method_get("stderr", |_, this| Ok(this.stderr.to_string()));
fields.add_field_method_get("stdout", |_, this| Ok(this.stdout.to_vec()));
fields.add_field_method_get("stderr", |_, this| Ok(this.stderr.to_vec()));
}
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
@ -359,8 +381,8 @@ impl UserData for Output {
let tbl = lua.create_table()?;
tbl.set("success", this.success)?;
tbl.set("exit_code", this.exit_code)?;
tbl.set("stdout", this.stdout.to_string())?;
tbl.set("stderr", this.stdout.to_string())?;
tbl.set("stdout", this.stdout.to_vec())?;
tbl.set("stderr", this.stdout.to_vec())?;
Ok(tbl)
});
}

@ -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.15.1"
version = "0.16.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
@ -12,7 +12,7 @@ license = "MIT OR Apache-2.0"
[dependencies]
async-compat = "0.2.1"
distant-core = { version = "=0.15.1", path = "../distant-core" }
distant-core = { version = "=0.16.0", path = "../distant-core" }
futures = "0.3.16"
log = "0.4.14"
rand = { version = "0.8.4", features = ["getrandom"] }
@ -20,7 +20,7 @@ rpassword = "5.0.1"
shell-words = "1.0"
smol = "1.2"
tokio = { version = "1.12.0", features = ["full"] }
wezterm-ssh = { version = "0.2.0", features = ["vendored-openssl"] }
wezterm-ssh = { version = "0.4.0", features = ["vendored-openssl"] }
# Optional serde support for data structures
serde = { version = "1.0.126", features = ["derive"], optional = true }

@ -1,6 +1,9 @@
use crate::process::{self, SpawnResult};
use async_compat::CompatExt;
use distant_core::{
data::{DirEntry, Error as DistantError, FileType, Metadata, RunningProcess, SystemInfo},
data::{
DirEntry, Error as DistantError, FileType, Metadata, PtySize, RunningProcess, SystemInfo,
},
Request, RequestData, Response, ResponseData,
};
use futures::future;
@ -8,18 +11,13 @@ use log::*;
use std::{
collections::HashMap,
future::Future,
io::{self, Read, Write},
io,
path::{Component, PathBuf},
pin::Pin,
sync::Arc,
};
use tokio::sync::{mpsc, Mutex};
use wezterm_ssh::{
Child, ExecResult, FilePermissions, OpenFileType, OpenOptions, Session as WezSession, WriteMode,
};
const MAX_PIPE_CHUNK_SIZE: usize = 8192;
const READ_PAUSE_MILLIS: u64 = 50;
use wezterm_ssh::{FilePermissions, OpenFileType, OpenOptions, Session as WezSession, WriteMode};
fn to_other_error<E>(err: E) -> io::Error
where
@ -38,13 +36,14 @@ struct Process {
cmd: String,
args: Vec<String>,
detached: bool,
stdin_tx: mpsc::Sender<String>,
stdin_tx: mpsc::Sender<Vec<u8>>,
kill_tx: mpsc::Sender<()>,
resize_tx: mpsc::Sender<PtySize>,
}
type ReplyRet = Pin<Box<dyn Future<Output = bool> + Send + 'static>>;
type PostHook = Box<dyn FnOnce() + Send>;
type PostHook = Box<dyn FnOnce(mpsc::Sender<Vec<ResponseData>>) + Send>;
struct Outgoing {
data: ResponseData,
post_hook: Option<PostHook>,
@ -66,15 +65,11 @@ pub(super) async fn process(
req: Request,
tx: mpsc::Sender<Response>,
) -> Result<(), mpsc::error::SendError<Response>> {
async fn inner<F>(
async fn inner(
session: WezSession,
state: Arc<Mutex<State>>,
data: RequestData,
reply: F,
) -> io::Result<Outgoing>
where
F: FnMut(Vec<ResponseData>) -> ReplyRet + Clone + Send + 'static,
{
) -> io::Result<Outgoing> {
match data {
RequestData::FileRead { path } => file_read(session, path).await,
RequestData::FileReadText { path } => file_read_text(session, path).await,
@ -103,7 +98,11 @@ pub(super) async fn process(
cmd,
args,
detached,
} => proc_run(session, state, reply, cmd, args, detached).await,
pty,
} => proc_spawn(session, state, cmd, args, detached, pty).await,
RequestData::ProcResizePty { id, size } => {
proc_resize_pty(session, state, id, size).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,
@ -126,10 +125,9 @@ pub(super) async fn process(
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 {
match inner(session, state_2, data).await {
Ok(outgoing) => outgoing,
Err(x) => Outgoing::from(ResponseData::from(x)),
}
@ -156,9 +154,18 @@ pub(super) async fn process(
// Send out our primary response from processing the request
let result = tx.send(res).await;
let (tx, mut rx) = mpsc::channel(1);
tokio::spawn(async move {
while let Some(payload) = rx.recv().await {
if !reply(payload).await {
break;
}
}
});
// Invoke all post hooks
for hook in post_hooks {
hook();
hook(tx.clone());
}
result
@ -602,54 +609,33 @@ async fn metadata(
})))
}
async fn proc_run<F>(
async fn proc_spawn(
session: WezSession,
state: Arc<Mutex<State>>,
reply: F,
cmd: String,
args: Vec<String>,
detached: bool,
) -> io::Result<Outgoing>
where
F: FnMut(Vec<ResponseData>) -> ReplyRet + Clone + Send + 'static,
{
let id = rand::random();
pty: Option<PtySize>,
) -> io::Result<Outgoing> {
let cmd_string = format!("{} {}", cmd, args.join(" "));
debug!("<Ssh> Spawning {} (pty: {:?})", cmd_string, pty);
debug!("<Ssh | Proc {}> Spawning {}", id, cmd_string);
let ExecResult {
mut stdin,
mut stdout,
mut stderr,
mut child,
} = session
.exec(&cmd_string, None)
.compat()
.await
.map_err(to_other_error)?;
let state_2 = Arc::clone(&state);
let cleanup = |id: usize| async move {
state_2.lock().await.processes.remove(&id);
};
// 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 SpawnResult {
id,
stdin,
killer,
resizer,
initialize,
} = match pty {
None => process::spawn_simple(&session, &cmd_string, cleanup).await?,
Some(size) => process::spawn_pty(&session, &cmd_string, size, cleanup).await?,
};
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 {
@ -657,195 +643,40 @@ where
cmd,
args,
detached,
stdin_tx,
kill_tx,
stdin_tx: stdin,
kill_tx: killer,
resize_tx: resizer,
},
);
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(x) => {
error!("<Ssh | Proc {}> Stdout unexpectedly closed: {}", id, x);
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(x) => {
error!("<Ssh | Proc {}> Stderr unexpectedly closed: {}", id, x);
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 {}> Killing", 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 {}> Completed and waiting on stdout & stderr tasks",
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: if success { Some(0) } else { None },
}];
if !reply_2(payload).await {
error!("<Ssh | Proc {}> Failed to send done", id,);
}
});
});
debug!(
"<Ssh | Proc {}> Spawned successfully! Will enter post hook later",
id
);
Ok(Outgoing {
data: ResponseData::ProcSpawned { id },
post_hook: Some(post_hook),
post_hook: Some(initialize),
})
}
async fn proc_resize_pty(
_session: WezSession,
state: Arc<Mutex<State>>,
id: usize,
size: PtySize,
) -> io::Result<Outgoing> {
if let Some(process) = state.lock().await.processes.get(&id) {
if process.resize_tx.send(size).await.is_ok() {
return Ok(Outgoing::from(ResponseData::Ok));
}
}
Err(io::Error::new(
io::ErrorKind::BrokenPipe,
format!("<Ssh | Proc {}> Unable to resize process", id),
))
}
async fn proc_kill(
_session: WezSession,
state: Arc<Mutex<State>>,
@ -867,7 +698,7 @@ async fn proc_stdin(
_session: WezSession,
state: Arc<Mutex<State>>,
id: usize,
data: String,
data: Vec<u8>,
) -> io::Result<Outgoing> {
if let Some(process) = state.lock().await.processes.get_mut(&id) {
if process.stdin_tx.send(data).await.is_ok() {
@ -892,6 +723,8 @@ async fn proc_list(_session: WezSession, state: Arc<Mutex<State>>) -> io::Result
cmd: p.cmd.to_string(),
args: p.args.clone(),
detached: p.detached,
// TODO: Support pty size from ssh
pty: None,
id: p.id,
})
.collect(),

@ -7,6 +7,7 @@ use log::*;
use smol::channel::Receiver as SmolReceiver;
use std::{
collections::BTreeMap,
fmt,
io::{self, Write},
net::{IpAddr, SocketAddr},
path::PathBuf,
@ -17,6 +18,38 @@ use tokio::sync::{mpsc, Mutex};
use wezterm_ssh::{Config as WezConfig, Session as WezSession, SessionEvent as WezSessionEvent};
mod handler;
mod process;
/// Represents the backend to use for ssh operations
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum SshBackend {
/// Use libssh as backend
LibSsh,
/// Use ssh2 as backend
Ssh2,
}
impl Default for SshBackend {
/// Defaults to ssh2
///
/// NOTE: There are currently bugs in libssh that cause our implementation to hang related to
/// process stdout/stderr and maybe other logic.
fn default() -> Self {
Self::Ssh2
}
}
impl fmt::Display for SshBackend {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::LibSsh => write!(f, "libssh"),
Self::Ssh2 => write!(f, "ssh2"),
}
}
}
/// Represents a singular authentication prompt for a new ssh session
#[derive(Debug)]
@ -50,6 +83,9 @@ pub struct Ssh2AuthEvent {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Ssh2SessionOpts {
/// Represents the backend to use for ssh operations
pub backend: SshBackend,
/// List of files from which the user's DSA, ECDSA, Ed25519, or RSA authentication identity
/// is read, defaulting to
///
@ -80,6 +116,9 @@ pub struct Ssh2SessionOpts {
/// - `~/.ssh/known_hosts2`
pub user_known_hosts_files: Vec<PathBuf>,
/// If true, will output tracing information from the underlying ssh implementation
pub verbose: bool,
/// Additional options to provide as defined by `ssh_config(5)`
pub other: BTreeMap<String, String>,
}
@ -249,6 +288,12 @@ impl Ssh2Session {
);
}
// Set verbosity optin for ssh lib
config.insert("wezterm_ssh_verbose".to_string(), opts.verbose.to_string());
// Set the backend to use going forward
config.insert("wezterm_ssh_backend".to_string(), opts.backend.to_string());
// Add in any of the other options provided
config.extend(opts.other);
@ -459,7 +504,7 @@ impl Ssh2Session {
// ssh session is closed
debug!("Executing {} {}", bin, args.join(" "));
let mut proc = session
.spawn("<ssh-launch>", bin, args, true)
.spawn("<ssh-launch>", bin, args, true, None)
.await
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let mut stdout = proc.stdout.take().unwrap();
@ -472,17 +517,19 @@ impl Ssh2Session {
// Close out ssh session
session.abort();
let _ = session.wait().await;
let mut output = String::new();
let mut output = Vec::new();
// If successful, grab the session information and establish a connection
// with the distant server
if success {
while let Ok(data) = stdout.read().await {
output.push_str(&data);
output.extend(&data);
}
// Iterate over output as individual lines, looking for session info
let maybe_info = output
.lines()
.split(|&b| b == b'\n')
.map(String::from_utf8_lossy)
.find_map(|line| line.parse::<SessionInfo>().ok());
match maybe_info {
Some(mut info) => {
@ -496,7 +543,7 @@ impl Ssh2Session {
}
} else {
while let Ok(data) = stderr.read().await {
output.push_str(&data);
output.extend(&data);
}
Err(io::Error::new(
@ -505,7 +552,10 @@ impl Ssh2Session {
"Spawning distant failed [{}]: {}",
code.map(|x| x.to_string())
.unwrap_or_else(|| String::from("???")),
output
match String::from_utf8(output) {
Ok(output) => output,
Err(x) => x.to_string(),
}
),
))
}

@ -0,0 +1,411 @@
use async_compat::CompatExt;
use distant_core::{PtySize, ResponseData};
use log::*;
use std::{
future::Future,
io::{self, Read, Write},
time::Duration,
};
use tokio::{sync::mpsc, task::JoinHandle};
use wezterm_ssh::{
Child, ChildKiller, ExecResult, MasterPty, PtySize as PortablePtySize, Session, SshChildProcess,
};
const MAX_PIPE_CHUNK_SIZE: usize = 8192;
const THREAD_PAUSE_MILLIS: u64 = 50;
/// Result of spawning a process, containing means to send stdin, means to kill the process,
/// and the initialization function to use to start processing stdin, stdout, and stderr
pub struct SpawnResult {
pub id: usize,
pub stdin: mpsc::Sender<Vec<u8>>,
pub killer: mpsc::Sender<()>,
pub resizer: mpsc::Sender<PtySize>,
pub initialize: Box<dyn FnOnce(mpsc::Sender<Vec<ResponseData>>) + Send>,
}
/// Spawns a non-pty process, returning a function that initializes processing
/// stdin, stdout, and stderr once called (for lazy processing)
pub async fn spawn_simple<F, R>(session: &Session, cmd: &str, cleanup: F) -> io::Result<SpawnResult>
where
F: FnOnce(usize) -> R + Send + 'static,
R: Future<Output = ()> + Send + 'static,
{
let ExecResult {
mut stdin,
mut stdout,
mut stderr,
mut child,
} = session
.exec(cmd, None)
.compat()
.await
.map_err(to_other_error)?;
// Update to be nonblocking for reading and writing
stdin.set_non_blocking(true).map_err(to_other_error)?;
stdout.set_non_blocking(true).map_err(to_other_error)?;
stderr.set_non_blocking(true).map_err(to_other_error)?;
// 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, stdin_rx) = mpsc::channel(1);
let (kill_tx, kill_rx) = mpsc::channel(1);
let id = rand::random();
let session = session.clone();
let initialize = Box::new(move |reply: mpsc::Sender<Vec<ResponseData>>| {
let stdout_task = spawn_nonblocking_stdout_task(id, stdout, reply.clone());
let stderr_task = spawn_nonblocking_stderr_task(id, stderr, reply.clone());
let stdin_task = spawn_nonblocking_stdin_task(id, stdin, stdin_rx);
let _ = spawn_cleanup_task(
session,
id,
child,
kill_rx,
stdin_task,
stdout_task,
Some(stderr_task),
reply,
cleanup,
);
});
// Create a resizer that is already closed since a simple process does not resize
let resizer = mpsc::channel(1).0;
Ok(SpawnResult {
id,
stdin: stdin_tx,
killer: kill_tx,
resizer,
initialize,
})
}
/// Spawns a pty process, returning a function that initializes processing
/// stdin and stdout/stderr once called (for lazy processing)
pub async fn spawn_pty<F, R>(
session: &Session,
cmd: &str,
size: PtySize,
cleanup: F,
) -> io::Result<SpawnResult>
where
F: FnOnce(usize) -> R + Send + 'static,
R: Future<Output = ()> + Send + 'static,
{
// TODO: Do we need to support other terminal types for TERM?
let (pty, mut child) = session
.request_pty("xterm-256color", to_portable_size(size), Some(cmd), None)
.compat()
.await
.map_err(to_other_error)?;
// 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 reader = pty
.try_clone_reader()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let writer = pty
.try_clone_writer()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let (stdin_tx, stdin_rx) = mpsc::channel(1);
let (kill_tx, kill_rx) = mpsc::channel(1);
let id = rand::random();
let session = session.clone();
let initialize = Box::new(move |reply: mpsc::Sender<Vec<ResponseData>>| {
let stdout_task = spawn_blocking_stdout_task(id, reader, reply.clone());
let stdin_task = spawn_blocking_stdin_task(id, writer, stdin_rx);
let _ = spawn_cleanup_task(
session,
id,
child,
kill_rx,
stdin_task,
stdout_task,
None,
reply,
cleanup,
);
});
let (resize_tx, mut resize_rx) = mpsc::channel::<PtySize>(1);
tokio::spawn(async move {
while let Some(size) = resize_rx.recv().await {
if pty.resize(to_portable_size(size)).is_err() {
break;
}
}
});
Ok(SpawnResult {
id,
stdin: stdin_tx,
killer: kill_tx,
resizer: resize_tx,
initialize,
})
}
fn spawn_blocking_stdout_task(
id: usize,
mut reader: impl Read + Send + 'static,
tx: mpsc::Sender<Vec<ResponseData>>,
) -> JoinHandle<()> {
tokio::task::spawn_blocking(move || {
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match reader.read(&mut buf) {
Ok(n) if n > 0 => {
let payload = vec![ResponseData::ProcStdout {
id,
data: buf[..n].to_vec(),
}];
if tx.blocking_send(payload).is_err() {
error!("<Ssh | Proc {}> Stdout channel closed", id);
break;
}
std::thread::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS));
}
Ok(_) => break,
Err(x) => {
error!("<Ssh | Proc {}> Stdout unexpectedly closed: {}", id, x);
break;
}
}
}
})
}
fn spawn_nonblocking_stdout_task(
id: usize,
mut reader: impl Read + Send + 'static,
tx: mpsc::Sender<Vec<ResponseData>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match reader.read(&mut buf) {
Ok(n) if n > 0 => {
let payload = vec![ResponseData::ProcStdout {
id,
data: buf[..n].to_vec(),
}];
if tx.send(payload).await.is_err() {
error!("<Ssh | Proc {}> Stdout channel closed", id);
break;
}
tokio::time::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS)).await;
}
Ok(_) => break,
Err(x) if x.kind() == io::ErrorKind::WouldBlock => {
tokio::time::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS)).await;
}
Err(x) => {
error!("<Ssh | Proc {}> Stdout unexpectedly closed: {}", id, x);
break;
}
}
}
})
}
fn spawn_nonblocking_stderr_task(
id: usize,
mut reader: impl Read + Send + 'static,
tx: mpsc::Sender<Vec<ResponseData>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut buf: [u8; MAX_PIPE_CHUNK_SIZE] = [0; MAX_PIPE_CHUNK_SIZE];
loop {
match reader.read(&mut buf) {
Ok(n) if n > 0 => {
let payload = vec![ResponseData::ProcStderr {
id,
data: buf[..n].to_vec(),
}];
if tx.send(payload).await.is_err() {
error!("<Ssh | Proc {}> Stderr channel closed", id);
break;
}
tokio::time::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS)).await;
}
Ok(_) => break,
Err(x) if x.kind() == io::ErrorKind::WouldBlock => {
tokio::time::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS)).await;
}
Err(x) => {
error!("<Ssh | Proc {}> Stderr unexpectedly closed: {}", id, x);
break;
}
}
}
})
}
fn spawn_blocking_stdin_task(
id: usize,
mut writer: impl Write + Send + 'static,
mut rx: mpsc::Receiver<Vec<u8>>,
) -> JoinHandle<()> {
tokio::task::spawn_blocking(move || {
while let Some(data) = rx.blocking_recv() {
if let Err(x) = writer.write_all(&data) {
error!("<Ssh | Proc {}> Failed to send stdin: {}", id, x);
break;
}
std::thread::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS));
}
})
}
fn spawn_nonblocking_stdin_task(
id: usize,
mut writer: impl Write + Send + 'static,
mut rx: mpsc::Receiver<Vec<u8>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
while let Some(data) = rx.recv().await {
if let Err(x) = writer.write_all(&data) {
// In non-blocking mode, we'll just pause and try again if
// the IO would block here; otherwise, stop the task
if x.kind() != io::ErrorKind::WouldBlock {
error!("<Ssh | Proc {}> Failed to send stdin: {}", id, x);
break;
}
}
tokio::time::sleep(Duration::from_millis(THREAD_PAUSE_MILLIS)).await;
}
})
}
#[allow(clippy::too_many_arguments)]
fn spawn_cleanup_task<F, R>(
session: Session,
id: usize,
mut child: SshChildProcess,
mut kill_rx: mpsc::Receiver<()>,
stdin_task: JoinHandle<()>,
stdout_task: JoinHandle<()>,
stderr_task: Option<JoinHandle<()>>,
tx: mpsc::Sender<Vec<ResponseData>>,
cleanup: F,
) -> JoinHandle<()>
where
F: FnOnce(usize) -> R + Send + 'static,
R: Future<Output = ()> + Send + 'static,
{
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 {}> Killing", 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 {}> Completed and waiting on stdout & stderr tasks",
id
);
}
// We're done with the child, so drop it
drop(child);
if let Some(task) = stderr_task {
if let Err(x) = 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);
}
cleanup(id).await;
let payload = vec![ResponseData::ProcDone {
id,
success: !should_kill && success,
code: if success { Some(0) } else { None },
}];
if tx.send(payload).await.is_err() {
error!("<Ssh | Proc {}> Failed to send done", id,);
}
})
}
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)
}
fn to_portable_size(size: PtySize) -> PortablePtySize {
PortablePtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
}
}

@ -1348,7 +1348,7 @@ async fn metadata_should_resolve_file_type_of_symlink_if_flag_specified(
#[rstest]
#[tokio::test]
async fn proc_run_should_send_error_over_stderr_on_failure(#[future] session: Session) {
async fn proc_spawn_should_send_error_over_stderr_on_failure(#[future] session: Session) {
let mut session = session.await;
let req = Request::new(
"test-tenant",
@ -1356,6 +1356,7 @@ async fn proc_run_should_send_error_over_stderr_on_failure(#[future] session: Se
cmd: DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(),
args: Vec::new(),
detached: false,
pty: None,
}],
);
@ -1394,7 +1395,7 @@ async fn proc_run_should_send_error_over_stderr_on_failure(#[future] session: Se
#[rstest]
#[tokio::test]
async fn proc_run_should_send_back_proc_start_on_success(#[future] session: Session) {
async fn proc_spawn_should_send_back_proc_start_on_success(#[future] session: Session) {
let mut session = session.await;
let req = Request::new(
"test-tenant",
@ -1402,6 +1403,7 @@ async fn proc_run_should_send_back_proc_start_on_success(#[future] session: Sess
cmd: SCRIPT_RUNNER.to_string(),
args: vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()],
detached: false,
pty: None,
}],
);
@ -1419,7 +1421,9 @@ async fn proc_run_should_send_back_proc_start_on_success(#[future] session: Sess
#[rstest]
#[tokio::test]
#[cfg_attr(windows, ignore)]
async fn proc_run_should_send_back_stdout_periodically_when_available(#[future] session: Session) {
async fn proc_spawn_should_send_back_stdout_periodically_when_available(
#[future] session: Session,
) {
let mut session = session.await;
// Run a program that echoes to stdout
let req = Request::new(
@ -1431,6 +1435,7 @@ async fn proc_run_should_send_back_stdout_periodically_when_available(#[future]
String::from("'some stdout'"),
],
detached: false,
pty: None,
}],
);
@ -1461,7 +1466,7 @@ async fn proc_run_should_send_back_stdout_periodically_when_available(#[future]
assert_eq!(res.payload.len(), 1, "Wrong payload size");
match &res.payload[0] {
ResponseData::ProcStdout { data, .. } => {
assert_eq!(data, "some stdout", "Got wrong stdout");
assert_eq!(data, b"some stdout", "Got wrong stdout");
got_stdout = true;
}
ResponseData::ProcDone { success, .. } => {
@ -1483,7 +1488,9 @@ async fn proc_run_should_send_back_stdout_periodically_when_available(#[future]
#[rstest]
#[tokio::test]
#[cfg_attr(windows, ignore)]
async fn proc_run_should_send_back_stderr_periodically_when_available(#[future] session: Session) {
async fn proc_spawn_should_send_back_stderr_periodically_when_available(
#[future] session: Session,
) {
let mut session = session.await;
// Run a program that echoes to stderr
let req = Request::new(
@ -1495,6 +1502,7 @@ async fn proc_run_should_send_back_stderr_periodically_when_available(#[future]
String::from("'some stderr'"),
],
detached: false,
pty: None,
}],
);
@ -1525,7 +1533,7 @@ async fn proc_run_should_send_back_stderr_periodically_when_available(#[future]
assert_eq!(res.payload.len(), 1, "Wrong payload size");
match &res.payload[0] {
ResponseData::ProcStderr { data, .. } => {
assert_eq!(data, "some stderr", "Got wrong stderr");
assert_eq!(data, b"some stderr", "Got wrong stderr");
got_stderr = true;
}
ResponseData::ProcDone { success, .. } => {
@ -1547,7 +1555,7 @@ async fn proc_run_should_send_back_stderr_periodically_when_available(#[future]
#[rstest]
#[tokio::test]
#[cfg_attr(windows, ignore)]
async fn proc_run_should_clear_process_from_state_when_done(#[future] session: Session) {
async fn proc_spawn_should_clear_process_from_state_when_done(#[future] session: Session) {
let mut session = session.await;
// Run a program that ends after a little bit
let req = Request::new(
@ -1556,6 +1564,7 @@ async fn proc_run_should_clear_process_from_state_when_done(#[future] session: S
cmd: SCRIPT_RUNNER.to_string(),
args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0.1")],
detached: false,
pty: None,
}],
);
let mut mailbox = session.mail(req).await.unwrap();
@ -1595,7 +1604,7 @@ async fn proc_run_should_clear_process_from_state_when_done(#[future] session: S
#[rstest]
#[tokio::test]
async fn proc_run_should_clear_process_from_state_when_killed(#[future] session: Session) {
async fn proc_spawn_should_clear_process_from_state_when_killed(#[future] session: Session) {
let mut session = session.await;
// Run a program that ends slowly
let req = Request::new(
@ -1604,6 +1613,7 @@ async fn proc_run_should_clear_process_from_state_when_killed(#[future] session:
cmd: SCRIPT_RUNNER.to_string(),
args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")],
detached: false,
pty: None,
}],
);
@ -1678,6 +1688,7 @@ async fn proc_kill_should_send_ok_and_done_responses_on_success(#[future] sessio
cmd: SCRIPT_RUNNER.to_string(),
args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")],
detached: false,
pty: None,
}],
);
@ -1721,7 +1732,7 @@ async fn proc_stdin_should_send_error_on_failure(#[future] session: Session) {
"test-tenant",
vec![RequestData::ProcStdin {
id: 0xDEADBEEF,
data: String::from("some input"),
data: b"some input".to_vec(),
}],
);
@ -1753,6 +1764,7 @@ async fn proc_stdin_should_send_ok_on_success_and_properly_send_stdin_to_process
cmd: SCRIPT_RUNNER.to_string(),
args: vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()],
detached: false,
pty: None,
}],
);
let mut mailbox = session.mail(req).await.unwrap();
@ -1773,7 +1785,7 @@ async fn proc_stdin_should_send_ok_on_success_and_properly_send_stdin_to_process
"test-tenant",
vec![RequestData::ProcStdin {
id,
data: String::from("hello world\n"),
data: b"hello world\n".to_vec(),
}],
);
let res = session.send(req).await.unwrap();
@ -1786,7 +1798,7 @@ async fn proc_stdin_should_send_ok_on_success_and_properly_send_stdin_to_process
let res = mailbox.next().await.unwrap();
match &res.payload[0] {
ResponseData::ProcStdout { data, .. } => {
assert_eq!(data, "hello world\n", "Mirrored data didn't match");
assert_eq!(data, b"hello world\n", "Mirrored data didn't match");
}
x => panic!("Unexpected response: {:?}", x),
}
@ -1802,6 +1814,7 @@ async fn proc_list_should_send_proc_entry_list(#[future] session: Session) {
cmd: SCRIPT_RUNNER.to_string(),
args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("10")],
detached: false,
pty: None,
}],
);
@ -1827,6 +1840,7 @@ async fn proc_list_should_send_proc_entry_list(#[future] session: Session) {
cmd: SCRIPT_RUNNER.to_string(),
args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("10")],
detached: false,
pty: None,
id,
}],
},

@ -23,7 +23,7 @@ macro_rules! from_pipes {
let stdin_task = tokio::spawn(async move {
loop {
if let Some(input) = stdin_rx.recv().await {
if let Err(x) = $stdin.write(input.as_str()).await {
if let Err(x) = $stdin.write(&*input).await {
break Err(x);
}
} else {
@ -37,7 +37,7 @@ macro_rules! from_pipes {
match $stdout.read().await {
Ok(output) => {
let mut out = handle.lock();
out.write_all(output.as_bytes())?;
out.write_all(&output)?;
out.flush()?;
}
Err(x) => break Err(x),
@ -50,7 +50,7 @@ macro_rules! from_pipes {
match $stderr.read().await {
Ok(output) => {
let mut out = handle.lock();
out.write_all(output.as_bytes())?;
out.write_all(&output)?;
out.flush()?;
}
Err(x) => break Err(x),

@ -150,6 +150,9 @@ pub enum Subcommand {
/// Specialized treatment of running a remote LSP process
Lsp(LspSubcommand),
/// Specialized treatment of running a remote shell process
Shell(ShellSubcommand),
}
impl Subcommand {
@ -160,6 +163,7 @@ impl Subcommand {
Self::Launch(cmd) => subcommand::launch::run(cmd, opt)?,
Self::Listen(cmd) => subcommand::listen::run(cmd, opt)?,
Self::Lsp(cmd) => subcommand::lsp::run(cmd, opt)?,
Self::Shell(cmd) => subcommand::shell::run(cmd, opt)?,
}
Ok(())
@ -171,7 +175,7 @@ impl Subcommand {
Self::Action(cmd) => cmd
.operation
.as_ref()
.map(|req| req.is_proc_run())
.map(|req| req.is_proc_spawn())
.unwrap_or_default(),
Self::Lsp(_) => true,
_ => false,
@ -678,9 +682,72 @@ pub struct LspSubcommand {
#[structopt(long)]
pub detached: bool,
/// If provided, will run LSP in a pty
#[structopt(long)]
pub pty: bool,
/// Command to run on the remote machine that represents an LSP server
pub cmd: String,
/// Additional arguments to supply to the remote machine
pub args: Vec<String>,
}
/// Represents subcommand to execute some shell on a remote machine
#[derive(Clone, Debug, StructOpt)]
#[structopt(verbatim_doc_comment)]
pub struct ShellSubcommand {
/// Represents the format that results should be returned
///
/// Currently, there are two possible formats:
///
/// 1. "json": printing out JSON for external program usage
///
/// 2. "shell": printing out human-readable results for interactive shell usage
#[structopt(
short,
long,
case_insensitive = true,
default_value = Format::Shell.into(),
possible_values = Format::VARIANTS
)]
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
)]
pub session: SessionInput,
/// Contains additional information related to sessions
#[structopt(flatten)]
pub session_data: SessionOpt,
/// SSH connection settings when method is ssh
#[structopt(flatten)]
pub ssh_connection: SshConnectionOpts,
/// If provided, will run in detached mode, meaning that the process will not be killed if the
/// client disconnects from the server
#[structopt(long)]
pub detached: bool,
/// Command to run on the remote machine as the shell (defaults to $TERM)
pub cmd: Option<String>,
/// Additional arguments to supply to the shell (defaults to nothing)
pub args: Vec<String>,
}

@ -5,13 +5,14 @@ use distant_core::{
};
use log::*;
use std::io;
use std::io::Write;
/// Represents the output content and destination
pub enum ResponseOut {
Stdout(String),
StdoutLine(String),
Stderr(String),
StderrLine(String),
Stdout(Vec<u8>),
StdoutLine(Vec<u8>),
Stderr(Vec<u8>),
StderrLine(Vec<u8>),
None,
}
@ -22,7 +23,7 @@ impl ResponseOut {
Ok(match format {
Format::Json => ResponseOut::StdoutLine(
serde_json::to_string(&res)
serde_json::to_vec(&res)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?,
),
@ -49,21 +50,46 @@ impl ResponseOut {
// LSP protocol, the JSON content is not followed by a
// newline and was not picked up when the response was
// sent back to the client; so, we need to manually flush
use std::io::Write;
print!("{}", x);
if let Err(x) = std::io::stdout().lock().flush() {
if let Err(x) = io::stdout().lock().write_all(&x) {
error!("Failed to write stdout: {}", x);
}
if let Err(x) = io::stdout().lock().flush() {
error!("Failed to flush stdout: {}", x);
}
}
Self::StdoutLine(x) => println!("{}", x),
Self::StdoutLine(x) => {
if let Err(x) = io::stdout().lock().write_all(&x) {
error!("Failed to write stdout: {}", x);
}
if let Err(x) = io::stdout().lock().write(b"\n") {
error!("Failed to write stdout newline: {}", x);
}
}
Self::Stderr(x) => {
use std::io::Write;
eprint!("{}", x);
if let Err(x) = std::io::stderr().lock().flush() {
// NOTE: Because we are not including a newline in the output,
// it is not guaranteed to be written out. In the case of
// LSP protocol, the JSON content is not followed by a
// newline and was not picked up when the response was
// sent back to the client; so, we need to manually flush
if let Err(x) = io::stderr().lock().write_all(&x) {
error!("Failed to write stderr: {}", x);
}
if let Err(x) = io::stderr().lock().flush() {
error!("Failed to flush stderr: {}", x);
}
}
Self::StderrLine(x) => eprintln!("{}", x),
Self::StderrLine(x) => {
if let Err(x) = io::stderr().lock().write_all(&x) {
error!("Failed to write stderr: {}", x);
}
if let Err(x) = io::stderr().lock().write(b"\n") {
error!("Failed to write stderr newline: {}", x);
}
}
Self::None => {}
}
}
@ -73,12 +99,10 @@ fn format_shell(data: ResponseData) -> ResponseOut {
match data {
ResponseData::Ok => ResponseOut::None,
ResponseData::Error(Error { kind, description }) => {
ResponseOut::StderrLine(format!("Failed ({}): '{}'.", kind, description))
}
ResponseData::Blob { data } => {
ResponseOut::StdoutLine(String::from_utf8_lossy(&data).to_string())
ResponseOut::StderrLine(format!("Failed ({}): '{}'.", kind, description).into_bytes())
}
ResponseData::Text { data } => ResponseOut::StdoutLine(data),
ResponseData::Blob { data } => ResponseOut::StdoutLine(data),
ResponseData::Text { data } => ResponseOut::StdoutLine(data.into_bytes()),
ResponseData::DirEntries { entries, .. } => ResponseOut::StdoutLine(
entries
.into_iter()
@ -100,13 +124,14 @@ fn format_shell(data: ResponseData) -> ResponseOut {
)
})
.collect::<Vec<String>>()
.join("\n"),
.join("\n")
.into_bytes(),
),
ResponseData::Exists { value: exists } => {
if exists {
ResponseOut::StdoutLine("true".to_string())
ResponseOut::StdoutLine(b"true".to_vec())
} else {
ResponseOut::StdoutLine("false".to_string())
ResponseOut::StdoutLine(b"false".to_vec())
}
}
ResponseData::Metadata(Metadata {
@ -117,32 +142,36 @@ fn format_shell(data: ResponseData) -> ResponseOut {
accessed,
created,
modified,
}) => ResponseOut::StdoutLine(format!(
concat!(
"{}",
"Type: {}\n",
"Len: {}\n",
"Readonly: {}\n",
"Created: {}\n",
"Last Accessed: {}\n",
"Last Modified: {}",
),
canonicalized_path
.map(|p| format!("Canonicalized Path: {:?}\n", p))
.unwrap_or_default(),
file_type.as_ref(),
len,
readonly,
created.unwrap_or_default(),
accessed.unwrap_or_default(),
modified.unwrap_or_default(),
)),
}) => ResponseOut::StdoutLine(
format!(
concat!(
"{}",
"Type: {}\n",
"Len: {}\n",
"Readonly: {}\n",
"Created: {}\n",
"Last Accessed: {}\n",
"Last Modified: {}",
),
canonicalized_path
.map(|p| format!("Canonicalized Path: {:?}\n", p))
.unwrap_or_default(),
file_type.as_ref(),
len,
readonly,
created.unwrap_or_default(),
accessed.unwrap_or_default(),
modified.unwrap_or_default(),
)
.into_bytes(),
),
ResponseData::ProcEntries { entries } => ResponseOut::StdoutLine(
entries
.into_iter()
.map(|entry| format!("{}: {} {}", entry.id, entry.cmd, entry.args.join(" ")))
.collect::<Vec<String>>()
.join("\n"),
.join("\n")
.into_bytes(),
),
ResponseData::ProcSpawned { .. } => ResponseOut::None,
ResponseData::ProcStdout { data, .. } => ResponseOut::Stdout(data),
@ -151,9 +180,11 @@ fn format_shell(data: ResponseData) -> ResponseOut {
if success {
ResponseOut::None
} else if let Some(code) = code {
ResponseOut::StderrLine(format!("Proc {} failed with code {}", id, code))
ResponseOut::StderrLine(
format!("Proc {} failed with code {}", id, code).into_bytes(),
)
} else {
ResponseOut::StderrLine(format!("Proc {} failed", id))
ResponseOut::StderrLine(format!("Proc {} failed", id).into_bytes())
}
}
ResponseData::SystemInfo(SystemInfo {
@ -162,15 +193,18 @@ fn format_shell(data: ResponseData) -> ResponseOut {
arch,
current_dir,
main_separator,
}) => ResponseOut::StdoutLine(format!(
concat!(
"Family: {:?}\n",
"Operating System: {:?}\n",
"Arch: {:?}\n",
"Cwd: {:?}\n",
"Path Sep: {:?}",
),
family, os, arch, current_dir, main_separator,
)),
}) => ResponseOut::StdoutLine(
format!(
concat!(
"Family: {:?}\n",
"Operating System: {:?}\n",
"Arch: {:?}\n",
"Cwd: {:?}\n",
"Path Sep: {:?}",
),
family, os, arch, current_dir, main_separator,
)
.into_bytes(),
),
}
}

@ -31,7 +31,7 @@ impl CliSession {
tenant: String,
session: Session,
format: Format,
stdin_rx: mpsc::Receiver<String>,
stdin_rx: mpsc::Receiver<Vec<u8>>,
) -> Self {
let map_line = move |line: &str| match format {
Format::Json => serde_json::from_str(line)
@ -86,7 +86,7 @@ async fn process_mailbox(mut mailbox: Mailbox, format: Format, exit: oneshot::Re
/// responses
async fn process_outgoing_requests<F>(
mut session: Session,
mut stdin_rx: mpsc::Receiver<String>,
mut stdin_rx: mpsc::Receiver<Vec<u8>>,
format: Format,
map_line: F,
) where
@ -96,6 +96,15 @@ async fn process_outgoing_requests<F>(
let mut mailbox_exits = Vec::new();
while let Some(data) = stdin_rx.recv().await {
// TODO: Should we support raw bytes? If so, we need to rewrite map_line to take Vec<u8>
let data = match String::from_utf8(data) {
Ok(data) => data,
Err(x) => {
error!("Bad stdin: {}", x);
continue;
}
};
// Update our buffer with the new data and split it into concrete lines and remainder
buf.push_str(&data);
let (lines, new_buf) = buf.into_full_lines();

@ -7,7 +7,7 @@ use tokio::sync::mpsc;
/// Creates a new thread that performs stdin reads in a blocking fashion, returning
/// a handle to the thread and a receiver that will be sent input as it becomes available
pub fn spawn_channel(buffer: usize) -> (thread::JoinHandle<()>, mpsc::Receiver<String>) {
pub fn spawn_channel(buffer: usize) -> (thread::JoinHandle<()>, mpsc::Receiver<Vec<u8>>) {
let (tx, rx) = mpsc::channel(1);
// NOTE: Using blocking I/O per tokio's advice to read from stdin line-by-line and then
@ -22,16 +22,9 @@ pub fn spawn_channel(buffer: usize) -> (thread::JoinHandle<()>, mpsc::Receiver<S
match stdin.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
match String::from_utf8(buf[..n].to_vec()) {
Ok(text) => {
if let Err(x) = tx.blocking_send(text) {
error!("Stdin channel closed: {}", x);
break;
}
}
Err(x) => {
error!("Input over stdin is invalid: {}", x);
}
if let Err(x) = tx.blocking_send(buf[..n].to_vec()) {
error!("Stdin channel closed: {}", x);
break;
}
thread::yield_now();
}

@ -92,6 +92,7 @@ async fn start(
cmd,
args,
detached,
pty,
}),
) if is_shell_format => {
let mut proc = RemoteProcess::spawn(
@ -100,6 +101,7 @@ async fn start(
cmd,
args,
detached,
pty,
)
.await?;

@ -6,7 +6,8 @@ use crate::{
utils,
};
use derive_more::{Display, Error, From};
use distant_core::{LspData, RemoteLspProcess, RemoteProcessError, Session};
use distant_core::{LspData, PtySize, RemoteLspProcess, RemoteProcessError, Session};
use terminal_size::{terminal_size, Height, Width};
use tokio::io;
#[derive(Debug, Display, Error, From)]
@ -74,6 +75,12 @@ async fn start(
cmd.cmd,
cmd.args,
cmd.detached,
if cmd.pty {
terminal_size()
.map(|(Width(width), Height(height))| PtySize::from_rows_and_cols(height, width))
} else {
None
},
)
.await?;
@ -83,7 +90,7 @@ async fn start(
proc.stdin
.as_mut()
.unwrap()
.write(&data.to_string())
.write(data.to_string().as_bytes())
.await?;
}

@ -15,6 +15,7 @@ pub mod action;
pub mod launch;
pub mod listen;
pub mod lsp;
pub mod shell;
struct CommandRunner {
method: Method,

@ -0,0 +1,164 @@
use crate::{
exit::{ExitCode, ExitCodeError},
link::RemoteProcessLink,
opt::{CommonOpt, ShellSubcommand},
subcommand::CommandRunner,
utils,
};
use derive_more::{Display, Error, From};
use distant_core::{LspData, PtySize, RemoteProcess, RemoteProcessError, RemoteStdin, Session};
use log::*;
use terminal_size::{terminal_size, Height, Width};
use termwiz::{
caps::Capabilities,
input::{InputEvent, KeyCodeEncodeModes},
terminal::{new_terminal, Terminal},
};
use tokio::{io, time::Duration};
#[derive(Debug, Display, Error, From)]
pub enum Error {
#[display(fmt = "Process failed with exit code: {}", _0)]
BadProcessExit(#[error(not(source))] i32),
Io(io::Error),
RemoteProcess(RemoteProcessError),
}
impl ExitCodeError for Error {
fn is_silent(&self) -> bool {
match self {
Self::RemoteProcess(x) => x.is_silent(),
_ => false,
}
}
fn to_exit_code(&self) -> ExitCode {
match self {
Self::BadProcessExit(x) => ExitCode::Custom(*x),
Self::Io(x) => x.to_exit_code(),
Self::RemoteProcess(x) => x.to_exit_code(),
}
}
}
pub fn run(cmd: ShellSubcommand, opt: CommonOpt) -> Result<(), Error> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { run_async(cmd, opt).await })
}
async fn run_async(cmd: ShellSubcommand, 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();
CommandRunner {
method,
ssh_connection,
session_input,
session_file,
session_socket,
timeout,
}
.run(
|session, _, lsp_data| Box::pin(start(cmd, session, lsp_data)),
Error::Io,
)
.await
}
async fn start(
cmd: ShellSubcommand,
session: Session,
lsp_data: Option<LspData>,
) -> Result<(), Error> {
let mut proc = RemoteProcess::spawn(
utils::new_tenant(),
session.clone_channel(),
cmd.cmd.unwrap_or_else(|| "/bin/sh".to_string()),
cmd.args,
cmd.detached,
terminal_size().map(|(Width(cols), Height(rows))| PtySize::from_rows_and_cols(rows, cols)),
)
.await?;
// If we also parsed an LSP's initialize request for its session, we want to forward
// it along in the case of a process call
if let Some(data) = lsp_data {
proc.stdin
.as_mut()
.unwrap()
.write(data.to_string().as_bytes())
.await?;
}
// Create a new terminal in raw mode
let mut terminal = new_terminal(
Capabilities::new_from_env().map_err(|x| io::Error::new(io::ErrorKind::Other, x))?,
)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
terminal
.set_raw_mode()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let mut stdin = proc.stdin.take().unwrap();
let resizer = proc.clone_resizer();
tokio::spawn(async move {
while let Ok(input) = terminal.poll_input(Some(Duration::new(0, 0))) {
match input {
Some(InputEvent::Key(ev)) => {
if let Ok(input) = ev.key.encode(
ev.modifiers,
KeyCodeEncodeModes {
enable_csi_u_key_encoding: false,
application_cursor_keys: false,
newline_mode: false,
},
) {
if let Err(x) = stdin.write_str(input).await {
error!("Failed to write to stdin of remote process: {}", x);
break;
}
}
}
Some(InputEvent::Resized { cols, rows }) => {
if let Err(x) = resizer
.resize(PtySize::from_rows_and_cols(rows as u16, cols as u16))
.await
{
error!("Failed to resize remote process: {}", x);
break;
}
}
Some(_) => continue,
None => tokio::time::sleep(Duration::from_millis(1)).await,
}
}
});
// Now, map the remote LSP server's stdin/stdout/stderr to our own process
let link = RemoteProcessLink::from_remote_pipes(
RemoteStdin::disconnected(),
proc.stdout.take().unwrap(),
proc.stderr.take().unwrap(),
);
// Continually loop to check for terminal resize changes while the process is still running
let (success, exit_code) = proc.wait().await?;
// Shut down our link
link.shutdown().await;
if !success {
if let Some(code) = exit_code {
return Err(Error::BadProcessExit(code));
} else {
return Err(Error::BadProcessExit(1));
}
}
Ok(())
}

@ -9,7 +9,7 @@ mod file_read_text;
mod file_write;
mod file_write_text;
mod metadata;
mod proc_run;
mod proc_spawn;
mod remove;
mod rename;
mod system_info;

@ -83,9 +83,9 @@ macro_rules! next_two_msgs {
#[rstest]
fn should_execute_program_and_return_exit_status(mut action_cmd: Command) {
// distant action proc-run -- {cmd} [args]
// distant action proc-spawn -- {cmd} [args]
action_cmd
.args(&["proc-run", "--"])
.args(&["proc-spawn", "--"])
.arg(SCRIPT_RUNNER.as_str())
.arg(EXIT_CODE_SH.to_str().unwrap())
.arg("0")
@ -97,9 +97,9 @@ fn should_execute_program_and_return_exit_status(mut action_cmd: Command) {
#[rstest]
fn should_capture_and_print_stdout(mut action_cmd: Command) {
// distant action proc-run {cmd} [args]
// distant action proc-spawn {cmd} [args]
action_cmd
.args(&["proc-run", "--"])
.args(&["proc-spawn", "--"])
.arg(SCRIPT_RUNNER.as_str())
.arg(ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap())
.arg("hello world")
@ -111,9 +111,9 @@ fn should_capture_and_print_stdout(mut action_cmd: Command) {
#[rstest]
fn should_capture_and_print_stderr(mut action_cmd: Command) {
// distant action proc-run {cmd} [args]
// distant action proc-spawn {cmd} [args]
action_cmd
.args(&["proc-run", "--"])
.args(&["proc-spawn", "--"])
.arg(SCRIPT_RUNNER.as_str())
.arg(ECHO_ARGS_TO_STDERR_SH.to_str().unwrap())
.arg("hello world")
@ -125,9 +125,9 @@ fn should_capture_and_print_stderr(mut action_cmd: Command) {
#[rstest]
fn should_forward_stdin_to_remote_process(mut action_cmd: Command) {
// distant action proc-run {cmd} [args]
// distant action proc-spawn {cmd} [args]
action_cmd
.args(&["proc-run", "--"])
.args(&["proc-spawn", "--"])
.arg(SCRIPT_RUNNER.as_str())
.arg(ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap())
.write_stdin("hello world\n")
@ -139,9 +139,9 @@ fn should_forward_stdin_to_remote_process(mut action_cmd: Command) {
#[rstest]
fn reflect_the_exit_code_of_the_process(mut action_cmd: Command) {
// distant action proc-run {cmd} [args]
// distant action proc-spawn {cmd} [args]
action_cmd
.args(&["proc-run", "--"])
.args(&["proc-spawn", "--"])
.arg(SCRIPT_RUNNER.as_str())
.arg(EXIT_CODE_SH.to_str().unwrap())
.arg("99")
@ -153,9 +153,9 @@ fn reflect_the_exit_code_of_the_process(mut action_cmd: Command) {
#[rstest]
fn yield_an_error_when_fails(mut action_cmd: Command) {
// distant action proc-run {cmd} [args]
// distant action proc-spawn {cmd} [args]
action_cmd
.args(&["proc-run", "--"])
.args(&["proc-spawn", "--"])
.arg(DOES_NOT_EXIST_BIN.to_str().unwrap())
.assert()
.code(ExitCode::IoError.to_i32())
@ -172,6 +172,7 @@ fn should_support_json_to_execute_program_and_return_exit_status(mut action_cmd:
cmd: SCRIPT_RUNNER.to_string(),
args: vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()],
detached: false,
pty: None,
}],
};
@ -205,6 +206,7 @@ fn should_support_json_to_capture_and_print_stdout(ctx: &'_ DistantServerCtx) {
output.to_string(),
],
detached: false,
pty: None,
}],
};
@ -240,7 +242,7 @@ fn should_support_json_to_capture_and_print_stdout(ctx: &'_ DistantServerCtx) {
friendly_recv_line(&stdout, Duration::from_secs(1)).expect("Failed to get proc stdout");
let res: Response = serde_json::from_str(&out).unwrap();
match &res.payload[0] {
ResponseData::ProcStdout { data, .. } => assert_eq!(data, &output),
ResponseData::ProcStdout { data, .. } => assert_eq!(data, output.as_bytes()),
x => panic!("Unexpected response: {:?}", x),
};
@ -274,6 +276,7 @@ fn should_support_json_to_capture_and_print_stderr(ctx: &'_ DistantServerCtx) {
output.to_string(),
],
detached: false,
pty: None,
}],
};
@ -309,7 +312,7 @@ fn should_support_json_to_capture_and_print_stderr(ctx: &'_ DistantServerCtx) {
friendly_recv_line(&stdout, Duration::from_secs(1)).expect("Failed to get proc stderr");
let res: Response = serde_json::from_str(&out).unwrap();
match &res.payload[0] {
ResponseData::ProcStderr { data, .. } => assert_eq!(data, &output),
ResponseData::ProcStderr { data, .. } => assert_eq!(data, output.as_bytes()),
x => panic!("Unexpected response: {:?}", x),
};
@ -339,6 +342,7 @@ fn should_support_json_to_forward_stdin_to_remote_process(ctx: &'_ DistantServer
cmd: SCRIPT_RUNNER.to_string(),
args: vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()],
detached: false,
pty: None,
}],
};
@ -374,7 +378,7 @@ fn should_support_json_to_forward_stdin_to_remote_process(ctx: &'_ DistantServer
tenant: random_tenant(),
payload: vec![RequestData::ProcStdin {
id,
data: String::from("hello world\n"),
data: b"hello world\n".to_vec(),
}],
};
let req_string = format!("{}\n", serde_json::to_string(&req).unwrap());
@ -385,10 +389,10 @@ fn should_support_json_to_forward_stdin_to_remote_process(ctx: &'_ DistantServer
let (res1, res2) = next_two_msgs!(&stdout);
match (&res1.payload[0], &res2.payload[0]) {
(ResponseData::Ok, ResponseData::ProcStdout { data, .. }) => {
assert_eq!(data, "hello world\n")
assert_eq!(data, b"hello world\n")
}
(ResponseData::ProcStdout { data, .. }, ResponseData::Ok) => {
assert_eq!(data, "hello world\n")
assert_eq!(data, b"hello world\n")
}
x => panic!("Unexpected responses: {:?}", x),
};
@ -433,6 +437,7 @@ fn should_support_json_output_for_error(mut action_cmd: Command) {
cmd: DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(),
args: Vec::new(),
detached: false,
pty: None,
}],
};
Loading…
Cancel
Save