From b5ef6b92e71c7af66534fa1dd8c5e73a8b2899c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E7=99=BB=E5=B1=B1?= Date: Fri, 27 Mar 2026 13:12:46 +0800 Subject: [PATCH] feat: redesign launcher UI and harden runtime controls --- src-tauri/src/commands.rs | 5 + src-tauri/src/config.rs | 3 + src-tauri/src/error.rs | 3 + src-tauri/src/lib.rs | 3 +- src-tauri/src/process.rs | 26 +- src-tauri/src/state.rs | 42 +- src/app.rs | 246 +++++++--- src/components/config_form.rs | 540 ++++++++++++++++------ src/components/log_viewer.rs | 14 +- src/logs.css | 203 +++++---- src/types.rs | 8 + styles.css | 816 ++++++++++++++++++++-------------- 12 files changed, 1271 insertions(+), 638 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2df0d66..949e204 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -90,3 +90,8 @@ pub async fn check_tcp_connection(host: String, port: u16) -> Result { Ok(result) } + +#[tauri::command] +pub async fn is_rustfs_process_running() -> Result { + Ok(state::is_rustfs_process_running()) +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 79bdf7d..9c09742 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -8,6 +8,8 @@ pub struct RustFsConfig { #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub console_port: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub access_key: Option, @@ -22,6 +24,7 @@ impl Default for RustFsConfig { binary_path: None, data_path: String::new(), port: Some(9000), + console_port: Some(9001), host: Some("127.0.0.1".to_string()), access_key: Some("rustfsadmin".to_string()), secret_key: Some("rustfsadmin".to_string()), diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 33dd680..37bd897 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -11,6 +11,9 @@ pub enum Error { #[error("Data path does not exist: {0}")] DataPathNotExist(String), + #[error("API port and console port must be different")] + PortConflict, + #[error("RustFS binary not found at {0}")] BinaryNotFound(String), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0823408..53348ea 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -100,7 +100,8 @@ pub fn run() { commands::get_app_logs, commands::get_rustfs_logs, commands::diagnose_rustfs_binary, - commands::check_tcp_connection + commands::check_tcp_connection, + commands::is_rustfs_process_running ]) .build(tauri::generate_context!()) .expect("error building tauri application") diff --git a/src-tauri/src/process.rs b/src-tauri/src/process.rs index ef3789c..3f586ef 100644 --- a/src-tauri/src/process.rs +++ b/src-tauri/src/process.rs @@ -170,6 +170,13 @@ pub fn launch(config: RustFsConfig) -> Result { return Err(Error::DataPathRequired); } + let api_port = config.port.unwrap_or(9000); + let console_port = config.console_port.unwrap_or(9001); + + if config.console_enable && api_port == console_port { + return Err(Error::PortConflict); + } + let binary_path = match &config.binary_path { Some(path) => PathBuf::from(path), None => get_binary_path()?, @@ -199,10 +206,16 @@ pub fn launch(config: RustFsConfig) -> Result { let address = format!( "{}:{}", config.host.as_deref().unwrap_or("127.0.0.1"), - config.port.unwrap_or(9000) + api_port ); cmd.arg("--address").arg(&address); + let console_address = format!( + "{}:{}", + config.host.as_deref().unwrap_or("127.0.0.1"), + console_port + ); + if let Some(access_key) = &config.access_key { cmd.arg("--access-key").arg(access_key); } @@ -211,6 +224,7 @@ pub fn launch(config: RustFsConfig) -> Result { } if config.console_enable { cmd.arg("--console-enable"); + cmd.arg("--console-address").arg(&console_address); } #[cfg(windows)] @@ -220,7 +234,15 @@ pub fn launch(config: RustFsConfig) -> Result { cmd.creation_flags(CREATE_NO_WINDOW); } - add_app_log(format!("Spawning command: {:?}", cmd)); + add_app_log(format!( + "Spawning RustFS: binary={}, address={}, console_enable={}, console_address={}, access_key_set={}, secret_key_set={}", + binary_path.display(), + address, + config.console_enable, + console_address, + config.access_key.is_some(), + config.secret_key.is_some() + )); let mut child = cmd .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index e6dd3d7..d3157ef 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -15,14 +15,34 @@ lazy_static! { lazy_static! { static ref ANSI_REGEX: Regex = Regex::new(r"\x1B\[[0-9;]*m").unwrap(); + static ref SECRET_PATTERNS: Vec = vec![ + Regex::new(r#"(--access-key\s+)(\S+)"#).unwrap(), + Regex::new(r#"(--secret-key\s+)(\S+)"#).unwrap(), + Regex::new(r#"(access_key\s*=\s*)([^,\s]+)"#).unwrap(), + Regex::new(r#"(secret_key\s*=\s*)([^,\s]+)"#).unwrap(), + Regex::new(r#"(access_key:\s*Some\(")([^"]+)("\))"#).unwrap(), + Regex::new(r#"(secret_key:\s*Some\(")([^"]+)("\))"#).unwrap(), + Regex::new(r#"("access_key"\s*:\s*")([^"]+)(")"#).unwrap(), + Regex::new(r#"("secret_key"\s*:\s*")([^"]+)(")"#).unwrap(), + ]; } fn clean_ansi_codes(s: &str) -> String { ANSI_REGEX.replace_all(s, "").to_string() } +fn redact_secrets(s: &str) -> String { + SECRET_PATTERNS.iter().fold(s.to_string(), |acc, regex| { + regex.replace_all(&acc, "${1}[REDACTED]${3}").to_string() + }) +} + +fn clean_log_message(s: &str) -> String { + redact_secrets(&clean_ansi_codes(s)) +} + fn buffer_log(logs: &Arc>>, message: String, capacity: usize) -> String { - let cleaned_message = clean_ansi_codes(&message); + let cleaned_message = clean_log_message(&message); let log_entry = format!( "[{}] {}", chrono::Local::now().format("%H:%M:%S"), @@ -75,6 +95,26 @@ pub fn get_rustfs_logs() -> Vec { RUSTFS_LOGS.lock().unwrap().iter().cloned().collect() } +pub fn is_rustfs_process_running() -> bool { + let mut process_guard = RUSTFS_PROCESS.lock().unwrap(); + + match process_guard.as_mut() { + Some(child) => match child.try_wait() { + Ok(None) => true, + Ok(Some(_)) => { + *process_guard = None; + false + } + Err(e) => { + add_app_log(format!("Failed to inspect RustFS process status: {}", e)); + *process_guard = None; + false + } + }, + None => false, + } +} + pub fn set_rustfs_process(process: Child) { let pid = process.id(); *RUSTFS_PROCESS.lock().unwrap() = Some(process); diff --git a/src/app.rs b/src/app.rs index d5246d6..27a32a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,11 @@ use crate::components::config_form::ConfigForm; use crate::components::log_viewer::LogViewer; use crate::components::toast::{Toast, ToastMessage, ToastType}; -use crate::types::{CommandResponse, LogType, RustFsConfig}; +use crate::types::{CommandResponse, LogEntry, LogType, RustFsConfig}; use leptos::ev::SubmitEvent; use leptos::prelude::*; use std::collections::VecDeque; +use std::sync::atomic::{AtomicU64, Ordering}; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; @@ -26,12 +27,46 @@ fn is_tauri() -> bool { .unwrap_or(false) } +#[derive(serde::Serialize, serde::Deserialize)] +struct PersistedConfig { + data_path: String, + port: Option, + console_port: Option, + host: Option, + console_enable: bool, +} + +impl From<&RustFsConfig> for PersistedConfig { + fn from(config: &RustFsConfig) -> Self { + Self { + data_path: config.data_path.clone(), + port: config.port, + console_port: config.console_port, + host: config.host.clone(), + console_enable: config.console_enable, + } + } +} + +impl From for RustFsConfig { + fn from(config: PersistedConfig) -> Self { + RustFsConfig { + data_path: config.data_path, + port: config.port, + console_port: config.console_port, + host: config.host, + console_enable: config.console_enable, + ..RustFsConfig::default() + } + } +} + fn load_config() -> RustFsConfig { if let Some(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { if let Ok(Some(json)) = storage.get_item("rustfs_config") { - if let Ok(config) = serde_json::from_str(&json) { - return config; + if let Ok(config) = serde_json::from_str::(&json) { + return config.into(); } } } @@ -42,25 +77,89 @@ fn load_config() -> RustFsConfig { fn save_config(config: &RustFsConfig) { if let Some(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { - if let Ok(json) = serde_json::to_string(config) { + if let Ok(json) = serde_json::to_string(&PersistedConfig::from(config)) { let _ = storage.set_item("rustfs_config", &json); } } } } +fn describe_config(config: &RustFsConfig) -> String { + format!( + "data_path={}, host={:?}, port={:?}, console_enable={}, access_key_set={}, secret_key_set={}", + config.data_path, + config.host, + config.port, + config.console_enable, + config.access_key.is_some(), + config.secret_key.is_some() + ) +} + const APP_LOG_CAPACITY: usize = 100; const RUSTFS_LOG_CAPACITY: usize = 1000; +const DEFAULT_RUSTFS_PORT: u16 = 9000; +const DEFAULT_CONSOLE_PORT: u16 = 9001; +static NEXT_LOG_ID: AtomicU64 = AtomicU64::new(1); -fn push_log(writer: WriteSignal>, msg: String, capacity: usize) { +fn next_log_id() -> u64 { + NEXT_LOG_ID.fetch_add(1, Ordering::Relaxed) +} + +fn to_log_entry(message: String) -> LogEntry { + LogEntry { + id: next_log_id(), + message, + } +} + +fn push_log(writer: WriteSignal>, msg: String, capacity: usize) { writer.update(|logs| { - logs.push_back(msg); + logs.push_back(to_log_entry(msg)); if logs.len() > capacity { logs.pop_front(); } }); } +fn sync_runtime_status( + config: ReadSignal, + set_is_running: WriteSignal, + set_can_stop: WriteSignal, + set_service_status: WriteSignal, +) { + spawn_local(async move { + if !is_tauri() { + set_is_running.set(false); + set_can_stop.set(false); + set_service_status.set(false); + return; + } + + let current = config.get_untracked(); + let host = current.host.unwrap_or_else(|| "127.0.0.1".to_string()); + let port = current.port.unwrap_or(DEFAULT_RUSTFS_PORT); + + let tcp_args = js_sys::Object::new(); + js_sys::Reflect::set(&tcp_args, &"host".into(), &host.into()).unwrap(); + js_sys::Reflect::set(&tcp_args, &"port".into(), &port.into()).unwrap(); + + let service_online = tauri_invoke("check_tcp_connection", tcp_args.into()) + .await + .as_bool() + .unwrap_or(false); + let managed_running = + tauri_invoke("is_rustfs_process_running", js_sys::Object::new().into()) + .await + .as_bool() + .unwrap_or(false); + + set_service_status.set(service_online); + set_can_stop.set(managed_running); + set_is_running.set(managed_running || service_online); + }); +} + #[component] pub fn App() -> impl IntoView { let (config, set_config) = signal(load_config()); @@ -71,10 +170,11 @@ pub fn App() -> impl IntoView { let (toasts, set_toasts) = signal(Vec::::new()); let (is_running, set_is_running) = signal(false); - let (app_logs, set_app_logs) = signal(VecDeque::::new()); - let (rustfs_logs, set_rustfs_logs) = signal(VecDeque::::new()); + let (app_logs, set_app_logs) = signal(VecDeque::::new()); + let (rustfs_logs, set_rustfs_logs) = signal(VecDeque::::new()); let (current_log_type, set_current_log_type) = signal(LogType::App); let (service_status, set_service_status) = signal(false); + let (can_stop, set_can_stop) = signal(false); let remove_toast = Callback::new(move |id: u64| { set_toasts.update(|current| { @@ -106,41 +206,24 @@ pub fn App() -> impl IntoView { // Health Check Polling Effect::new(move |_| { - spawn_local(async move { - if !is_tauri() { - return; - } + if !is_tauri() { + return; + } - let closure = Closure::wrap(Box::new(move || { - spawn_local(async move { - let current = config.get_untracked(); - if let Some(port) = current.port { - let host = current.host.unwrap_or_else(|| "127.0.0.1".to_string()); - - let args = js_sys::Object::new(); - js_sys::Reflect::set(&args, &"host".into(), &host.into()).unwrap(); - js_sys::Reflect::set(&args, &"port".into(), &port.into()).unwrap(); - - match tauri_invoke("check_tcp_connection", args.into()) - .await - .as_bool() - { - Some(is_active) => set_service_status.set(is_active), - None => set_service_status.set(false), - } - } - }); - }) as Box); - - let _ = web_sys::window() - .unwrap() - .set_interval_with_callback_and_timeout_and_arguments_0( - closure.as_ref().unchecked_ref(), - 3000, // 3 seconds - ); + sync_runtime_status(config, set_is_running, set_can_stop, set_service_status); - closure.forget(); // Leak the closure to keep it alive - }); + let closure = Closure::wrap(Box::new(move || { + sync_runtime_status(config, set_is_running, set_can_stop, set_service_status); + }) as Box); + + let _ = web_sys::window() + .unwrap() + .set_interval_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + 3000, // 3 seconds + ); + + closure.forget(); // Leak the closure to keep it alive }); let app_log_writer = set_app_logs; @@ -167,7 +250,7 @@ pub fn App() -> impl IntoView { const RUSTFS_EXIT_EVENT: &str = "rustfs-exit"; fn create_log_listener( - logs_signal: WriteSignal>, + logs_signal: WriteSignal>, max_logs: usize, ) -> Closure { Closure::wrap(Box::new(move |event: JsValue| { @@ -183,6 +266,7 @@ pub fn App() -> impl IntoView { if let Ok(payload) = js_sys::Reflect::get(&event, &"payload".into()) { if let Some(exit_code) = payload.as_string() { set_is_running.set(false); + set_can_stop.set(false); set_service_status.set(false); show_toast( format!("RustFS exited with code: {}", exit_code), @@ -238,21 +322,22 @@ pub fn App() -> impl IntoView { // Fetch initial logs let app_logs_value = tauri_invoke("get_app_logs", js_sys::Object::new().into()).await; if let Ok(logs_vec) = serde_wasm_bindgen::from_value::>(app_logs_value) { - app_log_writer.set(logs_vec.into_iter().collect()); + app_log_writer.set(logs_vec.into_iter().map(to_log_entry).collect()); } let rustfs_logs_value = tauri_invoke("get_rustfs_logs", js_sys::Object::new().into()).await; if let Ok(logs_vec) = serde_wasm_bindgen::from_value::>(rustfs_logs_value) { - rustfs_log_writer.set(logs_vec.into_iter().collect()); + rustfs_log_writer.set(logs_vec.into_iter().map(to_log_entry).collect()); } }); let launch_rustfs = move |ev: SubmitEvent| { ev.prevent_default(); set_is_running.set(true); + set_can_stop.set(true); show_toast("Launching RustFS...".to_string(), ToastType::Info); - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Launch button clicked", now), @@ -260,7 +345,11 @@ pub fn App() -> impl IntoView { ); push_log( set_app_logs, - format!("[{}] Config: {:?}", now, config.get()), + format!( + "[{}] Config summary: {}", + now, + describe_config(&config.get()) + ), APP_LOG_CAPACITY, ); @@ -277,6 +366,7 @@ pub fn App() -> impl IntoView { APP_LOG_CAPACITY, ); set_is_running.set(false); + set_can_stop.set(false); return; } @@ -290,7 +380,7 @@ pub fn App() -> impl IntoView { current_config.host ); - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Calling tauri_invoke with command: launch_rustfs", now), @@ -303,7 +393,7 @@ pub fn App() -> impl IntoView { js_sys::Reflect::set(&args, &"config".into(), &config_js).unwrap(); let result_value = tauri_invoke("launch_rustfs", args.into()).await; - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Invoke result: {:?}", now, result_value), @@ -312,7 +402,7 @@ pub fn App() -> impl IntoView { match serde_wasm_bindgen::from_value::(result_value) { Ok(CommandResponse { success, message }) => { - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Result message: {}", now, message), @@ -324,7 +414,7 @@ pub fn App() -> impl IntoView { "RustFS launched successfully!".to_string(), ToastType::Success, ); - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Launch successful!", now), @@ -332,24 +422,26 @@ pub fn App() -> impl IntoView { ); } else { show_toast(format!("Launch failed: {}", message), ToastType::Error); - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Launch result: {}", now, message), APP_LOG_CAPACITY, ); set_is_running.set(false); + set_can_stop.set(false); } } Err(_) => { show_toast("RustFS launch command failed".to_string(), ToastType::Error); - let now = js_sys::Date::new_0().to_locale_time_string("en-US".into()); + let now = js_sys::Date::new_0().to_locale_time_string("en-US"); push_log( set_app_logs, format!("[{}] Launch completed but response parsing failed", now), APP_LOG_CAPACITY, ); set_is_running.set(false); + set_can_stop.set(false); } } }); @@ -370,6 +462,7 @@ pub fn App() -> impl IntoView { Ok(res) => { if res.success { set_is_running.set(false); + set_can_stop.set(false); set_service_status.set(false); show_toast("RustFS stopped".to_string(), ToastType::Success); push_log( @@ -402,14 +495,48 @@ pub fn App() -> impl IntoView {