This commit is contained in:
2026-06-22 11:33:18 +08:00
commit 9277e4a2b8
24 changed files with 3700 additions and 0 deletions
+360
View File
@@ -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
### 方式 BPowerShell 方式(兼容性好,但依赖 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 | 程序启动时即提权 | 始终以管理员运行,不够灵活 |
+307
View File
@@ -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()
```
+452
View File
@@ -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)
+344
View File
@@ -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 |
+178
View File
@@ -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)
```
+943
View File
@@ -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