# 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