Files
HomeAgent/internal/plugins/agentcli/pty_windows.go
root a62f5ba0fa agentcli: Windows ConPTY 适配分支(原生实现替代剔除桩)+ cabi loader.c build tag 修复
- agentcli 平台无关化:plugin.go 拆为公共层 + ptyTerm 接口,新增
  pty_linux.go(/dev/ptmx 原实现搬移,行为不变)与 pty_windows.go
  (ConPTY 原生实现,CreatePseudoConsole + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
  注入子进程,双管道读写/ResizePseudoConsole/GetExitCodeProcess,零第三方依赖)
- plugin_stub.go 收紧为 !linux && !windows(darwin 等仍走桩)
- plugin_test.go build tag 放宽到 linux || windows
- cabi/loader.c 补 linux || darwin build tag,修复 Windows 下
  cgo 禁用时残留 C 源文件的编译错误
- 验证:Linux 全量构建+测试通过;Windows CGO 交叉编译 + go vet 全包零错误
2026-08-07 12:32:45 +08:00

288 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build windows
package agentcli
import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"unsafe"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procCreatePseudoConsole = kernel32.NewProc("CreatePseudoConsole")
procResizePseudoConsole = kernel32.NewProc("ResizePseudoConsole")
procClosePseudoConsole = kernel32.NewProc("ClosePseudoConsole")
procInitializeProcThreadAttributeList = kernel32.NewProc("InitializeProcThreadAttributeList")
procUpdateProcThreadAttribute = kernel32.NewProc("UpdateProcThreadAttribute")
procDeleteProcThreadAttributeList = kernel32.NewProc("DeleteProcThreadAttributeList")
procCreateProcessW = kernel32.NewProc("CreateProcessW")
procGetExitCodeProcess = kernel32.NewProc("GetExitCodeProcess")
procTerminateProcess = kernel32.NewProc("TerminateProcess")
procCloseHandle = kernel32.NewProc("CloseHandle")
)
const (
procThreadAttributePseudoConsole = 0x16 // PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE (22)
extendedStartupinfoPresent = 0x00080000
createUnicodeEnvironment = 0x00000400
stillActive = 259 // STILL_ACTIVE
)
type coord struct {
x int16
y int16
}
type processInformation struct {
process syscall.Handle
thread syscall.Handle
pid uint32
tid uint32
}
// startupInfoEx 对应 STARTUPINFOEXWSTARTUPINFOW 之后追加 attribute list 指针。
type startupInfoEx struct {
cb uint32
lpReserved *uint16
lpDesktop *uint16
lpTitle *uint16
dwX uint32
dwY uint32
dwXSize uint32
dwYSize uint32
dwXCountChars uint32
dwYCountChars uint32
dwFillAttribute uint32
dwFlags uint32
wShowWindow uint16
cbReserved2 uint16
lpReserved2 *byte
hStdInput syscall.Handle
hStdOutput syscall.Handle
hStdErr syscall.Handle
lpAttributeList uintptr
}
func defaultShell() string { return "cmd.exe" }
// windowsPty 基于 Windows ConPTYPseudo Console的终端后端。
//
// ConPTY 通过 CreatePseudoConsole 创建伪控制台,子进程以
// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 挂到伪控制台。宿主侧使用两根
// 管道与伪控制台通信:我们写 inW输入、读 outR输出
type windowsPty struct {
hpc syscall.Handle // 伪控制台句柄
inW *os.File // 我们向伪控制台写输入
outR *os.File // 我们读伪控制台输出
proc syscall.Handle // 子进程句柄
procID int
cmd *exec.Cmd
attrList []byte
closeOnce sync.Once
}
// newCommandPty 创建 ConPTY 并在其上运行命令cmd.exe /c <command>)。
func newCommandPty(command string, rows, cols uint16) (ptyTerm, *exec.Cmd, error) {
inR, inW, err := os.Pipe()
if err != nil {
return nil, nil, fmt.Errorf("create input pipe: %w", err)
}
outR, outW, err := os.Pipe()
if err != nil {
inR.Close()
inW.Close()
return nil, nil, fmt.Errorf("create output pipe: %w", err)
}
sz := coord{x: int16(cols), y: int16(rows)}
var hpc syscall.Handle
r, _, e := procCreatePseudoConsole.Call(
uintptr(unsafe.Pointer(&sz)),
inW.Fd(),
outR.Fd(),
0,
uintptr(unsafe.Pointer(&hpc)),
)
if r == 0 {
inR.Close()
inW.Close()
outR.Close()
outW.Close()
return nil, nil, fmt.Errorf("CreatePseudoConsole: %v", e)
}
// 初始化 process thread attribute list 并注入伪控制台句柄
attrList, err := buildAttrList(hpc)
if err != nil {
procClosePseudoConsole.Call(uintptr(hpc))
inR.Close()
inW.Close()
outR.Close()
outW.Close()
return nil, nil, err
}
cmdLine := windowsCommandLine(command)
cli, err := syscall.UTF16PtrFromString(cmdLine)
if err != nil {
return nil, nil, err
}
var si startupInfoEx
si.cb = uint32(unsafe.Sizeof(si))
si.lpAttributeList = uintptr(unsafe.Pointer(&attrList[0]))
var pi processInformation
flags := uint32(extendedStartupinfoPresent | createUnicodeEnvironment)
r, _, e = procCreateProcessW.Call(
0, // 应用名
uintptr(unsafe.Pointer(cli)), // 命令行CreateProcessW 会就地改写,可写 buffer
0, 0, // 无安全属性
0, // bInheritHandles FALSE
uintptr(flags), // 创建标志
0, // 环境(继承)
0, // 工作目录
uintptr(unsafe.Pointer(&si)),
uintptr(unsafe.Pointer(&pi)),
)
if r == 0 {
procDeleteProcThreadAttributeList.Call(uintptr(unsafe.Pointer(&attrList[0])))
procClosePseudoConsole.Call(uintptr(hpc))
inR.Close()
inW.Close()
outR.Close()
outW.Close()
return nil, nil, fmt.Errorf("CreateProcessW: %v", e)
}
// 子进程无需 pipe 的父侧副本;我们只保留 inW/outR
inR.Close()
outW.Close()
cmdObj := exec.Command("cmd.exe")
cmdObj.Process = &os.Process{Pid: int(pi.pid)}
pt := &windowsPty{
hpc: hpc,
inW: inW,
outR: outR,
proc: pi.process,
procID: int(pi.pid),
cmd: cmdObj,
attrList: attrList,
}
return pt, cmdObj, nil
}
func buildAttrList(hpc syscall.Handle) ([]byte, error) {
var size uintptr
r, _, e := procInitializeProcThreadAttributeList.Call(0, 1, 0, uintptr(unsafe.Pointer(&size)))
if r == 0 || size == 0 {
return nil, fmt.Errorf("InitializeProcThreadAttributeList(size): %v", e)
}
buf := make([]byte, size)
r, _, e = procInitializeProcThreadAttributeList.Call(
uintptr(unsafe.Pointer(&buf[0])),
1,
0,
uintptr(unsafe.Pointer(&size)),
)
if r == 0 {
return nil, fmt.Errorf("InitializeProcThreadAttributeList: %v", e)
}
r, _, e = procUpdateProcThreadAttribute.Call(
uintptr(unsafe.Pointer(&buf[0])),
0,
procThreadAttributePseudoConsole,
uintptr(hpc),
unsafe.Sizeof(hpc),
0,
0,
)
if r == 0 {
procDeleteProcThreadAttributeList.Call(uintptr(unsafe.Pointer(&buf[0])))
return nil, fmt.Errorf("UpdateProcThreadAttribute: %v", e)
}
return buf, nil
}
func windowsCommandLine(command string) string {
return "cmd.exe /c " + command
}
func (p *windowsPty) Read(buf []byte) (int, error) {
return p.outR.Read(buf)
}
func (p *windowsPty) WriteString(s string) (int, error) {
return p.inW.WriteString(s)
}
func (p *windowsPty) Resize(rows, cols uint16) error {
if p.hpc == 0 {
return fmt.Errorf("pseudo console closed")
}
sz := coord{x: int16(cols), y: int16(rows)}
r, _, e := procResizePseudoConsole.Call(uintptr(p.hpc), uintptr(unsafe.Pointer(&sz)))
if r == 0 {
return fmt.Errorf("ResizePseudoConsole: %v", e)
}
return nil
}
func (p *windowsPty) Running() bool {
if p.proc == 0 {
return false
}
var code uint32
r, _, _ := procGetExitCodeProcess.Call(uintptr(p.proc), uintptr(unsafe.Pointer(&code)))
if r == 0 {
// 句柄失效(进程已退出并释放句柄)视为停止
return false
}
return code == stillActive
}
func (p *windowsPty) Kill() error {
if p.proc != 0 {
procTerminateProcess.Call(uintptr(p.proc), 1)
procCloseHandle.Call(uintptr(p.proc))
p.proc = 0
}
return nil
}
func (p *windowsPty) Close() error {
var errs []string
p.closeOnce.Do(func() {
if p.inW != nil {
if err := p.inW.Close(); err != nil {
errs = append(errs, err.Error())
}
}
if p.outR != nil {
if err := p.outR.Close(); err != nil {
errs = append(errs, err.Error())
}
}
if p.hpc != 0 {
procClosePseudoConsole.Call(uintptr(p.hpc))
p.hpc = 0
}
if len(p.attrList) > 0 {
procDeleteProcThreadAttributeList.Call(uintptr(unsafe.Pointer(&p.attrList[0])))
p.attrList = nil
}
_ = p.Kill()
})
if len(errs) > 0 {
return fmt.Errorf("close: %s", strings.Join(errs, "; "))
}
return nil
}