feat: file logging on client, logs command to fetch last N lines
This commit is contained in:
parent
23bbb5b603
commit
db3fa9f416
6 changed files with 130 additions and 9 deletions
59
crates/client/src/logger.rs
Normal file
59
crates/client/src/logger.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/// File logger — writes structured log lines alongside the pretty terminal output.
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
static LOG_FILE: OnceLock<Mutex<File>> = OnceLock::new();
|
||||||
|
static LOG_PATH: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn init() {
|
||||||
|
let path = log_path();
|
||||||
|
// Create parent dir if needed
|
||||||
|
if let Some(parent) = std::path::Path::new(&path).parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
match OpenOptions::new().create(true).append(true).open(&path) {
|
||||||
|
Ok(f) => {
|
||||||
|
LOG_PATH.set(path.clone()).ok();
|
||||||
|
LOG_FILE.set(Mutex::new(f)).ok();
|
||||||
|
write_line("INFO", "helios-remote started");
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("[logger] Failed to open log file {path}: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_path() -> String {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let base = std::env::var("LOCALAPPDATA")
|
||||||
|
.unwrap_or_else(|_| "C:\\Temp".to_string());
|
||||||
|
format!("{base}\\helios-remote\\helios-remote.log")
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
"/tmp/helios-remote.log".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_log_path() -> String {
|
||||||
|
LOG_PATH.get().cloned().unwrap_or_else(log_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_line(level: &str, msg: &str) {
|
||||||
|
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||||
|
let line = format!("{now} [{level:<5}] {msg}\n");
|
||||||
|
if let Some(mutex) = LOG_FILE.get() {
|
||||||
|
if let Ok(mut f) = mutex.lock() {
|
||||||
|
let _ = f.write_all(line.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the last `n` lines from the log file.
|
||||||
|
pub fn tail(n: u32) -> String {
|
||||||
|
let path = get_log_path();
|
||||||
|
let content = std::fs::read_to_string(&path).unwrap_or_default();
|
||||||
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
let start = lines.len().saturating_sub(n as usize);
|
||||||
|
lines[start..].join("\n")
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ use base64::Engine;
|
||||||
use helios_common::{ClientMessage, ServerMessage};
|
use helios_common::{ClientMessage, ServerMessage};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
mod logger;
|
||||||
mod shell;
|
mod shell;
|
||||||
mod screenshot;
|
mod screenshot;
|
||||||
mod input;
|
mod input;
|
||||||
|
|
@ -76,21 +77,27 @@ macro_rules! log_status {
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! log_ok {
|
macro_rules! log_ok {
|
||||||
($($arg:tt)*) => {
|
($($arg:tt)*) => {{
|
||||||
println!(" {}\t{}", "✅", format!($($arg)*));
|
let msg = format!($($arg)*);
|
||||||
};
|
println!(" {}\t{}", "✅", msg);
|
||||||
|
logger::write_line("OK", &msg);
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! log_err {
|
macro_rules! log_err {
|
||||||
($($arg:tt)*) => {
|
($($arg:tt)*) => {{
|
||||||
println!(" {}\t{}", "❌", format!($($arg)*));
|
let msg = format!($($arg)*);
|
||||||
};
|
println!(" {}\t{}", "❌", msg);
|
||||||
|
logger::write_line("ERROR", &msg);
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! log_cmd {
|
macro_rules! log_cmd {
|
||||||
($emoji:expr, $($arg:tt)*) => {
|
($emoji:expr, $($arg:tt)*) => {{
|
||||||
println!(" {}\t{}", $emoji, format!($($arg)*));
|
let msg = format!($($arg)*);
|
||||||
};
|
println!(" {}\t{}", $emoji, msg);
|
||||||
|
logger::write_line("CMD", &msg);
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -173,6 +180,7 @@ async fn main() {
|
||||||
// Enable ANSI color codes on Windows (required when running as admin)
|
// Enable ANSI color codes on Windows (required when running as admin)
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
enable_ansi();
|
enable_ansi();
|
||||||
|
logger::init();
|
||||||
|
|
||||||
// Suppress tracing output by default
|
// Suppress tracing output by default
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
|
@ -558,6 +566,13 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ServerMessage::LogsRequest { request_id, lines } => {
|
||||||
|
log_cmd!("📜", "logs (last {lines} lines)");
|
||||||
|
let content = logger::tail(lines);
|
||||||
|
let log_path = logger::get_log_path();
|
||||||
|
ClientMessage::LogsResponse { request_id, content, log_path }
|
||||||
|
}
|
||||||
|
|
||||||
ServerMessage::UploadRequest { request_id, path, content_base64 } => {
|
ServerMessage::UploadRequest { request_id, path, content_base64 } => {
|
||||||
log_cmd!("⬆ ", "upload → {}", path);
|
log_cmd!("⬆ ", "upload → {}", path);
|
||||||
match (|| -> Result<(), String> {
|
match (|| -> Result<(), String> {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ pub enum ServerMessage {
|
||||||
ScreenshotRequest { request_id: Uuid },
|
ScreenshotRequest { request_id: Uuid },
|
||||||
/// Capture a specific window by its HWND (works even if behind other windows)
|
/// Capture a specific window by its HWND (works even if behind other windows)
|
||||||
WindowScreenshotRequest { request_id: Uuid, window_id: u64 },
|
WindowScreenshotRequest { request_id: Uuid, window_id: u64 },
|
||||||
|
/// Fetch the last N lines of the client log file
|
||||||
|
LogsRequest { request_id: Uuid, lines: u32 },
|
||||||
/// Show a MessageBox on the client asking the user to do something.
|
/// Show a MessageBox on the client asking the user to do something.
|
||||||
/// Blocks until the user clicks OK — use this when you need the user
|
/// Blocks until the user clicks OK — use this when you need the user
|
||||||
/// to perform a manual action before continuing.
|
/// to perform a manual action before continuing.
|
||||||
|
|
@ -122,6 +124,11 @@ pub enum ClientMessage {
|
||||||
version: String,
|
version: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
},
|
},
|
||||||
|
LogsResponse {
|
||||||
|
request_id: Uuid,
|
||||||
|
content: String,
|
||||||
|
log_path: String,
|
||||||
|
},
|
||||||
/// Response to a download request
|
/// Response to a download request
|
||||||
DownloadResponse {
|
DownloadResponse {
|
||||||
request_id: Uuid,
|
request_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,29 @@ pub async fn window_screenshot(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GET /sessions/:id/logs?lines=100
|
||||||
|
pub async fn logs(
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let lines: u32 = params.get("lines").and_then(|v| v.parse().ok()).unwrap_or(100);
|
||||||
|
match dispatch(&state, &session_id, "logs", |rid| {
|
||||||
|
ServerMessage::LogsRequest { request_id: rid, lines }
|
||||||
|
}).await {
|
||||||
|
Ok(ClientMessage::LogsResponse { content, log_path, .. }) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(serde_json::json!({ "content": content, "log_path": log_path, "lines": lines })),
|
||||||
|
).into_response(),
|
||||||
|
Ok(ClientMessage::Error { message, .. }) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({ "error": message })),
|
||||||
|
).into_response(),
|
||||||
|
Ok(_) => (StatusCode::BAD_GATEWAY, Json(serde_json::json!({ "error": "Unexpected response" }))).into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /sessions/:id/screenshot
|
/// POST /sessions/:id/screenshot
|
||||||
pub async fn request_screenshot(
|
pub async fn request_screenshot(
|
||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.route("/sessions/:id/prompt", post(api::prompt_user))
|
.route("/sessions/:id/prompt", post(api::prompt_user))
|
||||||
.route("/sessions/:id/windows", get(api::list_windows))
|
.route("/sessions/:id/windows", get(api::list_windows))
|
||||||
.route("/sessions/:id/windows/minimize-all", post(api::minimize_all))
|
.route("/sessions/:id/windows/minimize-all", post(api::minimize_all))
|
||||||
|
.route("/sessions/:id/logs", get(api::logs))
|
||||||
.route("/sessions/:id/windows/:window_id/screenshot", post(api::window_screenshot))
|
.route("/sessions/:id/windows/:window_id/screenshot", post(api::window_screenshot))
|
||||||
.route("/sessions/:id/windows/:window_id/focus", post(api::focus_window))
|
.route("/sessions/:id/windows/:window_id/focus", post(api::focus_window))
|
||||||
.route("/sessions/:id/windows/:window_id/maximize", post(api::maximize_and_focus))
|
.route("/sessions/:id/windows/:window_id/maximize", post(api::maximize_and_focus))
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,17 @@ def cmd_maximize(args):
|
||||||
print(f"Window {wid} maximized on session {sid!r}.")
|
print(f"Window {wid} maximized on session {sid!r}.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_logs(args):
|
||||||
|
"""Fetch the last N lines of the client log file."""
|
||||||
|
sid = resolve_session(args.session_id)
|
||||||
|
resp = _req("GET", f"/sessions/{sid}/logs", params={"lines": args.lines})
|
||||||
|
data = resp.json()
|
||||||
|
if "error" in data:
|
||||||
|
sys.exit(f"[helios-remote] {data['error']}")
|
||||||
|
print(f"# {data.get('log_path', '?')} (last {args.lines} lines)")
|
||||||
|
print(data.get("content", ""))
|
||||||
|
|
||||||
|
|
||||||
def cmd_screenshot_window(args):
|
def cmd_screenshot_window(args):
|
||||||
"""Capture a specific window by ID or title substring → /tmp/helios-remote-screenshot.png"""
|
"""Capture a specific window by ID or title substring → /tmp/helios-remote-screenshot.png"""
|
||||||
sid = resolve_session(args.session_id)
|
sid = resolve_session(args.session_id)
|
||||||
|
|
@ -484,6 +495,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
stp = sub.add_parser("status", help="Show relay + client commit and sync status")
|
stp = sub.add_parser("status", help="Show relay + client commit and sync status")
|
||||||
stp.add_argument("session_id")
|
stp.add_argument("session_id")
|
||||||
|
|
||||||
|
lgp = sub.add_parser("logs", help="Fetch last N lines of client log file")
|
||||||
|
lgp.add_argument("session_id")
|
||||||
|
lgp.add_argument("--lines", type=int, default=100, metavar="N", help="Number of lines (default: 100)")
|
||||||
|
|
||||||
sub.add_parser("server-version", help="Get server version (no auth required)")
|
sub.add_parser("server-version", help="Get server version (no auth required)")
|
||||||
|
|
||||||
vp = sub.add_parser("version", help="Get client version for a session")
|
vp = sub.add_parser("version", help="Get client version for a session")
|
||||||
|
|
@ -550,6 +565,7 @@ def main():
|
||||||
"upload": cmd_upload,
|
"upload": cmd_upload,
|
||||||
"download": cmd_download,
|
"download": cmd_download,
|
||||||
"status": cmd_status,
|
"status": cmd_status,
|
||||||
|
"logs": cmd_logs,
|
||||||
"screenshot-window": cmd_screenshot_window,
|
"screenshot-window": cmd_screenshot_window,
|
||||||
"find-window": cmd_find_window,
|
"find-window": cmd_find_window,
|
||||||
"wait-for-window": cmd_wait_for_window,
|
"wait-for-window": cmd_wait_for_window,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue