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

27 KiB
Raw Blame History

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

// 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 基础封装

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

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 实现

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 的对比

当前项目使用 ShellExecuteWShellExecuteExW 增强点:

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

// 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 优雅终止实现

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 统一的进程管理器

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 改造示例

// 改造前
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 改造示例

// 改造前
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