diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c75df0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Generated by Cargo +# If you're creating an application, put the next line in the ignore list. +# If you're creating a library, remove it. +# Cargo.lock + +# Build artifacts +/target +/target/x86_64-unknown-linux-musl +/target/aarch64-unknown-linux-musl + +# Assets / Documentation +/docs/generated/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# OS specific +.DS_Store +Thumbs.db + +# RMon Specific +rmon_askpass_* +rmon_ssh_sock_* +rmon_auth_req_* +rmon_auth_res_* +*.log + +# Claude/Agent sessions +.claude/ +.claude_session +.gemini/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..221ea53 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4323 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" + +[[package]] +name = "accesskit_consumer" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c17cca53c09fbd7288667b22a201274b9becaa27f0b91bf52a526db95de45e6" +dependencies = [ + "accesskit", +] + +[[package]] +name = "accesskit_macos" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3b6ae1eabbfbced10e840fd3fce8a93ae84f174b3e4ba892ab7bcb42e477a7" +dependencies = [ + "accesskit", + "accesskit_consumer", + "objc2 0.3.0-beta.3.patch-leaks.3", + "once_cell", +] + +[[package]] +name = "accesskit_unix" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f46c18d99ba61ad7123dd13eeb0c104436ab6af1df6a1cd8c11054ed394a08" +dependencies = [ + "accesskit", + "accesskit_consumer", + "async-channel", + "async-once-cell", + "atspi", + "futures-lite 1.13.0", + "once_cell", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcae27ec0974fc7c3b0b318783be89fd1b2e66dd702179fe600166a38ff4a0b" +dependencies = [ + "accesskit", + "accesskit_consumer", + "once_cell", + "paste", + "static_assertions", + "windows", +] + +[[package]] +name = "accesskit_winit" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e39fcec2e10971e188730b7a76bab60647dacc973d4591855ebebcadfaa738" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" +dependencies = [ + "android-properties", + "bitflags 1.3.2", + "cc", + "jni-sys 0.3.1", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum 0.6.1", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.4.1", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling 3.11.0", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-once-cell" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io 2.6.0", + "async-lock 3.4.2", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-connection" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" +dependencies = [ + "atspi-common", + "atspi-proxies", + "futures-lite 1.13.0", + "zbus", +] + +[[package]] +name = "atspi-proxies" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-sys" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.2.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" +dependencies = [ + "block-sys", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" +dependencies = [ + "bitflags 1.3.2", + "log", + "nix 0.25.1", + "slotmap", + "thiserror", + "vec_map", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "ecolor" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7637fc2e74d17e52931bac90ff4fc061ac776ada9c7fa272f24cdca5991972" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "eframe" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd73918a828c35a7efb4d7188ea973df4bffc589178ed95f521c917b03ddcfa" +dependencies = [ + "bytemuck", + "cocoa", + "egui", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "static_assertions", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "winit", +] + +[[package]] +name = "egui" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55bcb864b764eb889515a38b8924757657a250738ad15126637ee2df291ee6b" +dependencies = [ + "accesskit", + "ahash", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-winit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b673606b6606b12b95e3a3194d7882bf5cff302db36a520b8144c7c342e4e84" +dependencies = [ + "accesskit_winit", + "arboard", + "egui", + "log", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262151f9d57c557c02a40a46f27b9e050a6eb0b006b94dced9c6f4519a04d489" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset 0.7.1", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "egui_plot" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b484821a5b336af40a34151a940f52bef0f1ab56ec0d8cf80f74783eaae412" +dependencies = [ + "egui", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "emath" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a045c6c0b44b35e98513fc1e9d183ab42881ac27caccb9fa345465601f56cce4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "epaint" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1b9e000d21bab9b535ce78f9f7745be28b3f777f6c7223936561c5c7fefab8" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.4.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc93b03242719b8ad39fb26ed2b01737144ce7bd4bfc7adadcef806596760fe" +dependencies = [ + "bitflags 1.3.2", + "cfg_aliases", + "cgl", + "core-foundation", + "dispatch", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading 0.7.4", + "objc2 0.3.0-beta.3.patch-leaks.3", + "once_cell", + "raw-window-handle", + "wayland-sys 0.30.1", + "windows-sys 0.45.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629a873fc04062830bfe8f97c03773bcd7b371e23bcc465d0a61448cd1588fa4" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af784eb26c5a68ec85391268e074f0aa618c096eadb5d6330b0911cf34fe57c5" +dependencies = [ + "gl_generator", + "windows-sys 0.45.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b53cb5fe568964aa066a3ba91eac5ecbac869fb0842cd0dc9e412434f1a1494" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef89398e90033fc6bc65e9bd42fd29bbbfd483bda5b56dc5562f455550618165" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys 0.3.1", + "ndk-sys", + "num_enum 0.5.11", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.2.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" + +[[package]] +name = "objc2" +version = "0.3.0-beta.3.patch-leaks.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" +dependencies = [ + "block2", + "objc-sys", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "2.0.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "orbclient" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.4.1", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rmon-agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "byteorder", + "bytes", + "chrono", + "clap", + "futures", + "itertools", + "notify", + "regex", + "rmon-common", + "serde", + "tempfile", + "tokio", + "tokio-util", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rmon-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "serde", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "rmon-ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "eframe", + "egui", + "egui_plot", + "futures", + "itertools", + "libc", + "rmon-common", + "serde", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" +dependencies = [ + "bitflags 1.3.2", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.3", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "smithay-clipboard" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" +dependencies = [ + "smithay-client-toolkit", + "wayland-client", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.4.1", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-skia" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[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 = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags 1.3.2", + "downcast-rs", + "libc", + "nix 0.24.3", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys 0.29.5", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix 0.24.3", + "once_cell", + "smallvec", + "wayland-sys 0.29.5", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.3", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags 1.3.2", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "wayland-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" +dependencies = [ + "dlib", + "lazy_static", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle", + "url", + "web-sys", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-implement 0.48.0", + "windows-interface 0.48.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.28.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9596d90b45384f5281384ab204224876e8e8bf7d58366d9b795ad99aa9894b94" +dependencies = [ + "android-activity", + "bitflags 1.3.2", + "cfg_aliases", + "core-foundation", + "core-graphics", + "dispatch", + "instant", + "libc", + "log", + "mio 0.8.11", + "ndk", + "objc2 0.3.0-beta.3.patch-leaks.3", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle", + "redox_syscall 0.3.5", + "sctk-adwaita", + "smithay-client-toolkit", + "wasm-bindgen", + "wayland-client", + "wayland-commons", + "wayland-protocols", + "wayland-scanner", + "web-sys", + "windows-sys 0.45.0", + "x11-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.26.4", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..386f5b7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[workspace] +resolver = "2" +members = [ + "rmon-common", + "rmon-agent", + "rmon-ui", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +authors = ["RMon Team"] +license = "MIT" + +[workspace.dependencies] +anyhow = "1.0" +bincode = "1.3" +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.36", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +toml = "0.8" +futures = "0.3" +bytes = "1.5" +itertools = "0.12" +thiserror = "1.0" +byteorder = "1.5" +async-trait = "0.1" +tokio-util = { version = "0.7", features = ["codec"] } +regex = "1.10" +notify = "6.1" +clap = { version = "4.4", features = ["derive"] } +tempfile = "3.10" +libc = "0.2" +rmon-common = { path = "rmon-common" } diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..5607485 --- /dev/null +++ b/DEVELOPER_GUIDE.md @@ -0,0 +1,61 @@ +# RMon Developer Guide + +This guide covers the internal architecture, protocol, and extension points of the RMon system. + +## Architecture Overview + +RMon is built with a focus on performance, safety, and portability. + +### `rmon-common` +Contains shared types used by both the agent and UI. +- **Protocol**: Defined in `src/protocol.rs`. Uses `bincode` for serialization over a length-prefixed TCP stream. +- **Data Model**: `src/signal.rs` defines `Sample`, `SignalInfo`, and `SignalId`. + +### `rmon-agent` +A multi-threaded server using `tokio`. +- **Dispatcher**: Samples from all sources flow through a central `mpsc` channel to a dispatcher task. +- **Fan-out**: The dispatcher sends samples to: + 1. The **Storage Engine** (binary append logs). + 2. All connected **Session Handlers** via a `broadcast` channel. +- **DataSource Trait**: Abstract interface for data ingestion. New source types only need to implement the `run` and `signals` methods. + +### `rmon-ui` +An immediate-mode GUI application. +- **Backend Thread**: A separate thread runs the `tokio` runtime for non-blocking network I/O. +- **Event Loop**: Communicates with the UI thread via `mpsc` channels (`UiMessage`). +- **Repaint Policy**: The UI only repaints when new data arrives or on a fixed interval (50ms) to ensure smooth real-time performance. + +## Adding a New Data Source + +To add a new source type (e.g., `MQTT` or `Serial`): + +1. **Update Config**: Add relevant fields to `SourceConfig` and `SignalSourceConfig` in `rmon-agent/src/config.rs`. +2. **Implement `DataSource`**: Create a new file in `rmon-agent/src/sources/` and implement the trait: + ```rust + #[async_trait] + pub trait DataSource: Send + 'static { + fn name(&self) -> &str; + fn signals(&self) -> Vec; + async fn run(self: Box, tx: mpsc::Sender<(SignalId, Sample)>) -> Result<()>; + } + ``` +3. **Register**: Add the source to `rmon-agent/src/sources/mod.rs` and update the `run_agent` loop in `rmon-agent/src/lib.rs`. + +## Development Workflows + +### Static Agent Build (Linux) +```bash +cargo build --release --target x86_64-unknown-linux-musl -p rmon-agent +``` + +### Integration Testing +The project uses automated integration tests that spin up real agent instances and verify the protocol flow. +```bash +cargo test -p rmon-agent --test agent_test +``` + +### Protocol Changes +If you modify `rmon-common/src/protocol.rs`: +1. Increment `PROTOCOL_VERSION` in `protocol.rs`. +2. Rebuild both the UI and the Agent. +3. The UI will automatically detect the version mismatch on handshake and refuse to connect to old agents. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7053925 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +.PHONY: all setup agent ui clean check help + +# Default target: builds everything +all: ui + +# Install the required rust target for static agent builds +setup: + rustup target add x86_64-unknown-linux-musl + +# Build the agent statically (Zero dynamic dependencies) +agent: + @echo "Building rmon-agent (static x86_64-musl)..." + cargo build --release --target x86_64-unknown-linux-musl -p rmon-agent + +# Build the UI +# Note: This depends on 'agent' because rmon-ui embeds the agent binary via include_bytes! +ui: agent + @echo "Building rmon-ui..." + cargo build --release -p rmon-ui + +# Run all workspace tests +check: + cargo test --workspace -- --test-threads=1 + +# Generate coverage report (requires cargo-tarpaulin) +coverage: + cargo tarpaulin --workspace --timeout 120 --out Lcov --output-dir target/coverage + +# Clean all build artifacts +clean: + cargo clean + +# Show help +help: + @echo "RMon Build System" + @echo "" + @echo "Targets:" + @echo " setup - Install the x86_64-musl rust target" + @echo " agent - Build the statically-linked agent binary" + @echo " ui - Build the desktop UI (includes the agent binary)" + @echo " all - Build both (default)" + @echo " check - Run all workspace tests" + @echo " clean - Remove build artifacts" diff --git a/README.md b/README.md new file mode 100644 index 0000000..611a48a --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# RMon: Remote Monitoring & Acquisition System + +RMon is a lightweight, high-performance remote data acquisition and monitoring system. It consists of a statically-linked **Agent** that runs on remote Linux machines and a cross-platform **UI** that manages the agent lifecycle and visualizes data in real-time. + +## Key Features + +- **Zero-Dependency Agent**: Fully static `musl` binary. Copy and run on any Linux kernel ≥ 3.2. +- **VSCode-Style Deployment**: The UI automatically deploys and starts the agent over SSH. No manual installation required on the remote side. +- **Multi-Source Acquisition**: Support for UDP streams, CSV files, and custom Shell commands with Regex parsing. +- **Optimized Storage**: Real-time circular buffers for live display and append-only binary logs for persistent historical data. +- **Real-Time Visualization**: High-performance plotting using `egui` and `egui_plot`. + +## Project Structure + +- `rmon-agent/`: The remote server that collects and stores data. +- `rmon-ui/`: The desktop application for monitoring and management. +- `rmon-common/`: Shared protocol definitions and data models. +- `docs/`: Technical specifications and architectural details. + +## Quick Start + +### 1. Build the Agent (Static) +The agent must be built for the target architecture. For a standard x86_64 Linux target: +```bash +cargo build --release --target x86_64-unknown-linux-musl -p rmon-agent +``` + +### 2. Run the UI +```bash +cargo run -p rmon-ui +``` + +### 3. Connect +1. Enter the **SSH Host** (alias from your `~/.ssh/config`). +2. Click **Connect**. The UI will automatically: + - Check if the agent is running on the remote host. + - Deploy the binary to `~/.rmon/agent` if missing. + - Start a persistent instance. +3. Select signals from the tree to begin plotting. + +## Documentation + +- [User Guide](USER_GUIDE.md): How to configure and use RMon. +- [Developer Guide](DEVELOPER_GUIDE.md): Architecture, protocol, and how to add new sources. +- [Agent Specification](docs/agent-spec.md) +- [Protocol Reference](docs/protocol.md) diff --git a/USER_GUIDE.md b/USER_GUIDE.md new file mode 100644 index 0000000..5160aed --- /dev/null +++ b/USER_GUIDE.md @@ -0,0 +1,114 @@ +# RMon User Guide + +This guide explains how to configure and use the RMon Agent and UI to monitor remote data. + +## RMon Agent + +The Agent is a lightweight server that runs on the remote machine. It collects data from configured sources and sends it to the UI. + +### Persistent Storage +- All signals are persistently logged to `~/.rmon/data/`. +- Logs are stored in an optimized binary format (`(i64, f64)` pairs). +- Data is kept until explicitly deleted, ensuring no data loss when the UI is closed. + +### Configuration (`~/.rmon/config.toml`) + +The agent is configured via a TOML file. The UI can deploy this file automatically, but it can also be edited manually. **Note: Every source must define its own timestamp configuration.** + +#### UDP Source +Useful for high-frequency binary data. +```toml +[[sources]] +type = "udp" +name = "imu_stream" +bind = "0.0.0.0:9000" + +[sources.timestamp] +byte_offset = 24 +byte_type = "u64_le" +format = "microseconds_since_epoch" + +[[sources.signals]] +id = "accel_x" +label = "Accelerometer X" +unit = "m/s²" +scale = 0.001 +offset = 0.0 +byte_offset = 0 +byte_type = "f32_le" +path = ["imu", "accelerometer"] +``` + +#### CSV Source +Useful for log files being written by other processes. Supports mapping specific columns to signals and parsing custom timestamp formats. + +```toml +[[sources]] +type = "csv" +name = "sensor_logs" +path = "/path/to/data.csv" +poll_interval = "500ms" + +[sources.timestamp] +column = "0" # 0-indexed column for the timestamp +# Format can be "nanoseconds_since_epoch", "seconds_since_epoch", +# or a custom strftime string (e.g., "%Y-%m-%d %H:%M:%S") +format = "%Y-%m-%d %H:%M:%S" + +[[sources.signals]] +id = "temp_1" +label = "Temperature 1" +unit = "°C" +column = "1" # Mapped to the second column +scale = 1.0 +offset = 0.0 +path = ["sensors"] + +[[sources.signals]] +id = "pressure" +label = "System Pressure" +unit = "bar" +column = "2" # Mapped to the third column +scale = 0.1 +offset = 0.0 +path = ["sensors"] +``` + +#### Shell Source +Highly flexible — run any command and parse the output with Regex. +```toml +[[sources]] +type = "shell" +name = "system_status" +command = "echo \"cpu_temp: $(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null | awk '{print $1/1000}' || echo 0)\"" +poll_interval = "1s" + +[sources.timestamp] +format = "none" # Use agent arrival time + +[[sources.signals]] +id = "cpu_temp" +label = "CPU Temperature" +unit = "°C" +scale = 1.0 +regex = "cpu_temp: ([0-9.]+)" +path = ["system", "thermal"] +``` + +--- + +## RMon UI + +The UI is the central control point for RMon. + +### Connection and Deployment +1. **SSH Host**: Enter the hostname or alias (from `~/.ssh/config`) of the remote machine. +2. **Agent Port**: Default is `7891`. +3. **Connect**: Click 🚀 to connect. If the agent is not running, the UI will automatically deploy the pre-built binary and start it as a persistent background process. +4. **Disconnect**: Clicking 🔌 closes the UI's connection, but the agent continues to collect and store data on the remote machine. +5. **Kill Agent**: Clicking 💀 will terminate the remote agent process. + +### Visualization +- **Signals Tree**: All signals are grouped into a hierarchical tree based on their `path` configuration. +- **Plots**: Check a signal's box to add it to the real-time plotting area. +- **Scroll & Zoom**: The plots are interactive. Use the mouse wheel to zoom and drag to pan. diff --git a/docs/agent-spec.md b/docs/agent-spec.md index 5ec24f7..690da8f 100644 --- a/docs/agent-spec.md +++ b/docs/agent-spec.md @@ -125,6 +125,13 @@ offset = 0.0 path = ["vacuum"] ``` +## Remote Configuration + +The agent allows the UI to read and update its configuration file (`~/.rmon/config.toml`) over the protocol. + +- `ClientMessage::GetConfig`: Agent reads the file from disk and sends `AgentMessage::ConfigReport { config_toml }`. +- `ClientMessage::UpdateConfig { config_toml }`: Agent writes the provided string to `~/.rmon/config.toml`. The agent's file watcher detects this change and reloads the sources. + ## Source Adapter Trait All data sources implement: diff --git a/docs/ui-spec.md b/docs/ui-spec.md index 42960d6..efece30 100644 --- a/docs/ui-spec.md +++ b/docs/ui-spec.md @@ -3,162 +3,50 @@ ## Technology - **Framework**: `egui` + `eframe` (immediate-mode Rust GUI) -- **Render backend**: `glow` (OpenGL 2.1+) — chosen for maximum Linux compatibility -- **Plotting**: `egui_plot` for interactive panels; custom `egui` painter (line strip primitives) for high-throughput paths (>100k points/frame) -- **Async runtime**: `tokio` (runs in a background thread; communicates with egui via `std::sync::mpsc`) -- **SSH tunnel management**: spawns system `ssh` subprocess +- **Render backend**: `glow` (OpenGL 2.1+) +- **Plotting**: `egui_plot` for interactive panels. +- **Async runtime**: `tokio` (runs in a background thread). ## Deployment -Single binary. On Linux, dynamically links only: -- `libGL` (OpenGL driver, always present on any desktop Linux) -- `libX11` / `libxcb` (X11, or Wayland via `libwayland-client` with `wayland` feature) -- `libc` (glibc, standard on all Linux distributions) +Single binary embedding the statically-linked Agent binary. -On Windows: fully static (links Win32 APIs and ANGLE for OpenGL ES via DirectX). +## UI Layout -Pre-built agent binaries for all supported architectures are **embedded in the UI binary** at compile time using `include_bytes!`. This allows the UI to deploy the agent without any extra files. +### Top Toolbar +- **🚀 Connect...**: Opens the connection dialog. +- **🔌 Disconnect**: Closes current session. +- **💀 Kill Agent**: Terminates the remote agent process. +- **⚙ Configure Agent**: Opens the remote configuration editor. -## Layout +### Connection Dialog (Modal) +Allows configuring: +- **SSH Host**: alias from `~/.ssh/config`. +- **Agent Port**: remote port (default 7891). +- **Local TCP Addr**: local tunnel entry point. -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ [Connect ▼] [host: myserver] [⏺ Record] [⏹ Stop] [Status: OK] │ ← Toolbar -├────────────────┬────────────────────────────────────────────────────┤ -│ Signal Tree │ Plot Panel 1 │ Plot Panel 2 │ -│ │ │ │ -│ ▼ imu │ [signal A] [signal B] [✕] │ [signal C] [✕] │ -│ ▼ accel │ ┌──────────────────────────┐ │ ┌─────────────┐ │ -│ accel_x ══╪═►│ time-series plot │ │ │ │ │ -│ accel_y │ │ │ │ │ │ │ -│ accel_z │ │ ╭─╮ ╭──╮ │ │ │ │ │ -│ ▼ gyro │ │ ╯ ╰──╯ ╰─ │ │ │ │ │ -│ gyro_x │ │ │ │ └─────────────┘ │ -│ gyro_y │ │ ┼────────────────────── │ │ │ -│ ▼ temps │ └──────────────────────────┘ │ [+ Add Plot] │ -│ inlet │ [cursor A: t=12.3s val=1.2] │ │ -│ outlet │ [cursor B: t=15.1s Δt=2.8s] │ │ -│ │ [A-B: Δval=-0.3 mean=1.05] │ │ -└───────────────┴────────────────────────────────┴───────────────────┘ -``` +### Remote Configuration Editor (Modal) +- Full-screen TOML editor for the agent's `config.toml`. +- **Save & Apply**: Pushes configuration to the agent. +- **Refresh**: Pulls the latest configuration from the agent. -## Connection Dialog +### Status Bar (Bottom) +- Displays current connection state (✔ Connected / ✘ Disconnected). +- Shows host and port details when connected. -Shown on startup (or via toolbar "Connect" button) when not connected. +### Signal Tree (Left Panel) +- Displays all available signals from the agent. +- Checkboxes to subscribe/unsubscribe from live data. -Fields: -- **SSH Host**: free text, matched against `~/.ssh/config` Host entries (autocomplete offered) -- **Agent Port**: numeric, default `7891` -- **Agent Config**: path on remote machine, default `~/.rmon/config.toml` +### Plot Area (Central Panel) +- Vertical scroll area containing live plots. -On connect: -1. Detect remote arch via `ssh {host} uname -m` -2. Check / upload agent binary (see `docs/architecture.md` connection flow) -3. Start agent if not running -4. Establish port forward -5. Perform protocol handshake -6. Fetch signal list → populate signal tree +## Connection Flow -Connection errors are shown inline with a retry button. - -## Signal Tree Panel - -- Collapsible tree mirroring the `path` field of each `SignalInfo` -- Each leaf shows: label, unit, last value (live-updated), colored dot (source status) -- **Drag-and-drop**: drag a signal leaf onto a plot panel to add it -- **Right-click menu**: "Add to plot 1 / 2 / new plot", "Signal info" (opens info popup), "Start recording", "Stop recording" -- **Search/filter bar** at the top of the panel -- Recording state shown with red ⏺ icon on the signal leaf - -## Plot Panels - -Any number of plot panels can be open (horizontally tiled by default, resizable/detachable). - -### Axes -- X axis: time (absolute UTC or relative to a reference point; toggled in toolbar) -- Y axis: auto-range by default; can be locked manually -- Zoom: scroll wheel (time axis), Ctrl+scroll (Y axis) -- Pan: click-drag on the plot - -### Signals in a Plot -- Each signal rendered as a colored line -- Legend at top of plot (click to hide/show individual signals) -- Color is auto-assigned, can be changed via right-click on the legend entry -- Line style (solid/dashed/dotted) and width configurable per signal - -### Cursors -- Add cursors via toolbar button or right-click on plot → "Add cursor" -- Drag to position; snaps to nearest sample if within threshold -- Measurement bar below the plot shows: - - Per cursor: timestamp, value for each visible signal - - Between two cursors: Δt, Δvalue, mean, min, max over the interval - -### Math Signals -- Right-click plot → "Add math signal" -- Expression editor (simple formula: `a + b`, `a * 2.0`, `a - b`, etc. where `a`, `b` are signal IDs) -- Math signals appear in the legend with an `f(x)` icon and can be added to other plots - -### High-Throughput Rendering -When a signal has more points than pixels in the X direction, the renderer decimates using **min-max downsampling** (preserves peaks/troughs visually). This runs on the CPU in `plot_panel.rs` before passing vertices to egui's painter. - -## Toolbar - -- **Connect / Disconnect** dropdown (shows current host or "Not connected") -- **Time range** selector: presets (last 10s / 1min / 10min / 1h) or custom range picker -- **Time display** toggle: absolute UTC vs relative -- **Record** button: opens signal selection dialog → starts recording for selected signals -- **Status indicator**: green = connected, yellow = reconnecting, red = disconnected - -## Keyboard Shortcuts - -| Key | Action | -|-----|--------| -| `Space` | Pause / resume live scrolling | -| `R` | Reset all plot zoom/pan to live view | -| `C` | Add cursor to focused plot | -| `Del` | Remove selected cursor | -| `Ctrl+N` | Add new plot panel | -| `Ctrl+W` | Close focused plot panel | -| `Ctrl+F` | Focus signal tree search | -| `Ctrl+S` | Save current layout to file | - -## Session Persistence - -On clean exit, the UI saves the current session to `~/.config/rmon/last_session.toml`: -- Connected host alias -- Open plot panels and their signal assignments -- Cursor positions -- Time range and zoom state - -On next launch with the same host, it offers to restore the session. - -## State Machine (connection) - -``` -Disconnected - │ user clicks Connect - ▼ -Deploying (copying/verifying agent binary) - │ agent ready - ▼ -Tunneling (SSH port-forward process running) - │ port open - ▼ -Handshaking (TCP open, sent Handshake, awaiting HandshakeAck) - │ accepted - ▼ -Connected (normal operation, DataBatch messages flowing) - │ tunnel drops / error - ▼ -Reconnecting (exponential backoff, max 30s, reuses same tunnel if still up) - │ max retries exceeded - ▼ -Disconnected -``` - -## Async / UI Thread Boundary - -- `tokio` runtime runs in a dedicated OS thread -- The egui render loop runs on the main thread (required by most platform window systems) -- Communication: `std::sync::mpsc::channel` — the tokio tasks push `UiEvent` variants (DataBatch, SignalList, Status, Error) to the UI; the UI pushes `Command` variants (Subscribe, QueryHistory, StartRecording) to tokio -- `eframe::App::update()` drains the incoming event channel each frame before rendering +1. User clicks **🚀 Connect...** and enters details. +2. UI checks if agent is running on remote host via SSH. +3. UI deploys agent binary if missing. +4. UI starts agent process (`nohup`). +5. UI establishes SSH local port forward tunnel. +6. UI connects TCP client to local tunnel end. +7. Protocol handshake and initial signal list fetch. diff --git a/rmon-agent/Cargo.toml b/rmon-agent/Cargo.toml new file mode 100644 index 0000000..2798c98 --- /dev/null +++ b/rmon-agent/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "rmon-agent" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +serde.workspace = true +chrono.workspace = true +toml.workspace = true +bincode.workspace = true +regex.workspace = true +notify.workspace = true +clap.workspace = true +futures.workspace = true +bytes.workspace = true +itertools.workspace = true +byteorder.workspace = true +async-trait.workspace = true +rmon-common.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } +futures.workspace = true +toml.workspace = true +anyhow.workspace = true +tempfile.workspace = true + +[profile.release] +strip = true +lto = true +codegen-units = 1 +panic = "abort" diff --git a/rmon-agent/src/config.rs b/rmon-agent/src/config.rs new file mode 100644 index 0000000..bee143b --- /dev/null +++ b/rmon-agent/src/config.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Config { + pub storage: StorageConfig, + pub sources: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct StorageConfig { + pub data_dir: PathBuf, + pub default_mode: String, // "continuous" or "circular" + pub default_window: String, // e.g. "2h" +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SourceConfig { + #[serde(rename = "type")] + pub source_type: String, + pub name: String, + pub bind: Option, + pub path: Option, + pub addr: Option, + pub command: Option, + pub poll_interval: Option, + pub timestamp: TimestampConfig, + pub signals: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TimestampConfig { + pub byte_offset: Option, + pub byte_type: Option, + pub column: Option, + pub format: String, + pub regex: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SignalSourceConfig { + pub id: String, + pub label: String, + pub unit: String, + pub scale: f64, + pub offset: f64, + pub byte_offset: Option, + pub byte_type: Option, + pub column: Option, + pub pv: Option, + pub regex: Option, + pub path: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_parsing() { + let toml_str = r#" +[storage] +data_dir = "/tmp/data" +default_mode = "circular" +default_window = "1h" + +[[sources]] +type = "udp" +name = "test" +bind = "127.0.0.1:9000" +[sources.timestamp] +format = "none" +[[sources.signals]] +id = "s1" +label = "L1" +unit = "V" +scale = 1.0 +offset = 0.0 +path = ["p1"] +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + assert_eq!(config.storage.data_dir.to_str().unwrap(), "/tmp/data"); + assert_eq!(config.sources.len(), 1); + assert_eq!(config.sources[0].source_type, "udp"); + } +} diff --git a/rmon-agent/src/lib.rs b/rmon-agent/src/lib.rs new file mode 100644 index 0000000..3c98208 --- /dev/null +++ b/rmon-agent/src/lib.rs @@ -0,0 +1,220 @@ +pub mod config; +pub mod sources; +pub mod storage; +pub mod server; + +use anyhow::Result; +use std::sync::Arc; +use tokio::net::TcpListener; +use tokio::sync::{broadcast, mpsc, RwLock}; +use std::collections::HashMap; +use crate::config::Config; +use crate::sources::{DataSource, UdpSource, CsvSource, ShellSource}; +use crate::storage::CircularBuffer; +use crate::server::SharedState; +use std::time::Duration; +use rmon_common::signal::{SignalId, SignalInfo, Sample}; +use std::path::PathBuf; +use notify::{Watcher, RecursiveMode, Config as NotifyConfig}; +use tokio::time::sleep; + +pub async fn run_agent(initial_config: Config, addr: &str, config_path: PathBuf) -> Result<()> { + tracing::info!("Starting RMon Agent core on {}...", addr); + + let (reload_tx, _reload_rx) = broadcast::channel::<()>(10); + let (tx, _rx) = broadcast::channel(10000); + let (sample_tx, mut sample_rx) = mpsc::channel::<(SignalId, Sample)>(1000); + + let signals_lock = Arc::new(RwLock::new(HashMap::::new())); + let storage_lock = Arc::new(RwLock::new(HashMap::>::new())); + + let shared_state = Arc::new(SharedState { + signals: signals_lock.clone(), + storage: storage_lock.clone(), + tx: tx.clone(), + reload_tx: reload_tx.clone(), + config_path: config_path.clone(), + }); + + let config_path_clone = config_path.clone(); + let reload_tx_clone = reload_tx.clone(); + + // File watcher for hot-reload + std::thread::spawn(move || { + let (tx, rx) = std::sync::mpsc::channel(); + if let Ok(mut watcher) = notify::RecommendedWatcher::new(tx, NotifyConfig::default()) { + let _ = watcher.watch(&config_path_clone, RecursiveMode::NonRecursive); + for res in rx { + if let Ok(_) = res { + tracing::info!("File watcher detected config change"); + let _ = reload_tx_clone.send(()); + } + } + } + }); + + // Start dispatcher task + let storage_dispatch = storage_lock.clone(); + let tx_dispatch = tx.clone(); + let data_dir = initial_config.storage.data_dir.clone(); + tokio::spawn(async move { + let mut continuous_writers: HashMap = HashMap::new(); + // Move session_start to be determined per signal or source if needed, + // but keep a process-level one as a default for consistent grouping. + let session_start = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0); + + while let Some((id, sample)) = sample_rx.recv().await { + let buf = { + let storage = storage_dispatch.read().await; + storage.get(&id).cloned() + }; + + if let Some(buf) = buf { + buf.push(sample).await; + } + + if !continuous_writers.contains_key(&id) { + let source_name = id.split("://").nth(1).and_then(|s| s.split('/').next()).unwrap_or("unknown"); + if let Ok(writer) = crate::storage::ContinuousStorage::create( + data_dir.clone(), + source_name, + &id, + session_start + ) { + continuous_writers.insert(id.clone(), writer); + } + } + + if let Some(writer) = continuous_writers.get_mut(&id) { + let _ = writer.append(sample); + let _ = writer.flush(); + } + + let _ = tx_dispatch.send((id, sample)); + } + }); + + // Start TCP server + let server_state = shared_state.clone(); + let server_addr = addr.to_string(); + tokio::spawn(async move { + match TcpListener::bind(&server_addr).await { + Ok(listener) => { + tracing::info!("Server listening on {}", server_addr); + loop { + if let Ok((stream, _)) = listener.accept().await { + let state = server_state.clone(); + tokio::spawn(async move { + let _ = server::handle_session(stream, state).await; + }); + } + } + } + Err(e) => tracing::error!("Failed to bind server: {}", e), + } + }); + + let mut current_config = initial_config; + let mut reload_rx = reload_tx.subscribe(); + + loop { + tracing::info!("Initializing data sources..."); + let mut new_signals = HashMap::new(); + let mut new_storage = HashMap::new(); + let mut sources: Vec> = Vec::new(); + + for source_config in ¤t_config.sources { + let source: Box = match source_config.source_type.as_str() { + "udp" => Box::new(UdpSource::new(source_config.clone())), + "csv" => Box::new(CsvSource::new(source_config.clone())), + "shell" => Box::new(ShellSource::new(source_config.clone())), + _ => continue, + }; + + for sig in source.signals() { + new_signals.insert(sig.id.clone(), sig.clone()); + new_storage.insert(sig.id.clone(), Arc::new(CircularBuffer::new(Duration::from_secs(3600)))); + } + sources.push(source); + } + + tracing::info!("Updating shared signal map ({} signals)", new_signals.len()); + { + let mut sigs = signals_lock.write().await; + *sigs = new_signals; + let mut stors = storage_lock.write().await; + *stors = new_storage; + } + + let (stop_tx, _) = broadcast::channel::<()>(1); + + for source in sources { + let s_tx = sample_tx.clone(); + let mut s_stop = stop_tx.subscribe(); + tokio::spawn(async move { + tokio::select! { + res = source.run(s_tx) => { + if let Err(e) = res { + tracing::error!("Source failed: {}", e); + } + } + _ = s_stop.recv() => { + tracing::debug!("Source stopped by reload"); + } + } + }); + } + + let _ = reload_rx.recv().await; + tracing::info!("Reload signal received, refreshing config..."); + drop(stop_tx); // Stop sources + + sleep(Duration::from_millis(200)).await; + if let Ok(content) = std::fs::read_to_string(&config_path) { + match toml::from_str::(&content) { + Ok(new_cfg) => { + tracing::info!("Config reloaded successfully"); + current_config = new_cfg; + } + Err(e) => { + tracing::error!("Failed to parse updated config: {}", e); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::StorageConfig; + + #[test] + fn test_agent_config_struct() { + let c = Config { + storage: StorageConfig { + data_dir: PathBuf::from("/tmp"), + default_mode: "c".to_string(), + default_window: "1".to_string(), + }, + sources: vec![], + }; + assert_eq!(c.sources.len(), 0); + } + + #[tokio::test] + async fn test_dispatcher_routing() { + let (tx, mut rx) = broadcast::channel::<(SignalId, Sample)>(10); + let (_stx, _srx) = mpsc::channel::<(SignalId, Sample)>(10); + let msg = ("s1".to_string(), Sample { timestamp: 1, value: 1.0 }); + tx.send(msg.clone()).unwrap(); + let received = rx.recv().await.unwrap(); + assert_eq!(received.0, "s1"); + } + + #[test] + fn test_source_factory_logic() { + let t = "udp"; + assert_eq!(t, "udp"); + } +} diff --git a/rmon-agent/src/main.rs b/rmon-agent/src/main.rs new file mode 100644 index 0000000..df200ab --- /dev/null +++ b/rmon-agent/src/main.rs @@ -0,0 +1,89 @@ +use anyhow::Result; +use rmon_agent::config::Config; +use rmon_agent::run_agent; +use std::path::PathBuf; +use std::fs; +use clap::Parser; +use tracing_subscriber::prelude::*; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Bind address + #[arg(short, long, default_value = "127.0.0.1")] + bind: String, + + /// Port to listen on + #[arg(short, long, default_value_t = 7891)] + port: u16, + + /// Path to config file + #[arg(short, long)] + config: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + let rmon_dir = PathBuf::from(&home).join(".rmon"); + let _ = fs::create_dir_all(&rmon_dir); + + let log_path = rmon_dir.join("agent.log"); + + // Open in append mode + let file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path)?; + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) + .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) + .with(tracing_subscriber::fmt::layer().with_writer(file).with_ansi(false)) + .init(); + + tracing::info!("--- RMon Agent Starting ---"); + tracing::info!("Log file: {:?}", log_path); + + let args = Args::parse(); + let config_path = args.config.unwrap_or_else(|| rmon_dir.join("config.toml")); + + let config: Config = if config_path.exists() { + tracing::info!("Loading config from {:?}", config_path); + let content = fs::read_to_string(&config_path)?; + toml::from_str(&content)? + } else { + tracing::info!("Using default configuration"); + let default_config = r#" +[storage] +data_dir = "/tmp/rmon-data" +default_mode = "circular" +default_window = "1h" + +[[sources]] +type = "shell" +name = "system" +command = "echo \"cpu_temp: $(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null | awk '{print $1/1000}' || echo 0)\"" +poll_interval = "1s" +[sources.timestamp] +format = "none" +[[sources.signals]] +id = "cpu_temp" +label = "CPU Temperature" +unit = "C" +scale = 1.0 +offset = 0.0 +regex = "cpu_temp: ([0-9.]+)" +path = ["system", "thermal"] +"#; + if let Some(parent) = config_path.parent() { + let _ = fs::create_dir_all(parent); + } + let _ = fs::write(&config_path, default_config); + toml::from_str(default_config)? + }; + + let addr = format!("{}:{}", args.bind, args.port); + tracing::info!("Binding to {}", addr); + run_agent(config, &addr, config_path).await +} diff --git a/rmon-agent/src/server/mod.rs b/rmon-agent/src/server/mod.rs new file mode 100644 index 0000000..e8b8d8a --- /dev/null +++ b/rmon-agent/src/server/mod.rs @@ -0,0 +1,2 @@ +pub mod session; +pub use self::session::{handle_session, SharedState}; diff --git a/rmon-agent/src/server/session.rs b/rmon-agent/src/server/session.rs new file mode 100644 index 0000000..b57dcbe --- /dev/null +++ b/rmon-agent/src/server/session.rs @@ -0,0 +1,250 @@ +use anyhow::Result; +use tokio::net::TcpStream; +use tokio_util::codec::Framed; +use futures::{StreamExt, SinkExt}; +use std::sync::Arc; +use tokio::sync::{broadcast, RwLock}; +use rmon_common::protocol::{ClientMessage, AgentMessage, BincodeCodec, PROTOCOL_VERSION, SignalBatch}; +use rmon_common::signal::{SignalId, SignalInfo, Sample}; +use crate::storage::CircularBuffer; +use std::collections::HashMap; +use bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; +use std::io; +use std::path::PathBuf; + +/// A custom codec that decodes ClientMessage and encodes AgentMessage +pub struct AgentCodec; + +impl Decoder for AgentCodec { + type Item = ClientMessage; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let mut codec = BincodeCodec::::default(); + codec.decode(src) + } +} + +impl Encoder for AgentCodec { + type Error = io::Error; + + fn encode(&mut self, item: AgentMessage, dst: &mut BytesMut) -> Result<(), Self::Error> { + let mut codec = BincodeCodec::::default(); + codec.encode(item, dst) + } +} + +pub struct SharedState { + pub signals: Arc>>, + pub storage: Arc>>>, + pub tx: broadcast::Sender<(SignalId, Sample)>, + pub reload_tx: broadcast::Sender<()>, + pub config_path: PathBuf, +} + +pub async fn handle_session( + stream: TcpStream, + state: Arc, +) -> Result<()> { + let addr = stream.peer_addr().map(|a| a.to_string()).unwrap_or_else(|_| "unknown".to_string()); + tracing::info!("[{}] Session established", addr); + + let mut framed = Framed::new(stream, AgentCodec); + + // 1. Handshake + match framed.next().await { + Some(Ok(ClientMessage::Handshake { protocol_version, client_version })) => { + if protocol_version == PROTOCOL_VERSION { + tracing::info!("[{}] Handshake ok (client: {})", addr, client_version); + framed.send(AgentMessage::HandshakeAck { + protocol_version: PROTOCOL_VERSION, + agent_version: env!("CARGO_PKG_VERSION").to_string(), + accepted: true, + reject_reason: None, + }).await?; + } else { + tracing::warn!("[{}] Handshake failed: version mismatch", addr); + let _ = framed.send(AgentMessage::HandshakeAck { + protocol_version: PROTOCOL_VERSION, + agent_version: env!("CARGO_PKG_VERSION").to_string(), + accepted: false, + reject_reason: Some("Version mismatch".to_string()), + }).await; + return Ok(()); + } + } + Some(Ok(msg)) => { + tracing::warn!("[{}] Expected handshake, got {:?}", addr, msg); + return Ok(()); + } + _ => { + tracing::warn!("[{}] Handshake failed or connection closed", addr); + return Ok(()); + } + } + + let mut rx = state.tx.subscribe(); + let mut subscriptions = Vec::::new(); + + loop { + tokio::select! { + msg_res = framed.next() => { + match msg_res { + Some(Ok(msg)) => { + match msg { + ClientMessage::ListSignals => { + let signals: Vec = { + let sigs = state.signals.read().await; + sigs.values().cloned().collect() + }; + tracing::info!("[{}] Sending signal list ({} signals)", addr, signals.len()); + framed.send(AgentMessage::SignalList { signals }).await?; + } + ClientMessage::GetConfig => { + tracing::info!("[{}] Fetching config...", addr); + match std::fs::read_to_string(&state.config_path) { + Ok(config_toml) => { + framed.send(AgentMessage::ConfigReport { config_toml }).await?; + } + Err(e) => { + tracing::error!("[{}] Config read failed: {}", addr, e); + framed.send(AgentMessage::Error { context: "GetConfig".to_string(), message: e.to_string() }).await?; + } + } + } + ClientMessage::UpdateConfig { config_toml } => { + tracing::info!("[{}] Updating config...", addr); + if let Err(e) = std::fs::write(&state.config_path, &config_toml) { + tracing::error!("[{}] Config write failed: {}", addr, e); + framed.send(AgentMessage::Error { context: "UpdateConfig".to_string(), message: e.to_string() }).await?; + } else { + let _ = state.reload_tx.send(()); + } + } + ClientMessage::Subscribe { ids, .. } => { + tracing::info!("[{}] Subscribing to {:?}", addr, ids); + subscriptions.extend(ids); + } + ClientMessage::Unsubscribe { ids } => { + tracing::info!("[{}] Unsubscribing from {:?}", addr, ids); + subscriptions.retain(|id| !ids.contains(id)); + } + _ => { + tracing::debug!("[{}] Unhandled client message: {:?}", addr, msg); + } + } + } + Some(Err(e)) => { + tracing::error!("[{}] Protocol error: {}", addr, e); + break; + } + None => { + tracing::info!("[{}] Connection closed by client", addr); + break; + } + } + } + sample_res = rx.recv() => { + if let Ok((id, sample)) = sample_res { + if subscriptions.contains(&id) { + if let Err(e) = framed.send(AgentMessage::DataBatch { + batches: vec![SignalBatch { id, samples: vec![sample] }] + }).await { + tracing::error!("[{}] Failed to send data: {}", addr, e); + break; + } + } + } + } + } + } + + tracing::info!("[{}] Session terminated", addr); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::sync::broadcast; + use rmon_common::protocol::PROTOCOL_VERSION; + use std::sync::Arc; + use tokio::sync::RwLock; + use tempfile::tempdir; + + #[tokio::test] + async fn test_shared_state_init() { + let (tx, _) = broadcast::channel(10); + let (rtx, _) = broadcast::channel(1); + let dir = tempdir().unwrap(); + let config_path = dir.path().join("config.toml"); + + let state = SharedState { + signals: Arc::new(RwLock::new(HashMap::new())), + storage: Arc::new(RwLock::new(HashMap::new())), + tx, + reload_tx: rtx, + config_path, + }; + + assert!(state.signals.read().await.is_empty()); + } + + #[test] + fn test_codec_defaults() { + let _ = AgentCodec; + } + + #[test] + fn test_subscription_list() { + let mut subs = Vec::::new(); + subs.push("s1".to_string()); + subs.push("s2".to_string()); + assert!(subs.contains(&"s1".to_string())); + subs.retain(|id| id != "s1"); + assert!(!subs.contains(&"s1".to_string())); + } + + #[test] + fn test_shared_state_fields() { + let (tx, _) = broadcast::channel(1); + let (rtx, _) = broadcast::channel(1); + let s = SharedState { + signals: Arc::new(RwLock::new(HashMap::new())), + storage: Arc::new(RwLock::new(HashMap::new())), + tx, reload_tx: rtx, config_path: PathBuf::from("."), + }; + assert!(s.config_path.exists()); + } + + #[test] + fn test_agent_message_variants() { + let m1 = AgentMessage::HandshakeAck { + protocol_version: PROTOCOL_VERSION, + agent_version: "1".to_string(), + accepted: true, + reject_reason: None, + }; + let _ = bincode::serialize(&m1).unwrap(); + } + + #[test] + fn test_status_report_serialization() { + use rmon_common::protocol::SourceStatus; + use rmon_common::signal::SourceType; + let report = AgentMessage::StatusReport { + agent_version: "1".to_string(), + hostname: "h".to_string(), + uptime_secs: 10, + sources: vec![SourceStatus { + name: "s".to_string(), + source_type: SourceType::Udp, + connected: true, + last_sample_ns: None, + error: None, + }], + }; + let _ = bincode::serialize(&report).unwrap(); + } +} diff --git a/rmon-agent/src/sources/csv.rs b/rmon-agent/src/sources/csv.rs new file mode 100644 index 0000000..1040407 --- /dev/null +++ b/rmon-agent/src/sources/csv.rs @@ -0,0 +1,217 @@ +use anyhow::Result; +use rmon_common::signal::{SignalId, SignalInfo, Sample, SourceType}; +use tokio::sync::mpsc; +use async_trait::async_trait; +use crate::sources::datasource::DataSource; +use crate::config::SourceConfig; +use std::time::Duration; +use tokio::time::sleep; +use std::fs::File; +use std::io::{BufRead, BufReader, Seek, SeekFrom}; +use chrono::{DateTime, Utc, NaiveDateTime}; + +pub struct CsvSource { + name: String, + signals: Vec, + config: SourceConfig, +} + +impl CsvSource { + pub fn new(config: SourceConfig) -> Self { + let signals = config.signals.iter().map(|s| { + SignalInfo { + id: format!("csv://{}/{}", config.name, s.id), + label: s.label.clone(), + unit: s.unit.clone(), + scale: s.scale, + offset: s.offset, + sampling_period: None, + source_type: SourceType::Csv, + path: s.path.clone(), + } + }).collect(); + + Self { + name: config.name.clone(), + signals, + config, + } + } + + fn parse_timestamp(&self, value: &str) -> i64 { + let value = value.trim(); + match self.config.timestamp.format.as_str() { + "nanoseconds_since_epoch" => value.parse::().unwrap_or(0), + "milliseconds_since_epoch" => value.parse::().unwrap_or(0) * 1_000_000, + "seconds_since_epoch" => value.parse::().map(|v| (v * 1e9) as i64).unwrap_or(0), + "none" => Utc::now().timestamp_nanos_opt().unwrap_or(0), + format_str => { + if let Ok(naive) = NaiveDateTime::parse_from_str(value, format_str) { + naive.and_utc().timestamp_nanos_opt().unwrap_or(0) + } else if let Ok(dt) = DateTime::parse_from_rfc3339(value) { + dt.timestamp_nanos_opt().unwrap_or(0) + } else { + Utc::now().timestamp_nanos_opt().unwrap_or(0) + } + } + } + } +} + +#[async_trait] +impl DataSource for CsvSource { + fn name(&self) -> &str { + &self.name + } + + fn signals(&self) -> Vec { + self.signals.clone() + } + + async fn run(self: Box, tx: mpsc::Sender<(SignalId, Sample)>) -> Result<()> { + let path = self.config.path.clone().ok_or_else(|| anyhow::anyhow!("CSV path missing"))?; + let poll_interval = if let Some(ref interval_str) = self.config.poll_interval { + if interval_str.ends_with("ms") { + Duration::from_millis(interval_str.trim_end_matches("ms").parse().unwrap_or(100)) + } else { + Duration::from_secs(interval_str.trim_end_matches("s").parse().unwrap_or(1)) + } + } else { + Duration::from_millis(500) + }; + + let mut pos = 0u64; + + loop { + if let Ok(mut file) = File::open(&path) { + let len = file.metadata()?.len(); + + if len < pos { + tracing::info!("CSV file truncated or rotated, resetting position"); + pos = 0; + } + + if len > pos { + file.seek(SeekFrom::Start(pos))?; + let mut reader = BufReader::new(file); + let mut line = String::new(); + + while reader.read_line(&mut line)? > 0 { + let bytes_read = line.len() as u64; + let trimmed = line.trim(); + + if !trimmed.is_empty() { + // If we are at start of file, skip the header + if pos == 0 { + pos += bytes_read; + line.clear(); + continue; + } + + let parts: Vec<&str> = trimmed.split(',').collect(); + + // 1. Get timestamp + let timestamp = if let Some(col_idx) = self.config.timestamp.column.as_ref().and_then(|c| c.parse::().ok()) { + if let Some(val) = parts.get(col_idx) { + self.parse_timestamp(val) + } else { + Utc::now().timestamp_nanos_opt().unwrap_or(0) + } + } else { + Utc::now().timestamp_nanos_opt().unwrap_or(0) + }; + + // 2. Map signals + for signal_config in &self.config.signals { + if let Some(col_idx) = signal_config.column.as_ref().and_then(|c| c.parse::().ok()) { + if let Some(raw_str) = parts.get(col_idx) { + if let Ok(raw_val) = raw_str.trim().parse::() { + let value = raw_val * signal_config.scale + signal_config.offset; + let id = format!("csv://{}/{}", self.name, signal_config.id); + let _ = tx.send((id, Sample { timestamp, value })).await; + } + } + } + } + } + pos += bytes_read; + line.clear(); + } + } + } + sleep(poll_interval).await; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{SourceConfig, TimestampConfig, SignalSourceConfig}; + use std::io::Write; + use tempfile::tempdir; + use std::path::PathBuf; + + #[test] + fn test_csv_metadata() { + let config = SourceConfig { + source_type: "csv".to_string(), + name: "tc".to_string(), + bind: None, path: Some(PathBuf::from("test.csv")), addr: None, command: None, poll_interval: None, + timestamp: TimestampConfig { byte_offset: None, byte_type: None, column: None, format: "none".to_string(), regex: None }, + signals: vec![SignalSourceConfig { + id: "s1".to_string(), label: "L1".to_string(), unit: "V".to_string(), scale: 1.0, offset: 0.0, + byte_offset: None, byte_type: None, column: None, pv: None, regex: None, path: vec![], + }], + }; + let source = CsvSource::new(config); + assert_eq!(source.name(), "tc"); + assert_eq!(source.signals()[0].id, "csv://tc/s1"); + } + + #[tokio::test] + async fn test_csv_batch_parsing() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test_batch.csv"); + let mut file = std::fs::File::create(&file_path).unwrap(); + + // Write header and two lines immediately + writeln!(file, "time,val1").unwrap(); + writeln!(file, "100,10.0").unwrap(); + writeln!(file, "200,20.0").unwrap(); + file.sync_all().unwrap(); + + let config = SourceConfig { + source_type: "csv".to_string(), + name: "test_batch".to_string(), + bind: None, + path: Some(file_path), + addr: None, + command: None, + poll_interval: Some("10ms".to_string()), + timestamp: TimestampConfig { + byte_offset: None, byte_type: None, + column: Some("0".to_string()), + format: "nanoseconds_since_epoch".to_string(), + regex: None + }, + signals: vec![ + SignalSourceConfig { + id: "s1".to_string(), label: "L1".to_string(), unit: "V".to_string(), scale: 1.0, offset: 0.0, + byte_offset: None, byte_type: None, column: Some("1".to_string()), pv: None, regex: None, path: vec![], + } + ], + }; + + let source = Box::new(CsvSource::new(config)); + let (tx, mut rx) = mpsc::channel(10); + tokio::spawn(async move { let _ = source.run(tx).await; }); + + // Should receive BOTH lines from initial load + let msg1 = tokio::time::timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap(); + assert_eq!(msg1.1.value, 10.0); + + let msg2 = tokio::time::timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap(); + assert_eq!(msg2.1.value, 20.0); + } +} diff --git a/rmon-agent/src/sources/datasource.rs b/rmon-agent/src/sources/datasource.rs new file mode 100644 index 0000000..7d33a26 --- /dev/null +++ b/rmon-agent/src/sources/datasource.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use rmon_common::signal::{SignalId, SignalInfo, Sample}; +use tokio::sync::mpsc; +use async_trait::async_trait; + +#[async_trait] +pub trait DataSource: Send + 'static { + /// Human-readable name (from config `name` field). + fn name(&self) -> &str; + + /// Full list of signals this source provides. + fn signals(&self) -> Vec; + + /// Start producing samples. Sends to `tx` until the returned future resolves or `tx` is dropped. + /// This runs in its own tokio task. + async fn run(self: Box, tx: mpsc::Sender<(SignalId, Sample)>) -> Result<()>; +} diff --git a/rmon-agent/src/sources/mod.rs b/rmon-agent/src/sources/mod.rs new file mode 100644 index 0000000..8850662 --- /dev/null +++ b/rmon-agent/src/sources/mod.rs @@ -0,0 +1,26 @@ +pub mod datasource; +pub mod udp; +pub mod csv; +pub mod shell; + +pub use self::datasource::DataSource; +pub use self::udp::UdpSource; +pub use self::csv::CsvSource; +pub use self::shell::ShellSource; + +#[cfg(test)] +mod tests { + use crate::config::{SourceConfig, TimestampConfig}; + + #[test] + fn test_source_types() { + let sc = SourceConfig { + source_type: "csv".to_string(), + name: "n".to_string(), + bind: None, path: None, addr: None, command: None, poll_interval: None, + timestamp: TimestampConfig { byte_offset: None, byte_type: None, column: None, format: "n".to_string(), regex: None }, + signals: vec![], + }; + assert_eq!(sc.source_type, "csv"); + } +} diff --git a/rmon-agent/src/sources/shell.rs b/rmon-agent/src/sources/shell.rs new file mode 100644 index 0000000..7608e35 --- /dev/null +++ b/rmon-agent/src/sources/shell.rs @@ -0,0 +1,149 @@ +use anyhow::Result; +use rmon_common::signal::{SignalId, SignalInfo, Sample, SourceType}; +use tokio::sync::mpsc; +use async_trait::async_trait; +use crate::sources::datasource::DataSource; +use crate::config::SourceConfig; +use std::time::Duration; +use tokio::time::sleep; +use tokio::process::Command; +use regex::Regex; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub struct ShellSource { + name: String, + signals: Vec, + config: SourceConfig, +} + +impl ShellSource { + pub fn new(config: SourceConfig) -> Self { + let signals = config.signals.iter().map(|s| { + SignalInfo { + id: format!("shell://{}/{}", config.name, s.id), + label: s.label.clone(), + unit: s.unit.clone(), + scale: s.scale, + offset: s.offset, + sampling_period: None, + source_type: SourceType::Custom("shell".to_string()), + path: s.path.clone(), + } + }).collect(); + + Self { + name: config.name.clone(), + signals, + config, + } + } +} + +#[async_trait] +impl DataSource for ShellSource { + fn name(&self) -> &str { + &self.name + } + + fn signals(&self) -> Vec { + self.signals.clone() + } + + async fn run(self: Box, tx: mpsc::Sender<(SignalId, Sample)>) -> Result<()> { + let command_str = self.config.command.clone().ok_or_else(|| anyhow::anyhow!("Shell command missing"))?; + let poll_interval = if let Some(ref interval_str) = self.config.poll_interval { + if interval_str.ends_with("ms") { + Duration::from_millis(interval_str.trim_end_matches("ms").parse()?) + } else if interval_str.ends_with("s") { + Duration::from_secs(interval_str.trim_end_matches("s").parse()?) + } else { + Duration::from_secs(1) + } + } else { + Duration::from_secs(1) + }; + + let signal_regexes: Vec<(String, Regex)> = self.config.signals.iter() + .filter_map(|s| { + s.regex.as_ref().and_then(|r| Regex::new(r).ok().map(|re| (s.id.clone(), re))) + }) + .collect(); + + tracing::info!("Shell source '{}' starting with {} regexes", self.name, signal_regexes.len()); + + loop { + match Command::new("sh").arg("-c").arg(&command_str).output().await { + Ok(output) => { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as i64; + let mut matched = false; + + for (id_local, regex) in &signal_regexes { + if let Some(caps) = regex.captures(&stdout) { + if let Some(m) = caps.get(1) { + if let Ok(raw_val) = m.as_str().parse::() { + if let Some(sig_config) = self.config.signals.iter().find(|s| s.id == *id_local) { + matched = true; + let value = raw_val * sig_config.scale + sig_config.offset; + let id = format!("shell://{}/{}", self.name, id_local); + let _ = tx.send((id, Sample { timestamp, value })).await; + } + } + } + } + } + if !matched { + tracing::warn!("Shell command '{}' succeeded but output didn't match any regex: '{}'", self.name, stdout.trim()); + } + } else { + tracing::error!("Shell command '{}' failed: {}", self.name, String::from_utf8_lossy(&output.stderr)); + } + } + Err(e) => tracing::error!("Failed to execute shell command '{}': {}", self.name, e), + } + sleep(poll_interval).await; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{SourceConfig, TimestampConfig, SignalSourceConfig}; + + #[test] + fn test_shell_metadata() { + let config = SourceConfig { + source_type: "shell".to_string(), + name: "ts".to_string(), + bind: None, path: None, addr: None, + command: Some("echo 42".to_string()), + poll_interval: Some("1s".to_string()), + timestamp: TimestampConfig { byte_offset: None, byte_type: None, column: None, format: "none".to_string(), regex: None }, + signals: vec![SignalSourceConfig { + id: "s1".to_string(), label: "L1".to_string(), unit: "V".to_string(), scale: 1.0, offset: 0.0, + byte_offset: None, byte_type: None, column: None, pv: None, + regex: Some("([0-9]+)".to_string()), + path: vec![], + }], + }; + let source = ShellSource::new(config); + assert_eq!(source.name(), "ts"); + assert_eq!(source.signals()[0].id, "shell://ts/s1"); + } + + #[test] + fn test_duration_parsing_logic() { + let s = "100ms"; + let ms = s.trim_end_matches("ms").parse::().unwrap(); + assert_eq!(ms, 100); + } + + #[test] + fn test_shell_regex_logic() { + let re = Regex::new("val: ([0-9]+)").unwrap(); + let caps = re.captures("val: 123").unwrap(); + assert_eq!(caps.get(1).unwrap().as_str(), "123"); + } +} diff --git a/rmon-agent/src/sources/udp.rs b/rmon-agent/src/sources/udp.rs new file mode 100644 index 0000000..71add9c --- /dev/null +++ b/rmon-agent/src/sources/udp.rs @@ -0,0 +1,128 @@ +use anyhow::Result; +use rmon_common::signal::{SignalId, SignalInfo, Sample, SourceType}; +use tokio::sync::mpsc; +use async_trait::async_trait; +use tokio::net::UdpSocket; +use crate::sources::datasource::DataSource; +use crate::config::SourceConfig; +use byteorder::{ByteOrder, LittleEndian, BigEndian}; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub struct UdpSource { + name: String, + pub(crate) bind_addr: String, + signals: Vec, + config: SourceConfig, +} + +impl UdpSource { + pub fn new(config: SourceConfig) -> Self { + let signals = config.signals.iter().map(|s| { + SignalInfo { + id: format!("udp://{}/{}", config.name, s.id), + label: s.label.clone(), + unit: s.unit.clone(), + scale: s.scale, + offset: s.offset, + sampling_period: None, + source_type: SourceType::Udp, + path: s.path.clone(), + } + }).collect(); + + Self { + name: config.name.clone(), + bind_addr: config.bind.clone().unwrap_or_else(|| "0.0.0.0:0".to_string()), + signals, + config, + } + } +} + +#[async_trait] +impl DataSource for UdpSource { + fn name(&self) -> &str { + &self.name + } + + fn signals(&self) -> Vec { + self.signals.clone() + } + + async fn run(self: Box, tx: mpsc::Sender<(SignalId, Sample)>) -> Result<()> { + let socket = UdpSocket::bind(&self.bind_addr).await?; + let mut buf = [0u8; 65535]; + + loop { + let (len, _addr) = socket.recv_from(&mut buf).await?; + let payload = &buf[..len]; + + let timestamp = if let Some(ts_offset) = self.config.timestamp.byte_offset { + match self.config.timestamp.byte_type.as_deref() { + Some("u64_le") => { + let ts = LittleEndian::read_u64(&payload[ts_offset..ts_offset+8]); + match self.config.timestamp.format.as_str() { + "microseconds_since_epoch" => (ts * 1000) as i64, + "nanoseconds_since_epoch" => ts as i64, + "seconds_since_epoch" => (ts * 1_000_000_000) as i64, + _ => SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as i64, + } + } + _ => SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as i64, + } + } else { + SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as i64 + }; + + for signal_config in &self.config.signals { + if let Some(offset) = signal_config.byte_offset { + if payload.len() < offset + 4 { continue; } + let raw_val = match signal_config.byte_type.as_deref() { + Some("f32_le") => LittleEndian::read_f32(&payload[offset..offset+4]) as f64, + Some("f32_be") => BigEndian::read_f32(&payload[offset..offset+4]) as f64, + Some("f64_le") => if payload.len() >= offset+8 { LittleEndian::read_f64(&payload[offset..offset+8]) } else { 0.0 }, + Some("f64_be") => if payload.len() >= offset+8 { BigEndian::read_f64(&payload[offset..offset+8]) } else { 0.0 }, + _ => 0.0, + }; + + let value = raw_val * signal_config.scale + signal_config.offset; + let id = format!("udp://{}/{}", self.name, signal_config.id); + let _ = tx.send((id, Sample { timestamp, value })).await; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{SourceConfig, TimestampConfig, SignalSourceConfig}; + + #[tokio::test] + async fn test_udp_metadata() { + let config = SourceConfig { + source_type: "udp".to_string(), + name: "tu".to_string(), + bind: None, path: None, addr: None, command: None, poll_interval: None, + timestamp: TimestampConfig { byte_offset: None, byte_type: None, column: None, format: "none".to_string(), regex: None }, + signals: vec![SignalSourceConfig { + id: "s1".to_string(), label: "L1".to_string(), unit: "V".to_string(), scale: 1.0, offset: 0.0, + byte_offset: Some(0), byte_type: Some("f32_le".to_string()), column: None, pv: None, regex: None, path: vec![], + }], + }; + let source = UdpSource::new(config); + assert_eq!(source.name(), "tu"); + assert_eq!(source.signals()[0].id, "udp://tu/s1"); + assert_eq!(source.bind_addr, "0.0.0.0:0"); + } + + #[test] + fn test_udp_parsing_logic() { + use byteorder::{ByteOrder, LittleEndian}; + let mut buf = [0u8; 4]; + LittleEndian::write_f32(&mut buf, 1.23); + let val = LittleEndian::read_f32(&buf) as f64; + assert!((val - 1.23).abs() < 0.001); + } +} diff --git a/rmon-agent/src/storage/circular.rs b/rmon-agent/src/storage/circular.rs new file mode 100644 index 0000000..74ee0a3 --- /dev/null +++ b/rmon-agent/src/storage/circular.rs @@ -0,0 +1,77 @@ +use std::collections::VecDeque; +use std::time::Duration; +use tokio::sync::RwLock; +use rmon_common::signal::Sample; + +pub struct CircularBuffer { + samples: RwLock>, + window: Duration, +} + +impl CircularBuffer { + pub fn new(window: Duration) -> Self { + Self { + samples: RwLock::new(VecDeque::new()), + window, + } + } + + pub async fn push(&self, sample: Sample) { + let mut samples = self.samples.write().await; + samples.push_back(sample); + + let cutoff = sample.timestamp - self.window.as_nanos() as i64; + while let Some(oldest) = samples.front() { + if oldest.timestamp < cutoff { + samples.pop_front(); + } else { + break; + } + } + } + + pub async fn get_samples(&self, from_ns: Option) -> Vec { + let samples = self.samples.read().await; + if let Some(from) = from_ns { + samples.iter().filter(|s| s.timestamp >= from).cloned().collect() + } else { + samples.iter().cloned().collect() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[tokio::test] + async fn test_circular_trimming() { + let window = Duration::from_nanos(100); + let buf = CircularBuffer::new(window); + + // Push sample at t=1000 + buf.push(Sample { timestamp: 1000, value: 1.0 }).await; + // Push sample at t=1050 (within window) + buf.push(Sample { timestamp: 1050, value: 2.0 }).await; + // Push sample at t=1110 (cutoff is now 1110-100 = 1010, so t=1000 should be dropped) + buf.push(Sample { timestamp: 1110, value: 3.0 }).await; + + let samples = buf.get_samples(None).await; + assert_eq!(samples.len(), 2); + assert_eq!(samples[0].timestamp, 1050); + assert_eq!(samples[1].timestamp, 1110); + } + + #[tokio::test] + async fn test_circular_query_range() { + let buf = CircularBuffer::new(Duration::from_nanos(1000)); + buf.push(Sample { timestamp: 100, value: 1.0 }).await; + buf.push(Sample { timestamp: 200, value: 2.0 }).await; + buf.push(Sample { timestamp: 300, value: 3.0 }).await; + + let samples = buf.get_samples(Some(200)).await; + assert_eq!(samples.len(), 2); + assert_eq!(samples[0].timestamp, 200); + } +} diff --git a/rmon-agent/src/storage/continuous.rs b/rmon-agent/src/storage/continuous.rs new file mode 100644 index 0000000..61e0d1a --- /dev/null +++ b/rmon-agent/src/storage/continuous.rs @@ -0,0 +1,82 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Write, BufWriter}; +use std::path::PathBuf; +use anyhow::Result; +use rmon_common::signal::Sample; +use byteorder::{LittleEndian, WriteBytesExt}; + +pub struct ContinuousStorage { + writer: BufWriter, + _path: PathBuf, +} + +impl ContinuousStorage { + pub fn create(data_dir: PathBuf, source_name: &str, signal_id: &str, session_start_ns: i64) -> Result { + let mut path = data_dir; + path.push(source_name); + path.push(signal_id.replace(":", "_").replace("/", "_")); // Escape signal ID for filesystem + std::fs::create_dir_all(&path)?; + + path.push(format!("{}.bin", session_start_ns)); + + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&path)?; + + Ok(Self { + writer: BufWriter::new(file), + _path: path, + }) + } + + pub fn append(&mut self, sample: Sample) -> Result<()> { + self.writer.write_i64::(sample.timestamp)?; + self.writer.write_f64::(sample.value)?; + Ok(()) + } + + pub fn flush(&mut self) -> Result<()> { + self.writer.flush()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + use std::fs; + + #[test] + fn test_continuous_storage_creation() { + let dir = tempdir().unwrap(); + let res = ContinuousStorage::create( + dir.path().to_path_buf(), + "src", + "sig1", + 12345 + ); + assert!(res.is_ok()); + } + + #[test] + fn test_continuous_append() { + let dir = tempdir().unwrap(); + let mut storage = ContinuousStorage::create( + dir.path().to_path_buf(), + "src", + "sig1", + 12345 + ).unwrap(); + + let sample = Sample { timestamp: 100, value: 42.0 }; + storage.append(sample).unwrap(); + storage.flush().unwrap(); + + // Verify file exists and has size 16 bytes (i64 + f64) + let path = dir.path().join("src/sig1/12345.bin"); + assert!(path.exists()); + assert_eq!(fs::metadata(path).unwrap().len(), 16); + } +} diff --git a/rmon-agent/src/storage/mod.rs b/rmon-agent/src/storage/mod.rs new file mode 100644 index 0000000..fbe975c --- /dev/null +++ b/rmon-agent/src/storage/mod.rs @@ -0,0 +1,5 @@ +pub mod circular; +pub mod continuous; + +pub use self::circular::CircularBuffer; +pub use self::continuous::ContinuousStorage; diff --git a/rmon-agent/tests/agent_test.rs b/rmon-agent/tests/agent_test.rs new file mode 100644 index 0000000..17eb2a1 --- /dev/null +++ b/rmon-agent/tests/agent_test.rs @@ -0,0 +1,200 @@ +use anyhow::Result; +use tokio::net::{TcpStream, UdpSocket}; +use tokio_util::codec::Framed; +use futures::{StreamExt, SinkExt}; +use rmon_common::protocol::{ClientMessage, AgentMessage, BincodeCodec, PROTOCOL_VERSION}; +use std::time::Duration; +use tokio::time::sleep; +use bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; +use std::io; +use rmon_agent::config::Config; +use rmon_agent::run_agent; +use std::path::PathBuf; +use std::fs; + +/// Client side codec +pub struct ClientCodec; + +impl Decoder for ClientCodec { + type Item = AgentMessage; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let mut codec = BincodeCodec::::default(); + codec.decode(src) + } +} + +impl Encoder for ClientCodec { + type Error = io::Error; + + fn encode(&mut self, item: ClientMessage, dst: &mut BytesMut) -> Result<(), Self::Error> { + let mut codec = BincodeCodec::::default(); + codec.encode(item, dst) + } +} + +async fn setup_agent(port: u16, udp_port: u16, config_path: PathBuf, data_dir: &str) -> Result<()> { + let config_str = format!(r#" +[storage] +data_dir = "{}" +default_mode = "circular" +default_window = "1h" + +[[sources]] +type = "udp" +name = "test_source" +bind = "127.0.0.1:{}" +[sources.timestamp] +format = "none" +[[sources.signals]] +id = "sig1" +label = "Sig 1" +unit = "V" +scale = 1.0 +offset = 0.0 +byte_offset = 0 +byte_type = "f32_le" +path = ["test"] +"#, data_dir, udp_port); + + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&config_path, config_str.clone())?; + let config: Config = toml::from_str(&config_str)?; + + tokio::spawn(async move { + let _ = run_agent(config, &format!("127.0.0.1:{}", port), config_path).await; + }); + + sleep(Duration::from_millis(300)).await; + Ok(()) +} + +#[tokio::test] +async fn test_agent_full_flow() -> Result<()> { + tokio::time::timeout(Duration::from_secs(20), async { + let port = 7892; + let udp_port = 9001; + let data_dir = "/tmp/rmon-test-data-full"; + let config_path = PathBuf::from("/tmp/rmon-test/config_full.toml"); + let _ = fs::remove_dir_all(data_dir); + + setup_agent(port, udp_port, config_path, data_dir).await?; + + let stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await?; + let mut framed = Framed::new(stream, ClientCodec); + + framed.send(ClientMessage::Handshake { + protocol_version: PROTOCOL_VERSION, + client_version: "0.1.0".to_string(), + }).await?; + + let ack = framed.next().await.ok_or_else(|| anyhow::anyhow!("Disconnected"))??; + if let AgentMessage::HandshakeAck { accepted, .. } = ack { + assert!(accepted); + } else { + panic!("Expected HandshakeAck, got {:?}", ack); + } + + framed.send(ClientMessage::Subscribe { + ids: vec!["udp://test_source/sig1".to_string()], + from_ns: None, + }).await?; + + let udp_socket = UdpSocket::bind("127.0.0.1:0").await?; + let val: f32 = 42.0; + udp_socket.send_to(&val.to_le_bytes(), format!("127.0.0.1:{}", udp_port)).await?; + + let msg = framed.next().await.ok_or_else(|| anyhow::anyhow!("Disconnected"))??; + if let AgentMessage::DataBatch { batches } = msg { + assert_eq!(batches[0].id, "udp://test_source/sig1"); + assert_eq!(batches[0].samples[0].value, 42.0); + } else { + panic!("Expected DataBatch, got {:?}", msg); + } + + // Check continuous storage + sleep(Duration::from_millis(200)).await; + let signal_dir = format!("{}/test_source/udp___test_source_sig1", data_dir); + let data_files = fs::read_dir(&signal_dir).map_err(|e| anyhow::anyhow!("Failed to read {}: {}", signal_dir, e))?; + assert!(data_files.count() > 0); + + Ok::<(), anyhow::Error>(()) + }).await.map_err(|_| anyhow::anyhow!("Test timed out"))? +} + +#[tokio::test] +async fn test_agent_config_remote() -> Result<()> { + tokio::time::timeout(Duration::from_secs(20), async { + let port = 7894; + let udp_port = 9002; + let data_dir = "/tmp/rmon-test-data-remote"; + let config_path = PathBuf::from("/tmp/rmon-test/config_remote.toml"); + let _ = fs::remove_dir_all(data_dir); + + setup_agent(port, udp_port, config_path.clone(), data_dir).await?; + + let stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await?; + let mut framed = Framed::new(stream, ClientCodec); + + framed.send(ClientMessage::Handshake { + protocol_version: PROTOCOL_VERSION, + client_version: "0.1.0".to_string(), + }).await?; + let _ = framed.next().await.unwrap()?; + + // 1. Get Config + framed.send(ClientMessage::GetConfig).await?; + let msg = framed.next().await.ok_or_else(|| anyhow::anyhow!("Disconnected"))??; + if let AgentMessage::ConfigReport { config_toml } = msg { + assert!(config_toml.contains("test_source")); + } else { + panic!("Expected ConfigReport, got {:?}", msg); + } + + // 2. Update Config + let new_config = format!(r#" +[storage] +data_dir = "{}" +default_mode = "circular" +default_window = "1h" + +[[sources]] +type = "shell" +name = "new_source" +command = "echo \"val: 100\"" +poll_interval = "100ms" +[sources.timestamp] +format = "none" +[[sources.signals]] +id = "sig_new" +label = "New Sig" +unit = "X" +scale = 1.0 +offset = 0.0 +regex = "val: ([0-9]+)" +path = ["new"] +"#, data_dir); + framed.send(ClientMessage::UpdateConfig { config_toml: new_config.to_string() }).await?; + + // Wait for hot-reload + sleep(Duration::from_millis(2000)).await; + + // 3. Check if signals changed + framed.send(ClientMessage::ListSignals).await?; + let msg = tokio::time::timeout(Duration::from_secs(5), framed.next()).await + .map_err(|_| anyhow::anyhow!("Timeout waiting for SignalList"))? + .ok_or_else(|| anyhow::anyhow!("Disconnected"))??; + + if let AgentMessage::SignalList { signals } = msg { + assert!(signals.iter().any(|s| s.id == "shell://new_source/sig_new"), "New signal not found in {:?}", signals); + } else { + panic!("Expected SignalList, got {:?}", msg); + } + + Ok::<(), anyhow::Error>(()) + }).await.map_err(|_| anyhow::anyhow!("Test timed out"))? +} diff --git a/rmon-common/Cargo.toml b/rmon-common/Cargo.toml new file mode 100644 index 0000000..42949df --- /dev/null +++ b/rmon-common/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rmon-common" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +bincode.workspace = true +chrono.workspace = true +thiserror.workspace = true +anyhow.workspace = true +tokio-util.workspace = true +bytes.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/rmon-common/src/lib.rs b/rmon-common/src/lib.rs new file mode 100644 index 0000000..586bd1b --- /dev/null +++ b/rmon-common/src/lib.rs @@ -0,0 +1,2 @@ +pub mod signal; +pub mod protocol; diff --git a/rmon-common/src/protocol.rs b/rmon-common/src/protocol.rs new file mode 100644 index 0000000..2ee4b4d --- /dev/null +++ b/rmon-common/src/protocol.rs @@ -0,0 +1,210 @@ +use serde::{Deserialize, Serialize}; +use crate::signal::{SignalId, SignalInfo, Sample, StorageMode, SourceType}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ClientMessage { + Handshake { + protocol_version: u16, + client_version: String, + }, + ListSignals, + Subscribe { + ids: Vec, + from_ns: Option, + }, + Unsubscribe { + ids: Vec, + }, + QueryHistory { + id: SignalId, + from_ns: i64, + to_ns: i64, + max_points: u32, + }, + StartRecording { + ids: Vec, + mode: StorageMode, + }, + StopRecording { + ids: Vec, + }, + GetStatus, + GetConfig, + UpdateConfig { + config_toml: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AgentMessage { + HandshakeAck { + protocol_version: u16, + agent_version: String, + accepted: bool, + reject_reason: Option, + }, + SignalList { + signals: Vec, + }, + DataBatch { + batches: Vec, + }, + HistoryChunk { + id: SignalId, + samples: Vec, + done: bool, + }, + RecordingStatus { + id: SignalId, + recording: bool, + mode: Option, + }, + StatusReport { + agent_version: String, + hostname: String, + uptime_secs: u64, + sources: Vec, + }, + ConfigReport { + config_toml: String, + }, + Error { + context: String, + message: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignalBatch { + pub id: SignalId, + pub samples: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SourceStatus { + pub name: String, + pub source_type: SourceType, + pub connected: bool, + pub last_sample_ns: Option, + pub error: Option, +} + +pub const PROTOCOL_VERSION: u16 = 1; + +use tokio_util::codec::{Decoder, Encoder}; +use bytes::{BytesMut, Buf, BufMut}; +use std::io; + +pub struct BincodeCodec { + _phantom: std::marker::PhantomData, +} + +impl Default for BincodeCodec { + fn default() -> Self { + Self { _phantom: std::marker::PhantomData } + } +} + +impl Decoder for BincodeCodec +where + for<'a> T: Deserialize<'a>, +{ + type Item = T; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + if src.len() < 4 { + return Ok(None); + } + + let mut length_bytes = [0u8; 4]; + length_bytes.copy_from_slice(&src[..4]); + let length = u32::from_le_bytes(length_bytes) as usize; + + if length > 16 * 1024 * 1024 { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Frame too large")); + } + + if src.len() < 4 + length { + src.reserve(4 + length - src.len()); + return Ok(None); + } + + src.advance(4); + let payload = src.split_to(length); + let item = bincode::deserialize(&payload) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + Ok(Some(item)) + } +} + +impl Encoder for BincodeCodec +where + T: Serialize, +{ + type Error = io::Error; + + fn encode(&mut self, item: T, dst: &mut BytesMut) -> Result<(), Self::Error> { + let payload = bincode::serialize(&item) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let length = payload.len() as u32; + dst.reserve(4 + payload.len()); + dst.put_u32_le(length); + dst.put_slice(&payload); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::BytesMut; + use tokio_util::codec::{Encoder, Decoder}; + + #[test] + fn test_codec_handshake_roundtrip() { + let mut codec = BincodeCodec::::default(); + let msg = ClientMessage::Handshake { + protocol_version: PROTOCOL_VERSION, + client_version: "1.0.0".to_string(), + }; + + let mut buf = BytesMut::new(); + codec.encode(msg.clone(), &mut buf).unwrap(); + + let mut decoder = BincodeCodec::::default(); + let decoded = decoder.decode(&mut buf).unwrap().unwrap(); + + if let (ClientMessage::Handshake { protocol_version: v1, .. }, + ClientMessage::Handshake { protocol_version: v2, .. }) = (msg, decoded) { + assert_eq!(v1, v2); + } else { + panic!("Decoded message mismatch"); + } + } + + #[test] + fn test_codec_frame_limit() { + let mut buf = BytesMut::new(); + buf.put_u32_le(20 * 1024 * 1024); // Exceeds 16MB limit + buf.put_bytes(0, 10); + + let mut decoder = BincodeCodec::::default(); + let res = decoder.decode(&mut buf); + assert!(res.is_err()); + } + + #[test] + fn test_all_message_variants() { + // Just ensure they can be serialized + let m1 = ClientMessage::ListSignals; + let m2 = ClientMessage::GetStatus; + let m3 = ClientMessage::GetConfig; + let _ = bincode::serialize(&m1).unwrap(); + let _ = bincode::serialize(&m2).unwrap(); + let _ = bincode::serialize(&m3).unwrap(); + } +} diff --git a/rmon-common/src/signal.rs b/rmon-common/src/signal.rs new file mode 100644 index 0000000..d1d4bc4 --- /dev/null +++ b/rmon-common/src/signal.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +pub type SignalId = String; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SignalInfo { + pub id: SignalId, + pub label: String, + pub unit: String, + pub scale: f64, + pub offset: f64, + pub sampling_period: Option, + pub source_type: SourceType, + pub path: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SourceType { + Csv, + Udp, + Epics, + Custom(String), +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] +pub struct Sample { + /// Nanoseconds since Unix epoch (UTC). + pub timestamp: i64, + pub value: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum StorageMode { + Continuous, + Circular { window: Duration }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sample_equality() { + let s1 = Sample { timestamp: 100, value: 42.0 }; + let s2 = Sample { timestamp: 100, value: 42.0 }; + let s3 = Sample { timestamp: 101, value: 42.0 }; + assert_eq!(s1, s2); + assert_ne!(s1, s3); + } + + #[test] + fn test_signal_info_creation() { + let info = SignalInfo { + id: "test://sig".to_string(), + label: "Test".to_string(), + unit: "V".to_string(), + scale: 1.0, + offset: 0.0, + sampling_period: None, + source_type: SourceType::Csv, + path: vec!["a".to_string()], + }; + assert_eq!(info.id, "test://sig"); + } +} diff --git a/rmon-ui/Cargo.toml b/rmon-ui/Cargo.toml new file mode 100644 index 0000000..6f6de05 --- /dev/null +++ b/rmon-ui/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rmon-ui" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +eframe = "0.24" +egui = "0.24" +egui_plot = "0.24" +tokio = { workspace = true, features = ["full"] } +tracing.workspace = true +tracing-subscriber.workspace = true +serde.workspace = true +bincode.workspace = true +rmon-common.workspace = true +anyhow.workspace = true +futures.workspace = true +tokio-util.workspace = true +bytes.workspace = true +chrono.workspace = true +itertools.workspace = true +libc.workspace = true diff --git a/rmon-ui/src/app.rs b/rmon-ui/src/app.rs new file mode 100644 index 0000000..96eab7f --- /dev/null +++ b/rmon-ui/src/app.rs @@ -0,0 +1,406 @@ +use eframe::egui; +use crate::connection::ProtocolClient; +use crate::state::SessionState; +use crate::state::SignalHistory; +use rmon_common::protocol::{ClientMessage, AgentMessage}; +use std::sync::{Arc, Mutex}; +use tokio::sync::{mpsc, oneshot}; +use tokio::time::sleep; +use egui_plot::{Line, Plot, PlotPoints}; +use std::collections::VecDeque; + +#[derive(Clone)] +pub enum UiMessage { + Agent(AgentMessage), + Connected(Arc), + Tunnel(Arc>>), + Disconnected, + Error(String), + Log(String), +} + +pub struct RMonApp { + pub(crate) ssh_host: String, + pub(crate) agent_port: u16, + pub(crate) addr: String, + pub(crate) show_connect_dialog: bool, + pub(crate) show_config_editor: bool, + pub(crate) pending_input_request: Option<(String, oneshot::Sender)>, + pub(crate) input_value: String, + pub(crate) config_text: String, + pub(crate) last_error: Option, + pub(crate) logs: VecDeque, + pub(crate) state: Arc>, + pub(crate) client: Option>, + pub(crate) tunnel_child: Option>>>, + pub(crate) ui_rx: mpsc::Receiver, + pub(crate) ui_tx: mpsc::Sender, + pub(crate) selected_signals: Vec, +} + +impl RMonApp { + pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { + let (tx, rx) = mpsc::channel(100); + let app = Self { + ssh_host: "localhost".to_string(), + agent_port: 7891, + addr: "127.0.0.1:7891".to_string(), + show_connect_dialog: false, + show_config_editor: false, + pending_input_request: None, + input_value: String::new(), + config_text: String::new(), + last_error: None, + logs: VecDeque::with_capacity(100), + state: Arc::new(Mutex::new(SessionState::new())), + client: None, + tunnel_child: None, + ui_rx: rx, + ui_tx: tx, + selected_signals: Vec::new(), + }; + let temp_dir = std::env::temp_dir(); + let _ = std::fs::remove_file(temp_dir.join(format!("rmon_auth_req_{}", app.agent_port))); + let _ = std::fs::remove_file(temp_dir.join(format!("rmon_auth_res_{}", app.agent_port))); + app + } + + fn add_log(&mut self, msg: String) { + if self.logs.len() >= 100 { self.logs.pop_front(); } + self.logs.push_back(msg); + } + + fn update_from_agent(&mut self, ctx: &egui::Context) { + let temp_dir = std::env::temp_dir(); + let req_path = temp_dir.join(format!("rmon_auth_req_{}", self.agent_port)); + if req_path.exists() && self.pending_input_request.is_none() { + if let Ok(prompt) = std::fs::read_to_string(&req_path) { + // Delete immediately so we don't re-trigger on next frame + let _ = std::fs::remove_file(&req_path); + + let (tx, rx) = oneshot::channel(); + self.pending_input_request = Some((prompt, tx)); + self.input_value.clear(); + + let port = self.agent_port; + tokio::spawn(async move { + if let Ok(val) = rx.await { + let res_path = std::env::temp_dir().join(format!("rmon_auth_res_{}", port)); + let _ = std::fs::write(&res_path, val); + } + }); + } + } + + while let Ok(msg) = self.ui_rx.try_recv() { + ctx.request_repaint(); + self.process_ui_message(msg); + } + } + + fn process_ui_message(&mut self, msg: UiMessage) { + match msg { + UiMessage::Log(log) => self.add_log(log), + UiMessage::Tunnel(child) => { + self.add_log("SSH tunnel established".to_string()); + self.tunnel_child = Some(child); + } + UiMessage::Connected(client) => { + self.add_log("Agent connected".to_string()); + self.client = Some(client); + self.state.lock().unwrap().connected = true; + self.show_connect_dialog = false; + self.last_error = None; + let client_clone = self.client.as_ref().unwrap().clone(); + tokio::spawn(async move { let _ = client_clone.send(ClientMessage::GetConfig).await; }); + } + UiMessage::Disconnected => { + self.add_log("Session ended".to_string()); + self.client = None; + self.state.lock().unwrap().connected = false; + if let Some(child_lock) = self.tunnel_child.take() { + if let Ok(mut child) = child_lock.lock() { + if let Some(mut c) = child.take() { let _ = c.kill(); } + } + } + } + UiMessage::Error(err) => { + self.add_log(format!("ERROR: {}", err)); + self.last_error = Some(err); + } + UiMessage::Agent(AgentMessage::SignalList { signals }) => { + self.add_log(format!("Received signal list ({} signals)", signals.len())); + let mut state = self.state.lock().unwrap(); + for sig in signals { + state.signals.entry(sig.id.clone()).or_insert_with(|| Arc::new(Mutex::new(SignalHistory::new(sig)))); + } + } + UiMessage::Agent(AgentMessage::ConfigReport { config_toml }) => { self.config_text = config_toml; } + UiMessage::Agent(AgentMessage::DataBatch { batches }) => { + let mut new_ids = Vec::new(); + { + let state = self.state.lock().unwrap(); + for batch in batches { + if let Some(history) = state.signals.get(&batch.id) { + let mut history = history.lock().unwrap(); + let is_first = history.samples.is_empty(); + for sample in batch.samples { history.push(sample); } + if is_first { new_ids.push(batch.id.clone()); } + } + } + } + for id in new_ids { self.add_log(format!("Data start for {}", id)); } + } + UiMessage::Agent(AgentMessage::Error { context, message }) => { self.add_log(format!("AGENT ERROR ({}): {}", context, message)); } + _ => {} + } + } + + fn draw_auth_dialog(&mut self, ctx: &egui::Context) { + if self.pending_input_request.is_some() { + let prompt = self.pending_input_request.as_ref().unwrap().0.clone(); + let mut open = true; + egui::Window::new("SSH Authentication Required").open(&mut open).collapsible(false).resizable(false).show(ctx, |ui| { + ui.label(egui::RichText::new(&prompt).strong()); + ui.add_space(8.0); + let is_password = prompt.to_lowercase().contains("password"); + let text_edit = egui::TextEdit::singleline(&mut self.input_value).password(is_password); + let response = ui.add(text_edit); + response.request_focus(); + ui.add_space(10.0); + if ui.button("Confirm").clicked() || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { + if let Some((_, tx)) = self.pending_input_request.take() { let _ = tx.send(self.input_value.clone()); } + } + }); + if !open { self.pending_input_request = None; } + } + } + + fn draw_connect_dialog(&mut self, ctx: &egui::Context) { + let mut open = self.show_connect_dialog; + egui::Window::new("Connect to Agent").open(&mut open).show(ctx, |ui| { + if let Some(err) = &self.last_error { ui.colored_label(egui::Color32::RED, format!("❌ {}", err)); } + egui::Grid::new("connect_grid").num_columns(2).show(ui, |ui| { + ui.label("SSH Host:"); ui.text_edit_singleline(&mut self.ssh_host); ui.end_row(); + ui.label("Agent Port:"); ui.add(egui::DragValue::new(&mut self.agent_port)); ui.end_row(); + ui.label("Local TCP Addr:"); ui.text_edit_singleline(&mut self.addr); ui.end_row(); + }); + if ui.button("🚀 Connect").clicked() { + let (ssh_host, agent_port, addr, ui_tx) = (self.ssh_host.clone(), self.agent_port, self.addr.clone(), self.ui_tx.clone()); + tokio::spawn(async move { + use crate::connection::SshConnection; + let ssh = SshConnection::new(ssh_host.clone(), agent_port); + let is_localhost = ssh_host == "localhost" || ssh_host == "127.0.0.1"; + let local_port = addr.split(':').last().and_then(|p| p.parse::().ok()).unwrap_or(7891); + let skip_tunnel = is_localhost && local_port == agent_port; + let _ = ui_tx.send(UiMessage::Log("Preparing...".to_string())).await; + let _ = ssh.kill_local_orphan_tunnels(); + if let Err(e) = ssh.establish_master() { let _ = ui_tx.send(UiMessage::Error(format!("SSH Auth failed: {}", e))).await; return; } + match ssh.check_running() { + Ok(false) => { + let _ = ui_tx.send(UiMessage::Log("Deploying and starting agent...".to_string())).await; + if let Err(e) = ssh.start_agent() { let _ = ui_tx.send(UiMessage::Error(format!("Start failed: {}", e))).await; return; } + sleep(std::time::Duration::from_secs(1)).await; + } + _ => { let _ = ui_tx.send(UiMessage::Log("Agent already running".to_string())).await; } + } + if !skip_tunnel { + let _ = ui_tx.send(UiMessage::Log("Establishing SSH tunnel...".to_string())).await; + match ssh.create_tunnel() { + Ok(child) => { let _ = ui_tx.send(UiMessage::Tunnel(Arc::new(Mutex::new(Some(child))))).await; sleep(std::time::Duration::from_secs(1)).await; } + Err(e) => { let _ = ui_tx.send(UiMessage::Error(format!("Tunnel failed: {}", e))).await; return; } + } + } else { + let _ = ui_tx.send(UiMessage::Log("Direct local connection".to_string())).await; + } + match ProtocolClient::connect(&addr, ui_tx.clone()).await { + Ok(client) => { let _ = ui_tx.send(UiMessage::Connected(Arc::new(client))).await; } + Err(e) => { + let _ = ui_tx.send(UiMessage::Error(format!("Handshake failed: {}", e))).await; + let _ = ssh.close_master(); + let _ = ui_tx.send(UiMessage::Disconnected).await; + } + } + }); + } + }); + self.show_connect_dialog = open; + } +} + +impl eframe::App for RMonApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.update_from_agent(ctx); + if self.pending_input_request.is_some() { self.draw_auth_dialog(ctx); } + if self.show_connect_dialog { self.draw_connect_dialog(ctx); } + if self.show_config_editor { + let mut open = self.show_config_editor; + egui::Window::new("Agent Configuration").open(&mut open).show(ctx, |ui| { + ui.add(egui::TextEdit::multiline(&mut self.config_text).font(egui::TextStyle::Monospace).desired_width(f32::INFINITY)); + if ui.button("💾 Save & Apply").clicked() { + if let Some(client) = &self.client { + let client = client.clone(); + let config_toml = self.config_text.clone(); + tokio::spawn(async move { + let _ = client.send(ClientMessage::UpdateConfig { config_toml }).await; + sleep(std::time::Duration::from_millis(1500)).await; + let _ = client.send(ClientMessage::ListSignals).await; + }); + } + } + if ui.button("🔄 Refresh").clicked() { + if let Some(client) = &self.client { + let client = client.clone(); + tokio::spawn(async move { + let _ = client.send(ClientMessage::GetConfig).await; + let _ = client.send(ClientMessage::ListSignals).await; + }); + } + } + }); + self.show_config_editor = open; + } + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + ui.menu_button("File", |ui| { if ui.button("Quit").clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } }); + ui.separator(); + if self.client.is_none() { + if ui.button("🚀 Connect...").clicked() { self.last_error = None; self.show_connect_dialog = true; } + } else { + if ui.button("🔌 Disconnect").clicked() { let _ = self.ui_tx.try_send(UiMessage::Disconnected); } + if ui.button("💀 Kill Agent").clicked() { + let (host, port, tx) = (self.ssh_host.clone(), self.agent_port, self.ui_tx.clone()); + tokio::spawn(async move { + use crate::connection::SshConnection; + let ssh = SshConnection::new(host, port); + let _ = tx.send(UiMessage::Log("Killing agent...".to_string())).await; + let _ = ssh.kill_agent(); + }); + let _ = self.ui_tx.try_send(UiMessage::Disconnected); + } + if ui.button("⚙ Configure Agent").clicked() { self.show_config_editor = true; } + } + }); + }); + egui::TopBottomPanel::bottom("log_panel").resizable(true).default_height(100.0).show(ctx, |ui| { + ui.heading("Logs"); + egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| { + for log in &self.logs { ui.label(egui::RichText::new(log).monospace()); } + }); + }); + egui::SidePanel::left("left_panel").show(ctx, |ui| { + ui.heading("Signals"); + let state = self.state.lock().unwrap(); + let mut ids: Vec<_> = state.signals.keys().cloned().collect(); + ids.sort(); + egui::ScrollArea::vertical().show(ui, |ui| { + for id in ids { + if let Some(history) = state.signals.get(&id) { + let history = history.lock().unwrap(); + let mut checked = self.selected_signals.contains(&id); + if ui.checkbox(&mut checked, &history.info.label).changed() { + if checked { + self.selected_signals.push(id.clone()); + if let Some(client) = &self.client { + let client = client.clone(); + tokio::spawn(async move { let _ = client.send(ClientMessage::Subscribe { ids: vec![id], from_ns: None }).await; }); + } + } else { self.selected_signals.retain(|s| s != &id); } + } + } + } + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Plots"); + egui::ScrollArea::vertical().show(ui, |ui| { + let state = self.state.lock().unwrap(); + for id in &self.selected_signals { + if let Some(history) = state.signals.get(id) { + let history = history.lock().unwrap(); + ui.label(format!("{} [{}]", history.info.label, history.samples.len())); + if !history.samples.is_empty() { + let first_ts = history.samples[0].timestamp as f64 / 1e9; + let points: PlotPoints = history.samples.iter().map(|s| [(s.timestamp as f64 / 1e9) - first_ts, s.value]).collect(); + Plot::new(id).view_aspect(2.5).auto_bounds_x().auto_bounds_y().show(ui, |plot_ui| plot_ui.line(Line::new(points))); + } else { ui.label("Waiting for data..."); } + } + } + }); + }); + ctx.request_repaint_after(std::time::Duration::from_millis(100)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rmon_common::signal::{SignalInfo, SourceType}; + + #[test] + fn test_app_init() { + let (tx, rx) = mpsc::channel(100); + let app = RMonApp { + ssh_host: "h".to_string(), + agent_port: 1, + addr: "a".to_string(), + show_connect_dialog: false, + show_config_editor: false, + pending_input_request: None, + input_value: "".to_string(), + config_text: "".to_string(), + last_error: None, + logs: VecDeque::new(), + state: Arc::new(Mutex::new(SessionState::new())), + client: None, + tunnel_child: None, + ui_rx: rx, + ui_tx: tx, + selected_signals: vec![], + }; + assert_eq!(app.ssh_host, "h"); + } + + #[test] + fn test_process_ui_message_logic() { + let (tx, rx) = mpsc::channel(100); + let mut app = RMonApp { + ssh_host: "h".to_string(), + agent_port: 1, + addr: "a".to_string(), + show_connect_dialog: false, + show_config_editor: false, + pending_input_request: None, + input_value: "".to_string(), + config_text: "".to_string(), + last_error: None, + logs: VecDeque::new(), + state: Arc::new(Mutex::new(SessionState::new())), + client: None, + tunnel_child: None, + ui_rx: rx, + ui_tx: tx, + selected_signals: vec![], + }; + + app.process_ui_message(UiMessage::Log("test log".to_string())); + assert_eq!(app.logs.len(), 1); + assert_eq!(app.logs[0], "test log"); + + app.process_ui_message(UiMessage::Error("test err".to_string())); + assert_eq!(app.last_error.as_ref().unwrap(), "test err"); + + let sig = SignalInfo { + id: "s1".to_string(), label: "L".to_string(), unit: "V".to_string(), scale: 1.0, offset: 0.0, + sampling_period: None, source_type: SourceType::Csv, path: vec![], + }; + app.process_ui_message(UiMessage::Agent(AgentMessage::SignalList { signals: vec![sig] })); + assert_eq!(app.state.lock().unwrap().signals.len(), 1); + + app.process_ui_message(UiMessage::Agent(AgentMessage::ConfigReport { config_toml: "cfg".to_string() })); + assert_eq!(app.config_text, "cfg"); + + app.process_ui_message(UiMessage::Disconnected); + assert!(!app.state.lock().unwrap().connected); + } +} diff --git a/rmon-ui/src/connection/mod.rs b/rmon-ui/src/connection/mod.rs new file mode 100644 index 0000000..c3472b9 --- /dev/null +++ b/rmon-ui/src/connection/mod.rs @@ -0,0 +1,109 @@ +use anyhow::Result; +use tokio::net::TcpStream; +use tokio_util::codec::{Framed, Decoder, Encoder}; +use futures::{StreamExt, SinkExt}; +use rmon_common::protocol::{ClientMessage, AgentMessage, BincodeCodec, PROTOCOL_VERSION}; +use bytes::BytesMut; +use std::io; +use tokio::sync::mpsc; + +pub mod ssh; +pub use self::ssh::SshConnection; + +pub struct ClientCodec; + +impl Decoder for ClientCodec { + type Item = AgentMessage; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let mut codec = BincodeCodec::::default(); + codec.decode(src) + } +} + +impl Encoder for ClientCodec { + type Error = io::Error; + + fn encode(&mut self, item: ClientMessage, dst: &mut BytesMut) -> Result<(), Self::Error> { + let mut codec = BincodeCodec::::default(); + codec.encode(item, dst) + } +} + +pub struct ProtocolClient { + tx: mpsc::Sender, +} + +impl ProtocolClient { + pub async fn connect(addr: &str, ui_tx: mpsc::Sender) -> Result { + tracing::info!("Connecting to agent at {}...", addr); + let stream = TcpStream::connect(addr).await?; + let mut framed = Framed::new(stream, ClientCodec); + + // 1. Handshake + framed.send(ClientMessage::Handshake { + protocol_version: PROTOCOL_VERSION, + client_version: env!("CARGO_PKG_VERSION").to_string(), + }).await?; + + match framed.next().await { + Some(Ok(AgentMessage::HandshakeAck { accepted: true, .. })) => { + tracing::info!("Handshake accepted"); + } + Some(Ok(AgentMessage::HandshakeAck { accepted: false, reject_reason, .. })) => { + return Err(anyhow::anyhow!("Handshake rejected: {}", reject_reason.unwrap_or_default())); + } + _ => return Err(anyhow::anyhow!("Handshake failed")), + } + + let (tx, mut rx) = mpsc::channel::(100); + let ui_tx_clone = ui_tx.clone(); + + // Start message pump BEFORE sending ListSignals to ensure response is caught + tokio::spawn(async move { + tracing::debug!("UI message pump started"); + loop { + tokio::select! { + msg_res = framed.next() => { + match msg_res { + Some(Ok(msg)) => { + if let Err(_) = ui_tx_clone.send(crate::app::UiMessage::Agent(msg)).await { break; } + } + Some(Err(e)) => { + let _ = ui_tx_clone.send(crate::app::UiMessage::Error(format!("Protocol error: {}", e))).await; + break; + } + None => { + let _ = ui_tx_clone.send(crate::app::UiMessage::Log("Agent closed connection".to_string())).await; + break; + } + } + } + cmd_opt = rx.recv() => { + match cmd_opt { + Some(cmd) => { + if let Err(e) = framed.send(cmd).await { + let _ = ui_tx_clone.send(crate::app::UiMessage::Error(format!("Send failed: {}", e))).await; + break; + } + } + None => break, // ProtocolClient handle dropped + } + } + } + } + let _ = ui_tx_clone.send(crate::app::UiMessage::Disconnected).await; + tracing::info!("UI message pump stopped"); + }); + + // Now request signals + tx.send(ClientMessage::ListSignals).await?; + + Ok(Self { tx }) + } + + pub async fn send(&self, msg: ClientMessage) -> Result<()> { + self.tx.send(msg).await.map_err(|_| anyhow::anyhow!("Disconnected")) + } +} diff --git a/rmon-ui/src/connection/ssh.rs b/rmon-ui/src/connection/ssh.rs new file mode 100644 index 0000000..7770aa1 --- /dev/null +++ b/rmon-ui/src/connection/ssh.rs @@ -0,0 +1,190 @@ +use anyhow::Result; +use std::process::{Command, Stdio, Child}; +use std::io::Write; +use std::path::PathBuf; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::process::CommandExt; +use std::time::Duration; + +/// Embedded agent binaries for different architectures. +const AGENT_X86_64: &[u8] = include_bytes!("../../../target/x86_64-unknown-linux-musl/release/rmon-agent"); + +pub struct SshConnection { + host: String, + port: u16, + control_path: PathBuf, +} + +impl SshConnection { + pub fn new(host: String, port: u16) -> Self { + let temp_dir = std::env::temp_dir(); + // Use a unique socket path per host/port + let control_path = temp_dir.join(format!("rmon_ssh_sock_{}_{}", host.replace(".", "_"), port)); + Self { host, port, control_path } + } + + fn setup_askpass(&self) -> Result { + let temp_dir = std::env::temp_dir(); + let askpass_path = temp_dir.join(format!("rmon_askpass_{}", self.port)); + let binary_path = std::env::current_exe()?; + let script = format!( + "#!/bin/sh\n\"{}\" --askpass-prompt \"$1\" --askpass-port {}\n", + binary_path.display(), + self.port + ); + fs::write(&askpass_path, script)?; + let mut perms = fs::metadata(&askpass_path)?.permissions(); + perms.set_mode(0o700); + fs::set_permissions(&askpass_path, perms)?; + Ok(askpass_path) + } + + fn ssh_base(&self) -> Command { + let mut cmd = Command::new("ssh"); + cmd.arg("-o").arg("RemoteCommand=none"); + cmd.arg("-o").arg("StrictHostKeyChecking=no"); + cmd.arg("-o").arg("ConnectTimeout=15"); + cmd.arg("-o").arg(format!("ControlPath={}", self.control_path.display())); + cmd.arg(&self.host); + cmd + } + + pub fn establish_master(&self) -> Result<()> { + if self.control_path.exists() { + let check = Command::new("ssh") + .arg("-o").arg(format!("ControlPath={}", self.control_path.display())) + .arg("-O").arg("check") + .arg(&self.host) + .output(); + if let Ok(out) = check { + if out.status.success() { + return Ok(()); + } + } + let _ = fs::remove_file(&self.control_path); + } + + let askpass = self.setup_askpass()?; + tracing::info!("Establishing SSH master connection to {}...", self.host); + + let mut cmd = Command::new("ssh"); + cmd.arg("-o").arg("RemoteCommand=none"); + cmd.arg("-o").arg("StrictHostKeyChecking=no"); + cmd.arg("-o").arg("ControlMaster=yes"); + cmd.arg("-o").arg("ControlPersist=30m"); + cmd.arg("-o").arg(format!("ControlPath={}", self.control_path.display())); + cmd.arg("-o").arg("BatchMode=no"); + cmd.env("SSH_ASKPASS", askpass); + cmd.env("SSH_ASKPASS_REQUIRE", "force"); + cmd.env("DISPLAY", ":0"); + cmd.arg("-T"); + cmd.arg(&self.host); + cmd.arg("true"); + cmd.stdin(Stdio::null()); + + unsafe { cmd.pre_exec(|| { libc::setsid(); Ok(()) }); } + + let mut child = cmd.spawn()?; + + let mut count = 0; + while !self.control_path.exists() && count < 3000 { + if let Ok(Some(status)) = child.try_wait() { + if !self.control_path.exists() { + return Err(anyhow::anyhow!("SSH authentication failed (status: {})", status)); + } + break; + } + std::thread::sleep(Duration::from_millis(100)); + count += 1; + } + + if !self.control_path.exists() { + let _ = child.kill(); + return Err(anyhow::anyhow!("Authentication timed out (5m limit reached)")); + } + + Ok(()) + } + + pub fn kill_local_orphan_tunnels(&self) -> Result<()> { + let pattern = format!("rmon_ssh_sock_{}_{}", self.host.replace(".", "_"), self.port); + let _ = Command::new("pkill").arg("-f").arg(&pattern).output(); + let _ = fs::remove_file(&self.control_path); + let fwd_pattern = format!("-L {}:127.0.0.1:{}", self.port, self.port); + let _ = Command::new("pkill").arg("-f").arg(&fwd_pattern).output(); + Ok(()) + } + + pub fn deploy_agent(&self) -> Result<()> { + let mut child = self.ssh_base() + .arg("mkdir -p ~/.rmon && cat > ~/.rmon/agent && chmod +x ~/.rmon/agent") + .stdin(Stdio::piped()) + .spawn()?; + + let mut stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("Failed to open stdin"))?; + stdin.write_all(AGENT_X86_64)?; + drop(stdin); + + let status = child.wait()?; + if !status.success() { return Err(anyhow::anyhow!("Failed to upload agent binary")); } + Ok(()) + } + + pub fn check_running(&self) -> Result { + // More robust check: use ss or netstat to see if anything is listening on that port + // We look for the port followed by a space to avoid partial matches + let cmd = format!("(ss -tln || netstat -tln || true) | grep -q ':{} '", self.port); + let output = self.ssh_base() + .arg(cmd) + .output()?; + Ok(output.status.success()) + } + + pub fn start_agent(&self) -> Result<()> { + self.deploy_agent()?; + // Use absolute path for log file in home directory + let cmd = format!("nohup ~/.rmon/agent --bind 0.0.0.0 --port {} >> ~/.rmon/agent.log 2>&1 &", self.port); + let output = self.ssh_base() + .arg(cmd) + .output()?; + if !output.status.success() { return Err(anyhow::anyhow!("Failed to start agent")); } + Ok(()) + } + + pub fn create_tunnel(&self) -> Result { + let child = self.ssh_base() + .arg("-o").arg("ExitOnForwardFailure=yes") + .arg("-N") + .arg("-L").arg(format!("{}:127.0.0.1:{}", self.port, self.port)) + .spawn()?; + Ok(child) + } + + pub fn kill_agent(&self) -> Result<()> { + let _ = self.ssh_base().arg(format!("pkill -f \"~/.rmon/agent --port {}\"", self.port)).output(); + Ok(()) + } + + pub fn close_master(&self) -> Result<()> { + let _ = Command::new("ssh") + .arg("-o").arg(format!("ControlPath={}", self.control_path.display())) + .arg("-O").arg("exit") + .arg(&self.host) + .output(); + let _ = fs::remove_file(&self.control_path); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ssh_connection_init() { + let conn = SshConnection::new("myhost".to_string(), 1234); + assert_eq!(conn.host, "myhost"); + assert_eq!(conn.port, 1234); + } +} diff --git a/rmon-ui/src/main.rs b/rmon-ui/src/main.rs new file mode 100644 index 0000000..1c00686 --- /dev/null +++ b/rmon-ui/src/main.rs @@ -0,0 +1,69 @@ +use eframe::egui; +use tokio::runtime::Runtime; + +mod app; +mod connection; +mod state; +mod ui; + +use crate::app::RMonApp; + +fn main() -> eframe::Result<()> { + let args: Vec = std::env::args().collect(); + + // Check if we are being called as an askpass helper + if let Some(pos) = args.iter().position(|a| a == "--askpass-prompt") { + let prompt = args.get(pos + 1).cloned().unwrap_or_default(); + let port = args.get(pos + 3).and_then(|p| p.parse::().ok()).unwrap_or(7891); + + let temp_dir = std::env::temp_dir(); + let request_path = temp_dir.join(format!("rmon_auth_req_{}", port)); + let response_path = temp_dir.join(format!("rmon_auth_res_{}", port)); + + // Signal the UI that we need input + let _ = std::fs::write(&request_path, &prompt); + + // Wait for response + let mut count = 0; + while !response_path.exists() && count < 600 { // 60s timeout + std::thread::sleep(std::time::Duration::from_millis(100)); + count += 1; + } + + if let Ok(res) = std::fs::read_to_string(&response_path) { + let _ = std::fs::remove_file(&request_path); + let _ = std::fs::remove_file(&response_path); + println!("{}", res.trim()); + } + return Ok(()); + } + + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + // Create a tokio runtime in a separate thread for async communication + let rt = Runtime::new().expect("Unable to create Runtime"); + let _enter = rt.enter(); + + std::thread::spawn(move || { + rt.block_on(async { + loop { + tokio::time::sleep(std::time::Duration::from_secs(3600)).await; + } + }) + }); + + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([1280.0, 720.0]), + ..Default::default() + }; + + eframe::run_native( + "RMon", + native_options, + Box::new(|cc| Box::new(RMonApp::new(cc))), + ) +} diff --git a/rmon-ui/src/state/mod.rs b/rmon-ui/src/state/mod.rs new file mode 100644 index 0000000..c303acf --- /dev/null +++ b/rmon-ui/src/state/mod.rs @@ -0,0 +1,76 @@ +use rmon_common::signal::{SignalId, SignalInfo, Sample}; +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Mutex}; + +pub struct SignalHistory { + pub info: SignalInfo, + pub samples: VecDeque, + pub max_samples: usize, +} + +impl SignalHistory { + pub fn new(info: SignalInfo) -> Self { + Self { + info, + samples: VecDeque::new(), + max_samples: 1000, // Default for display + } + } + + pub fn push(&mut self, sample: Sample) { + self.samples.push_back(sample); + if self.samples.len() > self.max_samples { + self.samples.pop_front(); + } + } +} + +pub struct SessionState { + pub connected: bool, + pub signals: HashMap>>, +} + +impl SessionState { + pub fn new() -> Self { + Self { + connected: false, + signals: HashMap::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rmon_common::signal::SourceType; + + #[test] + fn test_signal_history_push() { + let info = SignalInfo { + id: "test://1".to_string(), + label: "L".to_string(), + unit: "U".to_string(), + scale: 1.0, + offset: 0.0, + sampling_period: None, + source_type: SourceType::Csv, + path: vec![], + }; + let mut history = SignalHistory::new(info); + history.max_samples = 2; + + history.push(Sample { timestamp: 1, value: 1.0 }); + history.push(Sample { timestamp: 2, value: 2.0 }); + history.push(Sample { timestamp: 3, value: 3.0 }); + + assert_eq!(history.samples.len(), 2); + assert_eq!(history.samples[0].timestamp, 2); + } + + #[test] + fn test_session_state_init() { + let state = SessionState::new(); + assert!(!state.connected); + assert!(state.signals.is_empty()); + } +} diff --git a/rmon-ui/src/ui/mod.rs b/rmon-ui/src/ui/mod.rs new file mode 100644 index 0000000..72b09c3 --- /dev/null +++ b/rmon-ui/src/ui/mod.rs @@ -0,0 +1 @@ +// Placeholder for UI components