Persistent daemon + thin networked client #9

Open
harry wants to merge 14 commits from feat/daemon-client-split into main
Showing only changes of commit 63986e7e00 - Show all commits

View File

@@ -6,13 +6,22 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"sync"
"syscall" "syscall"
cpty "github.com/creack/pty" cpty "github.com/creack/pty"
) )
// PTY holds a child process attached to a pseudo-terminal master fd. // PTY holds a child process attached to a pseudo-terminal master fd.
//
// mu guards the master field only. Read/Write/Resize capture the *os.File
// under the lock and then do the (potentially blocking) I/O without holding
// it, so Close can swap master to nil and close the fd concurrently — closing
// the captured *os.File unblocks an in-flight Read. This avoids a data race
// between pumpChild's Read and Session.Shutdown's Close, which the daemon now
// hits routinely (daemon stop, not just process exit).
type PTY struct { type PTY struct {
mu sync.Mutex
master *os.File master *os.File
cmd *exec.Cmd cmd *exec.Cmd
} }
@@ -45,24 +54,33 @@ func Start(argv []string, env []string, workDir string, cols, rows uint16) (*PTY
} }
func (p *PTY) Read(b []byte) (int, error) { func (p *PTY) Read(b []byte) (int, error) {
if p.master == nil { p.mu.Lock()
m := p.master
p.mu.Unlock()
if m == nil {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
} }
return p.master.Read(b) return m.Read(b)
} }
func (p *PTY) Write(b []byte) (int, error) { func (p *PTY) Write(b []byte) (int, error) {
if p.master == nil { p.mu.Lock()
m := p.master
p.mu.Unlock()
if m == nil {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
} }
return p.master.Write(b) return m.Write(b)
} }
func (p *PTY) Resize(cols, rows uint16) error { func (p *PTY) Resize(cols, rows uint16) error {
if p.master == nil { p.mu.Lock()
m := p.master
p.mu.Unlock()
if m == nil {
return io.ErrClosedPipe return io.ErrClosedPipe
} }
return cpty.Setsize(p.master, &cpty.Winsize{Cols: cols, Rows: rows}) return cpty.Setsize(m, &cpty.Winsize{Cols: cols, Rows: rows})
} }
// Wait blocks until the child exits and returns its exit error if any. // Wait blocks until the child exits and returns its exit error if any.
@@ -83,12 +101,15 @@ func (p *PTY) Pid() int {
// Close terminates the child (best effort) and releases the master fd. // Close terminates the child (best effort) and releases the master fd.
func (p *PTY) Close() error { func (p *PTY) Close() error {
p.mu.Lock()
m := p.master
p.master = nil
p.mu.Unlock()
var firstErr error var firstErr error
if p.master != nil { if m != nil {
if err := p.master.Close(); err != nil && firstErr == nil { if err := m.Close(); err != nil {
firstErr = err firstErr = err
} }
p.master = nil
} }
if p.cmd != nil && p.cmd.Process != nil { if p.cmd != nil && p.cmd.Process != nil {
pid := p.cmd.Process.Pid pid := p.cmd.Process.Pid