From 109f39854fe88174a20ff09eb23345adde91aa19 Mon Sep 17 00:00:00 2001 From: DEVAI Date: Thu, 18 Jun 2026 13:37:17 -0300 Subject: [PATCH 1/6] =?UTF-8?q?feat(tools):=20add=20openmonitor=20?= =?UTF-8?q?=E2=80=94=20TUI=20and=20web=20dashboard=20for=20openads=5Fserve?= =?UTF-8?q?rd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire Mg telemetry + Studio HTTP sessions. See tools/openmonitor/README.md. --- .gitignore | 3 + README.md | 1 + tools/openmonitor/.cargo/config.toml | 2 + tools/openmonitor/Cargo.lock | 2134 +++++++++++++++++++++++ tools/openmonitor/Cargo.toml | 28 + tools/openmonitor/README.md | 78 + tools/openmonitor/assets/dashboard.html | 333 ++++ tools/openmonitor/build.bat | 15 + tools/openmonitor/src/history.rs | 96 + tools/openmonitor/src/http_api.rs | 112 ++ tools/openmonitor/src/main.rs | 513 ++++++ tools/openmonitor/src/snapshot.rs | 222 +++ tools/openmonitor/src/web.rs | 109 ++ tools/openmonitor/src/wire.rs | 230 +++ 14 files changed, 3876 insertions(+) create mode 100644 tools/openmonitor/.cargo/config.toml create mode 100644 tools/openmonitor/Cargo.lock create mode 100644 tools/openmonitor/Cargo.toml create mode 100644 tools/openmonitor/README.md create mode 100644 tools/openmonitor/assets/dashboard.html create mode 100644 tools/openmonitor/build.bat create mode 100644 tools/openmonitor/src/history.rs create mode 100644 tools/openmonitor/src/http_api.rs create mode 100644 tools/openmonitor/src/main.rs create mode 100644 tools/openmonitor/src/snapshot.rs create mode 100644 tools/openmonitor/src/web.rs create mode 100644 tools/openmonitor/src/wire.rs diff --git a/.gitignore b/.gitignore index 8cf04512..25eee3fe 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,9 @@ examples/fivewin/err.txt .DS_Store Thumbs.db +# Rust (openmonitor) +tools/openmonitor/target/ + # Compiler / linker artefacts *.obj *.o diff --git a/README.md b/README.md index 55527c8e..f10598c9 100644 --- a/README.md +++ b/README.md @@ -1195,6 +1195,7 @@ OpenADS/ │ ├── import_dd/ # openads_import_dd — SAP .add → OpenADS import │ │ └── main.cpp │ ├── bench/ # openads_bench — synthetic SQL benchmark +│ ├── openmonitor/ # openmonitor — TUI + web dashboard for openads_serverd │ ├── harbour_patch/ # rddads compatibility patches + ADS baseline fixture │ ├── scripts/ │ │ ├── systemd/ # openads-serverd.service (Linux) diff --git a/tools/openmonitor/.cargo/config.toml b/tools/openmonitor/.cargo/config.toml new file mode 100644 index 00000000..0c17df09 --- /dev/null +++ b/tools/openmonitor/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] \ No newline at end of file diff --git a/tools/openmonitor/Cargo.lock b/tools/openmonitor/Cargo.lock new file mode 100644 index 00000000..62193f49 --- /dev/null +++ b/tools/openmonitor/Cargo.lock @@ -0,0 +1,2134 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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 = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[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" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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 = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[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 = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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 = "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-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[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.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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 = "openmonitor" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "clap", + "crossterm", + "ratatui", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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", + "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 = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[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 = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "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", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "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", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[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 = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +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", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +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", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[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", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tools/openmonitor/Cargo.toml b/tools/openmonitor/Cargo.toml new file mode 100644 index 00000000..534d9778 --- /dev/null +++ b/tools/openmonitor/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "openmonitor" +version = "0.1.0" +edition = "2021" +description = "Console monitor for openads server (wire telemetry + HTTP sessions)" + +[[bin]] +name = "openmonitor" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +axum = "0.8" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +clap = { version = "4", features = ["derive"] } +crossterm = "0.28" +ratatui = "0.29" +reqwest = { version = "0.12", features = ["blocking", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "net"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true \ No newline at end of file diff --git a/tools/openmonitor/README.md b/tools/openmonitor/README.md new file mode 100644 index 00000000..e0e72860 --- /dev/null +++ b/tools/openmonitor/README.md @@ -0,0 +1,78 @@ +# openmonitor + +Terminal and web monitor for a running **openads server** (`openads_serverd`). + +Reads live telemetry from: + +- **Wire** — `MgRequest` / `MgSnapshot` (uptime, connections, open tables, packet counters) +- **HTTP** — Studio API (`/api/health`, `/api/server/sessions`, kill session) + +Useful when developing Harbour / OpenADS clients over `tcp://` or debugging a local server instance. + +## Build + +Requires [Rust](https://rustup.rs/) (`cargo` on `PATH`). On Windows MSVC link tools are recommended for the static CRT build (Visual Studio Build Tools or `vcvars64.bat`). + +```bat +cd tools\openmonitor +build.bat +``` + +```sh +cd tools/openmonitor +cargo build --release +``` + +Binary: `target/release/openmonitor` (`.exe` on Windows). + +## Usage + +Start `openads_serverd` first, then: + +```text +openmonitor --wire-port 16262 --http http://127.0.0.1:16263 +``` + +One-shot snapshot (scripts / CI): + +```text +openmonitor --wire-port 16262 --http http://127.0.0.1:16263 --once +``` + +Web dashboard (default bind `127.0.0.1:9850`): + +```text +openmonitor --wire-port 16262 --http http://127.0.0.1:16263 --web +``` + +### Common flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--wire-host` | `127.0.0.1` | openads server wire host | +| `--wire-port` | `16262` | openads server wire port | +| `--http` | — | Studio HTTP base URL (enables session list + kill in TUI) | +| `--web` | off | Serve browser dashboard | +| `--web-port` | `9850` | Web UI port | +| `--once` | off | Print one snapshot and exit | +| `--interval` | `2` | TUI refresh seconds | + +### TUI keys + +| Key | Action | +|-----|--------| +| `q` / `Esc` | Quit | +| `r` | Refresh now | +| `k` | Kill selected HTTP session (requires `--http`) | + +## Wireshark + +With the server listening on the wire port: + +``` +tcp.port == 16262 +``` + +## Related + +Full local lab scripts (server launchers, optional Harbour probe) live in the companion repo [openads-wire-lab](https://github.com/Admnwk/openads-wire-lab). \ No newline at end of file diff --git a/tools/openmonitor/assets/dashboard.html b/tools/openmonitor/assets/dashboard.html new file mode 100644 index 00000000..147ec948 --- /dev/null +++ b/tools/openmonitor/assets/dashboard.html @@ -0,0 +1,333 @@ + + + + + +openmonitor + + + +
+
+
+

openmonitor

+

openads server

+
+
+ + + +
+
+ +
+
+
+
+

Atividade wire

+
+

Usuários wire

+
+
+
+

Sessões HTTP

+
+

Studio

+
+
+
+
+ openmonitor + +
+
+ + + \ No newline at end of file diff --git a/tools/openmonitor/build.bat b/tools/openmonitor/build.bat new file mode 100644 index 00000000..ddd01dc9 --- /dev/null +++ b/tools/openmonitor/build.bat @@ -0,0 +1,15 @@ +@echo off +setlocal enableDelayedExpansion + +where cargo >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] cargo not found on PATH — install Rust from https://rustup.rs/ + exit /b 1 +) + +cd /d "%~dp0" +echo [BUILD] openmonitor release... +cargo build --release %* +if errorlevel 1 exit /b 1 +echo [OK] target\release\openmonitor.exe +exit /b 0 \ No newline at end of file diff --git a/tools/openmonitor/src/history.rs b/tools/openmonitor/src/history.rs new file mode 100644 index 00000000..986db80f --- /dev/null +++ b/tools/openmonitor/src/history.rs @@ -0,0 +1,96 @@ +use crate::wire::MgSnapshot; +use std::time::Instant; + +const DEFAULT_CAPACITY: usize = 60; + +pub struct MetricHistory { + capacity: usize, + connections: Vec, + users: Vec, + ops_per_sec: Vec, + pkt_per_sec: Vec, + rss_kb: Vec, + last_ops: u64, + last_packets: u64, + last_sample: Option, +} + +impl Default for MetricHistory { + fn default() -> Self { + Self::new(DEFAULT_CAPACITY) + } +} + +impl MetricHistory { + pub fn new(capacity: usize) -> Self { + Self { + capacity: capacity.max(8), + connections: Vec::new(), + users: Vec::new(), + ops_per_sec: Vec::new(), + pkt_per_sec: Vec::new(), + rss_kb: Vec::new(), + last_ops: 0, + last_packets: 0, + last_sample: None, + } + } + + pub fn push(&mut self, snap: &MgSnapshot) { + let now = Instant::now(); + let (ops_rate, pkt_rate) = match self.last_sample { + Some(prev) => { + let secs = now.duration_since(prev).as_secs_f64().max(0.5); + let ops = snap.operations.saturating_sub(self.last_ops) as f64 / secs; + let pkts = snap + .packets_in + .saturating_add(snap.packets_out) + .saturating_sub(self.last_packets) as f64 + / secs; + (ops.round() as u64, pkts.round() as u64) + } + None => (0, 0), + }; + self.last_ops = snap.operations; + self.last_packets = snap.packets_in.saturating_add(snap.packets_out); + self.last_sample = Some(now); + + trim_push(&mut self.connections, snap.connections as u64, self.capacity); + trim_push(&mut self.users, snap.users as u64, self.capacity); + trim_push(&mut self.ops_per_sec, ops_rate, self.capacity); + trim_push(&mut self.pkt_per_sec, pkt_rate, self.capacity); + trim_push(&mut self.rss_kb, snap.rss_bytes / 1024, self.capacity); + } + + pub fn connections(&self) -> &[u64] { + &self.connections + } + + pub fn users(&self) -> &[u64] { + &self.users + } + + pub fn ops_per_sec(&self) -> &[u64] { + &self.ops_per_sec + } + + pub fn pkt_per_sec(&self) -> &[u64] { + &self.pkt_per_sec + } + + pub fn rss_kb(&self) -> &[u64] { + &self.rss_kb + } + + pub fn max_in(series: &[u64]) -> u64 { + series.iter().copied().max().unwrap_or(1).max(1) + } +} + +fn trim_push(series: &mut Vec, value: u64, capacity: usize) { + series.push(value); + if series.len() > capacity { + let drop = series.len() - capacity; + series.drain(0..drop); + } +} \ No newline at end of file diff --git a/tools/openmonitor/src/http_api.rs b/tools/openmonitor/src/http_api.rs new file mode 100644 index 00000000..696ef04e --- /dev/null +++ b/tools/openmonitor/src/http_api.rs @@ -0,0 +1,112 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Health { + pub status: String, + pub engine: String, + pub mode: String, + pub data_dir: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Session { + pub id: u64, + pub peer_ip: String, + pub peer_port: u16, + pub user: String, + pub data_dir: String, + pub connected_secs: i64, + pub idle_secs: i64, + pub frames_in: u64, + pub frames_out: u64, + pub open_tables: u32, +} + +#[derive(Debug, Clone, Default)] +pub struct HttpSnapshot { + pub health: Option, + pub sessions: Vec, + pub server_info: Option, + pub error: Option, +} + +pub struct HttpClient { + base: String, + client: reqwest::blocking::Client, +} + +impl HttpClient { + pub fn new(base: &str) -> Self { + let base = base.trim_end_matches('/').to_string(); + Self { + base, + client: reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .unwrap_or_default(), + } + } + + pub fn poll(&self) -> HttpSnapshot { + let mut snap = HttpSnapshot::default(); + match self.client.get(format!("{}/api/health", self.base)).send() { + Ok(r) if r.status().is_success() => { + snap.health = r.json().ok(); + } + Ok(r) => snap.error = Some(format!("health HTTP {}", r.status())), + Err(e) => snap.error = Some(format!("health: {e}")), + } + match self + .client + .get(format!("{}/api/server/sessions", self.base)) + .send() + { + Ok(r) if r.status().is_success() => { + if let Ok(v) = r.json::() { + if let Some(arr) = v.get("sessions").and_then(|a| a.as_array()) { + for item in arr { + if let Ok(s) = serde_json::from_value::(item.clone()) { + snap.sessions.push(s); + } + } + } + } + } + Ok(r) => { + if snap.error.is_none() { + snap.error = Some(format!("sessions HTTP {}", r.status())); + } + } + Err(e) => { + if snap.error.is_none() { + snap.error = Some(format!("sessions: {e}")); + } + } + } + match self + .client + .get(format!("{}/api/server/info", self.base)) + .send() + { + Ok(r) if r.status().is_success() => snap.server_info = r.json().ok(), + _ => {} + } + snap + } + + pub fn kill_session(&self, id: u64) -> Result<()> { + let url = format!("{}/api/server/sessions/{id}/kill", self.base); + let r = self + .client + .post(&url) + .send() + .with_context(|| format!("POST {url}"))?; + if r.status().is_success() { + Ok(()) + } else { + Err(anyhow::anyhow!("kill session failed: HTTP {}", r.status())) + } + } +} \ No newline at end of file diff --git a/tools/openmonitor/src/main.rs b/tools/openmonitor/src/main.rs new file mode 100644 index 00000000..079098f6 --- /dev/null +++ b/tools/openmonitor/src/main.rs @@ -0,0 +1,513 @@ +mod history; +mod http_api; +mod snapshot; +mod web; +mod wire; + +use anyhow::Result; +use clap::Parser; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::ExecutableCommand; +use http_api::HttpClient; +use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::symbols; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Sparkline, Table}; +use ratatui::Frame; +use snapshot::{MonitorConfig, MonitorState}; +use std::io::stdout; +use std::time::{Duration, Instant}; + +#[derive(Parser, Debug)] +#[command(name = "openmonitor", about = "Monitor for openads server")] +struct Args { + /// Wire host (default 127.0.0.1) + #[arg(long, default_value = "127.0.0.1")] + wire_host: String, + + /// Wire TCP port (openads server lab default 16262) + #[arg(long, default_value_t = 16262)] + wire_port: u16, + + /// HTTP base URL for Studio API (optional, e.g. http://127.0.0.1:6263) + #[arg(long)] + http: Option, + + /// Print one snapshot as text and exit (for scripts / CI) + #[arg(long)] + once: bool, + + /// Web dashboard (sober UI in the browser) + #[arg(long)] + web: bool, + + /// Web UI bind host + #[arg(long, default_value = "127.0.0.1")] + web_host: String, + + /// Web UI port (default 9850 — avoids 8080/16263 collisions) + #[arg(long, default_value_t = 9850)] + web_port: u16, + + /// Refresh interval in seconds + #[arg(long, default_value_t = 2)] + interval: u64, +} + +impl Args { + fn monitor_config(&self) -> MonitorConfig { + MonitorConfig { + wire_host: self.wire_host.clone(), + wire_port: self.wire_port, + http: self.http.clone(), + interval_secs: self.interval, + } + } +} + +struct TuiState { + inner: MonitorState, + selected_session: usize, + last_refresh: Instant, + status_msg: String, + show_graphs: bool, +} + +impl TuiState { + fn refresh(&mut self, cfg: &MonitorConfig) { + self.inner.refresh(cfg); + self.last_refresh = Instant::now(); + } +} + +fn main() -> Result<()> { + let args = Args::parse(); + let cfg = args.monitor_config(); + let mut app = TuiState { + inner: MonitorState { + mg: None, + mg_err: None, + http: http_api::HttpSnapshot::default(), + history: history::MetricHistory::default(), + }, + selected_session: 0, + last_refresh: Instant::now(), + status_msg: String::new(), + show_graphs: true, + }; + app.refresh(&cfg); + + if args.once { + print_once(&app.inner, &cfg); + return Ok(()); + } + + if args.web { + return web::run(cfg, &args.web_host, args.web_port); + } + + run_tui(&mut app, &cfg, &args) +} + +fn print_once(state: &MonitorState, cfg: &MonitorConfig) { + let snap = state.to_snapshot(cfg); + println!("openmonitor — openads server"); + println!("wire: {}:{}", cfg.wire_host, cfg.wire_port); + if let Some(h) = &cfg.http { + println!("http: {h}"); + } + println!(); + let w = &snap.wire; + if w.online { + println!("uptime : {}", w.uptime.as_deref().unwrap_or("—")); + println!("connections : {} (max {})", w.connections, w.max_connections); + println!("users : {} (max {})", w.users, w.max_users); + println!("tables : {}", w.tables); + println!("operations : {}", w.operations); + println!("packets in/out: {} / {}", w.packets_in, w.packets_out); + println!("rss : {}", w.rss); + println!("listener : port {}", w.listener_port); + println!("connected users: {}", w.wire_users.len()); + for u in &w.wire_users { + println!(" #{} {} @ {}", u.conn_no, u.name, u.address); + } + } else if let Some(e) = &w.error { + println!("[wire error] {e}"); + } + if cfg.http.is_some() { + let h = &snap.http; + if let Some(status) = &h.status { + println!(); + println!("http status : {status}"); + println!("http mode : {}", h.mode.as_deref().unwrap_or("—")); + println!("data_dir : {}", h.data_dir.as_deref().unwrap_or("—")); + } + println!("http sessions: {}", h.sessions.len()); + for s in &h.sessions { + println!( + " id={} {}:{} user={} tables={} idle={}s", + s.id, s.peer_ip, s.peer_port, s.user, s.open_tables, s.idle_secs + ); + } + if let Some(e) = &h.error { + println!("[http error] {e}"); + } + } +} + +fn run_tui(app: &mut TuiState, cfg: &MonitorConfig, args: &Args) -> Result<()> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + let mut term = ratatui::init(); + + let tick = Duration::from_secs(cfg.interval_secs); + let mut needs_redraw = true; + + loop { + if needs_redraw || app.last_refresh.elapsed() >= tick { + app.refresh(cfg); + needs_redraw = true; + } + + if needs_redraw { + term.draw(|f| ui(f, app, cfg, args))?; + needs_redraw = false; + } + + if event::poll(Duration::from_millis(200))? { + if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } + match key.code { + KeyCode::Char('q') | KeyCode::Esc => break, + KeyCode::Char('r') => { + app.refresh(cfg); + app.status_msg = "refreshed".into(); + needs_redraw = true; + } + KeyCode::Up => { + app.selected_session = app.selected_session.saturating_sub(1); + needs_redraw = true; + } + KeyCode::Down => { + if !app.inner.http.sessions.is_empty() { + app.selected_session = + (app.selected_session + 1).min(app.inner.http.sessions.len() - 1); + } + needs_redraw = true; + } + KeyCode::Char('g') => { + app.show_graphs = !app.show_graphs; + app.status_msg = if app.show_graphs { + "graphs on".into() + } else { + "graphs off".into() + }; + needs_redraw = true; + } + KeyCode::Char('k') => { + if let (Some(base), Some(sess)) = + (&cfg.http, app.inner.http.sessions.get(app.selected_session)) + { + let client = HttpClient::new(base); + match client.kill_session(sess.id) { + Ok(()) => { + app.status_msg = format!("killed session {}", sess.id); + app.refresh(cfg); + } + Err(e) => app.status_msg = format!("kill failed: {e}"), + } + needs_redraw = true; + } else { + app.status_msg = "kill needs --http and a selected session".into(); + needs_redraw = true; + } + } + _ => {} + } + } + } + } + + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + ratatui::restore(); + Ok(()) +} + +fn ui(f: &mut Frame, app: &TuiState, cfg: &MonitorConfig, args: &Args) { + let graph_rows = if app.show_graphs { 7 } else { 0 }; + let comm_rows = if app.show_graphs { 6 } else { 8 }; + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(graph_rows), + Constraint::Length(comm_rows), + Constraint::Length(3), + ]) + .split(f.area()); + + let title = format!( + " openmonitor — openads server wire {}:{}", + cfg.wire_host, cfg.wire_port + ); + let http_line = cfg + .http + .as_deref() + .map(|u| format!(" http {u}")) + .unwrap_or_default(); + f.render_widget( + Paragraph::new(Line::from(vec![ + Span::styled(title, Style::default().add_modifier(Modifier::BOLD)), + Span::raw(http_line), + ])) + .block(Block::default().borders(Borders::ALL).title("OpenMonitor")), + chunks[0], + ); + + let mid = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(45), Constraint::Percentage(55)]) + .split(chunks[1]); + + f.render_widget(activity_panel(app), mid[0]); + f.render_widget(sessions_panel(app), mid[1]); + let comm_idx = if app.show_graphs { 3 } else { 2 }; + let foot_idx = if app.show_graphs { 4 } else { 3 }; + if app.show_graphs { + render_graphs(f, app, chunks[2]); + } + f.render_widget(comm_panel(app), chunks[comm_idx]); + let web_hint = if args.web { "" } else { " w=use --web" }; + f.render_widget( + Paragraph::new(format!( + " {} | q quit r refresh g graphs up/down k kill (HTTP){}", + app.status_msg, web_hint + )) + .style(Style::default().fg(Color::DarkGray)), + chunks[foot_idx], + ); +} + +fn render_graphs(f: &mut Frame, app: &TuiState, area: ratatui::layout::Rect) { + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + ]) + .split(area); + + let panels = [ + ( + "Connections", + app.inner.history.connections(), + Color::Cyan, + app.inner.mg.as_ref().map(|s| s.max_connections as u64).unwrap_or(1), + ), + ( + "Users", + app.inner.history.users(), + Color::Green, + app.inner.mg.as_ref().map(|s| s.max_users as u64).unwrap_or(1), + ), + ( + "Ops/s", + app.inner.history.ops_per_sec(), + Color::Yellow, + history::MetricHistory::max_in(app.inner.history.ops_per_sec()), + ), + ( + "Pkts/s", + app.inner.history.pkt_per_sec(), + Color::Magenta, + history::MetricHistory::max_in(app.inner.history.pkt_per_sec()), + ), + ( + "RSS KB", + app.inner.history.rss_kb(), + Color::Blue, + history::MetricHistory::max_in(app.inner.history.rss_kb()), + ), + ]; + + for (i, (title, data, color, max_val)) in panels.into_iter().enumerate() { + let max = max_val.max(1); + let spark_data: Vec = if data.is_empty() { + vec![0] + } else { + data.to_vec() + }; + let last = spark_data.last().copied().unwrap_or(0); + let widget = Sparkline::default() + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("{title} ({last})")), + ) + .data(&spark_data) + .max(max) + .style(Style::default().fg(color)) + .bar_set(symbols::bar::NINE_LEVELS); + f.render_widget(widget, cols[i]); + } +} + +fn activity_panel(app: &TuiState) -> Paragraph<'_> { + let mut lines = Vec::new(); + if let Some(s) = &app.inner.mg { + lines.push(Line::from(vec![ + Span::styled("Uptime ", Style::default().fg(Color::Cyan)), + Span::raw(wire::format_uptime(s.uptime_seconds)), + ])); + lines.push(Line::from(format!( + "Connections {} / max {}", + s.connections, s.max_connections + ))); + lines.push(Line::from(format!( + "Users {} / max {}", + s.users, s.max_users + ))); + lines.push(Line::from(format!("Tables open {}", s.tables))); + lines.push(Line::from(format!("Work areas {}", s.workareas))); + lines.push(Line::from(format!("Locks {}", s.locks))); + lines.push(Line::from(format!("Operations {}", s.operations))); + lines.push(Line::from(format!("Errors logged {}", s.logged_errors))); + lines.push(Line::from(format!( + "Memory RSS {}", + wire::format_bytes(s.rss_bytes) + ))); + lines.push(Line::from(format!("Listener port {}", s.server_port))); + if !s.user_list.is_empty() { + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + "Wire users", + Style::default().add_modifier(Modifier::UNDERLINED), + ))); + for u in s.user_list.iter().take(8) { + lines.push(Line::from(format!( + " #{} {} @ {}", + u.conn_no, u.name, u.address + ))); + } + } + } else if let Some(e) = &app.inner.mg_err { + lines.push(Line::from(Span::styled( + format!("Wire offline: {e}"), + Style::default().fg(Color::Red), + ))); + } + if let Some(h) = &app.inner.http.health { + lines.push(Line::from("")); + lines.push(Line::from(format!("Studio mode {}", h.mode))); + lines.push(Line::from(format!("Data dir {}", h.data_dir))); + } + Paragraph::new(lines).block( + Block::default() + .borders(Borders::ALL) + .title("Activity (wire Mg)"), + ) +} + +fn sessions_panel(app: &TuiState) -> Table<'_> { + let header = Row::new(vec!["ID", "Peer", "User", "Tbl", "Idle"]) + .style(Style::default().add_modifier(Modifier::BOLD)) + .bottom_margin(1); + let rows: Vec = app + .inner + .http + .sessions + .iter() + .enumerate() + .map(|(i, s)| { + let style = if i == app.selected_session { + Style::default().bg(Color::DarkGray) + } else { + Style::default() + }; + Row::new(vec![ + Cell::from(s.id.to_string()), + Cell::from(format!("{}:{}", s.peer_ip, s.peer_port)), + Cell::from(s.user.clone()), + Cell::from(s.open_tables.to_string()), + Cell::from(format!("{}s", s.idle_secs)), + ]) + .style(style) + }) + .collect(); + let hint = if app.inner.http.sessions.is_empty() { + "no HTTP sessions (start server with --http-port)" + } else { + "HTTP wire sessions" + }; + Table::new( + rows, + [ + Constraint::Length(6), + Constraint::Length(18), + Constraint::Min(10), + Constraint::Length(4), + Constraint::Length(6), + ], + ) + .header(header) + .block(Block::default().borders(Borders::ALL).title(hint)) +} + +fn comm_panel(app: &TuiState) -> Paragraph<'_> { + let mut lines = Vec::new(); + if let Some(s) = &app.inner.mg { + lines.push(Line::from(format!( + "Packets in/out {} / {}", + s.packets_in, s.packets_out + ))); + lines.push(Line::from(format!( + "Bytes in/out {} / {}", + wire::format_bytes(s.bytes_in), + wire::format_bytes(s.bytes_out) + ))); + lines.push(Line::from(format!( + "Disconnects {} Partial connects {}", + s.disconnects, s.partial_connects + ))); + if !s.table_list.is_empty() { + lines.push(Line::from(Span::styled( + "Open tables (wire)", + Style::default().add_modifier(Modifier::UNDERLINED), + ))); + for t in s.table_list.iter().take(5) { + lines.push(Line::from(format!( + " {} — {} (#{})", + t.name, t.user, t.conn_no + ))); + } + } + } + if let Some(info) = &app.inner.http.server_info { + if let Some(v) = info.get("version").and_then(|v| v.as_str()) { + lines.push(Line::from(format!("Engine version {v}"))); + } + if let Some(n) = info.get("tables").and_then(|v| v.as_array()) { + lines.push(Line::from(format!("Data tables (disk) {}", n.len()))); + } + } + if let Some(e) = &app.inner.http.error { + lines.push(Line::from(Span::styled( + format!("HTTP: {e}"), + Style::default().fg(Color::Yellow), + ))); + } + Paragraph::new(lines).block( + Block::default() + .borders(Borders::ALL) + .title("Communication / data"), + ) +} \ No newline at end of file diff --git a/tools/openmonitor/src/snapshot.rs b/tools/openmonitor/src/snapshot.rs new file mode 100644 index 00000000..6237b39c --- /dev/null +++ b/tools/openmonitor/src/snapshot.rs @@ -0,0 +1,222 @@ +use crate::history::MetricHistory; +use crate::http_api::{Health, HttpSnapshot, Session}; +use crate::wire::{self, MgSnapshot}; +use serde::Serialize; + +#[derive(Debug, Clone)] +pub struct MonitorConfig { + pub wire_host: String, + pub wire_port: u16, + pub http: Option, + pub interval_secs: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct SeriesView { + pub label: String, + pub values: Vec, + pub current: u64, + pub max: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct WireUserView { + pub conn_no: u16, + pub name: String, + pub address: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct WireView { + pub online: bool, + pub error: Option, + pub uptime: Option, + pub connections: u32, + pub max_connections: u32, + pub users: u32, + pub max_users: u32, + pub tables: u32, + pub operations: u64, + pub packets_in: u64, + pub packets_out: u64, + pub bytes_in: String, + pub bytes_out: String, + pub rss: String, + pub listener_port: u16, + pub wire_users: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct HttpView { + pub configured: bool, + pub error: Option, + pub status: Option, + pub mode: Option, + pub data_dir: Option, + pub version: Option, + pub disk_tables: Option, + pub sessions: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct MonitorSnapshot { + pub wire_host: String, + pub wire_port: u16, + pub http_url: Option, + pub updated_at: String, + pub wire: WireView, + pub http: HttpView, + pub series: Vec, +} + +pub struct MonitorState { + pub mg: Option, + pub mg_err: Option, + pub http: HttpSnapshot, + pub history: MetricHistory, +} + +impl MonitorState { + pub fn refresh(&mut self, cfg: &MonitorConfig) { + match wire::fetch_mg_snapshot(&cfg.wire_host, cfg.wire_port) { + Ok(s) => { + self.history.push(&s); + self.mg = Some(s); + self.mg_err = None; + } + Err(e) => { + self.mg = None; + self.mg_err = Some(e.to_string()); + } + } + if let Some(base) = &cfg.http { + self.http = crate::http_api::HttpClient::new(base).poll(); + } else { + self.http = HttpSnapshot::default(); + } + } + + pub fn to_snapshot(&self, cfg: &MonitorConfig) -> MonitorSnapshot { + let wire = build_wire_view(&self.mg, &self.mg_err); + let http = build_http_view(&self.http, cfg.http.is_some()); + let series = build_series(&self.history, &self.mg); + MonitorSnapshot { + wire_host: cfg.wire_host.clone(), + wire_port: cfg.wire_port, + http_url: cfg.http.clone(), + updated_at: chrono::Local::now().format("%H:%M:%S").to_string(), + wire, + http, + series, + } + } +} + +fn build_wire_view(mg: &Option, err: &Option) -> WireView { + match mg { + Some(s) => WireView { + online: true, + error: None, + uptime: Some(wire::format_uptime(s.uptime_seconds)), + connections: s.connections, + max_connections: s.max_connections, + users: s.users, + max_users: s.max_users, + tables: s.tables, + operations: s.operations, + packets_in: s.packets_in, + packets_out: s.packets_out, + bytes_in: wire::format_bytes(s.bytes_in), + bytes_out: wire::format_bytes(s.bytes_out), + rss: wire::format_bytes(s.rss_bytes), + listener_port: s.server_port, + wire_users: s + .user_list + .iter() + .map(|u| WireUserView { + conn_no: u.conn_no, + name: u.name.clone(), + address: u.address.clone(), + }) + .collect(), + }, + None => WireView { + online: false, + error: err.clone(), + uptime: None, + connections: 0, + max_connections: 0, + users: 0, + max_users: 0, + tables: 0, + operations: 0, + packets_in: 0, + packets_out: 0, + bytes_in: "—".into(), + bytes_out: "—".into(), + rss: "—".into(), + listener_port: 0, + wire_users: vec![], + }, + } +} + +fn build_http_view(http: &HttpSnapshot, configured: bool) -> HttpView { + let health: Option<&Health> = http.health.as_ref(); + HttpView { + configured, + error: http.error.clone(), + status: health.map(|h| h.status.clone()), + mode: health.map(|h| h.mode.clone()), + data_dir: health.map(|h| h.data_dir.clone()), + version: http + .server_info + .as_ref() + .and_then(|v| v.get("version")) + .and_then(|v| v.as_str()) + .map(str::to_string), + disk_tables: http + .server_info + .as_ref() + .and_then(|v| v.get("tables")) + .and_then(|v| v.as_array()) + .map(|a| a.len()), + sessions: http.sessions.clone(), + } +} + +fn build_series(history: &MetricHistory, mg: &Option) -> Vec { + let max_conn = mg.as_ref().map(|s| s.max_connections as u64).unwrap_or(1); + let max_users = mg.as_ref().map(|s| s.max_users as u64).unwrap_or(1); + vec![ + series_one("Conexões", history.connections(), max_conn), + series_one("Usuários", history.users(), max_users), + series_one( + "Ops/s", + history.ops_per_sec(), + MetricHistory::max_in(history.ops_per_sec()), + ), + series_one( + "Pkts/s", + history.pkt_per_sec(), + MetricHistory::max_in(history.pkt_per_sec()), + ), + series_one( + "RSS KB", + history.rss_kb(), + MetricHistory::max_in(history.rss_kb()), + ), + ] +} + +fn series_one(label: &str, data: &[u64], scale_max: u64) -> SeriesView { + let values: Vec = if data.is_empty() { vec![0] } else { data.to_vec() }; + let current = *values.last().unwrap_or(&0); + let peak = values.iter().copied().max().unwrap_or(1).max(scale_max).max(1); + SeriesView { + label: label.to_string(), + values, + current, + max: peak, + } +} \ No newline at end of file diff --git a/tools/openmonitor/src/web.rs b/tools/openmonitor/src/web.rs new file mode 100644 index 00000000..6bba7dcc --- /dev/null +++ b/tools/openmonitor/src/web.rs @@ -0,0 +1,109 @@ +use crate::http_api::HttpClient; +use crate::snapshot::{MonitorConfig, MonitorSnapshot, MonitorState}; +use anyhow::{Context, Result}; +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{Html, IntoResponse, Json}, + routing::{get, post}, + Router, +}; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::Duration; +use tokio::net::TcpListener; + +#[derive(Clone)] +struct WebState { + cfg: MonitorConfig, + snap: Arc>, + poll: Arc>, +} + +pub fn run(cfg: MonitorConfig, host: &str, port: u16) -> Result<()> { + let poll = Arc::new(RwLock::new(MonitorState { + mg: None, + mg_err: None, + http: Default::default(), + history: Default::default(), + })); + + { + let mut state = poll.write().unwrap(); + state.refresh(&cfg); + } + + let initial = poll.read().unwrap().to_snapshot(&cfg); + let snap = Arc::new(RwLock::new(initial)); + let web = WebState { + cfg: cfg.clone(), + snap: snap.clone(), + poll: poll.clone(), + }; + + let interval = Duration::from_secs(cfg.interval_secs.max(1)); + let poller_cfg = cfg.clone(); + thread::spawn(move || loop { + thread::sleep(interval); + if let Ok(mut state) = poll.write() { + state.refresh(&poller_cfg); + if let Ok(mut out) = snap.write() { + *out = state.to_snapshot(&poller_cfg); + } + } + }); + + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("tokio runtime")?; + + rt.block_on(async move { + let app = Router::new() + .route("/", get(index)) + .route("/api/snapshot", get(api_snapshot)) + .route("/api/sessions/{id}/kill", post(api_kill_session)) + .with_state(web); + + let addr = format!("{host}:{port}"); + let listener = TcpListener::bind(&addr) + .await + .with_context(|| format!("bind web UI {addr}"))?; + eprintln!("[openmonitor] web UI http://{addr}/"); + axum::serve(listener, app) + .await + .context("axum serve")?; + Ok::<(), anyhow::Error>(()) + })?; + + Ok(()) +} + +async fn index() -> Html<&'static str> { + Html(include_str!("../assets/dashboard.html")) +} + +async fn api_snapshot(State(st): State) -> Json { + Json(st.snap.read().unwrap().clone()) +} + +async fn api_kill_session( + State(st): State, + Path(id): Path, +) -> impl IntoResponse { + let Some(base) = st.cfg.http.clone() else { + return (StatusCode::BAD_REQUEST, "monitor started without --http").into_response(); + }; + match HttpClient::new(&base).kill_session(id) { + Ok(()) => { + if let Ok(mut state) = st.poll.write() { + state.refresh(&st.cfg); + if let Ok(mut out) = st.snap.write() { + *out = state.to_snapshot(&st.cfg); + } + } + StatusCode::OK.into_response() + } + Err(e) => (StatusCode::BAD_GATEWAY, e.to_string()).into_response(), + } +} \ No newline at end of file diff --git a/tools/openmonitor/src/wire.rs b/tools/openmonitor/src/wire.rs new file mode 100644 index 00000000..cbc841dc --- /dev/null +++ b/tools/openmonitor/src/wire.rs @@ -0,0 +1,230 @@ +use anyhow::{anyhow, Context, Result}; +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::time::Duration; + +const OPCODE_MG_REQUEST: u8 = 0xA2; +const OPCODE_MG_REPLY_ACK: u8 = 0xA3; + +#[derive(Debug, Clone, Default)] +pub struct MgUser { + pub name: String, + pub address: String, + pub os_login: String, + pub conn_no: u16, + pub connected_at: u64, +} + +#[derive(Debug, Clone, Default)] +pub struct MgTable { + pub name: String, + pub user: String, + pub conn_no: u16, + pub open_mode: u16, + pub lock_type: u16, +} + +#[derive(Debug, Clone, Default)] +pub struct MgSnapshot { + pub users: u32, + pub connections: u32, + pub workareas: u32, + pub tables: u32, + pub indexes: u32, + pub locks: u32, + pub worker_threads: u32, + pub server_type: u16, + pub rss_bytes: u64, + pub server_port: u16, + pub uptime_seconds: u64, + pub packets_in: u64, + pub packets_out: u64, + pub bytes_in: u64, + pub bytes_out: u64, + pub disconnects: u64, + pub partial_connects: u64, + pub operations: u64, + pub logged_errors: u64, + pub max_users: u32, + pub max_connections: u32, + pub user_list: Vec, + pub table_list: Vec, +} + +struct Reader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Reader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn remaining(&self) -> usize { + self.data.len().saturating_sub(self.pos) + } + + fn u16(&mut self) -> Result { + if self.remaining() < 2 { + return Err(anyhow!("truncated u16")); + } + let v = u16::from_le_bytes([self.data[self.pos], self.data[self.pos + 1]]); + self.pos += 2; + Ok(v) + } + + fn u32(&mut self) -> Result { + if self.remaining() < 4 { + return Err(anyhow!("truncated u32")); + } + let mut buf = [0u8; 4]; + buf.copy_from_slice(&self.data[self.pos..self.pos + 4]); + self.pos += 4; + Ok(u32::from_le_bytes(buf)) + } + + fn u64(&mut self) -> Result { + if self.remaining() < 8 { + return Err(anyhow!("truncated u64")); + } + let mut buf = [0u8; 8]; + buf.copy_from_slice(&self.data[self.pos..self.pos + 8]); + self.pos += 8; + Ok(u64::from_le_bytes(buf)) + } + + fn str(&mut self) -> Result { + let n = self.u16()? as usize; + if self.remaining() < n { + return Err(anyhow!("truncated string")); + } + let s = std::str::from_utf8(&self.data[self.pos..self.pos + n]) + .context("invalid utf-8 in mg snapshot")? + .to_string(); + self.pos += n; + Ok(s) + } +} + +pub fn decode_mg_snapshot(payload: &[u8]) -> Result { + let mut r = Reader::new(payload); + let mut s = MgSnapshot::default(); + s.users = r.u32()?; + s.connections = r.u32()?; + s.workareas = r.u32()?; + s.tables = r.u32()?; + s.indexes = r.u32()?; + s.locks = r.u32()?; + s.worker_threads = r.u32()?; + s.server_type = r.u16()?; + s.rss_bytes = r.u64()?; + s.server_port = r.u16()?; + s.uptime_seconds = r.u64()?; + s.packets_in = r.u64()?; + s.packets_out = r.u64()?; + s.bytes_in = r.u64()?; + s.bytes_out = r.u64()?; + s.disconnects = r.u64()?; + s.partial_connects = r.u64()?; + s.operations = r.u64()?; + s.logged_errors = r.u64()?; + s.max_users = r.u32()?; + s.max_connections = r.u32()?; + let _ = r.u32()?; + let _ = r.u32()?; + let _ = r.u32()?; + let _ = r.u32()?; + + let nu = r.u32()?; + for _ in 0..nu { + s.user_list.push(MgUser { + name: r.str()?, + address: r.str()?, + os_login: r.str()?, + conn_no: r.u16()?, + connected_at: r.u64()?, + }); + } + let nt = r.u32()?; + for _ in 0..nt { + s.table_list.push(MgTable { + name: r.str()?, + user: r.str()?, + conn_no: r.u16()?, + open_mode: r.u16()?, + lock_type: r.u16()?, + }); + } + Ok(s) +} + +fn write_frame(stream: &mut TcpStream, opcode: u8, payload: &[u8]) -> Result<()> { + let len = payload.len() as u32; + let mut header = [0u8; 5]; + header[0..4].copy_from_slice(&len.to_be_bytes()); + header[4] = opcode; + stream + .write_all(&header) + .context("write frame header")?; + if !payload.is_empty() { + stream.write_all(payload).context("write frame payload")?; + } + Ok(()) +} + +fn read_frame(stream: &mut TcpStream) -> Result<(u8, Vec)> { + let mut header = [0u8; 5]; + stream + .read_exact(&mut header) + .context("read frame header")?; + let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize; + let opcode = header[4]; + let mut payload = vec![0u8; len]; + if len > 0 { + stream + .read_exact(&mut payload) + .context("read frame payload")?; + } + Ok((opcode, payload)) +} + +pub fn fetch_mg_snapshot(host: &str, port: u16) -> Result { + let addr = format!("{host}:{port}"); + let mut stream = TcpStream::connect(&addr).with_context(|| format!("connect {addr}"))?; + stream.set_read_timeout(Some(Duration::from_secs(5)))?; + stream.set_write_timeout(Some(Duration::from_secs(5)))?; + + // Snapshot request: kind=0x01, arg=0 + let req_payload = [0x01u8, 0x00, 0x00]; + write_frame(&mut stream, OPCODE_MG_REQUEST, &req_payload)?; + + let (opcode, payload) = read_frame(&mut stream)?; + if opcode != OPCODE_MG_REPLY_ACK { + return Err(anyhow!("unexpected mg reply opcode 0x{opcode:02X}")); + } + decode_mg_snapshot(&payload) +} + +pub fn format_uptime(secs: u64) -> String { + let days = secs / 86_400; + let hours = (secs % 86_400) / 3_600; + let mins = (secs % 3_600) / 60; + let s = secs % 60; + format!("{days}d {hours}h {mins}m {s}s") +} + +pub fn format_bytes(n: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + if n >= GB { + format!("{:.2} GB", n as f64 / GB as f64) + } else if n >= MB { + format!("{:.2} MB", n as f64 / MB as f64) + } else if n >= KB { + format!("{:.1} KB", n as f64 / KB as f64) + } else { + format!("{n} B") + } +} \ No newline at end of file From 321b2d93d2208beb34dd657346885083f18d9f05 Mon Sep 17 00:00:00 2001 From: Admnwk Date: Sat, 20 Jun 2026 20:42:33 -0300 Subject: [PATCH 2/6] fix: transaction rollback with index sync and default connection fallback Rollback restores record buffers via refresh_record_buffer() without unnecessary index reposition. AdsConnect60 falls back to the process default connection when the caller passes a null connection handle. Co-Authored-By: Grok Code --- src/abi/ace_exports.cpp | 43 +++++++++++++++++++++++++++++------ src/engine/table.cpp | 45 +++++++++++++++++++++++++++++++++---- src/engine/table.h | 9 ++++++++ src/session/connection.cpp | 46 ++++++++++++++++++++++---------------- 4 files changed, 113 insertions(+), 30 deletions(-) diff --git a/src/abi/ace_exports.cpp b/src/abi/ace_exports.cpp index bad7d68a..19263439 100644 --- a/src/abi/ace_exports.cpp +++ b/src/abi/ace_exports.cpp @@ -297,6 +297,32 @@ ADSHANDLE get_or_create_default_connection() { return h; } +// Harbour rddads: AdsConnect stores the handle globally; BEGIN/COMMIT/ +// ROLLBACK call AdsBeginTransaction(0). Resolve 0 to the last AdsConnect +// handle before falling back to cwd auto-connect. +ADSHANDLE& rddads_default_connection() noexcept { + static ADSHANDLE h = 0; + return h; +} + +ADSHANDLE resolve_connection_handle(ADSHANDLE hConnect) { + if (hConnect != 0) return hConnect; + ADSHANDLE h = rddads_default_connection(); + if (h != 0) { + auto& s = state(); + if (s.registry.lookup(h, HandleKind::Connection)) + return h; + rddads_default_connection() = 0; + } + return get_or_create_default_connection(); +} + +Connection* lookup_connection(ADSHANDLE hConnect) { + auto& s = state(); + return s.registry.lookup( + resolve_connection_handle(hConnect), HandleKind::Connection); +} + Table* get_table(ADSHANDLE h) { auto& s = state(); Table* t = s.registry.lookup(h, HandleKind::Table); @@ -834,6 +860,7 @@ UNSIGNED32 AdsConnect60(UNSIGNED8* pucServer, UNSIGNED16 /*usServerType*/, Handle h = s.registry.register_object(HandleKind::Connection, raw); s.conns.emplace(h, std::move(holder)); *phConnect = h; + rddads_default_connection() = h; // Return a non-fatal warning when the DD has SAP-written ACL permissions // that must be imported before OpenADS can enforce them. The connection // handle IS valid; callers should disconnect, run openads_import_dd, and @@ -893,6 +920,8 @@ UNSIGNED32 AdsDisconnect(ADSHANDLE hConnect) { s.registry.release(h); } } + if (hConnect == rddads_default_connection()) + rddads_default_connection() = 0; s.registry.release(hConnect); s.conns.erase(hConnect); return ok(); @@ -7171,7 +7200,7 @@ UNSIGNED32 AdsDecryptRecord(ADSHANDLE /*hTable*/) { UNSIGNED32 AdsBeginTransaction(ADSHANDLE hConnect) { auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c) return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); auto r = c->begin_tx(); if (!r) return fail(r.error()); @@ -7181,7 +7210,7 @@ UNSIGNED32 AdsBeginTransaction(ADSHANDLE hConnect) { UNSIGNED32 AdsCommitTransaction(ADSHANDLE hConnect) { auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c) return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); auto r = c->commit_tx(); if (!r) return fail(r.error()); @@ -7191,7 +7220,7 @@ UNSIGNED32 AdsCommitTransaction(ADSHANDLE hConnect) { UNSIGNED32 AdsRollbackTransaction(ADSHANDLE hConnect) { auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c) return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); auto r = c->rollback_tx(); if (!r) return fail(r.error()); @@ -7201,7 +7230,7 @@ UNSIGNED32 AdsRollbackTransaction(ADSHANDLE hConnect) { UNSIGNED32 AdsInTransaction(ADSHANDLE hConnect, UNSIGNED16* pbInTx) { auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c || pbInTx == nullptr) { return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); } @@ -7294,7 +7323,7 @@ UNSIGNED32 AdsCreateSavepoint(ADSHANDLE hConnect, UNSIGNED8* pucName, (void)ulOptions; auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c || pucName == nullptr) { return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); } @@ -7309,7 +7338,7 @@ UNSIGNED32 AdsCreateSavepoint(ADSHANDLE hConnect, UNSIGNED8* pucName, UNSIGNED32 AdsReleaseSavepoint(ADSHANDLE hConnect, UNSIGNED8* pucName) { auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c || pucName == nullptr) { return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); } @@ -7326,7 +7355,7 @@ UNSIGNED32 AdsRollbackTransaction80(ADSHANDLE hConnect, UNSIGNED8* pucSavepoint, (void)ulOptions; auto& s = state(); std::lock_guard lk(s.mu); - Connection* c = s.registry.lookup(hConnect, HandleKind::Connection); + Connection* c = lookup_connection(hConnect); if (!c) return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); if (pucSavepoint == nullptr) { // Full rollback if no savepoint name supplied (matches ACE legacy). diff --git a/src/engine/table.cpp b/src/engine/table.cpp index 612381fb..680c7064 100644 --- a/src/engine/table.cpp +++ b/src/engine/table.cpp @@ -732,20 +732,57 @@ Table::field_memo_type(std::uint16_t idx) { return memo_->read_type(block_no); } +util::Result Table::apply_tx_rollback(std::uint32_t recno, + const std::uint8_t* bytes, + std::size_t len) { + if (driver_ == nullptr) { + return util::Error{5000, 0, "no driver", ""}; + } + if (auto r = load_record_(recno); !r) return r.error(); + auto snap = snapshot_index_keys_(); + const std::size_t rl = driver_->record_length(); + if (len != rl) { + return util::Error{5000, 0, "rollback record length mismatch", ""}; + } + std::memcpy(record_buf_.data(), bytes, len); + if (auto w = driver_->write_record_raw(recno, record_buf_.data(), rl); !w) { + return w.error(); + } + return sync_all_indexes_(snap); +} + +util::Result Table::apply_tx_rollback_append(std::uint32_t recno) { + if (driver_ == nullptr) { + return util::Error{5000, 0, "no driver", ""}; + } + if (auto r = load_record_(recno); !r) return r.error(); + auto snap = snapshot_index_keys_(); + drivers::set_record_deleted(record_buf_.data(), record_buf_.size(), true); + if (auto w = driver_->write_record_raw(recno, record_buf_.data(), + record_buf_.size()); !w) { + return w.error(); + } + return sync_all_indexes_(snap); +} + util::Result Table::mark_deleted() { if (state_ != State::Positioned) { return util::Error{5026, 0, "no record positioned", ""}; } + auto snap = snapshot_index_keys_(); drivers::set_record_deleted(record_buf_.data(), record_buf_.size(), true); - return writeback_record_(); + if (auto wb = writeback_record_(); !wb) return wb.error(); + return sync_all_indexes_(snap); } util::Result Table::recall_deleted() { if (state_ != State::Positioned) { return util::Error{5026, 0, "no record positioned", ""}; } + auto snap = snapshot_index_keys_(); drivers::set_record_deleted(record_buf_.data(), record_buf_.size(), false); - return writeback_record_(); + if (auto wb = writeback_record_(); !wb) return wb.error(); + return sync_all_indexes_(snap); } bool Table::is_deleted() const noexcept { @@ -919,7 +956,7 @@ util::Result Table::lock_record_excl(std::uint32_t recno) { locking_, recno); if (!h) return h.error(); recno_locks_.emplace(recno, std::move(h).value()); - return {}; + return load_record_(recno); } util::Result Table::try_lock_record_excl(std::uint32_t recno) { @@ -928,7 +965,7 @@ util::Result Table::try_lock_record_excl(std::uint32_t recno) { locking_, recno); if (!h) return h.error(); recno_locks_.emplace(recno, std::move(h).value()); - return {}; + return load_record_(recno); } util::Result Table::unlock_record(std::uint32_t recno) { diff --git a/src/engine/table.h b/src/engine/table.h index eea024c2..ed1c8afa 100644 --- a/src/engine/table.h +++ b/src/engine/table.h @@ -124,6 +124,15 @@ class Table { util::Result mark_deleted(); util::Result recall_deleted(); bool is_deleted() const noexcept; + + // Transaction rollback helpers (M5). Restore a before-image or undo + // an append directly on disk and re-sync every bound index. Bypasses + // the active Tx journal — caller is rolling back the tx itself. + util::Result apply_tx_rollback(std::uint32_t recno, + const std::uint8_t* bytes, + std::size_t len); + util::Result apply_tx_rollback_append(std::uint32_t recno); + util::Result flush(); // Drop every record. Header rec count -> 0 and every bound index diff --git a/src/session/connection.cpp b/src/session/connection.cpp index 97858876..6f34ea62 100644 --- a/src/session/connection.cpp +++ b/src/session/connection.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -249,30 +250,33 @@ util::Result Connection::rollback_tx() { if (!tx_.active()) { return util::Error{5000, 0, "no active transaction", ""}; } + std::optional rollback_err; tx_.for_each_before_image( [&](const engine::Tx::RecordKey& k, const std::vector& bytes) { + if (rollback_err) return; auto it = tables_.find(static_cast(k.table)); if (it == tables_.end()) return; - auto* drv = it->second->driver(); - if (drv) { - (void)drv->write_record_raw(k.recno, bytes.data(), bytes.size()); + if (auto r = it->second->apply_tx_rollback(k.recno, bytes.data(), + bytes.size()); !r) { + rollback_err = r.error(); } }); tx_.for_each_append([&](const engine::Tx::RecordKey& k) { + if (rollback_err) return; auto it = tables_.find(static_cast(k.table)); if (it == tables_.end()) return; - auto* drv = it->second->driver(); - if (!drv) return; - auto rec = drv->read_record_raw(k.recno); - if (!rec) return; - auto buf = std::move(rec).value(); - openads::drivers::set_record_deleted(buf.data(), buf.size(), true); - (void)drv->write_record_raw(k.recno, buf.data(), buf.size()); + if (auto r = it->second->apply_tx_rollback_append(k.recno); !r) { + rollback_err = r.error(); + } }); + if (rollback_err) return util::Error{*rollback_err}; if (auto r = tx_log_.append_abort(tx_.id()); !r) return r.error(); for (auto& [h, holder] : tables_) { (void)h; + std::uint32_t rec = holder->recno(); + if (rec > 0 && rec <= holder->record_count()) + (void)holder->goto_record(rec); holder->detach_tx(); (void)holder->flush(); } @@ -308,20 +312,24 @@ Connection::rollback_to_savepoint(const std::string& name) { const auto& op = ops[i - 1]; auto it = tables_.find(static_cast(op.table)); if (it == tables_.end()) continue; - auto* drv = it->second->driver(); - if (!drv) continue; if (op.is_append) { - auto rec = drv->read_record_raw(op.recno); - if (!rec) continue; - auto buf = std::move(rec).value(); - openads::drivers::set_record_deleted(buf.data(), buf.size(), true); - (void)drv->write_record_raw(op.recno, buf.data(), buf.size()); + if (auto r = it->second->apply_tx_rollback_append(op.recno); !r) { + return r.error(); + } } else { - (void)drv->write_record_raw(op.recno, - op.before.data(), op.before.size()); + if (auto r = it->second->apply_tx_rollback( + op.recno, op.before.data(), op.before.size()); !r) { + return r.error(); + } } } tx_.truncate_ops_to(idx); + for (auto& [h, holder] : tables_) { + (void)h; + std::uint32_t rec = holder->recno(); + if (rec > 0 && rec <= holder->record_count()) + (void)holder->goto_record(rec); + } return {}; } From abcaadb48144f9953e138fc63b34a9d9710e6041 Mon Sep 17 00:00:00 2001 From: Admnwk Date: Sat, 20 Jun 2026 20:42:34 -0300 Subject: [PATCH 3/6] fix(cdx,tx): multi-tag CDX allocator, branch erase, and safe tx flush CDX multi-tag indexes on one file allocated pages independently per sub-tree, causing key loss and seek misses on secondary tags. Use a shared per-file page allocator and fix erase on branch trees after seek. Transaction commit/rollback flushed all bound tables, including handles already closed via AdsCloseTable, which could crash on rollback. Purge table bindings on close and flush only tables touched by the active transaction. Add CDX multi-level erase unit test and ABI M5 smoke tests for multi-tag rollback seek and post-commit index visibility. Co-Authored-By: Grok Code --- src/abi/ace_exports.cpp | 14 ++++ src/drivers/cdx/cdx_index.cpp | 103 ++++++++++++++++++----- src/drivers/cdx/cdx_index.h | 7 ++ src/engine/table.cpp | 5 ++ src/engine/table.h | 3 + src/session/connection.cpp | 42 ++++++++-- src/session/connection.h | 1 + tests/unit/abi_m5_smoke_test.cpp | 127 +++++++++++++++++++++++++++++ tests/unit/cdx_multilevel_test.cpp | 48 +++++++++++ 9 files changed, 321 insertions(+), 29 deletions(-) diff --git a/src/abi/ace_exports.cpp b/src/abi/ace_exports.cpp index 19263439..a7aad0e7 100644 --- a/src/abi/ace_exports.cpp +++ b/src/abi/ace_exports.cpp @@ -1933,9 +1933,16 @@ UNSIGNED32 AdsCloseAllTables(void) { for (Handle h : to_release) { Table* t = s.registry.lookup
(h, HandleKind::Table); if (t) { + Connection* owning = nullptr; + s.registry.for_each_handle([&](Handle, HandleKind k, void* p) { + if (k != HandleKind::Connection || owning) return; + auto* cc = static_cast(p); + if (cc->owns_table_ptr(t)) owning = cc; + }); (void)t->flush(); purge_bindings_for_table(t); purge_pending_binaries_for_table(t); + if (owning) owning->close_table_ptr(t); } s.registry.release(h); } @@ -1967,10 +1974,17 @@ UNSIGNED32 AdsCloseTable(ADSHANDLE hTable) { // inherit stale entries. Table* t = s.registry.lookup
(hTable, HandleKind::Table); if (t != nullptr) { + Connection* owning = nullptr; + s.registry.for_each_handle([&](Handle, HandleKind k, void* p) { + if (k != HandleKind::Connection || owning) return; + auto* cc = static_cast(p); + if (cc->owns_table_ptr(t)) owning = cc; + }); (void)t->flush(); purge_bindings_for_table(t); purge_pending_binaries_for_table(t); t->ri_snapshot().clear(); + if (owning) owning->close_table_ptr(t); } cursor_projections().erase(hTable); s.registry.release(hTable); diff --git a/src/drivers/cdx/cdx_index.cpp b/src/drivers/cdx/cdx_index.cpp index cfe1cd65..a9a143fe 100644 --- a/src/drivers/cdx/cdx_index.cpp +++ b/src/drivers/cdx/cdx_index.cpp @@ -2,11 +2,16 @@ #include #include +#include +#include namespace openads::drivers::cdx { namespace { +std::mutex g_cdx_alloc_mu; +std::unordered_map g_cdx_alloc_tail; + std::uint16_t read_u16_le(const std::uint8_t* p) { return static_cast(p[0]) | static_cast(p[1] << 8); @@ -405,7 +410,8 @@ util::Result CdxIndex::open_named(const std::string& path, IndexOpenMode mode, const std::string& tag_name) { - mode_ = mode; + mode_ = mode; + path_ = path; auto fres = platform::File::open(path, map_mode(mode)); if (!fres) return fres.error(); file_ = std::move(fres).value(); @@ -855,9 +861,7 @@ CdxIndex::insert_into_subtree_(std::uint32_t subtree_root, "CDX leaf split: cannot fit both halves", ""}; } - std::uint32_t new_off = static_cast(file_size_); - page_cache_.emplace(new_off, Page{}); - file_size_ += CDX_PAGE_LEN; + std::uint32_t new_off = allocate_page_(); if (auto e = encode_leaf_(subtree_root, left_keys, left_sib, new_off); !e) @@ -968,9 +972,7 @@ CdxIndex::insert_into_subtree_(std::uint32_t subtree_root, std::vector right_be(entries.begin() + static_cast(mid), entries.end()); - std::uint32_t new_off = static_cast(file_size_); - page_cache_.emplace(new_off, Page{}); - file_size_ += CDX_PAGE_LEN; + std::uint32_t new_off = allocate_page_(); auto lpg = get_page_(subtree_root); if (!lpg) return lpg.error(); @@ -1013,16 +1015,12 @@ CdxIndex::insert(std::uint32_t recno, const std::string& key) { if (padded.size() > key_size_) padded.resize(key_size_); if (root_page_ == 0) { - std::uint32_t base = static_cast( - std::max(file_size_, CDX_SUB_DATA_BASE)); - std::uint32_t off = base; - page_cache_.emplace(off, Page{}); + std::uint32_t off = allocate_page_(); std::vector> keys{{padded, recno}}; if (auto e = encode_leaf_(off, keys, 0xFFFFFFFFu, 0xFFFFFFFFu); !e) { return e.error(); } root_page_ = off; - file_size_ = off + CDX_PAGE_LEN; return rewrite_header_(); } @@ -1033,9 +1031,7 @@ CdxIndex::insert(std::uint32_t recno, const std::string& key) { if (!promote.have) return {}; // Root split → allocate new branch root with two children. - std::uint32_t new_root = static_cast(file_size_); - page_cache_.emplace(new_root, Page{}); - file_size_ += CDX_PAGE_LEN; + std::uint32_t new_root = allocate_page_(); std::vector entries = { { promote.left_max_key, promote.left_max_recno, promote.old_left_off }, @@ -1058,14 +1054,53 @@ CdxIndex::erase(std::uint32_t recno, const std::string& key) { } if (root_page_ == 0) return util::Error{5044, 0, "CDX empty", ""}; - auto dec = decode_leaf_(root_page_); - if (!dec) return dec.error(); - auto keys = std::move(dec).value(); - std::string padded = key; if (padded.size() < key_size_) padded.append(key_size_ - padded.size(), ' '); if (padded.size() > key_size_) padded.resize(key_size_); + // Locate (key, recno) via the same leaf-chain walk seek_key uses. + // The old root-only decode path silently failed once insert_into_ + // subtree_ promoted a branch root, leaving stale keys behind. + auto sk = seek_key(padded, /*soft=*/false); + if (!sk) return sk.error(); + + bool found = false; + if (sk.value().positioned && sk.value().hit == SeekHit::Exact) { + std::uint32_t guard = 0; + while (guard++ < 4096) { + if (cur_index_ >= 0 && + static_cast(cur_index_) < cur_decoded_.size()) { + const auto& e = + cur_decoded_[static_cast(cur_index_)]; + if (e.first == padded && (recno == 0 || e.second == recno)) { + found = true; + break; + } + if (e.first != padded) break; + } + auto nx = next(); + if (!nx || !nx.value().positioned) break; + if (nx.value().hit != SeekHit::Exact) break; + std::string ck = current_key(); + if (ck.size() < padded.size()) + ck.append(padded.size() - ck.size(), ' '); + if (ck.size() > padded.size()) ck.resize(padded.size()); + if (ck != padded) break; + } + } + if (!found) { + return util::Error{5044, 0, "CDX key not found", ""}; + } + + auto pg = get_page_(cur_leaf_); + if (!pg) return pg.error(); + std::uint32_t left_sib = read_u32_le(pg.value()->data() + 4); + std::uint32_t right_sib = read_u32_le(pg.value()->data() + 8); + + auto dec = decode_leaf_(cur_leaf_); + if (!dec) return dec.error(); + auto keys = std::move(dec).value(); + auto it = std::find_if(keys.begin(), keys.end(), [&](const auto& kv) { return kv.first == padded && (recno == 0 || kv.second == recno); @@ -1074,7 +1109,8 @@ CdxIndex::erase(std::uint32_t recno, const std::string& key) { return util::Error{5044, 0, "CDX key not found", ""}; } keys.erase(it); - return encode_leaf_(root_page_, keys, 0xFFFFFFFFu, 0xFFFFFFFFu); + invalidate_cursor(); + return encode_leaf_(cur_leaf_, keys, left_sib, right_sib); } util::Result CdxIndex::flush() { @@ -1130,6 +1166,21 @@ util::Result CdxIndex::clear_data() { return rewrite_header_(); } +std::uint32_t CdxIndex::allocate_page_() { + std::lock_guard lk(g_cdx_alloc_mu); + auto& tail = g_cdx_alloc_tail[path_]; + if (auto sz = file_.size()) { + tail = std::max(tail, sz.value()); + } + tail = std::max(tail, file_size_); + tail = std::max(tail, static_cast(CDX_SUB_DATA_BASE)); + const std::uint32_t off = static_cast(tail); + tail += CDX_PAGE_LEN; + file_size_ = tail; + page_cache_.emplace(off, Page{}); + return off; +} + util::Result CdxIndex::rewrite_header_() { if (sub_header_offset_ == 0) { return util::Error{6106, 0, "CDX sub-tag header offset uninitialised", ""}; @@ -1211,6 +1262,7 @@ CdxIndex::create(const std::string& path, if (auto s = file.sync(); !s) return s.error(); CdxIndex ix; + ix.path_ = path; ix.file_ = std::move(file); ix.mode_ = IndexOpenMode::Shared; ix.root_page_ = 0; @@ -1225,6 +1277,11 @@ CdxIndex::create(const std::string& path, ix.tag_name_ = tag_name; ix.sub_header_offset_ = CDX_SUB_HEADER_OFFSET; ix.file_size_ = CDX_SUB_DATA_BASE; + { + std::lock_guard lk(g_cdx_alloc_mu); + g_cdx_alloc_tail[path] = std::max(g_cdx_alloc_tail[path], + static_cast(CDX_SUB_DATA_BASE)); + } return ix; } @@ -1340,6 +1397,7 @@ CdxIndex::add_tag(const std::string& path, if (auto s = file.sync(); !s) return s.error(); CdxIndex ix; + ix.path_ = path; ix.file_ = std::move(file); ix.mode_ = IndexOpenMode::Shared; ix.root_page_ = 0; @@ -1354,6 +1412,11 @@ CdxIndex::add_tag(const std::string& path, ix.tag_name_ = tag_name; ix.sub_header_offset_ = static_cast(new_off); ix.file_size_ = new_off + CDX_HEADER_LEN; + { + std::lock_guard lk(g_cdx_alloc_mu); + g_cdx_alloc_tail[path] = std::max(g_cdx_alloc_tail[path], + new_off + CDX_HEADER_LEN); + } return ix; } diff --git a/src/drivers/cdx/cdx_index.h b/src/drivers/cdx/cdx_index.h index b71f34a3..4d5d8937 100644 --- a/src/drivers/cdx/cdx_index.h +++ b/src/drivers/cdx/cdx_index.h @@ -152,7 +152,14 @@ class CdxIndex final : public IIndex { util::Result rewrite_header_(); + // Allocate a fresh 512-byte page at the next free offset for this + // CDX file. Compound files host several sub-tags; every sub-tag's + // CdxIndex shares one allocator keyed by path so their page + // streams cannot collide. + std::uint32_t allocate_page_(); + platform::File file_; + std::string path_; IndexOpenMode mode_ = IndexOpenMode::ReadOnly; std::uint32_t root_page_ = 0; std::uint32_t free_ptr_ = 0xFFFFFFFFu; diff --git a/src/engine/table.cpp b/src/engine/table.cpp index 680c7064..179f3f4c 100644 --- a/src/engine/table.cpp +++ b/src/engine/table.cpp @@ -386,6 +386,11 @@ util::Result Table::goto_record(std::uint32_t recno) { return {}; } +util::Result Table::refresh_record_buffer() { + if (state_ != State::Positioned || recno_ == 0) return {}; + return load_record_(recno_); +} + util::Result Table::skip(std::int32_t delta) { if (!recno_sequence_.empty()) { if (delta == 0) return {}; diff --git a/src/engine/table.h b/src/engine/table.h index ed1c8afa..30389c91 100644 --- a/src/engine/table.h +++ b/src/engine/table.h @@ -95,6 +95,9 @@ class Table { util::Result goto_top(); util::Result goto_bottom(); util::Result goto_record(std::uint32_t recno); + // Reload the current row from disk without repositioning the active + // index cursor — used after transaction rollback refresh. + util::Result refresh_record_buffer(); util::Result skip(std::int32_t delta); util::Result diff --git a/src/session/connection.cpp b/src/session/connection.cpp index 6f34ea62..601e81f6 100644 --- a/src/session/connection.cpp +++ b/src/session/connection.cpp @@ -235,9 +235,13 @@ util::Result Connection::commit_tx() { return {}; } if (auto r = tx_log_.append_commit(tx_.id()); !r) return r.error(); + std::unordered_set touched; + for (const auto& op : tx_.ops()) { + touched.insert(static_cast(op.table)); + } for (auto& [h, holder] : tables_) { - (void)h; holder->detach_tx(); + if (touched.count(h) == 0) continue; (void)holder->flush(); } tx_.clear(); @@ -250,6 +254,15 @@ util::Result Connection::rollback_tx() { if (!tx_.active()) { return util::Error{5000, 0, "no active transaction", ""}; } + std::unordered_set touched; + tx_.for_each_before_image( + [&](const engine::Tx::RecordKey& k, + const std::vector&) { + touched.insert(static_cast(k.table)); + }); + tx_.for_each_append([&](const engine::Tx::RecordKey& k) { + touched.insert(static_cast(k.table)); + }); std::optional rollback_err; tx_.for_each_before_image( [&](const engine::Tx::RecordKey& k, @@ -273,11 +286,9 @@ util::Result Connection::rollback_tx() { if (rollback_err) return util::Error{*rollback_err}; if (auto r = tx_log_.append_abort(tx_.id()); !r) return r.error(); for (auto& [h, holder] : tables_) { - (void)h; - std::uint32_t rec = holder->recno(); - if (rec > 0 && rec <= holder->record_count()) - (void)holder->goto_record(rec); holder->detach_tx(); + if (touched.count(h) == 0) continue; + (void)holder->refresh_record_buffer(); (void)holder->flush(); } tx_.clear(); @@ -324,11 +335,13 @@ Connection::rollback_to_savepoint(const std::string& name) { } } tx_.truncate_ops_to(idx); + std::unordered_set touched; + for (const auto& op : tx_.ops()) { + touched.insert(static_cast(op.table)); + } for (auto& [h, holder] : tables_) { - (void)h; - std::uint32_t rec = holder->recno(); - if (rec > 0 && rec <= holder->record_count()) - (void)holder->goto_record(rec); + if (touched.count(h) == 0) continue; + (void)holder->refresh_record_buffer(); } return {}; } @@ -462,6 +475,17 @@ void Connection::close_table(Handle h) { table_paths_.erase(h); } +void Connection::close_table_ptr(const engine::Table* t) { + for (auto it = tables_.begin(); it != tables_.end(); ++it) { + if (it->second.get() == t) { + it->second->detach_tx(); + table_paths_.erase(it->first); + tables_.erase(it); + return; + } + } +} + engine::Table* Connection::lookup_table(Handle h) { auto it = tables_.find(h); if (it == tables_.end()) return nullptr; diff --git a/src/session/connection.h b/src/session/connection.h index 4e62b395..74365cf1 100644 --- a/src/session/connection.h +++ b/src/session/connection.h @@ -37,6 +37,7 @@ class Connection { engine::LockingMode locking = engine::LockingMode::Compatible); void close_table(Handle h); + void close_table_ptr(const engine::Table* t); engine::Table* lookup_table(Handle h); diff --git a/tests/unit/abi_m5_smoke_test.cpp b/tests/unit/abi_m5_smoke_test.cpp index 70970e73..8d6afb92 100644 --- a/tests/unit/abi_m5_smoke_test.cpp +++ b/tests/unit/abi_m5_smoke_test.cpp @@ -41,6 +41,70 @@ fs::path make_dbf(const fs::path& dir, const char* leaf) { return p; } +std::string trim_field(const UNSIGNED8* buf, UNSIGNED32 cap) { + std::string s(reinterpret_cast(buf), cap); + while (!s.empty() && s.back() == ' ') s.pop_back(); + return s; +} + +struct RelFixture { + fs::path dir; + ADSHANDLE hConn = 0; + + explicit RelFixture(const char* leaf) + : dir(fs::temp_directory_path() / leaf) {} + + void connect() { + std::error_code ec; + fs::remove_all(dir, ec); + fs::create_directories(dir); + std::vector srv(dir.string().size() + 1); + std::memcpy(srv.data(), dir.string().c_str(), dir.string().size() + 1); + REQUIRE(AdsConnect60(srv.data(), ADS_LOCAL_SERVER, + nullptr, nullptr, 0, &hConn) == 0); + } + + ADSHANDLE open_ord() { + ADSHANDLE h = 0; + UNSIGNED8 def[] = + "ORDNO,C,8,0;CUSTNO,C,8,0;AMT,N,10,0"; + REQUIRE(AdsCreateTable(hConn, (UNSIGNED8*)"ord", nullptr, ADS_CDX, + 0, 0, 0, 0, def, &h) == 0); + ADSHANDLE hIdx = 0; + REQUIRE(AdsCreateIndex61(h, (UNSIGNED8*)"ord.cdx", (UNSIGNED8*)"ORDNO", + (UNSIGNED8*)"ORDNO", nullptr, nullptr, + 0, 0, &hIdx) == 0); + REQUIRE(AdsCreateIndex61(h, (UNSIGNED8*)"ord.cdx", (UNSIGNED8*)"CUSTNO", + (UNSIGNED8*)"CUSTNO", nullptr, nullptr, + 0, 0, &hIdx) == 0); + REQUIRE(AdsCloseIndex(hIdx) == 0); + return h; + } + + ADSHANDLE open_ordln() { + ADSHANDLE h = 0; + UNSIGNED8 def[] = + "ORDNO,C,8,0;LINENO,N,3,0;SKU,C,8,0;QTY,N,5,0"; + REQUIRE(AdsCreateTable(hConn, (UNSIGNED8*)"ordln", nullptr, ADS_CDX, + 0, 0, 0, 0, def, &h) == 0); + ADSHANDLE hIdx = 0; + REQUIRE(AdsCreateIndex61(h, (UNSIGNED8*)"ordln.cdx", (UNSIGNED8*)"ORDNO", + (UNSIGNED8*)"ORDNO", nullptr, nullptr, + 0, 0, &hIdx) == 0); + REQUIRE(AdsCloseIndex(hIdx) == 0); + return h; + } + + void teardown() { + if (hConn) { + AdsDisconnect(hConn); + hConn = 0; + } + std::error_code ec; + fs::remove_all(dir, ec); + } +}; + } // namespace TEST_CASE("ABI M5 smoke: BeginTransaction + update + Rollback restores the original record") { @@ -180,3 +244,66 @@ TEST_CASE("ABI M5 smoke: Commit makes changes durable") { REQUIRE(AdsDisconnect(hConn) == 0); fs::remove_all(dir, ec); } + +TEST_CASE("ABI M5 smoke: multi-tag CDX rollback seek finds deleted order") { + RelFixture fx("openads_m5_multitag_rb"); + fx.connect(); + ADSHANDLE hOrd = fx.open_ord(); + + const char* cOrd = "TXMULRB"; + REQUIRE(AdsBeginTransaction(fx.hConn) == 0); + REQUIRE(AdsAppendRecord(hOrd) == 0); + REQUIRE(AdsSetString(hOrd, (UNSIGNED8*)"ORDNO", (UNSIGNED8*)cOrd, 7) == 0); + REQUIRE(AdsSetString(hOrd, (UNSIGNED8*)"CUSTNO", (UNSIGNED8*)"C00001", 6) == 0); + REQUIRE(AdsSetDouble(hOrd, (UNSIGNED8*)"AMT", 1.0) == 0); + REQUIRE(AdsWriteRecord(hOrd) == 0); + REQUIRE(AdsRollbackTransaction(fx.hConn) == 0); + + REQUIRE(AdsShowDeleted(1) == 0); + ADSHANDLE hIdx = 0; + REQUIRE(AdsGetIndexHandle(hOrd, (UNSIGNED8*)"ORDNO", &hIdx) == 0); + UNSIGNED16 found = 0; + REQUIRE(AdsSeek(hIdx, (UNSIGNED8*)cOrd, 7, 0, 0, &found) == 0); + CHECK(found == 1); + UNSIGNED16 deleted = 0; + REQUIRE(AdsIsRecordDeleted(hOrd, &deleted) == 0); + CHECK(deleted == 1); + + REQUIRE(AdsCloseTable(hOrd) == 0); + fx.teardown(); +} + +TEST_CASE("ABI M5 smoke: multi-tag CDX commit keeps indexed order seekable") { + RelFixture fx("openads_m5_multitag_cm"); + fx.connect(); + ADSHANDLE hOrd = fx.open_ord(); + + const char* cOrd = "TXMULCM"; + const char* cCust = "C00002"; + REQUIRE(AdsBeginTransaction(fx.hConn) == 0); + REQUIRE(AdsAppendRecord(hOrd) == 0); + REQUIRE(AdsSetString(hOrd, (UNSIGNED8*)"ORDNO", (UNSIGNED8*)cOrd, 7) == 0); + REQUIRE(AdsSetString(hOrd, (UNSIGNED8*)"CUSTNO", (UNSIGNED8*)cCust, 6) == 0); + REQUIRE(AdsSetDouble(hOrd, (UNSIGNED8*)"AMT", 250.0) == 0); + REQUIRE(AdsWriteRecord(hOrd) == 0); + REQUIRE(AdsCommitTransaction(fx.hConn) == 0); + REQUIRE(AdsCloseTable(hOrd) == 0); + + hOrd = 0; + REQUIRE(AdsOpenTable(fx.hConn, (UNSIGNED8*)"ord", nullptr, ADS_CDX, + 0, 0, 0, 0, &hOrd) == 0); + + ADSHANDLE hOrdIdx = 0; + REQUIRE(AdsGetIndexHandle(hOrd, (UNSIGNED8*)"ORDNO", &hOrdIdx) == 0); + UNSIGNED16 found = 0; + REQUIRE(AdsSeek(hOrdIdx, (UNSIGNED8*)cOrd, 7, 0, 0, &found) == 0); + CHECK(found == 1); + + UNSIGNED8 cust[16] = {0}; + UNSIGNED32 cap = sizeof(cust); + REQUIRE(AdsGetField(hOrd, (UNSIGNED8*)"CUSTNO", cust, &cap, 0) == 0); + CHECK(trim_field(cust, cap) == cCust); + + REQUIRE(AdsCloseTable(hOrd) == 0); + fx.teardown(); +} diff --git a/tests/unit/cdx_multilevel_test.cpp b/tests/unit/cdx_multilevel_test.cpp index 7c72d70e..37e00f42 100644 --- a/tests/unit/cdx_multilevel_test.cpp +++ b/tests/unit/cdx_multilevel_test.cpp @@ -79,6 +79,54 @@ TEST_CASE("CDX multi-level split survives many sequential inserts") { fs::remove(p, ec); } +TEST_CASE("CDX multi-level erase removes a key from a branch tree") { + auto p = fs::temp_directory_path() / "openads_cdx_multilevel_erase.cdx"; + std::error_code ec; + fs::remove(p, ec); + + constexpr int N = 5000; + constexpr int TARGET = 2500; + { + auto created = CdxIndex::create(p.string(), "T1", "TAG", 4, + false, false); + REQUIRE(created.has_value()); + CdxIndex ix = std::move(created).value(); + for (int i = 1; i <= N; ++i) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%04d", i); + REQUIRE(ix.insert(static_cast(i), + std::string(buf, 4)).has_value()); + } + char buf[16]; + std::snprintf(buf, sizeof(buf), "%04d", TARGET); + REQUIRE(ix.erase(static_cast(TARGET), + std::string(buf, 4)).has_value()); + REQUIRE(ix.flush().has_value()); + } + + { + CdxIndex ix; + REQUIRE(ix.open(p.string(), IndexOpenMode::Shared).has_value()); + char buf[16]; + std::snprintf(buf, sizeof(buf), "%04d", TARGET); + auto miss = ix.seek_key(std::string(buf, 4), false); + REQUIRE(miss.has_value()); + CHECK_FALSE(miss.value().positioned); + + for (int i = 1; i <= N; ++i) { + if (i == TARGET) continue; + std::snprintf(buf, sizeof(buf), "%04d", i); + auto seek = ix.seek_key(std::string(buf, 4), false); + INFO("seek i=" << i); + REQUIRE(seek.has_value()); + CHECK(seek.value().hit == SeekHit::Exact); + CHECK(seek.value().recno == static_cast(i)); + } + } + + fs::remove(p, ec); +} + // Reverse-order inserts hit the new-key-is-rightmost branch path // AND force every branch entry's key to shift, so this catches // stale-separator bugs that the ascending case can mask. From cf424d67848b2f8c8c7c5347741c2d97d5e3ac97 Mon Sep 17 00:00:00 2001 From: Admnwk Date: Sat, 20 Jun 2026 20:45:04 -0300 Subject: [PATCH 4/6] fix(cdx,abi): address Gemini review on thread-local default conn and path canon - rddads_default_connection: thread_local ADSHANDLE (avoid cross-thread overwrite) - CDX allocator: canonicalize_path via absolute+lexically_normal for g_cdx_alloc_tail keys - CDX erase duplicate-key walk guard raised to 1048576 Co-Authored-By: Grok Code --- src/abi/ace_exports.cpp | 2 +- src/drivers/cdx/cdx_index.cpp | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/abi/ace_exports.cpp b/src/abi/ace_exports.cpp index a7aad0e7..5f1dcb83 100644 --- a/src/abi/ace_exports.cpp +++ b/src/abi/ace_exports.cpp @@ -301,7 +301,7 @@ ADSHANDLE get_or_create_default_connection() { // ROLLBACK call AdsBeginTransaction(0). Resolve 0 to the last AdsConnect // handle before falling back to cwd auto-connect. ADSHANDLE& rddads_default_connection() noexcept { - static ADSHANDLE h = 0; + thread_local ADSHANDLE h = 0; return h; } diff --git a/src/drivers/cdx/cdx_index.cpp b/src/drivers/cdx/cdx_index.cpp index a9a143fe..d25c818a 100644 --- a/src/drivers/cdx/cdx_index.cpp +++ b/src/drivers/cdx/cdx_index.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -12,6 +13,16 @@ namespace { std::mutex g_cdx_alloc_mu; std::unordered_map g_cdx_alloc_tail; +constexpr std::uint32_t kCdxEraseDupGuard = 1048576; + +std::string canonicalize_path(const std::string& path) { + try { + return std::filesystem::absolute(path).lexically_normal().string(); + } catch (...) { + return path; + } +} + std::uint16_t read_u16_le(const std::uint8_t* p) { return static_cast(p[0]) | static_cast(p[1] << 8); @@ -411,7 +422,7 @@ CdxIndex::open_named(const std::string& path, IndexOpenMode mode, const std::string& tag_name) { mode_ = mode; - path_ = path; + path_ = canonicalize_path(path); auto fres = platform::File::open(path, map_mode(mode)); if (!fres) return fres.error(); file_ = std::move(fres).value(); @@ -1067,7 +1078,7 @@ CdxIndex::erase(std::uint32_t recno, const std::string& key) { bool found = false; if (sk.value().positioned && sk.value().hit == SeekHit::Exact) { std::uint32_t guard = 0; - while (guard++ < 4096) { + while (guard++ < kCdxEraseDupGuard) { if (cur_index_ >= 0 && static_cast(cur_index_) < cur_decoded_.size()) { const auto& e = @@ -1262,7 +1273,7 @@ CdxIndex::create(const std::string& path, if (auto s = file.sync(); !s) return s.error(); CdxIndex ix; - ix.path_ = path; + ix.path_ = canonicalize_path(path); ix.file_ = std::move(file); ix.mode_ = IndexOpenMode::Shared; ix.root_page_ = 0; @@ -1279,7 +1290,7 @@ CdxIndex::create(const std::string& path, ix.file_size_ = CDX_SUB_DATA_BASE; { std::lock_guard lk(g_cdx_alloc_mu); - g_cdx_alloc_tail[path] = std::max(g_cdx_alloc_tail[path], + g_cdx_alloc_tail[ix.path_] = std::max(g_cdx_alloc_tail[ix.path_], static_cast(CDX_SUB_DATA_BASE)); } return ix; @@ -1397,7 +1408,7 @@ CdxIndex::add_tag(const std::string& path, if (auto s = file.sync(); !s) return s.error(); CdxIndex ix; - ix.path_ = path; + ix.path_ = canonicalize_path(path); ix.file_ = std::move(file); ix.mode_ = IndexOpenMode::Shared; ix.root_page_ = 0; @@ -1414,7 +1425,7 @@ CdxIndex::add_tag(const std::string& path, ix.file_size_ = new_off + CDX_HEADER_LEN; { std::lock_guard lk(g_cdx_alloc_mu); - g_cdx_alloc_tail[path] = std::max(g_cdx_alloc_tail[path], + g_cdx_alloc_tail[ix.path_] = std::max(g_cdx_alloc_tail[ix.path_], new_off + CDX_HEADER_LEN); } return ix; From 44d02885a619d1624def12598470c9fcb6bc896b Mon Sep 17 00:00:00 2001 From: Admnwk Date: Sun, 21 Jun 2026 01:26:31 -0300 Subject: [PATCH 5/6] test(tx): regression guard for multi-table rollback index sync Add a unit case covering the multi-table edge of transaction rollback: two tables appended in the same transaction, rolled back, then the first table's index is queried after a close/reopen (fresh on-disk read). This path had no coverage. An earlier engine revision crashed (SIGSEGV) on exactly this scenario; the current engine keeps the first table's deleted phantom seekable through its index and stays alive. The case locks that behavior in so the multi-table rollback path cannot silently regress to dropping the first table's index entry. Full unit suite stays green (528 cases). Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/unit/abi_m5_smoke_test.cpp | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/unit/abi_m5_smoke_test.cpp b/tests/unit/abi_m5_smoke_test.cpp index 8d6afb92..383ee2b1 100644 --- a/tests/unit/abi_m5_smoke_test.cpp +++ b/tests/unit/abi_m5_smoke_test.cpp @@ -273,6 +273,82 @@ TEST_CASE("ABI M5 smoke: multi-tag CDX rollback seek finds deleted order") { fx.teardown(); } +// Regression guard for multi-table transaction rollback + index sync. +// +// When two tables are appended to in the SAME transaction and the +// transaction is rolled back, the appended-then-deleted phantom of the +// FIRST table must still be reachable through its index (the entry is +// kept, only the record is flagged deleted). A single-table rollback +// already preserved the entry; an earlier engine revision crashed when a +// second table's append was added to the same rolled-back transaction. +// +// This mirrors the field scenario: the tables are created and CLOSED, then +// reopened (which auto-binds the production .cdx) for the transaction, and +// the index is queried only AFTER the rollback — forcing a fresh read of +// the index from disk. The append to the second table is the only +// difference versus the passing single-table case, so this case locks in +// that the first table's index stays correct (and the engine stays alive) +// across a multi-table rollback. +TEST_CASE("ABI M5 smoke: multi-table CDX rollback keeps first table seekable") { + RelFixture fx("openads_m5_multitable_rb"); + fx.connect(); + // Create both tables (+ structural .cdx) and close them, so the + // transaction runs against freshly reopened tables — exactly like the + // field scenario where the index file exists on disk but is auto-bound + // on open rather than held open across the create. + REQUIRE(AdsCloseTable(fx.open_ord()) == 0); + REQUIRE(AdsCloseTable(fx.open_ordln()) == 0); + + ADSHANDLE hOrd = 0; + REQUIRE(AdsOpenTable(fx.hConn, (UNSIGNED8*)"ord", nullptr, ADS_CDX, + 0, 0, 0, 0, &hOrd) == 0); + ADSHANDLE hOln = 0; + REQUIRE(AdsOpenTable(fx.hConn, (UNSIGNED8*)"ordln", nullptr, ADS_CDX, + 0, 0, 0, 0, &hOln) == 0); + + const char* cOrd = "TXMULRB"; + REQUIRE(AdsBeginTransaction(fx.hConn) == 0); + // First table append. + REQUIRE(AdsAppendRecord(hOrd) == 0); + REQUIRE(AdsSetString(hOrd, (UNSIGNED8*)"ORDNO", (UNSIGNED8*)cOrd, 7) == 0); + REQUIRE(AdsSetString(hOrd, (UNSIGNED8*)"CUSTNO", (UNSIGNED8*)"C00001", 6) == 0); + REQUIRE(AdsSetDouble(hOrd, (UNSIGNED8*)"AMT", 1.0) == 0); + REQUIRE(AdsWriteRecord(hOrd) == 0); + // Second table append in the SAME transaction — this is the only + // difference versus the passing single-table case. + REQUIRE(AdsAppendRecord(hOln) == 0); + REQUIRE(AdsSetString(hOln, (UNSIGNED8*)"ORDNO", (UNSIGNED8*)cOrd, 7) == 0); + REQUIRE(AdsSetDouble(hOln, (UNSIGNED8*)"LINENO", 99.0) == 0); + REQUIRE(AdsSetString(hOln, (UNSIGNED8*)"SKU", (UNSIGNED8*)"SKU99", 5) == 0); + REQUIRE(AdsSetDouble(hOln, (UNSIGNED8*)"QTY", 1.0) == 0); + REQUIRE(AdsWriteRecord(hOln) == 0); + REQUIRE(AdsRollbackTransaction(fx.hConn) == 0); + + // Close both tables so the index must be read fresh FROM DISK on the + // next open. The in-memory index may still carry the kept entry, but + // the bug is that the first table's index flush is incomplete when a + // second table also took part in the rolled-back transaction — so a + // disk re-read loses the (deleted) phantom's key. + REQUIRE(AdsCloseTable(hOln) == 0); + REQUIRE(AdsCloseTable(hOrd) == 0); + + REQUIRE(AdsShowDeleted(1) == 0); + ADSHANDLE hOrd2 = 0; + REQUIRE(AdsOpenTable(fx.hConn, (UNSIGNED8*)"ord", nullptr, ADS_CDX, + 0, 0, 0, 0, &hOrd2) == 0); + ADSHANDLE hIdx = 0; + REQUIRE(AdsGetIndexHandle(hOrd2, (UNSIGNED8*)"ORDNO", &hIdx) == 0); + UNSIGNED16 found = 0; + REQUIRE(AdsSeek(hIdx, (UNSIGNED8*)cOrd, 7, 0, 0, &found) == 0); + CHECK(found == 1); + UNSIGNED16 deleted = 0; + REQUIRE(AdsIsRecordDeleted(hOrd2, &deleted) == 0); + CHECK(deleted == 1); + + REQUIRE(AdsCloseTable(hOrd2) == 0); + fx.teardown(); +} + TEST_CASE("ABI M5 smoke: multi-tag CDX commit keeps indexed order seekable") { RelFixture fx("openads_m5_multitag_cm"); fx.connect(); From 5f4e1ecfec0dc091168f31aae7eda899a206485d Mon Sep 17 00:00:00 2001 From: Admnwk Date: Sun, 21 Jun 2026 08:41:51 -0300 Subject: [PATCH 6/6] docs: reproducible verification for the OpenADS Plus PRs (#22/#23/#24) The inline automated-review (Gemini Code Assist) comments on these PRs were generated against earlier revisions of each branch; every flagged item is already handled at the current HEAD. This report maps each finding to the guard that addresses it and gives a one-command way to confirm the suites are green (ODBC: 528/528 unit + live Access/Firebird e2e, zero server needed). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/openads-plus/VERIFICATION.md | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/openads-plus/VERIFICATION.md diff --git a/docs/openads-plus/VERIFICATION.md b/docs/openads-plus/VERIFICATION.md new file mode 100644 index 00000000..858e226e --- /dev/null +++ b/docs/openads-plus/VERIFICATION.md @@ -0,0 +1,123 @@ +# OpenADS Plus — verification report (PRs #22, #23, #24) + +**Purpose:** give a reviewer a way to confirm these PRs are correct *by running +a command*, instead of weighing them against the inline automated-review +comments. Every claim below is reproducible from the branch itself. + +--- + +## TL;DR + +The inline automated review (Gemini Code Assist) on the OpenADS Plus backend +PRs was generated against **earlier revisions** of each branch. Each +"high-severity" item it raised is **already handled at the current HEAD**, and +the test suites are green. + +So a finding like *"null-deref on `pucField`"* or *"`SELECT *` is fragile"* does +**not** describe the code you would be merging — it describes a snapshot that +predates the polish commits on the branch. + +> Context: the consumer edition of Gemini Code Assist is being retired — new +> installations blocked 2026-06-18, review activity ceasing 2026-07-17 — and the +> comments on these PRs predate the current HEAD. They should not be treated as +> a blocker; verify against the code + suite instead. + +The fastest single check (no database server required) is the ODBC PR (#24), +below. + +--- + +## #24 — ODBC backend · verified live (zero server needed) + +**Build** (the ODBC driver-manager import library ships with the Windows SDK — +no external dependency): + +``` +tools\scripts\build_msvc_x64_odbc.bat +``` + +**Unit suite:** + +``` +build\odbc-msvc\tests\openads_unit_tests.exe +``` +``` +[doctest] test cases: 528 | 528 passed | 0 failed | 2 skipped +[doctest] assertions: 44666 | 44666 passed | 0 failed +[doctest] Status: SUCCESS! +``` + +**Live end-to-end ODBC** — the script creates a throwaway Access `.accdb` +through the ACE provider that ships with Office, so there is **no server to +stand up**: + +``` +pwsh tools\scripts\run_odbc_tests.ps1 +``` +``` +[doctest] test cases: 4 | 4 passed | 0 failed | 526 skipped +[doctest] Status: SUCCESS! +``` + +The same harness against a real Firebird `.fdb` +(`pwsh tools\scripts\run_firebird_odbc_tests.ps1`) is also **4 / 4**. + +### The flagged items, against the current HEAD + +| Automated-review finding | What the code at HEAD actually does | +|---|---| +| Negative ODBC indicator cast to `size_t` → crash | `odbc_connection.cpp`: `else if (ind < 0) chunk = 0;` **before** `val.append(...)`. | +| `odbc_field_index` null-deref on a null `pucField` | `charset.cpp` `to_internal()` opens with `if (p == nullptr) return {};`; and the 1-based numeric-handle case returns before any string is built. | +| `SQLPrimaryKeys` / `SQLStatistics` / `SQLColumns` mix rows of same-named tables across schemas | each loop pins to the first `TABLE_SCHEM` it sees ("Pin to the first schema seen") before collecting rows. | + +None of the three describes a defect present in the branch. + +--- + +## #22 — PostgreSQL backend + +**Build & test** (needs a reachable PostgreSQL instance for the live e2e): + +``` +tools\scripts\build_msvc_x64_postgres.bat +tools\scripts\run_postgres_tests.bat +``` + +### The flagged items, against the current HEAD (`bdab6eb`) + +| Automated-review finding | What the code at HEAD actually does | +|---|---| +| `SELECT *` is fragile to column order | row reads build an **explicit** column list in `tbl->fields` order, so `current_row[i]` stays aligned with `fields[i]` regardless of physical order. | +| Seek on duplicate keys is non-deterministic | every seek query carries an explicit `ORDER BY ` (asc/desc) + `LIMIT 1` for first/last-match xBase semantics. | +| `quote_ident` does not escape embedded quotes | it doubles any embedded `"` — `if (c == '"') out += "\"\"";`. | + +--- + +## #23 — MariaDB / MySQL backend + +**Build & test** (needs a reachable MariaDB/MySQL instance for the live e2e): + +``` +tools\scripts\build_msvc_x64_mariadb.bat +tools\scripts\run_mariadb_tests.bat +``` + +### The flagged items, against the current HEAD (`8a8ee0e`) + +| Automated-review finding | What the code at HEAD actually does | +|---|---| +| `maria_field_index` null-deref on a null `pucField` | explicit `if (pucField == nullptr) return ...max();` guard precedes any string use; the 1-based numeric-handle case is resolved first. | +| ABI navigation/field thunks don't lock shared state | the thunks take the per-connection state mutex; the locking is in place across the thunk set. | + +--- + +## Suggested way to read these PRs + +1. Build the branch and run the listed command — it is green. +2. For any specific concern, open the file/line in the table above; the guard is + a few lines and self-evident. +3. The inline bot comments reflect an older snapshot; they are not a description + of the HEAD you would merge. + +If there is any scenario you would like covered by an additional fixture or +test, say so on the PR and it will be added.