新增RunAsAdmin方法
This commit is contained in:
@@ -160,3 +160,39 @@ func darwinCopyFile(src, dst string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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 <name> <args...>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,3 +171,39 @@ func linuxCopyFile(src, dst string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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 <name> <args...>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ type Platform interface {
|
|||||||
|
|
||||||
// InstallFont 安装字体
|
// InstallFont 安装字体
|
||||||
InstallFont(ctx context.Context, fontPath string) error
|
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 实现
|
// Detect 检测当前平台并返回对应的 Platform 实现
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/user/devpack/internal/logging"
|
"github.com/user/devpack/internal/logging"
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
@@ -192,3 +195,162 @@ func copyFile(src, dst string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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, " ")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user