b1.0
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
./log
|
||||||
|
./*.exe
|
||||||
|
./go.sum
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// DoConnect 模块:负责 EasyTier 子进程的完整生命周期管理(启动、监控、输出捕获、清理)。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectToVirtualHotspot 构建 EasyTier 命令行,清理旧进程,启动新进程,并在 500ms 内检测进程是否存活。
|
||||||
|
// 返回子进程的 *os.Process 引用,失败返回 nil。
|
||||||
|
func connectToVirtualHotspot() *os.Process {
|
||||||
|
log.Info().Msg("连接虚拟热点开始")
|
||||||
|
|
||||||
|
host := "magpie.snoware.sa-tale.eu.cc"
|
||||||
|
port := "11010"
|
||||||
|
netname := "scu_team"
|
||||||
|
token := "@5cut0t"
|
||||||
|
|
||||||
|
miner := Miner{}
|
||||||
|
cmd := miner.BuildTaskCommand(
|
||||||
|
fmt.Sprintf("tcp://%s:%s", host, port),
|
||||||
|
netname,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("host", host).
|
||||||
|
Str("port", port).
|
||||||
|
Str("network", netname).
|
||||||
|
Msg("启动 EasyTier 进程")
|
||||||
|
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("获取自身可执行文件路径失败")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
workdir := filepath.Dir(exePath)
|
||||||
|
log.Debug().Str("work_dir", workdir).Msg("工作目录")
|
||||||
|
|
||||||
|
cleanupOldEasyTier()
|
||||||
|
|
||||||
|
parts := strings.Fields(cmd)
|
||||||
|
easyTierPath := filepath.Join(workdir, parts[0][2:])
|
||||||
|
if _, err := os.Stat(easyTierPath); os.IsNotExist(err) {
|
||||||
|
log.Error().Str("path", easyTierPath).Msg("easytier-core.exe 不存在,请确保该文件与 srcc.exe 在同一目录")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := exec.Command(easyTierPath, parts[1:]...)
|
||||||
|
c.Dir = workdir
|
||||||
|
|
||||||
|
var stderr, stdout bytes.Buffer
|
||||||
|
c.Stderr = &stderr
|
||||||
|
c.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("EasyTier 启动失败")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- c.Wait()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
log.Error().Err(err).Str("stdout", stdout.String()).Str("stderr", stderr.String()).Msg("EasyTier 进程启动后立即退出")
|
||||||
|
return nil
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Int("pid", c.Process.Pid).Msg("EasyTier 进程已启动")
|
||||||
|
return c.Process
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupOldEasyTier 通过 taskkill 强制终止所有旧 easytier-core.exe 进程,避免端口冲突。
|
||||||
|
func cleanupOldEasyTier() {
|
||||||
|
cmd := exec.Command("taskkill", "/f", "/im", "easytier-core.exe")
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Warn().Msg("检测到旧的 EasyTier 进程,已终止")
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// EasyTier 模块:定义 Miner 结构体及其命令行构建方法。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Miner 代表 EasyTier 连接矿工,作为命令行构建的命名空间(零内存占用空结构体)。
|
||||||
|
type Miner struct{}
|
||||||
|
|
||||||
|
// BuildTaskCommand 根据目标节点、网络名、访问令牌构建 easytier-core.exe 的完整命令行字符串。
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - wholenode: 完整节点 URI,如 tcp://host:port
|
||||||
|
// - netname: 网络名称
|
||||||
|
// - accesstoken: 网络密钥
|
||||||
|
//
|
||||||
|
// 示例:./easytier-core.exe --network-name mynet --network-secret key --peers tcp://host:11010 -d
|
||||||
|
func (*Miner) BuildTaskCommand(wholenode string, netname string, accesstoken string) string {
|
||||||
|
cmd := fmt.Sprintf(
|
||||||
|
"./easytier-core.exe --network-name %s --network-secret %s --peers %s -d",
|
||||||
|
netname, accesstoken, wholenode,
|
||||||
|
)
|
||||||
|
log.Info().
|
||||||
|
Str("node", wholenode).
|
||||||
|
Str("network", netname).
|
||||||
|
Msg("构建 EasyTier 连接命令")
|
||||||
|
log.Debug().Str("command", cmd).Msg("EasyTier 完整命令")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Perm 模块:Windows UAC 管理员权限检测与提权。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
log.Debug().Bool("has_admin", hasAdmin).Msg("检查管理员权限")
|
||||||
|
return hasAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// runExeAsAdmin 通过 ShellExecuteW("runas") 以管理员身份启动指定的可执行文件。
|
||||||
|
//
|
||||||
|
// 参数 exe: 目标可执行文件的绝对路径。
|
||||||
|
// 成功时当前进程应退出,由新的管理员进程接管。
|
||||||
|
func (p *Permission) runExeAsAdmin(exe string) error {
|
||||||
|
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()
|
||||||
|
log.Error().Err(err).Int32("ret", int32(ret)).Msg("管理员提权失败")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("管理员提权成功,当前进程将退出")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSelf 获取当前可执行文件的绝对路径。
|
||||||
|
func (p *Permission) getSelf() (string, error) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("获取自身可执行文件路径失败")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
log.Debug().Str("exe_path", exe).Msg("获取自身路径")
|
||||||
|
return exe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runSelfAsAdmin 以管理员身份重新执行自身 (srcc.exe)。
|
||||||
|
// 返回当前可执行文件路径和可能的错误。
|
||||||
|
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,43 @@
|
|||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 预加载 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 // 显示窗口
|
||||||
|
)
|
||||||
|
|
||||||
|
// hideConsoleWindow 获取当前控制台句柄并隐藏窗口。若无控制台则无操作。
|
||||||
|
func hideConsoleWindow() {
|
||||||
|
hwnd, _, _ := getConsoleWindow.Call()
|
||||||
|
if hwnd != 0 {
|
||||||
|
showWindowAsync.Call(hwnd, SW_HIDE)
|
||||||
|
log.Debug().Msg("控制台窗口已隐藏")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showConsoleWindow 获取当前控制台句柄并显示窗口。若无控制台则无操作。
|
||||||
|
func showConsoleWindow() {
|
||||||
|
hwnd, _, _ := getConsoleWindow.Call()
|
||||||
|
if hwnd != 0 {
|
||||||
|
showWindowAsync.Call(hwnd, SW_SHOW)
|
||||||
|
log.Debug().Msg("控制台窗口已显示")
|
||||||
|
}
|
||||||
|
}
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 将PNG转换为systray可用的 []byte 格式
|
||||||
|
# 支持输出: Go byte slice 或 ICO 格式
|
||||||
|
|
||||||
|
require 'base64'
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
|
options = {
|
||||||
|
size: 32,
|
||||||
|
output_format: 'go', # 'go' | 'ico' | 'png'
|
||||||
|
input: 'image.png',
|
||||||
|
output: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "用法: ruby convert_icon.rb [选项]"
|
||||||
|
opts.on("-s", "--size SIZE", Integer, "图标尺寸 (默认: 32)") { |s| options[:size] = s }
|
||||||
|
opts.on("-f", "--format FORMAT", ["go", "ico", "png"], "输出格式: go/ico/png (默认: go)") { |f| options[:output_format] = f }
|
||||||
|
opts.on("-i", "--input FILE", "输入PNG文件路径 (默认: image.png)") { |i| options[:input] = i }
|
||||||
|
opts.on("-o", "--output FILE", "输出文件路径") { |o| options[:output] = o }
|
||||||
|
opts.on("-h", "--help", "显示帮助") { puts opts; exit }
|
||||||
|
end.parse!
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
def check_mini_magick
|
||||||
|
begin
|
||||||
|
require 'mini_magick'
|
||||||
|
true
|
||||||
|
rescue LoadError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_chunky_png
|
||||||
|
begin
|
||||||
|
require 'chunky_png'
|
||||||
|
true
|
||||||
|
rescue LoadError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 使用mini_magick (ImageMagick) 调整图片大小
|
||||||
|
def resize_with_magick(input_path, output_path, size)
|
||||||
|
image = MiniMagick::Image.open(input_path)
|
||||||
|
image.resize "#{size}x#{size}"
|
||||||
|
image.write(output_path)
|
||||||
|
output_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# 使用chunky_png (纯Ruby) 调整图片大小 - 简单最近邻插值
|
||||||
|
def resize_with_chunky_png(input_path, output_path, size)
|
||||||
|
require 'chunky_png'
|
||||||
|
|
||||||
|
source = ChunkyPNG::Image.from_file(input_path)
|
||||||
|
|
||||||
|
# 计算缩放比例
|
||||||
|
scale_x = source.width.to_f / size
|
||||||
|
scale_y = source.height.to_f / size
|
||||||
|
|
||||||
|
# 创建新图片
|
||||||
|
resized = ChunkyPNG::Image.new(size, size)
|
||||||
|
|
||||||
|
size.times do |y|
|
||||||
|
size.times do |x|
|
||||||
|
src_x = (x * scale_x).to_i
|
||||||
|
src_y = (y * scale_y).to_i
|
||||||
|
src_x = [src_x, source.width - 1].min
|
||||||
|
src_y = [src_y, source.height - 1].min
|
||||||
|
resized[x, y] = source[src_x, src_y]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resized.save(output_path)
|
||||||
|
output_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# 将图片转换为ICO格式 (多分辨率)
|
||||||
|
def convert_to_ico(input_path, output_path, sizes = [16, 32, 48, 64])
|
||||||
|
require 'mini_magick'
|
||||||
|
|
||||||
|
images = []
|
||||||
|
sizes.each do |size|
|
||||||
|
img = MiniMagick::Image.open(input_path)
|
||||||
|
img.resize "#{size}x#{size}"
|
||||||
|
img.format 'png'
|
||||||
|
temp_file = "temp_icon_#{size}.png"
|
||||||
|
img.write(temp_file)
|
||||||
|
images << temp_file
|
||||||
|
end
|
||||||
|
|
||||||
|
# 使用ImageMagick合并为ICO
|
||||||
|
MiniMagick::Tool::Convert.new do |convert|
|
||||||
|
images.each { |img| convert << img }
|
||||||
|
convert << output_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
images.each { |f| File.delete(f) if File.exist?(f) }
|
||||||
|
output_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# 生成Go []byte 格式
|
||||||
|
def generate_go_bytes(file_path, var_name = "iconData")
|
||||||
|
bytes = File.binread(file_path).bytes
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines << "var #{var_name} = []byte{"
|
||||||
|
|
||||||
|
bytes.each_slice(12) do |slice|
|
||||||
|
hex_values = slice.map { |b| "0x%02X" % b }
|
||||||
|
lines << " " + hex_values.join(", ") + ","
|
||||||
|
end
|
||||||
|
|
||||||
|
lines << "}"
|
||||||
|
lines.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
# 生成 base64 内嵌格式 (另一种Go方案)
|
||||||
|
def generate_go_base64(file_path, var_name = "iconData")
|
||||||
|
data = File.binread(file_path)
|
||||||
|
base64_str = Base64.strict_encode64(data)
|
||||||
|
|
||||||
|
<<~GO
|
||||||
|
var #{var_name}Base64 = `#{base64_str}`
|
||||||
|
|
||||||
|
// 使用时解码:
|
||||||
|
// import "encoding/base64"
|
||||||
|
// iconData, _ := base64.StdEncoding.DecodeString(#{var_name}Base64)
|
||||||
|
GO
|
||||||
|
end
|
||||||
|
|
||||||
|
# 主逻辑
|
||||||
|
puts "转换 systray 图标..."
|
||||||
|
puts "输入文件: #{options[:input]}"
|
||||||
|
puts "目标尺寸: #{options[:size]}x#{options[:size]}"
|
||||||
|
|
||||||
|
unless File.exist?(options[:input])
|
||||||
|
puts "错误: 找不到输入文件 #{options[:input]}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# 选择处理方式
|
||||||
|
if check_mini_magick
|
||||||
|
puts "使用 ImageMagick (mini_magick)..."
|
||||||
|
elsif check_chunky_png
|
||||||
|
puts "使用纯 Ruby (chunky_png)..."
|
||||||
|
else
|
||||||
|
puts "需要安装依赖. 请选择以下方式之一:"
|
||||||
|
puts " 1. gem install mini_magick (推荐, 需要安装ImageMagick)"
|
||||||
|
puts " 2. gem install chunky_png (纯Ruby, 无需ImageMagick)"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
if options[:output_format] == 'ico'
|
||||||
|
# 生成ICO文件
|
||||||
|
output = options[:output] || "icon.ico"
|
||||||
|
if check_mini_magick
|
||||||
|
convert_to_ico(options[:input], output)
|
||||||
|
else
|
||||||
|
puts "生成ICO需要ImageMagick, 请先安装: gem install mini_magick"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
puts "已生成ICO文件: #{output}"
|
||||||
|
|
||||||
|
elsif options[:output_format] == 'png'
|
||||||
|
# 生成PNG文件
|
||||||
|
output = options[:output] || "icon_#{options[:size]}.png"
|
||||||
|
if check_mini_magick
|
||||||
|
resize_with_magick(options[:input], output, options[:size])
|
||||||
|
else
|
||||||
|
resize_with_chunky_png(options[:input], output, options[:size])
|
||||||
|
end
|
||||||
|
puts "已生成PNG文件: #{output}"
|
||||||
|
|
||||||
|
else # go
|
||||||
|
# 生成Go代码
|
||||||
|
temp_file = "temp_icon_#{options[:size]}.png"
|
||||||
|
|
||||||
|
if check_mini_magick
|
||||||
|
resize_with_magick(options[:input], temp_file, options[:size])
|
||||||
|
else
|
||||||
|
resize_with_chunky_png(options[:input], temp_file, options[:size])
|
||||||
|
end
|
||||||
|
|
||||||
|
go_code = generate_go_bytes(temp_file, "iconData")
|
||||||
|
|
||||||
|
if options[:output]
|
||||||
|
File.write(options[:output], go_code)
|
||||||
|
puts "已生成Go代码文件: #{options[:output]}"
|
||||||
|
else
|
||||||
|
puts "\n=== 复制以下代码到你的Go文件 ===\n"
|
||||||
|
puts go_code
|
||||||
|
puts "\n=================================\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.delete(temp_file) if File.exist?(temp_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "完成!"
|
||||||
|
|
||||||
|
rescue => e
|
||||||
|
puts "错误: #{e.message}"
|
||||||
|
puts e.backtrace.first(5).join("\n")
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
# 管理员权限执行指令
|
||||||
|
|
||||||
|
> 适用于 Windows 平台,检查是否以管理员身份运行及提权执行命令
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 检测当前是否以管理员运行
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/user"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||||
|
isUserAnAdmin = shell32.NewProc("IsUserAnAdmin")
|
||||||
|
)
|
||||||
|
|
||||||
|
func isAdmin() bool {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
// Linux/macOS: 检测 UID 是否为 0
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return u.Uid == "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ := isUserAnAdmin.Call()
|
||||||
|
return ret != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if isAdmin() {
|
||||||
|
fmt.Println("当前以管理员身份运行")
|
||||||
|
} else {
|
||||||
|
fmt.Println("当前以普通用户身份运行")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 以管理员身份重新启动自身
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runAsAdmin() error {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return fmt.Errorf("此功能仅支持 Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, _ := os.Executable()
|
||||||
|
verb := "runas"
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
|
verbPtr, _ := syscall.UTF16PtrFromString(verb)
|
||||||
|
exePtr, _ := syscall.UTF16PtrFromString(exe)
|
||||||
|
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
|
||||||
|
|
||||||
|
var showCmd int32 = 1 // SW_NORMAL
|
||||||
|
|
||||||
|
return syscall.ShellExecute(0, verbPtr, exePtr, nil, cwdPtr, showCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用方式:检测到非管理员时提权重启
|
||||||
|
func ensureAdmin() {
|
||||||
|
if !isAdmin() {
|
||||||
|
fmt.Println("需要管理员权限,正在提权...")
|
||||||
|
runAsAdmin()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意:** `runAsAdmin` 会弹出 UAC 弹窗,用户需点击"是"确认。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 以管理员身份执行任意程序
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runAsAdminExe(targetExe string, args string) error {
|
||||||
|
verbPtr, _ := syscall.UTF16PtrFromString("runas")
|
||||||
|
exePtr, _ := syscall.UTF16PtrFromString(targetExe)
|
||||||
|
// args 传入时是 lpParameters,填充启动参数
|
||||||
|
paramsPtr, _ := syscall.UTF16PtrFromString(args)
|
||||||
|
dirPtr, _ := syscall.UTF16PtrFromString("")
|
||||||
|
|
||||||
|
var showCmd int32 = 1
|
||||||
|
return syscall.ShellExecute(0, verbPtr, exePtr, paramsPtr, dirPtr, showCmd)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 使用 PowerShell 管理员执行命令
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runAsAdminPSCommand(command string) *exec.Cmd {
|
||||||
|
// 组装 PowerShell 命令,-Verb RunAs 触发 UAC 提权
|
||||||
|
psCmd := fmt.Sprintf(
|
||||||
|
`Start-Process PowerShell -Verb RunAs -ArgumentList '-NoProfile -Command "%s"'`,
|
||||||
|
strings.ReplaceAll(command, `"`, `\"`),
|
||||||
|
)
|
||||||
|
return exec.Command("powershell", "-Command", psCmd)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**缺点:** `Start-Process` 会开启新窗口,无法直接获取输出。如需获取输出,见下方方式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 管理员执行命令 + 获取输出
|
||||||
|
|
||||||
|
### 方式 A:不走 PowerShell,纯 ShellExecuteEx + cmd.exe
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runAsAdminCmd(command string) (string, error) {
|
||||||
|
// 输出重定向到临时文件
|
||||||
|
tmpFile := filepath.Join(os.TempDir(),
|
||||||
|
fmt.Sprintf("cmdout_%d.txt", os.Getpid()))
|
||||||
|
|
||||||
|
// 拼命令: cmd /c "your_command > tmpFile 2>&1"
|
||||||
|
fullCmd := fmt.Sprintf(`%s > "%s" 2>&1`, command, tmpFile)
|
||||||
|
cmdLine, _ := syscall.UTF16PtrFromString(`/c ` + fullCmd)
|
||||||
|
|
||||||
|
var op uint32
|
||||||
|
shell32 := syscall.NewLazyDLL("shell32.dll")
|
||||||
|
procShellExecuteEx := shell32.NewProc("ShellExecuteExW")
|
||||||
|
|
||||||
|
// SEE_MASK_NOCLOSEPROCESS = 0x40 — 获取进程句柄以便等待
|
||||||
|
type SHELLEXECUTEINFO struct {
|
||||||
|
cbSize uint32
|
||||||
|
fMask uint32
|
||||||
|
hwnd syscall.Handle
|
||||||
|
lpVerb *uint16
|
||||||
|
lpFile *uint16
|
||||||
|
lpParameters *uint16
|
||||||
|
lpDirectory *uint16
|
||||||
|
nShow int32
|
||||||
|
hInstApp uintptr
|
||||||
|
lpIDList uintptr
|
||||||
|
lpClass *uint16
|
||||||
|
hkeyClass syscall.Handle
|
||||||
|
dwHotKey uint32
|
||||||
|
hIcon syscall.Handle
|
||||||
|
hProcess syscall.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
verb, _ := syscall.UTF16PtrFromString("runas")
|
||||||
|
cmdexe, _ := syscall.UTF16PtrFromString("cmd.exe")
|
||||||
|
|
||||||
|
info := SHELLEXECUTEINFO{
|
||||||
|
cbSize: uint32(unsafe.Sizeof(SHELLEXECUTEINFO{})),
|
||||||
|
fMask: 0x40, // SEE_MASK_NOCLOSEPROCESS
|
||||||
|
lpVerb: verb,
|
||||||
|
lpFile: cmdexe,
|
||||||
|
lpParameters: cmdLine,
|
||||||
|
nShow: 0, // SW_HIDE 隐藏窗口
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(&info)))
|
||||||
|
if ret == 0 {
|
||||||
|
return "", fmt.Errorf("ShellExecuteEx 失败")
|
||||||
|
}
|
||||||
|
defer syscall.CloseHandle(info.hProcess)
|
||||||
|
|
||||||
|
// 等待进程结束
|
||||||
|
syscall.WaitForSingleObject(info.hProcess, syscall.INFINITE)
|
||||||
|
|
||||||
|
// 读取输出文件
|
||||||
|
data, err := os.ReadFile(tmpFile)
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点:**
|
||||||
|
- `SEE_MASK_NOCLOSEPROCESS (0x40)` — 获取进程句柄
|
||||||
|
- `WaitForSingleObject` — 等待提权后的 cmd.exe 执行完毕
|
||||||
|
- `SW_HIDE (0)` — 隐藏 cmd 黑窗口
|
||||||
|
- `> tmpFile 2>&1` — stdout 和 stderr 都重定向到临时文件
|
||||||
|
- 无需 PowerShell,纯 Win32 API
|
||||||
|
|
||||||
|
### 方式 B:PowerShell 方式(兼容性好,但依赖 PS)
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runAsAdminWithPS(command string) (string, error) {
|
||||||
|
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("cmdout_%d.txt", os.Getpid()))
|
||||||
|
|
||||||
|
psCmd := fmt.Sprintf(
|
||||||
|
`Start-Process cmd -Verb RunAs -Wait -WindowStyle Hidden -RedirectStandardOutput "%s" -ArgumentList '/c %s'`,
|
||||||
|
tmpFile, command,
|
||||||
|
)
|
||||||
|
|
||||||
|
exec.Command("powershell", "-Command", psCmd).Run()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(tmpFile)
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 完整示例:Fyne GUI 管理员指令执行器
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
var isAdmin bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
shell32 := syscall.NewLazyDLL("shell32.dll")
|
||||||
|
proc := shell32.NewProc("IsUserAnAdmin")
|
||||||
|
ret, _, _ := proc.Call()
|
||||||
|
isAdmin = ret != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAdminCmd(command string) (string, error) {
|
||||||
|
tmpFile := filepath.Join(os.TempDir(),
|
||||||
|
fmt.Sprintf("srm_admin_out_%d.txt", os.Getpid()))
|
||||||
|
|
||||||
|
fullCmd := fmt.Sprintf(`%s > "%s" 2>&1`, command, tmpFile)
|
||||||
|
cmdLine, _ := syscall.UTF16PtrFromString(`/c ` + fullCmd)
|
||||||
|
|
||||||
|
type SHELLEXECUTEINFO struct {
|
||||||
|
cbSize, fMask uint32
|
||||||
|
hwnd syscall.Handle
|
||||||
|
lpVerb, lpFile *uint16
|
||||||
|
lpParameters *uint16
|
||||||
|
lpDirectory, _ *uint16
|
||||||
|
nShow int32
|
||||||
|
_, _, _, _ uintptr
|
||||||
|
hProcess syscall.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
verb, _ := syscall.UTF16PtrFromString("runas")
|
||||||
|
cmdexe, _ := syscall.UTF16PtrFromString("cmd.exe")
|
||||||
|
|
||||||
|
info := SHELLEXECUTEINFO{
|
||||||
|
cbSize: uint32(unsafe.Sizeof(SHELLEXECUTEINFO{})),
|
||||||
|
fMask: 0x40, // SEE_MASK_NOCLOSEPROCESS
|
||||||
|
lpVerb: verb,
|
||||||
|
lpFile: cmdexe,
|
||||||
|
lpParameters: cmdLine,
|
||||||
|
nShow: 0, // SW_HIDE
|
||||||
|
}
|
||||||
|
|
||||||
|
proc := syscall.NewLazyDLL("shell32.dll").NewProc("ShellExecuteExW")
|
||||||
|
ret, _, _ := proc.Call(uintptr(unsafe.Pointer(&info)))
|
||||||
|
if ret == 0 {
|
||||||
|
return "", fmt.Errorf("ShellExecuteEx 失败")
|
||||||
|
}
|
||||||
|
defer syscall.CloseHandle(info.hProcess)
|
||||||
|
|
||||||
|
syscall.WaitForSingleObject(info.hProcess, syscall.INFINITE)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(tmpFile)
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
myApp := app.New()
|
||||||
|
myWindow := myApp.NewWindow("管理员指令执行器")
|
||||||
|
myWindow.Resize(fyne.NewSize(600, 400))
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
cmdEntry := widget.NewEntry()
|
||||||
|
cmdEntry.SetPlaceHolder("输入命令,如: ipconfig /flushdns")
|
||||||
|
|
||||||
|
statusLabel := widget.NewLabel("当前状态: 用户")
|
||||||
|
if isAdmin {
|
||||||
|
statusLabel.SetText("当前状态: 管理员 ✓")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := widget.NewMultiLineEntry()
|
||||||
|
output.Disable()
|
||||||
|
|
||||||
|
progress := widget.NewProgressBarInfinite()
|
||||||
|
progress.Hide()
|
||||||
|
|
||||||
|
runBtn := widget.NewButton("以管理员身份执行", func() {
|
||||||
|
command := cmdEntry.Text
|
||||||
|
if command == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
mu.Lock()
|
||||||
|
progress.Show()
|
||||||
|
runBtn.Disable()
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
out, err := runAdminCmd(command)
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
progress.Hide()
|
||||||
|
runBtn.Enable()
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
output.SetText("错误: " + err.Error())
|
||||||
|
} else {
|
||||||
|
output.SetText(out)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
content := container.NewVBox(
|
||||||
|
statusLabel,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
cmdEntry,
|
||||||
|
runBtn,
|
||||||
|
progress,
|
||||||
|
widget.NewLabel("输出:"),
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
|
||||||
|
myWindow.SetContent(content)
|
||||||
|
myWindow.ShowAndRun()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 方法对比
|
||||||
|
|
||||||
|
| 方式 | 优点 | 缺点 |
|
||||||
|
|------|------|------|
|
||||||
|
| `ShellExecute("runas")` | 原生 API,稳定 | 只能启动 exe,无法获取输出 |
|
||||||
|
| PowerShell `Start-Process -Verb RunAs -RedirectStandardOutput` | 可获取输出 | 需临时文件,略慢 |
|
||||||
|
| 直接加 `go.mod` 嵌入 Manifest | 程序启动时即提权 | 始终以管理员运行,不够灵活 |
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# 命令执行日志获取
|
||||||
|
|
||||||
|
> 实时捕获命令输出,写入日志文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 基础日志捕获
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runAndLog(command string) string {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
cmd := exec.Command("cmd", "/c", command) // Windows
|
||||||
|
// cmd := exec.Command("sh", "-c", command) // Linux
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
result := fmt.Sprintf("[命令] %s\n", command)
|
||||||
|
result += fmt.Sprintf("[stdout]\n%s\n", stdout.String())
|
||||||
|
if stderr.Len() > 0 {
|
||||||
|
result += fmt.Sprintf("[stderr]\n%s\n", stderr.String())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
result += fmt.Sprintf("[错误] %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 实时逐行日志 + 写入文件
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runWithLiveLog(command string, logPath string) error {
|
||||||
|
cmd := exec.Command("cmd", "/c", command)
|
||||||
|
|
||||||
|
stdoutPipe, _ := cmd.StdoutPipe()
|
||||||
|
stderrPipe, _ := cmd.StderrPipe()
|
||||||
|
|
||||||
|
logFile, err := os.Create(logPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
|
||||||
|
writer := io.MultiWriter(os.Stdout, logFile) // 同时输出到终端和文件
|
||||||
|
|
||||||
|
cmd.Start()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
scanner := bufio.NewScanner(stdoutPipe)
|
||||||
|
for scanner.Scan() {
|
||||||
|
writer.Write([]byte("[OUT] " + scanner.Text() + "\n"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
scanner := bufio.NewScanner(stderrPipe)
|
||||||
|
for scanner.Scan() {
|
||||||
|
writer.Write([]byte("[ERR] " + scanner.Text() + "\n"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 结构化日志
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CommandLog struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Stdout string `json:"stdout"`
|
||||||
|
Stderr string `json:"stderr"`
|
||||||
|
ExitCode int `json:"exit_code"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStructuredLog(command string) *CommandLog {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd := exec.Command("cmd", "/c", command)
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
exitCode := 0
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
exitCode = exitErr.ExitCode()
|
||||||
|
} else {
|
||||||
|
exitCode = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommandLog{
|
||||||
|
Timestamp: start.Format(time.RFC3339),
|
||||||
|
Command: command,
|
||||||
|
Stdout: stdout.String(),
|
||||||
|
Stderr: stderr.String(),
|
||||||
|
ExitCode: exitCode,
|
||||||
|
Duration: elapsed.String(),
|
||||||
|
Success: err == nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加写入 JSON 日志文件
|
||||||
|
func appendLogJSON(logPath string, entry *CommandLog) error {
|
||||||
|
data, _ := json.Marshal(entry)
|
||||||
|
data = append(data, '\n')
|
||||||
|
|
||||||
|
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出示例 (`commands.log`):
|
||||||
|
```
|
||||||
|
{"timestamp":"2026-06-22T05:30:00+08:00","command":"ipconfig","stdout":"...","stderr":"","exit_code":0,"duration":"1.2s","success":true}
|
||||||
|
{"timestamp":"2026-06-22T05:31:15+08:00","command":"dir X:\\","stdout":"","stderr":"系统找不到指定的路径。\n","exit_code":1,"duration":"0.3s","success":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 带时间戳的日志文件
|
||||||
|
|
||||||
|
```go
|
||||||
|
func createLogFile(dir string) (*os.File, error) {
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
name := fmt.Sprintf("cmd_%s.log", time.Now().Format("20060102_150405"))
|
||||||
|
return os.Create(filepath.Join(dir, name))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 滚动日志(限制大小)
|
||||||
|
|
||||||
|
```go
|
||||||
|
const maxLogSize = 1 * 1024 * 1024 // 1MB
|
||||||
|
|
||||||
|
func appendRollingLog(logPath string, line string) error {
|
||||||
|
info, err := os.Stat(logPath)
|
||||||
|
if err == nil && info.Size() > maxLogSize {
|
||||||
|
// 超过 1MB 自动归档
|
||||||
|
backup := logPath + "." + time.Now().Format("20060102_150405")
|
||||||
|
os.Rename(logPath, backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.WriteString(line + "\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 超时 + 日志
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runWithTimeoutLog(command string, timeout time.Duration, logPath string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "cmd", "/c", command)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
entry := fmt.Sprintf("[%s] 命令: %s | 超时设置: %v | 成功: %v | stdout: %s | stderr: %s",
|
||||||
|
time.Now().Format(time.RFC3339),
|
||||||
|
command, timeout, err == nil,
|
||||||
|
stdout.String(), stderr.String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
entry += " | 结果: 超时"
|
||||||
|
}
|
||||||
|
|
||||||
|
appendRollingLog(logPath, entry)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Fyne GUI 日志查看器
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
myApp := app.New()
|
||||||
|
myWindow := myApp.NewWindow("命令日志查看器")
|
||||||
|
myWindow.Resize(fyne.NewSize(700, 500))
|
||||||
|
|
||||||
|
logPath := filepath.Join(os.TempDir(), "cmd_history.log")
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
cmdEntry := widget.NewEntry()
|
||||||
|
cmdEntry.SetPlaceHolder("输入命令...")
|
||||||
|
|
||||||
|
logView := widget.NewMultiLineEntry()
|
||||||
|
logView.Disable()
|
||||||
|
|
||||||
|
// 清除按钮
|
||||||
|
widget.NewButton("清除日志", func() {
|
||||||
|
logView.SetText("")
|
||||||
|
os.Remove(logPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行按钮
|
||||||
|
widget.NewButton("执行并记录日志", func() {
|
||||||
|
command := cmdEntry.Text
|
||||||
|
if command == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
entry := runAndLog(command)
|
||||||
|
appendRollingLog(logPath, entry)
|
||||||
|
|
||||||
|
current := logView.Text
|
||||||
|
logView.SetText(current + entry + "\n---\n")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载历史日志
|
||||||
|
widget.NewButton("加载历史日志", func() {
|
||||||
|
data, err := os.ReadFile(logPath)
|
||||||
|
if err == nil {
|
||||||
|
logView.SetText(string(data))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
myWindow.SetContent(container.NewBorder(
|
||||||
|
container.NewVBox(cmdEntry,
|
||||||
|
container.NewHBox(/* 按钮们 */),
|
||||||
|
),
|
||||||
|
nil, nil, nil, logView,
|
||||||
|
))
|
||||||
|
myWindow.ShowAndRun()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 关键类型速查
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 一次性获取全部输出
|
||||||
|
cmd.Output() // stdout
|
||||||
|
cmd.CombinedOutput() // stdout + stderr
|
||||||
|
|
||||||
|
// 手动分离获取
|
||||||
|
cmd.Stdout = &stdout // stdout 写入 buffer
|
||||||
|
cmd.Stderr = &stderr // stderr 写入 buffer
|
||||||
|
|
||||||
|
// 实时流式获取
|
||||||
|
cmd.StdoutPipe() // 返回 io.ReadCloser,需在 cmd.Start() 前调用
|
||||||
|
cmd.StderrPipe() // 同上
|
||||||
|
|
||||||
|
// 带超时
|
||||||
|
exec.CommandContext(ctx, name, args...)
|
||||||
|
|
||||||
|
// 获取退出码
|
||||||
|
exitErr, ok := err.(*exec.ExitError)
|
||||||
|
code := exitErr.ExitCode()
|
||||||
|
```
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
# Fyne + Go 命令执行与输出捕获教程
|
||||||
|
|
||||||
|
## 1. Fyne 快速入门
|
||||||
|
|
||||||
|
### 安装 Fyne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get fyne.io/fyne/v2@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 最小 Fyne 窗口
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
myApp := app.New()
|
||||||
|
myWindow := myApp.NewWindow("Hello Fyne")
|
||||||
|
|
||||||
|
myWindow.SetContent(widget.NewLabel("Hello, World!"))
|
||||||
|
myWindow.Resize(fyne.NewSize(400, 300))
|
||||||
|
myWindow.ShowAndRun()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用控件
|
||||||
|
|
||||||
|
| 控件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `widget.NewLabel(text)` | 文本标签 |
|
||||||
|
| `widget.NewButton(text, callback)` | 按钮 |
|
||||||
|
| `widget.NewEntry()` | 单行输入框 |
|
||||||
|
| `widget.NewMultiLineEntry()` | 多行输入框 |
|
||||||
|
| `widget.NewCheck(text, callback)` | 复选框 |
|
||||||
|
| `widget.NewSelect(options, callback)` | 下拉选择 |
|
||||||
|
| `widget.NewProgressBar()` | 进度条 |
|
||||||
|
|
||||||
|
### 布局容器
|
||||||
|
|
||||||
|
| 容器 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `container.NewVBox(...)` | 垂直排列 |
|
||||||
|
| `container.NewHBox(...)` | 水平排列 |
|
||||||
|
| `container.NewBorder(top, bottom, left, right, center)` | 边框布局 |
|
||||||
|
| `container.NewGridWithColumns(cols, ...)` | 网格布局 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Go 执行外部命令并获取输出
|
||||||
|
|
||||||
|
### 核心包: `os/exec`
|
||||||
|
|
||||||
|
Go 标准库 `os/exec` 包提供了执行外部命令的功能。
|
||||||
|
|
||||||
|
### 2.1 基础方法
|
||||||
|
|
||||||
|
#### 获取命令输出 (一次性)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// cmd := exec.Command("cmd", "/c", "dir") // Windows
|
||||||
|
cmd := exec.Command("ls", "-l") // Linux/macOS
|
||||||
|
|
||||||
|
output, err := cmd.Output() // 返回 []byte
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("执行出错:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(string(output))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 合并 stdout + stderr
|
||||||
|
|
||||||
|
```go
|
||||||
|
output, err := cmd.CombinedOutput() // stdout + stderr 合并
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 分别获取 stdout 和 stderr
|
||||||
|
|
||||||
|
```go
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
fmt.Println("stdout:", stdout.String())
|
||||||
|
fmt.Println("stderr:", stderr.String())
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 实时读取输出 (逐行)
|
||||||
|
|
||||||
|
适用于长时间运行的命令或需要实时显示进度的场景。
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runCommandWithLiveOutput(name string, args ...string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发读取 stdout
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fmt.Println("[OUT]", scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 并发读取 stderr
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fmt.Println("[ERR]", scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 设置超时
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, name, args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return "", fmt.Errorf("命令执行超时 (%v)", timeout)
|
||||||
|
}
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 设置工作目录和环境变量
|
||||||
|
|
||||||
|
```go
|
||||||
|
cmd := exec.Command("make", "build")
|
||||||
|
cmd.Dir = "/path/to/project" // 设置工作目录
|
||||||
|
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") // 追加环境变量
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Windows 特殊处理
|
||||||
|
|
||||||
|
Windows 下 `cmd` 命令需要 `/c` 前缀:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Windows
|
||||||
|
cmd := exec.Command("cmd", "/c", "dir")
|
||||||
|
// 或者使用 PowerShell
|
||||||
|
cmd := exec.Command("powershell", "-Command", "Get-ChildItem")
|
||||||
|
```
|
||||||
|
|
||||||
|
通用跨平台封装:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func shellCommand(command string) *exec.Cmd {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return exec.Command("cmd", "/c", command)
|
||||||
|
}
|
||||||
|
return exec.Command("sh", "-c", command)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fyne + 命令执行完整示例
|
||||||
|
|
||||||
|
### 3.1 简单的命令执行 GUI
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
myApp := app.New()
|
||||||
|
myWindow := myApp.NewWindow("命令执行器")
|
||||||
|
myWindow.Resize(fyne.NewSize(600, 400))
|
||||||
|
|
||||||
|
cmdEntry := widget.NewEntry()
|
||||||
|
cmdEntry.SetPlaceHolder("输入命令,如: ping 127.0.0.1")
|
||||||
|
|
||||||
|
outputLabel := widget.NewMultiLineEntry()
|
||||||
|
outputLabel.Disable() // 只读
|
||||||
|
outputLabel.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
runBtn := widget.NewButton("执行", func() {
|
||||||
|
command := cmdEntry.Text
|
||||||
|
if command == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd = exec.Command("cmd", "/c", command)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("sh", "-c", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
var errOut bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &errOut
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
outputLabel.SetText("错误: " + err.Error() + "\n" + errOut.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outputLabel.SetText(out.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
content := container.NewVBox(
|
||||||
|
widget.NewLabel("输入要执行的命令:"),
|
||||||
|
cmdEntry,
|
||||||
|
runBtn,
|
||||||
|
widget.NewLabel("输出:"),
|
||||||
|
outputLabel,
|
||||||
|
)
|
||||||
|
|
||||||
|
myWindow.SetContent(content)
|
||||||
|
myWindow.ShowAndRun()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 带实时输出的进阶版本
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
myApp := app.New()
|
||||||
|
myWindow := myApp.NewWindow("实时命令输出")
|
||||||
|
myWindow.Resize(fyne.NewSize(700, 500))
|
||||||
|
|
||||||
|
cmdEntry := widget.NewEntry()
|
||||||
|
cmdEntry.SetPlaceHolder("输入命令...")
|
||||||
|
|
||||||
|
output := widget.NewMultiLineEntry()
|
||||||
|
output.Disable()
|
||||||
|
output.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
appendOutput := func(text string) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
current := output.Text
|
||||||
|
if len(current) > 0 {
|
||||||
|
current += "\n"
|
||||||
|
}
|
||||||
|
output.SetText(current + text)
|
||||||
|
}
|
||||||
|
|
||||||
|
runBtn := widget.NewButton("执行", func() {
|
||||||
|
command := cmdEntry.Text
|
||||||
|
if command == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd = exec.Command("cmd", "/c", command)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("sh", "-c", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, _ := cmd.StdoutPipe()
|
||||||
|
stderr, _ := cmd.StderrPipe()
|
||||||
|
|
||||||
|
cmd.Start()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
appendOutput(scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
appendOutput("[ERR] " + scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cmd.Wait()
|
||||||
|
})
|
||||||
|
|
||||||
|
clearBtn := widget.NewButton("清空", func() {
|
||||||
|
output.SetText("")
|
||||||
|
})
|
||||||
|
|
||||||
|
content := container.NewBorder(
|
||||||
|
container.NewVBox(
|
||||||
|
widget.NewLabelWithStyle("命令执行器 (实时输出)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
cmdEntry,
|
||||||
|
container.NewHBox(runBtn, clearBtn),
|
||||||
|
),
|
||||||
|
nil, nil, nil,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
|
||||||
|
myWindow.SetContent(content)
|
||||||
|
myWindow.ShowAndRun()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 带进度条的长时间任务
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
myApp := app.New()
|
||||||
|
myWindow := myApp.NewWindow("带超时的命令执行")
|
||||||
|
myWindow.Resize(fyne.NewSize(500, 300))
|
||||||
|
|
||||||
|
progress := widget.NewProgressBar()
|
||||||
|
statusLabel := widget.NewLabel("就绪")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "ping", "-n", "5", "127.0.0.1") // Windows
|
||||||
|
// cmd := exec.CommandContext(ctx, "ping", "-c", "5", "127.0.0.1") // Linux/macOS
|
||||||
|
|
||||||
|
// 模拟进度更新
|
||||||
|
go func() {
|
||||||
|
for i := 0; i <= 100; i++ {
|
||||||
|
progress.SetValue(float64(i) / 100.0)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
statusLabel.SetText("执行超时!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText("错误: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statusLabel.SetText("完成! 输出: " + string(output))
|
||||||
|
}()
|
||||||
|
|
||||||
|
content := container.NewVBox(
|
||||||
|
widget.NewLabel("任务进度:"),
|
||||||
|
progress,
|
||||||
|
statusLabel,
|
||||||
|
)
|
||||||
|
|
||||||
|
myWindow.SetContent(content)
|
||||||
|
myWindow.ShowAndRun()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `os/exec` 关键方法对比
|
||||||
|
|
||||||
|
| 方法 | 用途 | 特点 |
|
||||||
|
|------|------|------|
|
||||||
|
| `cmd.Run()` | 执行命令并等待完成 | stdout/stderr 需手动设置 |
|
||||||
|
| `cmd.Output()` | 执行并返回 stdout | stderr 为空时出错 |
|
||||||
|
| `cmd.CombinedOutput()` | 执行并返回 stdout+stderr | stderr 合并到输出 |
|
||||||
|
| `cmd.Start()` + `cmd.Wait()` | 手动控制启停 | 可配合 Pipe 实现实时输出 |
|
||||||
|
| `cmd.StdoutPipe()` | 获取 stdout 管道 | 需在 Start 前调用 |
|
||||||
|
| `cmd.StderrPipe()` | 获取 stderr 管道 | 需在 Start 前调用 |
|
||||||
|
| `exec.CommandContext()` | 带 context 的命令 | 支持超时/取消 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 常见问题
|
||||||
|
|
||||||
|
### Q: `cmd.Output()` 报 "exit status 1"
|
||||||
|
**A:** 命令执行失败且 stderr 有内容时,`Output()` 会直接报错。改用 `CombinedOutput()` 或手动设置 `cmd.Stdout` / `cmd.Stderr`。
|
||||||
|
|
||||||
|
### Q: 如何执行 `cd` 命令?
|
||||||
|
**A:** `cd` 是 shell 内置命令,不能用 `exec.Command` 直接执行。应使用 `cmd.Dir` 设置工作目录。
|
||||||
|
|
||||||
|
### Q: Fyne 编译后文件太大?
|
||||||
|
**A:** 使用 `go build -ldflags="-s -w"` 压缩,或使用 `fyne package` 打包。
|
||||||
|
|
||||||
|
### Q: Fyne 不支持 Windows 7?
|
||||||
|
**A:** Fyne v2.4+ 需要 Windows 10 1809+。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 参考资料
|
||||||
|
|
||||||
|
- [Fyne 官方文档](https://developer.fyne.io/)
|
||||||
|
- [Go os/exec 文档](https://pkg.go.dev/os/exec)
|
||||||
|
- [Fyne GitHub](https://github.com/fyne-io/fyne)
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
# Gitee OpenAPI v5 参考文档
|
||||||
|
|
||||||
|
> 在线 Swagger: https://gitee.com/api/v5/swagger
|
||||||
|
> Base URL: `https://gitee.com/api/v5`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 认证方式
|
||||||
|
|
||||||
|
### 1.1 OAuth 2.0
|
||||||
|
|
||||||
|
**授权地址:** `https://gitee.com/oauth/authorize`
|
||||||
|
**Token 地址:** `https://gitee.com/oauth/token`
|
||||||
|
|
||||||
|
支持 grant_type:
|
||||||
|
- `authorization_code` — 授权码模式
|
||||||
|
- `password` — 密码模式
|
||||||
|
- `client_credentials` — 客户端模式
|
||||||
|
- `refresh_token` — 刷新令牌
|
||||||
|
|
||||||
|
### 1.2 请求认证
|
||||||
|
|
||||||
|
在 Header 中携带:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
或 Query 参数:
|
||||||
|
|
||||||
|
```
|
||||||
|
?access_token=<access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 仓库 (Repos)
|
||||||
|
|
||||||
|
### 2.1 仓库 CRUD
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}` | 获取仓库详情 |
|
||||||
|
| `POST` | `/user/repos` | 创建仓库 |
|
||||||
|
| `PATCH` | `/repos/{owner}/{repo}` | 更新仓库 |
|
||||||
|
| `DELETE` | `/repos/{owner}/{repo}` | 删除仓库 |
|
||||||
|
| `GET` | `/user/repos` | 当前用户仓库列表 |
|
||||||
|
| `GET` | `/users/{username}/repos` | 指定用户仓库列表 |
|
||||||
|
| `GET` | `/orgs/{org}/repos` | 组织仓库列表 |
|
||||||
|
|
||||||
|
#### 创建仓库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://gitee.com/api/v5/user/repos" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "my-project",
|
||||||
|
"description": "项目描述",
|
||||||
|
"private": false,
|
||||||
|
"auto_init": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 搜索仓库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://gitee.com/api/v5/search/repositories?q=springboot&order=desc&page=1&per_page=20"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 分支管理
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/branches` | 分支列表 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/branches/{branch}` | 分支详情 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/branches` | 创建分支 |
|
||||||
|
| `DELETE` | `/repos/{owner}/{repo}/branches/{branch}` | 删除分支 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建分支
|
||||||
|
curl -X POST "https://gitee.com/api/v5/repos/owner/repo/branches" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{
|
||||||
|
"branch_name": "feature-xxx",
|
||||||
|
"refs": "master"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 标签 (Tags)
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/tags` | 标签列表 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/tags` | 创建标签 |
|
||||||
|
| `DELETE` | `/repos/{owner}/{repo}/tags/{tag_name}` | 删除标签 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 提交 (Commits)
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/commits` | 提交列表 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/commits/{sha}` | 单个提交 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/commits/{sha}/diff` | 提交差异 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/compare/{base}...{head}` | 比较两个提交 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 文件内容
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/contents/{path}` | 获取文件/目录 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/contents/{path}` | 新建文件 |
|
||||||
|
| `PUT` | `/repos/{owner}/{repo}/contents/{path}` | 更新文件 |
|
||||||
|
| `DELETE` | `/repos/{owner}/{repo}/contents/{path}` | 删除文件 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 新建文件
|
||||||
|
curl -X POST "https://gitee.com/api/v5/repos/owner/repo/contents/src/main.go" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{
|
||||||
|
"content": "cGFja2FnZSBtYWluCgpmdW5jIG1haW4oKSB7Cn0=",
|
||||||
|
"message": "add main.go",
|
||||||
|
"branch": "master"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 Fork
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/forks` | Fork 列表 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/forks` | Fork 仓库 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 仓库贡献者/协作者
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/collaborators` | 协作者列表 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/collaborators/{username}` | 是否协作者 |
|
||||||
|
| `PUT` | `/repos/{owner}/{repo}/collaborators/{username}` | 添加/更新协作者 |
|
||||||
|
| `DELETE` | `/repos/{owner}/{repo}/collaborators/{username}` | 移除协作者 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/contributors` | 贡献者列表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 Star / Watch
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/stargazers` | Star 用户列表 |
|
||||||
|
| `PUT` | `/user/starred/{owner}/{repo}` | Star 仓库 |
|
||||||
|
| `DELETE` | `/user/starred/{owner}/{repo}` | 取消 Star |
|
||||||
|
| `GET` | `/user/starred` | 我 Star 的仓库 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/subscribers` | 订阅者列表 |
|
||||||
|
| `PUT` | `/user/subscriptions/{owner}/{repo}` | 订阅仓库 |
|
||||||
|
| `DELETE` | `/user/subscriptions/{owner}/{repo}` | 取消订阅 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Pull Request
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/pulls` | PR 列表 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/pulls` | 创建 PR |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/pulls/{number}` | PR 详情 |
|
||||||
|
| `PATCH` | `/repos/{owner}/{repo}/pulls/{number}` | 更新 PR |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/pulls/{number}/commits` | PR 提交列表 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/pulls/{number}/files` | PR 文件变更 |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/pulls/{number}/merge` | 检查是否可合并 |
|
||||||
|
| `PUT` | `/repos/{owner}/{repo}/pulls/{number}/merge` | 合并 PR |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建 PR
|
||||||
|
curl -X POST "https://gitee.com/api/v5/repos/owner/repo/pulls" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{
|
||||||
|
"title": "新功能",
|
||||||
|
"head": "feature-branch",
|
||||||
|
"base": "master",
|
||||||
|
"body": "PR 描述信息"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Issues
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/issues` | Issue 列表 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/issues` | 创建 Issue |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/issues/{number}` | Issue 详情 |
|
||||||
|
| `PATCH` | `/repos/{owner}/{repo}/issues/{number}` | 更新 Issue |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/issues/{number}/comments` | 评论列表 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/issues/{number}/comments` | 创建评论 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建 Issue
|
||||||
|
curl -X POST "https://gitee.com/api/v5/repos/owner/repo/issues" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{
|
||||||
|
"title": "Bug 报告",
|
||||||
|
"body": "问题描述...",
|
||||||
|
"labels": "bug,high-priority"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 用户 (Users)
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/user` | 当前用户信息 |
|
||||||
|
| `PATCH` | `/user` | 更新个人信息 |
|
||||||
|
| `GET` | `/users/{username}` | 指定用户信息 |
|
||||||
|
| `GET` | `/user/followers` | 我的粉丝列表 |
|
||||||
|
| `GET` | `/user/following` | 我关注的用户 |
|
||||||
|
| `PUT` | `/user/following/{username}` | 关注用户 |
|
||||||
|
| `DELETE` | `/user/following/{username}` | 取消关注 |
|
||||||
|
| `GET` | `/user/keys` | SSH 公钥列表 |
|
||||||
|
| `POST` | `/user/keys` | 添加 SSH 公钥 |
|
||||||
|
| `DELETE` | `/user/keys/{id}` | 删除 SSH 公钥 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 组织 (Organizations)
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/user/orgs` | 我的组织列表 |
|
||||||
|
| `GET` | `/users/{username}/orgs` | 用户组织列表 |
|
||||||
|
| `GET` | `/orgs/{org}` | 组织详情 |
|
||||||
|
| `GET` | `/orgs/{org}/members` | 成员列表 |
|
||||||
|
| `DELETE` | `/orgs/{org}/members/{username}` | 移除成员 |
|
||||||
|
| `GET` | `/orgs/{org}/repos` | 组织仓库 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Webhooks
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/hooks` | Hook 列表 |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/hooks` | 创建 Hook |
|
||||||
|
| `GET` | `/repos/{owner}/{repo}/hooks/{id}` | Hook 详情 |
|
||||||
|
| `PATCH` | `/repos/{owner}/{repo}/hooks/{id}` | 更新 Hook |
|
||||||
|
| `DELETE` | `/repos/{owner}/{repo}/hooks/{id}` | 删除 Hook |
|
||||||
|
| `POST` | `/repos/{owner}/{repo}/hooks/{id}/tests` | 测试 Hook |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建 Webhook
|
||||||
|
curl -X POST "https://gitee.com/api/v5/repos/owner/repo/hooks" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{
|
||||||
|
"url": "https://your-server.com/webhook",
|
||||||
|
"password": "optional_secret",
|
||||||
|
"push_events": true,
|
||||||
|
"tag_push_events": true,
|
||||||
|
"issues_events": true,
|
||||||
|
"merge_requests_events": true,
|
||||||
|
"note_events": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Go SDK 调用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GiteeAPIBase = "https://gitee.com/api/v5"
|
||||||
|
AccessToken = "your_access_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
func giteeRequest(method, path string, body map[string]interface{}) ([]byte, error) {
|
||||||
|
url := GiteeAPIBase + path
|
||||||
|
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, _ := json.Marshal(body)
|
||||||
|
reqBody = bytes.NewBuffer(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(method, url, reqBody)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+AccessToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 获取当前用户信息
|
||||||
|
data, _ := giteeRequest("GET", "/user", nil)
|
||||||
|
fmt.Println(string(data))
|
||||||
|
|
||||||
|
// 创建 Issue
|
||||||
|
data, _ = giteeRequest("POST", "/repos/owner/repo/issues", map[string]interface{}{
|
||||||
|
"title": "新 Issue",
|
||||||
|
"body": "通过 API 创建",
|
||||||
|
})
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 仓库参考
|
||||||
|
|
||||||
|
| 资源 | 地址 |
|
||||||
|
|------|------|
|
||||||
|
| Swagger UI | https://gitee.com/api/v5/swagger |
|
||||||
|
| OAuth 文档 | https://gitee.com/api/v5/oauth_doc |
|
||||||
|
| 帮助中心 | https://gitee.com/help |
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Go 学习路线图
|
||||||
|
|
||||||
|
> 从本项目 `SRemoteConnectClient` 中提取,按文件+主题整理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、Go 语言基础
|
||||||
|
|
||||||
|
| 主题 | 出现位置 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `package main` + `main()` | main.go:15 | 程序入口,可执行程序必须的 |
|
||||||
|
| `import` 包管理 | main.go:4-8 | 引入标准库和第三方库,依赖在 go.mod/go.sum |
|
||||||
|
| `var` 全局变量 | main.go:12, logger.go:14 | 包级别可访问变量 |
|
||||||
|
| `var` 分组声明 | Perm.go:13-18, console_windows.go:8-14 | 一次性声明多个相关变量 |
|
||||||
|
| `const` 常量 | console_windows.go:16-19 | 编译期确定的不可变值 |
|
||||||
|
| `struct` 结构体 | Perm.go:10, EasyTier.go:7 | 自定义数据类型 |
|
||||||
|
| 零值初始化 `Xxx{}` | EasyTier.go:7, DoConnect.go:22 | 空结构体作命名空间 |
|
||||||
|
| 构造函数 `NewXxx()` | Perm.go:20 | Go 惯用的工厂模式 |
|
||||||
|
| 方法 (接收者) | Perm.go:23, EasyTier.go:12 | `func (p *Type) Method()` |
|
||||||
|
| 指针接收者 vs 值接收者 | EasyTier.go:12 | `*Type` 可修改原值 |
|
||||||
|
| 条件分支 `if-else` | main.go:23-27 | 流程控制 |
|
||||||
|
| `return` 提前退出 | main.go:26 | 提前返回函数 |
|
||||||
|
| `for range` 循环 (Go 1.22+) | trayicon_windows.go:88-89 | `for y := range h` 新语法 |
|
||||||
|
| `defer` 延迟执行 | main.go:34 | 函数返回前自动执行,用于清理资源 |
|
||||||
|
| 错误处理 `if err != nil` | main.go:97 | Go 的错误处理模式 |
|
||||||
|
| `panic` 不可恢复错误 | logger.go:26 | 程序终止 |
|
||||||
|
| `make` / `new` 分配内存 | trayicon_windows.go:86 | make 用于 slice/map/chan |
|
||||||
|
|
||||||
|
### 指针
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `*os.Process` 指针类型 | DoConnect.go:14 | 作为返回值 |
|
||||||
|
| `&` 取地址 | DoConnect.go:63 | 获取变量地址 |
|
||||||
|
| `unsafe.Pointer` | Perm.go:44-48 | 通用指针,用于 WinAPI |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、并发编程 (goroutine + channel)
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `go func() {}` | main.go:61 | 启动一个 goroutine (轻量级并发) |
|
||||||
|
| `<-channel` | main.go:62 | 从 channel 阻塞读取 |
|
||||||
|
| `make(chan error, 1)` | DoConnect.go:72 | 创建带缓冲的 channel |
|
||||||
|
| `select` 多路复用 | DoConnect.go:76 | 同时等待多个 channel |
|
||||||
|
| `time.After` 超时 | DoConnect.go:80 | 超时 channel |
|
||||||
|
| goroutine + channel + select | DoConnect.go:71-80 | 经典超时等待模式 |
|
||||||
|
| 回调函数 | main.go:38 | 函数作为参数传入 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、os/exec 子进程管理
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `exec.Command(name, args...)` | DoConnect.go:58, 92 | 创建命令 (名字+参数分开传) |
|
||||||
|
| `cmd.Dir` | DoConnect.go:59 | 设置子进程工作目录 |
|
||||||
|
| `cmd.Stdout/Stderr` | DoConnect.go:62-64 | 捕获子进程输出 |
|
||||||
|
| `cmd.Start()` | DoConnect.go:66 | 非阻塞启动 |
|
||||||
|
| `cmd.Wait()` | DoConnect.go:74 | 等待进程结束 |
|
||||||
|
| `cmd.Run()` | DoConnect.go:92 | 启动 + 等待一步完成 |
|
||||||
|
| `process.Pid` | DoConnect.go:84 | 获取进程 PID |
|
||||||
|
| `process.Kill()` | main.go:98 | 终止进程 |
|
||||||
|
| `strings.Fields` 拆分命令行 | DoConnect.go:49 | 字符串按空格分割 |
|
||||||
|
| `...` 切片展开 | DoConnect.go:58 | `parts[1:]...` 展开为参数 |
|
||||||
|
| `bytes.Buffer` 捕获输出 | DoConnect.go:5, 62 | 内存缓冲区 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、文件与路径操作
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `filepath.Join` | DoConnect.go:10 | 跨平台路径拼接 |
|
||||||
|
| `filepath.Dir` | DoConnect.go:40 | 获取目录部分 |
|
||||||
|
| `os.Executable()` | DoConnect.go:35 | 获取当前可执行文件路径 |
|
||||||
|
| `os.Getwd()` | logger.go:20 | 获取当前工作目录 |
|
||||||
|
| `os.Stat()` | DoConnect.go:52 | 检查文件是否存在 |
|
||||||
|
| `os.IsNotExist()` | DoConnect.go:52 | 判断文件不存在错误 |
|
||||||
|
| `os.ReadFile()` | trayicon_windows.go:22 | 一次性读取文件 |
|
||||||
|
| `os.OpenFile()` | logger.go:33 | 创建/追加模式打开文件 |
|
||||||
|
| `os.MkdirAll()` | logger.go:25 | 递归创建目录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、第三方库
|
||||||
|
|
||||||
|
### 5.1 systray (系统托盘)
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `go get` 安装 | main.go:8 | `github.com/getlantern/systray` |
|
||||||
|
| `systray.Run(onReady, onExit)` | main.go:38 | 启动托盘 (阻塞) |
|
||||||
|
| `systray.SetIcon(data)` | main.go:45 | 设置托盘图标 |
|
||||||
|
| `systray.SetTooltip(text)` | main.go:48 | 悬停提示文字 |
|
||||||
|
| `systray.AddMenuItem(name, tooltip)` | main.go:49-54 | 添加右键菜单项 |
|
||||||
|
| `systray.AddSeparator()` | main.go:52 | 菜单分隔线 |
|
||||||
|
| `item.Disable()` | main.go:51 | 禁用菜单项 |
|
||||||
|
| `item.ClickedCh` | main.go:62 | channel,点击时收到信号 |
|
||||||
|
| `systray.Quit()` | main.go:63 | 退出托盘 |
|
||||||
|
|
||||||
|
### 5.2 zerolog (日志)
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `github.com/rs/zerolog` | logger.go:11 | 高性能结构化日志 |
|
||||||
|
| `zerolog.ConsoleWriter` | logger.go:40 | 控制台可读输出 (带颜色) |
|
||||||
|
| `io.MultiWriter` | logger.go:46 | 同时写文件和屏幕 |
|
||||||
|
| 链式配置 | logger.go:49-51 | `New().With().Timestamp().Caller()` |
|
||||||
|
| 链式日志调用 | DoConnect.go:30-32 | `Info().Str(key, val).Msg(msg)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Windows API 调用
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `syscall.NewLazyDLL()` | Perm.go:15 | 延迟加载 DLL |
|
||||||
|
| `dll.NewProc()` | Perm.go:16-17 | 获取函数指针 |
|
||||||
|
| `proc.Call()` | Perm.go:24 | 调用 WinAPI (返回 `(ret, _, _)`) |
|
||||||
|
| `syscall.UTF16PtrFromString()` | Perm.go:36-38 | Go 字符串 → UTF-16 指针 |
|
||||||
|
| `unsafe.Pointer` | Perm.go:44-48 | 通用指针传递 |
|
||||||
|
| `syscall.GetLastError()` | Perm.go:52 | 获取 Windows 错误码 |
|
||||||
|
| `ShellExecuteW("runas")` | Perm.go:42-49 | UAC 提权 |
|
||||||
|
| `IsUserAnAdmin` | Perm.go:24 | 检查管理员权限 |
|
||||||
|
| `GetConsoleWindow` | console_windows.go:13 | 获取控制台句柄 |
|
||||||
|
| `ShowWindowAsync` | console_windows.go:14 | 显示/隐藏窗口 |
|
||||||
|
| `kernel32.dll` | console_windows.go:10 | 核心 WinAPI |
|
||||||
|
| `user32.dll` | console_windows.go:12 | 用户界面 API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、图像处理
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `image.Decode` / `png.Decode` | trayicon_windows.go:29 | PNG 字节解码为图像对象 |
|
||||||
|
| `png.Encode` | trayicon_windows.go:40 | 图像对象编码为 PNG 字节 |
|
||||||
|
| `image.NewRGBA` | trayicon_windows.go:86 | 创建画布 |
|
||||||
|
| `image.Rect` | trayicon_windows.go:86 | 定义矩形 |
|
||||||
|
| `src.Bounds()` | trayicon_windows.go:87 | 获取图像尺寸 |
|
||||||
|
| `dst.Set(x, y, color)` | trayicon_windows.go:92 | 写像素 |
|
||||||
|
| `src.At(x, y)` | trayicon_windows.go:92 | 读像素 |
|
||||||
|
| `bytes.NewReader` | trayicon_windows.go:29 | 字节 → io.Reader |
|
||||||
|
| `bytes.Buffer` | trayicon_windows.go:39 | 内存缓冲区 (io.Writer) |
|
||||||
|
| `encoding/binary` | trayicon_windows.go:5 | 二进制数据处理 |
|
||||||
|
| ICO 格式包装 | trayicon_windows.go:52-82 | PNG 包 ICO 头以兼容 Windows |
|
||||||
|
| `//go:embed` (推荐) | icon_bytes.go:3 | Go 1.16+ 内嵌资源指令 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、其他标准库
|
||||||
|
|
||||||
|
| 主题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `fmt.Sprintf` | EasyTier.go:13 | 格式化字符串 `%s` 占位符 |
|
||||||
|
| `time.Now()` | logger.go:30 | 当前时间 |
|
||||||
|
| `time.Format()` | logger.go:30 | 格式化时间 ("2006-01-02") |
|
||||||
|
| `time.After` | DoConnect.go:80 | 超时 channel |
|
||||||
|
| `os` 包 | 多个文件 | 操作系统接口 |
|
||||||
|
| `strings.Fields` | DoConnect.go:49 | 按空白字符分割 |
|
||||||
|
| `io.MultiWriter` | logger.go:46 | 写多个目标 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、建议学习顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
第1周: Go 基础 (package, import, var, func, struct, if, for, return)
|
||||||
|
第2周: 错误处理, defer, 指针 (&, *, 接收者方法)
|
||||||
|
第3周: 文件操作 (os, filepath), 字符串处理 (fmt, strings)
|
||||||
|
第4周: goroutine + channel + select (并发核心)
|
||||||
|
第5周: os/exec 子进程管理
|
||||||
|
第6周: syscall + WinAPI 调用
|
||||||
|
第7周: 第三方库 (systray, zerolog)
|
||||||
|
第8周: 图像处理 (image/png, binary)
|
||||||
|
```
|
||||||
@@ -0,0 +1,943 @@
|
|||||||
|
# WinAPI 进程启动方案
|
||||||
|
|
||||||
|
> 基于 Windows API 的进程创建方案,覆盖完整生命周期(创建、监控、终止),适用于 Go 项目。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 现状分析
|
||||||
|
|
||||||
|
当前项目中进程启动使用以下方式:
|
||||||
|
|
||||||
|
| 位置 | 方式 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `DoConnect.go` | `exec.Command("cmd", "/c", cmd)` | 启动 EasyTier 核心进程 |
|
||||||
|
| `main.go` | `exec.Command("cmd", "/c", "start", url)` | 打开外部网页 |
|
||||||
|
| `Perm.go` | `ShellExecuteW` + `"runas"` | 管理员提权重启自身 |
|
||||||
|
|
||||||
|
**现有方案的不足:**
|
||||||
|
|
||||||
|
- 依赖 `cmd.exe` 中间层,会短暂闪现控制台窗口
|
||||||
|
- 无法精确控制子进程的窗口状态(如隐藏窗口)
|
||||||
|
- 进程终止仅通过 `Kill()`,无法优雅退出
|
||||||
|
- 无法实时捕获子进程 stdout/stderr
|
||||||
|
- 无法将子进程纳入 Job Object 统一管理生命周期
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. WinAPI 进程启动方案总览
|
||||||
|
|
||||||
|
| 方案 | 适用场景 | 控制粒度 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| **CreateProcess** | 启动任意可执行文件,完全控制 | ★★★★★ |
|
||||||
|
| **CreateProcessAsUser** | 以指定用户身份启动进程 | ★★★★★ |
|
||||||
|
| **CreateProcessWithTokenW** | 以显式 Token 启动(含降权) | ★★★★★ |
|
||||||
|
| **ShellExecuteEx** | 关联文件/URL/提权启动 | ★★★☆☆ |
|
||||||
|
| **CreateProcess + Job Object** | 进程组统一管理 | ★★★★★ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 方案一:CreateProcess(推荐默认方案)
|
||||||
|
|
||||||
|
### 3.1 核心 API
|
||||||
|
|
||||||
|
```go
|
||||||
|
// kernel32.dll
|
||||||
|
syscall.CreateProcess(
|
||||||
|
lpApplicationName *uint16, // 可执行文件路径(可为 nil)
|
||||||
|
lpCommandLine *uint16, // 命令行字符串
|
||||||
|
lpProcessAttributes *syscall.SecurityAttributes, // 进程安全属性
|
||||||
|
lpThreadAttributes *syscall.SecurityAttributes, // 线程安全属性
|
||||||
|
bInheritHandles bool, // 是否继承句柄
|
||||||
|
dwCreationFlags uint32, // 创建标志
|
||||||
|
lpEnvironment *uint16, // 环境变量(nil=继承)
|
||||||
|
lpCurrentDirectory *uint16, // 当前目录(nil=继承)
|
||||||
|
lpStartupInfo *syscall.StartupInfo, // 启动信息
|
||||||
|
lpProcessInformation *syscall.ProcessInformation, // 输出:进程信息
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 基础封装
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CREATE_NO_WINDOW = 0x08000000 // 不创建窗口(控制台程序隐藏)
|
||||||
|
CREATE_NEW_CONSOLE = 0x00000010 // 创建新控制台
|
||||||
|
CREATE_SUSPENDED = 0x00000004 // 挂起创建
|
||||||
|
CREATE_BREAKAWAY_FROM_JOB = 0x01000000
|
||||||
|
CREATE_DEFAULT_ERROR_MODE = 0x04000000
|
||||||
|
NORMAL_PRIORITY_CLASS = 0x00000020
|
||||||
|
CREATE_UNICODE_ENVIRONMENT = 0x00000400
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunProcessCreate 使用 CreateProcess 启动程序
|
||||||
|
func RunProcessCreate(exePath string, args string, hidden bool) (*syscall.ProcessInformation, error) {
|
||||||
|
// 完整命令行:exe路径 + 空格 + 参数
|
||||||
|
cmdLine := fmt.Sprintf(`"%s" %s`, exePath, args)
|
||||||
|
|
||||||
|
exePtr, err := syscall.UTF16PtrFromString(exePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("exe路径转换失败: %w", err)
|
||||||
|
}
|
||||||
|
cmdPtr, err := syscall.UTF16PtrFromString(cmdLine)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("命令行转换失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var si syscall.StartupInfo
|
||||||
|
si.Cb = uint32(unsafe.Sizeof(si))
|
||||||
|
if hidden {
|
||||||
|
si.Flags = syscall.STARTF_USESHOWWINDOW
|
||||||
|
si.ShowWindow = 0 // SW_HIDE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认不显示窗口,不强制开启新控制台
|
||||||
|
flags := uint32(NORMAL_PRIORITY_CLASS)
|
||||||
|
if hidden {
|
||||||
|
flags |= CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
|
var pi syscall.ProcessInformation
|
||||||
|
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procCreateProcess := kernel32.NewProc("CreateProcessW")
|
||||||
|
|
||||||
|
ret, _, lastErr := procCreateProcess.Call(
|
||||||
|
uintptr(unsafe.Pointer(exePtr)), // lpApplicationName
|
||||||
|
uintptr(unsafe.Pointer(cmdPtr)), // lpCommandLine
|
||||||
|
0, // lpProcessAttributes (NULL)
|
||||||
|
0, // lpThreadAttributes (NULL)
|
||||||
|
0, // bInheritHandles (FALSE)
|
||||||
|
uintptr(flags), // dwCreationFlags
|
||||||
|
0, // lpEnvironment (NULL)
|
||||||
|
0, // lpCurrentDirectory (NULL)
|
||||||
|
uintptr(unsafe.Pointer(&si)), // lpStartupInfo
|
||||||
|
uintptr(unsafe.Pointer(&pi)), // lpProcessInformation
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return nil, fmt.Errorf("CreateProcess 失败: %v", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
func ExampleCreateProcess() {
|
||||||
|
pi, err := RunProcessCreate(
|
||||||
|
`C:\tools\easytier-core.exe`,
|
||||||
|
`--network-name mynet --network-secret mysecret --peers tcp://host:11010 -d`,
|
||||||
|
true, // hidden=true 隐藏窗口
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("启动失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
syscall.CloseHandle(pi.Process)
|
||||||
|
syscall.CloseHandle(pi.Thread)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("进程已启动, PID=%d\n", pi.ProcessId)
|
||||||
|
|
||||||
|
// 等待进程结束(最多 30 秒)
|
||||||
|
// 或使用 WaitForSingleObject 无限等待
|
||||||
|
// syscall.WaitForSingleObject(pi.Process, syscall.INFINITE)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 优势对比
|
||||||
|
|
||||||
|
| 特性 | `os/exec` | `CreateProcess` |
|
||||||
|
|------|-----------|-----------------|
|
||||||
|
| 隐藏窗口 | 需 HOOK 或 cmd 包装 | `CREATE_NO_WINDOW` 原生支持 |
|
||||||
|
| 进程挂起启动 | 不支持 | `CREATE_SUSPENDED` |
|
||||||
|
| 环境变量注入 | `cmd.Env` | `lpEnvironment` 直接传 |
|
||||||
|
| 工作目录 | `cmd.Dir` | `lpCurrentDirectory` |
|
||||||
|
| 优先级控制 | 不支持 | `*_PRIORITY_CLASS` 系列 |
|
||||||
|
| 额外依赖 | 无 | 无(纯 WinAPI) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 方案二:CreateProcess + 句柄重定向(实时输出捕获)
|
||||||
|
|
||||||
|
### 4.1 匿名管道重定向 stdout/stderr
|
||||||
|
|
||||||
|
```go
|
||||||
|
func RunProcessCapture(exePath string, args string) (*syscall.ProcessInformation, syscall.Handle, syscall.Handle, error) {
|
||||||
|
// 创建管道
|
||||||
|
var sa syscall.SecurityAttributes
|
||||||
|
sa.Length = uint32(unsafe.Sizeof(sa))
|
||||||
|
sa.InheritHandle = 1 // 句柄可继承
|
||||||
|
|
||||||
|
var stdoutRead, stdoutWrite syscall.Handle
|
||||||
|
var stderrRead, stderrWrite syscall.Handle
|
||||||
|
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procCreatePipe := kernel32.NewProc("CreatePipe")
|
||||||
|
|
||||||
|
// 创建 stdout 管道
|
||||||
|
ret, _, err := procCreatePipe.Call(
|
||||||
|
uintptr(unsafe.Pointer(&stdoutRead)),
|
||||||
|
uintptr(unsafe.Pointer(&stdoutWrite)),
|
||||||
|
uintptr(unsafe.Pointer(&sa)),
|
||||||
|
0, // 缓冲区大小,0=默认
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return nil, 0, 0, fmt.Errorf("创建stdout管道失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 stderr 管道
|
||||||
|
ret, _, err = procCreatePipe.Call(
|
||||||
|
uintptr(unsafe.Pointer(&stderrRead)),
|
||||||
|
uintptr(unsafe.Pointer(&stderrWrite)),
|
||||||
|
uintptr(unsafe.Pointer(&sa)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
syscall.CloseHandle(stdoutRead)
|
||||||
|
syscall.CloseHandle(stdoutWrite)
|
||||||
|
return nil, 0, 0, fmt.Errorf("创建stderr管道失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置 StartupInfo 重定向
|
||||||
|
var si syscall.StartupInfo
|
||||||
|
si.Cb = uint32(unsafe.Sizeof(si))
|
||||||
|
si.Flags = syscall.STARTF_USESTDHANDLES | syscall.STARTF_USESHOWWINDOW
|
||||||
|
si.ShowWindow = 0 // SW_HIDE
|
||||||
|
si.StdOutput = stdoutWrite
|
||||||
|
si.StdErr = stderrWrite
|
||||||
|
|
||||||
|
cmdLine := fmt.Sprintf(`"%s" %s`, exePath, args)
|
||||||
|
cmdPtr, _ := syscall.UTF16PtrFromString(cmdLine)
|
||||||
|
|
||||||
|
var pi syscall.ProcessInformation
|
||||||
|
|
||||||
|
procCreateProcess := kernel32.NewProc("CreateProcessW")
|
||||||
|
ret, _, lastErr := procCreateProcess.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(cmdPtr)),
|
||||||
|
0, 0,
|
||||||
|
1, // bInheritHandles = TRUE
|
||||||
|
uintptr(CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS),
|
||||||
|
0, 0,
|
||||||
|
uintptr(unsafe.Pointer(&si)),
|
||||||
|
uintptr(unsafe.Pointer(&pi)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
syscall.CloseHandle(stdoutRead)
|
||||||
|
syscall.CloseHandle(stdoutWrite)
|
||||||
|
syscall.CloseHandle(stderrRead)
|
||||||
|
syscall.CloseHandle(stderrWrite)
|
||||||
|
return nil, 0, 0, fmt.Errorf("CreateProcess 失败: %v", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭子进程端的写句柄,父进程只保留读句柄
|
||||||
|
syscall.CloseHandle(stdoutWrite)
|
||||||
|
syscall.CloseHandle(stderrWrite)
|
||||||
|
|
||||||
|
return &pi, stdoutRead, stderrRead, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFromPipe 从管道读取数据(非阻塞轮询)
|
||||||
|
func ReadFromPipe(handle syscall.Handle, bufSize uint32) (string, error) {
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procReadFile := kernel32.NewProc("ReadFile")
|
||||||
|
|
||||||
|
buf := make([]byte, bufSize)
|
||||||
|
var bytesRead uint32
|
||||||
|
|
||||||
|
ret, _, err := procReadFile.Call(
|
||||||
|
uintptr(handle),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(bufSize),
|
||||||
|
uintptr(unsafe.Pointer(&bytesRead)),
|
||||||
|
0, // lpOverlapped = NULL (同步)
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return "", fmt.Errorf("ReadFile 失败: %v", err)
|
||||||
|
}
|
||||||
|
return string(buf[:bytesRead]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
func ExampleCaptureOutput() {
|
||||||
|
pi, stdoutRead, stderrRead, err := RunProcessCapture(
|
||||||
|
`C:\tools\easytier-core.exe`,
|
||||||
|
`--network-name mynet --network-secret mysecret --peers tcp://host:11010 -d`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("启动失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
syscall.CloseHandle(pi.Process)
|
||||||
|
syscall.CloseHandle(pi.Thread)
|
||||||
|
syscall.CloseHandle(stdoutRead)
|
||||||
|
syscall.CloseHandle(stderrRead)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("进程已启动, PID=%d\n", pi.ProcessId)
|
||||||
|
|
||||||
|
// 实时读取输出(goroutine 持续读取)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
data, err := ReadFromPipe(stdoutRead, 4096)
|
||||||
|
if err != nil || data == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Print("[stdout] ", data)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
data, err := ReadFromPipe(stderrRead, 4096)
|
||||||
|
if err != nil || data == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Print("[stderr] ", data)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 方案三:Job Object 进程组管理
|
||||||
|
|
||||||
|
### 5.1 概念
|
||||||
|
|
||||||
|
Job Object 可将多个子进程关联到一个"作业对象",实现统一的生命周期控制:
|
||||||
|
|
||||||
|
- 主进程退出 → Job 内所有子进程自动终止
|
||||||
|
- 资源限制(CPU、内存)
|
||||||
|
- 统一通知
|
||||||
|
|
||||||
|
### 5.2 实现
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
||||||
|
)
|
||||||
|
|
||||||
|
type JOBOBJECT_BASIC_LIMIT_INFORMATION struct {
|
||||||
|
PerProcessUserTimeLimit int64
|
||||||
|
PerJobUserTimeLimit int64
|
||||||
|
LimitFlags uint32
|
||||||
|
MinimumWorkingSetSize uintptr
|
||||||
|
MaximumWorkingSetSize uintptr
|
||||||
|
ActiveProcessLimit uint32
|
||||||
|
Affinity uintptr
|
||||||
|
PriorityClass uint32
|
||||||
|
SchedulingClass uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type JOBOBJECT_EXTENDED_LIMIT_INFORMATION struct {
|
||||||
|
BasicLimitInformation JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||||
|
IoInfo [48]byte // reserved
|
||||||
|
ProcessMemoryLimit uintptr
|
||||||
|
JobMemoryLimit uintptr
|
||||||
|
PeakProcessMemoryUsed uintptr
|
||||||
|
PeakJobMemoryUsed uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateJobWithAutoKill 创建 Job Object,主进程退出时自动终止子进程
|
||||||
|
func CreateJobWithAutoKill() (syscall.Handle, error) {
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procCreateJobObject := kernel32.NewProc("CreateJobObjectW")
|
||||||
|
procSetInformationJobObject := kernel32.NewProc("SetInformationJobObject")
|
||||||
|
|
||||||
|
// 创建 Job Object
|
||||||
|
jobName := syscall.StringToUTF16Ptr("")
|
||||||
|
job, _, err := procCreateJobObject.Call(
|
||||||
|
0, // lpJobAttributes = NULL
|
||||||
|
uintptr(unsafe.Pointer(jobName)),
|
||||||
|
)
|
||||||
|
if job == 0 {
|
||||||
|
return 0, fmt.Errorf("CreateJobObject 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置限制:当 Job 最后一个句柄关闭时,终止所有关联进程
|
||||||
|
var jeli JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||||
|
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||||
|
|
||||||
|
const JobObjectExtendedLimitInformation = 9
|
||||||
|
ret, _, err := procSetInformationJobObject.Call(
|
||||||
|
job,
|
||||||
|
uintptr(JobObjectExtendedLimitInformation),
|
||||||
|
uintptr(unsafe.Pointer(&jeli)),
|
||||||
|
uintptr(unsafe.Sizeof(jeli)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
syscall.CloseHandle(syscall.Handle(job))
|
||||||
|
return 0, fmt.Errorf("SetInformationJobObject 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return syscall.Handle(job), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignProcessToJob 将进程关联到 Job Object
|
||||||
|
func AssignProcessToJob(job syscall.Handle, process syscall.Handle) error {
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procAssignProcessToJobObject := kernel32.NewProc("AssignProcessToJobObject")
|
||||||
|
|
||||||
|
ret, _, err := procAssignProcessToJobObject.Call(
|
||||||
|
uintptr(job),
|
||||||
|
uintptr(process),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return fmt.Errorf("AssignProcessToJobObject 失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunProcessInJob 在 Job Object 中启动进程
|
||||||
|
func RunProcessInJob(exePath string, args string, hidden bool) (*syscall.ProcessInformation, syscall.Handle, error) {
|
||||||
|
// 先创建 Job
|
||||||
|
job, err := CreateJobWithAutoKill()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("创建Job失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动进程(挂起)
|
||||||
|
cmdLine := fmt.Sprintf(`"%s" %s`, exePath, args)
|
||||||
|
cmdPtr, _ := syscall.UTF16PtrFromString(cmdLine)
|
||||||
|
|
||||||
|
var si syscall.StartupInfo
|
||||||
|
si.Cb = uint32(unsafe.Sizeof(si))
|
||||||
|
if hidden {
|
||||||
|
si.Flags = syscall.STARTF_USESHOWWINDOW
|
||||||
|
si.ShowWindow = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var pi syscall.ProcessInformation
|
||||||
|
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procCreateProcess := kernel32.NewProc("CreateProcessW")
|
||||||
|
|
||||||
|
flags := uint32(CREATE_SUSPENDED | NORMAL_PRIORITY_CLASS)
|
||||||
|
if hidden {
|
||||||
|
flags |= CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, lastErr := procCreateProcess.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(cmdPtr)),
|
||||||
|
0, 0, 0,
|
||||||
|
uintptr(flags),
|
||||||
|
0, 0,
|
||||||
|
uintptr(unsafe.Pointer(&si)),
|
||||||
|
uintptr(unsafe.Pointer(&pi)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
syscall.CloseHandle(job)
|
||||||
|
return nil, 0, fmt.Errorf("CreateProcess 失败: %v", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联到 Job
|
||||||
|
if err := AssignProcessToJob(job, pi.Process); err != nil {
|
||||||
|
// 关联失败,终止进程并清理
|
||||||
|
syscall.TerminateProcess(pi.Process, 1)
|
||||||
|
syscall.CloseHandle(pi.Process)
|
||||||
|
syscall.CloseHandle(pi.Thread)
|
||||||
|
syscall.CloseHandle(job)
|
||||||
|
return nil, 0, fmt.Errorf("关联Job失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复进程执行
|
||||||
|
kernel32.NewProc("ResumeThread").Call(uintptr(pi.Thread))
|
||||||
|
|
||||||
|
return &pi, job, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
func ExampleJobObject() {
|
||||||
|
pi, job, err := RunProcessInJob(
|
||||||
|
`C:\tools\easytier-core.exe`,
|
||||||
|
`--network-name mynet --network-secret mysecret --peers tcp://host:11010 -d`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("启动失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:defer 顺序很重要!
|
||||||
|
// 先关闭 job → 触发 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE → 子进程自动终止
|
||||||
|
defer syscall.CloseHandle(job)
|
||||||
|
defer syscall.CloseHandle(pi.Process)
|
||||||
|
defer syscall.CloseHandle(pi.Thread)
|
||||||
|
|
||||||
|
fmt.Printf("进程已在 Job 中启动, PID=%d\n", pi.ProcessId)
|
||||||
|
fmt.Println("主进程退出时,所有子进程将自动终止")
|
||||||
|
|
||||||
|
// 正常结束流程:
|
||||||
|
// 1. 先向子进程发送优雅退出信号(如 WM_CLOSE)
|
||||||
|
// 2. 等待一段时间
|
||||||
|
// 3. 再 CloseHandle(job) 强制终止
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 关键特性
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` | Job 句柄关闭 → 所有关联进程被 `TerminateProcess` 强制终止 |
|
||||||
|
| `CREATE_SUSPENDED` | 挂起创建,确保进程在加入 Job 之前不会执行任何代码 |
|
||||||
|
| 自动清理 | 即使主进程崩溃,OS 也会回收 Job 并终止所有子进程 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 方案四:ShellExecuteEx(提升:已有实现增强)
|
||||||
|
|
||||||
|
### 6.1 与 ShellExecute 的对比
|
||||||
|
|
||||||
|
当前项目使用 `ShellExecuteW`,`ShellExecuteExW` 增强点:
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
SW_HIDE = 0
|
||||||
|
SW_NORMAL = 1
|
||||||
|
SW_MINIMIZE = 6
|
||||||
|
SEE_MASK_NOCLOSEPROCESS = 0x00000040 // 获取进程句柄
|
||||||
|
SEE_MASK_NO_CONSOLE = 0x00008000 // 不创建控制台
|
||||||
|
SEE_MASK_FLAG_NO_UI = 0x00000400 // 不显示错误对话框
|
||||||
|
)
|
||||||
|
|
||||||
|
type SHELLEXECUTEINFOW struct {
|
||||||
|
cbSize uint32
|
||||||
|
fMask uint32
|
||||||
|
hwnd syscall.Handle
|
||||||
|
lpVerb *uint16
|
||||||
|
lpFile *uint16
|
||||||
|
lpParameters *uint16
|
||||||
|
lpDirectory *uint16
|
||||||
|
nShow int32
|
||||||
|
hInstApp syscall.Handle
|
||||||
|
lpIDList uintptr
|
||||||
|
lpClass *uint16
|
||||||
|
hkeyClass syscall.Handle
|
||||||
|
dwHotKey uint32
|
||||||
|
hIcon syscall.Handle
|
||||||
|
hProcess syscall.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShellExecuteEx 增强封装
|
||||||
|
func ShellExec(exe string, params string, verb string, wait bool) (syscall.Handle, error) {
|
||||||
|
shell32 := syscall.NewLazyDLL("shell32.dll")
|
||||||
|
procShellExecuteEx := shell32.NewProc("ShellExecuteExW")
|
||||||
|
|
||||||
|
verbPtr, _ := syscall.UTF16PtrFromString(verb)
|
||||||
|
exePtr, _ := syscall.UTF16PtrFromString(exe)
|
||||||
|
|
||||||
|
var paramsPtr *uint16
|
||||||
|
if params != "" {
|
||||||
|
paramsPtr, _ = syscall.UTF16PtrFromString(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info SHELLEXECUTEINFOW
|
||||||
|
info.cbSize = uint32(unsafe.Sizeof(info))
|
||||||
|
info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE | SEE_MASK_FLAG_NO_UI
|
||||||
|
info.lpVerb = verbPtr
|
||||||
|
info.lpFile = exePtr
|
||||||
|
info.lpParameters = paramsPtr
|
||||||
|
info.nShow = SW_HIDE
|
||||||
|
|
||||||
|
ret, _, err := procShellExecuteEx.Call(uintptr(unsafe.Pointer(&info)))
|
||||||
|
if ret == 0 {
|
||||||
|
return 0, fmt.Errorf("ShellExecuteEx 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
// WaitForSingleObject 等待进程结束
|
||||||
|
syscall.WaitForSingleObject(info.hProcess, syscall.INFINITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.hProcess, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 方案五:CreateProcessAsUser / CreateProcessWithTokenW
|
||||||
|
|
||||||
|
### 7.1 场景
|
||||||
|
|
||||||
|
- 从 SYSTEM 服务降权到当前登录用户启动 GUI 进程
|
||||||
|
- 以指定域用户身份运行
|
||||||
|
- Token 窃取/复制后委托执行
|
||||||
|
|
||||||
|
### 7.2 降权启动(SYSTEM → User)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GetActiveUserToken 获取当前活动用户的 Token
|
||||||
|
func GetActiveUserToken() (syscall.Token, error) {
|
||||||
|
// 1. 枚举所有会话,找到活动控制台会话
|
||||||
|
// 2. WTSQueryUserToken 获取 Token
|
||||||
|
// 3. 返回用户 Token
|
||||||
|
|
||||||
|
wtsapi32 := syscall.NewLazyDLL("wtsapi32.dll")
|
||||||
|
procWTSGetActiveConsoleSessionId := wtsapi32.NewProc("WTSGetActiveConsoleSessionId")
|
||||||
|
|
||||||
|
sessionId, _, _ := procWTSGetActiveConsoleSessionId.Call()
|
||||||
|
|
||||||
|
procWTSQueryUserToken := wtsapi32.NewProc("WTSQueryUserTokenW")
|
||||||
|
var token syscall.Token
|
||||||
|
ret, _, err := procWTSQueryUserToken.Call(
|
||||||
|
sessionId,
|
||||||
|
uintptr(unsafe.Pointer(&token)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return 0, fmt.Errorf("WTSQueryUserToken 失败: %v", err)
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunAsUser 以指定用户身份启动进程
|
||||||
|
func RunAsUser(token syscall.Token, exePath string, args string) error {
|
||||||
|
var si syscall.StartupInfo
|
||||||
|
si.Cb = uint32(unsafe.Sizeof(si))
|
||||||
|
|
||||||
|
var pi syscall.ProcessInformation
|
||||||
|
|
||||||
|
cmdLine := fmt.Sprintf(`"%s" %s`, exePath, args)
|
||||||
|
cmdPtr, _ := syscall.UTF16PtrFromString(cmdLine)
|
||||||
|
|
||||||
|
advapi32 := syscall.NewLazyDLL("advapi32.dll")
|
||||||
|
procCreateProcessAsUser := advapi32.NewProc("CreateProcessAsUserW")
|
||||||
|
|
||||||
|
ret, _, err := procCreateProcessAsUser.Call(
|
||||||
|
uintptr(token),
|
||||||
|
0, // lpApplicationName
|
||||||
|
uintptr(unsafe.Pointer(cmdPtr)),
|
||||||
|
0, 0, // lpProcessAttributes, lpThreadAttributes
|
||||||
|
0, // bInheritHandles
|
||||||
|
0, // dwCreationFlags
|
||||||
|
0, 0, // lpEnvironment, lpCurrentDirectory
|
||||||
|
uintptr(unsafe.Pointer(&si)),
|
||||||
|
uintptr(unsafe.Pointer(&pi)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return fmt.Errorf("CreateProcessAsUser 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
syscall.CloseHandle(pi.Process)
|
||||||
|
syscall.CloseHandle(pi.Thread)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("以用户身份启动, PID=%d\n", pi.ProcessId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 方案六:进程优雅终止
|
||||||
|
|
||||||
|
### 8.1 当前问题
|
||||||
|
|
||||||
|
`proc.Kill()` → `TerminateProcess`,子进程无机会清理资源。
|
||||||
|
|
||||||
|
### 8.2 优雅终止实现
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
const (
|
||||||
|
CTRL_C_EVENT = 0
|
||||||
|
CTRL_BREAK_EVENT = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendCtrlC 向控制台进程发送 Ctrl+C
|
||||||
|
func SendCtrlC(pid uint32) error {
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procAttachConsole := kernel32.NewProc("AttachConsole")
|
||||||
|
procGenerateConsoleCtrlEvent := kernel32.NewProc("GenerateConsoleCtrlEvent")
|
||||||
|
procFreeConsole := kernel32.NewProc("FreeConsole")
|
||||||
|
|
||||||
|
// 附加到目标进程的控制台
|
||||||
|
ret, _, _ := procAttachConsole.Call(uintptr(pid))
|
||||||
|
if ret == 0 {
|
||||||
|
// 置零当前进程的错误模式,防止自身被 Ctrl+C 影响
|
||||||
|
kernel32.NewProc("SetConsoleCtrlHandler").Call(0, 1)
|
||||||
|
procAttachConsole.Call(uintptr(pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送 Ctrl+C
|
||||||
|
ret, _, _ = procGenerateConsoleCtrlEvent.Call(
|
||||||
|
uintptr(CTRL_C_EVENT),
|
||||||
|
0, // 0 = 发送到当前附加的控制台中的所有进程
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分离
|
||||||
|
procFreeConsole.Call()
|
||||||
|
|
||||||
|
if ret == 0 {
|
||||||
|
return fmt.Errorf("GenerateConsoleCtrlEvent 失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefulKill 优雅终止进程
|
||||||
|
func GracefulKill(process syscall.Handle, timeoutMs uint32) error {
|
||||||
|
// 1. 尝试 Ctrl+C(仅对控制台进程有效)
|
||||||
|
// 2. 等待 timeoutMs 毫秒
|
||||||
|
// 3. 超时则 TerminateProcess
|
||||||
|
|
||||||
|
// 获取进程 PID 用于 AttachConsole
|
||||||
|
pid := syscall.GetProcessId(process)
|
||||||
|
|
||||||
|
if err := SendCtrlC(pid); err == nil {
|
||||||
|
// 等待进程自己退出
|
||||||
|
event, _ := syscall.WaitForSingleObject(process, timeoutMs)
|
||||||
|
if event == syscall.WAIT_OBJECT_0 {
|
||||||
|
return nil // 进程正常退出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制终止
|
||||||
|
return syscall.TerminateProcess(process, 0)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 完整封装:ProcessManager
|
||||||
|
|
||||||
|
### 9.1 统一的进程管理器
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
processes map[uint32]*ManagedProcess
|
||||||
|
job syscall.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManagedProcess struct {
|
||||||
|
Process syscall.Handle
|
||||||
|
Thread syscall.Handle
|
||||||
|
PID uint32
|
||||||
|
StdoutRead syscall.Handle
|
||||||
|
StderrRead syscall.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProcessManager() (*ProcessManager, error) {
|
||||||
|
job, err := CreateJobWithAutoKill()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ProcessManager{
|
||||||
|
processes: make(map[uint32]*ManagedProcess),
|
||||||
|
job: job,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) Launch(opts LaunchOptions) (*ManagedProcess, error) {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
cmdLine := fmt.Sprintf(`"%s" %s`, opts.ExePath, opts.Args)
|
||||||
|
cmdPtr, _ := syscall.UTF16PtrFromString(cmdLine)
|
||||||
|
|
||||||
|
var si syscall.StartupInfo
|
||||||
|
si.Cb = uint32(unsafe.Sizeof(si))
|
||||||
|
if opts.HideWindow {
|
||||||
|
si.Flags = syscall.STARTF_USESHOWWINDOW
|
||||||
|
si.ShowWindow = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果开启输出捕获
|
||||||
|
var stdoutRead, stderrRead syscall.Handle
|
||||||
|
if opts.CaptureOutput {
|
||||||
|
// ... 创建管道,配置 si.StdOutput/si.StdErr ...
|
||||||
|
}
|
||||||
|
|
||||||
|
var pi syscall.ProcessInformation
|
||||||
|
|
||||||
|
flags := uint32(CREATE_SUSPENDED | NORMAL_PRIORITY_CLASS)
|
||||||
|
if opts.HideWindow {
|
||||||
|
flags |= CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procCreateProcess := kernel32.NewProc("CreateProcessW")
|
||||||
|
|
||||||
|
ret, _, err := procCreateProcess.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(cmdPtr)),
|
||||||
|
0, 0, 0,
|
||||||
|
uintptr(flags),
|
||||||
|
0, 0,
|
||||||
|
uintptr(unsafe.Pointer(&si)),
|
||||||
|
uintptr(unsafe.Pointer(&pi)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return nil, fmt.Errorf("CreateProcess 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联 Job
|
||||||
|
AssignProcessToJob(pm.job, pi.Process)
|
||||||
|
|
||||||
|
// 恢复线程
|
||||||
|
kernel32.NewProc("ResumeThread").Call(uintptr(pi.Thread))
|
||||||
|
|
||||||
|
mp := &ManagedProcess{
|
||||||
|
Process: pi.Process,
|
||||||
|
Thread: pi.Thread,
|
||||||
|
PID: pi.ProcessId,
|
||||||
|
StdoutRead: stdoutRead,
|
||||||
|
StderrRead: stderrRead,
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.processes[pi.ProcessId] = mp
|
||||||
|
return mp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LaunchOptions 启动选项
|
||||||
|
type LaunchOptions struct {
|
||||||
|
ExePath string
|
||||||
|
Args string
|
||||||
|
HideWindow bool
|
||||||
|
CaptureOutput bool
|
||||||
|
WorkingDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown 关闭所有子进程
|
||||||
|
func (pm *ProcessManager) Shutdown() {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
// 关闭 Job → 所有子进程自动终止
|
||||||
|
if pm.job != 0 {
|
||||||
|
syscall.CloseHandle(pm.job)
|
||||||
|
pm.job = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理句柄
|
||||||
|
for _, mp := range pm.processes {
|
||||||
|
syscall.CloseHandle(mp.Process)
|
||||||
|
syscall.CloseHandle(mp.Thread)
|
||||||
|
if mp.StdoutRead != 0 {
|
||||||
|
syscall.CloseHandle(mp.StdoutRead)
|
||||||
|
}
|
||||||
|
if mp.StderrRead != 0 {
|
||||||
|
syscall.CloseHandle(mp.StderrRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pm.processes = make(map[uint32]*ManagedProcess)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 迁移建议
|
||||||
|
|
||||||
|
### 10.1 迁移路径
|
||||||
|
|
||||||
|
```
|
||||||
|
当前:
|
||||||
|
DoConnect.go → exec.Command("cmd", "/c", cmd) → cmd.exe 包装
|
||||||
|
main.go → exec.Command("cmd", "/c", start) → cmd.exe 包装
|
||||||
|
Perm.go → ShellExecuteW("runas") → 提权可靠
|
||||||
|
|
||||||
|
建议:
|
||||||
|
DoConnect.go → ProcessManager.Launch() → 直接 CreateProcess + Job
|
||||||
|
main.go → ShellExecuteEx("open", url) → 去掉 cmd 中间层
|
||||||
|
Perm.go → 保持不变 → 已是最佳实践
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 DoConnect.go 改造示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 改造前
|
||||||
|
func connectToVirtualHotspot() *os.Process {
|
||||||
|
cmd := miner.BuildTaskCommand(...)
|
||||||
|
c := exec.Command("cmd", "/c", cmd)
|
||||||
|
c.Start()
|
||||||
|
return c.Process
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改造后
|
||||||
|
var pm *ProcessManager
|
||||||
|
|
||||||
|
func connectToVirtualHotspot() (*ManagedProcess, error) {
|
||||||
|
cmdArgs := fmt.Sprintf(
|
||||||
|
"--network-name %s --network-secret %s --peers tcp://%s:%s -d",
|
||||||
|
netname, token, host, port,
|
||||||
|
)
|
||||||
|
return pm.Launch(LaunchOptions{
|
||||||
|
ExePath: `.\easytier-core.exe`,
|
||||||
|
Args: cmdArgs,
|
||||||
|
HideWindow: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func onExit() {
|
||||||
|
log.Info().Msg("关闭所有子进程")
|
||||||
|
pm.Shutdown()
|
||||||
|
log.Info().Msg("程序退出")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 main.go 改造示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 改造前
|
||||||
|
exec.Command("cmd", "/c", "start", "http://...").Start()
|
||||||
|
|
||||||
|
// 改造后
|
||||||
|
shell32 := syscall.NewLazyDLL("shell32.dll")
|
||||||
|
procShellExecute := shell32.NewProc("ShellExecuteW")
|
||||||
|
|
||||||
|
func openURL(url string) {
|
||||||
|
urlPtr, _ := syscall.UTF16PtrFromString(url)
|
||||||
|
verbPtr, _ := syscall.UTF16PtrFromString("open")
|
||||||
|
procShellExecute.Call(0,
|
||||||
|
uintptr(unsafe.Pointer(verbPtr)),
|
||||||
|
uintptr(unsafe.Pointer(urlPtr)),
|
||||||
|
0, 0, 1, // SW_NORMAL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 方案对比总结
|
||||||
|
|
||||||
|
| 特性 | `os/exec` | `CreateProcess` | `CreateProcess` + Job | `ShellExecuteEx` |
|
||||||
|
|------|-----------|-----------------|----------------------|-------------------|
|
||||||
|
| 隐藏窗口 | ❌ 需 cmd 包装 | ✅ `CREATE_NO_WINDOW` | ✅ | ✅ `SW_HIDE` |
|
||||||
|
| 实时输出捕获 | ✅ `cmd.StdoutPipe` | ✅ 匿名管道 | ✅ | ❌ 需重定向到文件 |
|
||||||
|
| 进程组管理 | ❌ | ❌ | ✅ Job Object | ❌ |
|
||||||
|
| 优雅终止 | ❌ 仅 `Kill()` | ✅ `GenerateConsoleCtrlEvent` | ✅ Job 自动清理 | ❌ |
|
||||||
|
| 提权启动 | ❌ | ❌ | ❌ | ✅ `runas` verb |
|
||||||
|
| 无 cmd 依赖 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 跨平台 | ✅ | ❌ Windows only | ❌ | ❌ |
|
||||||
|
| 推荐场景 | 开发/调试 | 生产环境最简单的改进 | **生产环境推荐** | URL/文档打开、提权 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 注意事项
|
||||||
|
|
||||||
|
1. **句柄泄漏**:务必 `CloseHandle(pi.Process)` 和 `CloseHandle(pi.Thread)`,否则进程对象不会释放
|
||||||
|
2. **Job Object 限制**:一个进程只能属于一个 Job,子进程默认继承父进程的 Job
|
||||||
|
3. **管道死锁**:如果子进程写满管道缓冲区且父进程不读取,子进程会阻塞
|
||||||
|
4. **CREATE_NO_WINDOW**:仅对控制台子系统程序有效,GUI 程序需要 `SW_HIDE` + `STARTF_USESHOWWINDOW`
|
||||||
|
5. **UAC 提权**:非管理员进程无法直接启动管理员进程,必须通过 `ShellExecute("runas")` 触发 UAC
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
module gitee.com/SCU_team/srcc
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/getlantern/systray v1.2.2
|
||||||
|
github.com/rs/zerolog v1.35.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||||
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||||
|
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
|
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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// icon_bytes 模块:存储 32x32 PNG 图标的原始字节数据。
|
||||||
|
// 由 Ruby 脚本自动生成:ruby convert_icon.rb -s 32
|
||||||
|
// 推荐未来使用 Go 1.16+ embed://go:embed image.png
|
||||||
|
package main
|
||||||
|
|
||||||
|
// iconData 内嵌的 32x32 PNG 托盘图标原始字节。
|
||||||
|
var iconData = []byte{
|
||||||
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||||
|
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
|
||||||
|
0x08, 0x02, 0x00, 0x00, 0x00, 0xFC, 0x18, 0xED, 0xA3, 0x00, 0x00, 0x08,
|
||||||
|
0x7C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x55, 0x56, 0x69, 0x70, 0x14,
|
||||||
|
0xC7, 0x15, 0xEE, 0x6B, 0x66, 0xF6, 0xD2, 0x2D, 0x84, 0x40, 0xE8, 0x00,
|
||||||
|
0x61, 0x40, 0x88, 0xC3, 0x36, 0x36, 0x25, 0x43, 0x0A, 0x03, 0x0E, 0x22,
|
||||||
|
0x1C, 0xE1, 0x48, 0x71, 0xD9, 0xC8, 0x80, 0x1D, 0xE7, 0x20, 0x71, 0xB0,
|
||||||
|
0xA9, 0x94, 0x43, 0x7C, 0xA4, 0xCA, 0x85, 0x7F, 0xD8, 0x15, 0x70, 0x39,
|
||||||
|
0x8E, 0x53, 0x21, 0x21, 0x4E, 0x88, 0x92, 0xE0, 0x0A, 0x95, 0xC3, 0x31,
|
||||||
|
0xA9, 0x90, 0x94, 0x53, 0xD8, 0x29, 0x9B, 0xD8, 0xE6, 0xB6, 0x81, 0x08,
|
||||||
|
0x09, 0x09, 0x16, 0x24, 0x0B, 0x09, 0x1D, 0xBB, 0x92, 0xD8, 0xD9, 0x9D,
|
||||||
|
0xAB, 0xBB, 0xF3, 0x7A, 0x66, 0xB5, 0xC8, 0xBD, 0x53, 0x53, 0xD3, 0x33,
|
||||||
|
0xDD, 0xEF, 0x7D, 0xEF, 0xFB, 0xDE, 0x7B, 0xBD, 0x24, 0x56, 0x33, 0xA9,
|
||||||
|
0xA4, 0x6E, 0x6A, 0x45, 0xC3, 0x3D, 0x53, 0x97, 0x2F, 0x9E, 0xBD, 0x65,
|
||||||
|
0xE5, 0xBC, 0x1D, 0x1B, 0x17, 0xEC, 0x7C, 0x74, 0xD1, 0xD3, 0x4F, 0xAC,
|
||||||
|
0x7D, 0xF9, 0xB9, 0x8D, 0x3F, 0x7B, 0xA9, 0xE9, 0xE0, 0xFE, 0xC7, 0x0F,
|
||||||
|
0xBD, 0xBE, 0xED, 0xE0, 0xAB, 0x5B, 0x7F, 0xB1, 0x6F, 0xD3, 0x4F, 0x5F,
|
||||||
|
0x82, 0x97, 0x8B, 0x7F, 0xB0, 0xF3, 0x81, 0x6F, 0x35, 0xCD, 0xDE, 0xB2,
|
||||||
|
0xA6, 0xB6, 0x71, 0x51, 0xD5, 0x82, 0xFB, 0xC6, 0xDF, 0x37, 0xAB, 0x64,
|
||||||
|
0xD6, 0xB4, 0xFC, 0xC9, 0xD5, 0x91, 0xCA, 0xB2, 0xC8, 0xA4, 0xB2, 0xE8,
|
||||||
|
0xC4, 0x71, 0x91, 0xF2, 0xD2, 0xE8, 0xF8, 0xD2, 0x70, 0x69, 0x61, 0x6C,
|
||||||
|
0x7C, 0x69, 0x64, 0x42, 0x19, 0x11, 0x69, 0xCB, 0xBE, 0x9D, 0x4A, 0xF5,
|
||||||
|
0x0C, 0x0E, 0xDF, 0xEA, 0xED, 0x8F, 0x77, 0xF7, 0x5D, 0xBB, 0xDE, 0xDD,
|
||||||
|
0x76, 0xED, 0x66, 0x47, 0xBC, 0xE5, 0xD4, 0xD9, 0x2B, 0x27, 0xCF, 0xB7,
|
||||||
|
0x7C, 0x7C, 0xEA, 0xD3, 0x8F, 0x3E, 0xF9, 0xDF, 0xC9, 0x53, 0xAD, 0xA7,
|
||||||
|
0xCF, 0xB4, 0x9C, 0x39, 0xDF, 0x76, 0xFA, 0x5C, 0xE7, 0xA5, 0xCB, 0x9F,
|
||||||
|
0x5F, 0x69, 0xEF, 0x8F, 0xDF, 0x48, 0xF5, 0x0D, 0x98, 0x23, 0x23, 0xCE,
|
||||||
|
0x70, 0xC6, 0x1A, 0x49, 0x71, 0x3B, 0x8D, 0x1D, 0x2E, 0x2C, 0xC7, 0xB3,
|
||||||
|
0x6C, 0xE1, 0xBA, 0xDC, 0x71, 0x24, 0x17, 0x9E, 0xE7, 0x22, 0xCF, 0x23,
|
||||||
|
0xAE, 0x6B, 0x73, 0xCB, 0xF2, 0xD2, 0x66, 0xA6, 0x3F, 0x91, 0xEA, 0xEE,
|
||||||
|
0x1B, 0xEA, 0xEC, 0x19, 0xBA, 0xDE, 0x3D, 0xD0, 0x7A, 0x35, 0xD9, 0x7A,
|
||||||
|
0xA5, 0xFB, 0xDC, 0x67, 0x5D, 0xE7, 0x2F, 0x74, 0x83, 0xD1, 0x73, 0x17,
|
||||||
|
0xE2, 0x67, 0xCE, 0xF5, 0x7E, 0x7A, 0xB1, 0xBF, 0xB5, 0x23, 0xD1, 0x7E,
|
||||||
|
0xE3, 0x76, 0x67, 0xAF, 0xD5, 0x3B, 0x98, 0x19, 0x1C, 0xB2, 0x93, 0x23,
|
||||||
|
0xD6, 0x50, 0xC2, 0x4B, 0xDF, 0x06, 0xBB, 0xD2, 0x76, 0x05, 0xE7, 0x02,
|
||||||
|
0x86, 0xC7, 0xB9, 0x14, 0xD8, 0x7F, 0x14, 0x9E, 0x43, 0xA4, 0xC7, 0x3D,
|
||||||
|
0xC7, 0x75, 0x5C, 0xDB, 0x36, 0xD3, 0x9E, 0x69, 0xDA, 0xC3, 0xC3, 0xCE,
|
||||||
|
0x50, 0x32, 0x93, 0x48, 0xA4, 0xFA, 0x93, 0xA9, 0x5B, 0x83, 0x66, 0xFF,
|
||||||
|
0x80, 0xD9, 0x9F, 0x4C, 0xF7, 0x26, 0x9C, 0xC4, 0x88, 0xDD, 0x37, 0x64,
|
||||||
|
0x0D, 0x26, 0xED, 0x44, 0xD2, 0x1A, 0x1A, 0x4A, 0x8F, 0x0C, 0xDB, 0xA9,
|
||||||
|
0x94, 0x95, 0x36, 0x5D, 0xC7, 0x11, 0x8E, 0x00, 0x6B, 0x80, 0x19, 0x50,
|
||||||
|
0xC3, 0xC5, 0xB9, 0x7A, 0xF0, 0x90, 0xE4, 0xAE, 0x72, 0x45, 0xB0, 0xA7,
|
||||||
|
0x5E, 0xC1, 0x04, 0xD9, 0x1E, 0xB7, 0x1C, 0x69, 0xB9, 0x8E, 0x6D, 0xBB,
|
||||||
|
0xB6, 0xE3, 0x5A, 0x36, 0xCF, 0xD8, 0x6E, 0xD2, 0xB4, 0x52, 0xA6, 0x9D,
|
||||||
|
0x4E, 0x39, 0x23, 0x66, 0xC6, 0x34, 0x2D, 0x33, 0x6D, 0x5B, 0x96, 0x9D,
|
||||||
|
0xB1, 0x84, 0xED, 0x7A, 0x69, 0x0B, 0x65, 0x1C, 0x60, 0x06, 0xB9, 0x1C,
|
||||||
|
0xDB, 0x1E, 0x3C, 0x00, 0x56, 0xB0, 0x46, 0xB8, 0x84, 0xBB, 0x7A, 0x29,
|
||||||
|
0x24, 0xBC, 0x21, 0x10, 0x07, 0xCC, 0xE1, 0x82, 0xC1, 0xFD, 0x21, 0x1D,
|
||||||
|
0x0F, 0x3E, 0x13, 0x97, 0x7B, 0xAE, 0x07, 0x00, 0x79, 0x2A, 0xC3, 0x4D,
|
||||||
|
0xCB, 0x33, 0x33, 0xE0, 0x0F, 0x10, 0x80, 0x69, 0xE9, 0xF2, 0x00, 0xA9,
|
||||||
|
0xE7, 0x8F, 0x80, 0x8C, 0x2C, 0x39, 0xDC, 0xCB, 0xCD, 0x82, 0x80, 0x48,
|
||||||
|
0x60, 0x14, 0xC1, 0x5C, 0x4A, 0x75, 0x87, 0x67, 0xFF, 0x01, 0x76, 0x22,
|
||||||
|
0xCE, 0x31, 0x92, 0x25, 0x95, 0x55, 0x0D, 0x8F, 0x6D, 0xC6, 0xE5, 0x45,
|
||||||
|
0x5A, 0x59, 0xF1, 0xAC, 0xAD, 0x6B, 0xC2, 0xE5, 0x65, 0x46, 0xC8, 0xE0,
|
||||||
|
0xAE, 0x4B, 0x31, 0xC6, 0xC1, 0x16, 0xCF, 0x83, 0x5D, 0xB0, 0x01, 0x9E,
|
||||||
|
0x11, 0x98, 0xE1, 0x5F, 0x18, 0x84, 0x20, 0x04, 0xEB, 0x02, 0xEB, 0xF0,
|
||||||
|
0x19, 0x76, 0x85, 0x0B, 0xF2, 0x2A, 0x16, 0xCE, 0xAB, 0x5A, 0xB2, 0xB0,
|
||||||
|
0xEA, 0xCB, 0x5F, 0x8A, 0xCD, 0x9C, 0xF2, 0xB5, 0xA3, 0x07, 0x6A, 0x76,
|
||||||
|
0x6E, 0xFA, 0xE0, 0xC6, 0xF5, 0x4F, 0xBA, 0xBA, 0xDE, 0xFE, 0xF5, 0x5B,
|
||||||
|
0x17, 0xAE, 0x5D, 0x3D, 0x31, 0xDC, 0xFF, 0xC7, 0x54, 0xD7, 0xE3, 0xFF,
|
||||||
|
0x39, 0xB2, 0xFD, 0xA3, 0x23, 0xDB, 0x3F, 0xFC, 0xF3, 0xD7, 0x3F, 0xFC,
|
||||||
|
0xCB, 0xD6, 0xF7, 0xDE, 0x5A, 0xF7, 0x6E, 0x33, 0x29, 0x2F, 0x0E, 0x8D,
|
||||||
|
0x2B, 0x42, 0x12, 0x81, 0xCD, 0xE0, 0x82, 0x41, 0xC0, 0xB4, 0x11, 0x8D,
|
||||||
|
0x18, 0x65, 0x45, 0x91, 0x09, 0xA5, 0x91, 0xAA, 0x89, 0xF9, 0xD5, 0x93,
|
||||||
|
0x70, 0x2C, 0x6F, 0x30, 0xFE, 0xF9, 0xA6, 0x7D, 0x2F, 0xAE, 0xFB, 0xF1,
|
||||||
|
0x73, 0xE7, 0xCF, 0x5E, 0xFC, 0x61, 0xDD, 0x83, 0xAF, 0xCC, 0x59, 0x56,
|
||||||
|
0x29, 0x68, 0x09, 0x17, 0x86, 0x44, 0x06, 0xC6, 0x65, 0x1E, 0x5A, 0xA0,
|
||||||
|
0x95, 0xFC, 0x68, 0xFE, 0xCA, 0xBD, 0xF7, 0xAE, 0xDE, 0x7B, 0xFF, 0xF2,
|
||||||
|
0x17, 0xE6, 0x2D, 0x7F, 0x66, 0xD6, 0xE2, 0xBF, 0x6F, 0xFB, 0xBE, 0xA1,
|
||||||
|
0x1B, 0xDC, 0x60, 0x46, 0x7E, 0x54, 0x8B, 0x45, 0x58, 0x08, 0xD6, 0x2A,
|
||||||
|
0xE8, 0x24, 0x5C, 0x52, 0x48, 0xC3, 0x61, 0x85, 0x5D, 0x22, 0xC8, 0x2D,
|
||||||
|
0x44, 0xB0, 0x83, 0x5C, 0x44, 0xE5, 0x1B, 0x6B, 0x1E, 0xFE, 0xE5, 0xEA,
|
||||||
|
0x6D, 0xB3, 0xA6, 0x54, 0xCF, 0xA8, 0xA9, 0x9A, 0x76, 0x57, 0xED, 0x9C,
|
||||||
|
0x7B, 0x67, 0xF7, 0x0C, 0xDC, 0x14, 0x88, 0x23, 0xA4, 0xA2, 0x04, 0x68,
|
||||||
|
0x7E, 0xD8, 0x72, 0xCB, 0x96, 0x47, 0xEA, 0x6A, 0x2A, 0xE7, 0xCF, 0x9D,
|
||||||
|
0x4B, 0x63, 0xE1, 0xA3, 0x6D, 0x17, 0x6B, 0x1E, 0x5D, 0xA7, 0x68, 0x25,
|
||||||
|
0x18, 0xE9, 0x2C, 0x52, 0x5C, 0x10, 0x2D, 0x29, 0x21, 0x12, 0x98, 0x91,
|
||||||
|
0x9C, 0x00, 0x43, 0x20, 0xB8, 0xF4, 0x40, 0x28, 0x86, 0xC8, 0xFA, 0xD7,
|
||||||
|
0xF6, 0xBE, 0xD3, 0xD9, 0xF1, 0xAF, 0x1B, 0x57, 0xDF, 0x8F, 0xC7, 0x7F,
|
||||||
|
0x77, 0xF4, 0x1D, 0x16, 0x89, 0x3A, 0x66, 0x7A, 0x49, 0xC3, 0xC2, 0xBA,
|
||||||
|
0xE9, 0x33, 0x66, 0x4C, 0xAD, 0x9D, 0x39, 0x75, 0x4A, 0x6D, 0xED, 0xE4,
|
||||||
|
0xE9, 0xB5, 0x35, 0xD3, 0x27, 0x57, 0x7F, 0x76, 0xF6, 0x54, 0xD3, 0x77,
|
||||||
|
0xBE, 0x7D, 0x39, 0x1E, 0x6F, 0xB9, 0xD8, 0x52, 0x8A, 0xF4, 0xFB, 0x1F,
|
||||||
|
0xD9, 0xC0, 0x00, 0xA4, 0x54, 0x97, 0x12, 0x5F, 0x7A, 0x84, 0x67, 0x2B,
|
||||||
|
0x43, 0x60, 0xA9, 0xA4, 0x86, 0xD2, 0xF0, 0x24, 0xDE, 0xF6, 0xD5, 0xCD,
|
||||||
|
0x51, 0x8E, 0x2B, 0x04, 0x29, 0x17, 0x68, 0xFE, 0xEC, 0x7B, 0x2E, 0x9D,
|
||||||
|
0xBF, 0x70, 0xA1, 0xAD, 0xFD, 0x6C, 0x7B, 0xC7, 0x99, 0xD6, 0xCB, 0x27,
|
||||||
|
0x3B, 0xDA, 0xFF, 0xDD, 0xDE, 0xB1, 0x62, 0xF7, 0x93, 0x0A, 0x3F, 0x46,
|
||||||
|
0x1F, 0x77, 0xB4, 0xBD, 0xB0, 0xE7, 0x79, 0x80, 0x06, 0x9A, 0xC7, 0x90,
|
||||||
|
0xE2, 0x40, 0x60, 0x14, 0x0C, 0xE9, 0x27, 0x0E, 0x01, 0x57, 0x7E, 0xAC,
|
||||||
|
0x1C, 0x2E, 0x0C, 0x14, 0xB8, 0x9E, 0xA4, 0x3C, 0x5F, 0xA2, 0x0A, 0x81,
|
||||||
|
0x01, 0x0B, 0xB0, 0x01, 0xE1, 0x62, 0x44, 0x19, 0xC2, 0x21, 0xC2, 0xA2,
|
||||||
|
0x88, 0xE5, 0x09, 0x3C, 0x1E, 0x8B, 0x67, 0x9F, 0xDC, 0x15, 0xAB, 0xBB,
|
||||||
|
0x8B, 0x69, 0x1A, 0x4C, 0x01, 0xAD, 0x5A, 0x06, 0xAB, 0x30, 0xCE, 0xC3,
|
||||||
|
0x0C, 0xBC, 0x06, 0x3E, 0x04, 0x51, 0x95, 0x40, 0xB2, 0xDE, 0x40, 0x73,
|
||||||
|
0xDF, 0x0F, 0xF8, 0xA3, 0x94, 0x85, 0xA5, 0xAF, 0x10, 0x26, 0x7E, 0x32,
|
||||||
|
0x80, 0x0F, 0xA5, 0x10, 0x05, 0x91, 0x90, 0x7A, 0x8F, 0x25, 0x29, 0xC5,
|
||||||
|
0x94, 0x81, 0x12, 0x98, 0x28, 0xD1, 0x46, 0x8D, 0xC0, 0x02, 0x0D, 0x56,
|
||||||
|
0x4B, 0xDF, 0xBA, 0xF2, 0xEB, 0x67, 0x91, 0x9F, 0x49, 0x59, 0xC5, 0x5C,
|
||||||
|
0xC1, 0x83, 0x02, 0x21, 0x88, 0xF9, 0xDF, 0xF8, 0x68, 0x0E, 0x2B, 0x8C,
|
||||||
|
0xCA, 0x20, 0xCE, 0xC6, 0xCF, 0x84, 0x88, 0x61, 0x0D, 0x11, 0x42, 0x03,
|
||||||
|
0x7B, 0xFE, 0x00, 0xBC, 0x3A, 0xF0, 0x40, 0x01, 0x17, 0xC9, 0xAD, 0x24,
|
||||||
|
0x41, 0x44, 0x8A, 0x23, 0x21, 0x80, 0x4A, 0x78, 0x62, 0x0A, 0x38, 0x7C,
|
||||||
|
0x87, 0x3B, 0x45, 0x0A, 0x30, 0x1E, 0x9B, 0x36, 0xB0, 0x57, 0x59, 0x40,
|
||||||
|
0x2C, 0x56, 0x10, 0x55, 0x53, 0x7F, 0x8D, 0xB2, 0xEE, 0x67, 0x17, 0xC4,
|
||||||
|
0x89, 0x7D, 0xB8, 0xCA, 0xB4, 0xBF, 0x0B, 0x02, 0xCC, 0xC6, 0xE2, 0x13,
|
||||||
|
0x85, 0x7C, 0x5D, 0x70, 0x08, 0x56, 0x8F, 0x5A, 0xCC, 0x01, 0xCC, 0x79,
|
||||||
|
0xF2, 0xCD, 0x89, 0xC4, 0x60, 0x32, 0x37, 0xF5, 0xC3, 0x55, 0xB1, 0xD8,
|
||||||
|
0x54, 0x52, 0x9A, 0xDD, 0x48, 0x81, 0x6B, 0x4A, 0x15, 0x45, 0x5C, 0xC9,
|
||||||
|
0x88, 0x94, 0x22, 0xBE, 0x39, 0x6A, 0xB0, 0xB1, 0x16, 0x83, 0x97, 0xCA,
|
||||||
|
0xDF, 0x1D, 0x6B, 0x6A, 0x4C, 0x28, 0x2B, 0x21, 0x8C, 0x8E, 0x45, 0x40,
|
||||||
|
0x7C, 0xBC, 0x82, 0xA3, 0x2C, 0x45, 0x20, 0x1C, 0xA1, 0x8A, 0x22, 0x10,
|
||||||
|
0x47, 0x61, 0xF7, 0xB9, 0x53, 0x0B, 0xB9, 0xB4, 0xE0, 0x15, 0x92, 0x39,
|
||||||
|
0x37, 0x01, 0xBF, 0x48, 0x7C, 0x21, 0x20, 0x46, 0x34, 0xF8, 0x51, 0x94,
|
||||||
|
0x45, 0x00, 0x77, 0x68, 0x46, 0x70, 0xD8, 0x50, 0xDF, 0xBC, 0xC6, 0x98,
|
||||||
|
0x2A, 0x2E, 0xAC, 0xB2, 0x46, 0x41, 0xCB, 0xE6, 0x19, 0xC0, 0x57, 0xF4,
|
||||||
|
0x42, 0x36, 0xC3, 0x6A, 0xA5, 0x4B, 0x96, 0x3A, 0x99, 0xAD, 0xDE, 0xB1,
|
||||||
|
0x9C, 0x38, 0x58, 0x40, 0x04, 0x62, 0x54, 0x79, 0x30, 0x42, 0x10, 0x49,
|
||||||
|
0x71, 0x97, 0x60, 0x06, 0x97, 0xA4, 0x04, 0x28, 0x22, 0x01, 0x64, 0x39,
|
||||||
|
0x4A, 0xB7, 0xCF, 0x1C, 0x93, 0x3A, 0xB3, 0x09, 0x23, 0x8A, 0x68, 0x1C,
|
||||||
|
0xC4, 0x91, 0x23, 0x6A, 0xEC, 0x28, 0x28, 0x29, 0x1E, 0x3B, 0x85, 0x05,
|
||||||
|
0xB7, 0x90, 0xE8, 0x33, 0x6F, 0x7B, 0x90, 0x7C, 0x44, 0x11, 0x0E, 0x7C,
|
||||||
|
0xC1, 0x45, 0x82, 0xFD, 0x81, 0x27, 0xE5, 0x51, 0x22, 0x46, 0x71, 0x17,
|
||||||
|
0xB6, 0x1D, 0x0A, 0x3D, 0x04, 0x7C, 0xDC, 0x51, 0x38, 0x67, 0x48, 0x39,
|
||||||
|
0x93, 0xD8, 0x81, 0x73, 0x51, 0xA2, 0x60, 0x09, 0xA4, 0x10, 0xA0, 0x19,
|
||||||
|
0x26, 0xE2, 0xF4, 0xEE, 0x17, 0x21, 0x2B, 0xA9, 0x6A, 0x0C, 0x4A, 0x74,
|
||||||
|
0xF8, 0x46, 0xFC, 0x30, 0xC0, 0x17, 0xD4, 0x17, 0x85, 0x42, 0x16, 0x18,
|
||||||
|
0xCE, 0x1D, 0xF4, 0x87, 0xB3, 0x1F, 0xF4, 0x48, 0xA8, 0x6B, 0x90, 0x43,
|
||||||
|
0xFA, 0xA9, 0x3B, 0xD6, 0x07, 0x56, 0xE6, 0xB0, 0x4C, 0x3B, 0x0E, 0xD2,
|
||||||
|
0xA8, 0x90, 0x70, 0x82, 0xA9, 0x03, 0x72, 0x04, 0x8B, 0xCD, 0xAB, 0x56,
|
||||||
|
0x24, 0x6F, 0x25, 0x14, 0x48, 0x4A, 0x72, 0x41, 0x93, 0x40, 0x20, 0xDF,
|
||||||
|
0x09, 0x86, 0x1C, 0x0B, 0x30, 0xBE, 0xFF, 0xF4, 0xF3, 0xC7, 0xE3, 0x97,
|
||||||
|
0x06, 0x30, 0xEE, 0xA4, 0xFC, 0x06, 0x75, 0x5C, 0x2C, 0x94, 0x15, 0xE9,
|
||||||
|
0x9F, 0x53, 0x08, 0x79, 0x08, 0xF5, 0x20, 0x57, 0x0B, 0x69, 0xA0, 0x20,
|
||||||
|
0xF4, 0xAF, 0xB4, 0x44, 0xEF, 0x75, 0xB7, 0x35, 0xAE, 0x6D, 0x8C, 0xA6,
|
||||||
|
0xAC, 0x9A, 0x07, 0x1B, 0x8C, 0x82, 0x7C, 0x16, 0x8B, 0x52, 0xA6, 0x63,
|
||||||
|
0x4D, 0xD7, 0xA9, 0x4E, 0xEE, 0x90, 0x08, 0x3B, 0xC1, 0x84, 0xEB, 0x59,
|
||||||
|
0xF1, 0x1E, 0xF3, 0x7A, 0xCF, 0xCF, 0x77, 0x3C, 0xB1, 0x7A, 0xD9, 0x03,
|
||||||
|
0x1B, 0x56, 0x3C, 0xD4, 0xB4, 0x7E, 0xF5, 0xAE, 0x03, 0xFB, 0x7E, 0xDF,
|
||||||
|
0x76, 0xF2, 0x52, 0xA2, 0x37, 0x8D, 0xA5, 0x29, 0x45, 0x3F, 0x91, 0x23,
|
||||||
|
0x94, 0xB4, 0x24, 0x07, 0x18, 0x63, 0x83, 0x14, 0x2D, 0x7B, 0x6C, 0xDD,
|
||||||
|
0x33, 0x9B, 0x9B, 0x36, 0x6E, 0x6F, 0x6A, 0x3E, 0xFE, 0xEE, 0xDD, 0x1B,
|
||||||
|
0xD7, 0x44, 0xCB, 0x8A, 0xB5, 0x70, 0x88, 0x85, 0xC2, 0x9A, 0xA1, 0x23,
|
||||||
|
0xA6, 0x91, 0x20, 0x6C, 0xE1, 0x37, 0x41, 0xA2, 0x31, 0x1A, 0x09, 0x17,
|
||||||
|
0xD6, 0xD7, 0x4E, 0x6C, 0xB8, 0x7B, 0xF6, 0x8E, 0x2D, 0xE5, 0xF5, 0xF5,
|
||||||
|
0x05, 0x05, 0x85, 0xCE, 0xCD, 0xFE, 0x63, 0xAF, 0xBC, 0xFE, 0x93, 0x6F,
|
||||||
|
0xEE, 0xDA, 0xFE, 0xF0, 0x86, 0xA5, 0x2B, 0x97, 0x34, 0xAE, 0xFF, 0xCA,
|
||||||
|
0xAA, 0xF5, 0x8D, 0x07, 0xFF, 0x71, 0x98, 0x76, 0x74, 0x82, 0x4A, 0x6B,
|
||||||
|
0x96, 0x2F, 0x75, 0x5B, 0xE2, 0xAF, 0x36, 0x1F, 0xFA, 0xEE, 0xDA, 0xAD,
|
||||||
|
0xA5, 0x02, 0x17, 0x45, 0xF3, 0xF5, 0xFC, 0x7C, 0xC2, 0x74, 0x1A, 0x62,
|
||||||
|
0xA0, 0x32, 0x33, 0x54, 0xA1, 0x09, 0x4A, 0x34, 0x50, 0x59, 0x6A, 0x94,
|
||||||
|
0x18, 0x91, 0x50, 0x34, 0x2F, 0x9A, 0x5F, 0xD8, 0xB8, 0xE7, 0xA9, 0x3D,
|
||||||
|
0xDB, 0x77, 0xBE, 0xB9, 0xFF, 0xC0, 0xA1, 0xC3, 0x47, 0x0E, 0x9F, 0x38,
|
||||||
|
0x71, 0xFC, 0x4A, 0xCB, 0xCB, 0xBF, 0xF9, 0xD5, 0xEA, 0xA6, 0xA6, 0x09,
|
||||||
|
0xD3, 0xA7, 0x1B, 0x4C, 0xC3, 0xD7, 0x06, 0xFF, 0xF9, 0xD4, 0x5E, 0xF8,
|
||||||
|
0x1B, 0x90, 0x31, 0xD3, 0xE3, 0xC6, 0x4D, 0x38, 0xF6, 0xDF, 0x93, 0x8B,
|
||||||
|
0xAA, 0xEB, 0xA0, 0xF5, 0xEA, 0x02, 0x97, 0x1A, 0x61, 0x6C, 0x68, 0x46,
|
||||||
|
0x58, 0xC7, 0x3A, 0x03, 0x1F, 0x18, 0x32, 0x15, 0xF2, 0x52, 0xA8, 0x36,
|
||||||
|
0x48, 0xC1, 0xAD, 0x0E, 0xBB, 0x43, 0x1A, 0x9C, 0xA0, 0xD3, 0x26, 0x56,
|
||||||
|
0x14, 0x41, 0x67, 0x46, 0xA8, 0x8C, 0xB3, 0x4A, 0xA4, 0x4F, 0x62, 0xD1,
|
||||||
|
0xC5, 0x93, 0xEB, 0x9F, 0x6D, 0xFA, 0x46, 0xF3, 0xFE, 0x37, 0xFE, 0xF6,
|
||||||
|
0xA7, 0xB7, 0x0F, 0x9F, 0x3E, 0xB1, 0xE7, 0xD8, 0x5F, 0xF3, 0x26, 0x55,
|
||||||
|
0x94, 0x16, 0x17, 0x36, 0xBF, 0xF9, 0xDB, 0x7C, 0xAA, 0x81, 0xF2, 0xD0,
|
||||||
|
0x83, 0xA0, 0x14, 0xE0, 0xC4, 0xD4, 0x23, 0x3A, 0x81, 0x3E, 0x6E, 0x30,
|
||||||
|
0x46, 0x82, 0x42, 0xF0, 0x07, 0x54, 0xB7, 0x06, 0xEE, 0x0C, 0x5D, 0xCF,
|
||||||
|
0x0B, 0x45, 0x0B, 0x0B, 0xC2, 0x28, 0x14, 0xE5, 0x50, 0x0B, 0x98, 0xAA,
|
||||||
|
0x76, 0xAA, 0xFA, 0x0C, 0x94, 0x31, 0xE5, 0x32, 0x84, 0xA8, 0x2E, 0x50,
|
||||||
|
0x05, 0x63, 0x0D, 0xD5, 0xB5, 0xAB, 0x76, 0x7F, 0xAF, 0x7E, 0xE9, 0x43,
|
||||||
|
0x11, 0xE2, 0x7F, 0x82, 0x9B, 0x4A, 0x7B, 0x04, 0x9A, 0x46, 0x0A, 0x0B,
|
||||||
|
0x71, 0x88, 0xA9, 0x63, 0x13, 0xD0, 0x13, 0x1C, 0xA4, 0x0F, 0x18, 0xA2,
|
||||||
|
0x12, 0xF4, 0xC0, 0x84, 0x6A, 0xA1, 0x48, 0x2C, 0xCA, 0xE0, 0xCF, 0x0B,
|
||||||
|
0xE6, 0x2A, 0xCF, 0x54, 0xEF, 0x54, 0xC5, 0xC2, 0x54, 0x67, 0x56, 0x9D,
|
||||||
|
0x07, 0xFC, 0x19, 0x82, 0x55, 0x12, 0x63, 0x4E, 0x5D, 0xFD, 0xCC, 0x79,
|
||||||
|
0x73, 0x55, 0xDD, 0x8C, 0x26, 0x0A, 0x94, 0x82, 0x6A, 0x6E, 0xF0, 0x63,
|
||||||
|
0x1A, 0x50, 0x02, 0x7D, 0x16, 0x44, 0xF5, 0x4F, 0x34, 0x75, 0x4C, 0x83,
|
||||||
|
0x7F, 0xD0, 0x80, 0x60, 0x60, 0x29, 0x1A, 0x66, 0x58, 0x95, 0x22, 0x05,
|
||||||
|
0x6B, 0xD0, 0x34, 0x24, 0xE6, 0x18, 0x2E, 0x38, 0xA1, 0x54, 0x36, 0xAB,
|
||||||
|
0x56, 0x2D, 0x84, 0x86, 0xF1, 0x38, 0x66, 0x14, 0x46, 0x62, 0x52, 0xD5,
|
||||||
|
0x8A, 0xAA, 0x18, 0x30, 0x02, 0xFF, 0xBA, 0x00, 0xAA, 0x11, 0x8E, 0x00,
|
||||||
|
0x3B, 0x50, 0x51, 0xD9, 0x0E, 0x08, 0x65, 0x0D, 0xD1, 0x01, 0x1D, 0xD0,
|
||||||
|
0x08, 0xE1, 0x24, 0x80, 0x26, 0x45, 0x74, 0x0D, 0x4E, 0x9A, 0x5C, 0xD3,
|
||||||
|
0x81, 0xCE, 0x8C, 0x7D, 0xE0, 0xEA, 0x84, 0x03, 0x2F, 0xAA, 0xCF, 0x43,
|
||||||
|
0x15, 0x61, 0x5D, 0xB2, 0x58, 0x24, 0x2F, 0x57, 0xDE, 0x40, 0x94, 0x0B,
|
||||||
|
0x20, 0x5C, 0xA9, 0x85, 0xC2, 0x3E, 0xEB, 0x0C, 0x3A, 0x15, 0x63, 0xF4,
|
||||||
|
0xFF, 0xA8, 0x63, 0x0E, 0xBA, 0xF5, 0x11, 0x69, 0x7E, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{"level":"info","time":"2026-06-22T09:52:26+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:11","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":false,"time":"2026-06-22T09:52:26+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"warn","time":"2026-06-22T09:52:26+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:15","message":"未检测到管理员权限,尝试提权运行"}
|
||||||
|
{"level":"debug","exe_path":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T09:52:26+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:66","message":"获取自身路径"}
|
||||||
|
{"level":"info","exe":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T09:52:26+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:31","message":"尝试以管理员身份运行"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:29+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:57","message":"管理员提权成功,当前进程将退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:11","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":true,"time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:19","message":"管理员权限已确认"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:21","message":"开始连接虚拟热点"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:9","message":"连接虚拟热点开始"}
|
||||||
|
{"level":"info","node":"tcp://magpie.snoware.sa-tale.eu.cc:11010","network":"scu_team","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:18","message":"构建 EasyTier 连接命令"}
|
||||||
|
{"level":"debug","command":"easytier-core --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:19","message":"EasyTier 完整命令"}
|
||||||
|
{"level":"info","host":"magpie.snoware.sa-tale.eu.cc","port":"11010","network":"scu_team","time":"2026-06-22T09:52:30+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:27","message":"启动 EasyTier 进程"}
|
||||||
|
{"level":"info","pid":14612,"time":"2026-06-22T09:52:31+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:34","message":"EasyTier 进程已启动"}
|
||||||
|
{"level":"info","pid":14612,"time":"2026-06-22T09:52:31+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:39","message":"终止 EasyTier 进程"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:31+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:24","message":"启动系统托盘"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:52:31+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:29","message":"系统托盘就绪"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:54:11+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:40","message":"用户点击退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T09:54:11+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:58","message":"程序退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:38+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:14","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":false,"time":"2026-06-22T10:22:38+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"warn","time":"2026-06-22T10:22:38+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:18","message":"未检测到管理员权限,尝试提权运行"}
|
||||||
|
{"level":"debug","exe_path":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T10:22:38+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:66","message":"获取自身路径"}
|
||||||
|
{"level":"info","exe":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T10:22:38+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:31","message":"尝试以管理员身份运行"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:57","message":"管理员提权成功,当前进程将退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:14","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":true,"time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:22","message":"管理员权限已确认"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:24","message":"开始连接虚拟热点"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:10","message":"连接虚拟热点开始"}
|
||||||
|
{"level":"info","node":"tcp://magpie.snoware.sa-tale.eu.cc:11010","network":"scu_team","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:18","message":"构建 EasyTier 连接命令"}
|
||||||
|
{"level":"debug","command":"./easytier-core.exe --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:19","message":"EasyTier 完整命令"}
|
||||||
|
{"level":"info","host":"magpie.snoware.sa-tale.eu.cc","port":"11010","network":"scu_team","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:28","message":"启动 EasyTier 进程"}
|
||||||
|
{"level":"info","pid":35460,"time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:35","message":"EasyTier 进程已启动"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:41+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:26","message":"启动系统托盘"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:22:42+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:31","message":"系统托盘就绪"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:14","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":false,"time":"2026-06-22T10:23:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"warn","time":"2026-06-22T10:23:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:18","message":"未检测到管理员权限,尝试提权运行"}
|
||||||
|
{"level":"debug","exe_path":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T10:23:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:66","message":"获取自身路径"}
|
||||||
|
{"level":"info","exe":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T10:23:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:31","message":"尝试以管理员身份运行"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:56+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:57","message":"管理员提权成功,当前进程将退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:14","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":true,"time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:22","message":"管理员权限已确认"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:24","message":"开始连接虚拟热点"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:10","message":"连接虚拟热点开始"}
|
||||||
|
{"level":"info","node":"tcp://magpie.snoware.sa-tale.eu.cc:11010","network":"scu_team","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:18","message":"构建 EasyTier 连接命令"}
|
||||||
|
{"level":"debug","command":"./easytier-core.exe --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:19","message":"EasyTier 完整命令"}
|
||||||
|
{"level":"info","host":"magpie.snoware.sa-tale.eu.cc","port":"11010","network":"scu_team","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:28","message":"启动 EasyTier 进程"}
|
||||||
|
{"level":"error","error":"exec: \"./easytier-core.exe --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d\": executable file not found in %PATH%","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:32","message":"EasyTier 启动失败"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:26","message":"启动系统托盘"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:23:57+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:31","message":"系统托盘就绪"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:51+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:14","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":false,"time":"2026-06-22T10:28:51+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"warn","time":"2026-06-22T10:28:51+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:18","message":"未检测到管理员权限,尝试提权运行"}
|
||||||
|
{"level":"debug","exe_path":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T10:28:51+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:66","message":"获取自身路径"}
|
||||||
|
{"level":"info","exe":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T10:28:51+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:31","message":"尝试以管理员身份运行"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:57","message":"管理员提权成功,当前进程将退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:14","message":"程序启动"}
|
||||||
|
{"level":"debug","has_admin":true,"time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:26","message":"检查管理员权限"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:22","message":"管理员权限已确认"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:24","message":"开始连接虚拟热点"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:10","message":"连接虚拟热点开始"}
|
||||||
|
{"level":"info","node":"tcp://magpie.snoware.sa-tale.eu.cc:11010","network":"scu_team","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:18","message":"构建 EasyTier 连接命令"}
|
||||||
|
{"level":"debug","command":"./easytier-core.exe --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:19","message":"EasyTier 完整命令"}
|
||||||
|
{"level":"info","host":"magpie.snoware.sa-tale.eu.cc","port":"11010","network":"scu_team","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:28","message":"启动 EasyTier 进程"}
|
||||||
|
{"level":"debug","work_dir":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:35","message":"如果Easytier启动失败,请检查当前工作目录。"}
|
||||||
|
{"level":"error","error":"exec: \"./easytier-core.exe --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d\": executable file not found in %PATH%","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:38","message":"EasyTier 启动失败"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:26","message":"启动系统托盘"}
|
||||||
|
{"level":"info","time":"2026-06-22T10:28:55+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:31","message":"系统托盘就绪"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:17","message":"程序启动"}
|
||||||
|
{"level":"debug","time":"2026-06-22T11:18:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/console_windows.go:27","message":"控制台窗口已隐藏"}
|
||||||
|
{"level":"debug","has_admin":false,"time":"2026-06-22T11:18:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:30","message":"检查管理员权限"}
|
||||||
|
{"level":"warn","time":"2026-06-22T11:18:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:24","message":"未检测到管理员权限,尝试提权运行"}
|
||||||
|
{"level":"debug","exe_path":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T11:18:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:72","message":"获取自身路径"}
|
||||||
|
{"level":"info","exe":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\srcc.exe","time":"2026-06-22T11:18:40+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:36","message":"尝试以管理员身份运行"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:43+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:63","message":"管理员提权成功,当前进程将退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:17","message":"程序启动"}
|
||||||
|
{"level":"debug","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/console_windows.go:27","message":"控制台窗口已隐藏"}
|
||||||
|
{"level":"debug","has_admin":true,"time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/Perm.go:30","message":"检查管理员权限"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:28","message":"管理员权限已确认"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:30","message":"开始连接虚拟热点"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:15","message":"连接虚拟热点开始"}
|
||||||
|
{"level":"info","node":"tcp://magpie.snoware.sa-tale.eu.cc:11010","network":"scu_team","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:20","message":"构建 EasyTier 连接命令"}
|
||||||
|
{"level":"debug","command":"./easytier-core.exe --network-name scu_team --network-secret @5cut0t --peers tcp://magpie.snoware.sa-tale.eu.cc:11010 -d","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/EasyTier.go:21","message":"EasyTier 完整命令"}
|
||||||
|
{"level":"info","host":"magpie.snoware.sa-tale.eu.cc","port":"11010","network":"scu_team","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:35","message":"启动 EasyTier 进程"}
|
||||||
|
{"level":"debug","work_dir":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient","time":"2026-06-22T11:18:44+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:44","message":"工作目录"}
|
||||||
|
{"level":"error","path":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\easytier-core.exe","time":"2026-06-22T11:18:45+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/DoConnect.go:53","message":"easytier-core.exe 不存在,请确保该文件与 srcc.exe 在同一目录"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:45+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:36","message":"启动系统托盘"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:18:45+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:42","message":"系统托盘就绪"}
|
||||||
|
{"level":"debug","file":"C:\\Users\\sakeen\\Desktop\\SRemoteConnectClient\\image.png","time":"2026-06-22T11:18:45+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/trayicon_windows.go:24","message":"从文件读取图标"}
|
||||||
|
{"level":"debug","size":1919,"time":"2026-06-22T11:18:45+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/trayicon_windows.go:47","message":"托盘图标已生成 (ICO 32x32)"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:20:53+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:63","message":"用户点击退出"}
|
||||||
|
{"level":"info","time":"2026-06-22T11:20:54+08:00","caller":"C:/Users/sakeen/Desktop/SRemoteConnectClient/main.go:94","message":"程序退出"}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// logger 模块:基于 zerolog 的日志初始化,支持控制台输出(可读格式)+ 文件输出(JSON 结构化)。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// log 包级全局日志实例,所有 .go 文件共用。
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
|
// initLogger 初始化全局日志:
|
||||||
|
// - 日志文件存储在 srcc.exe 同目录的 log/ 下,按日期命名
|
||||||
|
// - 控制台使用可读的 ConsoleWriter(带颜色)
|
||||||
|
// - 同时输出到控制台和文件
|
||||||
|
func initLogger() {
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
exePath, _ = os.Getwd()
|
||||||
|
}
|
||||||
|
exeDir := filepath.Dir(exePath)
|
||||||
|
|
||||||
|
logDir := filepath.Join(exeDir, "log")
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
panic(fmt.Sprintf("无法创建日志目录: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
logFile := filepath.Join(logDir, today+".log")
|
||||||
|
|
||||||
|
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("无法打开日志文件: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stdout,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
NoColor: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
multi := io.MultiWriter(consoleWriter, file)
|
||||||
|
|
||||||
|
log = zerolog.New(multi).
|
||||||
|
With().
|
||||||
|
Timestamp().
|
||||||
|
Caller().
|
||||||
|
Logger().
|
||||||
|
Level(zerolog.DebugLevel)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// main 包:SRemoteConnectClient 主入口,负责 UAC 提权、启动 EasyTier 子进程、系统托盘管理。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/getlantern/systray"
|
||||||
|
)
|
||||||
|
|
||||||
|
// proc 保存 EasyTier 子进程的引用,用于退出时清理。
|
||||||
|
var proc *os.Process = nil
|
||||||
|
|
||||||
|
// main 程序入口:初始化日志 → 隐藏控制台 → UAC 提权 → 启动 EasyTier → 系统托盘。
|
||||||
|
func main() {
|
||||||
|
initLogger()
|
||||||
|
log.Info().Msg("程序启动")
|
||||||
|
|
||||||
|
hideConsoleWindow()
|
||||||
|
|
||||||
|
p := NewPermission()
|
||||||
|
if !p.HasUAC() {
|
||||||
|
log.Warn().Msg("未检测到管理员权限,尝试提权运行")
|
||||||
|
p.runSelfAsAdmin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info().Msg("管理员权限已确认")
|
||||||
|
|
||||||
|
log.Info().Msg("开始连接虚拟热点")
|
||||||
|
proc = connectToVirtualHotspot()
|
||||||
|
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
log.Info().Msg("启动系统托盘")
|
||||||
|
systray.Run(onReady, onExit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// onReady 系统托盘就绪回调:设置图标、工具提示、构建右键菜单、注册各菜单项的点击事件。
|
||||||
|
func onReady() {
|
||||||
|
log.Info().Msg("系统托盘就绪")
|
||||||
|
|
||||||
|
icon := loadIcon()
|
||||||
|
systray.SetIcon(icon)
|
||||||
|
|
||||||
|
systray.SetTooltip("若一切正常,您现可连接到SCU的所有队内服务")
|
||||||
|
|
||||||
|
mTitle := systray.AddMenuItem("<<*>> SCU Remote Connect Client", "已连接")
|
||||||
|
mTitle.Disable()
|
||||||
|
systray.AddSeparator()
|
||||||
|
|
||||||
|
mQuit := systray.AddMenuItem("退出", "退出程序,关闭现有连接")
|
||||||
|
mVisitGitea := systray.AddMenuItem("访问代码托管服务 Gitea", "SASWE 代码托管服务")
|
||||||
|
mVisitCloudreve := systray.AddMenuItem("访问文件分发服务 Cloudreve", "SASWE 小文件分发服务")
|
||||||
|
systray.AddSeparator()
|
||||||
|
mShowConsoleWindow := systray.AddMenuItem("显示控制台窗口", "调试用")
|
||||||
|
mHideConsoleWindow := systray.AddMenuItem("隐藏控制台窗口", "调试用")
|
||||||
|
|
||||||
|
// 退出按钮 → 调用 systray.Quit() 触发 onExit
|
||||||
|
go func() {
|
||||||
|
<-mQuit.ClickedCh
|
||||||
|
log.Info().Msg("用户点击退出")
|
||||||
|
systray.Quit()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 访问 Gitea → 浏览器打开 SASWE 代码托管
|
||||||
|
go func() {
|
||||||
|
<-mVisitGitea.ClickedCh
|
||||||
|
log.Info().Msg("用户点击访问 Gitea")
|
||||||
|
exec.Command("cmd", "/c", "start", "http://103.217.186.98:3000/").Start()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 访问 Cloudreve → 浏览器打开 SASWE 文件分发
|
||||||
|
go func() {
|
||||||
|
<-mVisitCloudreve.ClickedCh
|
||||||
|
log.Info().Msg("用户点击访问 Cloudreve")
|
||||||
|
exec.Command("cmd", "/c", "start", "http://103.217.186.98:5212/").Start()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 显示控制台 → 调用 showConsoleWindow()
|
||||||
|
go func() {
|
||||||
|
<-mShowConsoleWindow.ClickedCh
|
||||||
|
log.Info().Msg("用户点击显示控制台窗口")
|
||||||
|
showConsoleWindow()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 隐藏控制台 → 调用 hideConsoleWindow()
|
||||||
|
go func() {
|
||||||
|
<-mHideConsoleWindow.ClickedCh
|
||||||
|
log.Info().Msg("用户点击隐藏控制台窗口")
|
||||||
|
hideConsoleWindow()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// onExit 系统托盘退出回调:清理子进程并记录退出日志。
|
||||||
|
func onExit() {
|
||||||
|
cleanup()
|
||||||
|
log.Info().Msg("程序退出")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup 终止 EasyTier 子进程,防止残留进程占用端口。
|
||||||
|
func cleanup() {
|
||||||
|
if proc != nil {
|
||||||
|
log.Info().Int("pid", proc.Pid).Msg("终止 EasyTier 进程")
|
||||||
|
if err := proc.Kill(); err != nil {
|
||||||
|
log.Warn().Err(err).Msg("终止 EasyTier 进程失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadIcon 加载托盘图标。
|
||||||
|
// 优先从 image.png 文件读取,回退到内嵌 iconData。
|
||||||
|
// 解码 PNG → 缩放到 32x32 → 重新编码 → 包装为 ICO 格式返回。
|
||||||
|
func loadIcon() []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