@@ -20,12 +20,14 @@ import (
2020 "strconv"
2121 "strings"
2222 "sync"
23+ "syscall"
2324 "time"
2425
2526 "github.com/buildkite/cleanroom/internal/backend"
2627 "github.com/buildkite/cleanroom/internal/paths"
2728 "github.com/buildkite/cleanroom/internal/policy"
2829 "github.com/buildkite/cleanroom/internal/vsockexec"
30+ "github.com/creack/pty"
2931 fcvsock "github.com/firecracker-microvm/firecracker-go-sdk/vsock"
3032)
3133
@@ -253,7 +255,7 @@ func (a *Adapter) run(ctx context.Context, req backend.RunRequest, stream backen
253255 }, nil
254256 }
255257
256- exitCode , stdout , stderr , err := runHostPassthrough (ctx , req .CWD , req .Command , stream )
258+ exitCode , stdout , stderr , err := runHostPassthrough (ctx , req .CWD , req .Command , req . TTY , stream )
257259 if err != nil {
258260 return nil , err
259261 }
@@ -579,7 +581,11 @@ func runResultMessage(base string) string {
579581 return base + "; rootfs writes discarded after run"
580582}
581583
582- func runHostPassthrough (ctx context.Context , cwd string , command []string , stream backend.OutputStream ) (int , string , string , error ) {
584+ func runHostPassthrough (ctx context.Context , cwd string , command []string , tty bool , stream backend.OutputStream ) (int , string , string , error ) {
585+ if tty {
586+ return runHostPassthroughTTY (ctx , cwd , command , stream )
587+ }
588+
583589 cmd := exec .CommandContext (ctx , command [0 ], command [1 :]... )
584590 cmd .Dir = cwd
585591 stdoutPipe , err := cmd .StdoutPipe ()
@@ -630,6 +636,76 @@ func runHostPassthrough(ctx context.Context, cwd string, command []string, strea
630636 return 1 , stdout .String (), stderr .String (), fmt .Errorf ("run host passthrough command: %w" , err )
631637}
632638
639+ func runHostPassthroughTTY (ctx context.Context , cwd string , command []string , stream backend.OutputStream ) (int , string , string , error ) {
640+ cmd := exec .CommandContext (ctx , command [0 ], command [1 :]... )
641+ cmd .Dir = cwd
642+
643+ ptyFile , err := pty .Start (cmd )
644+ if err != nil {
645+ return 1 , "" , "" , fmt .Errorf ("start host passthrough tty command: %w" , err )
646+ }
647+ defer ptyFile .Close ()
648+
649+ if stream .OnAttach != nil {
650+ stream .OnAttach (backend.AttachIO {
651+ WriteStdin : func (data []byte ) error {
652+ if len (data ) == 0 {
653+ return nil
654+ }
655+ _ , writeErr := ptyFile .Write (data )
656+ return writeErr
657+ },
658+ ResizeTTY : func (cols , rows uint32 ) error {
659+ if cols == 0 || rows == 0 {
660+ return nil
661+ }
662+ return pty .Setsize (ptyFile , & pty.Winsize {
663+ Cols : uint16 (cols ),
664+ Rows : uint16 (rows ),
665+ })
666+ },
667+ })
668+ }
669+
670+ var stdout bytes.Buffer
671+ copyDone := make (chan error , 1 )
672+ go func () {
673+ _ , copyErr := io .Copy (io .MultiWriter (& stdout , callbackWriter {cb : stream .OnStdout }), ptyFile )
674+ copyDone <- copyErr
675+ }()
676+
677+ err = cmd .Wait ()
678+ _ = ptyFile .Close ()
679+ copyErr := <- copyDone
680+ if copyErr != nil && ! expectedPTYReadError (copyErr ) && err == nil {
681+ err = copyErr
682+ }
683+
684+ if err == nil {
685+ return 0 , stdout .String (), "" , nil
686+ }
687+
688+ var exitErr * exec.ExitError
689+ if errors .As (err , & exitErr ) {
690+ return exitErr .ExitCode (), stdout .String (), "" , nil
691+ }
692+ return 1 , stdout .String (), "" , fmt .Errorf ("run host passthrough tty command: %w" , err )
693+ }
694+
695+ func expectedPTYReadError (err error ) bool {
696+ if err == nil {
697+ return false
698+ }
699+ if errors .Is (err , io .EOF ) {
700+ return true
701+ }
702+ var pathErr * os.PathError
703+ if errors .As (err , & pathErr ) {
704+ return errors .Is (pathErr .Err , syscall .EIO )
705+ }
706+ return errors .Is (err , syscall .EIO )
707+ }
708+
633709type guestExecTiming struct {
634710 WaitForAgent time.Duration
635711 AgentReadyAt time.Time
0 commit comments