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.
1191 lines
34 KiB
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" }
|