package platform import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "syscall" "unsafe" "github.com/user/devpack/internal/logging" "golang.org/x/sys/windows/registry" ) // WindowsPlatform Windows 平台实现 type WindowsPlatform struct { homeDir string } // NewWindowsPlatform 创建 Windows 平台实例 func NewWindowsPlatform() *WindowsPlatform { return &WindowsPlatform{ homeDir: os.Getenv("USERPROFILE"), } } func (p *WindowsPlatform) OS() string { return "windows" } func (p *WindowsPlatform) Arch() string { return os.Getenv("PROCESSOR_ARCHITECTURE") } func (p *WindowsPlatform) HomeDir() string { return p.homeDir } func (p *WindowsPlatform) ConfigDir() string { if dir := os.Getenv("APPDATA"); dir != "" { return dir } return p.homeDir + "\\AppData\\Roaming" } func (p *WindowsPlatform) DataDir() string { if dir := os.Getenv("LOCALAPPDATA"); dir != "" { return dir } return p.homeDir + "\\AppData\\Local" } func (p *WindowsPlatform) GetEnvVar(key string) string { return os.Getenv(key) } func (p *WindowsPlatform) SetEnvVar(ctx context.Context, key, value string) error { cmd := exec.CommandContext(ctx, "setx", key, value) return cmd.Run() } func (p *WindowsPlatform) AddToPath(ctx context.Context, dir string) error { currentPath := os.Getenv("PATH") newPath := dir + ";" + currentPath return p.SetEnvVar(ctx, "PATH", newPath) } func (p *WindowsPlatform) IsAdmin() bool { // 检查 Windows 上是否以管理员身份运行 cmd := exec.Command("net", "session") err := cmd.Run() return err == nil } func (p *WindowsPlatform) PackageManagers() []string { var pms []string if _, err := exec.LookPath("scoop"); err == nil { pms = append(pms, "scoop") } if _, err := exec.LookPath("choco"); err == nil { pms = append(pms, "chocolatey") } if _, err := exec.LookPath("winget"); err == nil { pms = append(pms, "winget") } return pms } func (p *WindowsPlatform) DefaultShell() string { if _, err := exec.LookPath("pwsh"); err == nil { return "pwsh" // PowerShell 7+ 版本 } return "powershell" // Windows PowerShell 5.1 } // InstallFont 安装字体到用户级字体目录并注册到注册表 // 支持 .ttf (TrueType) 和 .otf (OpenType) 格式 func (p *WindowsPlatform) InstallFont(ctx context.Context, fontPath string) error { log := logging.G() // 验证源字体文件是否存在且可访问 info, err := os.Stat(fontPath) if err != nil { log.Errorf("无法访问字体文件: %v", err) return fmt.Errorf("无法访问字体文件: %w", err) } if info.IsDir() { return fmt.Errorf("字体路径是目录而非文件: %s", fontPath) } // 检查字体格式是否受支持 fontName := filepath.Base(fontPath) ext := filepath.Ext(fontName) var regSuffix string switch ext { case ".ttf": regSuffix = " (TrueType)" case ".otf": regSuffix = " (OpenType)" default: log.Errorf("不支持的字体格式: %s", ext) return fmt.Errorf("不支持的字体格式: %s,仅支持 .ttf 和 .otf", ext) } // 目标路径为用户级字体目录 // C:\Users\\AppData\Local\Microsoft\Windows\Fonts destDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "Windows", "Fonts") log.Debugf("正在安装字体: %s 到 %s", fontPath, destDir) // 确保目标目录存在 if err := os.MkdirAll(destDir, 0755); err != nil { return fmt.Errorf("创建字体目录失败: %w", err) } // 检查字体是否已安装 destPath := filepath.Join(destDir, fontName) if _, err := os.Stat(destPath); err == nil { log.Infof("字体已存在,跳过安装: %s", destPath) return nil } // 复制字体文件到目标路径 if err := copyFile(fontPath, destPath); err != nil { return fmt.Errorf("复制字体文件失败: %w", err) } // 写注册表以注册字体(用户级,无需管理员权限) key, _, err := registry.CreateKey( registry.CURRENT_USER, `Software\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.ALL_ACCESS, ) if err != nil { // 注册表写入失败时清理已复制的字体文件 os.Remove(destPath) return fmt.Errorf("打开字体注册表失败: %w", err) } defer key.Close() // 注册表键名:去掉扩展名的字体名 + 类型后缀 baseName := fontName[:len(fontName)-len(ext)] regName := baseName + regSuffix if err := key.SetStringValue(regName, destPath); err != nil { os.Remove(destPath) return fmt.Errorf("注册字体失败: %w", err) } log.Infof("字体安装成功: %s", fontName) return nil } // copyFile 复制文件,失败时自动清理目标文件 func copyFile(src, dst string) error { input, err := os.Open(src) if err != nil { return err } defer input.Close() output, err := os.Create(dst) if err != nil { return err } _, err = io.Copy(output, input) // 先关闭文件确保数据写入磁盘 closeErr := output.Close() if err != nil { os.Remove(dst) // 复制失败,清理残留文件 return err } if closeErr != nil { os.Remove(dst) return closeErr } 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, " ") }