| 1 | //go:build !windows |
| 2 | |
| 3 | package persistentshell |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "os/exec" |
| 8 | "sync" |
| 9 | "syscall" |
| 10 | |
| 11 | "github.com/creack/pty" |
| 12 | |
| 13 | "reasonix/internal/proc" |
| 14 | ) |
| 15 | |
| 16 | type unixPTY struct { |
| 17 | file *os.File |
| 18 | cmd *exec.Cmd |
| 19 | closeOnce sync.Once |
| 20 | } |
| 21 | |
| 22 | func startPTY(argv []string, dir string, env []string) (ptyConn, error) { |
| 23 | if len(argv) == 0 { |
| 24 | return nil, errEmptyArgv |
| 25 | } |
| 26 | cmd := proc.Command(argv[0], argv[1:]...) |
| 27 | cmd.Dir = dir |
| 28 | cmd.Env = env |
| 29 | file, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 24, Cols: 80}) |
| 30 | if err != nil { |
| 31 | return nil, err |
| 32 | } |
| 33 | // Reap the shell without blocking: its exit surfaces to callers as a read |
| 34 | // error on the master side, so the wait status itself carries no decision. |
| 35 | go func() { _ = cmd.Wait() }() |
| 36 | return &unixPTY{file: file, cmd: cmd}, nil |
| 37 | } |
| 38 | |
| 39 | func (c *unixPTY) Read(p []byte) (int, error) { |
| 40 | return c.file.Read(p) |
| 41 | } |
| 42 | |
| 43 | func (c *unixPTY) Write(p []byte) (int, error) { |
| 44 | return c.file.Write(p) |
| 45 | } |
| 46 | |
| 47 | func (c *unixPTY) Close() error { |
| 48 | var closeErr error |
| 49 | c.closeOnce.Do(func() { |
| 50 | if c.cmd != nil && c.cmd.Process != nil { |
| 51 | _ = syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL) |
| 52 | } |
| 53 | if c.file != nil { |
| 54 | closeErr = c.file.Close() |
| 55 | } |
| 56 | }) |
| 57 | return closeErr |
| 58 | } |
| 59 |