Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<img src="https://img.shields.io/github/v/tag/goforj/execx?label=version&sort=semver" alt="Latest tag">
<a href="https://codecov.io/gh/goforj/execx" ><img src="https://codecov.io/github/goforj/execx/graph/badge.svg?token=RBB8T6WQ0U"/></a>
<!-- test-count:embed:start -->
<img src="https://img.shields.io/badge/tests-113-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/tests-116-brightgreen" alt="Tests">
<!-- test-count:embed:end -->
<a href="https://goreportcard.com/report/github.com/goforj/execx"><img src="https://goreportcard.com/badge/github.com/goforj/execx" alt="Go Report Card"></a>
</p>
Expand Down
8 changes: 2 additions & 6 deletions pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,8 @@ func (p *pipeline) start() {
if stg.ptyMaster != nil {
stg.ptyDone = make(chan error, 1)
go func(st *stage) {
_, err := io.Copy(st.ptyWriter, st.ptyMaster)
if err != nil {
st.ptyDone <- err
} else {
st.ptyDone <- nil
}
_, err := io.Copy(st.ptyWriter, ptyOutputReader(st.ptyMaster))
st.ptyDone <- err
_ = st.ptyMaster.Close()
}(stg)
_ = stg.ptySlave.Close()
Expand Down
10 changes: 10 additions & 0 deletions pty_read_error_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !linux && !darwin

package execx

import "io"

// ptyOutputReader preserves native read failures on platforms without Unix PTY hangup semantics.
func ptyOutputReader(reader io.Reader) io.Reader {
return reader
}
28 changes: 28 additions & 0 deletions pty_read_error_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build linux || darwin

package execx

import (
"errors"
"io"
"syscall"
)

// ptyEOFReader converts the Unix terminal hangup convention without changing other read errors.
type ptyEOFReader struct {
reader io.Reader
}

// Read treats a Unix PTY hangup as EOF because the slave has already closed normally.
func (r ptyEOFReader) Read(p []byte) (int, error) {
n, err := r.reader.Read(p)
if errors.Is(err, syscall.EIO) {
return n, io.EOF
}
return n, err
}

// ptyOutputReader limits terminal hangup normalization to reads so writer failures remain visible.
func ptyOutputReader(reader io.Reader) io.Reader {
return ptyEOFReader{reader: reader}
}
59 changes: 59 additions & 0 deletions pty_read_error_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build linux || darwin

package execx

import (
"errors"
"io"
"syscall"
"testing"
)

// ptyErrorReader provides deterministic terminal read failures for the platform adapter tests.
type ptyErrorReader struct {
err error
}

// Read returns the configured error so PTY EOF handling can be tested without kernel timing.
func (r ptyErrorReader) Read([]byte) (int, error) {
return 0, r.err
}

// TestPTYEOFReader distinguishes a normal Unix terminal hangup from a real read failure.
func TestPTYEOFReader(t *testing.T) {
reader := ptyOutputReader(ptyErrorReader{err: syscall.EIO})
if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
t.Fatalf("ptyOutputReader(EIO) error = %v, want EOF", err)
}

want := errors.New("read failed")
reader = ptyOutputReader(ptyErrorReader{err: want})
if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, want) {
t.Fatalf("ptyOutputReader() error = %v, want %v", err, want)
}
}

// TestWithPTYTreatsHangupAsEOF exercises the kernel PTY path that pipes cannot reproduce.
func TestWithPTYTreatsHangupAsEOF(t *testing.T) {
result, err := Command("printf", "hello").WithPTY().Run()
if err != nil {
t.Fatalf("WithPTY().Run() error = %v", err)
}
if result.Stdout != "hello" {
t.Fatalf("WithPTY().Run() stdout = %q, want %q", result.Stdout, "hello")
}
}

// TestWithPTYPreservesExitStatusAfterHangup keeps terminal EOF handling from hiding child failures.
func TestWithPTYPreservesExitStatusAfterHangup(t *testing.T) {
result, err := Command("sh", "-c", "printf failed; exit 7").WithPTY().Run()
if err != nil {
t.Fatalf("WithPTY().Run() error = %v", err)
}
if result.ExitCode != 7 {
t.Fatalf("WithPTY().Run() exit code = %d, want 7", result.ExitCode)
}
if result.Stdout != "failed" {
t.Fatalf("WithPTY().Run() stdout = %q, want %q", result.Stdout, "failed")
}
}
Loading