From d567b7b80ab5c03c12d8e178eebab1afb67d9bcd Mon Sep 17 00:00:00 2001 From: zyj Date: Fri, 6 Mar 2026 14:29:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9ERunAsAdmin=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/platform/darwin.go | 36 ++++++++ internal/platform/linux.go | 36 ++++++++ internal/platform/platform.go | 5 ++ internal/platform/windows.go | 162 ++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+) diff --git a/internal/platform/darwin.go b/internal/platform/darwin.go index 1f094c6..dcc8d48 100644 --- a/internal/platform/darwin.go +++ b/internal/platform/darwin.go @@ -160,3 +160,39 @@ func darwinCopyFile(src, dst string) error { } return nil } + +// RunAsAdmin 以 root 权限运行命令 +// 在 macOS 上通过 sudo 请求 root 权限 +// 如果当前已具有 root 权限,则直接执行命令 +func (p *DarwinPlatform) RunAsAdmin(ctx context.Context, name string, args ...string) error { + log := logging.G() + + // 如果已经是 root 身份,直接执行 + if p.IsAdmin() { + log.Debugf("当前已具有 root 权限,直接执行: %s", name) + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + + log.Debugf("请求 sudo 权限运行: %s %v", name, args) + + // 构建 sudo 命令参数: sudo + sudoArgs := make([]string, 0, len(args)+1) + sudoArgs = append(sudoArgs, name) + sudoArgs = append(sudoArgs, args...) + + cmd := exec.CommandContext(ctx, "sudo", sudoArgs...) + cmd.Stdin = os.Stdin // 转发标准输入以便用户输入密码 + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("管理员命令执行失败: %w", err) + } + + log.Debugf("管理员命令执行完成: %s", name) + return nil +} diff --git a/internal/platform/linux.go b/internal/platform/linux.go index 002862b..c994564 100644 --- a/internal/platform/linux.go +++ b/internal/platform/linux.go @@ -171,3 +171,39 @@ func linuxCopyFile(src, dst string) error { } return nil } + +// RunAsAdmin 以 root 权限运行命令 +// 在 Linux 上通过 sudo 请求 root 权限 +// 如果当前已具有 root 权限,则直接执行命令 +func (p *LinuxPlatform) RunAsAdmin(ctx context.Context, name string, args ...string) error { + log := logging.G() + + // 如果已经是 root 身份,直接执行 + if p.IsAdmin() { + log.Debugf("当前已具有 root 权限,直接执行: %s", name) + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + + log.Debugf("请求 sudo 权限运行: %s %v", name, args) + + // 构建 sudo 命令参数: sudo + sudoArgs := make([]string, 0, len(args)+1) + sudoArgs = append(sudoArgs, name) + sudoArgs = append(sudoArgs, args...) + + cmd := exec.CommandContext(ctx, "sudo", sudoArgs...) + cmd.Stdin = os.Stdin // 转发标准输入以便用户输入密码 + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("管理员命令执行失败: %w", err) + } + + log.Debugf("管理员命令执行完成: %s", name) + return nil +} diff --git a/internal/platform/platform.go b/internal/platform/platform.go index fefb548..c2863bb 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -42,6 +42,11 @@ type Platform interface { // InstallFont 安装字体 InstallFont(ctx context.Context, fontPath string) error + + // RunAsAdmin 以管理员/root 权限运行命令 + // Windows 上通过 UAC 对话框提升权限 + // Linux/macOS 上通过 sudo 获取 root 权限 + RunAsAdmin(ctx context.Context, name string, args ...string) error } // Detect 检测当前平台并返回对应的 Platform 实现 diff --git a/internal/platform/windows.go b/internal/platform/windows.go index 6ab09a6..45c8e7e 100644 --- a/internal/platform/windows.go +++ b/internal/platform/windows.go @@ -7,6 +7,9 @@ import ( "os" "os/exec" "path/filepath" + "strings" + "syscall" + "unsafe" "github.com/user/devpack/internal/logging" "golang.org/x/sys/windows/registry" @@ -192,3 +195,162 @@ func copyFile(src, dst string) error { } return nil } + +// ---------- RunAsAdmin 相关定义 ---------- + +// shellExecuteInfoW 对应 Windows SHELLEXECUTEINFOW 结构体 +type shellExecuteInfoW struct { + cbSize uint32 + fMask uint32 + hwnd uintptr + lpVerb *uint16 + lpFile *uint16 + lpParameters *uint16 + lpDirectory *uint16 + nShow int32 + hInstApp uintptr + lpIDList uintptr + lpClass *uint16 + hkeyClass uintptr + dwHotKey uint32 + hIconOrMonitor uintptr + hProcess uintptr +} + +const ( + // SEE_MASK_NOCLOSEPROCESS 使 ShellExecuteEx 返回进程句柄 + _SEE_MASK_NOCLOSEPROCESS = 0x00000040 + // SW_HIDE 隐藏窗口,避免控制台窗口闪烁 + _SW_HIDE = 0 +) + +var ( + modShell32 = syscall.NewLazyDLL("shell32.dll") + modKernel32 = syscall.NewLazyDLL("kernel32.dll") + procShellExecuteExW = modShell32.NewProc("ShellExecuteExW") + procGetExitCodeProcess = modKernel32.NewProc("GetExitCodeProcess") +) + +// RunAsAdmin 以管理员权限运行命令 +// 在 Windows 上通过 UAC 对话框请求提升权限 +// 如果当前已具有管理员权限,则直接执行命令 +func (p *WindowsPlatform) RunAsAdmin(ctx context.Context, name string, args ...string) error { + log := logging.G() + + // 如果已经是管理员身份,直接执行 + if p.IsAdmin() { + log.Debugf("当前已具有管理员权限,直接执行: %s", name) + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + + // 解析可执行文件的完整路径,确保 ShellExecuteEx 能找到目标程序 + exePath, err := exec.LookPath(name) + if err != nil { + return fmt.Errorf("找不到可执行文件 %s: %w", name, err) + } + + log.Debugf("请求 UAC 管理员权限运行: %s %v", exePath, args) + + // 构建参数字符串,对包含空格的参数自动加引号 + paramStr := buildWindowsArgString(args) + + // 将字符串转换为 UTF-16 指针 + verbPtr, _ := syscall.UTF16PtrFromString("runas") + filePtr, err := syscall.UTF16PtrFromString(exePath) + if err != nil { + return fmt.Errorf("可执行文件路径包含非法字符: %w", err) + } + + // 构建 SHELLEXECUTEINFOW 结构体 + sei := &shellExecuteInfoW{ + fMask: _SEE_MASK_NOCLOSEPROCESS, + lpVerb: verbPtr, + lpFile: filePtr, + nShow: _SW_HIDE, + } + if paramStr != "" { + paramPtr, err := syscall.UTF16PtrFromString(paramStr) + if err != nil { + return fmt.Errorf("命令参数包含非法字符: %w", err) + } + sei.lpParameters = paramPtr + } + sei.cbSize = uint32(unsafe.Sizeof(*sei)) + + // 调用 ShellExecuteExW 触发 UAC 提权对话框 + ret, _, callErr := procShellExecuteExW.Call(uintptr(unsafe.Pointer(sei))) + if ret == 0 { + return fmt.Errorf("请求管理员权限失败 (用户可能拒绝了 UAC 提示): %w", callErr) + } + + // 等待提升权限的进程完成 + if sei.hProcess != 0 { + handle := syscall.Handle(sei.hProcess) + defer syscall.CloseHandle(handle) + + // 在 goroutine 中等待进程退出,以支持 context 取消 + type waitResult struct { + exitCode uint32 + err error + } + done := make(chan waitResult, 1) + + go func() { + event, e := syscall.WaitForSingleObject(handle, syscall.INFINITE) + if e != nil { + done <- waitResult{err: fmt.Errorf("等待进程完成失败: %w", e)} + return + } + if event != 0 { // WAIT_OBJECT_0 = 0 + done <- waitResult{err: fmt.Errorf("WaitForSingleObject 返回异常: 0x%X", event)} + return + } + + // 获取进程退出码 + var exitCode uint32 + r, _, e := procGetExitCodeProcess.Call( + uintptr(handle), + uintptr(unsafe.Pointer(&exitCode)), + ) + if r == 0 { + done <- waitResult{err: fmt.Errorf("获取进程退出码失败: %v", e)} + return + } + done <- waitResult{exitCode: exitCode} + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case result := <-done: + if result.err != nil { + return result.err + } + if result.exitCode != 0 { + return fmt.Errorf("管理员命令执行失败,退出码: %d", result.exitCode) + } + } + } + + log.Debugf("管理员命令执行完成: %s", name) + return nil +} + +// buildWindowsArgString 将参数列表构建为 Windows 命令行参数字符串 +// 对包含空格或特殊字符的参数自动添加双引号 +func buildWindowsArgString(args []string) string { + if len(args) == 0 { + return "" + } + quoted := make([]string, len(args)) + for i, arg := range args { + if arg == "" || strings.ContainsAny(arg, " \t\"") { + arg = `"` + strings.ReplaceAll(arg, `"`, `\"`) + `"` + } + quoted[i] = arg + } + return strings.Join(quoted, " ") +}