├── console ├── winpty │ ├── winpty.dll │ └── winpty-agent.exe ├── iface │ └── iface.go ├── go-winpty │ ├── util.go │ ├── winpty_amd64.go │ ├── defines.go │ └── winpty.go ├── console.go ├── common.go └── console_windows.go ├── main.go ├── .gitignore ├── go.mod ├── cmd └── start │ ├── control_windows.go │ ├── control.go │ ├── control_common.go │ └── start.go ├── LICENSE ├── test └── index.js ├── README_CN.md ├── README.md ├── utils └── coder.go ├── go.sum └── .github └── workflows └── build.yml /console/winpty/winpty.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCSManager/PTY/HEAD/console/winpty/winpty.dll -------------------------------------------------------------------------------- /console/winpty/winpty-agent.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCSManager/PTY/HEAD/console/winpty/winpty-agent.exe -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/MCSManager/pty/cmd/start" 4 | 5 | func main() { 6 | start.Main() 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | !winpty-agent.exe 5 | *.dll 6 | !winpty.dll 7 | *.so 8 | *.dylib 9 | *.log 10 | .idea 11 | # Test binary, built with `go test -c` 12 | *.test 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | pty 16 | pty.exe 17 | mcsm.ico 18 | pty.rc 19 | pty.syso 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | cache/ 23 | *.jar 24 | 25 | server/ 26 | tmp/ 27 | main 28 | 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MCSManager/pty 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.6 6 | 7 | require ( 8 | github.com/Microsoft/go-winio v0.6.2 9 | github.com/creack/pty v1.1.21 10 | github.com/juju/mutex/v2 v2.0.0 11 | github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb 12 | github.com/zijiren233/stream v0.5.2 13 | golang.org/x/term v0.23.0 14 | golang.org/x/text v0.17.0 15 | ) 16 | 17 | require ( 18 | github.com/juju/errors v1.0.0 // indirect 19 | github.com/mattn/go-isatty v0.0.20 // indirect 20 | golang.org/x/crypto v0.26.0 // indirect 21 | golang.org/x/net v0.28.0 // indirect 22 | golang.org/x/sys v0.24.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /console/iface/iface.go: -------------------------------------------------------------------------------- 1 | package iface 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // Console communication interface 9 | type Console interface { 10 | io.Reader 11 | io.Writer 12 | // close the pty and kill the subroutine 13 | io.Closer 14 | 15 | // start pty subroutine 16 | Start(dir string, command []string) error 17 | 18 | // set pty window size 19 | SetSize(cols uint, rows uint) error 20 | 21 | // ResizeWithString("50,50") 22 | ResizeWithString(sizeText string) error 23 | 24 | // Get pty window size 25 | GetSize() (uint, uint) 26 | 27 | // Add environment variables before start 28 | AddENV(environ []string) error 29 | 30 | // Get the process id of the pty subprogram 31 | Pid() int 32 | 33 | // wait for the pty subroutine to exit 34 | Wait() (*os.ProcessState, error) 35 | 36 | // Force kill pty subroutine,try to kill all child processes 37 | Kill() error 38 | 39 | // Send system signals to pty subroutines 40 | Signal(sig os.Signal) error 41 | 42 | StdIn() io.Writer 43 | 44 | StdOut() io.Reader 45 | 46 | // nil in unix 47 | StdErr() io.Reader 48 | } 49 | -------------------------------------------------------------------------------- /cmd/start/control_windows.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | pty "github.com/MCSManager/pty/console" 9 | 10 | winio "github.com/Microsoft/go-winio" 11 | ) 12 | 13 | // \\.\pipe\mypipe 14 | func runControl(fifo string, con pty.Console) error { 15 | n, err := winio.ListenPipe(fifo, &winio.PipeConfig{}) 16 | if err != nil { 17 | return fmt.Errorf("open fifo error: %w", err) 18 | } 19 | defer n.Close() 20 | 21 | if testFifoResize { 22 | go func() { 23 | time.Sleep(time.Second * 5) 24 | _ = testWinResize(fifo) 25 | }() 26 | } 27 | 28 | for { 29 | conn, err := n.Accept() 30 | if err != nil { 31 | return fmt.Errorf("accept fifo error: %w", err) 32 | } 33 | go func() { 34 | defer conn.Close() 35 | u := newConnUtils(conn, io.Discard) 36 | _ = handleConn(u, con) 37 | }() 38 | } 39 | } 40 | 41 | func testWinResize(fifo string) error { 42 | n, err := winio.DialPipe(fifo, nil) 43 | if err != nil { 44 | return fmt.Errorf("open fifo error: %w", err) 45 | } 46 | u := newConnUtils(nil, n) 47 | return testResize(u) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zijiren & unitwk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/start/control.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package start 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "syscall" 11 | "time" 12 | 13 | pty "github.com/MCSManager/pty/console" 14 | ) 15 | 16 | func runControl(fifo string, con pty.Console) error { 17 | err := os.Remove(fifo) 18 | if err != nil { 19 | if !os.IsNotExist(err) { 20 | return fmt.Errorf("remove fifo error: %w", err) 21 | } 22 | } 23 | if err := syscall.Mkfifo(fifo, 0666); err != nil { 24 | return fmt.Errorf("create fifo error: %w", err) 25 | } 26 | 27 | if testFifoResize { 28 | go func() { 29 | time.Sleep(time.Second * 5) 30 | _ = testUnixResize(fifo) 31 | }() 32 | } 33 | 34 | for { 35 | f, err := os.OpenFile(fifo, os.O_RDONLY, os.ModeNamedPipe) 36 | if err != nil { 37 | return fmt.Errorf("open fifo error: %w", err) 38 | } 39 | defer f.Close() 40 | u := newConnUtils(f, io.Discard) 41 | _ = handleConn(u, con) 42 | } 43 | } 44 | 45 | func testUnixResize(fifo string) error { 46 | n, err := os.OpenFile(fifo, os.O_WRONLY, os.ModeNamedPipe) 47 | if err != nil { 48 | return fmt.Errorf("open fifo error: %w", err) 49 | } 50 | defer n.Close() 51 | u := newConnUtils(nil, n) 52 | return testResize(u) 53 | } 54 | -------------------------------------------------------------------------------- /console/go-winpty/util.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winpty 5 | 6 | import ( 7 | "syscall" 8 | "unicode/utf16" 9 | "unsafe" 10 | ) 11 | 12 | func UTF16PtrToString(p *uint16) string { 13 | var ( 14 | sizeTest uint16 15 | finalStr = make([]uint16, 0) 16 | ) 17 | for { 18 | if *p == uint16(0) { 19 | break 20 | } 21 | 22 | finalStr = append(finalStr, *p) 23 | p = (*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(sizeTest))) 24 | } 25 | return string(utf16.Decode(finalStr[0:])) 26 | } 27 | 28 | func UTF16PtrFromStringArray(s []string) (*uint16, error) { 29 | var r []uint16 30 | 31 | for _, ss := range s { 32 | a, err := syscall.UTF16FromString(ss) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | r = append(r, a...) 38 | } 39 | 40 | r = append(r, 0) 41 | 42 | return &r[0], nil 43 | } 44 | 45 | func GetErrorMessage(err uintptr) string { 46 | msgPtr, _, _ := winpty_error_msg.Call(err) 47 | if msgPtr == uintptr(0) { 48 | return "Unknown Error" 49 | } 50 | return UTF16PtrToString((*uint16)(unsafe.Pointer(msgPtr))) 51 | } 52 | 53 | func GetErrorCode(err uintptr) uint32 { 54 | code, _, _ := winpty_error_code.Call(err) 55 | return uint32(code) 56 | } 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require("child_process"); 2 | const readline = require("readline"); 3 | 4 | // process.chdir("../"); 5 | 6 | // const command = JSON.stringify(['"C:\\Program Files\\Java\\jdk-17.0.2\\bin\\java"', "-jar", "paper-1.18.1-215.jar"]); 7 | // const command = JSON.stringify(["TerrariaServer.exe"]); 8 | const command = JSON.stringify(["java -jar PaperSpigot-1.8.8.jar nogui"]); 9 | 10 | const p = spawn( 11 | "../pty.exe", 12 | [ 13 | "-dir", 14 | "C:\\Users\\zijiren\\Downloads\\Compressed\\MCSManager_v9.5.0_win64\\daemon\\data\\InstanceData\\e0398c751178467f8f0c6858fc4e378d", 15 | "-cmd", 16 | command, 17 | "-size", 18 | "80,80", 19 | "-color", 20 | "-coder", 21 | "GBK", 22 | ], 23 | { 24 | cwd: ".", 25 | stdio: "pipe", 26 | windowsHide: true, 27 | } 28 | ); 29 | 30 | if (!p.pid) throw new Error("[DEBUG] ERR: PID IS NULL"); 31 | console.log("Process started!"); 32 | 33 | p.on("exit", (err) => { 34 | console.log("[DEBUG] OK:", err); 35 | }); 36 | 37 | const rl = readline.createInterface({ 38 | input: p.stdout, 39 | crlfDelay: Infinity, 40 | }); 41 | 42 | rl.on("line", (line = "") => { 43 | console.log("FirstLine:", line); 44 | listen(line); 45 | rl.removeAllListeners(); 46 | }); 47 | 48 | function listen(line) { 49 | // const processInfo = JSON.parse(line); 50 | console.log("PTY SubProcess Info:", line); 51 | p.stdout.on("data", (v = "") => { 52 | process.stdout.write(v); 53 | }); 54 | 55 | process.stdin.on("data", (v) => { 56 | p.stdin.write(v); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /console/go-winpty/winpty_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winpty 5 | 6 | import ( 7 | "fmt" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | func createAgentCfg(flags uint64) (uintptr, error) { 13 | err := winpty_config_new.Find() 14 | if err != nil { 15 | return 0, err 16 | } 17 | var errorPtr uintptr 18 | defer winpty_error_free.Call(errorPtr) 19 | winptyConfigT, _, _ := winpty_config_new.Call(uintptr(flags), uintptr(unsafe.Pointer(errorPtr))) 20 | if winptyConfigT == uintptr(0) { 21 | return 0, fmt.Errorf("unable to create agent config, %s", GetErrorMessage(errorPtr)) 22 | } 23 | 24 | return winptyConfigT, nil 25 | } 26 | 27 | func createSpawnCfg(flags uint32, filePath, cmdline, cwd string, env []string) (uintptr, error) { 28 | var errorPtr uintptr 29 | defer winpty_error_free.Call(errorPtr) 30 | 31 | cmdLineStr, err := syscall.UTF16PtrFromString(cmdline) 32 | if err != nil { 33 | return 0, fmt.Errorf("failed to convert cmd to pointer") 34 | } 35 | 36 | filepath, err := syscall.UTF16PtrFromString(filePath) 37 | if err != nil { 38 | return 0, fmt.Errorf("failed to convert app name to pointer") 39 | } 40 | 41 | cwdStr, err := syscall.UTF16PtrFromString(cwd) 42 | if err != nil { 43 | return 0, fmt.Errorf("failed to convert working directory to pointer") 44 | } 45 | 46 | envStr, err := UTF16PtrFromStringArray(env) 47 | 48 | if err != nil { 49 | return 0, fmt.Errorf("failed to convert cmd to pointer") 50 | } 51 | 52 | spawnCfg, _, _ := winpty_spawn_config_new.Call( 53 | uintptr(flags), 54 | uintptr(unsafe.Pointer(filepath)), 55 | uintptr(unsafe.Pointer(cmdLineStr)), 56 | uintptr(unsafe.Pointer(cwdStr)), 57 | uintptr(unsafe.Pointer(envStr)), 58 | uintptr(unsafe.Pointer(errorPtr)), 59 | ) 60 | 61 | if spawnCfg == uintptr(0) { 62 | return 0, fmt.Errorf("unable to create spawn config, %s", GetErrorMessage(errorPtr)) 63 | } 64 | 65 | return spawnCfg, nil 66 | } 67 | -------------------------------------------------------------------------------- /cmd/start/control_common.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | pty "github.com/MCSManager/pty/console" 9 | "github.com/zijiren233/stream" 10 | ) 11 | 12 | type connUtils struct { 13 | r *stream.Reader 14 | w *stream.Writer 15 | } 16 | 17 | func newConnUtils(r io.Reader, w io.Writer) *connUtils { 18 | return &connUtils{ 19 | r: stream.NewReader(r, stream.BigEndian), 20 | w: stream.NewWriter(w, stream.BigEndian), 21 | } 22 | } 23 | 24 | func (cu *connUtils) ReadMessage() (uint8, []byte, error) { 25 | var ( 26 | length uint16 27 | msgType uint8 28 | ) 29 | data, err := cu.r.U8(&msgType).U16(&length).ReadBytes(int(length)) 30 | return msgType, data, err 31 | } 32 | 33 | func (cu *connUtils) SendMessage(msgType uint8, data any) error { 34 | b, err := json.Marshal(data) 35 | if err != nil { 36 | return err 37 | } 38 | return cu.w.U8(msgType).U16(uint16(len(b))).Bytes(b).Error() 39 | } 40 | 41 | func handleConn(u *connUtils, con pty.Console) error { 42 | for { 43 | t, msg, err := u.ReadMessage() 44 | if err != nil { 45 | return fmt.Errorf("read message error: %w", err) 46 | } 47 | switch t { 48 | case RESIZE: 49 | resize := resizeMsg{} 50 | err := json.Unmarshal(msg, &resize) 51 | if err != nil { 52 | _ = u.SendMessage( 53 | ERROR, 54 | &errorMsg{ 55 | Msg: fmt.Sprintf("unmarshal resize message error: %s", err), 56 | }, 57 | ) 58 | continue 59 | } 60 | err = con.SetSize(resize.Width, resize.Height) 61 | if err != nil { 62 | _ = u.SendMessage( 63 | ERROR, 64 | &errorMsg{ 65 | Msg: fmt.Sprintf("resize error: %s", err), 66 | }, 67 | ) 68 | continue 69 | } 70 | } 71 | } 72 | } 73 | 74 | func testResize(u *connUtils) error { 75 | err := u.SendMessage( 76 | RESIZE, 77 | &resizeMsg{ 78 | Width: 20, 79 | Height: 20, 80 | }, 81 | ) 82 | if err != nil { 83 | return fmt.Errorf("send resize message error: %w", err) 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Pseudo-teletype App 2 | 3 | [![--](https://img.shields.io/badge/Go_Version-1.19-green.svg)](https://github.com/MCSManager) 4 | [![--](https://img.shields.io/badge/Support-Windows/Linux-yellow.svg)](https://github.com/MCSManager) 5 | [![--](https://img.shields.io/badge/License-MIT-red.svg)](https://github.com/MCSManager) 6 | 7 | 仿真终端应用程序,支持运行**所有 Linux/Windows 程序**,可以为您的更高层应用带来完全终端控制能力。 8 | 9 | 中文 | [English](README.md) 10 | 11 |
12 | 13 | ![terminal image](https://user-images.githubusercontent.com/18360009/202891148-e7e5bf63-c4a9-454f-8f62-c91dc594cefa.png) 14 | 15 | 16 |
17 | 18 | > 图片中表示的是,使用仿真终端运行 Minecraft 服务器,并且按下 Tab 键来选取提示。 19 | 20 |
21 | 22 | ## 什么是 PTY/TTY? 23 | 24 | tty = "teletype",pty = "pseudo-teletype" 25 | 26 | 众所周知,程序拥有输入与输出流,但是数据流与显示器之间有一个区别,那便是缺少行和高的排列维度。简而言之,PTY 的中文意义就是伪装设备终端,让我们的程序伪装成一个拥有固定高宽的显示器,接受来自程序的输出内容。 27 | 28 |
29 | 30 | ## 使用 31 | 32 | 开一个 PTY 并执行命令,设置固定窗口大小,IO 流直接转发。 33 | 34 | - 注意:-cmd 接收的是一个数组, 命令的参数以数组的形式传递,且需要序列化,如:`[\"java\",\"-jar\",\"ser.jar\",\"nogui\"]` 35 | 36 | ```bash 37 | go build 38 | ./pty -dir "." -cmd [\"bash\"] -size 50,50 39 | ``` 40 | 41 | 接下来您会得到一个设置好大小宽度的窗口,并且您可以像 SSH 终端一样,进行任何交互。 42 | 43 | ``` 44 | ping google.com 45 | top 46 | htop 47 | ``` 48 | 49 |
50 | 51 | ## 参数: 52 | 53 | ``` 54 | -cmd string 55 | command 56 | -coder string 57 | Coder (default "UTF-8") 58 | -dir string 59 | command work path (default ".") 60 | -size string 61 | Initialize pty size, stdin will be forwarded directly (default "50,50") 62 | -test 63 | Test whether the system environment is pty compatible 64 | ``` 65 | 66 |
67 | 68 | ## 兼容性 69 | 70 | - 支持所有现代主流版本 Linux 系统。 71 | - 支持 Windows 7 到 Windows 11 所有版本系统,包括 Server 系列。 72 | - 支持 windows amd64 / linux amd64 & arm64。 73 | 74 | 75 |
76 | 77 | ## MCSManager 78 | 79 | MCSManager 是一款开源,分布式,开箱即用,支持 Minecraft 和其他控制台应用的程序管理面板。 80 | 81 | 这个程序是专门为了 MCSManager 而设计,您也可以尝试嵌入到您自己的程序中。 82 | 83 | More info: [https://github.com/mcsmanager](https://github.com/mcsmanager) 84 | 85 |
86 | 87 | ## 贡献 88 | 89 | 此程序属于 MCSManager 的最重要的核心功能之一,非必要不新增功能。 90 | 91 | - 如果您想为这个项目提供新功能,那您必须开一个 `issue` 说明此功能,并提供编程思路,我们一起经过讨论后再决定是否开发 92 | 93 | - 如果您是修复 BUG,可以直接提交 PR 并说明情况 94 | 95 |
96 | 97 | ## MIT license 98 | 99 | 遵循 [MIT License](https://opensource.org/licenses/MIT) 开源协议。 100 | 101 | 版权所有 [zijiren233](https://github.com/zijiren233) 和贡献者们。 102 | -------------------------------------------------------------------------------- /console/console.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package console 5 | 6 | import ( 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "syscall" 12 | 13 | "github.com/MCSManager/pty/console/iface" 14 | "github.com/MCSManager/pty/utils" 15 | "github.com/creack/pty" 16 | ) 17 | 18 | var _ iface.Console = (*console)(nil) 19 | 20 | type console struct { 21 | file *os.File 22 | cmd *exec.Cmd 23 | coder utils.CoderType 24 | 25 | stdIn io.Writer 26 | stdOut io.Reader 27 | stdErr io.Reader // nil 28 | 29 | initialCols uint 30 | initialRows uint 31 | 32 | env []string 33 | } 34 | 35 | // start pty subroutine 36 | func (c *console) Start(dir string, command []string) error { 37 | if dir, err := filepath.Abs(dir); err != nil { 38 | return err 39 | } else if err := os.Chdir(dir); err != nil { 40 | return err 41 | } 42 | cmd, err := c.buildCmd(command) 43 | if err != nil { 44 | return err 45 | } 46 | c.cmd = cmd 47 | cmd.Dir = dir 48 | cmd.Env = c.env 49 | f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: uint16(c.initialRows), Cols: uint16(c.initialCols)}) 50 | if err != nil { 51 | return err 52 | } 53 | c.stdIn = utils.DecoderWriter(c.coder, f) 54 | c.stdOut = utils.DecoderReader(c.coder, f) 55 | c.stdErr = nil 56 | c.file = f 57 | return nil 58 | } 59 | 60 | func (c *console) buildCmd(args []string) (*exec.Cmd, error) { 61 | if len(args) == 0 { 62 | return nil, ErrInvalidCmd 63 | } 64 | var err error 65 | if args[0], err = exec.LookPath(args[0]); err != nil { 66 | return nil, err 67 | } 68 | cmd := exec.Command(args[0], args[1:]...) 69 | return cmd, nil 70 | } 71 | 72 | // set pty window size 73 | func (c *console) SetSize(cols uint, rows uint) error { 74 | c.initialRows = rows 75 | c.initialCols = cols 76 | if c.file == nil { 77 | return nil 78 | } 79 | return pty.Setsize(c.file, &pty.Winsize{Cols: uint16(cols), Rows: uint16(rows)}) 80 | } 81 | 82 | // Get the process id of the pty subprogram 83 | func (c *console) Pid() int { 84 | if c.cmd == nil { 85 | return 0 86 | } 87 | 88 | return c.cmd.Process.Pid 89 | } 90 | 91 | func (c *console) findProcess() (*os.Process, error) { 92 | if c.cmd == nil { 93 | return nil, ErrProcessNotStarted 94 | } 95 | return c.cmd.Process, nil 96 | } 97 | 98 | // Force kill pty subroutine 99 | func (c *console) Kill() error { 100 | proc, err := c.findProcess() 101 | if err != nil { 102 | return err 103 | } 104 | // try to kill all child processes 105 | pgid, err := syscall.Getpgid(proc.Pid) 106 | if err != nil { 107 | return proc.Kill() 108 | } 109 | return syscall.Kill(-pgid, syscall.SIGKILL) 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Pseudo-teletype App 3 | 4 | [![--](https://img.shields.io/badge/Go_Version-1.19-green.svg)](https://github.com/MCSManager) 5 | [![--](https://img.shields.io/badge/Support-Windows/Linux-yellow.svg)](https://github.com/MCSManager) 6 | [![--](https://img.shields.io/badge/License-MIT-red.svg)](https://github.com/MCSManager) 7 | 8 | 9 | English | [简体中文](README_CN.md) 10 | 11 |
12 | 13 | ## What is PTY? 14 | 15 | 16 |
17 | 18 | ![terminal image](https://user-images.githubusercontent.com/18360009/202891148-e7e5bf63-c4a9-454f-8f62-c91dc594cefa.png) 19 | 20 | 21 |
22 | 23 | 24 | 25 | tty = "teletype",pty = "pseudo-teletype" 26 | 27 | In UNIX, /dev/tty\* is any device that acts like a "teletype" 28 | 29 | A pty is a pseudotty, a device entry that acts like a terminal to the process reading and writing there, 30 | but is managed by something else. 31 | They first appeared for X Window and screen and the like, 32 | where you needed something that acted like a terminal but could be used from another program. 33 | 34 |
35 | 36 | ## Quickstart 37 | 38 | Start a PTY and set window size. 39 | 40 | - Note: -cmd receives an array, and the parameters of the command are passed in the form of an array and needs to be serialized, such as:`[\"java\",\"-jar\",\"ser.jar\",\"nogui\"]` 41 | 42 | ```bash 43 | go build 44 | ./pty -dir "." -cmd [\"bash\"] -size 50,50 45 | ``` 46 | 47 | You can execute any command, just like the SSH terminal. 48 | 49 | ``` 50 | ping google.com 51 | top 52 | htop 53 | ``` 54 | 55 |
56 | 57 | ## Flags: 58 | 59 | ``` 60 | -cmd string 61 | command 62 | -coder string 63 | Coder (default "UTF-8") 64 | -dir string 65 | command work path (default ".") 66 | -size string 67 | Initialize pty size, stdin will be forwarded directly (default "50,50") 68 | ``` 69 | 70 |
71 | 72 | ## MCSManager 73 | 74 | MCSManager is a Distributed, Docker-supported, Multilingual, and Lightweight control panel for Minecraft server and all console programs. 75 | 76 | This application will provide PTY functionality for MCSManager, 77 | it is specifically designed for MCSManager, 78 | you can also try porting to your own application. 79 | 80 | More info: [https://github.com/mcsmanager/mcsmanager](https://github.com/mcsmanager/mcsmanager) 81 | 82 |
83 | 84 | ## Contributing 85 | 86 | Interested in getting involved? 87 | 88 | - If you want to add a new feature, please create an issue first to describe the new feature, as well as the implementation approach. Once a proposal is accepted, create an implementation of the new features and submit it as a pull request. 89 | - If you are just fixing bugs, you can simply submit PR. 90 | 91 |
92 | 93 | ## MIT license 94 | 95 | Released under the [MIT License](https://opensource.org/licenses/MIT). 96 | 97 | Copyright 2022 [zijiren233](https://github.com/zijiren233) and contributors. 98 | -------------------------------------------------------------------------------- /cmd/start/start.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "runtime" 10 | 11 | pty "github.com/MCSManager/pty/console" 12 | "github.com/MCSManager/pty/utils" 13 | "github.com/zijiren233/go-colorable" 14 | "golang.org/x/term" 15 | ) 16 | 17 | var ( 18 | dir, cmd, coder, ptySize string 19 | cmds []string 20 | fifo string 21 | testFifoResize bool 22 | ) 23 | 24 | type PtyInfo struct { 25 | Pid int `json:"pid"` 26 | } 27 | 28 | func init() { 29 | if runtime.GOOS == "windows" { 30 | flag.StringVar(&cmd, "cmd", "[\"cmd\"]", "command") 31 | } else { 32 | flag.StringVar(&cmd, "cmd", "[\"sh\"]", "command") 33 | } 34 | 35 | flag.StringVar(&coder, "coder", "auto", "Coder") 36 | flag.StringVar(&dir, "dir", ".", "command work path") 37 | flag.StringVar(&ptySize, "size", "80,50", "Initialize pty size, stdin will be forwarded directly") 38 | flag.StringVar(&fifo, "fifo", "", "control FIFO name") 39 | flag.BoolVar(&testFifoResize, "test-fifo-resize", false, "test fifo resize") 40 | } 41 | 42 | func Main() { 43 | flag.Parse() 44 | con, err := newPTY() 45 | if err != nil { 46 | fmt.Printf("[MCSMANAGER-PTY] New pty error: %v\n", err) 47 | return 48 | } 49 | err = con.Start(dir, cmds) 50 | if err != nil { 51 | fmt.Printf("[MCSMANAGER-PTY] Process start error: %v\n", err) 52 | return 53 | } 54 | info, _ := json.Marshal(&PtyInfo{ 55 | Pid: con.Pid(), 56 | }) 57 | fmt.Println(string(info)) 58 | defer con.Close() 59 | if fifo != "" { 60 | go func() { 61 | err := runControl(fifo, con) 62 | if err != nil { 63 | fmt.Println("[MCSMANAGER-PTY] Control error: ", err) 64 | } 65 | }() 66 | } 67 | if err = handleStdIO(con); err != nil { 68 | fmt.Println("[MCSMANAGER-PTY] Handle stdio error: ", err) 69 | } 70 | _, _ = con.Wait() 71 | } 72 | 73 | func newPTY() (pty.Console, error) { 74 | if err := json.Unmarshal([]byte(cmd), &cmds); err != nil { 75 | return nil, fmt.Errorf("unmarshal command error: %w", err) 76 | } 77 | con := pty.New(utils.CoderToType(coder)) 78 | if err := con.ResizeWithString(ptySize); err != nil { 79 | return nil, fmt.Errorf("pty resize error: %w", err) 80 | } 81 | return con, nil 82 | } 83 | 84 | func handleStdIO(c pty.Console) error { 85 | if colorable.IsReaderTerminal(os.Stdin) { 86 | oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 87 | if err != nil { 88 | return fmt.Errorf("make raw error: %w", err) 89 | } 90 | defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() 91 | } 92 | go func() { _, _ = io.Copy(c.StdIn(), os.Stdin) }() 93 | if runtime.GOOS == "windows" && c.StdErr() != nil { 94 | go func() { _, _ = io.Copy(colorable.NewColorableStderr(), c.StdErr()) }() 95 | } 96 | _, ok := c.StdOut().(io.WriterTo) 97 | if !ok { 98 | return fmt.Errorf("StdOut is not io.WriterTo") 99 | } 100 | _, _ = io.Copy(colorable.NewColorableStdout(), c.StdOut()) 101 | return nil 102 | } 103 | 104 | const ( 105 | ERROR uint8 = iota + 2 106 | PING 107 | RESIZE 108 | ) 109 | 110 | type errorMsg struct { 111 | Msg string `json:"msg"` 112 | } 113 | 114 | type resizeMsg struct { 115 | Width uint `json:"width"` 116 | Height uint `json:"height"` 117 | } 118 | -------------------------------------------------------------------------------- /console/common.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/MCSManager/pty/console/iface" 13 | "github.com/MCSManager/pty/utils" 14 | ) 15 | 16 | var ( 17 | ErrProcessNotStarted = errors.New("process has not been started") 18 | ErrInvalidCmd = errors.New("invalid command") 19 | ) 20 | 21 | type Console iface.Console 22 | 23 | // Create a new pty 24 | func New(coder utils.CoderType) Console { 25 | return newNative(coder, 50, 50) 26 | } 27 | 28 | // Create a new pty and initialize the size 29 | func NewWithSize(coder utils.CoderType, Cols, Rows uint) Console { 30 | return newNative(coder, Cols, Rows) 31 | } 32 | 33 | func newNative(coder utils.CoderType, Cols, Rows uint) Console { 34 | if Cols == 0 { 35 | Cols = 50 36 | } 37 | if Rows == 0 { 38 | Rows = 50 39 | } 40 | console := console{ 41 | initialCols: Cols, 42 | initialRows: Rows, 43 | coder: coder, 44 | 45 | file: nil, 46 | } 47 | if runtime.GOOS == "windows" { 48 | console.env = os.Environ() 49 | } else { 50 | console.env = append(os.Environ(), "TERM=xterm-256color") 51 | } 52 | return &console 53 | } 54 | 55 | // Read data from pty console 56 | func (c *console) Read(b []byte) (int, error) { 57 | if c.file == nil { 58 | return 0, ErrProcessNotStarted 59 | } 60 | 61 | return c.StdOut().Read(b) 62 | } 63 | 64 | // Write data to the pty console 65 | func (c *console) Write(b []byte) (int, error) { 66 | if c.file == nil { 67 | return 0, ErrProcessNotStarted 68 | } 69 | 70 | return c.StdIn().Write(b) 71 | } 72 | 73 | func (c *console) StdIn() io.Writer { 74 | return c.stdIn 75 | } 76 | 77 | func (c *console) StdOut() io.Reader { 78 | return c.stdOut 79 | } 80 | 81 | // nil in unix 82 | func (c *console) StdErr() io.Reader { 83 | return c.stdErr 84 | } 85 | 86 | // Add environment variables before start 87 | func (c *console) AddENV(environ []string) error { 88 | c.env = append(c.env, environ...) 89 | return nil 90 | } 91 | 92 | // close the pty and kill the subroutine 93 | func (c *console) Close() error { 94 | if c.file == nil { 95 | return ErrProcessNotStarted 96 | } 97 | 98 | return c.file.Close() 99 | } 100 | 101 | // wait for the pty subroutine to exit 102 | func (c *console) Wait() (*os.ProcessState, error) { 103 | proc, err := c.findProcess() 104 | if err != nil { 105 | return nil, err 106 | } 107 | return proc.Wait() 108 | } 109 | 110 | // Send system signals to pty subroutines 111 | func (c *console) Signal(sig os.Signal) error { 112 | proc, err := c.findProcess() 113 | if err != nil { 114 | return err 115 | } 116 | 117 | return proc.Signal(sig) 118 | } 119 | 120 | // ResizeWithString("50,50") 121 | func (c *console) ResizeWithString(sizeText string) error { 122 | arr := strings.Split(sizeText, ",") 123 | if len(arr) != 2 { 124 | return fmt.Errorf("the parameter is incorrect") 125 | } 126 | cols, err1 := strconv.Atoi(arr[0]) 127 | rows, err2 := strconv.Atoi(arr[1]) 128 | if err1 != nil || err2 != nil { 129 | return fmt.Errorf("failed to set window size") 130 | } 131 | if cols < 0 || rows < 0 { 132 | return fmt.Errorf("failed to set window size") 133 | } 134 | return c.SetSize(uint(cols), uint(rows)) 135 | } 136 | 137 | // Get pty window size 138 | func (c *console) GetSize() (uint, uint) { 139 | return c.initialCols, c.initialRows 140 | } 141 | -------------------------------------------------------------------------------- /console/go-winpty/defines.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winpty 5 | 6 | import ( 7 | "path/filepath" 8 | "syscall" 9 | ) 10 | 11 | const ( 12 | WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN = 1 13 | WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN = 2 14 | 15 | WINPTY_FLAG_CONERR = 0x1 16 | WINPTY_FLAG_PLAIN_OUTPUT = 0x2 17 | WINPTY_FLAG_COLOR_ESCAPES = 0x4 18 | WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION = 0x8 19 | 20 | WINPTY_MOUSE_MODE_NONE = 0 21 | WINPTY_MOUSE_MODE_AUTO = 1 22 | WINPTY_MOUSE_MODE_FORCE = 2 23 | ) 24 | 25 | var ( 26 | modWinPTY *syscall.LazyDLL 27 | kernel32 *syscall.LazyDLL 28 | // Error handling... 29 | winpty_error_code *syscall.LazyProc 30 | winpty_error_msg *syscall.LazyProc 31 | winpty_error_free *syscall.LazyProc 32 | 33 | // Configuration of a new agent. 34 | winpty_config_new *syscall.LazyProc 35 | winpty_config_free *syscall.LazyProc 36 | winpty_config_set_initial_size *syscall.LazyProc 37 | winpty_config_set_mouse_mode *syscall.LazyProc 38 | winpty_config_set_agent_timeout *syscall.LazyProc 39 | 40 | // Start the agent. 41 | winpty_open *syscall.LazyProc 42 | winpty_agent_process *syscall.LazyProc 43 | 44 | // I/O Pipes 45 | winpty_conin_name *syscall.LazyProc 46 | winpty_conout_name *syscall.LazyProc 47 | winpty_conerr_name *syscall.LazyProc 48 | 49 | // Agent RPC Calls 50 | winpty_spawn_config_new *syscall.LazyProc 51 | winpty_spawn_config_free *syscall.LazyProc 52 | winpty_spawn *syscall.LazyProc 53 | winpty_set_size *syscall.LazyProc 54 | winpty_free *syscall.LazyProc 55 | 56 | //windows api 57 | GetProcessId *syscall.LazyProc 58 | ) 59 | 60 | func setupKernel32() { 61 | if kernel32 != nil { 62 | return 63 | } 64 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 65 | GetProcessId = kernel32.NewProc("GetProcessId") 66 | } 67 | 68 | func setupDefines(dllPrefix string) { 69 | 70 | if modWinPTY != nil { 71 | return 72 | } 73 | 74 | modWinPTY = syscall.NewLazyDLL(filepath.Join(dllPrefix, `winpty.dll`)) 75 | // Error handling... 76 | winpty_error_code = modWinPTY.NewProc("winpty_error_code") 77 | winpty_error_msg = modWinPTY.NewProc("winpty_error_msg") 78 | winpty_error_free = modWinPTY.NewProc("winpty_error_free") 79 | 80 | // Configuration of a new agent. 81 | winpty_config_new = modWinPTY.NewProc("winpty_config_new") 82 | winpty_config_free = modWinPTY.NewProc("winpty_config_free") 83 | winpty_config_set_initial_size = modWinPTY.NewProc("winpty_config_set_initial_size") 84 | winpty_config_set_mouse_mode = modWinPTY.NewProc("winpty_config_set_mouse_mode") 85 | winpty_config_set_agent_timeout = modWinPTY.NewProc("winpty_config_set_agent_timeout") 86 | 87 | // Start the agent. 88 | winpty_open = modWinPTY.NewProc("winpty_open") 89 | winpty_agent_process = modWinPTY.NewProc("winpty_agent_process") 90 | 91 | // I/O Pipes 92 | winpty_conin_name = modWinPTY.NewProc("winpty_conin_name") 93 | winpty_conout_name = modWinPTY.NewProc("winpty_conout_name") 94 | winpty_conerr_name = modWinPTY.NewProc("winpty_conerr_name") 95 | 96 | // Agent RPC Calls 97 | winpty_spawn_config_new = modWinPTY.NewProc("winpty_spawn_config_new") 98 | winpty_spawn_config_free = modWinPTY.NewProc("winpty_spawn_config_free") 99 | winpty_spawn = modWinPTY.NewProc("winpty_spawn") 100 | winpty_set_size = modWinPTY.NewProc("winpty_set_size") 101 | winpty_free = modWinPTY.NewProc("winpty_free") 102 | } 103 | -------------------------------------------------------------------------------- /utils/coder.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "golang.org/x/text/encoding" 8 | "golang.org/x/text/encoding/japanese" 9 | "golang.org/x/text/encoding/korean" 10 | "golang.org/x/text/encoding/simplifiedchinese" 11 | "golang.org/x/text/encoding/traditionalchinese" 12 | "golang.org/x/text/encoding/unicode" 13 | "golang.org/x/text/transform" 14 | ) 15 | 16 | type CoderType int 17 | 18 | const ( 19 | T_Auto CoderType = iota 20 | T_UTF8 21 | T_GBK 22 | T_Big5 23 | T_ShiftJIS 24 | T_EUCKR 25 | T_GB18030 26 | T_UTF16_L 27 | T_UTF16_B 28 | ) 29 | 30 | var chcp = map[CoderType]string{ 31 | T_UTF8: "65001", T_Auto: "65001", 32 | T_UTF16_L: "1200", T_UTF16_B: "1200", 33 | T_GBK: "936", 34 | T_GB18030: "54936", 35 | T_Big5: "950", 36 | T_EUCKR: "949", 37 | T_ShiftJIS: "932", 38 | } 39 | 40 | func CodePage(types CoderType) string { 41 | if cp, ok := chcp[types]; ok { 42 | return cp 43 | } else { 44 | return "65001" 45 | } 46 | } 47 | 48 | func CoderToType(types string) CoderType { 49 | types = strings.ToUpper(types) 50 | switch types { 51 | case "GBK": 52 | return T_GBK 53 | case "BIG5", "BIG5-HKSCS": 54 | return T_Big5 55 | case "SHIFTJIS": 56 | return T_ShiftJIS 57 | case "KS_C_5601": 58 | return T_EUCKR 59 | case "GB18030", "GB2312": 60 | return T_GB18030 61 | case "UTF-16", "UTF-16-L": 62 | return T_UTF16_L 63 | case "UTF-16-B": 64 | return T_UTF16_B 65 | case "AUTO": 66 | return T_Auto 67 | default: 68 | return T_UTF8 69 | } 70 | } 71 | 72 | func DecoderReader(types CoderType, r io.Reader) io.Reader { 73 | t := newDecoder(types) 74 | if t == nil { 75 | return r 76 | } 77 | return transform.NewReader(r, newDecoder(types)) 78 | } 79 | 80 | func DecoderWriter(types CoderType, r io.Writer) io.Writer { 81 | t := newDecoder(types) 82 | if t == nil { 83 | return r 84 | } 85 | return transform.NewWriter(r, t) 86 | } 87 | 88 | func EncoderReader(types CoderType, r io.Reader) io.Reader { 89 | t := newEecoder(types) 90 | if t == nil { 91 | return r 92 | } 93 | return transform.NewReader(r, t) 94 | } 95 | 96 | func EncoderWriter(types CoderType, r io.Writer) io.Writer { 97 | t := newEecoder(types) 98 | if t == nil { 99 | return r 100 | } 101 | return transform.NewWriter(r, t) 102 | } 103 | 104 | func newDecoder(coder CoderType) *encoding.Decoder { 105 | var decoder *encoding.Decoder 106 | switch coder { 107 | case T_GBK: 108 | decoder = simplifiedchinese.GBK.NewDecoder() 109 | case T_Big5: 110 | decoder = traditionalchinese.Big5.NewDecoder() 111 | case T_ShiftJIS: 112 | decoder = japanese.ShiftJIS.NewDecoder() 113 | case T_EUCKR: 114 | decoder = korean.EUCKR.NewDecoder() 115 | case T_GB18030: 116 | decoder = simplifiedchinese.GB18030.NewDecoder() 117 | case T_UTF16_L: 118 | decoder = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder() 119 | case T_UTF16_B: 120 | decoder = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder() 121 | default: 122 | } 123 | return decoder 124 | } 125 | 126 | func newEecoder(coder CoderType) *encoding.Encoder { 127 | var encoder *encoding.Encoder 128 | switch coder { 129 | case T_GBK: 130 | encoder = simplifiedchinese.GBK.NewEncoder() 131 | case T_Big5: 132 | encoder = traditionalchinese.Big5.NewEncoder() 133 | case T_ShiftJIS: 134 | encoder = japanese.ShiftJIS.NewEncoder() 135 | case T_EUCKR: 136 | encoder = korean.EUCKR.NewEncoder() 137 | case T_GB18030: 138 | encoder = simplifiedchinese.GB18030.NewEncoder() 139 | case T_UTF16_L: 140 | encoder = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() 141 | case T_UTF16_B: 142 | encoder = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder() 143 | default: 144 | } 145 | return encoder 146 | } 147 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 2 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 3 | github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 4 | github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 5 | github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= 6 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 7 | github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= 8 | github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= 9 | github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271 h1:4R626WTwa7pRYQFiIRLVPepMhm05eZMEx+wIurRnMLc= 10 | github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY= 11 | github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= 12 | github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= 13 | github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 h1:NO5tuyw++EGLnz56Q8KMyDZRwJwWO8jQnj285J3FOmY= 14 | github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg= 15 | github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 h1:/WiCm+Vpj87e4QWuWwPD/bNE9kDrWCLvPBHOQNcG2+A= 16 | github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= 17 | github.com/juju/mutex/v2 v2.0.0 h1:rVmJdOaXGWF8rjcFHBNd4x57/1tks5CgXHx55O55SB0= 18 | github.com/juju/mutex/v2 v2.0.0/go.mod h1:jwCfBs/smYDaeZLqeaCi8CB8M+tOes4yf827HoOEoqk= 19 | github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= 20 | github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= 21 | github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 h1:XEDzpuZb8Ma7vLja3+5hzUqVTvAqm5Y+ygvnDs5iTMM= 22 | github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494/go.mod h1:rUquetT0ALL48LHZhyRGvjjBH8xZaZ8dFClulKK5wK4= 23 | github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a h1:5ZWDCeCF0RaITrZGemzmDFIhjR/MVSvBUqgSyaeTMbE= 24 | github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a/go.mod h1:LzwbbEN7buYjySp4nqnti6c6olSqRXUk6RkbSUUP1n8= 25 | github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23 h1:wtEPbidt1VyHlb8RSztU6ySQj29FLsOQiI9XiJhXDM4= 26 | github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23/go.mod h1:Ljlbryh9sYaUSGXucslAEDf0A2XUSGvDbHJgW8ps6nc= 27 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 28 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 32 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 33 | github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb h1:0DyOxf/TbbGodHhOVHNoPk+7v/YBJACs22gKpKlatWw= 34 | github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb/go.mod h1:6TCzjDiQ8+5gWZiwsC3pnA5M0vUy2jV2Y7ciHJh729g= 35 | github.com/zijiren233/stream v0.5.2 h1:K8xPvXtETH7qo9P99xdvi7q0MXALfxb1XBtzpz/Zn0A= 36 | github.com/zijiren233/stream v0.5.2/go.mod h1:iIrOm3qgIepQFmptD/HDY+YzamSSzQOtPjpVcK7FCOw= 37 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 38 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 39 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 40 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 41 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 43 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 45 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 46 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 47 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 48 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 50 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 51 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 52 | -------------------------------------------------------------------------------- /console/console_windows.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "embed" 5 | _ "embed" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | 14 | "github.com/MCSManager/pty/console/go-winpty" 15 | "github.com/MCSManager/pty/console/iface" 16 | "github.com/MCSManager/pty/utils" 17 | mutex "github.com/juju/mutex/v2" 18 | ) 19 | 20 | //go:embed all:winpty 21 | var winpty_embed embed.FS 22 | 23 | var _ iface.Console = (*console)(nil) 24 | 25 | type console struct { 26 | file *winpty.WinPTY 27 | coder utils.CoderType 28 | 29 | stdIn io.Writer 30 | stdOut io.Reader 31 | stdErr io.Reader 32 | 33 | initialCols uint 34 | initialRows uint 35 | 36 | env []string 37 | } 38 | 39 | // start pty subroutine 40 | func (c *console) Start(dir string, command []string) error { 41 | r, err := mutex.Acquire(mutex.Spec{Name: "pty-winpty-lock", Timeout: time.Second * 15, Delay: time.Millisecond * 5, Clock: &fakeClock{}}) 42 | if err != nil { 43 | return err 44 | } 45 | defer r.Release() 46 | if dir, err = filepath.Abs(dir); err != nil { 47 | return err 48 | } 49 | if err := os.Chdir(dir); err != nil { 50 | return err 51 | } 52 | dllDir, err := c.findDll() 53 | if err != nil { 54 | return err 55 | } 56 | cmd, err := c.buildCmd(command) 57 | if err != nil { 58 | return err 59 | } 60 | option := winpty.Options{ 61 | DllDir: dllDir, 62 | Command: cmd, 63 | Dir: dir, 64 | Env: c.env, 65 | InitialCols: uint32(c.initialCols), 66 | InitialRows: uint32(c.initialRows), 67 | } 68 | 69 | // creat stderr 70 | option.AgentFlags = winpty.WINPTY_FLAG_CONERR | winpty.WINPTY_FLAG_COLOR_ESCAPES 71 | 72 | var pty *winpty.WinPTY 73 | if pty, err = winpty.OpenWithOptions(option); err != nil { 74 | return err 75 | } 76 | c.stdIn = pty.Stdin 77 | c.stdOut = pty.Stdout 78 | c.stdErr = pty.Stderr 79 | c.file = pty 80 | return nil 81 | } 82 | 83 | func (c *console) buildCmd(args []string) (string, error) { 84 | if len(args) == 0 { 85 | return "", ErrInvalidCmd 86 | } 87 | var cmds = fmt.Sprintf( 88 | "cmd /C chcp %s > nul & %s", 89 | utils.CodePage(c.coder), 90 | strings.Join(args, " "), 91 | ) 92 | return cmds, nil 93 | } 94 | 95 | type fakeClock struct { 96 | delay time.Duration 97 | } 98 | 99 | func (f *fakeClock) After(time.Duration) <-chan time.Time { 100 | return time.After(f.delay) 101 | } 102 | 103 | func (f *fakeClock) Now() time.Time { 104 | return time.Now() 105 | } 106 | 107 | func (c *console) findDll() (string, error) { 108 | dllDir := filepath.Join(os.TempDir(), "pty_winpty") 109 | 110 | if err := os.MkdirAll(dllDir, os.ModePerm); err != nil { 111 | return "", err 112 | } 113 | 114 | dir, err := winpty_embed.ReadDir("winpty") 115 | if err != nil { 116 | return "", fmt.Errorf("read embed dir error: %w", err) 117 | } 118 | 119 | for _, de := range dir { 120 | info, err := de.Info() 121 | if err != nil { 122 | return "", err 123 | } 124 | var exist bool 125 | df, err := os.Stat(filepath.Join(dllDir, de.Name())) 126 | if err != nil { 127 | if !os.IsNotExist(err) { 128 | return "", err 129 | } 130 | } else { 131 | if !df.ModTime().Before(info.ModTime()) { 132 | exist = true 133 | } 134 | } 135 | if !exist { 136 | data, err := winpty_embed.ReadFile(fmt.Sprintf("winpty/%s", de.Name())) 137 | if err != nil { 138 | return "", fmt.Errorf("read embed file error: %w", err) 139 | } 140 | if err := os.WriteFile(filepath.Join(dllDir, de.Name()), data, os.ModePerm); err != nil { 141 | return "", fmt.Errorf("write file error: %w", err) 142 | } 143 | } 144 | } 145 | 146 | return dllDir, nil 147 | } 148 | 149 | // set pty window size 150 | func (c *console) SetSize(cols uint, rows uint) error { 151 | c.initialRows = rows 152 | c.initialCols = cols 153 | if c.file == nil { 154 | return nil 155 | } 156 | err := c.file.SetSize(uint32(c.initialCols), uint32(c.initialRows)) 157 | // Error special handling 158 | if err.Error() != "The operation completed successfully." { 159 | return err 160 | } 161 | return nil 162 | } 163 | 164 | // Get the process id of the pty subprogram 165 | func (c *console) Pid() int { 166 | if c.file == nil { 167 | return 0 168 | } 169 | 170 | return c.file.Pid() 171 | } 172 | 173 | func (c *console) findProcess() (*os.Process, error) { 174 | if c.file == nil { 175 | return nil, ErrProcessNotStarted 176 | } 177 | return os.FindProcess(c.Pid()) 178 | } 179 | 180 | // Force kill pty subroutine 181 | func (c *console) Kill() error { 182 | _, err := c.findProcess() 183 | if err != nil { 184 | return err 185 | } 186 | // try to kill all child processes 187 | return exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprint(c.Pid())).Run() 188 | } 189 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: 1.22 18 | 19 | - name: Build 20 | run: | 21 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_win32_x64.exe 22 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_x64 23 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_arm64 24 | CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_arm 25 | CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_386 26 | CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_mips 27 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_mips64 28 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_mips64le 29 | CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_mipsle 30 | CGO_ENABLED=0 GOOS=linux GOARCH=ppc64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_ppc64 31 | CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_ppc64le 32 | CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_riscv64 33 | CGO_ENABLED=0 GOOS=linux GOARCH=s390x go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_linux_s390x 34 | CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_netbsd_386 35 | CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_netbsd_x64 36 | CGO_ENABLED=0 GOOS=netbsd GOARCH=arm go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_netbsd_arm 37 | CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_netbsd_arm64 38 | CGO_ENABLED=0 GOOS=openbsd GOARCH=386 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_openbsd_386 39 | CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_openbsd_x64 40 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_openbsd_arm 41 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_openbsd_arm64 42 | CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_freebsd_386 43 | CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_freebsd_x64 44 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_freebsd_arm 45 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_freebsd_arm64 46 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_darwin_x64 47 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o pty_darwin_arm64 48 | 49 | - uses: "marvinpinto/action-automatic-releases@latest" 50 | with: 51 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 52 | automatic_release_tag: "latest" 53 | title: Development Build 54 | prerelease: true 55 | files: | 56 | pty_win32_x64.exe 57 | pty_linux_x64 58 | pty_linux_arm64 59 | pty_linux_arm 60 | pty_linux_386 61 | pty_linux_mips 62 | pty_linux_mips64 63 | pty_linux_mips64le 64 | pty_linux_mipsle 65 | pty_linux_ppc64 66 | pty_linux_ppc64le 67 | pty_linux_riscv64 68 | pty_linux_s390x 69 | pty_netbsd_386 70 | pty_netbsd_x64 71 | pty_netbsd_arm 72 | pty_netbsd_arm64 73 | pty_openbsd_386 74 | pty_openbsd_x64 75 | pty_openbsd_arm 76 | pty_openbsd_arm64 77 | pty_freebsd_386 78 | pty_freebsd_x64 79 | pty_freebsd_arm 80 | pty_freebsd_arm64 81 | pty_darwin_x64 82 | pty_darwin_arm64 83 | -------------------------------------------------------------------------------- /console/go-winpty/winpty.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winpty 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | type Options struct { 14 | // DllDir is the path to winpty.dll and winpty-agent.exe 15 | DllDir string 16 | // FilePath sets the title of the console 17 | FilePath string 18 | // Command is the full command to launch 19 | Command string 20 | // Dir sets the current working directory for the command 21 | Dir string 22 | // Env sets the environment variables. Use the format VAR=VAL. 23 | Env []string 24 | // AgentFlags to pass to agent config creation 25 | AgentFlags uint64 26 | SpawnFlag uint32 27 | MouseModes uint 28 | // Initial size for Columns and Rows 29 | InitialCols uint32 30 | InitialRows uint32 31 | agentTimeoutMs *uint64 32 | } 33 | 34 | type WinPTY struct { 35 | Stdin *os.File 36 | Stdout *os.File 37 | Stderr *os.File 38 | pty uintptr 39 | procHandle uintptr 40 | closed bool 41 | } 42 | 43 | // the same as open, but uses defaults for Env 44 | func OpenDefault(dllPrefix, cmd, dir string) (*WinPTY, error) { 45 | var flag uint64 = WINPTY_FLAG_COLOR_ESCAPES 46 | // flag = flag | WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION 47 | return OpenWithOptions(Options{ 48 | DllDir: dllPrefix, 49 | Command: cmd, 50 | Dir: dir, 51 | Env: os.Environ(), 52 | AgentFlags: flag, 53 | }) 54 | } 55 | 56 | func setOptsDefaultValues(options *Options) { 57 | if options.InitialCols < 5 { 58 | options.InitialCols = 5 59 | } 60 | if options.InitialRows < 5 { 61 | options.InitialRows = 5 62 | } 63 | if options.agentTimeoutMs == nil { 64 | t := uint64(syscall.INFINITE) 65 | options.agentTimeoutMs = &t 66 | } 67 | if options.SpawnFlag != 1 && options.SpawnFlag != 2 { 68 | options.SpawnFlag = 1 69 | } 70 | if options.MouseModes >= 3 { 71 | options.MouseModes = 0 72 | } 73 | } 74 | 75 | func OpenWithOptions(options Options) (*WinPTY, error) { 76 | setOptsDefaultValues(&options) 77 | setupDefines(options.DllDir) 78 | // create config with specified AgentFlags 79 | winptyConfigT, err := createAgentCfg(options.AgentFlags) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | winpty_config_set_initial_size.Call(winptyConfigT, uintptr(options.InitialCols), uintptr(options.InitialRows)) 85 | SetMouseMode(winptyConfigT, options.MouseModes) 86 | 87 | var openErr uintptr 88 | defer winpty_error_free.Call(openErr) 89 | pty, _, _ := winpty_open.Call(winptyConfigT, uintptr(unsafe.Pointer(openErr))) 90 | 91 | if pty == uintptr(0) { 92 | return nil, fmt.Errorf("error Launching WinPTY agent, %s", GetErrorMessage(openErr)) 93 | } 94 | 95 | SetAgentTimeout(winptyConfigT, *options.agentTimeoutMs) 96 | winpty_config_free.Call(winptyConfigT) 97 | 98 | stdinName, _, _ := winpty_conin_name.Call(pty) 99 | stdoutName, _, _ := winpty_conout_name.Call(pty) 100 | stderrName, _, _ := winpty_conerr_name.Call(pty) 101 | 102 | obj := &WinPTY{} 103 | 104 | stdinHandle, err := syscall.CreateFile((*uint16)(unsafe.Pointer(stdinName)), syscall.GENERIC_WRITE, 0, nil, syscall.OPEN_EXISTING, 0, 0) 105 | if err != nil { 106 | return nil, fmt.Errorf("error getting stdin handle. %s", err) 107 | } 108 | obj.Stdin = os.NewFile(uintptr(stdinHandle), "stdin") 109 | 110 | stdoutHandle, err := syscall.CreateFile((*uint16)(unsafe.Pointer(stdoutName)), syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING, 0, 0) 111 | if err != nil { 112 | return nil, fmt.Errorf("error getting stdout handle. %s", err) 113 | } 114 | obj.Stdout = os.NewFile(uintptr(stdoutHandle), "stdout") 115 | 116 | if options.AgentFlags&WINPTY_FLAG_CONERR == WINPTY_FLAG_CONERR { 117 | stderrHandle, err := syscall.CreateFile((*uint16)(unsafe.Pointer(stderrName)), syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING, 0, 0) 118 | if err != nil { 119 | return nil, fmt.Errorf("error getting stderr handle. %s", err) 120 | } 121 | obj.Stderr = os.NewFile(uintptr(stderrHandle), "stderr") 122 | } 123 | 124 | spawnCfg, err := createSpawnCfg(options.SpawnFlag, options.FilePath, options.Command, options.Dir, options.Env) 125 | if err != nil { 126 | return nil, err 127 | } 128 | var ( 129 | spawnErr uintptr 130 | lastError *uint32 131 | ) 132 | spawnRet, _, _ := winpty_spawn.Call(pty, spawnCfg, uintptr(unsafe.Pointer(&obj.procHandle)), uintptr(0), uintptr(unsafe.Pointer(lastError)), uintptr(unsafe.Pointer(spawnErr))) 133 | _, _, _ = winpty_spawn_config_free.Call(spawnCfg) 134 | defer winpty_error_free.Call(spawnErr) 135 | 136 | if spawnRet == 0 { 137 | return nil, fmt.Errorf("error spawning process") 138 | } else { 139 | obj.pty = pty 140 | return obj, nil 141 | } 142 | } 143 | 144 | func (pty *WinPTY) Pid() int { 145 | setupKernel32() 146 | pid, _, _ := GetProcessId.Call(pty.procHandle) 147 | return int(pid) 148 | } 149 | 150 | // set windows size 151 | func (pty *WinPTY) SetSize(wsCol, wsRow uint32) error { 152 | if wsCol == 0 || wsRow == 0 { 153 | return fmt.Errorf("wsCol or wsRow = 0") 154 | } 155 | _, _, err := winpty_set_size.Call(pty.pty, uintptr(wsCol), uintptr(wsRow), uintptr(0)) 156 | return err 157 | } 158 | 159 | // close proc 160 | func (pty *WinPTY) Close() error { 161 | if pty.closed { 162 | return nil 163 | } 164 | winpty_free.Call(pty.pty) 165 | pty.Stdin.Close() 166 | pty.Stdout.Close() 167 | if pty.Stderr != nil { 168 | pty.Stderr.Close() 169 | } 170 | err := syscall.CloseHandle(syscall.Handle(pty.procHandle)) 171 | if err != nil { 172 | return err 173 | } 174 | pty.closed = true 175 | return nil 176 | 177 | } 178 | 179 | // get pty sub proc 180 | func (pty *WinPTY) GetProcHandle() uintptr { 181 | return pty.procHandle 182 | } 183 | 184 | // get pty proc 185 | func (pty *WinPTY) GetAgentProcHandle() uintptr { 186 | agentProcH, _, _ := winpty_agent_process.Call(pty.pty) 187 | return agentProcH 188 | } 189 | 190 | // set pty timeout 191 | func SetAgentTimeout(winptyConfigT uintptr, timeoutMs uint64) { 192 | winpty_config_set_agent_timeout.Call(winptyConfigT, uintptr(timeoutMs)) 193 | } 194 | 195 | // set pty mouse mode 196 | func SetMouseMode(winptyConfigT uintptr, mode uint) { 197 | winpty_config_set_mouse_mode.Call(winptyConfigT, uintptr(mode)) 198 | } 199 | --------------------------------------------------------------------------------