rust: local shell client on portable-pty
This commit is contained in:
Generated
+168
-15
@@ -89,6 +89,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -296,6 +302,12 @@ dependencies = [
|
|||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.2"
|
version = "2.13.2"
|
||||||
@@ -435,6 +447,12 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg_aliases"
|
name = "cfg_aliases"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -784,6 +802,12 @@ dependencies = [
|
|||||||
"ctutils",
|
"ctutils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast-rs"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecdsa"
|
name = "ecdsa"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@@ -956,6 +980,17 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
|
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filedescriptor"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@@ -1373,6 +1408,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.190"
|
version = "0.2.190"
|
||||||
@@ -1504,6 +1545,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"keyring",
|
"keyring",
|
||||||
|
"portable-pty",
|
||||||
"rand 0.8.8",
|
"rand 0.8.8",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"russh",
|
"russh",
|
||||||
@@ -1516,15 +1558,27 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases 0.1.1",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.2",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
@@ -1535,9 +1589,9 @@ version = "0.31.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.2",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1702,7 +1756,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"rand 0.10.3",
|
"rand 0.10.3",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"thiserror",
|
"thiserror 2.0.21",
|
||||||
"tokio",
|
"tokio",
|
||||||
"windows",
|
"windows",
|
||||||
"windows-strings",
|
"windows-strings",
|
||||||
@@ -1897,6 +1951,27 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-pty"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"downcast-rs",
|
||||||
|
"filedescriptor",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"nix 0.28.0",
|
||||||
|
"serial2",
|
||||||
|
"shared_library",
|
||||||
|
"shell-words",
|
||||||
|
"winapi",
|
||||||
|
"winreg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -2019,7 +2094,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2071,7 +2146,7 @@ version = "0.37.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
|
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
@@ -2086,7 +2161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ba61e87b9ec9a39a59a6bbed4c0b8ff7fb07073405b24a767a1f2c8560ac8f58"
|
checksum = "ba61e87b9ec9a39a59a6bbed4c0b8ff7fb07073405b24a767a1f2c8560ac8f58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.9.3",
|
"aes 0.9.3",
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"block-padding 0.4.2",
|
"block-padding 0.4.2",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2143,7 +2218,7 @@ dependencies = [
|
|||||||
"ssh-encoding",
|
"ssh-encoding",
|
||||||
"ssh-key",
|
"ssh-key",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror",
|
"thiserror 2.0.21",
|
||||||
"tokio",
|
"tokio",
|
||||||
"typenum",
|
"typenum",
|
||||||
"universal-hash 0.6.1",
|
"universal-hash 0.6.1",
|
||||||
@@ -2168,7 +2243,7 @@ version = "3.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9bb17f72aba4314656b359ffba72c68dcbd70ed9e97497898b75498fe7198042"
|
checksum = "9bb17f72aba4314656b359ffba72c68dcbd70ed9e97497898b75498fe7198042"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
@@ -2176,7 +2251,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"thiserror",
|
"thiserror 2.0.21",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@@ -2209,7 +2284,7 @@ version = "1.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -2289,7 +2364,7 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2302,7 +2377,7 @@ version = "3.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.2",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2399,6 +2474,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serial2"
|
||||||
|
version = "0.2.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -2464,6 +2550,22 @@ dependencies = [
|
|||||||
"sponge-cursor",
|
"sponge-cursor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared_library"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -2640,13 +2742,33 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.21"
|
version = "2.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2898,6 +3020,28 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
@@ -3182,6 +3326,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wnaf"
|
name = "wnaf"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ tokio = { version = "1.53.2", features = ["full"] }
|
|||||||
sha2 = "0.11.0"
|
sha2 = "0.11.0"
|
||||||
hmac = "0.13.0"
|
hmac = "0.13.0"
|
||||||
sha1 = "0.11.0"
|
sha1 = "0.11.0"
|
||||||
|
portable-pty = "0.9.0"
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
//! Local shell in a PTY (forkpty on Unix, ConPTY on Windows), a port of internal/localshell.
|
||||||
|
|
||||||
|
use super::Callbacks;
|
||||||
|
use crate::Result;
|
||||||
|
use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
pub fn default_shell() -> String {
|
||||||
|
if cfg!(windows) {
|
||||||
|
let ps = std::env::var_os("SystemRoot")
|
||||||
|
.map(|r| PathBuf::from(r).join(r"System32\WindowsPowerShell\v1.0\powershell.exe"))
|
||||||
|
.filter(|p| p.exists());
|
||||||
|
return ps.map_or_else(|| "cmd.exe".into(), |p| p.to_string_lossy().into());
|
||||||
|
}
|
||||||
|
std::env::var("SHELL").ok().filter(|s| !s.is_empty()).unwrap_or_else(|| "/bin/sh".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_utf8_locale(v: &str) -> bool {
|
||||||
|
let v = v.to_lowercase();
|
||||||
|
v.contains("utf-8") || v.contains("utf8")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TERM=xterm-256color and a UTF-8 character locale, keeping the user's environment otherwise.
|
||||||
|
pub fn local_shell_env(env: &mut Vec<(String, String)>) {
|
||||||
|
let set = |env: &mut Vec<(String, String)>, k: &str, v: &str| {
|
||||||
|
env.retain(|(ek, _)| ek != k);
|
||||||
|
env.push((k.into(), v.into()));
|
||||||
|
};
|
||||||
|
set(env, "TERM", "xterm-256color");
|
||||||
|
let get = |k: &str| env.iter().find(|(ek, _)| ek == k).map(|(_, v)| v.clone());
|
||||||
|
let effective = get("LC_ALL").or_else(|| get("LC_CTYPE")).or_else(|| get("LANG"));
|
||||||
|
if effective.as_deref().is_some_and(is_utf8_locale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let locale = if cfg!(target_os = "macos") { "en_US.UTF-8" } else { "C.UTF-8" };
|
||||||
|
let key = if get("LC_ALL").is_some() {
|
||||||
|
"LC_ALL"
|
||||||
|
} else if get("LC_CTYPE").is_some() || get("LANG").is_some() {
|
||||||
|
"LC_CTYPE"
|
||||||
|
} else {
|
||||||
|
"LANG"
|
||||||
|
};
|
||||||
|
set(env, key, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Running {
|
||||||
|
master: Box<dyn MasterPty + Send>,
|
||||||
|
writer: Box<dyn Write + Send>,
|
||||||
|
killer: Box<dyn portable_pty::ChildKiller + Send + Sync>,
|
||||||
|
pid: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LocalShell {
|
||||||
|
shell: String,
|
||||||
|
cb: Callbacks,
|
||||||
|
state: Mutex<(Option<Running>, PathBuf, u16, u16, bool)>, // running, cwd, cols, rows, closed
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalShell {
|
||||||
|
pub fn new(shell: &str, start_dir: Option<PathBuf>, cols: u16, rows: u16, cb: Callbacks) -> Arc<LocalShell> {
|
||||||
|
let shell = if shell.is_empty() { default_shell() } else { shell.to_string() };
|
||||||
|
let dir = start_dir.filter(|d| d.is_dir()).or_else(home_dir).unwrap_or_else(|| PathBuf::from("/"));
|
||||||
|
Arc::new(LocalShell { shell, cb, state: Mutex::new((None, dir, cols.max(1), rows.max(1), false)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self: &Arc<Self>) -> Result<()> {
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
if st.4 {
|
||||||
|
return Err("shell already closed".into());
|
||||||
|
}
|
||||||
|
if st.0.is_some() {
|
||||||
|
return Err("shell already running".into());
|
||||||
|
}
|
||||||
|
let pair = native_pty_system()
|
||||||
|
.openpty(PtySize { rows: st.3, cols: st.2, pixel_width: 0, pixel_height: 0 })
|
||||||
|
.map_err(|e| format!("failed to start shell {:?}: {e}", self.shell))?;
|
||||||
|
let mut cmd = CommandBuilder::new(&self.shell);
|
||||||
|
let mut env: Vec<(String, String)> = std::env::vars().collect();
|
||||||
|
local_shell_env(&mut env);
|
||||||
|
cmd.env_clear();
|
||||||
|
for (k, v) in env {
|
||||||
|
cmd.env(k, v);
|
||||||
|
}
|
||||||
|
cmd.cwd(&st.1);
|
||||||
|
let mut child = pair.slave.spawn_command(cmd).map_err(|e| format!("failed to start shell {:?}: {e}", self.shell))?;
|
||||||
|
drop(pair.slave);
|
||||||
|
let mut reader = pair.master.try_clone_reader()?;
|
||||||
|
let writer = pair.master.take_writer()?;
|
||||||
|
let pid = child.process_id();
|
||||||
|
st.0 = Some(Running { master: pair.master, writer, killer: child.clone_killer(), pid });
|
||||||
|
drop(st);
|
||||||
|
|
||||||
|
let me = self.clone();
|
||||||
|
std::thread::Builder::new().name("nexus-local-read".into()).spawn(move || {
|
||||||
|
let mut buf = vec![0u8; 32 * 1024];
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut buf) {
|
||||||
|
Ok(0) | Err(_) => break,
|
||||||
|
Ok(n) => (me.cb.on_data)(&buf[..n]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let me = self.clone();
|
||||||
|
std::thread::Builder::new().name("nexus-local-wait".into()).spawn(move || {
|
||||||
|
let _ = child.wait();
|
||||||
|
let was_closed = {
|
||||||
|
let mut st = me.state.lock().unwrap();
|
||||||
|
let was = st.4;
|
||||||
|
st.4 = true;
|
||||||
|
st.0 = None;
|
||||||
|
was
|
||||||
|
};
|
||||||
|
if !was_closed {
|
||||||
|
(me.cb.on_close)();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self, data: &[u8]) -> Result<()> {
|
||||||
|
if data.iter().any(|b| matches!(b, b'\r' | b'\n' | 4)) {
|
||||||
|
self.capture_cwd();
|
||||||
|
}
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
let r = st.0.as_mut().ok_or("shell not running")?;
|
||||||
|
r.writer.write_all(data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
if cols > 0 {
|
||||||
|
st.2 = cols;
|
||||||
|
}
|
||||||
|
if rows > 0 {
|
||||||
|
st.3 = rows;
|
||||||
|
}
|
||||||
|
let size = PtySize { rows: st.3, cols: st.2, pixel_width: 0, pixel_height: 0 };
|
||||||
|
if let Some(r) = &st.0 {
|
||||||
|
r.master.resize(size)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Idempotent; does not fire on_close (matches Go: an explicit close is not an exit).
|
||||||
|
pub fn close(&self) {
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
if st.4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
st.4 = true;
|
||||||
|
if let Some(mut r) = st.0.take() {
|
||||||
|
let _ = r.killer.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last directory the shell was seen in (used to restart in the same place).
|
||||||
|
pub fn working_directory(&self) -> PathBuf {
|
||||||
|
self.state.lock().unwrap().1.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capture_cwd(&self) {
|
||||||
|
let pid = self.state.lock().unwrap().0.as_ref().and_then(|r| r.pid);
|
||||||
|
if let Some(dir) = pid.and_then(process_cwd).filter(|d| d.is_dir()) {
|
||||||
|
self.state.lock().unwrap().1 = dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home_dir() -> Option<PathBuf> {
|
||||||
|
std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).map(PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_cwd(pid: u32) -> Option<PathBuf> {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
return std::fs::read_link(format!("/proc/{pid}/cwd")).ok();
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
let out = std::process::Command::new("/usr/sbin/lsof").args(["-a", "-p", &pid.to_string(), "-d", "cwd", "-Fn"]).output().ok()?;
|
||||||
|
return String::from_utf8_lossy(&out.stdout).lines().find_map(|l| l.strip_prefix('n').map(PathBuf::from));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, unix))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runs_echoes_and_reports_exit() {
|
||||||
|
let seen: Arc<Mutex<Vec<u8>>> = Arc::default();
|
||||||
|
let closed = Arc::new(Mutex::new(0));
|
||||||
|
let (s2, c2) = (seen.clone(), closed.clone());
|
||||||
|
let cb = Callbacks {
|
||||||
|
on_data: Arc::new(move |d| s2.lock().unwrap().extend_from_slice(d)),
|
||||||
|
on_error: Arc::new(|_| {}),
|
||||||
|
on_close: Arc::new(move || *c2.lock().unwrap() += 1),
|
||||||
|
};
|
||||||
|
let sh = LocalShell::new("/bin/sh", Some("/tmp".into()), 80, 24, cb);
|
||||||
|
sh.start().unwrap();
|
||||||
|
sh.write(b"cd / && echo local-$((2*21))\n").unwrap();
|
||||||
|
let t = Instant::now();
|
||||||
|
while !String::from_utf8_lossy(&seen.lock().unwrap()).contains("local-42") {
|
||||||
|
assert!(t.elapsed() < Duration::from_secs(5));
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
sh.resize(100, 30).unwrap();
|
||||||
|
sh.write(b"exit\n").unwrap();
|
||||||
|
while *closed.lock().unwrap() == 0 {
|
||||||
|
assert!(t.elapsed() < Duration::from_secs(5));
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
assert_eq!(sh.working_directory(), PathBuf::from("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn env_gets_term_and_utf8() {
|
||||||
|
let mut e = vec![("LANG".to_string(), "C".to_string())];
|
||||||
|
local_shell_env(&mut e);
|
||||||
|
assert!(e.contains(&("TERM".into(), "xterm-256color".into())));
|
||||||
|
assert!(e.iter().any(|(k, v)| k == "LC_CTYPE" && v.ends_with("UTF-8")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//! - `write`/`resize`/`close` are callable from any thread; `close` is idempotent and must not
|
//! - `write`/`resize`/`close` are callable from any thread; `close` is idempotent and must not
|
||||||
//! fire `on_close` re-entrantly while holding locks.
|
//! fire `on_close` re-entrantly while holding locks.
|
||||||
|
|
||||||
|
pub mod local;
|
||||||
pub mod sftp;
|
pub mod sftp;
|
||||||
pub mod ssh;
|
pub mod ssh;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user