Compare commits
23 Commits
b381e6fa7f
...
82fe053ba8
| Author | SHA1 | Date | |
|---|---|---|---|
| 82fe053ba8 | |||
| 67800bdb28 | |||
| 0b0ff84c30 | |||
| d64e9d735c | |||
| 2aae341416 | |||
| 43d1b29aa8 | |||
| be79af3be3 | |||
| 2991dfc9e5 | |||
| f054f76302 | |||
| 3b068c000a | |||
| 76469eb207 | |||
| 75923404a4 | |||
| 3f62bb3190 | |||
| 00a9fe8643 | |||
| 8797fd4936 | |||
| b6f2558075 | |||
| 09992a9114 | |||
|
|
b107231e46 | ||
|
|
0583eecf81 | ||
| 24fec093d2 | |||
|
|
7b64d979f8 | ||
| f6de003bbd | |||
|
|
5343ce30a2 |
444
Cargo.lock
generated
Normal file
444
Cargo.lock
generated
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "homedir"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"nix",
|
||||||
|
"widestring",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-regex"
|
||||||
|
version = "3.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4994ba703f78b083e2f7946dac9251abd83fd43a0365f030e99b69be5b4b9ef9"
|
||||||
|
dependencies = [
|
||||||
|
"lazy-regex-proc_macros",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-regex-proc_macros"
|
||||||
|
version = "3.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd97232314824e6dbef1918a871bb93f51070455e3715bf26e19a6d01aa977a0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.189"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multi-ssh"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"homedir",
|
||||||
|
"lazy-regex",
|
||||||
|
"shell-words",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.61.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections",
|
||||||
|
"windows-core",
|
||||||
|
"windows-future",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-numerics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-threading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-numerics"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-threading"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "multi-ssh"
|
name = "multi-ssh"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -84,7 +84,3 @@ Once you have that installed, just run
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
and you will find an executable in `target/release`.
|
and you will find an executable in `target/release`.
|
||||||
|
|
||||||
### Unit tests
|
|
||||||
|
|
||||||
In order for the unit tests to pass, you will need `python3`.
|
|
||||||
|
|||||||
62
flake.lock
Normal file
62
flake.lock
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1759580034,
|
||||||
|
"narHash": "sha256-YWo57PL7mGZU7D4WeKFMiW4ex/O6ZolUS6UNBHTZfkI=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3bcc93c5f7a4b30335d31f21e2f1281cba68c318",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744536153,
|
||||||
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1759631821,
|
||||||
|
"narHash": "sha256-V8A1L0FaU/aSXZ1QNJScxC12uP4hANeRBgI4YdhHeRM=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "1d7cbdaad90f8a5255a89a6eddd8af24dc89cafe",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
116
flake.nix
Normal file
116
flake.nix
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
description = "Flake of https://dev.stupstech.de/Mr_Steppy/multi-ssh";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, rust-overlay, ... }:
|
||||||
|
let
|
||||||
|
systems = nixpkgs.lib.systems.flakeExposed;
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let
|
||||||
|
overlays = [ rust-overlay.overlays.default ];
|
||||||
|
pkgs = import nixpkgs { inherit system; overlays = overlays; };
|
||||||
|
rustToolchain = pkgs.rust-bin.stable."1.90.0".minimal;
|
||||||
|
rustPlatform = pkgs.makeRustPlatform { cargo = rustToolchain; rustc = rustToolchain; };
|
||||||
|
in {
|
||||||
|
multiSsh = rustPlatform.buildRustPackage {
|
||||||
|
pname = "multi-ssh";
|
||||||
|
version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
|
||||||
|
src = ./.;
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
nativeCheckInputs = [ pkgs.python3 ];
|
||||||
|
release = true;
|
||||||
|
};
|
||||||
|
default = self.packages.${system}.multiSsh;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
apps = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkg = self.packages.${system}.multiSsh;
|
||||||
|
in {
|
||||||
|
default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkg}/bin/multi-ssh";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
nixosModules.multiSsh = { lib, pkgs, config, ... }: {
|
||||||
|
options = {
|
||||||
|
programs.multiSsh = {
|
||||||
|
enable = lib.mkEnableOption "multi-ssh";
|
||||||
|
editor = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
description = "Editor to forward to multi-ssh (MSSH_EDITOR). If null, the variable is not set.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
description = "Derivation to install for multi-ssh";
|
||||||
|
default = self.packages.${pkgs.stdenv.hostPlatform.system}.multiSsh;
|
||||||
|
};
|
||||||
|
servers = {
|
||||||
|
local = {
|
||||||
|
dir = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''Local server directory, relative to the user's HOME. If set, a local entry ".:<dir>" is added to MSSH_SERVERS.'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
server = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
|
||||||
|
options = {
|
||||||
|
dir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Server directory for ${name} (remote ssh host), as a string.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
default = {};
|
||||||
|
description = "Attribute set of servers where each attribute name is the ssh host and its value provides a 'dir' string.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
msshAlias = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "If true, install an 'mssh' wrapper (alias) that calls 'multi-ssh' and sets env vars.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.programs.multiSsh.enable (let
|
||||||
|
# Build MSSH_SERVERS from options
|
||||||
|
serverEntries = lib.mapAttrsToList (name: value: "${name}:${value.dir}") config.programs.multiSsh.servers.server;
|
||||||
|
localEntry = lib.optional (config.programs.multiSsh.servers.local.dir != null) ".:${config.programs.multiSsh.servers.local.dir}";
|
||||||
|
msshServers = lib.concatStringsSep "," (localEntry ++ serverEntries);
|
||||||
|
|
||||||
|
# wrapper that sets env and execs the real binary (installed as 'multi-ssh')
|
||||||
|
multiSshWrapper = pkgs.writeShellScriptBin "multi-ssh" ''
|
||||||
|
#!${pkgs.runtimeShell}
|
||||||
|
${lib.optionalString (config.programs.multiSsh.editor != null) ''export MSSH_EDITOR="${config.programs.multiSsh.editor}"''}
|
||||||
|
${lib.optionalString (msshServers != "") ''export MSSH_SERVERS="${msshServers}"''}
|
||||||
|
exec ${config.programs.multiSsh.package}/bin/multi-ssh "$@"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# optional 'mssh' alias that delegates to the wrapped 'multi-ssh'
|
||||||
|
msshWrapper = pkgs.writeShellScriptBin "mssh" ''
|
||||||
|
#!${pkgs.runtimeShell}
|
||||||
|
exec ${multiSshWrapper}/bin/multi-ssh "$@"
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
environment.systemPackages = [
|
||||||
|
multiSshWrapper
|
||||||
|
] ++ lib.optional config.programs.multiSsh.msshAlias msshWrapper;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
200
src/command.rs
200
src/command.rs
@ -1,200 +0,0 @@
|
|||||||
use crate::log;
|
|
||||||
use crate::logger::{LogLevel, Logger};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
|
||||||
use std::io;
|
|
||||||
use std::iter::once;
|
|
||||||
use std::process::{Command, ExitStatus, Output};
|
|
||||||
|
|
||||||
pub trait LogRunnable {
|
|
||||||
fn run(&mut self, logger: &Logger) -> Result<(), CommandSpecificError<ExecutionError>>;
|
|
||||||
fn collect_output(&mut self) -> Result<Output, CommandSpecificError<ExecutionError>>;
|
|
||||||
fn collect_full_output(&mut self) -> Result<Output, CommandSpecificError<StartError>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogRunnable for Command {
|
|
||||||
fn run(&mut self, logger: &Logger) -> Result<(), CommandSpecificError<ExecutionError>> {
|
|
||||||
run(self, logger).map_err(|error| CommandSpecificError {
|
|
||||||
command: self,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_output(&mut self) -> Result<Output, CommandSpecificError<ExecutionError>> {
|
|
||||||
collect_output(self, None).map_err(|error| CommandSpecificError {
|
|
||||||
command: self,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_full_output(&mut self) -> Result<Output, CommandSpecificError<StartError>> {
|
|
||||||
collect_full_output(self).map_err(|error| CommandSpecificError {
|
|
||||||
command: self,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(command: &mut Command, logger: &Logger) -> Result<(), ExecutionError> {
|
|
||||||
match logger.level {
|
|
||||||
LogLevel::Debug | LogLevel::Info => {
|
|
||||||
let status = command.status()?;
|
|
||||||
if !status.success() {
|
|
||||||
Err(status)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogLevel::Error => {
|
|
||||||
collect_output(command, Some(logger))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_output(
|
|
||||||
command: &mut Command,
|
|
||||||
logger: Option<&Logger>,
|
|
||||||
) -> Result<Output, ExecutionError> {
|
|
||||||
let output = collect_full_output(command)?;
|
|
||||||
if !output.status.success() {
|
|
||||||
if let Some(logger) = logger {
|
|
||||||
log!(logger, error, "{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
log!(logger, error, "{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
}
|
|
||||||
Err(output.status)?;
|
|
||||||
}
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_full_output(command: &mut Command) -> Result<Output, StartError> {
|
|
||||||
Ok(command.output()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CommandSpecificError<'a, E> {
|
|
||||||
pub command: &'a Command,
|
|
||||||
pub error: E,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> Display for CommandSpecificError<'_, E>
|
|
||||||
where
|
|
||||||
E: Display,
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Failed to execute command '{}': {}",
|
|
||||||
command_to_string(self.command),
|
|
||||||
self.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_to_string(command: &Command) -> String {
|
|
||||||
once(command.get_program().to_string_lossy().to_string())
|
|
||||||
.chain(command.get_args().map(|arg| {
|
|
||||||
let arg_str = arg.to_string_lossy();
|
|
||||||
if arg_str.contains(' ') {
|
|
||||||
format!("\"{arg_str}\"")
|
|
||||||
} else {
|
|
||||||
arg_str.to_string()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> Error for CommandSpecificError<'_, E> where E: Debug + Display {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct StartError(io::Error);
|
|
||||||
|
|
||||||
impl From<io::Error> for StartError {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
StartError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for StartError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Failed to start command: {}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for StartError {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ExecutionError {
|
|
||||||
StartError(StartError),
|
|
||||||
BadExitStatus(ExitStatus),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for ExecutionError {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
Self::StartError(StartError(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<StartError> for ExecutionError {
|
|
||||||
fn from(value: StartError) -> Self {
|
|
||||||
Self::StartError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ExitStatus> for ExecutionError {
|
|
||||||
fn from(value: ExitStatus) -> Self {
|
|
||||||
Self::BadExitStatus(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ExecutionError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ExecutionError::StartError(e) => Display::fmt(e, f),
|
|
||||||
ExecutionError::BadExitStatus(status) => write!(f, "Command failed with {}", status),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for ExecutionError {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use crate::command::{CommandSpecificError, ExecutionError, LogRunnable};
|
|
||||||
use crate::logger::Logger;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown_command() {
|
|
||||||
let mut command = Command::new("python7");
|
|
||||||
let Err(
|
|
||||||
e @ CommandSpecificError {
|
|
||||||
error: ExecutionError::StartError(_),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) = command
|
|
||||||
.args([PathBuf::from("test-ressources/python/exit_1.py")])
|
|
||||||
.run(&Logger::default())
|
|
||||||
else {
|
|
||||||
panic!("command shouldn't exist");
|
|
||||||
};
|
|
||||||
assert_eq!(e.to_string(), "Failed to execute command 'python7': Failed to start command: No such file or directory (os error 2)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error() {
|
|
||||||
let mut command = Command::new("python3");
|
|
||||||
let Err(
|
|
||||||
e @ CommandSpecificError {
|
|
||||||
error: ExecutionError::BadExitStatus(_),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) = command
|
|
||||||
.arg("test-ressources/python/exit_1.py")
|
|
||||||
.run(&Logger::default())
|
|
||||||
else {
|
|
||||||
panic!("command should return exit-code 1")
|
|
||||||
};
|
|
||||||
assert_eq!(e.to_string(), "Failed to execute command 'python3 test-ressources/python/exit_1.py': Command failed with exit status: 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
96
src/environment.rs
Normal file
96
src/environment.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use crate::shell_interface::{
|
||||||
|
build_command_from_shell_command, CommandOutput, CommandResult, ExitStatus, ShellCommand,
|
||||||
|
ShellInterface, StartError,
|
||||||
|
};
|
||||||
|
use std::env::VarError;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, io};
|
||||||
|
|
||||||
|
pub trait Environment {
|
||||||
|
fn args_os(&self) -> Vec<OsString>;
|
||||||
|
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>;
|
||||||
|
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.var_os(key)
|
||||||
|
.ok_or(VarError::NotPresent)
|
||||||
|
.and_then(|s| s.into_string().map_err(VarError::NotUnicode))
|
||||||
|
}
|
||||||
|
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
V: AsRef<OsStr>;
|
||||||
|
fn get_home_directory(&self) -> Option<PathBuf>;
|
||||||
|
fn read_line(&mut self) -> Result<String, io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Prod;
|
||||||
|
|
||||||
|
impl Environment for Prod {
|
||||||
|
fn args_os(&self) -> Vec<OsString> {
|
||||||
|
env::args_os().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>
|
||||||
|
{
|
||||||
|
env::var_os(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
env::var(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
V: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
env::set_var(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||||
|
homedir::my_home().ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line(&mut self) -> Result<String, io::Error> {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
io::stdin().read_line(&mut buffer)?;
|
||||||
|
Ok(buffer.trim().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellInterface for Prod {
|
||||||
|
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError> {
|
||||||
|
CommandResult {
|
||||||
|
result: build_command_from_shell_command(&command)
|
||||||
|
.status()
|
||||||
|
.map(ExitStatus::from)
|
||||||
|
.map_err(StartError::from),
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_command_output(
|
||||||
|
&mut self,
|
||||||
|
command: ShellCommand,
|
||||||
|
) -> CommandResult<CommandOutput, StartError> {
|
||||||
|
CommandResult {
|
||||||
|
result: build_command_from_shell_command(&command)
|
||||||
|
.output()
|
||||||
|
.map(CommandOutput::from)
|
||||||
|
.map_err(StartError::from),
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/integration_test.rs
Normal file
90
src/integration_test.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use crate::environment::Environment;
|
||||||
|
use crate::shell_interface::{
|
||||||
|
CommandOutput, CommandResult, ExitStatus, ShellCommand, ShellInterface, StartError,
|
||||||
|
};
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::io::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TestEnvironment {
|
||||||
|
/// passed command line arguments
|
||||||
|
args_os: Vec<OsString>,
|
||||||
|
/// set environment variables - we assume a pure environment by default
|
||||||
|
env_vars: HashMap<OsString, OsString>,
|
||||||
|
/// home directory, relative to the target/test folder
|
||||||
|
home_dir: PathBuf,
|
||||||
|
/// pending lines of std input
|
||||||
|
stdin: VecDeque<String>,
|
||||||
|
/// whether an ssh agent has been started successfully
|
||||||
|
ssh_agent_started: bool,
|
||||||
|
// TODO ssh servers and local server
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SshServer {
|
||||||
|
pub name: String,
|
||||||
|
pub home_dir: FsEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FsEntry {
|
||||||
|
pub name: OsString,
|
||||||
|
pub kind: FsEntryKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FsEntryKind {
|
||||||
|
Directory(Dir),
|
||||||
|
File {
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Dir {
|
||||||
|
pub contents: Vec<FsEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Environment for TestEnvironment {
|
||||||
|
fn args_os(&self) -> Vec<OsString> {
|
||||||
|
self.args_os.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>
|
||||||
|
{
|
||||||
|
self.env_vars.get(key.as_ref()).map(|s| s.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
V: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
self.env_vars.insert(key.as_ref().into(), value.as_ref().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||||
|
PathBuf::from("target/integration_test").join(&self.home_dir).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line(&mut self) -> Result<String, Error> {
|
||||||
|
self.stdin.pop_front().ok_or_else(|| Error::other("Unexpected call to read_line: No input prepared"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellInterface for TestEnvironment {
|
||||||
|
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_command_output(
|
||||||
|
&mut self,
|
||||||
|
command: ShellCommand,
|
||||||
|
) -> CommandResult<CommandOutput, StartError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
1239
src/main.rs
1239
src/main.rs
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
|||||||
use crate::get_home_directory;
|
|
||||||
use std::cell::LazyCell;
|
use std::cell::LazyCell;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
@ -6,7 +5,6 @@ use std::fs;
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ServerReference {
|
pub enum ServerReference {
|
||||||
@ -15,6 +13,19 @@ pub enum ServerReference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ServerReference {
|
impl ServerReference {
|
||||||
|
pub fn from_str<F>(s: &str, get_home_directory: F) -> Result<Self, ServerReferenceParseError>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<PathBuf, String>,
|
||||||
|
{
|
||||||
|
Server::from_str(
|
||||||
|
s,
|
||||||
|
RelativeLocalPathAnker::CurrentDirectory,
|
||||||
|
get_home_directory,
|
||||||
|
)
|
||||||
|
.map(Self::Resolved)
|
||||||
|
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_identifier(&self) -> &str {
|
pub fn get_identifier(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ServerReference::Resolved(server) => server.address.identifier(),
|
ServerReference::Resolved(server) => server.address.identifier(),
|
||||||
@ -68,16 +79,6 @@ impl ServerReference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ServerReference {
|
|
||||||
type Err = ServerReferenceParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Server::from_str(s, RelativeLocalPathAnker::CurrentDirectory)
|
|
||||||
.map(Self::Resolved)
|
|
||||||
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for ServerReference {
|
impl PartialEq for ServerReference {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.get_identifier() == other.get_identifier()
|
self.get_identifier() == other.get_identifier()
|
||||||
@ -126,10 +127,14 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(
|
pub fn from_str<F>(
|
||||||
s: &str,
|
s: &str,
|
||||||
relative_local_path_anker: RelativeLocalPathAnker,
|
relative_local_path_anker: RelativeLocalPathAnker,
|
||||||
) -> Result<Self, ServerParseError> {
|
get_home_directory: F,
|
||||||
|
) -> Result<Self, ServerParseError>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<PathBuf, String>,
|
||||||
|
{
|
||||||
s.split_once(':')
|
s.split_once(':')
|
||||||
.ok_or(ServerParseError::MissingServerDirectory)
|
.ok_or(ServerParseError::MissingServerDirectory)
|
||||||
.and_then(|(identifier, server_directory)| {
|
.and_then(|(identifier, server_directory)| {
|
||||||
@ -245,13 +250,12 @@ impl Error for ServerParseError {}
|
|||||||
mod test_server_reference {
|
mod test_server_reference {
|
||||||
use crate::server::{Server, ServerAddress, ServerReference};
|
use crate::server::{Server, ServerAddress, ServerReference};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_str() {
|
fn test_from_str() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ServerReference::Identifier("foo".to_string()),
|
ServerReference::Identifier("foo".to_string()),
|
||||||
ServerReference::from_str("foo").unwrap()
|
ServerReference::from_str("foo", || panic!("shouldn't be called")).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ServerReference::Resolved(Server {
|
ServerReference::Resolved(Server {
|
||||||
@ -260,7 +264,7 @@ mod test_server_reference {
|
|||||||
},
|
},
|
||||||
server_directory_path: PathBuf::from("server/creative2")
|
server_directory_path: PathBuf::from("server/creative2")
|
||||||
}),
|
}),
|
||||||
ServerReference::from_str("crea:server/creative2").unwrap()
|
ServerReference::from_str("crea:server/creative2", || panic!("shouldn't be called")).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
562
src/shell_interface.rs
Normal file
562
src/shell_interface.rs
Normal file
@ -0,0 +1,562 @@
|
|||||||
|
use crate::logger::{LogLevel, Logger};
|
||||||
|
use crate::server::{Server, ServerAddress};
|
||||||
|
use crate::{log, osf};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::iter::once;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Output};
|
||||||
|
use std::{io, process};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnvCommand<'a, E> {
|
||||||
|
command: ShellCommand,
|
||||||
|
environment: &'a mut E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> EnvCommand<'_, E>
|
||||||
|
where
|
||||||
|
E: ShellInterface,
|
||||||
|
{
|
||||||
|
pub fn run(self) -> CommandResult<ExitStatus, StartError> {
|
||||||
|
self.environment.run_command(self.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output(self) -> CommandResult<CommandOutput, StartError> {
|
||||||
|
self.environment.collect_command_output(self.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_logged(self, logger: &Logger) -> CommandResult<LoggedRunOutput, StartError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
match logger.level {
|
||||||
|
LogLevel::Debug | LogLevel::Info => {
|
||||||
|
let res = self.run();
|
||||||
|
CommandResult {
|
||||||
|
result: res.result.map(LoggedRunOutput::from),
|
||||||
|
command: res.command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogLevel::Error => {
|
||||||
|
let res = self.output();
|
||||||
|
CommandResult {
|
||||||
|
result: res.result.map(LoggedRunOutput::from),
|
||||||
|
command: res.command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ShellCommand {
|
||||||
|
Ssh {
|
||||||
|
address: String,
|
||||||
|
server_command: ServerCommand,
|
||||||
|
},
|
||||||
|
Scp {
|
||||||
|
source: ScpParam,
|
||||||
|
destination: ScpParam,
|
||||||
|
},
|
||||||
|
SshAgent,
|
||||||
|
ShhAdd,
|
||||||
|
Editor(Vec<OsString>),
|
||||||
|
Execute {
|
||||||
|
working_directory: PathBuf,
|
||||||
|
command: Vec<OsString>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellCommand {
|
||||||
|
pub fn in_env<E>(self, environment: &mut E) -> EnvCommand<E> {
|
||||||
|
EnvCommand {
|
||||||
|
command: self,
|
||||||
|
environment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServerCommand {
|
||||||
|
Realpath {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Ls {
|
||||||
|
dir: PathBuf,
|
||||||
|
},
|
||||||
|
Rm {
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
Mv {
|
||||||
|
source: PathBuf,
|
||||||
|
destination: PathBuf,
|
||||||
|
},
|
||||||
|
Execute {
|
||||||
|
working_directory: PathBuf,
|
||||||
|
command: OsString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScpParam {
|
||||||
|
pub server: Option<String>,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> From<(&Server, P)> for ScpParam
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
fn from((server, path): (&Server, P)) -> Self {
|
||||||
|
Self {
|
||||||
|
server: match &server.address {
|
||||||
|
ServerAddress::Ssh { ssh_address } => Some(ssh_address.into()),
|
||||||
|
ServerAddress::Localhost => None,
|
||||||
|
},
|
||||||
|
path: PathBuf::from(path.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Path> for ScpParam {
|
||||||
|
fn from(value: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
server: None,
|
||||||
|
path: PathBuf::from(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ScpParam> for OsString {
|
||||||
|
fn from(value: &ScpParam) -> Self {
|
||||||
|
let mut builder = osf!();
|
||||||
|
if let Some(server) = &value.server {
|
||||||
|
builder += format!("{server}:");
|
||||||
|
}
|
||||||
|
builder += &value.path;
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ShellCommand {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
command_to_string(&build_command_from_shell_command(self))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_to_string(command: &Command) -> String {
|
||||||
|
once(command.get_program().to_string_lossy().to_string())
|
||||||
|
.chain(command.get_args().map(|arg| {
|
||||||
|
let arg_str = arg.to_string_lossy();
|
||||||
|
if arg_str.contains(' ') {
|
||||||
|
format!("\"{arg_str}\"")
|
||||||
|
} else {
|
||||||
|
arg_str.to_string()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! cmd {
|
||||||
|
($programm: expr $(, $arg:expr )*)=> {{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut cmd = Command::new($programm);
|
||||||
|
$( cmd.arg($arg); )*
|
||||||
|
cmd
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_command_from_shell_command(shell_command: &ShellCommand) -> Command {
|
||||||
|
match shell_command {
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address,
|
||||||
|
server_command,
|
||||||
|
} => cmd!(
|
||||||
|
"ssh",
|
||||||
|
address,
|
||||||
|
match server_command {
|
||||||
|
ServerCommand::Realpath { path } => osf!("realpath -e ") + path,
|
||||||
|
ServerCommand::Ls { dir } => osf!("ls ") + dir,
|
||||||
|
ServerCommand::Rm { file } => osf!("rm ") + file,
|
||||||
|
ServerCommand::Mv {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
} => osf!("mv ") + source + " " + destination,
|
||||||
|
ServerCommand::Execute {
|
||||||
|
working_directory,
|
||||||
|
command,
|
||||||
|
} => osf!("cd ") + working_directory + "; " + command,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ShellCommand::Scp {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
} => cmd!("scp", OsString::from(source), OsString::from(destination)),
|
||||||
|
ShellCommand::SshAgent => cmd!("ssh-agent", "-s"),
|
||||||
|
ShellCommand::ShhAdd => cmd!("ssh-add"),
|
||||||
|
ShellCommand::Editor(args) => {
|
||||||
|
let mut args = args.clone();
|
||||||
|
let mut cmd = cmd!(args.remove(0));
|
||||||
|
cmd.args(args);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
ShellCommand::Execute {
|
||||||
|
working_directory,
|
||||||
|
command,
|
||||||
|
} => {
|
||||||
|
let mut args = command.clone();
|
||||||
|
let mut cmd = cmd!(args.remove(0));
|
||||||
|
cmd.args(args).current_dir(working_directory);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ShellInterface {
|
||||||
|
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError>;
|
||||||
|
fn collect_command_output(
|
||||||
|
&mut self,
|
||||||
|
command: ShellCommand,
|
||||||
|
) -> CommandResult<CommandOutput, StartError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MaybeCast<T> {
|
||||||
|
fn maybe_cast(&self) -> Option<&T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MaybeCast<T> for T {
|
||||||
|
fn maybe_cast(&self) -> Option<&T> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoggedRunOutput {
|
||||||
|
ExitStatus(ExitStatus),
|
||||||
|
CommandOutput(CommandOutput),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExitStatus> for LoggedRunOutput {
|
||||||
|
fn from(value: ExitStatus) -> Self {
|
||||||
|
Self::ExitStatus(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CommandOutput> for LoggedRunOutput {
|
||||||
|
fn from(value: CommandOutput) -> Self {
|
||||||
|
Self::CommandOutput(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ExitStatus> for LoggedRunOutput {
|
||||||
|
fn as_ref(&self) -> &ExitStatus {
|
||||||
|
match self {
|
||||||
|
LoggedRunOutput::ExitStatus(status) => status,
|
||||||
|
LoggedRunOutput::CommandOutput(output) => output.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaybeCast<CommandOutput> for LoggedRunOutput {
|
||||||
|
fn maybe_cast(&self) -> Option<&CommandOutput> {
|
||||||
|
match self {
|
||||||
|
LoggedRunOutput::ExitStatus(_) => None,
|
||||||
|
LoggedRunOutput::CommandOutput(output) => Some(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandResult<T, E> {
|
||||||
|
pub command: ShellCommand,
|
||||||
|
pub result: Result<T, E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> CommandResult<T, E> {
|
||||||
|
pub fn into_result(self) -> Result<T, CommandError<E>> {
|
||||||
|
self.result.map_err(|error| CommandError {
|
||||||
|
command: self.command,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CommandResult<T, StartError> {
|
||||||
|
pub fn and_expect_success(self) -> CommandResult<T, ExecutionError<T>>
|
||||||
|
where
|
||||||
|
T: AsRef<ExitStatus>,
|
||||||
|
{
|
||||||
|
CommandResult {
|
||||||
|
result: self.result.map_err(ExecutionError::from).and_then(|t| {
|
||||||
|
if t.as_ref().success {
|
||||||
|
Ok(t)
|
||||||
|
} else {
|
||||||
|
Err(ExecutionError::BadExitStatus(t))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
command: self.command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CommandResult<T, ExecutionError<T>> {
|
||||||
|
pub fn into_result_with_error_logging(
|
||||||
|
self,
|
||||||
|
logger: &Logger,
|
||||||
|
) -> Result<T, CommandError<ExecutionError<T>>>
|
||||||
|
where
|
||||||
|
T: MaybeCast<CommandOutput>,
|
||||||
|
{
|
||||||
|
self.result.map_err(|error| {
|
||||||
|
if let ExecutionError::BadExitStatus(t) = &error {
|
||||||
|
if let Some(output) = t.maybe_cast() {
|
||||||
|
log!(logger, error, "{}", output.stdout.to_string_lossy());
|
||||||
|
log!(logger, error, "{}", output.stderr.to_string_lossy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CommandError {
|
||||||
|
command: self.command,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CommandOutput {
|
||||||
|
pub stdout: OsString,
|
||||||
|
pub stderr: OsString,
|
||||||
|
pub status: ExitStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Output> for CommandOutput {
|
||||||
|
fn from(value: Output) -> Self {
|
||||||
|
Self {
|
||||||
|
stdout: os_string_from_ssh_output(value.stdout),
|
||||||
|
stderr: os_string_from_ssh_output(value.stderr),
|
||||||
|
status: value.status.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn os_string_from_ssh_output(output: Vec<u8>) -> OsString {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStringExt;
|
||||||
|
OsString::from_vec(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ExitStatus> for CommandOutput {
|
||||||
|
fn as_ref(&self) -> &ExitStatus {
|
||||||
|
&self.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExitStatus {
|
||||||
|
pub success: bool,
|
||||||
|
pub string_form: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub code: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<process::ExitStatus> for ExitStatus {
|
||||||
|
fn from(value: process::ExitStatus) -> Self {
|
||||||
|
Self {
|
||||||
|
success: value.success(),
|
||||||
|
string_form: value.to_string(),
|
||||||
|
code: value.code(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ExitStatus> for ExitStatus {
|
||||||
|
fn as_ref(&self) -> &ExitStatus {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ExitStatus {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.string_form, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandError<E> {
|
||||||
|
pub command: ShellCommand,
|
||||||
|
pub error: E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<CommandError<E>> for String
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn from(value: CommandError<E>) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> Display for CommandError<E>
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Error while running command '{}': {}",
|
||||||
|
self.command, self.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> Error for CommandError<E> where E: Error {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StartError(io::Error);
|
||||||
|
|
||||||
|
impl From<io::Error> for StartError {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
StartError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StartError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Failed to run command: {}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for StartError {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ExecutionError<T> {
|
||||||
|
StartError(StartError),
|
||||||
|
BadExitStatus(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<StartError> for ExecutionError<T> {
|
||||||
|
fn from(value: StartError) -> Self {
|
||||||
|
Self::StartError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Display for ExecutionError<T>
|
||||||
|
where
|
||||||
|
T: AsRef<ExitStatus>,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ExecutionError::StartError(e) => Display::fmt(e, f),
|
||||||
|
ExecutionError::BadExitStatus(status) => {
|
||||||
|
write!(f, "execution failed with {}", status.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Error for ExecutionError<T> where T: AsRef<ExitStatus> + Debug {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_commands {
|
||||||
|
use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_string() {
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Realpath {
|
||||||
|
path: PathBuf::from("plugins/*.jar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "realpath -e plugins/*.jar""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Ls {
|
||||||
|
dir: PathBuf::from("creative/plugins")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "ls creative/plugins""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Rm {
|
||||||
|
file: PathBuf::from("foo.txt")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "rm foo.txt""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Mv {
|
||||||
|
source: PathBuf::from("foo"),
|
||||||
|
destination: PathBuf::from("bar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "mv foo bar""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Execute {
|
||||||
|
working_directory: PathBuf::from(".."),
|
||||||
|
command: "sudo rm -rf *".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "cd ..; sudo rm -rf *""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Scp {
|
||||||
|
source: ScpParam {
|
||||||
|
server: None,
|
||||||
|
path: PathBuf::from("target/mssh")
|
||||||
|
},
|
||||||
|
destination: ScpParam {
|
||||||
|
server: Some("crea".into()),
|
||||||
|
path: PathBuf::from("/usr/bin")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"scp target/mssh crea:/usr/bin"#
|
||||||
|
);
|
||||||
|
assert_eq!(ShellCommand::SshAgent.to_string(), r#"ssh-agent -s"#);
|
||||||
|
assert_eq!(ShellCommand::ShhAdd.to_string(), r#"ssh-add"#);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Editor(vec!["kate".into(), "-b".into(), "test.txt".into()]).to_string(),
|
||||||
|
r#"kate -b test.txt"#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Execute {
|
||||||
|
working_directory: PathBuf::from("/home/me/server"),
|
||||||
|
command: vec!["java".into(), "-jar".into(), "paper.jar".into()]
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"java -jar paper.jar"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
exit(1)
|
|
||||||
Loading…
Reference in New Issue
Block a user