Files
SRCC/docs/winapi-process-launch.md
T
2026-06-22 11:33:18 +08:00

944 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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