Fix PTY workdir and process group teardown

This commit is contained in:
2026-05-27 13:19:35 +01:00
parent da46340a82
commit b72a32bbc6
3 changed files with 93 additions and 2 deletions

View File

@@ -228,7 +228,7 @@ func (c *Child) startPTY(cols, rows uint16) (uint64, error) {
} }
starting := StatusStarting starting := StatusStarting
c.status.Store(&starting) c.status.Store(&starting)
p, err := pkgpty.Start(c.Argv, c.Env, cols, rows) p, err := pkgpty.Start(c.Argv, c.Env, c.WorkDir, cols, rows)
if err != nil { if err != nil {
em.Close() em.Close()
errored := StatusErrored errored := StatusErrored

View File

@@ -6,6 +6,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"syscall"
cpty "github.com/creack/pty" cpty "github.com/creack/pty"
) )
@@ -19,11 +20,13 @@ type PTY struct {
// Start spawns argv with stdin/stdout/stderr attached to a new PTY sized // Start spawns argv with stdin/stdout/stderr attached to a new PTY sized
// (cols, rows). The returned PTY exposes the master fd for the parent to // (cols, rows). The returned PTY exposes the master fd for the parent to
// read from and write to. // read from and write to.
func Start(argv []string, env []string, cols, rows uint16) (*PTY, error) { func Start(argv []string, env []string, workDir string, cols, rows uint16) (*PTY, error) {
if len(argv) == 0 { if len(argv) == 0 {
return nil, fmt.Errorf("pty: empty argv") return nil, fmt.Errorf("pty: empty argv")
} }
cmd := exec.Command(argv[0], argv[1:]...) cmd := exec.Command(argv[0], argv[1:]...)
cmd.Dir = workDir
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true}
if env != nil { if env != nil {
cmd.Env = ensureTerm(env) cmd.Env = ensureTerm(env)
} else { } else {
@@ -88,6 +91,10 @@ func (p *PTY) Close() error {
p.master = nil p.master = nil
} }
if p.cmd != nil && p.cmd.Process != nil { if p.cmd != nil && p.cmd.Process != nil {
pid := p.cmd.Process.Pid
if pid > 0 {
_ = syscall.Kill(-pid, syscall.SIGKILL)
}
_ = p.cmd.Process.Kill() _ = p.cmd.Process.Kill()
} }
return firstErr return firstErr

84
internal/pty/pty_test.go Normal file
View File

@@ -0,0 +1,84 @@
package pty
import (
"bytes"
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"time"
)
func TestStartUsesWorkDir(t *testing.T) {
dir := t.TempDir()
p, err := Start([]string{"sh", "-c", "pwd"}, nil, dir, 80, 24)
if err != nil {
t.Fatalf("Start: %v", err)
}
defer p.Close()
var out bytes.Buffer
buf := make([]byte, 256)
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
n, err := p.Read(buf)
if n > 0 {
out.Write(buf[:n])
if strings.Contains(out.String(), dir) {
break
}
}
if err != nil {
break
}
}
_ = p.Wait()
if got := strings.TrimSpace(out.String()); got != dir {
t.Fatalf("pwd output = %q, want %q", got, dir)
}
}
func TestCloseKillsProcessGroup(t *testing.T) {
dir := t.TempDir()
pidFile := filepath.Join(dir, "sleep.pid")
env := append(os.Environ(), "PIDFILE="+pidFile)
p, err := Start([]string{"sh", "-c", "sleep 30 & echo $! > \"$PIDFILE\"; wait"}, env, "", 80, 24)
if err != nil {
t.Fatalf("Start: %v", err)
}
deadline := time.Now().Add(5 * time.Second)
var childPID int
for time.Now().Before(deadline) {
b, err := os.ReadFile(pidFile)
if err == nil {
childPID, _ = strconv.Atoi(strings.TrimSpace(string(b)))
if childPID > 0 {
break
}
}
time.Sleep(20 * time.Millisecond)
}
if childPID <= 0 {
_ = p.Close()
t.Fatalf("background child pid was not written")
}
if err := p.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
_ = p.Wait()
deadline = time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
err := syscall.Kill(childPID, 0)
if errors.Is(err, syscall.ESRCH) {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("background child pid %d still exists after PTY.Close", childPID)
}