From b9e6eeeb806fd76754580af4ba5cfeff2e47da09 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Mon, 18 Apr 2022 20:53:57 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 1 + Cargo.lock | 649 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 ++ src/circle.rs | 42 +++ src/collector.rs | 85 +++++++ src/docker.rs | 119 +++++++++ src/main.rs | 26 ++ src/state.rs | 16 ++ src/ui.rs | 197 ++++++++++++++ 9 files changed, 1151 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/circle.rs create mode 100644 src/collector.rs create mode 100644 src/docker.rs create mode 100644 src/main.rs create mode 100644 src/state.rs create mode 100644 src/ui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8d68d71 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,649 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aad2534fad53df1cc12519c5cda696dd3e20e6118a027e24054aea14a0bdcbe" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "composetui" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "crossterm 0.23.2", + "serde", + "serde_json", + "tokio", + "tui", +] + +[[package]] +name = "crossterm" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.7.14", + "parking_lot 0.11.2", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.8.2", + "parking_lot 0.12.0", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.2", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio 0.7.14", + "mio 0.8.2", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "mio 0.8.2", + "num_cpus", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tui" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ed0a32c88b039b73f1b6c5acbd0554bfa5b6be94467375fd947c4de3a02271" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.22.1", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..66a24cc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "composetui" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "3.1.9", features = ["derive"] } +crossterm = "0.23.2" +tui = "0.17.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +anyhow = "1" + +[dependencies.tokio] +version = "1.17.0" +features = ["rt-multi-thread", "fs", "macros", "process", "sync", "time"] diff --git a/src/circle.rs b/src/circle.rs new file mode 100644 index 0000000..9af7591 --- /dev/null +++ b/src/circle.rs @@ -0,0 +1,42 @@ +use std::f64::consts::PI; +use tui::style::Color; +use tui::widgets::canvas::{Painter, Shape}; + +pub struct Circle { + pub x: f64, + pub y: f64, + pub r: f64, + pub start: u16, + pub stop: u16, + pub color: Color, +} + +impl Default for Circle { + fn default() -> Self { + Self { + x: 0.0, + y: 0.0, + r: 1.0, + start: 0, + stop: 360, + color: Color::White, + } + } +} + +impl Shape for Circle { + fn draw(&self, painter: &mut Painter) { + let (x, y) = (self.x, self.y - self.r); + + for angle in (self.start..self.stop).map(|n| n as f64 / 180.0 * PI) { + let (x, y) = rotate(x, y, angle); + if let Some((x, y)) = painter.get_point(x, y) { + painter.paint(x, y, self.color); + } + } + } +} + +fn rotate(x: f64, y: f64, a: f64) -> (f64, f64) { + ((x * a.cos() - y * a.sin()), (x * a.sin() + y * a.cos())) +} diff --git a/src/collector.rs b/src/collector.rs new file mode 100644 index 0000000..28eb33e --- /dev/null +++ b/src/collector.rs @@ -0,0 +1,85 @@ +use crate::docker; +use crate::state::{StackStats, StateEvent}; +use std::collections::HashMap; +use tokio::sync::mpsc; +use tokio::time::{sleep, Duration}; + +pub(crate) async fn start_collector(events: mpsc::Sender) -> anyhow::Result<()> { + let mut old_stacks: HashMap = HashMap::new(); + + loop { + let new_stacks = collect_data()?; + + for (name, stats) in &new_stacks { + let mut send_put = false; + if let Some(old_stats) = old_stacks.get(name) { + if old_stats != stats { + send_put = true; + } + } else { + send_put = true; + } + + if send_put { + events + .send(StateEvent::Put { + name: name.clone(), + stats: stats.clone(), + }) + .await?; + } + } + + for name in old_stacks.keys() { + if !new_stacks.contains_key(name) { + events + .send(StateEvent::Delete { name: name.clone() }) + .await?; + } + } + + old_stacks = new_stacks; + + sleep(Duration::from_secs(1)).await; + } +} + +pub(crate) fn collect_data() -> anyhow::Result> { + let docker_stacks = docker::list_stacks()?; + + let mut out = HashMap::new(); + + for docker_stack in docker_stacks { + let containers = docker::list_containers(&docker_stack)?; + let processes = containers + .iter() + .map(|container| docker::list_processes(&docker_stack, &container)) + .flatten(/* ignore errors */) + .flatten(/* flatten per-container process list */) + .collect::>(); + + let memory = (1usize << 20) * 16; + let memory_usage = processes.iter().map(|proc| proc.memory_usage).sum(); + + let container_count = containers.len() as u32; + let running_containers = containers + .iter() + .filter(|c| c.state.contains("running")) + .count() as u32; + let stopped_containers = container_count - running_containers; + + let stats = StackStats { + containers: container_count, + running_containers, + stopped_containers, + process_count: processes.len() as u32, + cpu_percent: 0.5, // TODO + memory_usage, + memory_percent: (memory_usage as f64 / memory as f64), + }; + + out.insert(docker_stack.name, stats); + } + + Ok(out) +} diff --git a/src/docker.rs b/src/docker.rs new file mode 100644 index 0000000..d03cefc --- /dev/null +++ b/src/docker.rs @@ -0,0 +1,119 @@ +use serde::Deserialize; +use std::fs; +use std::process::Command; + +#[derive(Deserialize)] +pub struct Stack { + #[serde(rename = "ConfigFiles")] + pub config_file: String, + + #[serde(rename = "Name")] + pub name: String, + + #[serde(rename = "Status")] + pub status: String, +} + +#[derive(Deserialize)] +pub struct Container { + #[serde(rename = "Name")] + pub name: String, + + #[serde(rename = "Service")] + pub service: String, + + #[serde(rename = "State")] + pub state: String, + + #[serde(rename = "Health")] + pub health: String, + + #[serde(rename = "Project")] + pub project: String, +} + +/// Run `docker compose ls` and parse the output +pub fn list_stacks() -> anyhow::Result> { + let output = Command::new("docker") + .args(&["compose", "ls", "--format", "json"]) + .output()?; + let stdout = std::str::from_utf8(&output.stdout)?; + Ok(serde_json::from_str(&stdout)?) +} + +/// Run `docker compose ps` and parse the output +pub fn list_containers(stack: &Stack) -> anyhow::Result> { + let output = Command::new("docker") + .arg("compose") + .args(&["--file", &stack.config_file]) + .args(&["ps", "--format", "json"]) + .output()?; + + let stdout = std::str::from_utf8(&output.stdout)?; + Ok(serde_json::from_str(&stdout)?) +} + +pub struct Process { + pub uid: String, + pub pid: u32, + pub ppid: u32, + pub cmd: String, + + /// Memory usage in KBs + pub memory_usage: usize, +} + +/// Run `docker compose top` and parse the output +pub fn list_processes(stack: &Stack, container: &Container) -> anyhow::Result> { + let output = Command::new("docker") + .arg("compose") + .args(&["--file", &stack.config_file]) + .args(&["top", &container.service]) + .output()?; + let stdout = std::str::from_utf8(&output.stdout)?; + + let mut processes = Vec::new(); + + for line in stdout.lines().skip(2) { + if line.trim().is_empty() { + continue; + } + + let mut words = line.split_whitespace(); + + let err = || anyhow::format_err!("invalid docker top output"); + let uid = words.next().ok_or_else(err)?.to_string(); + let pid = words.next().ok_or_else(err)?.parse()?; + let ppid = words.next().ok_or_else(err)?.parse()?; + let _c = words.next().ok_or_else(err)?; + let _stime = words.next().ok_or_else(err)?; + let _tty = words.next().ok_or_else(err)?; + let _time = words.next().ok_or_else(err)?; + let cmd: String = words.collect(); + + let mut memory_usage = 0; + + let proc_info = fs::read_to_string(format!("/proc/{pid}/status"))?; + + for (key, value) in proc_info.lines().flat_map(|line| line.split_once(':')) { + let value = value.trim(); + + match key { + "VmRSS" => { + memory_usage = value.trim_end_matches(" kB").parse()?; + } + _ => {} + } + } + + processes.push(Process { + uid, + pid, + ppid, + cmd, + memory_usage, + }) + } + + Ok(processes) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e612c8c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +mod circle; +mod collector; +mod docker; +mod state; +mod ui; + +use tokio::sync::mpsc; +use tokio::task; +use ui::Ui; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let (event_tx, event_rx) = mpsc::channel(128); + + let collector = task::spawn(collector::start_collector(event_tx)); + + let mut ui = Ui::new(event_rx); + + if let Err(e) = task::spawn_blocking(move || ui.start()).await? { + println!("{e}"); + } + + collector.abort(); + + Ok(()) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b45e641 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,16 @@ +#[derive(Clone, Debug, PartialEq)] +pub struct StackStats { + pub containers: u32, + pub running_containers: u32, + pub stopped_containers: u32, + pub process_count: u32, + pub cpu_percent: f64, + pub memory_usage: usize, + pub memory_percent: f64, +} + +#[derive(Debug)] +pub enum StateEvent { + Put { name: String, stats: StackStats }, + Delete { name: String }, +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..5e15b25 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,197 @@ +use crate::circle::Circle; +use crate::state::{StackStats, StateEvent}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::collections::BTreeMap; +use std::time::Duration; +use std::{io, iter}; +use tokio::sync::mpsc::{self, error::TryRecvError}; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::canvas::Canvas, + widgets::{Block, Borders, Paragraph}, + Frame, Terminal, +}; + +pub struct Ui { + stacks: BTreeMap, + events: mpsc::Receiver, +} + +impl Ui { + pub fn new(events: mpsc::Receiver) -> Self { + Self { + stacks: Default::default(), + events, + } + } + + pub fn start(&mut self) -> anyhow::Result<()> { + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let result = self.run(&mut terminal); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + result + } + + fn run(&mut self, terminal: &mut Terminal) -> anyhow::Result<()> { + loop { + terminal.draw(|f| self.draw(f))?; + + loop { + let mut draw = false; + let timeout = Duration::from_millis(16); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + draw = true; + if let KeyCode::Char('q') = key.code { + return Ok(()); + } + } + } + + match self.events.try_recv() { + Err(TryRecvError::Empty) => {} + Err(e) => return Err(e.into()), + Ok(event) => { + self.handle_event(event); + draw = true; + } + } + + if draw { + break; + } + } + } + } + + fn handle_event(&mut self, event: StateEvent) { + match event { + StateEvent::Delete { name } => { + self.stacks.remove(&name); + } + StateEvent::Put { name, stats } => { + self.stacks.insert(name, stats); + } + } + } + + fn draw(&self, f: &mut Frame<'_, B>) { + let size = f.size(); + + const BOX_HEIGHT: u16 = 9; + let fitted_boxes = size.height / BOX_HEIGHT; + let partial_box_size = size.height % BOX_HEIGHT; + let partial_box_exists = partial_box_size != 0; + + let constraints: Vec<_> = iter::repeat(Constraint::Length(BOX_HEIGHT)) + .take(fitted_boxes as usize) + .chain(partial_box_exists.then(|| Constraint::Length(partial_box_size))) + .collect(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(f.size()); + + for (i, (name, info)) in (0..fitted_boxes).zip(self.stacks.iter()) { + let area = chunks[i as usize]; + + self.draw_stack(f, area, name, info); + } + + if partial_box_exists { + let block = Block::default() + .title("Partial") + .borders(Borders::ALL.difference(Borders::BOTTOM)); + f.render_widget(block, chunks[chunks.len() - 1]); + } + } + + fn draw_stack(&self, f: &mut Frame, area: Rect, name: &str, info: &StackStats) { + let title_style = Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::BOLD); + + let block = Block::default() + .title(Span::styled(name, title_style)) + .borders(Borders::ALL); + let inner = block.inner(area); + f.render_widget(block, area); + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(20), + Constraint::Length(15), + Constraint::Length(15), + Constraint::Length(0), // pad with white-space + ]) + .split(inner); + + let services = Paragraph::new(vec![ + Spans::from(""), + Spans::from(format!("containers: {}", info.containers)), + if info.stopped_containers != 0 { + Span::styled( + format!("stopped: {} (!)", info.stopped_containers), + Style::default().fg(Color::Red), + ) + .into() + } else { + Spans::from("") + }, + Spans::from(""), + Spans::from(format!("processes: {}", info.process_count)), + Spans::from(format!("memory: {} KBs", info.memory_usage)), + ]); + f.render_widget(services, chunks[0]); + + let gauge_canvas = |percent: f64, name: &'static str| { + Canvas::default() + .x_bounds([-5.0, 5.0]) + .y_bounds([-5.0, 5.0]) + .paint(move |ctx| { + ctx.draw(&Circle { + r: 4.0, + color: Color::Blue, + ..Default::default() + }); + ctx.draw(&Circle { + r: 4.0, + color: Color::Green, + start: 360 - (percent * 360.0) as u16, + stop: 360, + ..Default::default() + }); + ctx.print(-0.5, 0.0, name); + }) + }; + + f.render_widget(gauge_canvas(info.cpu_percent, "CPU"), chunks[1]); + f.render_widget(gauge_canvas(info.memory_percent, "MEM"), chunks[2]); + } +}