This commit is contained in:
2026-06-24 03:16:40 +08:00
commit aea407612d
6 changed files with 375 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
// Copyright (c) 2026 S.A. (SNOWARE). Written for SCU Team.
// Licensed under MulanPubL-2.0.
// You may obtain a copy at: http://license.coscl.org.cn/MulanPubL-2.0
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND.
// console_windows 模块:通过 WinAPIGetConsoleWindow + ShowWindowAsync)控制控制台窗口的显示/隐藏。
package console
import (
"syscall"
"github.com/rs/zerolog/log"
)
// 预加载 WinAPI 所需的 DLL 和函数。
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
user32 = syscall.NewLazyDLL("user32.dll")
getConsoleWindow = kernel32.NewProc("GetConsoleWindow")
showWindowAsync = user32.NewProc("ShowWindowAsync")
)
// ShowWindow 常量。
const (
SW_HIDE = 0 // 隐藏窗口
SW_SHOW = 5 // 显示窗口
)
// cachedConsoleHwnd 在 initConsoleHwnd() 中缓存,避免 systray.Run() 后 GetConsoleWindow 返回 0。
var cachedConsoleHwnd uintptr
// initConsoleHwnd 在 main() 中提前调用,缓存控制台窗口句柄。
func initConsoleHwnd() {
hwnd, _, _ := getConsoleWindow.Call()
cachedConsoleHwnd = hwnd
}
// hideConsoleWindow 隐藏控制台窗口。若无控制台则无操作。
func hideConsoleWindow() {
if cachedConsoleHwnd != 0 {
showWindowAsync.Call(cachedConsoleHwnd, SW_HIDE)
log.Debug().Msg("控制台窗口已隐藏")
}
}
// showConsoleWindow 显示控制台窗口。若无控制台则无操作。
func showConsoleWindow() {
if cachedConsoleHwnd != 0 {
showWindowAsync.Call(cachedConsoleHwnd, SW_SHOW)
log.Debug().Msg("控制台窗口已显示")
}
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) 2026 S.A. (SNOWARE). Written for SCU Team.
// Licensed under MulanPubL-2.0.
// You may obtain a copy at: http://license.coscl.org.cn/MulanPubL-2.0
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND.
// Package winperm 提供 Windows UAC 管理员权限检测与提权。
package perm
import (
"os"
"syscall"
"unsafe"
"gitee.com/snoware/swlib/logger"
)
// Permission 权限管理器,封装管理员检测和提权操作。
type Permission struct{}
// 预加载 shell32.dll 中的 Windows API。
var (
shell32 = syscall.NewLazyDLL("shell32.dll")
isUserAnAdmin = shell32.NewProc("IsUserAnAdmin")
shellExecuteW = shell32.NewProc("ShellExecuteW")
)
// NewPermission 创建 Permission 实例。
func NewPermission() *Permission {
return &Permission{}
}
// HasUAC 调用 IsUserAnAdmin 检查当前进程是否以管理员权限运行。
// 返回 true 表示已拥有管理员权限。
func (p *Permission) HasUAC() bool {
ret, _, _ := isUserAnAdmin.Call()
hasAdmin := ret != 0
logger.Log.Debug().Bool("has_admin", hasAdmin).Msg("检查管理员权限")
return hasAdmin
}
// RunExeAsAdmin 通过 ShellExecuteW("runas") 以管理员身份启动指定的可执行文件。
//
// 参数 exe: 目标可执行文件的绝对路径。
// 成功时当前进程应退出,由新的管理员进程接管。
func (p *Permission) RunExeAsAdmin(exe string) error {
logger.Log.Info().Str("exe", exe).Msg("尝试以管理员身份运行")
verb := "runas"
cwd, _ := os.Getwd()
verbPtr, _ := syscall.UTF16PtrFromString(verb)
exePtr, _ := syscall.UTF16PtrFromString(exe)
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
var showCmd int32 = 1 // SW_NORMAL
ret, _, _ := shellExecuteW.Call(
0,
uintptr(unsafe.Pointer(verbPtr)),
uintptr(unsafe.Pointer(exePtr)),
0,
uintptr(unsafe.Pointer(cwdPtr)),
uintptr(showCmd),
)
if ret <= 32 {
err := syscall.GetLastError()
logger.Log.Error().Err(err).Int32("ret", int32(ret)).Msg("管理员提权失败")
return err
}
logger.Log.Info().Msg("管理员提权成功,当前进程将退出")
return nil
}
// GetSelf 获取当前可执行文件的绝对路径。
func (p *Permission) GetSelf() (string, error) {
exe, err := os.Executable()
if err != nil {
logger.Log.Error().Err(err).Msg("获取自身可执行文件路径失败")
return "", err
}
logger.Log.Debug().Str("exe_path", exe).Msg("获取自身路径")
return exe, nil
}
// RunSelfAsAdmin 以管理员身份重新执行自身。
// 返回当前可执行文件路径和可能的错误。
func (p *Permission) RunSelfAsAdmin() (string, error) {
exe, err := p.GetSelf()
if err != nil {
return "", err
}
err = p.RunExeAsAdmin(exe)
return exe, err
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright (c) 2026 S.A. (SNOWARE). Written for SCU Team.
// Licensed under MulanPubL-2.0.
// You may obtain a copy at: http://license.coscl.org.cn/MulanPubL-2.0
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND.
// trayicon_windows 模块:托盘图标加载,将 PNG 解码、缩放并包装为 Windows ICO 格式。
// getlantern/systray 在 Windows 上存在 GDI+ → CreateIconIndirect 兼容性 bug
// 解法是将 PNG 包装成 ICO 格式(现代 Windows 原生支持 ICO 内嵌 PNG)。
package main
import (
"bytes"
"encoding/binary"
"image"
"image/png"
"os"
"path/filepath"
"github.com/rs/zerolog/log"
)
// loadIcon 加载托盘图标。
// 优先从 image.png 文件读取,回退到内嵌 iconData。
// 解码 PNG → 缩放到 32x32 → 重新编码 → 包装为 ICO 格式返回。
func loadIcon(iconData []byte) []byte {
raw := iconData
exePath, err := os.Executable()
if err == nil {
iconFile := filepath.Join(filepath.Dir(exePath), "image.png")
if data, err := os.ReadFile(iconFile); err == nil {
raw = data
log.Debug().Str("file", iconFile).Msg("从文件读取图标")
}
}
img, err := png.Decode(bytes.NewReader(raw))
if err != nil {
log.Warn().Err(err).Msg("PNG 解码失败")
return nil
}
icon := resizeIcon(img, 32, 32)
var pngBuf bytes.Buffer
if err := png.Encode(&pngBuf, icon); err != nil {
log.Warn().Err(err).Msg("PNG 重新编码失败")
return nil
}
ico := pngToICO(pngBuf.Bytes(), 32, 32)
log.Debug().Int("size", len(ico)).Msg("托盘图标已生成 (ICO 32x32)")
return ico
}
// pngToICO 将 PNG 字节数据包装为 Windows ICO 文件格式。
// ICO 结构:6 字节文件头 + 16 字节目录项 + PNG 图像数据。
func pngToICO(pngData []byte, w, h int) []byte {
var buf bytes.Buffer
// ICO 文件头
binary.Write(&buf, binary.LittleEndian, uint16(0)) // 保留
binary.Write(&buf, binary.LittleEndian, uint16(1)) // 类型: ICO
binary.Write(&buf, binary.LittleEndian, uint16(1)) // 图标数量
// 处理 256 边界的宽度/高度
width := uint8(w)
if w >= 256 {
width = 0
}
height := uint8(h)
if h >= 256 {
height = 0
}
// ICO 目录项
binary.Write(&buf, binary.LittleEndian, width)
binary.Write(&buf, binary.LittleEndian, height)
binary.Write(&buf, binary.LittleEndian, uint8(0))
binary.Write(&buf, binary.LittleEndian, uint8(0))
binary.Write(&buf, binary.LittleEndian, uint16(1))
binary.Write(&buf, binary.LittleEndian, uint16(32))
binary.Write(&buf, binary.LittleEndian, uint32(len(pngData)))
binary.Write(&buf, binary.LittleEndian, uint32(22))
buf.Write(pngData)
return buf.Bytes()
}
// resizeIcon 使用最近邻插值将图像缩放到指定尺寸。
// 返回 *image.RGBA 避免与原图共享底层数据。
func resizeIcon(src image.Image, w, h int) *image.RGBA {
dst := image.NewRGBA(image.Rect(0, 0, w, h))
sb := src.Bounds()
for y := range h {
for x := range w {
sx := sb.Min.X + x*sb.Dx()/w
sy := sb.Min.Y + y*sb.Dy()/h
dst.Set(x, y, src.At(sx, sy))
}
}
return dst
}