From 8dce06c30ebb6bb08affdbfcb5dd724ab01b492c Mon Sep 17 00:00:00 2001 From: landaiqing Date: Tue, 8 Jul 2025 12:41:30 +0800 Subject: [PATCH] :bug: Fixed the reboot issue on different platforms --- .../internal/services/hotkeyservice.ts | 10 +- internal/services/restart_darwin.go | 112 +++++++++++++++++ internal/services/restart_linux.go | 112 +++++++++++++++++ internal/services/restart_windows.go | 118 ++++++++++++++++++ internal/services/self_update_service.go | 68 +--------- 5 files changed, 344 insertions(+), 76 deletions(-) create mode 100644 internal/services/restart_darwin.go create mode 100644 internal/services/restart_linux.go create mode 100644 internal/services/restart_windows.go diff --git a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts index 78da475..f85d66d 100644 --- a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts +++ b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * HotkeyService Windows全局热键服务 + * HotkeyService Linux全局热键服务 * @module */ @@ -53,14 +53,6 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(157291181) as any; - return $resultPromise; -} - /** * UnregisterHotkey 取消注册全局热键 */ diff --git a/internal/services/restart_darwin.go b/internal/services/restart_darwin.go new file mode 100644 index 0000000..71d5a66 --- /dev/null +++ b/internal/services/restart_darwin.go @@ -0,0 +1,112 @@ +//go:build darwin + +package services + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" +) + +// restartApplication Darwin(macOS)平台的重启实现 +func (s *SelfUpdateService) restartApplication() error { + // 获取当前可执行文件路径 + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // 获取当前工作目录 + workDir, err := os.Getwd() + if err != nil { + s.logger.Error("Failed to get working directory", "error", err) + workDir = filepath.Dir(exe) // 如果获取失败,使用可执行文件所在目录 + } + + // 在macOS上,我们使用一个shell脚本来重启应用程序 + // 创建一个唯一的临时shell脚本 + scriptPath := fmt.Sprintf("/tmp/restart_voidraft_%d_%d.sh", os.Getpid(), time.Now().Unix()) + scriptContent := fmt.Sprintf(`#!/bin/bash +sleep 1 +cd %s +%s %s & +rm "%s" +`, + shellEscape(workDir), shellEscape(exe), + shellEscapeArgs(os.Args[1:]), scriptPath) + + s.logger.Info("Creating restart script", "path", scriptPath) + + // 写入脚本文件 + err = os.WriteFile(scriptPath, []byte(scriptContent), 0755) + if err != nil { + return fmt.Errorf("failed to create restart script: %w", err) + } + + // 启动脚本 + cmd := exec.Command("/bin/bash", scriptPath) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, // 创建新的会话,使进程独立于父进程 + } + + err = cmd.Start() + if err != nil { + return fmt.Errorf("failed to start restart script: %w", err) + } + + // 给脚本一点时间启动 + time.Sleep(100 * time.Millisecond) + + // 立即退出当前进程 + os.Exit(0) + + return nil // 不会执行到这里 +} + +// shellEscape 转义单个shell参数或路径 +func shellEscape(arg string) string { + if arg == "" { + return "''" + } + + // 如果参数只包含安全字符,不需要转义 + if isSafeShellString(arg) { + return arg + } + + // 使用单引号转义,但需要处理参数中的单引号 + // 将单引号替换为 '"'"' + escaped := strings.ReplaceAll(arg, "'", `'"'"'`) + return "'" + escaped + "'" +} + +// shellEscapeArgs 转义多个shell参数 +func shellEscapeArgs(args []string) string { + if len(args) == 0 { + return "" + } + + var escaped []string + for _, arg := range args { + escaped = append(escaped, shellEscape(arg)) + } + + return strings.Join(escaped, " ") +} + +// isSafeShellString 检查字符串是否包含需要转义的字符 +func isSafeShellString(s string) bool { + // 只包含字母、数字、下划线、连字符、点号和斜杠的字符串是安全的 + for _, r := range s { + if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || r == '_' || r == '-' || + r == '.' || r == '/') { + return false + } + } + return len(s) > 0 +} diff --git a/internal/services/restart_linux.go b/internal/services/restart_linux.go new file mode 100644 index 0000000..6e98647 --- /dev/null +++ b/internal/services/restart_linux.go @@ -0,0 +1,112 @@ +//go:build linux + +package services + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" +) + +// restartApplication Linux平台的重启实现 +func (s *SelfUpdateService) restartApplication() error { + // 获取当前可执行文件路径 + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // 获取当前工作目录 + workDir, err := os.Getwd() + if err != nil { + s.logger.Error("Failed to get working directory", "error", err) + workDir = filepath.Dir(exe) // 如果获取失败,使用可执行文件所在目录 + } + + // 在Linux上,我们使用一个shell脚本来重启应用程序 + // 创建一个唯一的临时shell脚本 + scriptPath := fmt.Sprintf("/tmp/restart_voidraft_%d_%d.sh", os.Getpid(), time.Now().Unix()) + scriptContent := fmt.Sprintf(`#!/bin/bash +sleep 1 +cd %s +%s %s & +rm "%s" +`, + shellEscape(workDir), shellEscape(exe), + shellEscapeArgs(os.Args[1:]), scriptPath) + + s.logger.Info("Creating restart script", "path", scriptPath) + + // 写入脚本文件 + err = os.WriteFile(scriptPath, []byte(scriptContent), 0755) + if err != nil { + return fmt.Errorf("failed to create restart script: %w", err) + } + + // 启动脚本 + cmd := exec.Command("/bin/bash", scriptPath) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, // 创建新的会话,使进程独立于父进程 + } + + err = cmd.Start() + if err != nil { + return fmt.Errorf("failed to start restart script: %w", err) + } + + // 给脚本一点时间启动 + time.Sleep(100 * time.Millisecond) + + // 立即退出当前进程 + os.Exit(0) + + return nil // 不会执行到这里 +} + +// shellEscape 转义单个shell参数或路径 +func shellEscape(arg string) string { + if arg == "" { + return "''" + } + + // 如果参数只包含安全字符,不需要转义 + if isSafeShellString(arg) { + return arg + } + + // 使用单引号转义,但需要处理参数中的单引号 + // 将单引号替换为 '"'"' + escaped := strings.ReplaceAll(arg, "'", `'"'"'`) + return "'" + escaped + "'" +} + +// shellEscapeArgs 转义多个shell参数 +func shellEscapeArgs(args []string) string { + if len(args) == 0 { + return "" + } + + var escaped []string + for _, arg := range args { + escaped = append(escaped, shellEscape(arg)) + } + + return strings.Join(escaped, " ") +} + +// isSafeShellString 检查字符串是否包含需要转义的字符 +func isSafeShellString(s string) bool { + // 只包含字母、数字、下划线、连字符、点号和斜杠的字符串是安全的 + for _, r := range s { + if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || r == '_' || r == '-' || + r == '.' || r == '/') { + return false + } + } + return len(s) > 0 +} diff --git a/internal/services/restart_windows.go b/internal/services/restart_windows.go new file mode 100644 index 0000000..810a80b --- /dev/null +++ b/internal/services/restart_windows.go @@ -0,0 +1,118 @@ +//go:build windows + +package services + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" +) + +// restartApplication Windows平台的重启实现 +func (s *SelfUpdateService) restartApplication() error { + // 获取当前可执行文件路径 + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // 获取当前工作目录 + workDir, err := os.Getwd() + if err != nil { + s.logger.Error("Failed to get working directory", "error", err) + workDir = filepath.Dir(exe) // 如果获取失败,使用可执行文件所在目录 + } + + // 创建唯一的批处理文件来重启应用程序 + // 使用进程ID和时间戳确保文件名唯一性 + batchFile := filepath.Join(os.TempDir(), fmt.Sprintf("restart_voidraft_%d_%d.bat", os.Getpid(), time.Now().Unix())) + + // 正确转义命令行参数 + escapedArgs := escapeWindowsArgs(os.Args[1:]) + batchContent := fmt.Sprintf(`@echo off +timeout /t 1 /nobreak > NUL +cd /d "%s" +start "" "%s" %s +del "%s" +`, workDir, exe, escapedArgs, batchFile) + + s.logger.Info("Creating batch file", "path", batchFile, "content", batchContent) + + // 写入批处理文件 + err = os.WriteFile(batchFile, []byte(batchContent), 0644) + if err != nil { + return fmt.Errorf("failed to create batch file: %w", err) + } + + // 启动批处理文件 + cmd := exec.Command("cmd.exe", "/C", batchFile) + cmd.Stdout = nil + cmd.Stderr = nil + cmd.Stdin = nil + // 分离进程,这样即使父进程退出,批处理文件仍然会继续执行 + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } + + err = cmd.Start() + if err != nil { + return fmt.Errorf("failed to start batch file: %w", err) + } + + // 立即退出当前进程 + os.Exit(0) + + return nil // 不会执行到这里 +} + +// escapeWindowsArgs 转义Windows命令行参数 +func escapeWindowsArgs(args []string) string { + if len(args) == 0 { + return "" + } + + var escaped []string + for _, arg := range args { + escaped = append(escaped, escapeWindowsArg(arg)) + } + + return strings.Join(escaped, " ") +} + +// escapeWindowsArg 转义单个Windows命令行参数 +func escapeWindowsArg(arg string) string { + // 如果参数不包含空格、制表符、换行符、双引号或反斜杠,则不需要转义 + if !strings.ContainsAny(arg, " \t\n\r\"\\") { + return arg + } + + // 需要转义的参数用双引号包围 + var result strings.Builder + result.WriteByte('"') + + for i := 0; i < len(arg); i++ { + c := arg[i] + switch c { + case '"': + // 双引号需要转义 + result.WriteString(`\"`) + case '\\': + // 反斜杠需要特殊处理 + // 如果后面跟着双引号,需要转义反斜杠 + if i+1 < len(arg) && arg[i+1] == '"' { + result.WriteString(`\\`) + } else { + result.WriteByte(c) + } + default: + result.WriteByte(c) + } + } + + result.WriteByte('"') + return result.String() +} diff --git a/internal/services/self_update_service.go b/internal/services/self_update_service.go index 074c5c9..bc70551 100644 --- a/internal/services/self_update_service.go +++ b/internal/services/self_update_service.go @@ -7,11 +7,7 @@ import ( "github.com/creativeprojects/go-selfupdate" "github.com/wailsapp/wails/v3/pkg/services/log" "os" - "os/exec" - "path/filepath" "runtime" - "strings" - "syscall" "time" "voidraft/internal/models" ) @@ -428,69 +424,7 @@ func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType // RestartApplication 重启应用程序 func (s *SelfUpdateService) RestartApplication() error { - - // 获取当前可执行文件路径 - exe, err := os.Executable() - if err != nil { - return fmt.Errorf("failed to get executable path: %w", err) - } - - // Windows平台需要特殊处理 - if runtime.GOOS == "windows" { - - // 获取当前工作目录 - workDir, err := os.Getwd() - if err != nil { - s.logger.Error("Failed to get working directory", "error", err) - workDir = filepath.Dir(exe) // 如果获取失败,使用可执行文件所在目录 - } - - // 创建批处理文件来重启应用程序 - // 批处理文件会等待当前进程退出,然后启动新进程 - batchFile := filepath.Join(os.TempDir(), "restart_voidraft.bat") - batchContent := fmt.Sprintf(`@echo off -timeout /t 1 /nobreak > NUL -cd /d "%s" -start "" "%s" %s -del "%s" -`, workDir, exe, strings.Join(os.Args[1:], " "), batchFile) - - s.logger.Info("Creating batch file", "path", batchFile, "content", batchContent) - - // 写入批处理文件 - err = os.WriteFile(batchFile, []byte(batchContent), 0644) - if err != nil { - return fmt.Errorf("failed to create batch file: %w", err) - } - - // 启动批处理文件 - cmd := exec.Command("cmd.exe", "/C", batchFile) - cmd.Stdout = nil - cmd.Stderr = nil - cmd.Stdin = nil - // 分离进程,这样即使父进程退出,批处理文件仍然会继续执行 - cmd.SysProcAttr = &syscall.SysProcAttr{ - CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, - } - - err = cmd.Start() - if err != nil { - return fmt.Errorf("failed to start batch file: %w", err) - } - - // 立即退出当前进程 - os.Exit(0) - - return nil // 不会执行到这里 - } - - // 使用syscall.Exec替换当前进程 - err = syscall.Exec(exe, os.Args, os.Environ()) - if err != nil { - return fmt.Errorf("failed to exec: %w", err) - } - - return nil + return s.restartApplication() } // updateConfigVersion 更新配置中的版本号