diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e379e95 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5912f6a --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..146d531 --- /dev/null +++ b/src/cli.rs @@ -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, + }, + + /// 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 { + 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(), + }) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4c7d3d3 --- /dev/null +++ b/src/main.rs @@ -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::>(); + 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(()) +} diff --git a/src/vm.rs b/src/vm.rs new file mode 100644 index 0000000..104b4dc --- /dev/null +++ b/src/vm.rs @@ -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) -> Result { + 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) -> 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) -> 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> { + 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, + 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, + + /// 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 { + 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) -> Result { + 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, + args: SpawnArguments, + ) -> Result { + 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, + remote_path: impl AsRef, + ) -> 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, + local_path: impl AsRef, + ) -> 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, + ) -> Result<(Vec, u64)> { + let args = args.into_iter().collect::>(); + + 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( + &mut self, + command: &'static str, + args: S, + ) -> Result { + 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( + &mut self, + command: &'static str, + args: S, + ) -> Result { + 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(()) + } +}