diff --git a/Cargo.lock b/Cargo.lock index e8123db..f621120 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -136,7 +142,9 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "crossterm 0.29.0", "hex", + "ratatui", "serde", "serde_json", "sha2", @@ -144,6 +152,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "uuid", + "vlogger", "warp", "wasm-bindgen", "web-sys", @@ -161,6 +170,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[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.34" @@ -236,6 +260,29 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -251,6 +298,49 @@ dependencies = [ "libc", ] +[[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" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.8", + "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 = "crypto-common" version = "0.1.6" @@ -261,12 +351,68 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -277,18 +423,49 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[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 = "form_urlencoded" version = "1.2.2" @@ -391,6 +568,11 @@ 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 = "headers" @@ -536,6 +718,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.11.0" @@ -546,6 +734,25 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -563,6 +770,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[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.15" @@ -585,6 +801,24 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "lock_api" version = "0.4.13" @@ -601,6 +835,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.5" @@ -639,6 +882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -696,6 +940,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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" @@ -796,6 +1046,27 @@ dependencies = [ "getrandom", ] +[[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 0.28.1", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -811,6 +1082,32 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[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.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -907,6 +1204,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -938,12 +1256,40 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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 = "syn" version = "2.0.106" @@ -1092,6 +1438,35 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[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 = "utf-8" version = "0.7.6" @@ -1122,6 +1497,10 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vlogger" +version = "0.1.0" + [[package]] name = "warp" version = "0.4.2" @@ -1235,6 +1614,28 @@ dependencies = [ "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.61.2" diff --git a/Cargo.toml b/Cargo.toml index 68b663c..86a3ca1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ uuid = { version = "1.18.0", features = ["v4", "serde"] } warp = { version = "0.4.2", features = ["server", "websocket"] } wasm-bindgen = "0.2.100" web-sys = { version = "0.3.77", features = ["WebSocket"] } +vlogger = { path = "./lib/logger-rs" } +ratatui = "0.29.0" +crossterm = "0.29.0" diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +/target diff --git a/lib/logger-rs/Cargo.lock b/lib/logger-rs/Cargo.lock new file mode 100644 index 0000000..a670227 --- /dev/null +++ b/lib/logger-rs/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "vlogger" +version = "0.1.0" diff --git a/lib/logger-rs/Cargo.toml b/lib/logger-rs/Cargo.toml new file mode 100644 index 0000000..ae797a5 --- /dev/null +++ b/lib/logger-rs/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "vlogger" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/lib/logger-rs/README.md b/lib/logger-rs/README.md new file mode 100644 index 0000000..32f3168 --- /dev/null +++ b/lib/logger-rs/README.md @@ -0,0 +1,11 @@ +# Simple Logger + +A lightweight logging utility for Rust with both printing and string formatting capabilities. + +## Features + +- **Two macros**: `log!` for immediate printing, `msg!` for string formatting +- **Color support**: ANSI color codes for terminal output +- **Multiple log levels**: INFO, DEBUG, WARNING, ERROR, FATAL +- **No external dependencies**: Uses only standard library +- **File/line info**: DEBUG, WARNING, ERROR, and FATAL levels include source location diff --git a/lib/logger-rs/src/lib.rs b/lib/logger-rs/src/lib.rs new file mode 100644 index 0000000..ad76aed --- /dev/null +++ b/lib/logger-rs/src/lib.rs @@ -0,0 +1,173 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub const INFO: usize = 0; +pub const DEBUG: usize = 1; +pub const WARNING: usize = 2; +pub const ERROR: usize = 3; +pub const FATAL: usize = 4; + +pub const LOG_LEVEL: [&str; 5] = [ + "INFO", + "DEBUG", + "WARNING", + "ERROR", + "FATAL", +]; + +pub fn colored(text: &str, color: &str) -> String { + // For terminals: use ANSI escape codes + let ansi_code = match color { + "black" => "30", + "red" => "31", + "green" => "32", + "yellow" => "33", + "blue" => "34", + "purple" => "35", + "cyan" => "36", + "white" => "37", + _ => "0", // default no color + }; + format!("\x1b[{ansi_code}m{text}\x1b[0m") +} + +pub fn current_timestamp() -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + + let total_seconds = now.as_secs(); + let seconds_in_day = total_seconds % 86400; // Seconds within current day + + let hours = seconds_in_day / 3600; + let minutes = (seconds_in_day % 3600) / 60; + let seconds = seconds_in_day % 60; + + format!("{:02}:{:02}:{:02}", hours, minutes, seconds) +} + +/// Creates a formatted log message string and returns it +/// +/// log_levels are: +/// - INFO +/// - DEBUG +/// - WARNING +/// - ERROR +/// - FATAL +/// +/// Returns a formatted string ready for display or further processing. +#[macro_export] +macro_rules! msg { + ($level:expr, $($arg:tt)*) => {{ + let formatted_msg = format!($($arg)*); + match $level { + $crate::INFO => { + format!( + "[{}] {} | {}\n", + $crate::colored($crate::LOG_LEVEL[$level], "green"), + $crate::current_timestamp(), + formatted_msg + ) + }, + $crate::DEBUG => { + format!( + "[{}] [{}:{}] {} | {}\n", + $crate::LOG_LEVEL[$level], + file!(), + line!(), + $crate::current_timestamp(), + formatted_msg + ) + }, + $crate::WARNING => { + format!( + "[{}] {} | {}\n", + $crate::colored($crate::LOG_LEVEL[$level], "yellow"), + $crate::current_timestamp(), + formatted_msg + ) + }, + $crate::ERROR => { + format!( + "[{}] {} | {}\n", + $crate::colored($crate::LOG_LEVEL[$level], "red"), + $crate::current_timestamp(), + formatted_msg + ) + }, + $crate::FATAL => { + format!( + "[{}] [{}:{}] {} | {}\n", + $crate::colored($crate::LOG_LEVEL[$level], "red"), + file!(), + line!(), + $crate::current_timestamp(), + formatted_msg + ) + }, + _ => { + format!( + "[{}] {} {}", + $crate::LOG_LEVEL[$crate::ERROR], + "logging error: log_level value invalid:", + $level + ) + } + } + }}; +} + +/// Creates a formatted log message and prints it to stdout +/// +/// log_levels are: +/// - INFO +/// - DEBUG +/// - WARNING +/// - ERROR +/// - FATAL +/// +/// FATAL will cause the thread to panic and stop execution. +#[macro_export] +macro_rules! log { + ($level:expr, $($arg:tt)*) => {{ + let log_message = $crate::msg!($level, $($arg)*); + println!("{}", log_message); + + // Handle FATAL level panic + if $level == $crate::FATAL { + panic!("Fatal error encountered: {}", format!($($arg)*)); + } + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_msg_macro() { + let info_msg = msg!(INFO, "Test info message"); + assert!(info_msg.contains("INFO")); + assert!(info_msg.contains("Test info message")); + + let error_msg = msg!(ERROR, "Test error: {}", 42); + assert!(error_msg.contains("ERROR")); + assert!(error_msg.contains("Test error: 42")); + } + + #[test] + fn test_colored() { + let red_text = colored("error", "red"); + assert!(red_text.contains("\x1b[31m")); + assert!(red_text.contains("error")); + assert!(red_text.contains("\x1b[0m")); + } + + #[test] + fn test_timestamp() { + let ts = current_timestamp(); + assert!(ts.contains(":")); + // Should be in HH:MM:SS format + assert_eq!(ts.len(), 8); + } +} + diff --git a/src/args.rs b/src/args.rs index abef15c..ce20fba 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,51 +4,26 @@ use crate::core; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Args { - #[command(subcommand)] - command: Commands + /// Provide address on which node will listen + #[arg(short = 'a', long)] + pub addr: Option, + + /// Provide File with current chain + #[arg(short = 'f', long)] + pub seed_file: Option, } #[derive(Subcommand, Debug)] pub enum Commands { - /// Transaction Options (add, ...) - #[command(short_flag = 't')] - Tx { - #[command(subcommand)] - tx_command: TxCmd - }, - - /// Show accounts and balances - #[command(short_flag = 'l')] - List, - - /// Start as seed node - #[command(short_flag = 's')] - Seed { - #[arg(short = 'a')] - addr: String - }, - - /// listen on addr - #[command(short_flag = 'r')] - Run { - #[arg(short = 'a')] - addr: String - }, } #[derive(Subcommand, Debug)] pub enum TxCmd { - /// Add a new transaction to the DB - #[command(short_flag = 'a')] - Add(core::Tx) -} - -impl Args { - pub fn get_commands(&self) -> &Commands { - &self.command - } + /// Add a new transaction to the DB + #[command(short_flag = 'a')] + Add(core::Tx) } pub fn get_args() -> Args { - Args::parse() + Args::parse() } diff --git a/src/core/block.rs b/src/core/block.rs index a760741..d3e7579 100644 --- a/src/core/block.rs +++ b/src/core/block.rs @@ -1,6 +1,6 @@ use crate::core; -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Default)] pub struct BlockHeader { pub previous_hash: String, pub timestamp: u64, @@ -9,7 +9,7 @@ pub struct BlockHeader { pub nonce: u32 } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Default)] pub struct Block { pub head: BlockHeader, pub tx: Vec diff --git a/src/core/blockchain.rs b/src/core/blockchain.rs index 360c6a5..9d1b9c8 100644 --- a/src/core/blockchain.rs +++ b/src/core/blockchain.rs @@ -1,8 +1,7 @@ use sha2::Digest; use sha2::Sha256; -use crate::core::block; -use crate::log::*; +use vlogger::*; use crate::core; use crate::error::{ BlockchainError, TxError }; @@ -18,31 +17,13 @@ pub enum ValidationError { InvalidPreviousBlockHash } -#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] -pub struct Genesis { - pub genesis_time: String, - pub chain_id: String, - pub balances: std::collections::HashMap -} - -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Blockchain { - genesis: Genesis, balances: std::collections::HashMap, blocks: Vec, tx_mempool: Vec, } -impl Genesis { - pub fn new() -> Self { - Self { - genesis_time: String::new(), - chain_id: String::new(), - balances: std::collections::HashMap::new() - } - } -} - #[allow(dead_code)] impl Blockchain { pub fn open_account(&mut self, tx: core::Tx) -> Result<(), BlockchainError> { @@ -185,9 +166,8 @@ impl Blockchain { Ok(()) } - pub fn new(balances: HashMap, blocks: Vec, tx_mempool: Vec, genesis: Genesis) -> Blockchain { + pub fn new(balances: HashMap, blocks: Vec, tx_mempool: Vec) -> Blockchain { return Self { - genesis, balances, blocks, tx_mempool @@ -224,10 +204,6 @@ impl Blockchain { &self.blocks } - pub fn genesis(&self) -> &Genesis { - &self.genesis - } - pub fn add_block(&mut self, block: core::Block) { match self.validate_block(&block) { Ok(()) => self.blocks.push(block), @@ -266,10 +242,9 @@ impl Blockchain { Ok(()) } - pub fn from_genesis(genesis: Genesis, blocks: Vec) -> Result { + pub fn build(blocks: Vec) -> Result { log!(INFO, "Starting Chain Build from Genesis"); let chain = Blockchain { - genesis, blocks, balances: HashMap::new(), tx_mempool: vec![] diff --git a/src/log.rs b/src/log.rs deleted file mode 100644 index f88c409..0000000 --- a/src/log.rs +++ /dev/null @@ -1,115 +0,0 @@ -pub const INFO: usize = 0; -pub const DEBUG: usize = 1; -pub const WARNING: usize = 2; -pub const ERROR: usize = 3; -pub const FATAL: usize = 4; - -pub const LOG_LEVEL: [&str; 5] = [ - "INFO", - "DEBUG", - "WARNING", - "ERROR", - "FATAL", -]; - -use chrono::Utc; - -pub fn colored(text: &str, color: &str) -> String { - // For terminals: use ANSI escape codes - let ansi_code = match color { - "black" => "30", - "red" => "31", - "green" => "32", - "yellow" => "33", - "blue" => "34", - "purple" => "35", - "cyan" => "36", - "white" => "37", - _ => "0", // default no color - }; - format!("\x1b[{ansi_code}m{text}\x1b[0m") -} - -pub fn current_timestamp() -> String { - Utc::now().format("%Y-%m-%d@%H:%M:%S").to_string() -} - -/// You can use this macro to create error messages that will show in the console -/// if run on the client or in the terminal if run on the server -/// -/// log_levels are : -/// - INFO -/// - DEBUG -/// - WARNING -/// - ERROR -/// - FATAL -/// -/// important: FATAL will cause the thread to panic and stop execution -#[macro_export] -macro_rules! log { - ($level:expr, $($arg:tt)*) => {{ - let formatted_msg = format!($($arg)*); - match $level { - $crate::log::INFO => { - println!( - "[{}] {} | {}", - $crate::log::colored($crate::log::LOG_LEVEL[$level], "green"), - $crate::log::current_timestamp(), - formatted_msg - ); - }, - $crate::log::DEBUG => { - println!( - "[{}] [{}:{}] {} | {}", - $crate::log::LOG_LEVEL[$level], - file!(), - line!(), - $crate::log::current_timestamp(), - formatted_msg - ); - }, - $crate::log::WARNING => { - println!( - "[{}] [{}:{}] {} | {}", - $crate::log::colored($crate::log::LOG_LEVEL[$level], "yellow"), - file!(), - line!(), - $crate::log::current_timestamp(), - formatted_msg - ); - }, - $crate::log::ERROR => { - println!( - "[{}] [{}:{}] {} | {}", - $crate::log::colored($crate::log::LOG_LEVEL[$level], "red"), - file!(), - line!(), - $crate::log::current_timestamp(), - formatted_msg - ); - }, - $crate::log::FATAL => { - println!( - "[{}] [{}:{}] {} | {}", - $crate::log::colored($crate::log::LOG_LEVEL[$level], "red"), - file!(), - line!(), - $crate::log::current_timestamp(), - formatted_msg - ); - if $level == $crate::log::FATAL { - panic!("Fatal error encountered: {}", formatted_msg); - } - }, - _ => { - println!( - "[{}] {} {}", - $crate::log::LOG_LEVEL[$crate::log::ERROR], - "loggin error: log_level value invalid:", - $level - ); - } - } - }}; -} - diff --git a/src/main.rs b/src/main.rs index acfd7bd..ec62e4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,13 @@ use error::{ BlockchainError, handle_error }; -#[macro_use] -pub mod log; pub mod error; pub mod args; pub mod core; pub mod native_node; pub mod seeds_constants; +pub mod watcher; -use crate::native_node::node::NativeNode; -use crate::args::{get_args, TxCmd, Commands}; +use crate::{args::get_args, watcher::watcher::Watcher}; const SEED_ADDR: &str = "127.0.0.1:8333"; @@ -26,24 +24,13 @@ async fn main() { let args = get_args(); - match args.get_commands() { - Commands::Tx{ tx_command } => { - match tx_command { - TxCmd::Add(tx) => { - add_transaction(tx.clone()).unwrap_or_else(|e| handle_error(e)) - } - } - }, - Commands::List => { - list_accounts() - } - Commands::Seed { addr: _ } => { - NativeNode::seed(SEED_ADDR.to_string()).run_native().await; - } - Commands::Run{ addr } => { - dbg!(&addr); - NativeNode::bootstrap(addr).await.unwrap().run_native().await; - } + let mut watcher = Watcher::build().file(args.seed_file).addr(args.addr).start(); + + loop { + if !watcher.poll().await.is_ok() { + break ; + } } - println!("Hello, world!"); + + println!("Hello, world!"); } diff --git a/src/native_node/cli.rs b/src/native_node/cli.rs index 6e7a3bd..750bbb6 100644 --- a/src/native_node/cli.rs +++ b/src/native_node/cli.rs @@ -1,5 +1,5 @@ -use crate::native_node::{message, node}; -use crate::log::*; +use crate::native_node::{node}; +use vlogger::*; use tokio::sync::mpsc; use crate::core; use std::io::{self, Write}; @@ -22,51 +22,6 @@ impl node::NativeNode { let args = &parts[1..]; match command { - "id" => { - command_sender.send(node::NodeCommand::DebugShowId).await.unwrap(); - }, - "tx" => { - if args.len() != 4 { - log!(ERROR, "Invalid arg count! Expected {}, got {}", 4, args.len()); - continue; - } - let from = args[0]; - let to = args[1]; - let value = args[2].parse::().unwrap(); - let data = args[3]; - - let tx = core::Tx::new( - from.to_string(), - to.to_string(), - value, - data.to_string() - ); - - let cmd = node::NodeCommand::Transaction { tx }; - - command_sender.send(cmd).await.unwrap(); - }, - "block" => { - let cmd = node::NodeCommand::CreateBlock; - command_sender.send(cmd).await.unwrap(); - }, - "list" => { - if args.len() != 1 { - log!(ERROR, "{command}: Invalid arg! (blocks, peers)"); - continue; - } - match args[0] { - "blocks" => command_sender.send(node::NodeCommand::DebugListBlocks).await.unwrap(), - "peers" => command_sender.send(node::NodeCommand::DebugListPeers).await.unwrap(), - _ => log!(ERROR, "Unkown arg: {}", args[0]), - } - }, - "dump_blocks" => { - command_sender.send(node::NodeCommand::DebugDumpBlocks).await.unwrap(); - }, - "connect" => { - command_sender.send(node::NodeCommand::ConnectToSeeds).await.unwrap(); - } _ => { log!(ERROR, "Unkown command {command}"); continue; diff --git a/src/native_node/message.rs b/src/native_node/message.rs index df45079..561e1de 100644 --- a/src/native_node/message.rs +++ b/src/native_node/message.rs @@ -4,7 +4,7 @@ use crate::native_node::node; use crate::native_node::error; use crate::core; -use crate::log::*; +use vlogger::*; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub enum ProtocolMessage { @@ -13,7 +13,6 @@ pub enum ProtocolMessage { version: String }, BootstrapResponse { - genesis: core::Genesis, blocks: Vec }, GetPeersRequest { @@ -78,7 +77,6 @@ impl node::NativeNode { log!(INFO, "Received BootstrapRequest from {peer_id}"); let peer = &self.tcp_peers[&peer_id]; let resp = ProtocolMessage::BootstrapResponse { - genesis: self.chain.genesis().clone(), blocks: self.chain.blocks().to_vec() }; peer.sender.send(resp).await.unwrap(); diff --git a/src/native_node/network.rs b/src/native_node/network.rs index 4bda228..09d29ee 100644 --- a/src/native_node/network.rs +++ b/src/native_node/network.rs @@ -2,7 +2,7 @@ use crate::native_node::{message, node}; use crate::seeds_constants::SEED_NODES; -use crate::log::*; +use vlogger::*; use tokio::select; use tokio::sync::mpsc; diff --git a/src/native_node/node.rs b/src/native_node/node.rs index 205d752..aabe7e5 100644 --- a/src/native_node/node.rs +++ b/src/native_node/node.rs @@ -1,275 +1,237 @@ -use crate::core::{self, ValidationError}; +use crate::core::{self, Blockchain, ValidationError}; use crate::native_node::message::{self, ProtocolMessage}; use crate::seeds_constants::SEED_NODES; +use crate::watcher::executor::ExecutorCommand; -use std::io::{Read, Write}; use std::collections::HashMap; -use crate::log::*; +use vlogger::*; use tokio::sync::mpsc; use uuid::Uuid; pub struct TcpPeer { - pub id: Uuid, - pub addr: String, - pub sender: tokio::sync::mpsc::Sender + pub id: Uuid, + pub addr: String, + pub sender: tokio::sync::mpsc::Sender } pub struct NativeNode { - pub id: Uuid, - pub addr: String, - pub tcp_peers: HashMap, - pub ws: Vec, - pub chain: core::Blockchain, - pub db_file: std::fs::File + pub id: Uuid, + pub addr: String, + pub tcp_peers: HashMap, + pub chain: core::Blockchain, + exec_tx: mpsc::Sender, + rx: mpsc::Receiver, } #[derive(Debug)] pub enum NodeCommand { - AddPeer { peer_id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender }, - RemovePeer { peer_id: Uuid }, - ProcessMessage { peer_id: Uuid, message: ProtocolMessage }, - Transaction { tx: core::Tx }, - CreateBlock, - DebugListBlocks, - DebugListPeers, - DebugShowId, - DebugDumpBlocks, - ConnectToSeeds + AddPeer { peer_id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender }, + RemovePeer { peer_id: Uuid }, + ProcessMessage { peer_id: Uuid, message: ProtocolMessage }, + Transaction { tx: core::Tx }, + CreateBlock, + DebugListBlocks, + DebugListPeers, + DebugShowId, + DebugDumpBlocks, + ConnectToSeeds } impl NativeNode { - pub fn peer_addresses(&self) -> Vec { - let mut addr: Vec = self.tcp_peers.iter().map(|p| p.1.addr.to_string()).collect(); - addr.push(self.addr.clone()); - addr + pub fn peer_addresses(&self) -> Vec { + let mut addr: Vec = self.tcp_peers.iter().map(|p| p.1.addr.to_string()).collect(); + addr.push(self.addr.clone()); + addr + } + + pub fn list_peers(&self) { + println!("Peer List\n-----------"); + for (i, p) in self.tcp_peers.iter().enumerate() { + println!("Peer #{i}: {}", p.1.id) } + } - pub fn list_peers(&self) { - println!("Peer List\n-----------"); - for (i, p) in self.tcp_peers.iter().enumerate() { - println!("Peer #{i}: {}", p.1.id) - } + pub fn show_id(&self) { + println!("Node Id: {}", self.id) + } + + fn remove_tcp_peer(&mut self, peer_id: Uuid) { + log!(INFO, "Removing Peer {peer_id}"); + self.tcp_peers.remove_entry(&peer_id); + } + + fn add_tcp_peer(&mut self, id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender) { + let peer = TcpPeer { + id: id, + addr, + sender + }; + + log!(INFO, "Adding Peer {}", peer.id); + + self.tcp_peers.insert(id, peer); + } + + pub fn new_with_id( + id: uuid::Uuid, + exec_tx: mpsc::Sender, + rx: mpsc::Receiver + ) -> Self { + Self { + id, + tcp_peers: HashMap::new(), + chain: Default::default(), + addr: String::new(), + exec_tx, + rx, } + } - pub fn show_id(&self) { - println!("Node Id: {}", self.id) - } - - fn remove_tcp_peer(&mut self, peer_id: Uuid) { - log!(INFO, "Removing Peer {peer_id}"); - self.tcp_peers.remove_entry(&peer_id); - } - - fn add_tcp_peer(&mut self, id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender) { - let peer = TcpPeer { - id: id, - addr, - sender - }; - - log!(INFO, "Adding Peer {}", peer.id); - - self.tcp_peers.insert(id, peer); - } - - - fn persist(&mut self) { - for t in self.chain.blocks() { - let json = serde_json::to_string(&t).unwrap(); - self.db_file.write(json.as_bytes()).unwrap(); - } - } - - pub fn new_with_id(id: uuid::Uuid, chain: core::Blockchain, db_file: std::fs::File, addr: String) -> Self { - Self { - id, - tcp_peers: HashMap::new(), - ws: Vec::new(), - chain, - addr, - db_file - } - } - - pub fn new(chain: core::Blockchain, db_file: std::fs::File, addr: String) -> Self { - Self { - id: Uuid::new_v4(), - tcp_peers: HashMap::new(), - ws: Vec::new(), - chain, - addr, - db_file - } - } - - pub async fn send_handshake(id: uuid::Uuid, stream: &mut tokio::net::TcpStream) -> Result { - let handshake = ProtocolMessage::Handshake { node_id: id.clone(), version: "".to_string() }; - NativeNode::send_message(stream, &handshake).await.unwrap(); - if let Ok(response) = NativeNode::receive_message(stream).await { - match response { - message::ProtocolMessage::Handshake { node_id, version: _ } => { - Ok(node_id) - }, - _ => { - log!(ERROR, "Invalid response on Handshake"); - Err(ValidationError::InvalidBlockHash) - } - } + pub fn new( + addr: Option, + blocks: Option>, + exec_tx: mpsc::Sender, + rx: mpsc::Receiver, + ) -> Self { + Self { + id: Uuid::new_v4(), + tcp_peers: HashMap::new(), + chain: { + if blocks.is_some() { + Blockchain::build(blocks.unwrap()).unwrap_or(Default::default()) } else { + Default::default() + } + }, + addr: if addr.is_some() { addr.unwrap() } else { String::new() }, + exec_tx, + rx + } + } + + pub async fn send_handshake(id: uuid::Uuid, stream: &mut tokio::net::TcpStream) -> Result { + let handshake = ProtocolMessage::Handshake { node_id: id.clone(), version: "".to_string() }; + NativeNode::send_message(stream, &handshake).await.unwrap(); + if let Ok(response) = NativeNode::receive_message(stream).await { + match response { + message::ProtocolMessage::Handshake { node_id, version: _ } => { + Ok(node_id) + }, + _ => { + log!(ERROR, "Invalid response on Handshake"); + Err(ValidationError::InvalidBlockHash) + } + } + } else { + Err(ValidationError::InvalidBlockHash) + } + } + + pub async fn bootstrap(&mut self) -> Result<(), ValidationError> { + log!(INFO, "Running As Native Node"); + + let mut stream = tokio::net::TcpStream::connect(SEED_NODES[0]).await.unwrap(); + + let id = uuid::Uuid::new_v4(); + + if let Ok(_) = NativeNode::send_handshake(id, &mut stream).await { + let message = message::ProtocolMessage::BootstrapRequest { node_id: id.clone(), version: "".to_string() }; + NativeNode::send_message(&mut stream, &message).await.unwrap(); + log!(INFO, "Sent BootstrapRequest to seed"); + if let Ok(response) = NativeNode::receive_message(&mut stream).await { + match response { + ProtocolMessage::BootstrapResponse { blocks } => { + log!(INFO, "Received BootstrapResponse from seed"); + self.chain = core::Blockchain::build(blocks).unwrap(); + Ok(()) + }, + _ => { + log!(ERROR, "Invalid Response from BootstrapRequest: {:?}", &response); Err(ValidationError::InvalidBlockHash) + } } + } else { + Err(ValidationError::InvalidBlockHash) + } + } else { + Err(ValidationError::InvalidBlockHash) } + } - pub async fn bootstrap(addr: &str) -> Result { - log!(INFO, "Running As Native Node"); + pub async fn broadcast_transaction(&self, tx: &core::Tx) { + for (id, peer) in &self.tcp_peers { + let message = ProtocolMessage::Transaction{peer_id: self.id, tx: tx.clone()}; + peer.sender.send(message).await.unwrap(); + log!(DEBUG, "Send Transaction message to {id}"); + } + } - let mut stream = tokio::net::TcpStream::connect(SEED_NODES[0]).await.unwrap(); + pub async fn broadcast_block(&self, block: &core::Block) { + for (id, peer) in &self.tcp_peers { + let message = ProtocolMessage::Block{ + peer_id: self.id, + height: self.chain.blocks().len() as u64, + block: block.clone() + }; + peer.sender.send(message).await.unwrap(); + log!(DEBUG, "Send Block message to {id}"); + } + } - let id = uuid::Uuid::new_v4(); + pub async fn run_native(&mut self) { + let tcp_listner = tokio::net::TcpListener::bind(&self.addr).await.unwrap(); - if let Ok(_) = NativeNode::send_handshake(id, &mut stream).await { - let message = message::ProtocolMessage::BootstrapRequest { node_id: id.clone(), version: "".to_string() }; - NativeNode::send_message(&mut stream, &message).await.unwrap(); - log!(INFO, "Sent BootstrapRequest to seed"); - if let Ok(response) = NativeNode::receive_message(&mut stream).await { - match response { - ProtocolMessage::BootstrapResponse { genesis, blocks } => { - log!(INFO, "Received BootstrapResponse from seed"); - let chain = core::Blockchain::from_genesis(genesis, blocks)?; - let node = Self::new_with_id(id, chain, std::fs::File::open("./database/tx.db").unwrap(), addr.to_string()); - Ok(node) - }, - _ => { - log!(ERROR, "Invalid Response from BootstrapRequest: {:?}", &response); - Err(ValidationError::InvalidBlockHash) - } - } - } else { - Err(ValidationError::InvalidBlockHash) - } - } else { - Err(ValidationError::InvalidBlockHash) + let (channel_write, mut channel_read) = mpsc::channel::(100); + + let id = self.id.clone(); + tokio::spawn({ + let c = channel_write.clone(); + async move { + NativeNode::accept_connections(tcp_listner, c, id).await; + }}); + + while let Some(command) = channel_read.recv().await { + match command { + NodeCommand::ConnectToSeeds => { + self.connect_to_seeds(channel_write.clone()).await; + }, + NodeCommand::AddPeer { peer_id, addr, sender } => { + self.add_tcp_peer(peer_id, addr, sender); + }, + NodeCommand::RemovePeer { peer_id } => { + self.remove_tcp_peer(peer_id); } - } - - pub fn seed(addr: String) -> Self { - log!(INFO, "Running As Seed Node"); - let cwd = std::env::current_dir().unwrap(); - let mut genpath = std::path::PathBuf::from(&cwd); - genpath.push("database"); - genpath.push("genesis.json"); - let mut gen_file = std::fs::File::open(genpath).unwrap(); - - let mut buf = String::new(); - gen_file.read_to_string(&mut buf).unwrap(); - - let mut db_file: std::fs::File = { - let mut db_path = std::path::PathBuf::from(&cwd); - db_path.push("database"); - db_path.push("tx.db"); - - std::fs::OpenOptions::new().read(true).write(true).create(true).open(&db_path).unwrap() - }; - - let genesis = serde_json::from_str::(&buf).unwrap(); - - buf.clear(); - db_file.read_to_string(&mut buf).unwrap(); - - let buf = buf.trim(); - - log!(DEBUG, "Buf content: {:#?}", buf); - let blocks = if !buf.is_empty() { - serde_json::from_str::>(&buf).unwrap() - } else { - vec![] - }; - let chain = core::Blockchain::from_genesis(genesis, blocks).unwrap(); - - Self::new(chain, db_file, addr) - } - - pub async fn broadcast_transaction(&self, tx: &core::Tx) { - for (id, peer) in &self.tcp_peers { - let message = ProtocolMessage::Transaction{peer_id: self.id, tx: tx.clone()}; - peer.sender.send(message).await.unwrap(); - log!(DEBUG, "Send Transaction message to {id}"); - } - } - - pub async fn broadcast_block(&self, block: &core::Block) { - for (id, peer) in &self.tcp_peers { - let message = ProtocolMessage::Block{ - peer_id: self.id, - height: self.chain.blocks().len() as u64, - block: block.clone() - }; - peer.sender.send(message).await.unwrap(); - log!(DEBUG, "Send Block message to {id}"); - } - } - - pub async fn run_native(&mut self) { - let tcp_listner = tokio::net::TcpListener::bind(&self.addr).await.unwrap(); - - let (channel_write, mut channel_read) = mpsc::channel::(100); - - let id = self.id.clone(); - tokio::spawn({ - let c = channel_write.clone(); - async move { - NativeNode::accept_connections(tcp_listner, c, id).await; - }}); - - tokio::spawn({ - let c = channel_write.clone(); - async move { - NativeNode::cli(c).await; - } - }); - - while let Some(command) = channel_read.recv().await { - match command { - NodeCommand::ConnectToSeeds => { - self.connect_to_seeds(channel_write.clone()).await; - }, - NodeCommand::AddPeer { peer_id, addr, sender } => { - self.add_tcp_peer(peer_id, addr, sender); - }, - NodeCommand::RemovePeer { peer_id } => { - self.remove_tcp_peer(peer_id); - } - NodeCommand::ProcessMessage { peer_id, message } => { - self.process_message(peer_id, &message).await; - }, - NodeCommand::Transaction { tx } => { - self.chain.apply(&tx).unwrap(); - self.broadcast_transaction(&tx).await; - }, - NodeCommand::CreateBlock => { - log!(INFO, "Received CreateBlock Command"); - let block = self.chain.create_block(); - self.broadcast_block(&block).await; - }, - NodeCommand::DebugListBlocks => { - log!(INFO, "Received DebugListBlocks command"); - self.chain.print_blocks(); - }, - NodeCommand::DebugListPeers => { - log!(INFO, "Received DebugListPeers command"); - self.list_peers(); - }, - NodeCommand::DebugShowId => { - log!(INFO, "Received DebugListBlocks command"); - self.show_id(); - }, - NodeCommand::DebugDumpBlocks => { - self.chain.dump_blocks(&mut self.db_file); - } - } + NodeCommand::ProcessMessage { peer_id, message } => { + self.process_message(peer_id, &message).await; + }, + NodeCommand::Transaction { tx } => { + self.chain.apply(&tx).unwrap(); + self.broadcast_transaction(&tx).await; + }, + NodeCommand::CreateBlock => { + log!(INFO, "Received CreateBlock Command"); + let block = self.chain.create_block(); + self.broadcast_block(&block).await; + }, + NodeCommand::DebugListBlocks => { + log!(INFO, "Received DebugListBlocks command"); + self.chain.print_blocks(); + }, + NodeCommand::DebugListPeers => { + log!(INFO, "Received DebugListPeers command"); + self.list_peers(); + }, + NodeCommand::DebugShowId => { + log!(INFO, "Received DebugListBlocks command"); + self.show_id(); + }, + NodeCommand::DebugDumpBlocks => { + // self.chain.dump_blocks(&mut self.db_file); } + } } + } } diff --git a/src/watcher.rs b/src/watcher.rs new file mode 100644 index 0000000..a8ecf5e --- /dev/null +++ b/src/watcher.rs @@ -0,0 +1,8 @@ +pub mod executor; +pub mod parser; +pub mod renderer; +pub mod watcher; + +pub use executor::*; +pub use parser::*; +pub use renderer::*; diff --git a/src/watcher/executor.rs b/src/watcher/executor.rs new file mode 100644 index 0000000..7cefd54 --- /dev/null +++ b/src/watcher/executor.rs @@ -0,0 +1,109 @@ +use crate::{native_node::node::NodeCommand, watcher::renderer::*}; +use tokio::sync::mpsc; + +pub enum ExecutorCommand { + NodeResponse(String), + Echo(Vec), + InvalidCommand(String), + Node(NodeCommand), + Clear(RenderPane), + Exit +} + +pub struct Executor { + render_tx: mpsc::Sender, + node_tx: mpsc::Sender, + rx: mpsc::Receiver, + exit: bool +} + +impl Executor { + pub fn new(render_tx: mpsc::Sender, node_tx: mpsc::Sender, rx: mpsc::Receiver) -> Self { + Self { + render_tx, + node_tx, + rx, + exit: false + } + } + + fn exit(&mut self) { + self.exit = true + } + + async fn listen(&mut self) { + if let Some(cmd) = self.rx.recv().await { + let _ = self.execute(cmd); + } + } + + async fn send_node_cmd(&self, cmd: NodeCommand) { + self.node_tx.send(cmd).await.unwrap() + } + + async fn handle_node_cmd(&self, cmd: NodeCommand) { + self.send_node_cmd(cmd).await; + + // match cmd { + // NodeCommand::AddPeer { peer_id, addr, sender } => {}, + // NodeCommand::RemovePeer { peer_id } => {}, + // NodeCommand::ProcessMessage { peer_id, message } => {}, + // NodeCommand::Transaction { tx } => {}, + // NodeCommand::CreateBlock => {}, + // NodeCommand::DebugListBlocks => {}, + // NodeCommand::DebugListPeers => {}, + // NodeCommand::DebugShowId => {}, + // NodeCommand::DebugDumpBlocks => {}, + // NodeCommand::ConnectToSeeds => {} + // } + } + + fn render_string(&self, str: String) { + let rd_cmd = RenderCommand::RenderStringToPane{ + str, + pane: RenderPane::CliOutput + }; + let _ = self.render_tx.send(rd_cmd); + } + + fn echo(&self, s: Vec) { + let mut str = s.join(" "); + str.push_str("\n"); + let rd_cmd = RenderCommand::RenderStringToPane{ + str, + pane: RenderPane::CliOutput + }; + let _ = self.render_tx.send(rd_cmd); + } + + fn clear(&self, p: RenderPane) { + let rd_cmd = RenderCommand::ClearPane(p); + let _ = self.render_tx.send(rd_cmd); + } + + fn invalid_command(&self, str: String) { + let rd_cmd = RenderCommand::RenderStringToPane{ + str, + pane: RenderPane::CliOutput + }; + let _ = self.render_tx.send(rd_cmd); + } + + async fn execute(&mut self, cmd: ExecutorCommand) { + match cmd { + ExecutorCommand::NodeResponse(resp) => self.render_string(resp), + ExecutorCommand::Node(n) => self.handle_node_cmd(n).await, + ExecutorCommand::Clear(p) => self.clear(p), + ExecutorCommand::Echo(s) => self.echo(s), + ExecutorCommand::InvalidCommand(str) => self.invalid_command(str), + ExecutorCommand::Exit => self.exit(), + } + } + + pub async fn run(&mut self) { + while !self.exit { + self.listen().await; + } + } +} + diff --git a/src/watcher/parser.rs b/src/watcher/parser.rs new file mode 100644 index 0000000..462e36c --- /dev/null +++ b/src/watcher/parser.rs @@ -0,0 +1,144 @@ +use crate::native_node::node::NodeCommand; +use crate::watcher::executor::{ExecutorCommand}; +use vlogger::*; + +use crate::core; + +use tokio::time::{timeout, Duration}; + +use tokio::sync::mpsc; + +use crate::watcher::renderer::RenderPane; + +#[derive(Debug)] +pub struct Parser { + rx: mpsc::Receiver, + exec_tx: mpsc::Sender, + exit: bool +} + +pub enum ParserCommand { + ParseCmdString(String), + Exit +} + +const CMD_ECHO: &str = "echo"; +const CMD_CLEAR: &str = "clear"; +const CMD_NODE: &str = "node"; + +impl Parser { + pub fn new( + rx: mpsc::Receiver, + exec_tx: mpsc::Sender + ) -> Self { + Self { + rx, + exec_tx, + exit: false + } + } + + fn exit(&mut self) { + self.exit = true; + } + + pub async fn run(&mut self) { + while !self.exit { + self.listen().await; + } + } + + async fn listen(&mut self) { + if let Ok(Some(mes)) = timeout(Duration::from_millis(400), self.rx.recv()).await { + match mes { + ParserCommand::ParseCmdString(s) => { + let s_split: Vec<&str> = s.split(" ").collect(); + + if s_split.len() != 0 { + let cmd = &s_split[0]; + let args = &s_split[1..]; + let exec_cmd = match *cmd { + CMD_NODE => { + if !args.is_empty() { + match args[0] { + "id" => { + ExecutorCommand::Node(NodeCommand::DebugShowId) + }, + "tx" => { + if args.len() != 4 { + log!(ERROR, "Invalid arg count! Expected {}, got {}", 4, args.len()); + } + let from = args[0]; + let to = args[1]; + let value = args[2].parse::().unwrap(); + let data = args[3]; + + let tx = core::Tx::new( + from.to_string(), + to.to_string(), + value, + data.to_string() + ); + + ExecutorCommand::Node(NodeCommand::Transaction { tx }) + }, + "block" => { + ExecutorCommand::Node(NodeCommand::CreateBlock) + }, + "list" => { + if args.len() != 1 { + log!(ERROR, "{cmd}: Invalid arg! (blocks, peers)"); + } + match args[0] { + "blocks" => ExecutorCommand::Node(NodeCommand::DebugListBlocks), + "peers" => ExecutorCommand::Node(NodeCommand::DebugListPeers), + _ => ExecutorCommand::InvalidCommand(msg!(ERROR, "Unkown arg: {}", args[0])), + } + }, + "dump_blocks" => { + ExecutorCommand::Node(NodeCommand::DebugDumpBlocks) + }, + "connect" => { + ExecutorCommand::Node(NodeCommand::ConnectToSeeds) + } + _ => { + ExecutorCommand::InvalidCommand(msg!(ERROR, "node: unknown argument {}", args[0])) + } + } + } else { + ExecutorCommand::InvalidCommand(msg!(ERROR, "node: expected arguments")) + } + } + CMD_ECHO => { + if args.is_empty() { + ExecutorCommand::InvalidCommand(msg!(ERROR, "print expects args")) + } else { + ExecutorCommand::Echo(args.iter().map(|a| a.to_string()).collect()) + } + } + CMD_CLEAR => { + if args.is_empty() { + ExecutorCommand::Clear(RenderPane::All) + } else if args[0] == "in" { + ExecutorCommand::Clear(RenderPane::CliInput) + } else if args[0] == "out" { + ExecutorCommand::Clear(RenderPane::CliOutput) + } else { + ExecutorCommand::InvalidCommand(msg!(ERROR, "clear: Unknown arg {}", args[0])) + } + } + _ => { + ExecutorCommand::InvalidCommand(msg!(ERROR, "Unknown Command {cmd}")) + } + }; + let _ = self.exec_tx.send(exec_cmd); + } + } + ParserCommand::Exit => { + self.exit(); + } + } + } + } +} + diff --git a/src/watcher/renderer.rs b/src/watcher/renderer.rs new file mode 100644 index 0000000..603d4fc --- /dev/null +++ b/src/watcher/renderer.rs @@ -0,0 +1,210 @@ +use crossterm::event::KeyCode; +use ratatui::prelude::*; +use ratatui::widgets::Wrap; +use ratatui::{ + buffer::Buffer, + layout::Rect, + symbols::border, + widgets::{Block, Paragraph, Widget}, + DefaultTerminal, Frame, + +}; + +use tokio::sync::mpsc; +use tokio::time::{timeout, Duration}; +use std::io; + +#[derive(Debug)] +pub struct Renderer { + buffer: String, + exit: bool, + rx: mpsc::Receiver, + layout: RenderLayout +} + +#[derive(Debug, PartialEq)] +pub struct Pane { + title: Option, + target: RenderPane, + layout_index: u8, + buffer: String, +} + +#[derive(Debug, PartialEq)] +pub enum RenderPane { + All, + CliInput, + CliOutput +} + +pub enum RenderCommand { + RenderStringToPane{ + str: String, + pane: RenderPane + }, + RenderInput(KeyCode), + ChangeLayout(RenderLayoutKind), + ClearPane(RenderPane), + Exit, +} + +#[derive(Debug, Clone)] +pub enum RenderLayoutKind { + Cli, +} + +#[derive(Debug)] +pub struct RenderLayout { + kind: RenderLayoutKind, + panes: Vec, +} + +impl RenderLayoutKind { + pub fn rects(&self, area: Rect) -> std::rc::Rc<[Rect]> { + match self { + Self::Cli => { + Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Percentage(30), + Constraint::Percentage(70) + ]) + .split(area) + } + } + } + + pub fn generate(&self) -> RenderLayout { + RenderLayout { + kind: self.clone(), + panes: vec![ + Pane { + title: Some(" Input Pane ".to_string()), + target: RenderPane::CliInput, + layout_index: 0, + buffer: String::with_capacity(CLI_INPUT_BUFFE_SIZE) + "> ", + }, + Pane { + title: Some(" Output Pane ".to_string()), + target: RenderPane::CliOutput, + layout_index: 1, + buffer: String::with_capacity(CLI_OUTPUT_BUFFE_SIZE), + } + ] + } + } +} + +const CLI_INPUT_BUFFE_SIZE: usize = 4096; +const CLI_OUTPUT_BUFFE_SIZE: usize = 4096; + +#[allow(dead_code)] +impl Renderer { + pub fn new(rx: mpsc::Receiver, layout: RenderLayoutKind) -> Self { + Self { + buffer: String::new(), + rx, + exit: false, + layout: layout.generate() + } + } + pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + while !self.exit { + terminal.draw(|frame| self.draw(frame))?; + self.listen().await? + } + Ok(()) + } + + pub fn draw(&self, frame: &mut Frame) { + frame.render_widget(self, frame.area()); + } + + fn exit(&mut self) { + self.exit = true; + } + + fn buffer_extend>(&mut self, input: S) { + self.buffer.push_str(input.as_ref()); + } + + fn input_pane(&mut self) -> Option<&mut Pane> { + self.layout.panes.iter_mut().find(|p| p.target == RenderPane::CliInput) + } + + async fn listen(&mut self) -> io::Result<()> { + if let Ok(Some(mes)) = timeout(Duration::from_millis(400), self.rx.recv()).await { + match mes { + RenderCommand::RenderInput(k) => { + if let Some(p) = self.input_pane() { + match k { + KeyCode::Char(c) => { + p.buffer.push(c); + } + KeyCode::Backspace => { + if !p.buffer.ends_with("> ") { + p.buffer.pop(); + } + } + KeyCode::Enter => { + p.buffer.push_str("\n> "); + } + _ => {} + } + } + }, + RenderCommand::RenderStringToPane{ str, pane } => { + if let Some(p) = self.layout.panes.iter_mut().find(|p| p.target == pane) { + p.buffer.push_str(&str); + } + } + RenderCommand::Exit => { + self.exit(); + } + RenderCommand::ChangeLayout(l) => { + match l { + RenderLayoutKind::Cli => { + self.layout = l.generate(); + } + } + } + RenderCommand::ClearPane(pane) => { + if matches!(pane, RenderPane::All) { + for p in self.layout.panes.iter_mut() { + p.buffer.clear(); + } + } else if let Some(p) = self.layout.panes.iter_mut().find(|p| p.target == pane) { + p.buffer.clear(); + } + } + } + } + Ok(()) + } +} + +impl Widget for &Renderer { + fn render(self, area: Rect, buf: &mut Buffer) { + + let layout = self.layout.kind.rects(area); + + for p in self.layout.panes.iter() { + let block = Block::bordered() + .title({ + if let Some(t) = &p.title { + t.clone() + } else { + Default::default() + } + }) + .border_set(border::THICK); + + Paragraph::new(p.buffer.clone()) + .wrap(Wrap::default()) + .left_aligned() + .block(block) + .render(layout[p.layout_index as usize], buf); + } + } +} + diff --git a/src/watcher/watcher.rs b/src/watcher/watcher.rs new file mode 100644 index 0000000..1ef0103 --- /dev/null +++ b/src/watcher/watcher.rs @@ -0,0 +1,141 @@ +use crate::watcher::*; + +use crossterm::{event::{self, Event, KeyCode, KeyEventKind}}; +use tokio::sync::mpsc; +use std::io::{self}; + +use crate::{native_node::node::{NativeNode, NodeCommand}}; + +pub struct Watcher { + render_tx: mpsc::Sender, + parser_tx: mpsc::Sender, + node_tx: mpsc::Sender, + exec_tx: mpsc::Sender, + cmd_buffer: String, + handles: Vec> +} + +#[derive(Default)] +pub struct WatcherBuilder { + addr: Option, + seed_file: Option +} + +impl Watcher { + pub fn build() -> WatcherBuilder { + WatcherBuilder::new() + } + + pub async fn exit(self) { + ratatui::restore(); + // for (i, handle) in self.handles.into_iter().enumerate() { + // let _ = handle.await; + // println!("Joined thread #{i}") + // } + } + + pub async fn poll(&mut self) -> io::Result { + match event::read()? { + Event::Key(k) if k.kind == KeyEventKind::Press => { + match k.code { + KeyCode::Char(c) => { + self.cmd_buffer.push(c); + let message = RenderCommand::RenderInput(k.code); + let _ = self.render_tx.send(message); + } + KeyCode::Backspace => { + self.cmd_buffer.pop(); + let message = RenderCommand::RenderInput(k.code); + let _ = self.render_tx.send(message); + }, + KeyCode::Enter => { + let rd_mes = RenderCommand::RenderInput(k.code); + let pr_mes = ParserCommand::ParseCmdString(self.cmd_buffer.clone()); + let _ = self.render_tx.send(rd_mes); + let _ = self.parser_tx.send(pr_mes); + self.cmd_buffer.clear(); + } + KeyCode::Esc => { + let rd_mes = RenderCommand::Exit; + let pr_mes = ParserCommand::Exit; + let exec_mes = ExecutorCommand::Exit; + let _ = self.render_tx.send(rd_mes); + let _ = self.parser_tx.send(pr_mes); + let _ = self.exec_tx.send(exec_mes); + return Ok(false); + } + _ => {} + }; + } + _ => {} + } + Ok(true) + } +} + +impl WatcherBuilder { + fn new() -> Self { + Self::default() + } + + pub fn addr(mut self, addr: Option) -> Self { + self.addr = addr; + self + } + + pub fn file(mut self, seed_file: Option) -> Self { + self.seed_file = seed_file; + self + } + + pub fn start(self) -> Watcher { + let (render_tx, render_rx) = mpsc::channel::(100); + let (parser_tx, parser_rx) = mpsc::channel::(100); + let (exec_tx, exec_rx) = mpsc::channel::(100); + let (node_tx, node_rx) = mpsc::channel::(100); + + let mut terminal = ratatui::init(); + let render_handle = tokio::spawn({ + async move { + let _ = Renderer::new(render_rx, RenderLayoutKind::Cli).run(&mut terminal).await; + } + }); + + let parser_handle = tokio::spawn({ + let exec_tx = exec_tx.clone(); + async move { + let _ = Parser::new(parser_rx, exec_tx).run().await; + } + }); + + let executor_handle = tokio::spawn({ + let rend_tx = render_tx.clone(); + let node_tx = node_tx.clone(); + async move { + let _ = Executor::new(rend_tx, node_tx, exec_rx).run().await; + } + }); + + let blocks = self.seed_file + .as_ref() + .and_then(|path| std::fs::read_to_string(path).ok()) + .and_then(|content| serde_json::from_str(&content).ok()); + + + let node_handle = tokio::spawn({ + let exec_tx = exec_tx.clone(); + async move { + let _ = NativeNode::new(self.addr.clone(), blocks, exec_tx, node_rx); + } + }); + + Watcher { + render_tx, + node_tx, + parser_tx, + exec_tx, + cmd_buffer: String::new(), + handles: vec![parser_handle, render_handle, executor_handle, node_handle] + } + } +}