in case i get hit by a bus
This commit is contained in:
parent
80c321cbd1
commit
bc8270b30f
|
@ -0,0 +1,713 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[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 = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[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.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"sharded-slab",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vmctl"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cfg-if",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"eyre",
|
||||
"hex",
|
||||
"indicatif",
|
||||
"nix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "vmctl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[features]
|
||||
default = ["unicode"]
|
||||
unicode = []
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
cfg-if = "1.0.0"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
color-eyre = "0.6.3"
|
||||
eyre = "0.6.12"
|
||||
hex = "0.4.3"
|
||||
indicatif = "0.17.11"
|
||||
nix = { version = "0.30.1", features = ["signal"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
thiserror = "2.0.12"
|
|
@ -0,0 +1,113 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
/// VM controller for AirgapOS
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
pub struct App {
|
||||
// global options go here
|
||||
#[arg(long, global = true, default_value = "/var/run/netvm.pid")]
|
||||
pub lockfile: PathBuf,
|
||||
|
||||
//
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum Commands {
|
||||
/// Start a headless VM in the background.
|
||||
Start,
|
||||
|
||||
/// Stop a headless VM.
|
||||
Stop,
|
||||
|
||||
/// Open a VM in the foreground with a serial terminal.
|
||||
Shell,
|
||||
|
||||
/// Get the hostname and uptime of a running VM.
|
||||
Status,
|
||||
|
||||
/// Attach a USB device to a running VM.
|
||||
Attach {
|
||||
/// The device to attach.
|
||||
device: DeviceIdentifier,
|
||||
},
|
||||
|
||||
/// Push a file to a currently running VM.
|
||||
Push {
|
||||
/// The local path to push.
|
||||
local_path: PathBuf,
|
||||
|
||||
/// The remote path to push to.
|
||||
remote_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Pull a file from a currently running VM.
|
||||
Pull {
|
||||
/// The remote path to pull.
|
||||
remote_path: PathBuf,
|
||||
|
||||
/// The local path to pull to.
|
||||
local_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Run a command in a currently running VM.
|
||||
Run {
|
||||
/// The command to run.
|
||||
command: String,
|
||||
|
||||
/// Arguments to pass to the running command.
|
||||
args: Vec<String>,
|
||||
},
|
||||
|
||||
/// Test synchronization by repeatedly running commands.
|
||||
Test {}
|
||||
}
|
||||
|
||||
/// An attachable USB device identifier.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeviceIdentifier {
|
||||
/// The Vendor ID.
|
||||
pub vendorid: String,
|
||||
|
||||
/// The Device ID.
|
||||
pub deviceid: String,
|
||||
}
|
||||
|
||||
/// An error encountered while parsing a USB device identifier
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum DeviceIdentifierFromStrError {
|
||||
#[error("could not split input by colon; expected output similar to `lsusb`")]
|
||||
CouldNotSplitByColon,
|
||||
|
||||
#[error("found non-hex {0} at position {1}")]
|
||||
BadChar(char, usize),
|
||||
}
|
||||
|
||||
impl FromStr for DeviceIdentifier {
|
||||
type Err = DeviceIdentifierFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let Some((first, last)) = s.split_once(':') else {
|
||||
return Err(DeviceIdentifierFromStrError::CouldNotSplitByColon);
|
||||
};
|
||||
if let Some((position, ch)) = first
|
||||
.chars()
|
||||
.enumerate()
|
||||
.find(|(_, ch)| !ch.is_ascii_hexdigit())
|
||||
{
|
||||
return Err(DeviceIdentifierFromStrError::BadChar(ch, position));
|
||||
}
|
||||
if let Some((position, ch)) = last
|
||||
.chars()
|
||||
.enumerate()
|
||||
.find(|(_, ch)| !ch.is_ascii_hexdigit())
|
||||
{
|
||||
return Err(DeviceIdentifierFromStrError::BadChar(ch, position));
|
||||
}
|
||||
Ok(Self {
|
||||
vendorid: first.to_owned(),
|
||||
deviceid: last.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
use clap::Parser;
|
||||
use eyre::WrapErr;
|
||||
use std::io::Write;
|
||||
|
||||
mod cli;
|
||||
mod vm;
|
||||
|
||||
use vm::{SpawnArguments, VirtualMachine};
|
||||
|
||||
fn main() -> eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let mut args = std::env::args().collect::<Vec<_>>();
|
||||
let ignore_opts = String::from("--");
|
||||
if let Some(run_pos) = args.iter().position(|e| e == "run") {
|
||||
if !args.contains(&ignore_opts) && args.get(run_pos + 1).is_some_and(|arg| arg != "--help")
|
||||
{
|
||||
args.insert(run_pos + 1, ignore_opts);
|
||||
}
|
||||
}
|
||||
let opts = cli::App::parse_from(args);
|
||||
|
||||
match opts.subcommand {
|
||||
cli::Commands::Start => {
|
||||
let spawn_args = SpawnArguments::default();
|
||||
let mut vm = VirtualMachine::start(spawn_args)?;
|
||||
let pid = vm.pid();
|
||||
std::fs::write(&opts.lockfile, pid.to_string()).with_context(|| {
|
||||
format!(
|
||||
"could not write PID {pid} to {lockfile}",
|
||||
lockfile = opts.lockfile.display(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// temp
|
||||
vm.run_command("uptime", [])?;
|
||||
}
|
||||
cli::Commands::Stop => {
|
||||
let spawn_arguments = SpawnArguments::default();
|
||||
let vm = VirtualMachine::load(spawn_arguments, None)?;
|
||||
vm.kill()?;
|
||||
}
|
||||
cli::Commands::Shell => {
|
||||
// TODO: qemu inline, is it possible to pass through stdin/stdout w/o buffering?
|
||||
todo!()
|
||||
}
|
||||
cli::Commands::Status => {
|
||||
let spawn_arguments = SpawnArguments::default();
|
||||
let mut vm = VirtualMachine::load(spawn_arguments, None)?;
|
||||
let result = vm.execute("guest-get-host-name", serde_json::json!({}))?;
|
||||
let hostname = result
|
||||
.get("host-name")
|
||||
.ok_or(eyre::eyre!("no hostname"))?
|
||||
.as_str()
|
||||
.ok_or(eyre::eyre!("hostname is not str"))?;
|
||||
let uptime = vm.run_command("uptime", [])?;
|
||||
eprintln!("hostname: {hostname}");
|
||||
eprint!("{}", String::from_utf8_lossy(&uptime.0));
|
||||
}
|
||||
cli::Commands::Attach { device } => todo!(),
|
||||
cli::Commands::Push {
|
||||
local_path,
|
||||
remote_path,
|
||||
} => {
|
||||
let spawn_arguments = SpawnArguments::default();
|
||||
let mut vm = VirtualMachine::load(spawn_arguments, None)?;
|
||||
vm.push(local_path, remote_path)?;
|
||||
}
|
||||
cli::Commands::Pull {
|
||||
remote_path,
|
||||
local_path,
|
||||
} => {
|
||||
let spawn_arguments = SpawnArguments::default();
|
||||
let mut vm = VirtualMachine::load(spawn_arguments, None)?;
|
||||
vm.pull(remote_path, local_path)?;
|
||||
}
|
||||
cli::Commands::Run { command, args } => {
|
||||
let spawn_arguments = SpawnArguments::default();
|
||||
let mut vm = VirtualMachine::load(spawn_arguments, None)?;
|
||||
let (response, exit_code) = vm.run_command(&command, args)?;
|
||||
std::io::stdout().write_all(&response)?;
|
||||
std::process::exit(exit_code as i32);
|
||||
}
|
||||
cli::Commands::Test {} => {
|
||||
let spawn_arguments = SpawnArguments::default();
|
||||
let mut vm = VirtualMachine::load(spawn_arguments, None)?;
|
||||
for i in 0..10 {
|
||||
let sleep_command = format!("sleep 10; echo {i}");
|
||||
let (response, exit_code) =
|
||||
vm.run_command("sh", [String::from("-c"), sleep_command])?;
|
||||
eprint!(
|
||||
"exit code {}, output {}",
|
||||
exit_code,
|
||||
String::from_utf8_lossy(&response),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,674 @@
|
|||
use base64::prelude::*;
|
||||
use eyre::{Result, WrapErr};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt::{Debug, Display},
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
os::unix::net::UnixStream,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
const CHUNK_SIZE: usize = 1024 * 16;
|
||||
|
||||
fn spinner(msg: impl Display) -> ProgressBar {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "unicode")] {
|
||||
let style = ProgressStyle::default_spinner();
|
||||
let delay = Duration::from_millis(100);
|
||||
} else {
|
||||
let style = ProgressStyle::default_spinner().tick_chars(r#"\|/-!"#);
|
||||
let delay = Duration::from_millis(200);
|
||||
}
|
||||
};
|
||||
let bar = ProgressBar::new_spinner().with_style(style);
|
||||
bar.enable_steady_tick(delay);
|
||||
bar.set_message(msg.to_string());
|
||||
bar
|
||||
}
|
||||
|
||||
fn bar(count: u64, msg: impl Display) -> ProgressBar {
|
||||
let template = "[{elapsed_precise}] {wide_bar} {percent}% {msg}";
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "unicode")] {
|
||||
let style = ProgressStyle::with_template(template).unwrap();
|
||||
} else {
|
||||
let style = ProgressStyle::with_template(template).unwrap().progress_chars("=> ");
|
||||
}
|
||||
}
|
||||
let bar = ProgressBar::new(count).with_style(style);
|
||||
bar.set_message(msg.to_string());
|
||||
bar
|
||||
}
|
||||
|
||||
fn get_pid(path: impl AsRef<Path>) -> Result<u32> {
|
||||
let path = path.as_ref();
|
||||
let pid_str = std::fs::read_to_string(path).context("could not read PID")?;
|
||||
pid_str.parse().context("could not parse PID")
|
||||
}
|
||||
|
||||
fn remove_if_exists(path: impl AsRef<Path>) -> Result<()> {
|
||||
if let Err(e) = std::fs::remove_file(path) {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(e).context("could not remove stale file");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_lowercase_hexlike(s: impl AsRef<str>) -> String {
|
||||
let mut s = s.as_ref().trim();
|
||||
if s.starts_with("0x") {
|
||||
s = &s[2..];
|
||||
}
|
||||
s.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Device {
|
||||
vendor_id: u16,
|
||||
device_id: u16,
|
||||
bus_id: String,
|
||||
}
|
||||
|
||||
fn find_pci_device_by_class(class: u16) -> Result<Vec<Device>> {
|
||||
let devices = std::fs::read_dir("/sys/bus/pci/devices")?;
|
||||
let mut valid_devices = vec![];
|
||||
for entry in devices {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let bus_address = entry.file_name();
|
||||
let class_string = std::fs::read_to_string(path.join("class"))?;
|
||||
if to_lowercase_hexlike(class_string)[..4] == hex::encode(class.to_be_bytes()) {
|
||||
let device_id = u16::from_be_bytes(
|
||||
hex::decode(to_lowercase_hexlike(std::fs::read_to_string(
|
||||
path.join("device"),
|
||||
)?))?
|
||||
.try_into()
|
||||
.map_err(|e| eyre::eyre!("could not convert to u16: {e:?}"))?,
|
||||
);
|
||||
let vendor_id = u16::from_be_bytes(
|
||||
hex::decode(to_lowercase_hexlike(std::fs::read_to_string(
|
||||
path.join("vendor"),
|
||||
)?))?
|
||||
.try_into()
|
||||
.map_err(|e| eyre::eyre!("could not convert to u16: {e:?}"))?,
|
||||
);
|
||||
|
||||
let bus_id = bus_address
|
||||
.into_string()
|
||||
.map_err(|bad| eyre::eyre!("non-utf8 bus address: {bad:?}"))?
|
||||
.split_once(":")
|
||||
.ok_or(eyre::eyre!("bad path ID"))?
|
||||
.1
|
||||
.to_string();
|
||||
valid_devices.push(Device {
|
||||
vendor_id,
|
||||
device_id,
|
||||
bus_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(valid_devices)
|
||||
}
|
||||
|
||||
// NOTE: Do not implement `clone`, as there is side-effect state involved.
|
||||
#[derive(Debug)]
|
||||
pub struct VirtualMachine {
|
||||
pid: u32,
|
||||
writer: UnixStream,
|
||||
reader: BufReader<UnixStream>,
|
||||
args: SpawnArguments,
|
||||
}
|
||||
|
||||
/// The configuration to use when starting a VM.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpawnArguments {
|
||||
/// The PCI device to use for connecting to a network.
|
||||
pub network_pci_device: Option<String>,
|
||||
|
||||
/// The image file to use when booting the machine.
|
||||
///
|
||||
/// By default, this is "/guest.img".
|
||||
pub guest_image: PathBuf,
|
||||
|
||||
/// The path for the guest agent socket.
|
||||
pub guest_agent_socket_path: PathBuf,
|
||||
|
||||
/// The path for the QMP socket.
|
||||
pub qmp_socket_path: PathBuf,
|
||||
|
||||
/// The path for the lockfile.
|
||||
pub lockfile_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for SpawnArguments {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
network_pci_device: None,
|
||||
guest_image: PathBuf::from("/guest.img"),
|
||||
guest_agent_socket_path: PathBuf::from("/var/run/netvm_qga.sock"),
|
||||
qmp_socket_path: PathBuf::from("/var/run/netvm_qmp.sock"),
|
||||
lockfile_path: PathBuf::from("/var/run/netvm.lock"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
pub fn start(args: SpawnArguments) -> eyre::Result<Self> {
|
||||
let eth_devices = find_pci_device_by_class(0x0200)?;
|
||||
|
||||
// Ensure VM isn't already started
|
||||
if std::fs::exists(&args.lockfile_path)? {
|
||||
// Check if VM is running
|
||||
use nix::unistd::{getpgid, Pid};
|
||||
let pid = get_pid(&args.lockfile_path)?;
|
||||
if getpgid(Some(Pid::from_raw(pid as i32))).is_ok() {
|
||||
// process exists, exit
|
||||
return Err(eyre::eyre!(
|
||||
"VM with this configuration exists as PID {pid}"
|
||||
));
|
||||
} else {
|
||||
// Remove old state
|
||||
eprintln!("Found stale lockfile (PID terminated), removing all state");
|
||||
remove_if_exists(&args.guest_agent_socket_path)?;
|
||||
remove_if_exists(&args.qmp_socket_path)?;
|
||||
remove_if_exists(&args.lockfile_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut chardev_opts = OsString::from("socket,path=");
|
||||
chardev_opts.push(args.guest_agent_socket_path.as_os_str());
|
||||
chardev_opts.push(",server=on,wait=off,id=qga0");
|
||||
|
||||
let mut qmp_opts = OsString::from("unix:");
|
||||
qmp_opts.push(args.qmp_socket_path.as_os_str());
|
||||
qmp_opts.push(",server,nowait");
|
||||
|
||||
// TODO: https://git.distrust.co/public/airgap/src/commit/73ab8eae21898d80625062d6037e84dff61fabf8/src/host/rootfs/usr/local/bin/netvm#L90-L100
|
||||
// Add these options if necessary.
|
||||
|
||||
let bar = spinner("Loading VM");
|
||||
|
||||
if !eth_devices.is_empty() {
|
||||
std::fs::write(
|
||||
"/sys/module/vfio_iommu_type1/parameters/allow_unsafe_interrupts",
|
||||
"Y",
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut net_args = vec![];
|
||||
for dev in eth_devices {
|
||||
bar.println(format!("Attaching Ethernet device: {dev:?}"));
|
||||
let bus_id = &dev.bus_id;
|
||||
std::fs::write(
|
||||
"/sys/bus/pci/drivers/vfio-pci/new_id",
|
||||
format!(
|
||||
"{} {}",
|
||||
hex::encode(dev.vendor_id.to_be_bytes()),
|
||||
hex::encode(dev.device_id.to_be_bytes())
|
||||
),
|
||||
)?;
|
||||
net_args.push("-device".to_string());
|
||||
net_args.push(format!("vfio-pci,host={bus_id}"))
|
||||
}
|
||||
|
||||
let mut child = Command::new("qemu-system-x86_64")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.args(["-m", "4G"])
|
||||
.args(["-machine", "q35"])
|
||||
.arg("-nographic")
|
||||
.args(["-serial", "none"])
|
||||
.args(["-monitor", "none"])
|
||||
.args(["-net", "none"])
|
||||
.arg("-cdrom")
|
||||
.arg(&args.guest_image)
|
||||
.args(["-boot", "order=d"])
|
||||
.arg("-chardev")
|
||||
.arg(chardev_opts)
|
||||
.arg("-qmp")
|
||||
.arg(qmp_opts)
|
||||
.args(net_args)
|
||||
.args(["-device", "qemu-xhci,id=usb"])
|
||||
.args(["-device", "virtio-serial"])
|
||||
.args([
|
||||
"-device",
|
||||
"virtserialport,chardev=qga0,name=org.qemu.guest_agent.0",
|
||||
])
|
||||
.spawn()
|
||||
.context("unable to spawn qemu vm")?;
|
||||
|
||||
std::fs::write(&args.lockfile_path, child.id().to_string())
|
||||
.context("could not write PID to file")?;
|
||||
|
||||
loop {
|
||||
// Check if the child has exited prematurely, and if so, exit
|
||||
let result = child
|
||||
.try_wait()
|
||||
.context("error checking child process status")?;
|
||||
if let Some(status) = result {
|
||||
return Err(eyre::eyre!("child exited with code {:?}", status.code()));
|
||||
}
|
||||
|
||||
if std::fs::exists(&args.guest_agent_socket_path)? {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
bar.finish_and_clear();
|
||||
|
||||
Self::load(args, Some(child.id()))
|
||||
}
|
||||
|
||||
pub fn load(args: SpawnArguments, pid: Option<u32>) -> Result<Self> {
|
||||
let bar = spinner("Connecting to VM");
|
||||
let pid = match pid {
|
||||
Some(pid) => pid,
|
||||
None => {
|
||||
let pid_str = std::fs::read_to_string(&args.lockfile_path)
|
||||
.context("error reading PID from lockfile")?;
|
||||
pid_str.parse().context("could not parse PID")?
|
||||
}
|
||||
};
|
||||
|
||||
let writer = UnixStream::connect(&args.guest_agent_socket_path)
|
||||
.context("could not open socket to QVM guest agent")?;
|
||||
|
||||
let reader = BufReader::new(
|
||||
writer
|
||||
.try_clone()
|
||||
.context("couldn't clone socket to make buffered reader")?,
|
||||
);
|
||||
|
||||
bar.println(format!(
|
||||
"Connected to VM with PID {} and socket {}",
|
||||
pid,
|
||||
&args.guest_agent_socket_path.display(),
|
||||
));
|
||||
bar.finish_and_clear();
|
||||
|
||||
let vm = Self::from_parts(pid, writer, reader, args)?;
|
||||
|
||||
Ok(vm)
|
||||
}
|
||||
|
||||
fn from_parts(
|
||||
pid: u32,
|
||||
writer: UnixStream,
|
||||
reader: BufReader<UnixStream>,
|
||||
args: SpawnArguments,
|
||||
) -> Result<Self> {
|
||||
let mut vm = Self {
|
||||
pid,
|
||||
writer,
|
||||
reader,
|
||||
args,
|
||||
};
|
||||
|
||||
vm.flush()?;
|
||||
// NOTE: it is fine to use the system time here, modulo u32 for sending over
|
||||
// the wire. this is because this is not meant to be _secure_, it's just meant
|
||||
// to be _unique_, and we can assume this machine will not be running for 2^32
|
||||
// seconds, and _definitely_ won't be having a collision. i'm fine with it
|
||||
// crashing if those circumstances happen to be met.
|
||||
let time = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
||||
|
||||
let identifier = time.as_secs() % (u32::MAX as u64);
|
||||
|
||||
let ping_response = vm
|
||||
.execute_internal("guest-sync", serde_json::json!({"id": identifier}))
|
||||
.context("couldn't ping")?;
|
||||
|
||||
if ping_response.as_u64().is_none_or(|id| id != identifier) {
|
||||
return Err(eyre::eyre!(
|
||||
"known id {identifier} != given identifier {ping_response:?}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(vm)
|
||||
}
|
||||
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
// flush steps:
|
||||
// * put the reader into nonblocking mode
|
||||
// * read all the data possible to chew what's left over
|
||||
// * put the reader back into blocking mode
|
||||
// * put the parser in a bad state to reset it
|
||||
// * read a line from the parser to reset the input
|
||||
|
||||
let bar = spinner("Re-establishing connection...");
|
||||
self.writer
|
||||
.set_nonblocking(true)
|
||||
.context("flush: can't set nonblocking")?;
|
||||
if let Err(e) = self.reader.read_to_end(&mut vec![]) {
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock {
|
||||
return Err(e).context("flush: can't read nonblocked data");
|
||||
}
|
||||
}
|
||||
self.writer
|
||||
.set_nonblocking(false)
|
||||
.context("flush: can't set blocking")?;
|
||||
self.writer
|
||||
.write_all(&[0x1b])
|
||||
.context("flush: can't send reset byte")?;
|
||||
self.reader
|
||||
.read_line(&mut String::new())
|
||||
.context("flush: can't read error")?;
|
||||
bar.finish_and_clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&mut self,
|
||||
local_path: impl AsRef<Path>,
|
||||
remote_path: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
let remote_path_as_str = remote_path
|
||||
.as_ref()
|
||||
.to_str()
|
||||
.ok_or(eyre::eyre!("non-utf8 remote path filename"))?;
|
||||
let metadata = std::fs::metadata(&local_path)?;
|
||||
let bar = bar(
|
||||
metadata.len(),
|
||||
format!(
|
||||
"cp {local_path} vm:{remote_path}",
|
||||
local_path = local_path.as_ref().display(),
|
||||
remote_path = remote_path_as_str,
|
||||
),
|
||||
);
|
||||
let mut local_reader = std::fs::File::open(local_path)?;
|
||||
let remote_handle = self
|
||||
.execute_internal(
|
||||
"guest-file-open",
|
||||
serde_json::json!({
|
||||
"path": remote_path_as_str,
|
||||
"mode": "w",
|
||||
}),
|
||||
)
|
||||
.context("could not open file")?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("response was non-u64"))?;
|
||||
|
||||
// put this on the heap, we don't want smash the stack.
|
||||
let mut buf = vec![0u8; CHUNK_SIZE];
|
||||
while let Ok(size) = local_reader.read(&mut buf) {
|
||||
bar.inc(size as u64);
|
||||
if size == 0 {
|
||||
break;
|
||||
}
|
||||
let mut written = 0usize;
|
||||
loop {
|
||||
let response = self.execute_internal(
|
||||
"guest-file-write",
|
||||
serde_json::json!({
|
||||
"handle": remote_handle,
|
||||
"buf-b64": BASE64_STANDARD.encode(&buf[written..size]),
|
||||
}),
|
||||
)?;
|
||||
let response_written = response
|
||||
.get("count")
|
||||
.ok_or(eyre::eyre!("not given 'count' of bytes written"))?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("'count' not u64"))?;
|
||||
written += response_written as usize;
|
||||
if written == size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.execute_internal(
|
||||
"guest-file-close",
|
||||
serde_json::json!({"handle": remote_handle}),
|
||||
)?;
|
||||
bar.finish_and_clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pull(
|
||||
&mut self,
|
||||
remote_path: impl AsRef<Path>,
|
||||
local_path: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
let remote_path_as_str = remote_path
|
||||
.as_ref()
|
||||
.to_str()
|
||||
.ok_or(eyre::eyre!("non-utf8 remote path filename"))?;
|
||||
let spinner = spinner("finding remote file");
|
||||
let remote_handle = self
|
||||
.execute_internal(
|
||||
"guest-file-open",
|
||||
serde_json::json!({
|
||||
"path": remote_path_as_str,
|
||||
"mode": "r",
|
||||
}),
|
||||
)
|
||||
.context("could not open file")?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("response was non-u64"))?;
|
||||
let response = self.execute_internal(
|
||||
"guest-file-seek",
|
||||
serde_json::json!({
|
||||
"handle": remote_handle,
|
||||
"offset": 0,
|
||||
"whence": "end",
|
||||
}),
|
||||
)?;
|
||||
let size = response
|
||||
.get("position")
|
||||
.ok_or(eyre::eyre!("not given 'position' of bytes"))?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("'position' not u64"))?;
|
||||
|
||||
self.execute_internal(
|
||||
"guest-file-close",
|
||||
serde_json::json!({"handle": remote_handle}),
|
||||
)?;
|
||||
spinner.finish_and_clear();
|
||||
|
||||
let bar = bar(
|
||||
size,
|
||||
format!(
|
||||
"cp vm:{remote_path} {local_path}",
|
||||
remote_path = remote_path_as_str,
|
||||
local_path = local_path.as_ref().display()
|
||||
),
|
||||
);
|
||||
let mut local_writer = std::fs::File::create(local_path)?;
|
||||
let remote_handle = self
|
||||
.execute_internal(
|
||||
"guest-file-open",
|
||||
serde_json::json!({
|
||||
"path": remote_path_as_str,
|
||||
"mode": "r",
|
||||
}),
|
||||
)
|
||||
.context("could not open file")?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("response was non-u64"))?;
|
||||
let mut buf = vec![];
|
||||
loop {
|
||||
let chunk = self.execute_internal(
|
||||
"guest-file-read",
|
||||
serde_json::json!({
|
||||
"handle": remote_handle,
|
||||
}),
|
||||
)?;
|
||||
let base64_data = chunk
|
||||
.get("buf-b64")
|
||||
.ok_or(eyre::eyre!("not givenu 'buf-b64'"))?
|
||||
.as_str()
|
||||
.ok_or(eyre::eyre!("'buf-b64' not str"))?;
|
||||
BASE64_STANDARD.decode_vec(base64_data, &mut buf)?;
|
||||
local_writer.write_all(&buf)?;
|
||||
bar.inc(buf.len() as u64);
|
||||
buf.clear();
|
||||
if chunk
|
||||
.get("eof")
|
||||
.is_some_and(|eof| eof.as_bool().is_some_and(|b| b))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.execute_internal(
|
||||
"guest-file-close",
|
||||
serde_json::json!({"handle": remote_handle}),
|
||||
)?;
|
||||
bar.finish_and_clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: make this return status, stdout, stderr
|
||||
// TODO: accept optional: env, input-data, disable capture-output
|
||||
pub fn run_command(
|
||||
&mut self,
|
||||
command: &str,
|
||||
args: impl IntoIterator<Item = String>,
|
||||
) -> Result<(Vec<u8>, u64)> {
|
||||
let args = args.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"path": command,
|
||||
"arg": args,
|
||||
"capture-output": true,
|
||||
});
|
||||
|
||||
let bar = spinner(format!("Running: {command:?} {args:?}"));
|
||||
let result = self.execute_internal("guest-exec", &payload)?;
|
||||
let pid = result
|
||||
.get("pid")
|
||||
.ok_or(eyre::eyre!("no PID: {result:?}"))?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("pid is not u64"))?;
|
||||
|
||||
let payload = serde_json::json!({"pid": pid});
|
||||
let mut status;
|
||||
loop {
|
||||
status = self.execute_internal("guest-exec-status", &payload)?;
|
||||
if status
|
||||
.get("exited")
|
||||
.is_some_and(|e| e.as_bool().is_some_and(|b| b))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
bar.finish_and_clear();
|
||||
|
||||
let out_data = status
|
||||
.get("out-data")
|
||||
.ok_or(eyre::eyre!("response had no out-data"))?
|
||||
.as_str()
|
||||
.ok_or(eyre::eyre!("response['out-data'] is not string"))?;
|
||||
let parsed_data = BASE64_STANDARD
|
||||
.decode(out_data)
|
||||
.context("response output was not base64")?;
|
||||
let exit_code = status
|
||||
.get("exitcode")
|
||||
.ok_or(eyre::eyre!("response had no exitcode"))?
|
||||
.as_number()
|
||||
.ok_or(eyre::eyre!("response['exitcode'] is not number"))?
|
||||
.as_u64()
|
||||
.ok_or(eyre::eyre!("response['exitcode'] is not integer"))?;
|
||||
|
||||
Ok((parsed_data, exit_code))
|
||||
}
|
||||
|
||||
fn execute_internal<S: serde::Serialize>(
|
||||
&mut self,
|
||||
command: &'static str,
|
||||
args: S,
|
||||
) -> Result<serde_json::Value> {
|
||||
let message = serde_json::json!({
|
||||
"execute": command,
|
||||
"arguments": args,
|
||||
});
|
||||
|
||||
serde_json::to_writer(&mut self.writer, &message)
|
||||
.context("could not send message over socket")?;
|
||||
writeln!(&mut self.writer).context("could not send newline over socket")?;
|
||||
|
||||
let mut line = String::new();
|
||||
self.reader
|
||||
.read_line(&mut line)
|
||||
.context("can't read line from socket")?;
|
||||
|
||||
let response: serde_json::Value =
|
||||
serde_json::from_str(&line).context("response from qemu-guest-agent is not json")?;
|
||||
|
||||
if let Some(response) = response.get("return") {
|
||||
Ok(response.clone())
|
||||
} else if let Some(error) = response.get("error") {
|
||||
Err(eyre::eyre!("error response from qemu: {error:?}"))
|
||||
} else {
|
||||
Err(eyre::eyre!("invalid response from qemu: {response:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute<S: serde::Serialize + Debug>(
|
||||
&mut self,
|
||||
command: &'static str,
|
||||
args: S,
|
||||
) -> Result<serde_json::Value> {
|
||||
let bar = spinner(format!("Executing: {command:?} with {args:?}"));
|
||||
let result = self.execute_internal(command, args);
|
||||
bar.finish_and_clear();
|
||||
result
|
||||
}
|
||||
|
||||
// NOTE: u32 is returned from Process::id(), i32 is the Linux internal version
|
||||
// This should be safe; the kernel wouldn't give a value that, when converted
|
||||
// to a u32, can't be made back into an i32
|
||||
pub fn kill(self) -> Result<()> {
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
sys::signal::{kill, SIGKILL},
|
||||
unistd::{getpgid, Pid},
|
||||
};
|
||||
let pid = Pid::from_raw(self.pid as i32);
|
||||
if getpgid(Some(pid)).is_err() {
|
||||
eprintln!("Process not found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bar = spinner("Killing existing PID");
|
||||
loop {
|
||||
if let Err(e) = kill(pid, SIGKILL) {
|
||||
if e == Errno::ESRCH {
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
/*
|
||||
// NOTE: `waitpid()` will not work for non-child processes
|
||||
// instead, continuously kill(pid, SIGKILL)
|
||||
let result = waitpid(pid, Some(WaitPidFlag::WEXITED));
|
||||
if let Err(e) = result {
|
||||
if e != Errno::ECHILD {
|
||||
return Err(eyre::eyre!("couldn't await child {pid} (os error {e})"));
|
||||
}
|
||||
}
|
||||
*/
|
||||
remove_if_exists(&self.args.guest_agent_socket_path)?;
|
||||
remove_if_exists(&self.args.qmp_socket_path)?;
|
||||
remove_if_exists(&self.args.lockfile_path)?;
|
||||
bar.finish_and_clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue