You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buildx/monitor/dap/dap.go

1191 lines
34 KiB
Go

package dap
// Ported from https://github.com/ktock/buildg/blob/v0.4.1/pkg/dap/dap.go
// Copyright The buildg Authors.
// Licensed under the Apache License, Version 2.0
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"text/tabwriter"
"time"
"github.com/docker/buildx/controller"
"github.com/docker/buildx/controller/control"
controllererror "github.com/docker/buildx/controller/errdefs"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/walker"
"github.com/docker/cli/cli/command"
"github.com/google/go-dap"
"github.com/google/shlex"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/solver/pb"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"golang.org/x/sync/errgroup"
)
const AttachContainerCommand = "_INTERNAL_DAP_ATTACH_CONTAINER"
func NewServer(dockerCli command.Cli, r io.Reader, w io.Writer) (*Server, error) {
conn := &stdioConn{r, w}
ctx, cancel := context.WithCancel(context.TODO())
eg := new(errgroup.Group)
s := &Server{
conn: conn,
ctx: ctx,
cancel: cancel,
eg: eg,
breakpoints: &sync.Map{},
dockerCli: dockerCli,
}
return s, nil
}
type debugContext struct {
breakCtx atomic.Value
launchConfig *LaunchConfig
dockerfileName string
walkerController *walker.Controller
controller control.BuildxController
ref string
cancel func()
}
type Server struct {
conn net.Conn
sendMu sync.Mutex
ctx context.Context
cancel func()
eg *errgroup.Group
breakpoints *sync.Map
dockerCli command.Cli
debugCtx atomic.Value
}
func (s *Server) Serve() error {
var eg errgroup.Group
r := bufio.NewReader(s.conn)
for {
req, err := dap.ReadProtocolMessage(r)
if err != nil {
if err == io.EOF {
break
}
return err
}
eg.Go(func() error { return s.handle(req) })
}
return eg.Wait()
}
func (s *Server) send(message dap.Message) {
s.sendMu.Lock()
dap.WriteProtocolMessage(s.conn, message)
logrus.WithField("dst", s.conn.RemoteAddr()).Debugf("message sent %+v", message)
s.sendMu.Unlock()
}
func (s *Server) breakContext() *walker.BreakContext {
var bCtx *walker.BreakContext
if dbgCtx := s.debugContext(); dbgCtx != nil {
if bc := dbgCtx.breakCtx.Load(); bc != nil {
bCtx = bc.(*walker.BreakContext)
}
}
return bCtx
}
func (s *Server) debugContext() *debugContext {
if dbgCtx := s.debugCtx.Load(); dbgCtx != nil {
return dbgCtx.(*debugContext)
}
return nil
}
const (
unsupportedError = 1000
failedError = 1001
unknownError = 9999
)
var errorMessage = map[int]string{
unsupportedError: "unsupported",
failedError: "failed",
unknownError: "unknown",
}
func (s *Server) sendErrorResponse(requestSeq int, command string, errID int, message string, showUser bool) {
id, summary := unknownError, errorMessage[unknownError]
if m, ok := errorMessage[errID]; ok {
id, summary = errID, m
}
r := &dap.ErrorResponse{}
r.Response = *newResponse(requestSeq, command)
r.Success = false
r.Message = summary
r.Body.Error = &dap.ErrorMessage{}
r.Body.Error.Format = message
r.Body.Error.Id = id
r.Body.Error.ShowUser = showUser
s.send(r)
}
func (s *Server) sendUnsupportedResponse(requestSeq int, command string, message string) {
s.sendErrorResponse(requestSeq, command, unsupportedError, message, false)
}
func (s *Server) outputStdoutWriter() io.Writer {
return &outputWriter{s, "stdout"}
}
func (s *Server) handle(request dap.Message) error {
logrus.Debugf("got request: %+v", request)
switch request := request.(type) {
case *dap.InitializeRequest:
s.onInitializeRequest(request)
case *dap.LaunchRequest:
s.onLaunchRequest(request)
case *dap.AttachRequest:
s.onAttachRequest(request)
case *dap.DisconnectRequest:
s.onDisconnectRequest(request)
case *dap.TerminateRequest:
s.onTerminateRequest(request)
case *dap.RestartRequest:
s.onRestartRequest(request)
case *dap.SetBreakpointsRequest:
s.onSetBreakpointsRequest(request)
case *dap.SetFunctionBreakpointsRequest:
s.onSetFunctionBreakpointsRequest(request)
case *dap.SetExceptionBreakpointsRequest:
s.onSetExceptionBreakpointsRequest(request)
case *dap.ConfigurationDoneRequest:
s.onConfigurationDoneRequest(request)
case *dap.ContinueRequest:
s.onContinueRequest(request)
case *dap.NextRequest:
s.onNextRequest(request)
case *dap.StepInRequest:
s.onStepInRequest(request)
case *dap.StepOutRequest:
s.onStepOutRequest(request)
case *dap.StepBackRequest:
s.onStepBackRequest(request)
case *dap.ReverseContinueRequest:
s.onReverseContinueRequest(request)
case *dap.RestartFrameRequest:
s.onRestartFrameRequest(request)
case *dap.GotoRequest:
s.onGotoRequest(request)
case *dap.PauseRequest:
s.onPauseRequest(request)
case *dap.StackTraceRequest:
s.onStackTraceRequest(request)
case *dap.ScopesRequest:
s.onScopesRequest(request)
case *dap.VariablesRequest:
s.onVariablesRequest(request)
case *dap.SetVariableRequest:
s.onSetVariableRequest(request)
case *dap.SetExpressionRequest:
s.onSetExpressionRequest(request)
case *dap.SourceRequest:
s.onSourceRequest(request)
case *dap.ThreadsRequest:
s.onThreadsRequest(request)
case *dap.TerminateThreadsRequest:
s.onTerminateThreadsRequest(request)
case *dap.EvaluateRequest:
s.onEvaluateRequest(request)
case *dap.StepInTargetsRequest:
s.onStepInTargetsRequest(request)
case *dap.GotoTargetsRequest:
s.onGotoTargetsRequest(request)
case *dap.CompletionsRequest:
s.onCompletionsRequest(request)
case *dap.ExceptionInfoRequest:
s.onExceptionInfoRequest(request)
case *dap.LoadedSourcesRequest:
s.onLoadedSourcesRequest(request)
case *dap.DataBreakpointInfoRequest:
s.onDataBreakpointInfoRequest(request)
case *dap.SetDataBreakpointsRequest:
s.onSetDataBreakpointsRequest(request)
case *dap.ReadMemoryRequest:
s.onReadMemoryRequest(request)
case *dap.DisassembleRequest:
s.onDisassembleRequest(request)
case *dap.CancelRequest:
s.onCancelRequest(request)
case *dap.BreakpointLocationsRequest:
s.onBreakpointLocationsRequest(request)
default:
logrus.Warnf("Unable to process %#v\n", request)
}
return nil
}
func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
response := &dap.InitializeResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body.SupportsConfigurationDoneRequest = true
response.Body.SupportsFunctionBreakpoints = false
response.Body.SupportsConditionalBreakpoints = false
response.Body.SupportsHitConditionalBreakpoints = false
response.Body.SupportsEvaluateForHovers = false
response.Body.ExceptionBreakpointFilters = make([]dap.ExceptionBreakpointsFilter, 0)
response.Body.SupportsStepBack = false
response.Body.SupportsSetVariable = false
response.Body.SupportsRestartFrame = false
response.Body.SupportsGotoTargetsRequest = false
response.Body.SupportsStepInTargetsRequest = false
response.Body.SupportsCompletionsRequest = false
response.Body.CompletionTriggerCharacters = make([]string, 0)
response.Body.SupportsModulesRequest = false
response.Body.AdditionalModuleColumns = make([]dap.ColumnDescriptor, 0)
response.Body.SupportedChecksumAlgorithms = make([]dap.ChecksumAlgorithm, 0)
response.Body.SupportsRestartRequest = false
response.Body.SupportsExceptionOptions = false
response.Body.SupportsValueFormattingOptions = false
response.Body.SupportsExceptionInfoRequest = false
response.Body.SupportTerminateDebuggee = false
response.Body.SupportSuspendDebuggee = false
response.Body.SupportsDelayedStackTraceLoading = false
response.Body.SupportsLoadedSourcesRequest = false
response.Body.SupportsLogPoints = false
response.Body.SupportsTerminateThreadsRequest = false
response.Body.SupportsSetExpression = false
response.Body.SupportsTerminateRequest = false
response.Body.SupportsDataBreakpoints = false
response.Body.SupportsReadMemoryRequest = false
response.Body.SupportsWriteMemoryRequest = false
response.Body.SupportsDisassembleRequest = false
response.Body.SupportsCancelRequest = false
response.Body.SupportsBreakpointLocationsRequest = false
response.Body.SupportsClipboardContext = false
response.Body.SupportsSteppingGranularity = false
response.Body.SupportsInstructionBreakpoints = false
response.Body.SupportsExceptionFilterOptions = false
s.send(response)
s.send(&dap.InitializedEvent{Event: *newEvent("initialized")})
}
func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
cfg := new(LaunchConfig)
if err := json.Unmarshal(request.Arguments, cfg); err != nil {
s.sendErrorResponse(request.Seq, request.Command, failedError, fmt.Sprintf("failed to launch: %v", err), true)
return
}
if err := s.launchDebugger(*cfg); err != nil {
s.sendErrorResponse(request.Seq, request.Command, failedError, fmt.Sprintf("failed to launch: %v", err), true)
return
}
response := &dap.LaunchResponse{}
response.Response = *newResponse(request.Seq, request.Command)
s.send(response)
}
type LaunchConfig struct {
Program string `json:"program"`
StopOnEntry bool `json:"stopOnEntry"`
Target string `json:"target"`
BuildArgs []string `json:"build-args"`
SSH []string `json:"ssh"`
Secrets []string `json:"secrets"`
Root string `json:"root"`
ControllerMode string `json:"controller-mode"` // "local" or "remote" (default)
ServerConfig string `json:"server-config"`
}
func parseLaunchConfig(cfg LaunchConfig) (bo controllerapi.BuildOptions, _ error) {
if cfg.Program == "" {
return bo, errors.Errorf("program must be specified")
}
contextPath, dockerfile := filepath.Split(cfg.Program)
bo.ContextPath = contextPath
bo.DockerfileName = filepath.Join(contextPath, dockerfile)
if target := cfg.Target; target != "" {
bo.Target = target
}
bo.BuildArgs = listToMap(cfg.BuildArgs, true)
bo.Exports = append(bo.Exports, &controllerapi.ExportEntry{
Type: "image",
})
var err error
bo.Secrets, err = buildflags.ParseSecretSpecs(cfg.Secrets)
if err != nil {
return bo, err
}
bo.SSH, err = buildflags.ParseSSHSpecs(cfg.SSH)
if err != nil {
return bo, err
}
// TODO
// - CacheFrom, CacheTo
// - Contexts
// - ExtraHosts
// ...
return bo, nil
}
func (s *Server) launchDebugger(cfg LaunchConfig) (retErr error) {
if cfg.Program == "" {
return errors.Errorf("launch error: program must be specified")
}
if dbgCtx := s.debugContext(); dbgCtx != nil && dbgCtx.cancel != nil {
dbgCtx.cancel()
}
buildOpt, err := parseLaunchConfig(cfg)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.TODO())
defer func() {
if retErr != nil {
cancel()
}
}()
var printer *progress.Printer
printer, err = progress.NewPrinter(ctx, s.outputStdoutWriter(), nil, "plain",
progress.WithOnClose(func() {
printWarnings(s.outputStdoutWriter(), printer.Warnings())
}),
)
if err != nil {
return err
}
c, err := controller.NewController(ctx, control.ControlOptions{
Detach: cfg.ControllerMode != "local",
ServerConfig: cfg.ServerConfig,
Root: cfg.Root,
}, s.dockerCli, printer)
if err != nil {
return err
}
buildOpt.Debug = true // we don't get the result but get only the build definition via error.
ref, _, err := c.Build(ctx, buildOpt, nil, printer)
if err != nil {
var be *controllererror.BuildError
if errors.As(err, &be) {
ref = be.Ref
// We can proceed to dap session
} else {
return err
}
}
st, err := c.Inspect(ctx, ref)
if err != nil {
return err
}
bpsA, _ := s.breakpoints.LoadOrStore(buildOpt.DockerfileName, walker.NewBreakpoints())
bps := bpsA.(*walker.Breakpoints)
if cfg.StopOnEntry {
bps.Add("stopOnEntry", walker.NewStopOnEntryBreakpoint())
} // TODO: clear on false?
bps.Add("onError", walker.NewOnErrorBreakpoint()) // always break on error
dbgCtx := &debugContext{}
doneCh := make(chan struct{})
wc := walker.NewController(st.Definition, bps,
func(ctx context.Context, bCtx *walker.BreakContext) error {
for key, r := range bCtx.Hits {
logrus.Debugf("Breakpoint[%s]: %v", key, r)
}
breakpoints := make([]int, 0)
for si := range bCtx.Hits {
keyI, err := strconv.ParseInt(si, 10, 64)
if err != nil {
logrus.WithError(err).Warnf("failed to parse breakpoint key")
continue
}
breakpoints = append(breakpoints, int(keyI))
}
reason := "breakpoint"
if len(breakpoints) == 0 {
reason = "step"
}
s.send(&dap.StoppedEvent{
Event: *newEvent("stopped"),
Body: dap.StoppedEventBody{Reason: reason, ThreadId: 1, AllThreadsStopped: true, HitBreakpointIds: breakpoints},
})
dbgCtx.breakCtx.Store(bCtx)
return nil
},
func(ctx context.Context, st llb.State) error {
def, err := st.Marshal(ctx)
if err != nil {
return errors.Errorf("solve: failed to marshal definition: %v", err)
}
return c.Solve(ctx, ref, def.ToPB(), printer)
},
func(err error) {
s.breakpoints.Delete(buildOpt.DockerfileName)
s.send(&dap.ThreadEvent{Event: *newEvent("thread"), Body: dap.ThreadEventBody{Reason: "exited", ThreadId: 1}})
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
s.send(&dap.ExitedEvent{Event: *newEvent("exited")})
close(doneCh)
},
)
dbgCtx.launchConfig = &cfg
dbgCtx.dockerfileName = buildOpt.DockerfileName
dbgCtx.walkerController = wc
dbgCtx.controller = c
dbgCtx.ref = ref
var once sync.Once
dbgCtx.cancel = func() {
once.Do(func() {
cancel()
wc.WalkCancel()
<-doneCh
})
}
s.debugCtx.Store(dbgCtx)
// notify started
s.send(&dap.ThreadEvent{Event: *newEvent("thread"), Body: dap.ThreadEventBody{Reason: "started", ThreadId: 1}})
if err := wc.StartWalk(); err != nil {
return err
}
return nil
}
func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
if s.cancel != nil {
s.cancel()
}
if err := s.eg.Wait(); err != nil { // wait for container cleanup
logrus.WithError(err).Warnf("failed to close tasks")
}
if dbgCtx := s.debugContext(); dbgCtx != nil && dbgCtx.cancel != nil {
dbgCtx.cancel()
}
response := &dap.DisconnectResponse{}
response.Response = *newResponse(request.Seq, request.Command)
s.send(response)
os.Exit(0) // TODO: return the control to the main func if needed
}
func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
args := request.Arguments
breakpoints := make([]dap.Breakpoint, 0)
bpsA, _ := s.breakpoints.LoadOrStore(args.Source.Path, walker.NewBreakpoints())
bps := bpsA.(*walker.Breakpoints)
bps.ClearAll()
bps.Add("onError", walker.NewOnErrorBreakpoint()) // always break on error
for i := 0; i < len(args.Breakpoints); i++ {
bp := walker.NewLineBreakpoint(int64(args.Breakpoints[i].Line))
key, err := bps.Add("", bp)
if err != nil {
logrus.WithError(err).Warnf("failed to add breakpoints")
continue
}
keyI, err := strconv.ParseInt(key, 10, 64)
if err != nil {
logrus.WithError(err).Warnf("failed to parse breakpoint key")
continue
}
breakpoints = append(breakpoints, dap.Breakpoint{
Id: int(keyI),
Source: &args.Source,
Line: args.Breakpoints[i].Line,
Verified: true,
})
}
response := &dap.SetBreakpointsResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body.Breakpoints = breakpoints
s.send(response)
}
func (s *Server) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest) {
response := &dap.ConfigurationDoneResponse{}
response.Response = *newResponse(request.Seq, request.Command)
s.send(response)
}
func (s *Server) onContinueRequest(request *dap.ContinueRequest) {
response := &dap.ContinueResponse{}
response.Response = *newResponse(request.Seq, request.Command)
s.send(response)
if dbgCtx := s.debugContext(); dbgCtx != nil {
if wc := dbgCtx.walkerController; wc != nil {
wc.Continue()
}
}
}
func (s *Server) onNextRequest(request *dap.NextRequest) {
response := &dap.NextResponse{}
response.Response = *newResponse(request.Seq, request.Command)
s.send(response)
if dbgCtx := s.debugContext(); dbgCtx != nil {
if wc := dbgCtx.walkerController; wc != nil {
wc.Next()
}
}
}
func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
response := &dap.StackTraceResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body = dap.StackTraceResponseBody{
StackFrames: make([]dap.StackFrame, 0),
}
bCtx := s.breakContext()
var launchConfig *LaunchConfig
if dbgCtx := s.debugContext(); dbgCtx != nil {
launchConfig = dbgCtx.launchConfig
}
if bCtx == nil || launchConfig == nil {
// no stack trace is available now
s.send(response)
return
}
var lines []*pb.Range
// If there are hit breakpoints on the current Op, return them.
// FIXME: This is a workaround against stackFrame doesn't support
// multiple sources per frame. Once dap support it, we can
// return all current locations.
// TODO: show non-breakpoint locations to output as well
for _, ranges := range bCtx.Hits {
lines = append(lines, ranges...)
}
if len(lines) == 0 {
// no breakpoints on the current Op. This can happen on
// step execution.
for _, r := range bCtx.Cursors {
rr := r
lines = append(lines, &rr)
}
}
if len(lines) > 0 {
name := "instruction"
if _, _, op, _, err := bCtx.State.Output().Vertex(context.TODO(), nil).Marshal(context.TODO(), nil); err == nil {
if n, ok := op.Description["com.docker.dockerfile.v1.command"]; ok {
name = n
}
}
f := launchConfig.Program
response.Body.StackFrames = []dap.StackFrame{
{
Id: 0,
Source: &dap.Source{Name: filepath.Base(f), Path: f},
// FIXME: We only return lines[0] because stackFrame doesn't support
// multiple sources per frame. Once dap support it, we can
// return all current locations.
Line: int(lines[0].Start.Line),
EndLine: int(lines[0].End.Line),
Name: name,
},
}
response.Body.TotalFrames = 1
}
s.send(response)
}
func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
response := &dap.ScopesResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body = dap.ScopesResponseBody{
Scopes: []dap.Scope{
{
Name: "Environment Variables",
VariablesReference: 1,
},
},
}
s.send(response)
}
func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
response := &dap.VariablesResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body = dap.VariablesResponseBody{
Variables: make([]dap.Variable, 0), // neovim doesn't allow nil
}
bCtx := s.breakContext()
if bCtx == nil {
s.send(response)
return
}
var variables []dap.Variable
_, dt, _, _, err := bCtx.State.Output().Vertex(context.TODO(), nil).Marshal(context.TODO(), nil)
if err != nil {
logrus.WithError(err).Warnf("failed to marshal execop")
s.send(response)
return
}
var pbop pb.Op
if err := pbop.Unmarshal(dt); err != nil {
logrus.WithError(err).Warnf("failed to unmarshal execop")
s.send(response)
return
}
switch op := pbop.GetOp().(type) {
case *pb.Op_Exec:
for _, e := range op.Exec.Meta.Env {
var k, v string
if kv := strings.SplitN(e, "=", 2); len(kv) >= 2 {
k, v = kv[0], kv[1]
} else if len(kv) == 1 {
k = kv[0]
} else {
continue
}
variables = append(variables, dap.Variable{
Name: k,
Value: v,
})
}
default:
// TODO: support other Ops
}
if s := request.Arguments.Start; s > 0 {
if s < len(variables) {
variables = variables[s:]
} else {
variables = nil
}
}
if c := request.Arguments.Count; c > 0 {
if c < len(variables) {
variables = variables[:c]
}
}
response.Body.Variables = append(response.Body.Variables, variables...)
s.send(response)
}
func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
response := &dap.ThreadsResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body = dap.ThreadsResponseBody{Threads: []dap.Thread{{Id: 1, Name: "build"}}}
s.send(response)
}
type handlerContext struct {
breakContext *walker.BreakContext
stdout io.Writer
evaluateDoneCallback func()
controller control.BuildxController
ref string
launchConfig *LaunchConfig
}
type replCommand func(ctx context.Context, hCtx *handlerContext) cli.Command
func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
if request.Arguments.Context != "repl" {
s.sendUnsupportedResponse(request.Seq, request.Command, "evaluating non-repl input is unsupported as of now")
return
}
bCtx := s.breakContext()
if bCtx == nil {
s.sendErrorResponse(request.Seq, request.Command, failedError, "no breakpoint available", true)
return
}
replCommands := []replCommand{s.execCommand, s.psCommand, s.attachCommand}
hCtx := new(handlerContext)
out := new(bytes.Buffer)
if args, err := shlex.Split(request.Arguments.Expression); err != nil {
logrus.WithError(err).Warnf("failed to parse line")
} else if len(args) > 0 && args[0] != "" {
app := cli.NewApp()
rootCmd := "buildx"
app.Name = rootCmd
app.HelpName = rootCmd
app.Usage = "Buildx Interactive Debugger"
app.UsageText = "command [command options] [arguments...]"
app.ExitErrHandler = func(context *cli.Context, err error) {}
app.UseShortOptionHandling = true
app.Writer = out
hCtx = &handlerContext{
breakContext: bCtx,
stdout: out,
}
dbgCtx := s.debugContext()
if dbgCtx == nil {
s.sendErrorResponse(request.Seq, request.Command, failedError, "debugger isn't launched", true)
return
}
hCtx.controller = dbgCtx.controller
hCtx.ref = dbgCtx.ref
hCtx.launchConfig = dbgCtx.launchConfig
for _, fn := range replCommands {
app.Commands = append(app.Commands, fn(s.ctx, hCtx)) // s.ctx is cancelled on disconnect
}
if err := app.Run(append([]string{rootCmd}, args...)); err != nil {
out.WriteString(err.Error() + "\n")
}
}
response := &dap.EvaluateResponse{}
response.Response = *newResponse(request.Seq, request.Command)
response.Body = dap.EvaluateResponseBody{
Result: out.String(),
}
s.send(response)
if hCtx.evaluateDoneCallback != nil {
hCtx.evaluateDoneCallback()
}
}
func (s *Server) psCommand(ctx context.Context, hCtx *handlerContext) cli.Command {
return cli.Command{
Name: "ps",
Usage: "List attachable processes.",
UsageText: "ps",
Action: func(clicontext *cli.Context) (retErr error) {
// gctx := s.ctx // cancelled on disconnect
plist, err := hCtx.controller.ListProcesses(ctx, hCtx.ref)
if err != nil {
return err
}
tw := tabwriter.NewWriter(hCtx.stdout, 1, 8, 1, '\t', 0)
fmt.Fprintln(tw, "PID\tCOMMAND")
for _, p := range plist {
fmt.Fprintf(tw, "%-20s\t%v\n", p.ProcessID, append(p.InvokeConfig.Entrypoint, p.InvokeConfig.Cmd...))
}
tw.Flush()
return nil
},
}
}
func (s *Server) attachCommand(ctx context.Context, hCtx *handlerContext) cli.Command {
return cli.Command{
Name: "attach",
Usage: "Attach to a processes.",
UsageText: "attach PID",
Action: func(clicontext *cli.Context) (retErr error) {
args := clicontext.Args()
if len(args) == 0 || args[0] == "" {
return errors.Errorf("specify PID")
}
return s.invoke(ctx, hCtx, args[0], controllerapi.InvokeConfig{}, true, true)
},
}
}
func (s *Server) execCommand(ctx context.Context, hCtx *handlerContext) cli.Command {
return cli.Command{
Name: "exec",
Aliases: []string{"e"},
Usage: "Execute command in the step",
UsageText: `exec [OPTIONS] [ARGS...]
If ARGS isn't provided, "/bin/sh" is used by default.
`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "init-state",
Usage: "Execute commands in an initial state of that step",
},
cli.BoolTFlag{
Name: "tty,t",
Usage: "Allocate tty (enabled by default)",
},
cli.BoolTFlag{
Name: "i",
Usage: "Enable stdin (FIXME: must be set with tty) (enabled by default)",
},
cli.StringSliceFlag{
Name: "env,e",
Usage: "Set environment variables",
},
cli.StringFlag{
Name: "workdir,w",
Usage: "Working directory inside the container",
},
cli.BoolFlag{
Name: "rollback",
Usage: "Kill running processes and recreate the debugging container",
},
},
Action: func(clicontext *cli.Context) (retErr error) {
args := clicontext.Args()
if len(args) == 0 || args[0] == "" {
args = []string{"/bin/sh"}
}
flagI := clicontext.Bool("i")
flagT := clicontext.Bool("tty")
if flagI && !flagT || !flagI && flagT {
return errors.Errorf("flag \"-i\" and \"-t\" must be set together") // FIXME
}
cwd, noCwd := "", false
if c := clicontext.String("workdir"); c == "" {
noCwd = true
} else {
cwd = c
}
rollback := clicontext.Bool("rollback")
invokeConfig := controllerapi.InvokeConfig{
Entrypoint: []string{args[0]},
Cmd: args[1:],
Env: clicontext.StringSlice("env"),
NoUser: true,
Cwd: cwd,
NoCwd: noCwd,
Tty: clicontext.Bool("tty"),
Initial: clicontext.Bool("init-state"),
Rollback: rollback,
}
pid := identity.NewID()
return s.invoke(ctx, hCtx, pid, invokeConfig, flagI, flagT)
},
}
}
func (s *Server) invoke(ctx context.Context, hCtx *handlerContext, pid string, invokeConfig controllerapi.InvokeConfig, enableStdin, enableTty bool) (retErr error) {
// gCtx := s.ctx // cancelled on disconnect
var cleanups []func()
defer func() {
if retErr != nil {
for i := len(cleanups) - 1; i >= 0; i-- {
cleanups[i]()
}
}
}()
// Prepare state dir
tmpRoot, err := os.MkdirTemp("", "buildx-serve-state")
if err != nil {
return err
}
cleanups = append(cleanups, func() { os.RemoveAll(tmpRoot) })
// Server IO
logrus.Debugf("container root %+v", tmpRoot)
stdin, stdout, stderr, done, err := serveContainerIO(ctx, tmpRoot)
if err != nil {
return err
}
cleanups = append(cleanups, func() { done() })
// Search container client
self, err := os.Executable()
if err != nil {
return err
}
if !enableStdin {
stdin = nil
}
doneCh := make(chan struct{})
errCh := make(chan error)
go func() {
defer close(doneCh)
if err := hCtx.controller.Invoke(context.TODO(), hCtx.ref, pid, invokeConfig, stdin, stdout, stderr); err != nil {
errCh <- err
return
}
}()
// Let the caller to attach to the container after evaluation response received.
hCtx.evaluateDoneCallback = func() {
s.send(&dap.RunInTerminalRequest{
Request: dap.Request{
ProtocolMessage: dap.ProtocolMessage{
Seq: 0,
Type: "request",
},
Command: "runInTerminal",
},
Arguments: dap.RunInTerminalRequestArguments{
Kind: "integrated",
Title: "containerclient",
Args: []string{self, AttachContainerCommand, "--set-tty-raw=" + strconv.FormatBool(enableTty), tmpRoot},
// TODO: use envvar once all editors support it.
// Env: map[string]interface{}{"BUILDX_EXPERIMENTAL": "1"},
// emacs requires this nonempty otherwise error (Wrong type argument: stringp, nil) will occur on dap-ui-breakpoints()
Cwd: filepath.Dir(hCtx.launchConfig.Program),
},
})
}
s.eg.Go(func() error {
select {
case <-doneCh:
s.outputStdoutWriter().Write([]byte("container finished"))
case err := <-errCh:
s.outputStdoutWriter().Write([]byte(fmt.Sprintf("container finished(%v)", err)))
case err := <-ctx.Done():
s.outputStdoutWriter().Write([]byte(fmt.Sprintf("finishing container due to server shutdown: %v", err)))
}
for i := len(cleanups) - 1; i >= 0; i-- {
cleanups[i]()
}
select {
case <-doneCh:
case err := <-errCh:
s.outputStdoutWriter().Write([]byte(fmt.Sprintf("container exit(%v)", err)))
case <-time.After(3 * time.Second):
s.outputStdoutWriter().Write([]byte("container exit timeout"))
}
return nil
})
return nil
}
func (s *Server) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "Request unsupported")
}
func (s *Server) onRestartRequest(request *dap.RestartRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "RestartRequest unsupported")
}
func (s *Server) onAttachRequest(request *dap.AttachRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "AttachRequest unsupported")
}
func (s *Server) onTerminateRequest(request *dap.TerminateRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "TerminateRequest unsupported")
}
func (s *Server) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpointsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "FunctionBreakpointsRequest unsupported")
}
func (s *Server) onStepInRequest(request *dap.StepInRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "StepInRequest unsupported")
}
func (s *Server) onStepOutRequest(request *dap.StepOutRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "StepOutRequest unsupported")
}
func (s *Server) onStepBackRequest(request *dap.StepBackRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "StepBackRequest unsupported")
}
func (s *Server) onReverseContinueRequest(request *dap.ReverseContinueRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "ReverseContinueRequest unsupported")
}
func (s *Server) onRestartFrameRequest(request *dap.RestartFrameRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "RestartFrameRequest unsupported")
}
func (s *Server) onGotoRequest(request *dap.GotoRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "GotoRequest unsupported")
}
func (s *Server) onPauseRequest(request *dap.PauseRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "PauseRequest unsupported")
}
func (s *Server) onSetVariableRequest(request *dap.SetVariableRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "SetVariablesRequest unsupported")
}
func (s *Server) onSetExpressionRequest(request *dap.SetExpressionRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "SetExpressionRequest unsupported")
}
func (s *Server) onSourceRequest(request *dap.SourceRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "SourceRequest unsupported")
}
func (s *Server) onTerminateThreadsRequest(request *dap.TerminateThreadsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "TerminateRequest unsupported")
}
func (s *Server) onStepInTargetsRequest(request *dap.StepInTargetsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "StepInTargetsRequest unsupported")
}
func (s *Server) onGotoTargetsRequest(request *dap.GotoTargetsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "GotoTargetsRequest unsupported")
}
func (s *Server) onCompletionsRequest(request *dap.CompletionsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "CompletionsRequest unsupported")
}
func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "ExceptionInfoRequest unsupported")
}
func (s *Server) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "LoadedSourcesRequest unsupported")
}
func (s *Server) onDataBreakpointInfoRequest(request *dap.DataBreakpointInfoRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "DataBreakpointInfoRequest unsupported")
}
func (s *Server) onSetDataBreakpointsRequest(request *dap.SetDataBreakpointsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "SetDataBreakpointsRequest unsupported")
}
func (s *Server) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "ReadMemoryRequest unsupported")
}
func (s *Server) onDisassembleRequest(request *dap.DisassembleRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "DisassembleRequest unsupported")
}
func (s *Server) onCancelRequest(request *dap.CancelRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "CancelRequest unsupported")
}
func (s *Server) onBreakpointLocationsRequest(request *dap.BreakpointLocationsRequest) {
s.sendUnsupportedResponse(request.Seq, request.Command, "BreakpointLocationsRequest unsupported")
}
type outputWriter struct {
s *Server
category string
}
func (w *outputWriter) Write(p []byte) (int, error) {
w.s.send(&dap.OutputEvent{Event: *newEvent("output"), Body: dap.OutputEventBody{Category: w.category, Output: string(p)}})
return len(p), nil
}
func newEvent(event string) *dap.Event {
return &dap.Event{
ProtocolMessage: dap.ProtocolMessage{
Seq: 0,
Type: "event",
},
Event: event,
}
}
func newResponse(requestSeq int, command string) *dap.Response {
return &dap.Response{
ProtocolMessage: dap.ProtocolMessage{
Seq: 0,
Type: "response",
},
Command: command,
RequestSeq: requestSeq,
Success: true,
}
}
func printWarnings(w io.Writer, warnings []client.VertexWarning) {
if len(warnings) == 0 {
return
}
fmt.Fprintf(w, "\n ")
sb := &bytes.Buffer{}
if len(warnings) == 1 {
fmt.Fprintf(sb, "1 warning found")
} else {
fmt.Fprintf(sb, "%d warnings found", len(warnings))
}
fmt.Fprintf(sb, ":\n")
for _, warn := range warnings {
fmt.Fprintf(w, " - %s\n", warn.Short)
for _, d := range warn.Detail {
fmt.Fprintf(w, "%s\n", d)
}
if warn.URL != "" {
fmt.Fprintf(w, "More info: %s\n", warn.URL)
}
if warn.SourceInfo != nil && warn.Range != nil {
src := errdefs.Source{
Info: warn.SourceInfo,
Ranges: warn.Range,
}
src.Print(w)
}
fmt.Fprintf(w, "\n")
}
}
func listToMap(values []string, defaultEnv bool) map[string]string {
result := make(map[string]string, len(values))
for _, value := range values {
kv := strings.SplitN(value, "=", 2)
if len(kv) == 1 {
if defaultEnv {
v, ok := os.LookupEnv(kv[0])
if ok {
result[kv[0]] = v
}
} else {
result[kv[0]] = ""
}
} else {
result[kv[0]] = kv[1]
}
}
return result
}
type stdioConn struct {
io.Reader
io.Writer
}
func (c *stdioConn) Read(b []byte) (n int, err error) {
return c.Reader.Read(b)
}
func (c *stdioConn) Write(b []byte) (n int, err error) {
return c.Writer.Write(b)
}
func (c *stdioConn) Close() error { return nil }
func (c *stdioConn) LocalAddr() net.Addr { return dummyAddr{} }
func (c *stdioConn) RemoteAddr() net.Addr { return dummyAddr{} }
func (c *stdioConn) SetDeadline(t time.Time) error { return nil }
func (c *stdioConn) SetReadDeadline(t time.Time) error { return nil }
func (c *stdioConn) SetWriteDeadline(t time.Time) error { return nil }
type dummyAddr struct{}
func (a dummyAddr) Network() string { return "dummy" }
func (a dummyAddr) String() string { return "dummy" }