From 28382b249535e964ce983fe5a0821a898a077775 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 28 Feb 2026 17:35:40 +0100 Subject: multi file for both sides --- Cargo.lock | 171 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + client.db | Bin 0 -> 12288 bytes src/bin/client.rs | 98 ------------------------- src/bin/client/connection.rs | 32 ++++++++ src/bin/client/database.rs | 30 ++++++++ src/bin/client/main.rs | 17 +++++ src/bin/client/mod.rs | 4 + src/bin/client/platform.rs | 28 +++++++ src/bin/client/repl.rs | 108 +++++++++++++++++++++++++++ src/bin/server.rs | 66 ----------------- src/bin/server/args.rs | 41 +++++++++++ src/bin/server/handler.rs | 15 ++++ src/bin/server/main.rs | 23 ++++++ src/bin/server/mod.rs | 2 + 15 files changed, 472 insertions(+), 164 deletions(-) create mode 100644 client.db delete mode 100644 src/bin/client.rs create mode 100644 src/bin/client/connection.rs create mode 100644 src/bin/client/database.rs create mode 100644 src/bin/client/main.rs create mode 100644 src/bin/client/mod.rs create mode 100644 src/bin/client/platform.rs create mode 100644 src/bin/client/repl.rs delete mode 100644 src/bin/server.rs create mode 100644 src/bin/server/args.rs create mode 100644 src/bin/server/handler.rs create mode 100644 src/bin/server/main.rs create mode 100644 src/bin/server/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 17b9b81..63805f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,156 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "winapi" version = "0.3.9" @@ -28,5 +178,26 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "zagan-bet" version = "0.1.0" dependencies = [ + "rusqlite", "winapi", ] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 33903f4..85cacac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +rusqlite = "0.29" winapi = { version = "0.3", features = ["consoleapi", "processenv", "wincon", "minwindef", "winbase"] } diff --git a/client.db b/client.db new file mode 100644 index 0000000..8ead0e2 Binary files /dev/null and b/client.db differ diff --git a/src/bin/client.rs b/src/bin/client.rs deleted file mode 100644 index 45c5090..0000000 --- a/src/bin/client.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::io::{BufRead, BufReader, Write, stdin, stdout}; -use std::net::TcpStream; -use std::time::Instant; - -#[cfg(windows)] -mod win { - use std::io; - use std::os::windows::io::AsRawHandle; - use winapi::um::consoleapi::GetConsoleMode; - use winapi::um::consoleapi::SetConsoleMode; - use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; - use winapi::um::winbase::STD_OUTPUT_HANDLE; - use winapi::um::processenv::GetStdHandle; - use winapi::shared::minwindef::DWORD; - - pub fn enable_vt() -> io::Result<()> { - unsafe { - let h = GetStdHandle(STD_OUTPUT_HANDLE); - if h.is_null() { return Err(io::Error::last_os_error()); } - - let mut mode: DWORD = 0; - if GetConsoleMode(h, &mut mode) == 0 { - return Err(io::Error::last_os_error()); - } - let new = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; - if SetConsoleMode(h, new) == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) - } - } -} - -fn main() { - #[cfg(windows)] - win::enable_vt().unwrap(); - - let mut addr: Option; - let mut stream: Option = None; - loop { - print!("Command: "); - stdout().flush().unwrap(); - let mut line = String::new(); - stdin().read_line(&mut line).unwrap(); - let line = line.trim(); - if line.is_empty() { - continue; - } - let parts: Vec<&str> = line.splitn(2, ' ').collect(); - match parts[0] { - "connect" => { - if parts.len() < 2 { - println!("Usage: connect "); - continue; - } - addr = Some(parts[1].to_string()); - match TcpStream::connect(addr.as_ref().unwrap()) { - Ok(s) => { - let mut ping_writer = s.try_clone().unwrap(); - let mut ping_reader = BufReader::new(s.try_clone().unwrap()); - let start = Instant::now(); - writeln!(ping_writer, "PING").unwrap(); - let mut response = String::new(); - ping_reader.read_line(&mut response).unwrap(); - let elapsed = start.elapsed(); - println!("Connected to {} with ping delay: {:?}", addr.as_ref().unwrap(), elapsed); - stream = Some(s); - } - Err(e) => { - println!("Failed to connect: {}", e); - } - } - } - "send" => { - if stream.is_none() { - println!("Not connected. Use 'connect ' first."); - continue; - } - if parts.len() < 2 { - println!("Usage: send "); - continue; - } - let msg = parts[1]; - let s = stream.as_mut().unwrap(); - let writer_stream = s.try_clone().unwrap(); - let mut writer = writer_stream; - let response_reader_stream = s.try_clone().unwrap(); - let mut response_reader = BufReader::new(response_reader_stream); - writeln!(writer, "{}", msg).unwrap(); - let mut response = String::new(); - response_reader.read_line(&mut response).unwrap(); - println!("Echo: {}", response.trim()); - } - "quit" => break, - _ => println!("Unknown command. Available: connect, send, quit"), - } - } -} diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs new file mode 100644 index 0000000..9e402cb --- /dev/null +++ b/src/bin/client/connection.rs @@ -0,0 +1,32 @@ +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +pub fn connect(addr: &str) -> Result> { + let (tx, rx) = mpsc::channel(); + let addr_str = addr.to_string(); + thread::spawn(move || { + match TcpStream::connect(&addr_str) { + Ok(s) => tx.send(Ok(s)).unwrap(), + Err(e) => tx.send(Err(e)).unwrap(), + } + }); + match rx.recv_timeout(Duration::from_secs(3)) { + Ok(Ok(s)) => { + let mut ping_writer = s.try_clone().unwrap(); + let mut ping_reader = BufReader::new(s.try_clone().unwrap()); + let start = Instant::now(); + writeln!(ping_writer, "PING").unwrap(); + let mut response = String::new(); + ping_reader.read_line(&mut response).unwrap(); + let elapsed = start.elapsed(); + println!("Connected to {} with ping delay: {:?}", addr, elapsed); + Ok(s) + } + Ok(Err(e)) => Err(Box::new(e)), + Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), + Err(_) => Err("Connection attempt failed".into()), + } +} diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs new file mode 100644 index 0000000..bc8f6f8 --- /dev/null +++ b/src/bin/client/database.rs @@ -0,0 +1,30 @@ +use rusqlite::Connection; + +pub fn init_db() -> Connection { + let conn = Connection::open("client.db").unwrap(); + conn.execute( + "CREATE TABLE IF NOT EXISTS hosts ( + host TEXT PRIMARY KEY, + last_used DATETIME DEFAULT CURRENT_TIMESTAMP + )", + [], + ).unwrap(); + conn +} + +pub fn get_last_host(conn: &Connection) -> Option { + let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap(); + let mut rows = stmt.query([]).unwrap(); + if let Ok(Some(row)) = rows.next() { + row.get(0).ok() + } else { + None + } +} + +pub fn save_host(conn: &Connection, host: &str) { + conn.execute( + "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", + [host], + ).unwrap(); +} diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs new file mode 100644 index 0000000..ce9c92f --- /dev/null +++ b/src/bin/client/main.rs @@ -0,0 +1,17 @@ +mod repl; +mod database; +mod connection; +mod platform; + +use repl::ClientState; + +fn main() { + #[cfg(windows)] + platform::win::enable_vt().unwrap(); + + let mut state = ClientState::new(); + if let Some(host) = state.addr.clone() { + state.connect_to(&host); + } + state.run(); +} diff --git a/src/bin/client/mod.rs b/src/bin/client/mod.rs new file mode 100644 index 0000000..938d862 --- /dev/null +++ b/src/bin/client/mod.rs @@ -0,0 +1,4 @@ +pub mod database; +pub mod connection; +pub mod platform; +pub mod repl; diff --git a/src/bin/client/platform.rs b/src/bin/client/platform.rs new file mode 100644 index 0000000..1a6d71b --- /dev/null +++ b/src/bin/client/platform.rs @@ -0,0 +1,28 @@ +#[cfg(windows)] +pub mod win { + use std::io; + use std::os::windows::io::AsRawHandle; + use winapi::um::consoleapi::GetConsoleMode; + use winapi::um::consoleapi::SetConsoleMode; + use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + use winapi::um::winbase::STD_OUTPUT_HANDLE; + use winapi::um::processenv::GetStdHandle; + use winapi::shared::minwindef::DWORD; + + pub fn enable_vt() -> io::Result<()> { + unsafe { + let h = GetStdHandle(STD_OUTPUT_HANDLE); + if h.is_null() { return Err(io::Error::last_os_error()); } + + let mut mode: DWORD = 0; + if GetConsoleMode(h, &mut mode) == 0 { + return Err(io::Error::last_os_error()); + } + let new = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if SetConsoleMode(h, new) == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + } +} diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs new file mode 100644 index 0000000..d38c54d --- /dev/null +++ b/src/bin/client/repl.rs @@ -0,0 +1,108 @@ +use std::io::{BufRead, BufReader, Write, stdin, stdout}; +use std::net::TcpStream; +use rusqlite::Connection; + +use crate::connection; +use crate::database; + +pub struct ClientState { + pub stream: Option, + pub conn: Connection, + pub addr: Option, + pub default_port: u16, +} + +impl ClientState { + pub fn new() -> Self { + let conn = database::init_db(); + let default_ip = "127.0.0.1".to_string(); + let default_port = 11525; + let last_host = database::get_last_host(&conn); + let default_host = format!("{}:{}", default_ip, default_port); + let addr = last_host.or(Some(default_host)); + + Self { + stream: None, + conn, + addr, + default_port, + } + } + + pub fn connect_to(&mut self, addr: &str) { + self.addr = Some(addr.to_string()); + match connection::connect(addr) { + Ok(s) => { + self.stream = Some(s); + database::save_host(&self.conn, addr); + } + Err(e) => println!("Failed to connect: {}", e), + } + } + + pub fn run(&mut self) { + loop { + print!("> "); + stdout().flush().unwrap(); + let mut line = String::new(); + stdin().read_line(&mut line).unwrap(); + let line = line.trim(); + if line.is_empty() { + continue; + } + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + match parts[0] { + "connect" => { + let addr_str = if parts.len() < 2 { + if let Some(h) = &self.addr { + h.clone() + } else { + println!("No host specified and no previous connection. Usage: connect :"); + continue; + } + } else { + let mut a = parts[1].to_string(); + if !a.contains(':') { + a = format!("{}:{}", a, self.default_port); + } + a + }; + self.connect_to(&addr_str); + } + "send" => { + if self.stream.is_none() { + println!("Not connected. Use 'connect :' first."); + continue; + } + if parts.len() < 2 { + println!("Usage: send "); + continue; + } + let msg = parts[1]; + let s = self.stream.as_mut().unwrap(); + let writer_stream = s.try_clone().unwrap(); + let mut writer = writer_stream; + let response_reader_stream = s.try_clone().unwrap(); + let mut response_reader = BufReader::new(response_reader_stream); + writeln!(writer, "{}", msg).unwrap(); + let mut response = String::new(); + response_reader.read_line(&mut response).unwrap(); + println!("Echo: {}", response.trim()); + } + "disconnect" => { + self.stream = None; + println!("Disconnected"); + } + "help" => { + help(); + } + "quit" => break, + _ => help(), + } + } + } +} + +fn help() { + println!("Available commands: connect :, send , disconnect, help, quit"); +} diff --git a/src/bin/server.rs b/src/bin/server.rs deleted file mode 100644 index 215d2ae..0000000 --- a/src/bin/server.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::io::{BufRead, BufReader, Write}; -use std::net::TcpListener; -use std::thread; - -fn handle_client(stream: std::net::TcpStream) { - println!("Client connected"); - let reader_stream = stream.try_clone().unwrap(); - let reader = BufReader::new(reader_stream); - let mut writer = stream; - for line in reader.lines() { - let line = line.unwrap(); - println!("Received: {}", line); - writeln!(writer, "{}", line).unwrap(); - } - println!("Client disconnected"); -} - -fn main() { - let args: Vec = std::env::args().collect(); - let mut ip = "127.0.0.1".to_string(); - let mut port = 8080; - let mut i = 1; - while i < args.len() { - if args[i] == "-p" { - if i + 1 < args.len() { - port = args[i + 1].parse::().unwrap_or_else(|_| { - eprintln!("Invalid port number: {}", args[i + 1]); - std::process::exit(1); - }); - i += 2; - } else { - eprintln!("-p requires a port number"); - std::process::exit(1); - } - } else if args[i] == "-i" { - if i + 1 < args.len() { - ip = args[i + 1].clone(); - if ip != "127.0.0.1" && ip != "0.0.0.0" { - eprintln!("Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0", ip); - std::process::exit(1); - } - i += 2; - } else { - eprintln!("-i requires an IP address"); - std::process::exit(1); - } - } else { - eprintln!("Unknown argument: {}", args[i]); - std::process::exit(1); - } - } - let listener = match TcpListener::bind(format!("{}:{}", ip, port)) { - Ok(l) => l, - Err(e) => { - eprintln!("Failed to bind to {}:{}: {}", ip, port, e); - std::process::exit(1); - } - }; - println!("Server listening on {}", listener.local_addr().unwrap()); - for stream in listener.incoming() { - let stream = stream.unwrap(); - thread::spawn(move || { - handle_client(stream); - }); - } -} diff --git a/src/bin/server/args.rs b/src/bin/server/args.rs new file mode 100644 index 0000000..31c0acc --- /dev/null +++ b/src/bin/server/args.rs @@ -0,0 +1,41 @@ +pub struct Args { + pub ip: String, + pub port: u16, +} + +pub fn parse_args() -> Args { + let args: Vec = std::env::args().collect(); + let mut ip = "127.0.0.1".to_string(); + let mut port = 11525; + let mut i = 1; + while i < args.len() { + if args[i] == "-p" { + if i + 1 < args.len() { + port = args[i + 1].parse::().unwrap_or_else(|_| { + eprintln!("Invalid port number: {}", args[i + 1]); + std::process::exit(1); + }); + i += 2; + } else { + eprintln!("-p requires a port number"); + std::process::exit(1); + } + } else if args[i] == "-i" { + if i + 1 < args.len() { + ip = args[i + 1].clone(); + if ip != "127.0.0.1" && ip != "0.0.0.0" { + eprintln!("Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0", ip); + std::process::exit(1); + } + i += 2; + } else { + eprintln!("-i requires an IP address"); + std::process::exit(1); + } + } else { + eprintln!("Unknown argument: {}", args[i]); + std::process::exit(1); + } + } + Args { ip, port } +} diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs new file mode 100644 index 0000000..906a91f --- /dev/null +++ b/src/bin/server/handler.rs @@ -0,0 +1,15 @@ +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; + +pub fn handle_client(stream: TcpStream) { + println!("Client connected"); + let reader_stream = stream.try_clone().unwrap(); + let reader = BufReader::new(reader_stream); + let mut writer = stream; + for line in reader.lines() { + let line = line.unwrap(); + println!("Received: {}", line); + writeln!(writer, "{}", line).unwrap(); + } + println!("Client disconnected"); +} diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs new file mode 100644 index 0000000..2c6e1dc --- /dev/null +++ b/src/bin/server/main.rs @@ -0,0 +1,23 @@ +mod args; +mod handler; + +use std::net::TcpListener; +use std::thread; + +fn main() { + let args = args::parse_args(); + let listener = match TcpListener::bind(format!("{}:{}", args.ip, args.port)) { + Ok(l) => l, + Err(e) => { + eprintln!("Failed to bind to {}:{}: {}", args.ip, args.port, e); + std::process::exit(1); + } + }; + println!("Server listening on {}", listener.local_addr().unwrap()); + for stream in listener.incoming() { + let stream = stream.unwrap(); + thread::spawn(move || { + handler::handle_client(stream); + }); + } +} diff --git a/src/bin/server/mod.rs b/src/bin/server/mod.rs new file mode 100644 index 0000000..adb60bb --- /dev/null +++ b/src/bin/server/mod.rs @@ -0,0 +1,2 @@ +pub mod args; +pub mod handler; -- cgit v1.2.3