├── 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://github.com/MCSManager)
4 | [](https://github.com/MCSManager)
5 | [](https://github.com/MCSManager)
6 |
7 | 仿真终端应用程序,支持运行**所有 Linux/Windows 程序**,可以为您的更高层应用带来完全终端控制能力。
8 |
9 | 中文 | [English](README.md)
10 |
11 |
12 |
13 | 
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://github.com/MCSManager)
5 | [](https://github.com/MCSManager)
6 | [](https://github.com/MCSManager)
7 |
8 |
9 | English | [简体中文](README_CN.md)
10 |
11 |
12 |
13 | ## What is PTY?
14 |
15 |
16 |
17 |
18 | 
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 |
--------------------------------------------------------------------------------