b1.0
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user