added tui

This commit is contained in:
wt committed 2025-11-20 20:49:10 +07:00
1 parent 5108a23425
commit 78f302fc71
3 files changed
+1052 -52

No files matched your search

Generated
+371 -10
View File
@@ -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"
+3
View File
@@ -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"
+678 -42
View File
@@ -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<Self, Box<dyn std::error::Error>> {
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<String>,
}
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<dyn std::error::Error>> {
let cli = Cli::parse();
@@ -52,68 +121,635 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
async fn server(port: u16) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<Mutex<BenchmarkStats>>) {
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<dyn std::error::Error>> {
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<Mutex<BenchmarkStats>>,
) -> Result<(), Box<dyn std::error::Error>> {
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<Line> = 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<Mutex<BenchmarkStats>>,
seconds: u64,
) -> Result<(), Box<dyn std::error::Error>> {
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<Line> = 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)
}
}