From 1d019fa2b48e9013f3d3c0f727a34c7082566b6a Mon Sep 17 00:00:00 2001 From: Helios Date: Tue, 3 Mar 2026 13:55:22 +0100 Subject: [PATCH] client: verbose CLI output, TOML config in APPDATA, desktop install - install.ps1: place exe on Desktop instead of TEMP, start with visible window - main.rs: banner on startup, [CMD]/[OK]/[ERR] prefixed logs with HH:MM:SS timestamps - Config: switch from JSON to TOML (config.toml in %APPDATA%\helios-remote\) - First-run wizard prompts for Relay URL + API Key (relay_code -> api_key) - Add chrono + toml deps to Cargo.toml --- crates/client/Cargo.toml | 2 + crates/client/src/main.rs | 165 +++++++++++++++++++++++--------------- scripts/install.ps1 | 4 +- 3 files changed, 104 insertions(+), 67 deletions(-) diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index a0a968b..2169c35 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -13,6 +13,8 @@ tokio-tungstenite = { version = "0.21", features = ["connect", "native-tls"] } native-tls = { version = "0.2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" +toml = "0.8" +chrono = "0.4" helios-common = { path = "../common" } dirs = "5" tracing = "0.1" diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 468d097..abea5e4 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -2,12 +2,13 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use chrono::Local; use futures_util::{SinkExt, StreamExt}; use native_tls::TlsConnector; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Connector}; -use tracing::{error, info, warn}; +use tracing::{error, warn}; use helios_common::{ClientMessage, ServerMessage}; @@ -16,10 +17,27 @@ mod screenshot; mod input; mod windows_mgmt; +fn ts() -> String { + Local::now().format("%H:%M:%S").to_string() +} + +macro_rules! log_info { + ($($arg:tt)*) => { println!("[{}] {}", ts(), format!($($arg)*)) }; +} +macro_rules! log_cmd { + ($($arg:tt)*) => { println!("[{}] [CMD] {}", ts(), format!($($arg)*)) }; +} +macro_rules! log_ok { + ($($arg:tt)*) => { println!("[{}] [OK] {}", ts(), format!($($arg)*)) }; +} +macro_rules! log_err { + ($($arg:tt)*) => { println!("[{}] [ERR] {}", ts(), format!($($arg)*)) }; +} + #[derive(Debug, Serialize, Deserialize)] struct Config { relay_url: String, - relay_code: String, + api_key: String, label: Option, } @@ -28,19 +46,19 @@ impl Config { let base = dirs::config_dir() .or_else(|| dirs::home_dir()) .unwrap_or_else(|| PathBuf::from(".")); - base.join("helios-remote").join("config.json") + base.join("helios-remote").join("config.toml") } fn load() -> Option { let path = Self::config_path(); let data = std::fs::read_to_string(&path).ok()?; - serde_json::from_str(&data).ok() + toml::from_str(&data).ok() } fn save(&self) -> std::io::Result<()> { let path = Self::config_path(); std::fs::create_dir_all(path.parent().unwrap())?; - let data = serde_json::to_string_pretty(self).unwrap(); + let data = toml::to_string_pretty(self).unwrap(); std::fs::write(&path, data)?; Ok(()) } @@ -48,7 +66,7 @@ impl Config { fn prompt_config() -> Config { let relay_url = { - println!("Relay server URL [default: wss://remote.agent-helios.me/ws]: "); + print!("Relay server URL [default: wss://remote.agent-helios.me/ws]: "); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); let trimmed = input.trim(); @@ -59,46 +77,41 @@ fn prompt_config() -> Config { } }; - let relay_code = { - println!("Enter relay code: "); + let api_key = { + print!("API Key: "); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); input.trim().to_string() }; let label = { - println!("Label for this machine (optional, press Enter to skip): "); + print!("Label for this machine (optional, press Enter to skip): "); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); let trimmed = input.trim().to_string(); if trimmed.is_empty() { None } else { Some(trimmed) } }; - Config { relay_url, relay_code, label } + Config { relay_url, api_key, label } } #[tokio::main] async fn main() { - tracing_subscriber::fmt() - .with_env_filter( - std::env::var("RUST_LOG") - .unwrap_or_else(|_| "helios_client=info".to_string()), - ) - .init(); + println!("=== Helios Remote Client ==="); // Load or prompt for config let config = match Config::load() { Some(c) => { - info!("Loaded config from {:?}", Config::config_path()); + log_info!("Config loaded from {:?}", Config::config_path()); c } None => { - info!("No config found — prompting for setup"); + log_info!("No config found — first-time setup"); let c = prompt_config(); if let Err(e) = c.save() { - error!("Failed to save config: {e}"); + log_err!("Failed to save config: {e}"); } else { - info!("Config saved to {:?}", Config::config_path()); + log_info!("Config saved to {:?}", Config::config_path()); } c } @@ -112,7 +125,7 @@ async fn main() { const MAX_BACKOFF: Duration = Duration::from_secs(30); loop { - info!("Connecting to {}", config.relay_url); + log_info!("Connecting to {}...", config.relay_url); // Build TLS connector - accepts self-signed certs for internal CA (Caddy tls internal) let tls_connector = TlsConnector::builder() .danger_accept_invalid_certs(true) @@ -121,7 +134,7 @@ async fn main() { let connector = Connector::NativeTls(tls_connector); match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await { Ok((ws_stream, _)) => { - info!("Connected!"); + log_info!("Connected!"); backoff = Duration::from_secs(1); // reset on success let (mut write, mut read) = ws_stream.split(); @@ -132,7 +145,7 @@ async fn main() { }; let hello_json = serde_json::to_string(&hello).unwrap(); if let Err(e) = write.send(Message::Text(hello_json)).await { - error!("Failed to send Hello: {e}"); + log_err!("Failed to send Hello: {e}"); tokio::time::sleep(backoff).await; backoff = (backoff * 2).min(MAX_BACKOFF); continue; @@ -148,7 +161,7 @@ async fn main() { let server_msg: ServerMessage = match serde_json::from_str(&text) { Ok(m) => m, Err(e) => { - warn!("Failed to parse server message: {e}\nRaw: {text}"); + log_err!("Failed to parse server message: {e} | raw: {text}"); continue; } }; @@ -161,7 +174,7 @@ async fn main() { let json = serde_json::to_string(&response).unwrap(); let mut w = write_clone.lock().await; if let Err(e) = w.send(Message::Text(json)).await { - error!("Failed to send response: {e}"); + log_err!("Failed to send response: {e}"); } }); } @@ -170,11 +183,11 @@ async fn main() { let _ = w.send(Message::Pong(data)).await; } Ok(Message::Close(_)) => { - info!("Server closed connection"); + log_info!("Server closed connection"); break; } Err(e) => { - error!("WebSocket error: {e}"); + log_err!("WebSocket error: {e}"); break; } _ => {} @@ -184,7 +197,7 @@ async fn main() { warn!("Disconnected. Reconnecting in {:?}...", backoff); } Err(e) => { - error!("Connection failed: {e}"); + log_err!("Connection failed: {e}"); } } @@ -199,15 +212,19 @@ async fn handle_message( ) -> ClientMessage { match msg { ServerMessage::ScreenshotRequest { request_id } => { + log_cmd!("screenshot"); match screenshot::take_screenshot() { - Ok((image_base64, width, height)) => ClientMessage::ScreenshotResponse { - request_id, - image_base64, - width, - height, - }, + Ok((image_base64, width, height)) => { + log_ok!("screenshot taken ({}x{})", width, height); + ClientMessage::ScreenshotResponse { + request_id, + image_base64, + width, + height, + } + } Err(e) => { - error!("Screenshot failed: {e}"); + log_err!("Screenshot failed: {e}"); ClientMessage::Error { request_id, message: format!("Screenshot failed: {e}"), @@ -217,17 +234,20 @@ async fn handle_message( } ServerMessage::ExecRequest { request_id, command } => { - info!("Exec: {command}"); + log_cmd!("exec: {command}"); let mut sh = shell.lock().await; match sh.run(&command).await { - Ok((stdout, stderr, exit_code)) => ClientMessage::ExecResponse { - request_id, - stdout, - stderr, - exit_code, - }, + Ok((stdout, stderr, exit_code)) => { + log_ok!("exec completed (exit {})", exit_code); + ClientMessage::ExecResponse { + request_id, + stdout, + stderr, + exit_code, + } + } Err(e) => { - error!("Exec failed for command {:?}: {e}", command); + log_err!("exec failed for {:?}: {e}", command); ClientMessage::Error { request_id, message: format!( @@ -240,11 +260,14 @@ async fn handle_message( } ServerMessage::ClickRequest { request_id, x, y, button } => { - info!("Click: ({x},{y}) {:?}", button); + log_cmd!("click {} {} {:?}", x, y, button); match input::click(x, y, &button) { - Ok(()) => ClientMessage::Ack { request_id }, + Ok(()) => { + log_ok!("click done"); + ClientMessage::Ack { request_id } + } Err(e) => { - error!("Click failed at ({x},{y}): {e}"); + log_err!("click failed at ({x},{y}): {e}"); ClientMessage::Error { request_id, message: format!("Click at ({x},{y}) failed: {e}"), @@ -254,11 +277,14 @@ async fn handle_message( } ServerMessage::TypeRequest { request_id, text } => { - info!("Type: {} chars", text.len()); + log_cmd!("type: {} chars", text.len()); match input::type_text(&text) { - Ok(()) => ClientMessage::Ack { request_id }, + Ok(()) => { + log_ok!("type done"); + ClientMessage::Ack { request_id } + } Err(e) => { - error!("Type failed: {e}"); + log_err!("type failed: {e}"); ClientMessage::Error { request_id, message: format!("Type failed: {e}"), @@ -268,59 +294,68 @@ async fn handle_message( } ServerMessage::ListWindowsRequest { request_id } => { - info!("ListWindows"); + log_cmd!("list-windows"); match windows_mgmt::list_windows() { - Ok(windows) => ClientMessage::ListWindowsResponse { request_id, windows }, + Ok(windows) => { + log_ok!("list-windows: {} windows", windows.len()); + ClientMessage::ListWindowsResponse { request_id, windows } + } Err(e) => { - error!("ListWindows failed: {e}"); + log_err!("list-windows failed: {e}"); ClientMessage::Error { request_id, message: e } } } } ServerMessage::MinimizeAllRequest { request_id } => { - info!("MinimizeAll"); + log_cmd!("minimize-all"); match windows_mgmt::minimize_all() { - Ok(()) => ClientMessage::Ack { request_id }, + Ok(()) => { + log_ok!("minimize-all done"); + ClientMessage::Ack { request_id } + } Err(e) => { - error!("MinimizeAll failed: {e}"); + log_err!("minimize-all failed: {e}"); ClientMessage::Error { request_id, message: e } } } } ServerMessage::FocusWindowRequest { request_id, window_id } => { - info!("FocusWindow: {window_id}"); + log_cmd!("focus-window: {window_id}"); match windows_mgmt::focus_window(window_id) { - Ok(()) => ClientMessage::Ack { request_id }, + Ok(()) => { + log_ok!("focus-window done"); + ClientMessage::Ack { request_id } + } Err(e) => { - error!("FocusWindow failed: {e}"); + log_err!("focus-window failed: {e}"); ClientMessage::Error { request_id, message: e } } } } ServerMessage::MaximizeAndFocusRequest { request_id, window_id } => { - info!("MaximizeAndFocus: {window_id}"); + log_cmd!("maximize-and-focus: {window_id}"); match windows_mgmt::maximize_and_focus(window_id) { - Ok(()) => ClientMessage::Ack { request_id }, + Ok(()) => { + log_ok!("maximize-and-focus done"); + ClientMessage::Ack { request_id } + } Err(e) => { - error!("MaximizeAndFocus failed: {e}"); + log_err!("maximize-and-focus failed: {e}"); ClientMessage::Error { request_id, message: e } } } } ServerMessage::Ack { request_id } => { - info!("Server ack for {request_id}"); // Nothing to do - server acked something we sent ClientMessage::Ack { request_id } } ServerMessage::Error { request_id, message } => { - error!("Server error (req={request_id:?}): {message}"); - // No meaningful response needed but we need to return something - // Use a dummy ack if we have a request_id + log_err!("server error (req={request_id:?}): {message}"); if let Some(rid) = request_id { ClientMessage::Ack { request_id: rid } } else { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index e95cb29..6e48230 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -10,10 +10,10 @@ $ErrorActionPreference = "Stop" $url = "https://github.com/agent-helios/helios-remote/releases/latest/download/helios-remote-client-windows.exe" -$dest = "$env:TEMP\helios-remote.exe" +$dest = "$env:USERPROFILE\Desktop\Helios Remote.exe" Write-Host "Downloading helios-remote client..." Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing Write-Host "Starting..." -Start-Process -FilePath $dest -NoNewWindow +Start-Process -FilePath $dest