Init
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
module gitee.com/snoware/swlib
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require github.com/rs/zerolog v1.35.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// 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 logger 基于 zerolog 的日志初始化,支持控制台输出(可读格式)+ 文件输出(JSON 结构化)。
|
||||||
|
// 日志目录优先级:exe 同目录 log/ → %APPDATA%/SRemoteConnectClient/log/ → 当前工作目录 log/ → 仅控制台输出。
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log 包级全局日志实例,供外部包使用。
|
||||||
|
var Log zerolog.Logger
|
||||||
|
|
||||||
|
// Init 初始化全局日志。
|
||||||
|
// 尝试在 exe 目录下创建 log/,若无写权限(多用户安装场景)则回退到 %APPDATA%。
|
||||||
|
// 日志按日期命名,控制台使用可读格式(带颜色),文件使用 JSON 结构化格式。
|
||||||
|
func Init() {
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stdout,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
NoColor: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
logFile := openLogFile()
|
||||||
|
if logFile == nil {
|
||||||
|
// 连 %APPDATA% 都写不了,仅控制台输出
|
||||||
|
Log = zerolog.New(consoleWriter).
|
||||||
|
With().Timestamp().Caller().Logger().
|
||||||
|
Level(zerolog.DebugLevel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
multi := io.MultiWriter(consoleWriter, logFile)
|
||||||
|
Log = zerolog.New(multi).
|
||||||
|
With().Timestamp().Caller().Logger().
|
||||||
|
Level(zerolog.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// openLogFile 按优先级尝试打开日志文件。
|
||||||
|
// 返回 nil 表示无法创建日志文件,此时程序仅输出到控制台。
|
||||||
|
func openLogFile() *os.File {
|
||||||
|
logDir, err := resolveLogDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
logPath := filepath.Join(logDir, today+".log")
|
||||||
|
|
||||||
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveLogDir 解析日志目录。
|
||||||
|
// 优先使用 exe 同目录下的 log/;若不可写则使用 %APPDATA%/SRemoteConnectClient/log/。
|
||||||
|
// [PORTBARR]
|
||||||
|
func resolveLogDir() (string, error) {
|
||||||
|
// 1. 尝试 exe 同目录
|
||||||
|
exePath, _ := os.Executable()
|
||||||
|
if exePath != "" {
|
||||||
|
dir := filepath.Join(filepath.Dir(exePath), "log")
|
||||||
|
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||||
|
// 验证确实可写
|
||||||
|
testFile := filepath.Join(dir, ".write_test")
|
||||||
|
if f, err := os.Create(testFile); err == nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(testFile)
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 回退到 %APPDATA%/SRemoteConnectClient/log
|
||||||
|
appData := os.Getenv("APPDATA")
|
||||||
|
if appData == "" {
|
||||||
|
appData = os.Getenv("LOCALAPPDATA")
|
||||||
|
}
|
||||||
|
if appData != "" {
|
||||||
|
dir := filepath.Join(appData, "SRemoteConnectClient", "log")
|
||||||
|
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 最后尝试当前工作目录
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
dir := filepath.Join(cwd, "log")
|
||||||
|
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", os.ErrPermission
|
||||||
|
}
|
||||||
@@ -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 模块:通过 WinAPI(GetConsoleWindow + 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("控制台窗口已显示")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user