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
This commit is contained in:
parent
6e044e3e05
commit
1d019fa2b4
3 changed files with 104 additions and 67 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
|
|
@ -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<Self> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue