# 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
