From 78f302fc712175a9f4c14abc91703629611e108d Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Thu, 20 Nov 2025 20:49:10 +0700 Subject: [PATCH] added tui --- Cargo.lock | 381 ++++++++++++++++++++++++++- Cargo.toml | 3 + src/main.rs | 720 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 1052 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 845b7ca..c0ef9e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cfg-if" version = "1.0.4" @@ -98,7 +104,7 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -116,18 +122,162 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "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 = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "libc" version = "0.2.177" @@ -143,6 +293,30 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.1.0" @@ -183,12 +357,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -207,6 +393,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2e4cd95294a85c3b4446e63ef054eea43e0205b1fd60120c16b74ff7ff96ad" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "indoc", + "itertools", + "paste", + "strum", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -222,15 +425,45 @@ version = "0.1.0" dependencies = [ "bytes", "clap", + "crossterm", + "futures", + "ratatui", "tokio", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[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 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -240,6 +473,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" @@ -262,6 +501,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.110" @@ -281,7 +542,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", - "mio", + "mio 1.1.0", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -307,6 +568,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "utf8parse" version = "0.2.2" @@ -319,19 +592,50 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[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-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -343,6 +647,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -350,28 +669,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.53.1" @@ -384,24 +721,48 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index 9a10354..0204f10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,6 @@ edition = "2024" clap = { version = "4.4", features = ["derive"] } tokio = { version = "1.0", features = ["full"] } bytes = "1.0" +ratatui = "0.23" +crossterm = "0.27" +futures = "0.3" diff --git a/src/main.rs b/src/main.rs index 870f351..4f9981b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,47 @@ use bytes::BytesMut; use clap::{Parser, Subcommand}; +use crossterm::{ + event::{self, Event, KeyCode}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::{ + Terminal, + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, Gauge, Paragraph}, +}; +use std::io::{self as std_io}; +use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; use tokio::net::TcpStream; -use tokio::time::{Duration, Instant}; +use tokio::sync::Mutex; +use tokio::time::{Duration, Instant, interval}; const DEFAULT_PORT: u16 = 8888; const BUFFER_SIZE: usize = 8192; +const UPDATE_INTERVAL_MS: u64 = 500; + +// Структура для автоматического восстановления терминала +struct TerminalGuard; + +impl TerminalGuard { + fn new() -> Result> { + enable_raw_mode()?; + execute!(std_io::stdout(), EnterAlternateScreen)?; + Ok(Self) + } +} + +impl Drop for TerminalGuard { + fn drop(&mut self) { + let _ = disable_raw_mode(); + let _ = execute!(std_io::stdout(), LeaveAlternateScreen); + } +} #[derive(Parser)] #[command(version, about)] @@ -31,6 +66,40 @@ enum Commands { }, } +#[derive(Clone)] +struct BenchmarkStats { + total_bytes: u64, + current_speed: f64, + average_speed: f64, + duration: Duration, + is_running: bool, + clients_connected: usize, + logs: Vec, +} + +impl Default for BenchmarkStats { + fn default() -> Self { + Self { + total_bytes: 0, + current_speed: 0.0, + average_speed: 0.0, + duration: Duration::from_secs(0), + is_running: true, + clients_connected: 0, + logs: Vec::new(), + } + } +} + +impl BenchmarkStats { + fn add_log(&mut self, message: String) { + self.logs.push(message); + if self.logs.len() > 50 { + self.logs.remove(0); + } + } +} + #[tokio::main] async fn main() -> Result<(), Box> { let cli = Cli::parse(); @@ -52,68 +121,635 @@ async fn main() -> Result<(), Box> { } async fn server(port: u16) -> Result<(), Box> { + let stats = Arc::new(Mutex::new(BenchmarkStats::default())); + let stats_clone = stats.clone(); + + let tui_handle = tokio::spawn(async move { + if let Err(e) = run_server_tui(stats_clone).await { + eprintln!("TUI error: {}", e); + } + }); + let listener = TcpListener::bind(("0.0.0.0", port)).await?; - println!("Server listening on port {}", port); + + { + let mut stats_guard = stats.lock().await; + stats_guard.is_running = true; + stats_guard.add_log(format!("Server started on port {}", port)); + } + + let mut client_tasks = vec![]; + let mut check_interval = interval(Duration::from_millis(100)); loop { - let (mut socket, addr) = listener.accept().await?; - println!("Client connected from {}", addr); + tokio::select! { + _ = check_interval.tick() => { + // Проверяем, нужно ли завершать работу + let should_exit = { + let stats_guard = stats.lock().await; + !stats_guard.is_running + }; + if should_exit { + break; + } + } + accept_result = listener.accept() => { + match accept_result { + Ok((socket, addr)) => { + let log_msg = format!("Client connected from {}", addr); + { + let mut stats_guard = stats.lock().await; + stats_guard.clients_connected += 1; + stats_guard.add_log(log_msg); + } - tokio::spawn(async move { - let mut buf = BytesMut::with_capacity(BUFFER_SIZE); - let mut total_received = 0u64; - let start_time = Instant::now(); - - loop { - match socket.read_buf(&mut buf).await { - Ok(0) => break, // Соединение закрыто - Ok(n) => { - total_received += n as u64; - buf.clear(); + let stats_for_client = stats.clone(); + let task = tokio::spawn(async move { + handle_client(socket, stats_for_client).await; + }); + client_tasks.push(task); } Err(e) => { - eprintln!("Read error: {}", e); - break; + let log_msg = format!("Accept error: {}", e); + { + let mut stats_guard = stats.lock().await; + stats_guard.add_log(log_msg); + } } } } - - let duration = start_time.elapsed(); - let rate = (total_received as f64) / duration.as_secs_f64() / 1024.0 / 1024.0; - println!( - "Received {} bytes in {:.2?} ({:.2} MB/s)", - total_received, duration, rate - ); - }); + _ = tokio::signal::ctrl_c() => { + { + let mut stats_guard = stats.lock().await; + stats_guard.add_log("Received CTRL+C, shutting down...".to_string()); + stats_guard.is_running = false; + } + break; + } + } } + + // Ждем завершения всех клиентских задач + for task in client_tasks { + let _ = task.await; + } + + // Останавливаем TUI + { + let mut stats_guard = stats.lock().await; + stats_guard.is_running = false; + } + + // Даем TUI время завершиться + tokio::time::sleep(Duration::from_millis(500)).await; + tui_handle.abort(); + + println!("Server shutdown complete"); + Ok(()) } -async fn client(host: &str, port: u16, seconds: u64) -> Result<(), Box> { - let mut stream = TcpStream::connect((host, port)).await?; - println!("Connected to server {}:{}", host, port); - - let data = vec![0u8; BUFFER_SIZE]; - let mut total_sent = 0u64; +async fn handle_client(mut socket: TcpStream, stats: Arc>) { + let mut buf = BytesMut::with_capacity(BUFFER_SIZE); + let mut total_received = 0u64; let start_time = Instant::now(); - let timeout = Duration::from_secs(seconds); + let mut last_update = Instant::now(); + let mut last_total = 0u64; - while start_time.elapsed() < timeout { - match stream.write_all(&data).await { - Ok(_) => total_sent += data.len() as u64, + loop { + match socket.read_buf(&mut buf).await { + Ok(0) => break, + Ok(n) => { + total_received += n as u64; + buf.clear(); + + let now = Instant::now(); + let time_since_last_update = now.duration_since(last_update).as_secs_f64(); + + if time_since_last_update >= 0.5 { + let current_speed = ((total_received - last_total) as f64) + / time_since_last_update + / 1024.0 + / 1024.0; + let total_duration = now.duration_since(start_time).as_secs_f64(); + let average_speed = total_received as f64 / total_duration / 1024.0 / 1024.0; + + { + let mut stats_guard = stats.lock().await; + stats_guard.total_bytes = total_received; + stats_guard.current_speed = current_speed; + stats_guard.average_speed = average_speed; + stats_guard.duration = now.duration_since(start_time); + } + + last_update = now; + last_total = total_received; + } + } Err(e) => { - eprintln!("Write error: {}", e); + let log_msg = format!("Read error: {}", e); + { + let mut stats_guard = stats.lock().await; + stats_guard.add_log(log_msg); + } break; } } } let duration = start_time.elapsed(); - let rate = (total_sent as f64) / duration.as_secs_f64() / 1024.0 / 1024.0; - println!( - "Sent {} bytes in {:.2?} ({:.2} MB/s)", - total_sent, duration, rate - ); + let rate = total_received as f64 / duration.as_secs_f64() / 1024.0 / 1024.0; - stream.shutdown().await?; + let log_msg = format!( + "Connection finished: {:.2} MB in {:.2?} ({:.2} MB/s)", + total_received as f64 / 1024.0 / 1024.0, + duration, + rate + ); + { + let mut stats_guard = stats.lock().await; + stats_guard.add_log(log_msg); + stats_guard.clients_connected = stats_guard.clients_connected.saturating_sub(1); + } +} + +async fn client(host: &str, port: u16, seconds: u64) -> Result<(), Box> { + let stats = Arc::new(Mutex::new(BenchmarkStats::default())); + let stats_clone = stats.clone(); + + let tui_handle = tokio::spawn(async move { + if let Err(e) = run_client_tui(stats_clone, seconds.clone()).await { + eprintln!("TUI error: {}", e); + } + }); + + let mut stream = TcpStream::connect((host, port)).await?; + + { + let mut stats_guard = stats.lock().await; + stats_guard.is_running = true; + stats_guard.add_log(format!("Connected to {}:{}", host, port)); + } + + let data = vec![0u8; BUFFER_SIZE]; + let mut total_sent = 0u64; + let start_time = Instant::now(); + let timeout = Duration::from_secs(seconds); + let mut last_update = Instant::now(); + let mut last_total = 0u64; + let mut check_interval = interval(Duration::from_millis(100)); + + 'main_loop: while start_time.elapsed() < timeout { + tokio::select! { + _ = check_interval.tick() => { + // Проверяем, нужно ли завершать работу + let should_exit = { + let stats_guard = stats.lock().await; + !stats_guard.is_running + }; + if should_exit { + break 'main_loop; + } + } + _ = async { + match stream.write_all(&data).await { + Ok(_) => { + total_sent += data.len() as u64; + + let now = Instant::now(); + let time_since_last_update = now.duration_since(last_update).as_secs_f64(); + + if time_since_last_update >= 0.5 { + let current_speed = ((total_sent - last_total) as f64) / time_since_last_update / 1024.0 / 1024.0; + let total_duration = now.duration_since(start_time).as_secs_f64(); + let average_speed = total_sent as f64 / total_duration / 1024.0 / 1024.0; + + { + let mut stats_guard = stats.lock().await; + stats_guard.total_bytes = total_sent; + stats_guard.current_speed = current_speed; + stats_guard.average_speed = average_speed; + stats_guard.duration = now.duration_since(start_time); + } + + last_update = now; + last_total = total_sent; + } + } + Err(e) => { + let log_msg = format!("Write error: {}", e); + { + let mut stats_guard = stats.lock().await; + stats_guard.add_log(log_msg); + } + } + } + } => {} + } + } + + let duration = start_time.elapsed(); + let rate = total_sent as f64 / duration.as_secs_f64() / 1024.0 / 1024.0; + + { + let mut stats_guard = stats.lock().await; + stats_guard.total_bytes = total_sent; + stats_guard.current_speed = rate; + stats_guard.average_speed = rate; + stats_guard.duration = duration; + stats_guard.is_running = false; + stats_guard.add_log(format!("Benchmark completed: {:.2} MB/s", rate)); + } + + // Даем TUI время отобразить финальные результаты + tokio::time::sleep(Duration::from_secs(1)).await; + + tui_handle.abort(); + let _ = stream.shutdown().await; + + println!("Client shutdown complete"); Ok(()) } + +async fn run_server_tui( + stats: Arc>, +) -> Result<(), Box> { + let _guard = TerminalGuard::new()?; + let mut stdout = std_io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut update_interval = interval(Duration::from_millis(UPDATE_INTERVAL_MS)); + + loop { + let stats_guard = stats.lock().await; + + terminal.draw(|f| { + let rect = f.size(); + f.render_widget(Clear, rect); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(3), + Constraint::Length(8), + Constraint::Min(5), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(rect); + + // Title + let title = Paragraph::new(Line::from(vec![ + Span::styled("📡 ", Style::default().fg(Color::Yellow)), + Span::styled( + "NETWORK BENCHMARK SERVER", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + ])) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title, chunks[0]); + + // Stats in horizontal layout + let stats_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(chunks[1]); + + // Clients + let clients_block = Paragraph::new(vec![ + Line::from(Span::styled( + "CONNECTED CLIENTS\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{}", stats_guard.clients_connected), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(clients_block, stats_chunks[0]); + + // Total Data + let total_mb = stats_guard.total_bytes as f64 / 1024.0 / 1024.0; + let total_block = Paragraph::new(vec![ + Line::from(Span::styled( + "TOTAL DATA\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2} MB", total_mb), + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(total_block, stats_chunks[1]); + + // Current Speed + let current_speed_block = Paragraph::new(vec![ + Line::from(Span::styled( + "CURRENT SPEED\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2} MB/s", stats_guard.current_speed), + get_speed_style(stats_guard.current_speed), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(current_speed_block, stats_chunks[2]); + + // Average Speed + let average_speed_block = Paragraph::new(vec![ + Line::from(Span::styled( + "AVERAGE SPEED\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2} MB/s", stats_guard.average_speed), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(average_speed_block, stats_chunks[3]); + + // Logs + let logs: Vec = stats_guard + .logs + .iter() + .rev() + .take(10) + .map(|log| Line::from(Span::raw(log.clone()))) + .collect(); + + let logs_block = Paragraph::new(logs) + .block(Block::default().borders(Borders::ALL).title("Logs")) + .style(Style::default().fg(Color::Gray)); + f.render_widget(logs_block, chunks[2]); + + // Help + let help_text = if stats_guard.is_running { + "Press 'q' to quit | Server is running..." + } else { + "Press 'q' to quit | Server is shutting down..." + }; + + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("Help")) + .style(Style::default().fg(Color::Yellow)); + f.render_widget(help, chunks[3]); + })?; + + drop(stats_guard); + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.code == KeyCode::Char('q') { + // Устанавливаем флаг завершения + { + let mut stats_guard = stats.lock().await; + stats_guard.is_running = false; + } + break; + } + } + } + + update_interval.tick().await; + + { + let stats_guard = stats.lock().await; + if !stats_guard.is_running { + break; + } + } + } + + Ok(()) +} + +async fn run_client_tui( + stats: Arc>, + seconds: u64, +) -> Result<(), Box> { + let _guard = TerminalGuard::new()?; + let mut stdout = std_io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut update_interval = interval(Duration::from_millis(UPDATE_INTERVAL_MS)); + + loop { + let stats_guard = stats.lock().await; + + terminal.draw(|f| { + let rect = f.size(); + f.render_widget(Clear, rect); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(3), + Constraint::Length(9), + Constraint::Length(6), + Constraint::Min(5), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(rect); + + // Title + let title = Paragraph::new(vec![Line::from(vec![ + Span::styled("🚀 ", Style::default().fg(Color::Green)), + Span::styled( + "NETWORK BENCHMARK CLIENT", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), + ])]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title, chunks[0]); + + // Stats in horizontal layout + let stats_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(chunks[1]); + + // Total Data + let total_mb = stats_guard.total_bytes as f64 / 1024.0 / 1024.0; + let total_block = Paragraph::new(vec![ + Line::from(Span::styled( + "TOTAL DATA\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2} MB", total_mb), + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(total_block, stats_chunks[0]); + + // Current Speed + let current_speed_block = Paragraph::new(vec![ + Line::from(Span::styled( + "CURRENT SPEED\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2} MB/s", stats_guard.current_speed), + get_speed_style(stats_guard.current_speed), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(current_speed_block, stats_chunks[1]); + + // Average Speed + let average_speed_block = Paragraph::new(vec![ + Line::from(Span::styled( + "AVERAGE SPEED\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2} MB/s", stats_guard.average_speed), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(average_speed_block, stats_chunks[2]); + + // Duration + let duration_block = Paragraph::new(vec![ + Line::from(Span::styled( + "DURATION\n", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + format!("{:.2?}", stats_guard.duration), + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + )), + ]) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(duration_block, stats_chunks[3]); + + // Progress + let progress = if stats_guard.duration.as_secs() > 0 { + stats_guard.duration.as_secs_f64() % seconds as f64 / seconds as f64 + } else { + 0.0 + }; + + let gauge = Gauge::default() + .block(Block::default().borders(Borders::ALL).title("Activity")) + .gauge_style(Style::default().fg(Color::Cyan)) + .percent((progress * 100.0) as u16); + f.render_widget(gauge, chunks[2]); + + // Logs + let logs: Vec = stats_guard + .logs + .iter() + .rev() + .take(10) + .map(|log| Line::from(Span::raw(log.clone()))) + .collect(); + + let logs_block = Paragraph::new(logs) + .block(Block::default().borders(Borders::ALL).title("Logs")) + .style(Style::default().fg(Color::Gray)); + f.render_widget(logs_block, chunks[3]); + + // Status + let status_text = if stats_guard.is_running { + "🟢 Benchmark in progress..." + } else { + "✅ Benchmark completed!" + }; + + let help_text = format!("{}\nPress 'q' to quit", status_text); + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("Status")) + .style(Style::default().fg(Color::Yellow)); + f.render_widget(help, chunks[4]); + })?; + + drop(stats_guard); + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.code == KeyCode::Char('q') { + // Устанавливаем флаг завершения + { + let mut stats_guard = stats.lock().await; + stats_guard.is_running = false; + } + break; + } + } + } + + update_interval.tick().await; + + { + let stats_guard = stats.lock().await; + if !stats_guard.is_running { + break; + } + } + } + + Ok(()) +} + +fn get_speed_style(speed: f64) -> Style { + if speed > 100.0 { + Style::default() + .fg(Color::LightGreen) + .add_modifier(Modifier::BOLD) + } else if speed > 50.0 { + Style::default().fg(Color::Yellow) + } else if speed > 10.0 { + Style::default().fg(Color::LightRed) + } else { + Style::default().fg(Color::Red) + } +}