From 54b476a95ec75f703cc0811863bb086a85112549 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 20 Nov 2025 11:36:54 +0800 Subject: [PATCH 1/2] perf: pull up client err --- go-client/pkg/awaken/awaken.go | 30 +++++-- go-client/pkg/awaken/awaken_darwin.go | 28 ++++++- go-client/pkg/awaken/awaken_linux.go | 30 ++++++- go-client/pkg/awaken/awaken_windows.go | 28 +++++++ src-tauri/src/commands/get_token.rs | 11 ++- src-tauri/src/commands/pull_up.rs | 111 +++++++++++++++++++++++-- src-tauri/src/lib.rs | 8 +- ui/composables/useAssetAction.ts | 19 +++++ 8 files changed, 239 insertions(+), 26 deletions(-) diff --git a/go-client/pkg/awaken/awaken.go b/go-client/pkg/awaken/awaken.go index 3e9276d4b..120bd645c 100755 --- a/go-client/pkg/awaken/awaken.go +++ b/go-client/pkg/awaken/awaken.go @@ -108,7 +108,11 @@ func (r *Rouse) HandleRDP(appConfig *config.AppConfig) { } cmd := handleRDP(r, filePath, appConfig) if cmd != nil { - cmd.Run() + if err := cmd.Run(); err != nil { + errorMsg := fmt.Sprintf("Failed to execute RDP application: %v", err) + global.LOG.Error(errorMsg) + fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + } } else { errorMsg := "No RDP application configured or found" global.LOG.Error(errorMsg) @@ -119,7 +123,11 @@ func (r *Rouse) HandleRDP(appConfig *config.AppConfig) { func (r *Rouse) HandleVNC(appConfig *config.AppConfig) { cmd := handleVNC(r, appConfig) if cmd != nil { - cmd.Run() + if err := cmd.Run(); err != nil { + errorMsg := fmt.Sprintf("Failed to execute VNC application: %v", err) + global.LOG.Error(errorMsg) + fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + } } else { errorMsg := "No VNC application configured or found" global.LOG.Error(errorMsg) @@ -130,7 +138,11 @@ func (r *Rouse) HandleVNC(appConfig *config.AppConfig) { func (r *Rouse) HandleSSH(appConfig *config.AppConfig) { cmd := handleSSH(r, appConfig) if cmd != nil { - cmd.Run() + if err := cmd.Run(); err != nil { + errorMsg := fmt.Sprintf("Failed to execute SSH application: %v", err) + global.LOG.Error(errorMsg) + fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + } } else { errorMsg := "No SSH application configured or found" global.LOG.Error(errorMsg) @@ -141,7 +153,11 @@ func (r *Rouse) HandleSSH(appConfig *config.AppConfig) { func (r *Rouse) HandleDB(appConfig *config.AppConfig) { cmd := handleDB(r, appConfig) if cmd != nil { - cmd.Run() + if err := cmd.Run(); err != nil { + errorMsg := fmt.Sprintf("Failed to execute database application: %v", err) + global.LOG.Error(errorMsg) + fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + } } else { errorMsg := "No database application configured or found" global.LOG.Error(errorMsg) @@ -152,7 +168,11 @@ func (r *Rouse) HandleDB(appConfig *config.AppConfig) { func (r *Rouse) HandleCommand(appConfig *config.AppConfig) { cmd := handleCommand(r, appConfig) if cmd != nil { - cmd.Run() + if err := cmd.Run(); err != nil { + errorMsg := fmt.Sprintf("Failed to execute command: %v", err) + global.LOG.Error(errorMsg) + fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + } } else { errorMsg := "No command application configured or found" global.LOG.Error(errorMsg) diff --git a/go-client/pkg/awaken/awaken_darwin.go b/go-client/pkg/awaken/awaken_darwin.go index 8facb4c27..3b4ef1dbb 100755 --- a/go-client/pkg/awaken/awaken_darwin.go +++ b/go-client/pkg/awaken/awaken_darwin.go @@ -18,6 +18,18 @@ func getCommandFromArgs(connectInfo map[string]string, argFormat string) string return argFormat } +// validateAppPath checks if the application path exists +func validateAppPath(appPath string) error { + if appPath == "" { + return fmt.Errorf("application path is empty") + } + // Check if path exists + if _, err := os.Stat(appPath); os.IsNotExist(err) { + return fmt.Errorf("application path does not exist: %s", appPath) + } + return nil +} + func awakenRDPCommand(filePath string, cfg *config.AppConfig) *exec.Cmd { global.LOG.Debug(filePath) cmd := exec.Command("open", filePath) @@ -44,6 +56,10 @@ func awakenVNCCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { "host": r.Host, "port": strconv.Itoa(r.Port), } + if err := validateAppPath(appItem.Path); err != nil { + global.LOG.Error(err.Error()) + return nil + } commands := getCommandFromArgs(connectMap, appItem.ArgFormat) cmd := exec.Command(appItem.Path, strings.Split(commands, " ")...) // 设置环境变量(只对这个子进程有效) @@ -108,10 +124,12 @@ func awakenSSHCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { } } else { - var appPath string - appPath = appItem.Path + appPath := appItem.Path + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } commands := getCommandFromArgs(connectMap, appItem.ArgFormat) - appPath = appItem.Path cmd = exec.Command(appPath, strings.Split(commands, " ")...) } return cmd @@ -165,6 +183,10 @@ func awakenDBCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { ) return cmd } else { + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } if r.Protocol == "sqlserver" { connectMap["protocol"] = "mssql_jdbc_ms_new" } diff --git a/go-client/pkg/awaken/awaken_linux.go b/go-client/pkg/awaken/awaken_linux.go index 8f7688b69..638b4a0e0 100755 --- a/go-client/pkg/awaken/awaken_linux.go +++ b/go-client/pkg/awaken/awaken_linux.go @@ -18,6 +18,18 @@ func getCommandFromArgs(connectInfo map[string]string, argFormat string) string return argFormat } +// validateAppPath checks if the application path exists +func validateAppPath(appPath string) error { + if appPath == "" { + return fmt.Errorf("application path is empty") + } + // Check if path exists + if _, err := os.Stat(appPath); os.IsNotExist(err) { + return fmt.Errorf("application path does not exist: %s", appPath) + } + return nil +} + func awakenRDPCommand(filePath string, cfg *config.AppConfig) *exec.Cmd { global.LOG.Debug(filePath) var appItem *config.AppItem @@ -57,6 +69,10 @@ func awakenVNCCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { "port": strconv.Itoa(r.Port), } + if err := validateAppPath(appItem.Path); err != nil { + global.LOG.Error(err.Error()) + return nil + } commands := getCommandFromArgs(connectMap, appItem.ArgFormat) cmd := exec.Command(appItem.Path, strings.Split(commands, " ")...) // 设置环境变量(只对这个子进程有效) @@ -138,10 +154,12 @@ func awakenSSHCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { if r.Protocol == "sqlserver" { connectMap["protocol"] = "mssql_jdbc_ms_new" } - var appPath string - appPath = appItem.Path + appPath := appItem.Path + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } commands := getCommandFromArgs(connectMap, appItem.ArgFormat) - appPath = appItem.Path cmd = exec.Command(appPath, strings.Split(commands, " ")...) } return cmd @@ -208,7 +226,11 @@ func awakenDBCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { } return cmd } else { - appPath = appItem.Path + appPath := appItem.Path + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } commands := getCommandFromArgs(connectMap, appItem.ArgFormat) return exec.Command(appPath, strings.Split(commands, " ")...) } diff --git a/go-client/pkg/awaken/awaken_windows.go b/go-client/pkg/awaken/awaken_windows.go index 83c6a7450..21656b709 100755 --- a/go-client/pkg/awaken/awaken_windows.go +++ b/go-client/pkg/awaken/awaken_windows.go @@ -60,6 +60,18 @@ func getCommandFromArgs(connectInfo map[string]string, argFormat string) string return argFormat } +// validateAppPath checks if the application path exists +func validateAppPath(appPath string) error { + if appPath == "" { + return fmt.Errorf("application path is empty") + } + // Check if path exists + if _, err := os.Stat(appPath); os.IsNotExist(err) { + return fmt.Errorf("application path does not exist: %s", appPath) + } + return nil +} + func handleRDP(r *Rouse, filePath string, cfg *config.AppConfig) *exec.Cmd { var appItem *config.AppItem appLst := cfg.Windows.RemoteDesktop @@ -73,6 +85,10 @@ func handleRDP(r *Rouse, filePath string, cfg *config.AppConfig) *exec.Cmd { return nil } appPath := appItem.Path + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } connectMap := map[string]string{ "file": filePath, "name": r.getName(), @@ -105,6 +121,10 @@ func handleVNC(r *Rouse, cfg *config.AppConfig) *exec.Cmd { if appItem == nil { return nil } + if err := validateAppPath(appItem.Path); err != nil { + global.LOG.Error(err.Error()) + return nil + } connectMap := map[string]string{ "name": r.getName(), "protocol": r.Protocol, @@ -215,6 +235,10 @@ func handleSSH(r *Rouse, cfg *config.AppConfig) *exec.Cmd { } else { appPath = appItem.Path } + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } connectMap := map[string]string{ "name": r.getName(), "protocol": protocol, @@ -246,6 +270,10 @@ func handleDB(r *Rouse, cfg *config.AppConfig) *exec.Cmd { return nil } appPath := appItem.Path + if err := validateAppPath(appPath); err != nil { + global.LOG.Error(err.Error()) + return nil + } connectMap := map[string]string{ "name": r.getName(), diff --git a/src-tauri/src/commands/get_token.rs b/src-tauri/src/commands/get_token.rs index 86ce405cd..9c2da878d 100644 --- a/src-tauri/src/commands/get_token.rs +++ b/src-tauri/src/commands/get_token.rs @@ -36,10 +36,15 @@ pub async fn get_connect_token( // "get-token-success", // json!({ "status": url_data.status, "data": from_str::(&url_data.data).unwrap() }), // ); - pull_up( - app, + if let Err(e) = pull_up( + app.clone(), url_json.get("url").unwrap().as_str().unwrap().to_string(), - ); + ) { + let _ = app.emit( + "pull-up-failure", + json!({ "error": e }), + ); + } } else { let _ = app.emit( "get-token-failure", diff --git a/src-tauri/src/commands/pull_up.rs b/src-tauri/src/commands/pull_up.rs index 0b74f943b..4834f719b 100644 --- a/src-tauri/src/commands/pull_up.rs +++ b/src-tauri/src/commands/pull_up.rs @@ -1,7 +1,10 @@ use log::{error, info}; use std::env; +use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::process::{Command, Stdio}; +use std::thread; +use std::time::Duration; use tauri::AppHandle; // 映射平台/架构到二进制所在子目录 @@ -89,10 +92,11 @@ fn resolve_executable(is_dev: bool) -> Option { #[tauri::command] /// 启动本地 JumpServerClient 可执行文件,并传入 URL 参数 /// 前端:invoke('pull_up', { url }) -pub fn pull_up(_app: AppHandle, url: String) { +pub fn pull_up(_app: AppHandle, url: String) -> Result<(), String> { if url.trim().is_empty() { - error!("pull_up called with empty url"); - return; + let err_msg = "pull_up called with empty url"; + error!("{}", err_msg); + return Err(err_msg.to_string()); } // 对应 JS: is.dev && !process.env.IS_TEST @@ -102,22 +106,111 @@ pub fn pull_up(_app: AppHandle, url: String) { let is_dev = cfg!(debug_assertions) && !is_test; let Some(exe_path) = resolve_executable(is_dev) else { - error!( + let err_msg = format!( "JumpServerClient executable not found. Searched: {:?}", candidate_paths(is_dev) ); - return; + error!("{}", err_msg); + return Err(err_msg); }; info!("Launching client: {:?} {}", exe_path, url); - if let Err(e) = Command::new(&exe_path) + // 使用管道捕获 stderr,以便检测子进程的错误输出 + let mut child = Command::new(&exe_path) .arg(url) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::inherit()) + .stderr(Stdio::piped()) .spawn() - { - error!("Failed to launch client: {}", e); + .map_err(|e| { + let err_msg = format!("Failed to launch client: {}", e); + error!("{}", err_msg); + err_msg + })?; + + // 获取 stderr 的读取器 + let stderr = child.stderr.take().ok_or_else(|| { + let err_msg = "Failed to capture stderr from client process"; + error!("{}", err_msg); + err_msg.to_string() + })?; + + // 使用通道来在后台线程和主线程之间通信 + let (tx, rx) = std::sync::mpsc::channel::(); + + // 在后台线程中读取 stderr,检查是否有错误输出 + thread::spawn(move || { + let reader = BufReader::new(stderr); + + for line in reader.lines() { + match line { + Ok(line) => { + // 检查是否是错误行(Go 客户端错误通常以 "Error:" 开头) + if line.starts_with("Error:") || line.contains("Error:") { + error!("Client stderr: {}", line); + // 立即发送错误信号 + let _ = tx.send(line.clone()); + } else if !line.trim().is_empty() { + // 记录所有非空输出,可能是警告或错误 + error!("Client stderr: {}", line); + // 如果包含常见错误关键词,也收集起来 + if line.contains("not found") + || line.contains("not configured") + || line.contains("Failed") + || line.contains("failed") { + let _ = tx.send(line); + } + } + } + Err(e) => { + error!("Failed to read stderr line: {}", e); + break; + } + } + } + }); + + // 等待并循环检查错误输出,最多等待2秒 + for _ in 0..20 { + thread::sleep(Duration::from_millis(100)); + + // 检查是否有错误消息 + if let Ok(error_msg) = rx.try_recv() { + let err_msg = format!("Client error: {}", error_msg); + error!("{}", err_msg); + return Err(err_msg); + } + + // 检查进程是否已经退出(可能因为错误而退出) + if let Ok(Some(status)) = child.try_wait() { + if !status.success() { + // 进程已退出且状态不成功,尝试再读取一次错误 + thread::sleep(Duration::from_millis(200)); + if let Ok(error_msg) = rx.try_recv() { + let err_msg = format!("Client error: {}", error_msg); + error!("{}", err_msg); + return Err(err_msg); + } + // 如果没有错误消息,返回退出状态错误 + let err_msg = format!("Client process exited with status: {:?}", status); + error!("{}", err_msg); + return Err(err_msg); + } + // 进程成功退出,这是正常的(某些客户端可能立即退出) + break; + } + } + + // 最后再检查一次是否有错误消息(防止在循环结束后才收到错误) + if let Ok(error_msg) = rx.try_recv() { + let err_msg = format!("Client error: {}", error_msg); + error!("{}", err_msg); + return Err(err_msg); } + + // 如果进程仍在运行,这是正常的,让它在后台继续运行 + // 后续的错误会通过事件发送给前端 + + Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6d73c5a83..882f4d965 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -74,7 +74,9 @@ pub fn run() { // 处理启动时的深度链接 for url in &urls { error!("deep link original URL start_urls : {}", url.as_str()); - pull_up(app.app_handle().clone(), url.as_str().to_string()); + if let Err(e) = pull_up(app.app_handle().clone(), url.as_str().to_string()) { + error!("Failed to pull up client: {}", e); + } } // 深度链接启动时,调用完 pull_up 后直接退出 std::process::exit(0); @@ -85,7 +87,9 @@ pub fn run() { let urls = event.urls(); for url in &urls { error!("deep link original URL on_open_url: {}", url.as_str()); - pull_up(app_handle.clone(), url.as_str().to_string()); + if let Err(e) = pull_up(app_handle.clone(), url.as_str().to_string()) { + error!("Failed to pull up client: {}", e); + } } }); diff --git a/ui/composables/useAssetAction.ts b/ui/composables/useAssetAction.ts index 9cd8f05d6..ef7236ad4 100644 --- a/ui/composables/useAssetAction.ts +++ b/ui/composables/useAssetAction.ts @@ -17,6 +17,7 @@ let unlistenGetAssetDetailSuccess: UnlistenFn | null = null; let unlistenGetAssetDetailFailed: UnlistenFn | null = null; let unlistenRenameSuccess: UnlistenFn | null = null; let unlistenRenameError: UnlistenFn | null = null; +let unlistenPullUpFailure: UnlistenFn | null = null; function releaseTauriEventListeners() { tauriListenersRefCount = Math.max(tauriListenersRefCount - 1, 0); @@ -32,6 +33,7 @@ function releaseTauriEventListeners() { unlistenGetAssetDetailFailed?.(); unlistenRenameSuccess?.(); unlistenRenameError?.(); + unlistenPullUpFailure?.(); unlistenGetTokenFailure = null; unlistenGetTokenSuccess = null; unlistenFavoriteSuccess = null; @@ -42,6 +44,7 @@ function releaseTauriEventListeners() { unlistenGetAssetDetailFailed = null; unlistenRenameSuccess = null; unlistenRenameError = null; + unlistenPullUpFailure = null; tauriListenersInitialized = false; } } @@ -565,6 +568,22 @@ export const useAssetAction = () => { }); }); + unlistenPullUpFailure = await useTauriEventListen("pull-up-failure", (event) => { + interface eventPayload { + error: string; + } + + const payload = event.payload as eventPayload; + const errorMessage = payload.error || t("ConnectError.ConnectFailed"); + + toast.add({ + title: t("ConnectError.ConnectFailed"), + description: errorMessage, + color: "error", + icon: "line-md:close-circle" + }); + }); + tauriListenersInitialized = true; tauriListenersRefCount++; } finally { From b3939dca8cd19f75676bc6cf2fae497f9905f75e Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 20 Nov 2025 11:41:07 +0800 Subject: [PATCH 2/2] perf: update report --- go-client/pkg/awaken/awaken.go | 60 ++++++++++++++-------------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/go-client/pkg/awaken/awaken.go b/go-client/pkg/awaken/awaken.go index 120bd645c..b641cb032 100755 --- a/go-client/pkg/awaken/awaken.go +++ b/go-client/pkg/awaken/awaken.go @@ -82,6 +82,18 @@ func (r *Rouse) getName() string { return replacer.Replace(name) } +// reportError 统一处理错误输出:记录日志并输出到 stderr +func reportError(msg string) { + global.LOG.Error(msg) + fmt.Fprintf(os.Stderr, "Error: %s\n", msg) +} + +// reportErrorf 格式化错误消息并统一处理 +func reportErrorf(format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + reportError(msg) +} + func removeCurRdpVncFile() { re := regexp.MustCompile(`(?i)\.(rdp|vncpaxx)$`) dir, _ := os.UserConfigDir() @@ -101,22 +113,16 @@ func (r *Rouse) HandleRDP(appConfig *config.AppConfig) { filePath := filepath.Join(dir, "jumpserver-client", replacer.Replace(fileName)+".rdp") err := ioutil.WriteFile(filePath, []byte(r.Content), os.ModePerm) if err != nil { - errorMsg := err.Error() - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportError(err.Error()) return } cmd := handleRDP(r, filePath, appConfig) if cmd != nil { if err := cmd.Run(); err != nil { - errorMsg := fmt.Sprintf("Failed to execute RDP application: %v", err) - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportErrorf("Failed to execute RDP application: %v", err) } } else { - errorMsg := "No RDP application configured or found" - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportError("No RDP application configured or found") } } @@ -124,14 +130,10 @@ func (r *Rouse) HandleVNC(appConfig *config.AppConfig) { cmd := handleVNC(r, appConfig) if cmd != nil { if err := cmd.Run(); err != nil { - errorMsg := fmt.Sprintf("Failed to execute VNC application: %v", err) - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportErrorf("Failed to execute VNC application: %v", err) } } else { - errorMsg := "No VNC application configured or found" - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportError("No VNC application configured or found") } } @@ -139,14 +141,10 @@ func (r *Rouse) HandleSSH(appConfig *config.AppConfig) { cmd := handleSSH(r, appConfig) if cmd != nil { if err := cmd.Run(); err != nil { - errorMsg := fmt.Sprintf("Failed to execute SSH application: %v", err) - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportErrorf("Failed to execute SSH application: %v", err) } } else { - errorMsg := "No SSH application configured or found" - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportError("No SSH application configured or found") } } @@ -154,14 +152,10 @@ func (r *Rouse) HandleDB(appConfig *config.AppConfig) { cmd := handleDB(r, appConfig) if cmd != nil { if err := cmd.Run(); err != nil { - errorMsg := fmt.Sprintf("Failed to execute database application: %v", err) - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportErrorf("Failed to execute database application: %v", err) } } else { - errorMsg := "No database application configured or found" - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportError("No database application configured or found") } } @@ -169,14 +163,10 @@ func (r *Rouse) HandleCommand(appConfig *config.AppConfig) { cmd := handleCommand(r, appConfig) if cmd != nil { if err := cmd.Run(); err != nil { - errorMsg := fmt.Sprintf("Failed to execute command: %v", err) - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportErrorf("Failed to execute command: %v", err) } } else { - errorMsg := "No command application configured or found" - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportError("No command application configured or found") } } @@ -194,9 +184,7 @@ func (r *Rouse) Run() { case "mysql", "mariadb", "postgresql", "redis", "oracle", "sqlserver", "mongodb": r.HandleDB(&appConfig) default: - errorMsg := fmt.Sprintf("Unsupported protocol: %s", protocol) - global.LOG.Error(errorMsg) - fmt.Fprintf(os.Stderr, "Error: %s\n", errorMsg) + reportErrorf("Unsupported protocol: %s", protocol) } } else { r.HandleCommand(&appConfig)