From 74601a0149f314a49eb1a0278a4bb0ffca1e748d Mon Sep 17 00:00:00 2001 From: Fabio Pugliese Ornellas Date: Wed, 19 Nov 2025 15:45:53 +0000 Subject: [PATCH 1/2] draft pty --- cmd/root.go | 4 +- go.mod | 8 ++- go.sum | 8 ++- runner/runner.go | 126 ++++++++++++++++++++++++++++++++++++----------- 4 files changed, 113 insertions(+), 33 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 26c3047..7fb0352 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,7 +40,7 @@ var RootCmd = &cobra.Command{ } defer w.Close() - r := runner.NewRunner(killWait, args[0], args[1:]...) + r := runner.NewRunner(killWait, !disablePseudoTerminal, args[0], args[1:]...) sigCn := make(chan os.Signal, 1) signal.Notify(sigCn, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) @@ -68,6 +68,7 @@ var ignorePatterns []string var debounce time.Duration var killWait time.Duration var logLevel string +var disablePseudoTerminal bool func cobraInit() { if err := log.Setup(logLevel); err != nil { @@ -105,4 +106,5 @@ func init() { &logLevel, "log-level", "l", "info", "Logging level", ) + RootCmd.Flags().BoolVarP(&disablePseudoTerminal, "disable-pseudo-terminal", "n", false, "Disable pseudo terminal for spawned command.") } diff --git a/go.mod b/go.mod index 931d618..d5df50a 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,14 @@ module github.com/fornellas/rrb -go 1.23.6 +go 1.24.0 + +toolchain go1.24.10 require ( al.essio.dev/pkg/shellescape v1.5.1 github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/client9/misspell v0.3.4 + github.com/creack/pty v1.1.24 github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.8.0 github.com/fzipp/gocyclo v0.6.0 @@ -14,6 +17,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/williammartin/subreaper v0.0.0-20181101193406-731d9ece6883 + golang.org/x/term v0.37.0 golang.org/x/tools v0.29.0 honnef.co/go/tools v0.5.1 ) @@ -29,5 +33,5 @@ require ( golang.org/x/exp/typeparams v0.0.0-20250207012021-f9890c6ad9f3 // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index 89f05e4..12c86e9 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -109,8 +111,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= diff --git a/runner/runner.go b/runner/runner.go index fc11153..b31aceb 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -1,9 +1,12 @@ package runner import ( + "errors" "fmt" + "io" "os" "os/exec" + "os/signal" "strconv" "strings" "syscall" @@ -12,36 +15,41 @@ import ( "github.com/sirupsen/logrus" "al.essio.dev/pkg/shellescape" + "github.com/creack/pty" "github.com/williammartin/subreaper" + "golang.org/x/term" + "github.com/fornellas/rrb/process" ) type Runner struct { - KillWait time.Duration - Name string - Args []string - cmdStr string - idleCn chan struct{} - waitCn chan struct{} - killCn chan struct{} - cmd *exec.Cmd + KillWait time.Duration + PseudoTerminal bool + Name string + Args []string + cmdStr string + idleCn chan struct{} + waitCn chan struct{} + killCn chan struct{} + cmd *exec.Cmd } -func NewRunner(killWait time.Duration, name string, args ...string) *Runner { +func NewRunner(killWait time.Duration, pseudoTerminal bool, name string, args ...string) *Runner { escapedCmd := []string{} for _, s := range append([]string{name}, args...) { escapedCmd = append(escapedCmd, shellescape.Quote(s)) } r := Runner{ - KillWait: killWait, - Name: name, - Args: args, - cmdStr: strings.Join(escapedCmd, " "), - idleCn: make(chan struct{}), - waitCn: make(chan struct{}), - killCn: make(chan struct{}), + KillWait: killWait, + PseudoTerminal: pseudoTerminal, + Name: name, + Args: args, + cmdStr: strings.Join(escapedCmd, " "), + idleCn: make(chan struct{}), + waitCn: make(chan struct{}), + killCn: make(chan struct{}), } go func() { r.idleCn <- struct{}{} @@ -191,7 +199,7 @@ func (r *Runner) Kill() { <-r.idleCn } -func (r *Runner) Run() error { +func (r *Runner) Run() (err error) { idle: for { select { @@ -207,25 +215,87 @@ idle: } } + defer func() { + if err != nil { + r.idleCn <- struct{}{} + } + }() + // This ensures that orphan process will become children of the process // that called Start(), so we can babysit then. - if err := subreaper.Set(); err != nil { - r.idleCn <- struct{}{} + if err = subreaper.Set(); err != nil { return err } r.cmd = exec.Command(r.Name, r.Args...) r.cmd.Env = os.Environ() - r.cmd.Stdin = os.Stdin - r.cmd.Stdout = os.Stdout - r.cmd.Stderr = os.Stderr - r.cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } logrus.Infof("> %s", r.cmdStr) - if err := r.cmd.Start(); err != nil { - r.idleCn <- struct{}{} - return err + if r.PseudoTerminal { + var ptyFile *os.File + ptyFile, err = pty.Start(r.cmd) + if err != nil { + return err + } + // FIXME we can only close this pty after the process is killed + defer func() { err = errors.Join(err, ptyFile.Close()) }() + + sigWinchCh := make(chan os.Signal, 1) + // FIXME we can only close this channel after the process is killed + defer func() { + signal.Ignore(syscall.SIGWINCH) + close(sigWinchCh) + }() + signal.Notify(sigWinchCh, syscall.SIGWINCH) + // FIXME we must babysit this after the process is killed + go func() { + for range sigWinchCh { + if isErr := pty.InheritSize(ptyFile, os.Stdout); isErr != nil { + logrus.Errorf("Error resizing pty: %s", isErr) + } + } + }() + sigWinchCh <- syscall.SIGWINCH + + var origStdinTermState *term.State + origStdinTermState, err = term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return err + } + // FIXME we must only restore this after the process is killed + defer func() { + err = errors.Join(err, term.Restore(int(os.Stdin.Fd()), origStdinTermState)) + }() + + // FIXME we must babysit this after the process is killed + go func() { + if _, copyErr := io.Copy(ptyFile, os.Stdin); copyErr != nil { + err = errors.Join(err, copyErr) + } + }() + + // FIXME we must babysit this after the process is killed + go func() { + if _, copyErr := io.Copy(os.Stdout, ptyFile); copyErr != nil { + err = errors.Join(err, copyErr) + } + }() + + // FIXME we must babysit this after the process is killed + go func() { + if _, copyErr := io.Copy(os.Stderr, ptyFile); copyErr != nil { + err = errors.Join(err, copyErr) + } + }() + } else { + r.cmd.Stdin = nil + r.cmd.Stdout = os.Stdout + r.cmd.Stderr = os.Stderr + r.cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + if err = r.cmd.Start(); err != nil { + return err + } } go r.waitAll() From 08635f6edd700168cc0a664c07fd5cc78f9934c6 Mon Sep 17 00:00:00 2001 From: Fabio Pugliese Ornellas Date: Wed, 19 Nov 2025 15:48:10 +0000 Subject: [PATCH 2/2] draft --- runner/runner.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/runner/runner.go b/runner/runner.go index b31aceb..da10bde 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -237,18 +237,19 @@ idle: return err } // FIXME we can only close this pty after the process is killed - defer func() { err = errors.Join(err, ptyFile.Close()) }() + // defer func() { err = errors.Join(err, ptyFile.Close()) }() sigWinchCh := make(chan os.Signal, 1) // FIXME we can only close this channel after the process is killed - defer func() { - signal.Ignore(syscall.SIGWINCH) - close(sigWinchCh) - }() + // defer func() { + // signal.Ignore(syscall.SIGWINCH) + // close(sigWinchCh) + // }() signal.Notify(sigWinchCh, syscall.SIGWINCH) // FIXME we must babysit this after the process is killed go func() { for range sigWinchCh { + // FIXME this seems to not be working if isErr := pty.InheritSize(ptyFile, os.Stdout); isErr != nil { logrus.Errorf("Error resizing pty: %s", isErr) } @@ -256,15 +257,16 @@ idle: }() sigWinchCh <- syscall.SIGWINCH - var origStdinTermState *term.State - origStdinTermState, err = term.MakeRaw(int(os.Stdin.Fd())) + // var origStdinTermState *term.State + _, err = term.MakeRaw(int(os.Stdin.Fd())) + // origStdinTermState, err = term.MakeRaw(int(os.Stdin.Fd())) if err != nil { return err } // FIXME we must only restore this after the process is killed - defer func() { - err = errors.Join(err, term.Restore(int(os.Stdin.Fd()), origStdinTermState)) - }() + // defer func() { + // err = errors.Join(err, term.Restore(int(os.Stdin.Fd()), origStdinTermState)) + // }() // FIXME we must babysit this after the process is killed go func() {