Merge pull request #3701 from jsternberg/dap-fix-can-invoke

dap: fix the check to determine whether exec will succeed
This commit is contained in:
Tõnis Tiigi
2026-03-17 09:36:00 -07:00
committed by GitHub
3 changed files with 80 additions and 56 deletions
+54
View File
@@ -4,6 +4,9 @@ import (
"context"
_ "crypto/sha256" // ensure digests can be computed
"io"
"io/fs"
"os"
"path"
"sync"
"sync/atomic"
"syscall"
@@ -146,6 +149,57 @@ func (c *Container) Exec(ctx context.Context, cfg *InvokeConfig, stdin io.ReadCl
return err
}
func (c *Container) CanInvoke(ctx context.Context, cfg *InvokeConfig) error {
var cmd string
if len(cfg.Entrypoint) > 0 {
cmd = cfg.Entrypoint[0]
} else if len(cfg.Cmd) > 0 {
cmd = cfg.Cmd[0]
}
if cmd == "" {
return errors.New("no command specified")
}
const symlinkResolutionLimit = 40
for range symlinkResolutionLimit {
fpath, index, err := c.resultCtx.inferMountIndex(cmd, cfg)
if err != nil {
return err
}
st, err := c.container.StatFile(ctx, gateway.StatContainerRequest{
StatRequest: gateway.StatRequest{
Path: fpath,
},
MountIndex: index,
})
if err != nil {
return errors.Wrapf(err, "stat error: %s", cmd)
}
mode := fs.FileMode(st.Mode)
if mode&os.ModeSymlink != 0 {
// Follow the link.
if path.IsAbs(st.Linkname) {
cmd = st.Linkname
} else {
cmd = path.Join(path.Dir(fpath), st.Linkname)
}
continue
}
if !mode.IsRegular() {
return errors.Errorf("%s: not a file", cmd)
}
if mode&0o111 == 0 {
return errors.Errorf("%s: not an executable", cmd)
}
return nil
}
return errors.Errorf("%s: reached symlink resolution limit", cmd)
}
func (c *Container) ReadFile(ctx context.Context, req gateway.ReadContainerRequest) ([]byte, error) {
return c.container.ReadFile(ctx, req)
}
+18 -20
View File
@@ -7,7 +7,7 @@ import (
"encoding/json"
"io"
iofs "io/fs"
"path/filepath"
"path"
"slices"
"strings"
"sync"
@@ -19,7 +19,6 @@ import (
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tonistiigi/fsutil/types"
)
// NewResultHandle stores a gateway client, gateway reference, and the error from
@@ -81,38 +80,37 @@ func (r *ResultHandle) NewContainer(ctx context.Context, cfg *InvokeConfig) (gat
return r.gwClient.NewContainer(ctx, req)
}
func (r *ResultHandle) StatFile(ctx context.Context, fpath string, cfg *InvokeConfig) (*types.Stat, error) {
func (r *ResultHandle) inferMountIndex(fpath string, cfg *InvokeConfig) (string, int, error) {
containerCfg, err := r.getContainerConfig(cfg)
if err != nil {
return nil, err
return "", 0, err
}
candidateMounts := make([]gateway.Mount, 0, len(containerCfg.Mounts))
for _, m := range containerCfg.Mounts {
type mountCandidate struct {
gateway.Mount
Index int
}
candidateMounts := make([]mountCandidate, 0, len(containerCfg.Mounts))
for i, m := range containerCfg.Mounts {
if strings.HasPrefix(fpath, m.Dest) {
candidateMounts = append(candidateMounts, m)
candidateMounts = append(candidateMounts, mountCandidate{
Mount: m,
Index: i,
})
}
}
if len(candidateMounts) == 0 {
return nil, iofs.ErrNotExist
return "", 0, iofs.ErrNotExist
}
slices.SortFunc(candidateMounts, func(a, b gateway.Mount) int {
slices.SortFunc(candidateMounts, func(a, b mountCandidate) int {
return cmp.Compare(len(a.Dest), len(b.Dest))
})
m := candidateMounts[len(candidateMounts)-1]
relpath, err := filepath.Rel(m.Dest, fpath)
if err != nil {
return nil, err
}
if m.Ref == nil {
return nil, iofs.ErrNotExist
}
req := gateway.StatRequest{Path: filepath.ToSlash(relpath)}
return m.Ref.StatFile(ctx, req)
relpath := strings.TrimPrefix(fpath, m.Dest)
return path.Join("/", relpath), m.Index, nil
}
func (r *ResultHandle) getContainerConfig(cfg *InvokeConfig) (containerCfg gateway.NewContainerRequest, _ error) {
+8 -36
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"io/fs"
"net"
"os"
"path/filepath"
@@ -192,14 +191,6 @@ func (s *shell) attach(ctx context.Context, f dap.StackFrame, rCtx *build.Result
}
}()
// Check if the entrypoint is executable. If it isn't, don't bother
// trying to invoke.
if reason, ok := s.canInvoke(ctx, rCtx, cfg); !ok {
writeLineF(in.Stdout, "Build container is not executable. (reason: %s)", reason)
<-ctx.Done()
return context.Cause(ctx)
}
if err := s.sem.Acquire(ctx, 1); err != nil {
return err
}
@@ -211,6 +202,14 @@ func (s *shell) attach(ctx context.Context, f dap.StackFrame, rCtx *build.Result
}
defer ctr.Cancel()
// Check if the entrypoint is executable. If it isn't, don't bother
// trying to invoke.
if err := ctr.CanInvoke(ctx, cfg); err != nil {
writeLineF(in.Stdout, "Build container is not executable. (reason: %s)", err)
<-ctx.Done()
return context.Cause(ctx)
}
writeLineF(in.Stdout, "Running %s in build container from line %d.",
strings.Join(append(cfg.Entrypoint, cfg.Cmd...), " "),
f.Line,
@@ -231,33 +230,6 @@ func (s *shell) attach(ctx context.Context, f dap.StackFrame, rCtx *build.Result
return nil
}
func (s *shell) canInvoke(ctx context.Context, rCtx *build.ResultHandle, cfg *build.InvokeConfig) (reason string, ok bool) {
var cmd string
if len(cfg.Entrypoint) > 0 {
cmd = cfg.Entrypoint[0]
} else if len(cfg.Cmd) > 0 {
cmd = cfg.Cmd[0]
}
if cmd == "" {
return "no command specified", false
}
st, err := rCtx.StatFile(ctx, cmd, cfg)
if err != nil {
return fmt.Sprintf("stat error: %s", err), false
}
mode := fs.FileMode(st.Mode)
if !mode.IsRegular() {
return fmt.Sprintf("%s: not a file", cmd), false
}
if mode&0111 == 0 {
return fmt.Sprintf("%s: not an executable", cmd), false
}
return "", true
}
// SendRunInTerminalRequest will send the request to the client to attach to
// the socket path that was created by Init. This is intended to be run
// from the adapter and interact directly with the client.