mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
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 全包零错误
This commit is contained in:
@ -1,3 +1,5 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
|
||||||
// HomeAgent C ABI loader — C implementation (compiled alongside Go code via cgo)
|
// HomeAgent C ABI loader — C implementation (compiled alongside Go code via cgo)
|
||||||
|
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
//go:build linux
|
//go:build linux || windows
|
||||||
|
|
||||||
package agentcli
|
package agentcli
|
||||||
|
|
||||||
@ -12,19 +12,11 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PTY ioctl constants for Linux
|
|
||||||
const (
|
|
||||||
TIOCGPTN = 0x80045430
|
|
||||||
TIOCSPTLCK = 0x40045431
|
|
||||||
TIOCSWINSZ = 0x5414
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultTimeout = 5 * time.Minute
|
DefaultTimeout = 5 * time.Minute
|
||||||
ReadBufSize = 4096
|
ReadBufSize = 4096
|
||||||
@ -32,56 +24,31 @@ const (
|
|||||||
NotifyOutputDelay = 500 * time.Millisecond
|
NotifyOutputDelay = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
type winsize struct {
|
// ptyTerm 抽象平台终端后端(Linux PTY / Windows ConPTY)。
|
||||||
Row uint16
|
type ptyTerm interface {
|
||||||
Col uint16
|
Read(buf []byte) (int, error)
|
||||||
XPixel uint16
|
WriteString(s string) (int, error)
|
||||||
YPixel uint16
|
Resize(rows, cols uint16) error
|
||||||
|
Running() bool
|
||||||
|
Kill() error
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func ioctl(fd, cmd uintptr, ptr unsafe.Pointer) error {
|
// terminalRunning 判断终端后端进程是否仍在运行:优先走平台实现,
|
||||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, uintptr(ptr))
|
// 否则回落到 exec.Cmd 的 ProcessState(仅 Linux 传统路径)。
|
||||||
if errno != 0 {
|
func terminalRunning(t *TerminalSession) bool {
|
||||||
return errno
|
if p, ok := t.session.(interface {
|
||||||
|
Running() bool
|
||||||
|
}); ok && p.Running() {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return nil
|
return t.cmd != nil && (t.cmd.ProcessState == nil || !t.cmd.ProcessState.Exited())
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TerminalSession struct {
|
type TerminalSession struct {
|
||||||
id string
|
id string
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
master *os.File
|
session ptyTerm
|
||||||
slave *os.File
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
@ -92,11 +59,33 @@ type TerminalSession struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TerminalSession) Write(input string) (int, error) {
|
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) {
|
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 {
|
func (t *TerminalSession) ReadOutput() string {
|
||||||
@ -127,31 +116,6 @@ func (t *TerminalSession) appendOutput(data []byte) {
|
|||||||
t.buf.Write(data)
|
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 {
|
func (t *TerminalSession) IsExpired() bool {
|
||||||
return time.Since(t.createdAt) >= t.timeout
|
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) {
|
func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (interface{}, error) {
|
||||||
command, _ := args["command"].(string)
|
command, _ := args["command"].(string)
|
||||||
if command == "" {
|
if command == "" {
|
||||||
command = "bash"
|
command = defaultShell()
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutStr, _ := args["timeout"].(string)
|
timeoutStr, _ := args["timeout"].(string)
|
||||||
@ -379,32 +343,14 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in
|
|||||||
cols = uint16(c)
|
cols = uint16(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
master, slave, err := openPty()
|
term, cmd, err := newCommandPty(command, rows, cols)
|
||||||
if err != nil {
|
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{
|
session := &TerminalSession{
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
master: master,
|
session: term,
|
||||||
createdAt: time.Now(),
|
createdAt: time.Now(),
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
@ -415,9 +361,6 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in
|
|||||||
id := p.nextIDLocked()
|
id := p.nextIDLocked()
|
||||||
session.id = id
|
session.id = id
|
||||||
p.sessions[id] = session
|
p.sessions[id] = session
|
||||||
|
|
||||||
ws := winsize{Row: rows, Col: cols}
|
|
||||||
ioctl(uintptr(master.Fd()), TIOCSWINSZ, unsafe.Pointer(&ws))
|
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
@ -510,7 +453,7 @@ func (p *Plugin) handleRead(args map[string]interface{}) (interface{}, error) {
|
|||||||
"status": "ok",
|
"status": "ok",
|
||||||
"terminal": id,
|
"terminal": id,
|
||||||
"output": output,
|
"output": output,
|
||||||
"running": session.cmd.ProcessState == nil || !session.cmd.ProcessState.Exited(),
|
"running": terminalRunning(session),
|
||||||
"uptime": time.Since(session.createdAt).String(),
|
"uptime": time.Since(session.createdAt).String(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -586,7 +529,7 @@ func (p *Plugin) handleList() (interface{}, error) {
|
|||||||
|
|
||||||
var terms []termInfo
|
var terms []termInfo
|
||||||
for _, t := range p.sessions {
|
for _, t := range p.sessions {
|
||||||
running := t.cmd.ProcessState == nil || !t.cmd.ProcessState.Exited()
|
running := terminalRunning(t)
|
||||||
remaining := t.timeout - time.Since(t.createdAt)
|
remaining := t.timeout - time.Since(t.createdAt)
|
||||||
if remaining < 0 {
|
if remaining < 0 {
|
||||||
remaining = 0
|
remaining = 0
|
||||||
@ -631,7 +574,7 @@ func (p *Plugin) readLoop(t *TerminalSession, s *sdk.PluginSDK) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() {
|
if !terminalRunning(t) {
|
||||||
s.InjectText("agentcli", "agentcli", fmt.Sprintf("[终端 %s 中的进程已退出]", t.id))
|
s.InjectText("agentcli", "agentcli", fmt.Sprintf("[终端 %s 中的进程已退出]", t.id))
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
delete(p.sessions, t.id)
|
delete(p.sessions, t.id)
|
||||||
@ -672,7 +615,7 @@ type readResult struct {
|
|||||||
|
|
||||||
func (p *Plugin) reader(t *TerminalSession, buf []byte, ch chan<- readResult) {
|
func (p *Plugin) reader(t *TerminalSession, buf []byte, ch chan<- readResult) {
|
||||||
for {
|
for {
|
||||||
n, err := t.master.Read(buf)
|
n, err := t.session.Read(buf)
|
||||||
select {
|
select {
|
||||||
case ch <- readResult{n, err}:
|
case ch <- readResult{n, err}:
|
||||||
case <-t.stopCh:
|
case <-t.stopCh:
|
||||||
@ -703,7 +646,7 @@ func (p *Plugin) cleanupLoop(s *sdk.PluginSDK) {
|
|||||||
term.Close()
|
term.Close()
|
||||||
}(t)
|
}(t)
|
||||||
}
|
}
|
||||||
if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() {
|
if !terminalRunning(t) {
|
||||||
log.Printf("[agentcli] cleanup: terminal %s process exited", id)
|
log.Printf("[agentcli] cleanup: terminal %s process exited", id)
|
||||||
delete(p.sessions, id)
|
delete(p.sessions, id)
|
||||||
go func(term *TerminalSession) {
|
go func(term *TerminalSession) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
//go:build !linux
|
//go:build !linux && !windows
|
||||||
|
|
||||||
package agentcli
|
package agentcli
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build linux || windows
|
||||||
|
|
||||||
package agentcli
|
package agentcli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
133
internal/plugins/agentcli/pty_linux.go
Normal file
133
internal/plugins/agentcli/pty_linux.go
Normal file
@ -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
|
||||||
|
}
|
||||||
288
internal/plugins/agentcli/pty_windows.go
Normal file
288
internal/plugins/agentcli/pty_windows.go
Normal file
@ -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 <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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user