feat(pty): unify pty adapter across linux and darwin
- commit
- 68625c9655e58c0bb4c016ec00981c6aa34429fc
- author
- brice · · 2026-07-16 17:49 UTC
- parent
- acf9d96
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +131 -14
A internal/adapters/pty/pty_darwin_test.go +117 -0
| @@ -0,0 +1,117 @@ | |||
| 1 | + | //go:build darwin | |
| 2 | + | ||
| 3 | + | package pty_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "context" | |
| 8 | + | "io" | |
| 9 | + | "os" | |
| 10 | + | "strings" | |
| 11 | + | "testing" | |
| 12 | + | "time" | |
| 13 | + | ||
| 14 | + | "github.com/bnema/vev/internal/adapters/pty" | |
| 15 | + | "github.com/bnema/vev/internal/domain" | |
| 16 | + | "github.com/bnema/vev/internal/ports" | |
| 17 | + | "github.com/stretchr/testify/require" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | func readAllDarwin(t *testing.T, p ports.PTY, deadline time.Duration) []byte { | |
| 21 | + | t.Helper() | |
| 22 | + | type result struct { | |
| 23 | + | output []byte | |
| 24 | + | err error | |
| 25 | + | } | |
| 26 | + | resultCh := make(chan result, 1) | |
| 27 | + | go func() { | |
| 28 | + | var output bytes.Buffer | |
| 29 | + | _, err := io.Copy(&output, p) | |
| 30 | + | resultCh <- result{output: output.Bytes(), err: err} | |
| 31 | + | }() | |
| 32 | + | ||
| 33 | + | select { | |
| 34 | + | case result := <-resultCh: | |
| 35 | + | require.NoError(t, result.err) | |
| 36 | + | return result.output | |
| 37 | + | case <-time.After(deadline): | |
| 38 | + | t.Fatal("timed out reading from pty") | |
| 39 | + | return nil | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) { | |
| 44 | + | if testing.Short() { | |
| 45 | + | t.Skip("skipping pty integration test in -short mode") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | factory := pty.NewFactory() | |
| 49 | + | outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 50 | + | require.NoError(t, err) | |
| 51 | + | t.Cleanup(func() { _ = outputPTY.Close() }) | |
| 52 | + | require.Equal(t, "hello", string(readAllDarwin(t, outputPTY, 5*time.Second))) | |
| 53 | + | ||
| 54 | + | echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 55 | + | require.NoError(t, err) | |
| 56 | + | t.Cleanup(func() { _ = echoPTY.Close() }) | |
| 57 | + | _, err = io.WriteString(echoPTY, "roundtrip\n") | |
| 58 | + | require.NoError(t, err) | |
| 59 | + | ||
| 60 | + | buf := make([]byte, 256) | |
| 61 | + | deadline := time.Now().Add(5 * time.Second) | |
| 62 | + | var output string | |
| 63 | + | for !strings.Contains(output, "roundtrip") { | |
| 64 | + | if time.Now().After(deadline) { | |
| 65 | + | t.Fatalf("did not read echoed data, got %q", output) | |
| 66 | + | } | |
| 67 | + | n, readErr := echoPTY.Read(buf) | |
| 68 | + | output += string(buf[:n]) | |
| 69 | + | if readErr != nil { | |
| 70 | + | require.ErrorIs(t, readErr, io.EOF) | |
| 71 | + | break | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | require.Contains(t, output, "roundtrip") | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) { | |
| 78 | + | if testing.Short() { | |
| 79 | + | t.Skip("skipping pty integration test in -short mode") | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "sleep 0.2; stty size"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 83 | + | require.NoError(t, err) | |
| 84 | + | t.Cleanup(func() { _ = p.Close() }) | |
| 85 | + | ||
| 86 | + | pgid, err := p.ForegroundPgid() | |
| 87 | + | require.NoError(t, err) | |
| 88 | + | require.Greater(t, pgid, 0) | |
| 89 | + | require.NoError(t, p.Resize(domain.Size{Cols: 120, Rows: 40})) | |
| 90 | + | require.Equal(t, "40 120", strings.TrimSpace(string(readAllDarwin(t, p, 5*time.Second)))) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func TestOpen_DarwinContextCancellationEndsStream(t *testing.T) { | |
| 94 | + | if testing.Short() { | |
| 95 | + | t.Skip("skipping pty integration test in -short mode") | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 99 | + | defer cancel() | |
| 100 | + | p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 101 | + | require.NoError(t, err) | |
| 102 | + | t.Cleanup(func() { _ = p.Close() }) | |
| 103 | + | ||
| 104 | + | streamEnded := make(chan error, 1) | |
| 105 | + | go func() { | |
| 106 | + | _, err := io.Copy(io.Discard, p) | |
| 107 | + | streamEnded <- err | |
| 108 | + | }() | |
| 109 | + | ||
| 110 | + | cancel() | |
| 111 | + | select { | |
| 112 | + | case err := <-streamEnded: | |
| 113 | + | require.NoError(t, err) | |
| 114 | + | case <-time.After(5 * time.Second): | |
| 115 | + | t.Fatal("pty stream did not end after context cancellation") | |
| 116 | + | } | |
| 117 | + | } | |
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | //go:build darwin | |||
| 2 | + | ||||
| 3 | + | package pty_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "context" | |||
| 8 | + | "io" | |||
| 9 | + | "os" | |||
| 10 | + | "strings" | |||
| 11 | + | "testing" | |||
| 12 | + | "time" | |||
| 13 | + | ||||
| 14 | + | "github.com/bnema/vev/internal/adapters/pty" | |||
| 15 | + | "github.com/bnema/vev/internal/domain" | |||
| 16 | + | "github.com/bnema/vev/internal/ports" | |||
| 17 | + | "github.com/stretchr/testify/require" | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | func readAllDarwin(t *testing.T, p ports.PTY, deadline time.Duration) []byte { | |||
| 21 | + | t.Helper() | |||
| 22 | + | type result struct { | |||
| 23 | + | output []byte | |||
| 24 | + | err error | |||
| 25 | + | } | |||
| 26 | + | resultCh := make(chan result, 1) | |||
| 27 | + | go func() { | |||
| 28 | + | var output bytes.Buffer | |||
| 29 | + | _, err := io.Copy(&output, p) | |||
| 30 | + | resultCh <- result{output: output.Bytes(), err: err} | |||
| 31 | + | }() | |||
| 32 | + | ||||
| 33 | + | select { | |||
| 34 | + | case result := <-resultCh: | |||
| 35 | + | require.NoError(t, result.err) | |||
| 36 | + | return result.output | |||
| 37 | + | case <-time.After(deadline): | |||
| 38 | + | t.Fatal("timed out reading from pty") | |||
| 39 | + | return nil | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) { | |||
| 44 | + | if testing.Short() { | |||
| 45 | + | t.Skip("skipping pty integration test in -short mode") | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | factory := pty.NewFactory() | |||
| 49 | + | outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |||
| 50 | + | require.NoError(t, err) | |||
| 51 | + | t.Cleanup(func() { _ = outputPTY.Close() }) | |||
| 52 | + | require.Equal(t, "hello", string(readAllDarwin(t, outputPTY, 5*time.Second))) | |||
| 53 | + | ||||
| 54 | + | echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |||
| 55 | + | require.NoError(t, err) | |||
| 56 | + | t.Cleanup(func() { _ = echoPTY.Close() }) | |||
| 57 | + | _, err = io.WriteString(echoPTY, "roundtrip\n") | |||
| 58 | + | require.NoError(t, err) | |||
| 59 | + | ||||
| 60 | + | buf := make([]byte, 256) | |||
| 61 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 62 | + | var output string | |||
| 63 | + | for !strings.Contains(output, "roundtrip") { | |||
| 64 | + | if time.Now().After(deadline) { | |||
| 65 | + | t.Fatalf("did not read echoed data, got %q", output) | |||
| 66 | + | } | |||
| 67 | + | n, readErr := echoPTY.Read(buf) | |||
| 68 | + | output += string(buf[:n]) | |||
| 69 | + | if readErr != nil { | |||
| 70 | + | require.ErrorIs(t, readErr, io.EOF) | |||
| 71 | + | break | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | require.Contains(t, output, "roundtrip") | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) { | |||
| 78 | + | if testing.Short() { | |||
| 79 | + | t.Skip("skipping pty integration test in -short mode") | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "sleep 0.2; stty size"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |||
| 83 | + | require.NoError(t, err) | |||
| 84 | + | t.Cleanup(func() { _ = p.Close() }) | |||
| 85 | + | ||||
| 86 | + | pgid, err := p.ForegroundPgid() | |||
| 87 | + | require.NoError(t, err) | |||
| 88 | + | require.Greater(t, pgid, 0) | |||
| 89 | + | require.NoError(t, p.Resize(domain.Size{Cols: 120, Rows: 40})) | |||
| 90 | + | require.Equal(t, "40 120", strings.TrimSpace(string(readAllDarwin(t, p, 5*time.Second)))) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func TestOpen_DarwinContextCancellationEndsStream(t *testing.T) { | |||
| 94 | + | if testing.Short() { | |||
| 95 | + | t.Skip("skipping pty integration test in -short mode") | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 99 | + | defer cancel() | |||
| 100 | + | p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |||
| 101 | + | require.NoError(t, err) | |||
| 102 | + | t.Cleanup(func() { _ = p.Close() }) | |||
| 103 | + | ||||
| 104 | + | streamEnded := make(chan error, 1) | |||
| 105 | + | go func() { | |||
| 106 | + | _, err := io.Copy(io.Discard, p) | |||
| 107 | + | streamEnded <- err | |||
| 108 | + | }() | |||
| 109 | + | ||||
| 110 | + | cancel() | |||
| 111 | + | select { | |||
| 112 | + | case err := <-streamEnded: | |||
| 113 | + | require.NoError(t, err) | |||
| 114 | + | case <-time.After(5 * time.Second): | |||
| 115 | + | t.Fatal("pty stream did not end after context cancellation") | |||
| 116 | + | } | |||
| 117 | + | } | |||
R internal/adapters/pty/pty_linux.go → internal/adapters/pty/pty_unix.go +14 -14 renamed, 91% similar
| @@ -1,8 +1,8 @@ | |||
| 1 | - | //go:build linux | |
| 1 | + | //go:build linux || darwin | |
| 2 | 2 | ||
| 3 | 3 | package pty | |
| 4 | 4 | ||
| 5 | - | // The Linux adapter drives /dev/ptmx directly and delegates slave preparation | |
| 5 | + | // The Unix adapter drives /dev/ptmx directly and delegates slave preparation | |
| 6 | 6 | // to rawterm rather than depending on a third-party pty package, keeping the | |
| 7 | 7 | // dependency surface at the standard library alone. | |
| 8 | 8 | ||
| @@ -27,10 +27,10 @@ import ( | |||
| 27 | 27 | // escalating from SIGHUP to SIGKILL. | |
| 28 | 28 | const killGracePeriod = 2 * time.Second | |
| 29 | 29 | ||
| 30 | - | // Factory implements ports.PTYFactory for Linux. | |
| 30 | + | // Factory implements ports.PTYFactory on supported Unix platforms. | |
| 31 | 31 | type Factory struct{} | |
| 32 | 32 | ||
| 33 | - | // NewFactory returns a Linux PTY factory. | |
| 33 | + | // NewFactory returns a Unix PTY factory. | |
| 34 | 34 | func NewFactory() *Factory { return &Factory{} } | |
| 35 | 35 | ||
| 36 | 36 | // Open spawns command with args attached to a freshly allocated pseudo-terminal | |
| @@ -105,14 +105,14 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st | |||
| 105 | 105 | } | |
| 106 | 106 | ok = true | |
| 107 | 107 | ||
| 108 | - | return &linuxPTY{ | |
| 108 | + | return &unixPTY{ | |
| 109 | 109 | master: os.NewFile(uintptr(masterFd), "pty-master"), | |
| 110 | 110 | cmd: cmd, | |
| 111 | 111 | }, nil | |
| 112 | 112 | } | |
| 113 | 113 | ||
| 114 | - | // linuxPTY is a running child attached to the master end of a pty. | |
| 115 | - | type linuxPTY struct { | |
| 114 | + | // unixPTY is a running child attached to the master end of a pty. | |
| 115 | + | type unixPTY struct { | |
| 116 | 116 | master *os.File | |
| 117 | 117 | cmd *exec.Cmd | |
| 118 | 118 | ||
| @@ -120,14 +120,14 @@ type linuxPTY struct { | |||
| 120 | 120 | closeErr error | |
| 121 | 121 | } | |
| 122 | 122 | ||
| 123 | - | var _ ports.PTY = (*linuxPTY)(nil) | |
| 123 | + | var _ ports.PTY = (*unixPTY)(nil) | |
| 124 | 124 | ||
| 125 | 125 | // Read returns child output read from the master. | |
| 126 | 126 | // | |
| 127 | 127 | // When the child (the last process holding the slave open) exits, the kernel | |
| 128 | 128 | // makes reads of the master fail with EIO. Callers treat "child gone" as a | |
| 129 | 129 | // normal end of stream, so EIO is mapped to io.EOF here. | |
| 130 | - | func (p *linuxPTY) Read(b []byte) (int, error) { | |
| 130 | + | func (p *unixPTY) Read(b []byte) (int, error) { | |
| 131 | 131 | n, err := p.master.Read(b) | |
| 132 | 132 | if err != nil && errors.Is(err, syscall.EIO) { | |
| 133 | 133 | return n, io.EOF | |
| @@ -136,7 +136,7 @@ func (p *linuxPTY) Read(b []byte) (int, error) { | |||
| 136 | 136 | } | |
| 137 | 137 | ||
| 138 | 138 | // Write sends input bytes to the child via the master. | |
| 139 | - | func (p *linuxPTY) Write(b []byte) (int, error) { | |
| 139 | + | func (p *unixPTY) Write(b []byte) (int, error) { | |
| 140 | 140 | return p.master.Write(b) | |
| 141 | 141 | } | |
| 142 | 142 | ||
| @@ -148,7 +148,7 @@ func (p *linuxPTY) Write(b []byte) (int, error) { | |||
| 148 | 148 | // every subsequent Read into a thread-parking syscall and breaking the | |
| 149 | 149 | // Close-unblocks-Read behavior. Control also pins the fd for the duration and | |
| 150 | 150 | // fails cleanly (os.ErrClosed) after Close. | |
| 151 | - | func (p *linuxPTY) Resize(sz domain.Size) error { | |
| 151 | + | func (p *unixPTY) Resize(sz domain.Size) error { | |
| 152 | 152 | rc, err := p.master.SyscallConn() | |
| 153 | 153 | if err != nil { | |
| 154 | 154 | return fmt.Errorf("pty: resize: %w", err) | |
| @@ -163,13 +163,13 @@ func (p *linuxPTY) Resize(sz domain.Size) error { | |||
| 163 | 163 | } | |
| 164 | 164 | ||
| 165 | 165 | // Pid reports the child process id. | |
| 166 | - | func (p *linuxPTY) Pid() int { | |
| 166 | + | func (p *unixPTY) Pid() int { | |
| 167 | 167 | return p.cmd.Process.Pid | |
| 168 | 168 | } | |
| 169 | 169 | ||
| 170 | 170 | // ForegroundPgid reports the process group currently in the foreground for the | |
| 171 | 171 | // pty's controlling terminal. | |
| 172 | - | func (p *linuxPTY) ForegroundPgid() (int, error) { | |
| 172 | + | func (p *unixPTY) ForegroundPgid() (int, error) { | |
| 173 | 173 | rc, err := p.master.SyscallConn() | |
| 174 | 174 | if err != nil { | |
| 175 | 175 | return 0, fmt.Errorf("pty: foreground pgid: %w", err) | |
| @@ -192,7 +192,7 @@ func (p *linuxPTY) ForegroundPgid() (int, error) { | |||
| 192 | 192 | // escalates to SIGKILL. The child is reaped (via cmd.Wait) to avoid a zombie, | |
| 193 | 193 | // and the master fd is closed last so any blocked Read unblocks. Close is | |
| 194 | 194 | // idempotent: subsequent calls are no-ops and return the first result. | |
| 195 | - | func (p *linuxPTY) Close() error { | |
| 195 | + | func (p *unixPTY) Close() error { | |
| 196 | 196 | p.closeOnce.Do(func() { | |
| 197 | 197 | pid := p.cmd.Process.Pid | |
| 198 | 198 | ||
| @@ -1,8 +1,8 @@ | |||||
| 1 | - | //go:build linux | 1 | + | //go:build linux || darwin |
| 2 | 2 | ||||
| 3 | package pty | 3 | package pty | ||
| 4 | 4 | ||||
| 5 | - | // The Linux adapter drives /dev/ptmx directly and delegates slave preparation | 5 | + | // The Unix adapter drives /dev/ptmx directly and delegates slave preparation |
| 6 | // to rawterm rather than depending on a third-party pty package, keeping the | 6 | // to rawterm rather than depending on a third-party pty package, keeping the | ||
| 7 | // dependency surface at the standard library alone. | 7 | // dependency surface at the standard library alone. | ||
| 8 | 8 | ||||
| @@ -27,10 +27,10 @@ import ( | |||||
| 27 | // escalating from SIGHUP to SIGKILL. | 27 | // escalating from SIGHUP to SIGKILL. | ||
| 28 | const killGracePeriod = 2 * time.Second | 28 | const killGracePeriod = 2 * time.Second | ||
| 29 | 29 | ||||
| 30 | - | // Factory implements ports.PTYFactory for Linux. | 30 | + | // Factory implements ports.PTYFactory on supported Unix platforms. |
| 31 | type Factory struct{} | 31 | type Factory struct{} | ||
| 32 | 32 | ||||
| 33 | - | // NewFactory returns a Linux PTY factory. | 33 | + | // NewFactory returns a Unix PTY factory. |
| 34 | func NewFactory() *Factory { return &Factory{} } | 34 | func NewFactory() *Factory { return &Factory{} } | ||
| 35 | 35 | ||||
| 36 | // Open spawns command with args attached to a freshly allocated pseudo-terminal | 36 | // Open spawns command with args attached to a freshly allocated pseudo-terminal | ||
| @@ -105,14 +105,14 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st | |||||
| 105 | } | 105 | } | ||
| 106 | ok = true | 106 | ok = true | ||
| 107 | 107 | ||||
| 108 | - | return &linuxPTY{ | 108 | + | return &unixPTY{ |
| 109 | master: os.NewFile(uintptr(masterFd), "pty-master"), | 109 | master: os.NewFile(uintptr(masterFd), "pty-master"), | ||
| 110 | cmd: cmd, | 110 | cmd: cmd, | ||
| 111 | }, nil | 111 | }, nil | ||
| 112 | } | 112 | } | ||
| 113 | 113 | ||||
| 114 | - | // linuxPTY is a running child attached to the master end of a pty. | 114 | + | // unixPTY is a running child attached to the master end of a pty. |
| 115 | - | type linuxPTY struct { | 115 | + | type unixPTY struct { |
| 116 | master *os.File | 116 | master *os.File | ||
| 117 | cmd *exec.Cmd | 117 | cmd *exec.Cmd | ||
| 118 | 118 | ||||
| @@ -120,14 +120,14 @@ type linuxPTY struct { | |||||
| 120 | closeErr error | 120 | closeErr error | ||
| 121 | } | 121 | } | ||
| 122 | 122 | ||||
| 123 | - | var _ ports.PTY = (*linuxPTY)(nil) | 123 | + | var _ ports.PTY = (*unixPTY)(nil) |
| 124 | 124 | ||||
| 125 | // Read returns child output read from the master. | 125 | // Read returns child output read from the master. | ||
| 126 | // | 126 | // | ||
| 127 | // When the child (the last process holding the slave open) exits, the kernel | 127 | // When the child (the last process holding the slave open) exits, the kernel | ||
| 128 | // makes reads of the master fail with EIO. Callers treat "child gone" as a | 128 | // makes reads of the master fail with EIO. Callers treat "child gone" as a | ||
| 129 | // normal end of stream, so EIO is mapped to io.EOF here. | 129 | // normal end of stream, so EIO is mapped to io.EOF here. | ||
| 130 | - | func (p *linuxPTY) Read(b []byte) (int, error) { | 130 | + | func (p *unixPTY) Read(b []byte) (int, error) { |
| 131 | n, err := p.master.Read(b) | 131 | n, err := p.master.Read(b) | ||
| 132 | if err != nil && errors.Is(err, syscall.EIO) { | 132 | if err != nil && errors.Is(err, syscall.EIO) { | ||
| 133 | return n, io.EOF | 133 | return n, io.EOF | ||
| @@ -136,7 +136,7 @@ func (p *linuxPTY) Read(b []byte) (int, error) { | |||||
| 136 | } | 136 | } | ||
| 137 | 137 | ||||
| 138 | // Write sends input bytes to the child via the master. | 138 | // Write sends input bytes to the child via the master. | ||
| 139 | - | func (p *linuxPTY) Write(b []byte) (int, error) { | 139 | + | func (p *unixPTY) Write(b []byte) (int, error) { |
| 140 | return p.master.Write(b) | 140 | return p.master.Write(b) | ||
| 141 | } | 141 | } | ||
| 142 | 142 | ||||
| @@ -148,7 +148,7 @@ func (p *linuxPTY) Write(b []byte) (int, error) { | |||||
| 148 | // every subsequent Read into a thread-parking syscall and breaking the | 148 | // every subsequent Read into a thread-parking syscall and breaking the | ||
| 149 | // Close-unblocks-Read behavior. Control also pins the fd for the duration and | 149 | // Close-unblocks-Read behavior. Control also pins the fd for the duration and | ||
| 150 | // fails cleanly (os.ErrClosed) after Close. | 150 | // fails cleanly (os.ErrClosed) after Close. | ||
| 151 | - | func (p *linuxPTY) Resize(sz domain.Size) error { | 151 | + | func (p *unixPTY) Resize(sz domain.Size) error { |
| 152 | rc, err := p.master.SyscallConn() | 152 | rc, err := p.master.SyscallConn() | ||
| 153 | if err != nil { | 153 | if err != nil { | ||
| 154 | return fmt.Errorf("pty: resize: %w", err) | 154 | return fmt.Errorf("pty: resize: %w", err) | ||
| @@ -163,13 +163,13 @@ func (p *linuxPTY) Resize(sz domain.Size) error { | |||||
| 163 | } | 163 | } | ||
| 164 | 164 | ||||
| 165 | // Pid reports the child process id. | 165 | // Pid reports the child process id. | ||
| 166 | - | func (p *linuxPTY) Pid() int { | 166 | + | func (p *unixPTY) Pid() int { |
| 167 | return p.cmd.Process.Pid | 167 | return p.cmd.Process.Pid | ||
| 168 | } | 168 | } | ||
| 169 | 169 | ||||
| 170 | // ForegroundPgid reports the process group currently in the foreground for the | 170 | // ForegroundPgid reports the process group currently in the foreground for the | ||
| 171 | // pty's controlling terminal. | 171 | // pty's controlling terminal. | ||
| 172 | - | func (p *linuxPTY) ForegroundPgid() (int, error) { | 172 | + | func (p *unixPTY) ForegroundPgid() (int, error) { |
| 173 | rc, err := p.master.SyscallConn() | 173 | rc, err := p.master.SyscallConn() | ||
| 174 | if err != nil { | 174 | if err != nil { | ||
| 175 | return 0, fmt.Errorf("pty: foreground pgid: %w", err) | 175 | return 0, fmt.Errorf("pty: foreground pgid: %w", err) | ||
| @@ -192,7 +192,7 @@ func (p *linuxPTY) ForegroundPgid() (int, error) { | |||||
| 192 | // escalates to SIGKILL. The child is reaped (via cmd.Wait) to avoid a zombie, | 192 | // escalates to SIGKILL. The child is reaped (via cmd.Wait) to avoid a zombie, | ||
| 193 | // and the master fd is closed last so any blocked Read unblocks. Close is | 193 | // and the master fd is closed last so any blocked Read unblocks. Close is | ||
| 194 | // idempotent: subsequent calls are no-ops and return the first result. | 194 | // idempotent: subsequent calls are no-ops and return the first result. | ||
| 195 | - | func (p *linuxPTY) Close() error { | 195 | + | func (p *unixPTY) Close() error { |
| 196 | p.closeOnce.Do(func() { | 196 | p.closeOnce.Do(func() { | ||
| 197 | pid := p.cmd.Process.Pid | 197 | pid := p.cmd.Process.Pid | ||
| 198 | 198 | ||||