Skip to content

Commit 1d0e1f7

Browse files
committed
feat: add pseudo-ttys so piped output isn't buffered
1 parent f5f41a8 commit 1d0e1f7

5 files changed

Lines changed: 34 additions & 42 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/charmbracelet/x/ansi v0.9.2 // indirect
1818
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
1919
github.com/charmbracelet/x/term v0.2.1 // indirect
20+
github.com/creack/pty v1.1.24 // indirect
2021
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
2122
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2223
github.com/mattn/go-isatty v0.0.20 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ
1616
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
1717
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
1818
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
19+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
20+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
1921
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
2022
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
2123
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=

internal/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package internal
33
import (
44
"log"
55
"os"
6+
"os/exec"
7+
"sync"
68

79
"gopkg.in/yaml.v3"
810
)
@@ -27,6 +29,9 @@ type Command struct {
2729

2830
LogTail string
2931
Status CommandStatus
32+
33+
proc *exec.Cmd
34+
mu sync.Mutex
3035
}
3136

3237
func LoadConfig(configPath string) []*Command {

internal/execer.go

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,16 @@ import (
77
"log/slog"
88
"os/exec"
99
"strings"
10-
"sync"
1110
"syscall"
11+
12+
"github.com/creack/pty"
1213
)
1314

1415
type ExecerCallback func(c *Command, line string, err error, done bool)
1516

1617
type Execer struct {
1718
Command *Command
1819
Callback ExecerCallback
19-
20-
mu sync.Mutex
21-
proc *exec.Cmd
2220
}
2321

2422
func NewExecer(c *Command, cb ExecerCallback) *Execer {
@@ -34,64 +32,50 @@ func (e *Execer) Start() {
3432
parts := strings.Fields(e.Command.Exec)
3533
cmd := exec.Command(parts[0], parts[1:]...)
3634
cmd.Dir = e.Command.Cwd
37-
cmd.SysProcAttr = &syscall.SysProcAttr{
38-
Setpgid: true,
39-
}
40-
41-
stdout, err := cmd.StdoutPipe()
42-
if err != nil {
43-
e.Callback(e.Command, "", err, true)
44-
return
45-
}
4635

47-
stderr, err := cmd.StderrPipe()
36+
ptmx, err := pty.Start(cmd)
4837
if err != nil {
4938
e.Callback(e.Command, "", err, true)
5039
return
5140
}
5241

53-
if err := cmd.Start(); err != nil {
54-
e.Callback(e.Command, "", err, true)
55-
return
56-
}
57-
5842
slog.Info("Starting command", "cmd", strings.Join(cmd.Args, " "), "PID", cmd.Process.Pid)
5943

60-
e.mu.Lock()
61-
e.proc = cmd
62-
e.mu.Unlock()
44+
e.Command.mu.Lock()
45+
e.Command.proc = cmd
46+
e.Command.mu.Unlock()
6347

6448
go func() {
65-
r := io.MultiReader(stdout, stderr)
49+
r := io.MultiReader(ptmx)
6650
scanner := bufio.NewScanner(r)
6751

6852
for scanner.Scan() {
69-
e.mu.Lock()
53+
e.Command.mu.Lock()
7054
e.Callback(e.Command, fmt.Sprintf("%s", scanner.Text()), nil, false)
71-
e.mu.Unlock()
55+
e.Command.mu.Unlock()
7256
}
7357
}()
7458
err = cmd.Wait()
75-
e.mu.Lock()
59+
e.Command.mu.Lock()
7660
e.Callback(e.Command, "", err, true)
77-
e.mu.Unlock()
61+
e.Command.mu.Unlock()
7862
}
7963

8064
func (e *Execer) Stop() error {
81-
e.mu.Lock()
82-
defer e.mu.Unlock()
65+
e.Command.mu.Lock()
66+
defer e.Command.mu.Unlock()
8367

84-
if e.proc != nil && e.proc.Process != nil {
85-
slog.Debug("killing proc", "PID", e.proc.Process.Pid)
86-
pgid, err := syscall.Getpgid(e.proc.Process.Pid)
68+
if e.Command.proc != nil && e.Command.proc.Process != nil {
69+
slog.Debug("killing proc", "PID", e.Command.proc.Process.Pid)
70+
pgid, err := syscall.Getpgid(e.Command.proc.Process.Pid)
8771
if err == nil {
8872
err = syscall.Kill(-pgid, syscall.SIGKILL)
89-
slog.Debug("killed process group", "PID", e.proc.Process.Pid, "error", err)
73+
slog.Debug("killed process group", "PID", e.Command.proc.Process.Pid, "error", err)
9074
} else {
91-
err = e.proc.Process.Kill()
92-
slog.Debug("killed process", "PID", e.proc.Process.Pid, "error", err)
75+
err = e.Command.proc.Process.Kill()
76+
slog.Debug("killed process", "PID", e.Command.proc.Process.Pid, "error", err)
9377
}
94-
e.proc = nil
78+
e.Command.proc = nil
9579
return err
9680
}
9781

internal/execer_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ func TestExecerStopsOldProcessBeforeStart(t *testing.T) {
7979
time.Sleep(100 * time.Millisecond)
8080

8181
// Check that the process is running
82-
execer.mu.Lock()
83-
firstProc := execer.proc
84-
execer.mu.Unlock()
82+
cmd.mu.Lock()
83+
firstProc := cmd.proc
84+
cmd.mu.Unlock()
8585

8686
if firstProc == nil || firstProc.Process == nil {
8787
t.Fatal("expected first process to be started")
@@ -93,9 +93,9 @@ func TestExecerStopsOldProcessBeforeStart(t *testing.T) {
9393
// Wait for the stop to take effect
9494
time.Sleep(100 * time.Millisecond)
9595

96-
execer.mu.Lock()
97-
secondProc := execer.proc
98-
execer.mu.Unlock()
96+
cmd.mu.Lock()
97+
secondProc := cmd.proc
98+
cmd.mu.Unlock()
9999

100100
if secondProc == nil || secondProc.Process == nil {
101101
t.Fatal("expected second process to be started")

0 commit comments

Comments
 (0)