Skip to content

Commit 42aa0f4

Browse files
authored
Merge pull request #15 from buildkite/feat/console-command
Add interactive console command via AttachExecution
2 parents d6b35d4 + 89f36c1 commit 42aa0f4

File tree

11 files changed

+1022
-24
lines changed

11 files changed

+1022
-24
lines changed

go.mod

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ require (
66
connectrpc.com/connect v1.18.1
77
github.com/alecthomas/kong v1.12.1
88
github.com/charmbracelet/log v0.4.2
9+
github.com/creack/pty v1.1.24
910
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0
1011
github.com/mdlayher/vsock v1.1.1
11-
golang.org/x/sys v0.31.0
12+
golang.org/x/net v0.38.0
13+
golang.org/x/sys v0.32.0
14+
golang.org/x/term v0.31.0
1215
google.golang.org/protobuf v1.36.10
1316
gopkg.in/yaml.v3 v3.0.1
1417
)
@@ -30,6 +33,6 @@ require (
3033
github.com/sirupsen/logrus v1.8.3 // indirect
3134
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
3235
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
33-
golang.org/x/net v0.38.0 // indirect
34-
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
36+
golang.org/x/sync v0.12.0 // indirect
37+
golang.org/x/text v0.23.0 // indirect
3538
)

go.sum

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
224224
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
225225
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
226226
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
227+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
228+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
227229
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
228230
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
229231
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
@@ -823,8 +825,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
823825
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
824826
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
825827
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
826-
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
827828
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
829+
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
830+
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
828831
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
829832
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
830833
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -899,11 +902,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
899902
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
900903
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
901904
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
902-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
903-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
905+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
906+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
904907
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
905908
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
906909
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
910+
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
911+
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
907912
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
908913
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
909914
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

internal/backend/backend.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ type Adapter interface {
1111
Run(ctx context.Context, req RunRequest) (*RunResult, error)
1212
}
1313

14+
type AttachIO struct {
15+
WriteStdin func([]byte) error
16+
ResizeTTY func(cols, rows uint32) error
17+
}
18+
1419
type OutputStream struct {
1520
OnStdout func([]byte)
1621
OnStderr func([]byte)
22+
OnAttach func(AttachIO)
1723
}
1824

1925
// StreamingAdapter can push stdout/stderr chunks while a command is running.
@@ -27,6 +33,7 @@ type RunRequest struct {
2733
RunID string
2834
CWD string
2935
Command []string
36+
TTY bool
3037
Policy *policy.CompiledPolicy
3138
FirecrackerConfig
3239
}

internal/backend/firecracker/backend.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
633709
type guestExecTiming struct {
634710
WaitForAgent time.Duration
635711
AgentReadyAt time.Time

0 commit comments

Comments
 (0)