From a62f5ba0fae95b78ff033a3adedd506a475c467d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Aug 2026 12:32:45 +0800 Subject: [PATCH] =?UTF-8?q?agentcli:=20Windows=20ConPTY=20=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=88=86=E6=94=AF=EF=BC=88=E5=8E=9F=E7=94=9F=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=9B=BF=E4=BB=A3=E5=89=94=E9=99=A4=E6=A1=A9=EF=BC=89?= =?UTF-8?q?+=20cabi=20loader.c=20build=20tag=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 全包零错误 --- internal/plugin/cabi/loader.c | 2 + internal/plugins/agentcli/plugin.go | 159 ++++--------- internal/plugins/agentcli/plugin_stub.go | 2 +- internal/plugins/agentcli/plugin_test.go | 2 + internal/plugins/agentcli/pty_linux.go | 133 +++++++++++ internal/plugins/agentcli/pty_windows.go | 288 +++++++++++++++++++++++ 6 files changed, 477 insertions(+), 109 deletions(-) create mode 100644 internal/plugins/agentcli/pty_linux.go create mode 100644 internal/plugins/agentcli/pty_windows.go diff --git a/internal/plugin/cabi/loader.c b/internal/plugin/cabi/loader.c index d1c0cdd..5ac4a11 100644 --- a/internal/plugin/cabi/loader.c +++ b/internal/plugin/cabi/loader.c @@ -1,3 +1,5 @@ +//go:build linux || darwin + // HomeAgent C ABI loader — C implementation (compiled alongside Go code via cgo) #include diff --git a/internal/plugins/agentcli/plugin.go b/internal/plugins/agentcli/plugin.go index 2f53e86..e7dd340 100644 --- a/internal/plugins/agentcli/plugin.go +++ b/internal/plugins/agentcli/plugin.go @@ -1,4 +1,4 @@ -//go:build linux +//go:build linux || windows package agentcli @@ -12,19 +12,11 @@ import ( "sync" "syscall" "time" - "unsafe" "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) -// PTY ioctl constants for Linux -const ( - TIOCGPTN = 0x80045430 - TIOCSPTLCK = 0x40045431 - TIOCSWINSZ = 0x5414 -) - const ( DefaultTimeout = 5 * time.Minute ReadBufSize = 4096 @@ -32,56 +24,31 @@ const ( NotifyOutputDelay = 500 * time.Millisecond ) -type winsize struct { - Row uint16 - Col uint16 - XPixel uint16 - YPixel uint16 +// ptyTerm 抽象平台终端后端(Linux PTY / Windows ConPTY)。 +type ptyTerm interface { + Read(buf []byte) (int, error) + WriteString(s string) (int, error) + Resize(rows, cols uint16) error + Running() bool + Kill() error + Close() error } -func ioctl(fd, cmd uintptr, ptr unsafe.Pointer) error { - _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, uintptr(ptr)) - if errno != 0 { - return errno +// terminalRunning 判断终端后端进程是否仍在运行:优先走平台实现, +// 否则回落到 exec.Cmd 的 ProcessState(仅 Linux 传统路径)。 +func terminalRunning(t *TerminalSession) bool { + if p, ok := t.session.(interface { + Running() bool + }); ok && p.Running() { + return true } - return nil -} - -func openPty() (master *os.File, slave *os.File, err error) { - mfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY, 0) - if err != nil { - return nil, nil, fmt.Errorf("open /dev/ptmx: %w", err) - } - master = os.NewFile(uintptr(mfd), "/dev/ptmx") - - var unlock int32 - if err := ioctl(uintptr(mfd), TIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil { - master.Close() - return nil, nil, fmt.Errorf("TIOCSPTLCK: %w", err) - } - - var ptyno int32 - if err := ioctl(uintptr(mfd), TIOCGPTN, unsafe.Pointer(&ptyno)); err != nil { - master.Close() - return nil, nil, fmt.Errorf("TIOCGPTN: %w", err) - } - - slavePath := fmt.Sprintf("/dev/pts/%d", ptyno) - sfd, err := syscall.Open(slavePath, syscall.O_RDWR|syscall.O_NOCTTY, 0) - if err != nil { - master.Close() - return nil, nil, fmt.Errorf("open slave %s: %w", slavePath, err) - } - slave = os.NewFile(uintptr(sfd), slavePath) - - return master, slave, nil + return t.cmd != nil && (t.cmd.ProcessState == nil || !t.cmd.ProcessState.Exited()) } type TerminalSession struct { id string cmd *exec.Cmd - master *os.File - slave *os.File + session ptyTerm mu sync.Mutex buf bytes.Buffer createdAt time.Time @@ -92,11 +59,33 @@ type TerminalSession struct { } func (t *TerminalSession) Write(input string) (int, error) { - return t.master.WriteString(input) + return t.session.WriteString(input) } func (t *TerminalSession) Read(buf []byte) (int, error) { - return t.master.Read(buf) + return t.session.Read(buf) +} + +func (t *TerminalSession) Resize(rows, cols uint16) error { + return t.session.Resize(rows, cols) +} + +func (t *TerminalSession) Close() { + t.mu.Lock() + if t.closed { + t.mu.Unlock() + return + } + t.closed = true + t.mu.Unlock() + + close(t.stopCh) + t.session.Close() + <-t.done + + if t.cmd != nil && t.cmd.Process != nil { + t.cmd.Process.Kill() + } } func (t *TerminalSession) ReadOutput() string { @@ -127,31 +116,6 @@ func (t *TerminalSession) appendOutput(data []byte) { t.buf.Write(data) } -func (t *TerminalSession) Resize(rows, cols uint16) error { - ws := winsize{Row: rows, Col: cols} - if err := ioctl(uintptr(t.master.Fd()), TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil { - return fmt.Errorf("TIOCSWINSZ: %w", err) - } - return nil -} - -func (t *TerminalSession) Close() { - t.mu.Lock() - if t.closed { - t.mu.Unlock() - return - } - t.closed = true - t.mu.Unlock() - - close(t.stopCh) - t.master.Close() - <-t.done - - t.slave.Close() - t.cmd.Process.Kill() -} - func (t *TerminalSession) IsExpired() bool { return time.Since(t.createdAt) >= t.timeout } @@ -355,7 +319,7 @@ func (p *Plugin) nextIDLocked() string { func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (interface{}, error) { command, _ := args["command"].(string) if command == "" { - command = "bash" + command = defaultShell() } timeoutStr, _ := args["timeout"].(string) @@ -379,32 +343,14 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in cols = uint16(c) } - master, slave, err := openPty() + term, cmd, err := newCommandPty(command, rows, cols) if err != nil { - return map[string]interface{}{"error": fmt.Sprintf("创建 PTY 失败: %v", err)}, nil + return map[string]interface{}{"error": fmt.Sprintf("创建终端失败: %v", err)}, nil } - cmd := exec.Command("sh", "-c", command) - cmd.Stdin = slave - cmd.Stdout = slave - cmd.Stderr = slave - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - Setctty: true, - Ctty: 0, - } - - if err := cmd.Start(); err != nil { - master.Close() - slave.Close() - return map[string]interface{}{"error": fmt.Sprintf("启动命令失败: %v", err)}, nil - } - - slave.Close() - session := &TerminalSession{ cmd: cmd, - master: master, + session: term, createdAt: time.Now(), timeout: timeout, stopCh: make(chan struct{}), @@ -415,9 +361,6 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in id := p.nextIDLocked() session.id = id p.sessions[id] = session - - ws := winsize{Row: rows, Col: cols} - ioctl(uintptr(master.Fd()), TIOCSWINSZ, unsafe.Pointer(&ws)) p.mu.Unlock() p.wg.Add(1) @@ -510,7 +453,7 @@ func (p *Plugin) handleRead(args map[string]interface{}) (interface{}, error) { "status": "ok", "terminal": id, "output": output, - "running": session.cmd.ProcessState == nil || !session.cmd.ProcessState.Exited(), + "running": terminalRunning(session), "uptime": time.Since(session.createdAt).String(), }, nil } @@ -586,7 +529,7 @@ func (p *Plugin) handleList() (interface{}, error) { var terms []termInfo for _, t := range p.sessions { - running := t.cmd.ProcessState == nil || !t.cmd.ProcessState.Exited() + running := terminalRunning(t) remaining := t.timeout - time.Since(t.createdAt) if remaining < 0 { remaining = 0 @@ -631,7 +574,7 @@ func (p *Plugin) readLoop(t *TerminalSession, s *sdk.PluginSDK) { return } - if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() { + if !terminalRunning(t) { s.InjectText("agentcli", "agentcli", fmt.Sprintf("[终端 %s 中的进程已退出]", t.id)) p.mu.Lock() delete(p.sessions, t.id) @@ -672,7 +615,7 @@ type readResult struct { func (p *Plugin) reader(t *TerminalSession, buf []byte, ch chan<- readResult) { for { - n, err := t.master.Read(buf) + n, err := t.session.Read(buf) select { case ch <- readResult{n, err}: case <-t.stopCh: @@ -703,7 +646,7 @@ func (p *Plugin) cleanupLoop(s *sdk.PluginSDK) { term.Close() }(t) } - if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() { + if !terminalRunning(t) { log.Printf("[agentcli] cleanup: terminal %s process exited", id) delete(p.sessions, id) go func(term *TerminalSession) { diff --git a/internal/plugins/agentcli/plugin_stub.go b/internal/plugins/agentcli/plugin_stub.go index 57c2649..a5f4269 100644 --- a/internal/plugins/agentcli/plugin_stub.go +++ b/internal/plugins/agentcli/plugin_stub.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !linux && !windows package agentcli diff --git a/internal/plugins/agentcli/plugin_test.go b/internal/plugins/agentcli/plugin_test.go index c3e575e..fb6afae 100644 --- a/internal/plugins/agentcli/plugin_test.go +++ b/internal/plugins/agentcli/plugin_test.go @@ -1,3 +1,5 @@ +//go:build linux || windows + package agentcli import ( diff --git a/internal/plugins/agentcli/pty_linux.go b/internal/plugins/agentcli/pty_linux.go new file mode 100644 index 0000000..844f707 --- /dev/null +++ b/internal/plugins/agentcli/pty_linux.go @@ -0,0 +1,133 @@ +//go:build linux + +package agentcli + +import ( + "fmt" + "os" + "os/exec" + "syscall" + "unsafe" +) + +// PTY ioctl constants for Linux +const ( + TIOCGPTN = 0x80045430 + TIOCSPTLCK = 0x40045431 + TIOCSWINSZ = 0x5414 +) + +type winsize struct { + Row uint16 + Col uint16 + XPixel uint16 + YPixel uint16 +} + +func ioctl(fd, cmd uintptr, ptr unsafe.Pointer) error { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, uintptr(ptr)) + if errno != 0 { + return errno + } + return nil +} + +func openPty() (master *os.File, slave *os.File, err error) { + mfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY, 0) + if err != nil { + return nil, nil, fmt.Errorf("open /dev/ptmx: %w", err) + } + master = os.NewFile(uintptr(mfd), "/dev/ptmx") + + var unlock int32 + if err := ioctl(uintptr(mfd), TIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil { + master.Close() + return nil, nil, fmt.Errorf("TIOCSPTLCK: %w", err) + } + + var ptyno int32 + if err := ioctl(uintptr(mfd), TIOCGPTN, unsafe.Pointer(&ptyno)); err != nil { + master.Close() + return nil, nil, fmt.Errorf("TIOCGPTN: %w", err) + } + + slavePath := fmt.Sprintf("/dev/pts/%d", ptyno) + sfd, err := syscall.Open(slavePath, syscall.O_RDWR|syscall.O_NOCTTY, 0) + if err != nil { + master.Close() + return nil, nil, fmt.Errorf("open slave %s: %w", slavePath, err) + } + slave = os.NewFile(uintptr(sfd), slavePath) + + return master, slave, nil +} + +func defaultShell() string { return "bash" } + +// linuxPty 基于 Linux PTY 的终端后端。 +type linuxPty struct { + master *os.File + slave *os.File + cmd *exec.Cmd +} + +func (p *linuxPty) Read(buf []byte) (int, error) { return p.master.Read(buf) } + +func (p *linuxPty) WriteString(s string) (int, error) { return p.master.WriteString(s) } + +func (p *linuxPty) Resize(rows, cols uint16) error { + ws := winsize{Row: rows, Col: cols} + if err := ioctl(uintptr(p.master.Fd()), TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil { + return fmt.Errorf("TIOCSWINSZ: %w", err) + } + return nil +} + +// Running 在 Linux 上保持旧语义:进程退出通过 master EOF 由 readLoop/cleanup 感知, +// 因此这里恒返回 true,行为与改造前一致。 +func (p *linuxPty) Running() bool { return true } + +func (p *linuxPty) Kill() error { + if p.cmd != nil && p.cmd.Process != nil { + return p.cmd.Process.Kill() + } + return nil +} + +func (p *linuxPty) Close() error { + p.slave.Close() + return p.master.Close() +} + +// newCommandPty 创建 PTY 并在其上启动子命令(sh -c)。 +func newCommandPty(command string, rows, cols uint16) (ptyTerm, *exec.Cmd, error) { + master, slave, err := openPty() + if err != nil { + return nil, nil, err + } + + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = slave + cmd.Stdout = slave + cmd.Stderr = slave + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + Setctty: true, + Ctty: 0, + } + + if err := cmd.Start(); err != nil { + master.Close() + slave.Close() + return nil, nil, fmt.Errorf("start command: %w", err) + } + + slave.Close() + pt := &linuxPty{master: master, cmd: cmd} + if err := pt.Resize(rows, cols); err != nil { + master.Close() + cmd.Process.Kill() + return nil, nil, fmt.Errorf("resize pty: %w", err) + } + return pt, cmd, nil +} \ No newline at end of file diff --git a/internal/plugins/agentcli/pty_windows.go b/internal/plugins/agentcli/pty_windows.go new file mode 100644 index 0000000..b342c8e --- /dev/null +++ b/internal/plugins/agentcli/pty_windows.go @@ -0,0 +1,288 @@ +//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 对应 STARTUPINFOEXW:STARTUPINFOW 之后追加 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 ConPTY(Pseudo 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 )。 +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 +} \ No newline at end of file