승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
196 lines
5 KiB
Go
196 lines
5 KiB
Go
//go:build darwin || linux
|
|
|
|
package workspace
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
commandRootFD = 3
|
|
commandRecordFD = 4
|
|
commandStatusFD = 5
|
|
commandShimExit = 125
|
|
)
|
|
|
|
func startCommandProcess(record commandLaunchRecord, directory *os.File, output *commandOutput) (*commandProcess, error) {
|
|
encoded, err := json.Marshal(record)
|
|
if err != nil || len(encoded) == 0 || len(encoded) > commandLaunchRecordLimit || directory == nil {
|
|
return nil, errCommandLaunchInvalid
|
|
}
|
|
rootFD, err := unix.Dup(int(directory.Fd()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root := os.NewFile(uintptr(rootFD), "workspace-root")
|
|
recordReader, recordWriter, err := os.Pipe()
|
|
if err != nil {
|
|
_ = root.Close()
|
|
return nil, err
|
|
}
|
|
statusReader, statusWriter, err := os.Pipe()
|
|
if err != nil {
|
|
_ = root.Close()
|
|
_ = recordReader.Close()
|
|
_ = recordWriter.Close()
|
|
return nil, err
|
|
}
|
|
closeAll := func() {
|
|
_ = root.Close()
|
|
_ = recordReader.Close()
|
|
_ = recordWriter.Close()
|
|
_ = statusReader.Close()
|
|
_ = statusWriter.Close()
|
|
}
|
|
|
|
currentExecutable, err := os.Executable()
|
|
if err != nil {
|
|
closeAll()
|
|
return nil, err
|
|
}
|
|
if !filepath.IsAbs(currentExecutable) {
|
|
closeAll()
|
|
return nil, errCommandLaunchInvalid
|
|
}
|
|
cmd := exec.Command(currentExecutable, commandShimArgument)
|
|
cmd.Env = []string{commandShimEnvironment + "=1"}
|
|
cmd.ExtraFiles = []*os.File{root, recordReader, statusWriter}
|
|
cmd.Stdout = output.writer(false)
|
|
cmd.Stderr = output.writer(true)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
if err := cmd.Start(); err != nil {
|
|
closeAll()
|
|
return nil, err
|
|
}
|
|
_ = root.Close()
|
|
_ = recordReader.Close()
|
|
_ = statusWriter.Close()
|
|
|
|
go func() {
|
|
_, _ = io.Copy(recordWriter, bytes.NewReader(encoded))
|
|
_ = recordWriter.Close()
|
|
}()
|
|
launch := make(chan commandLaunchStatus, 1)
|
|
go func() {
|
|
data, readErr := io.ReadAll(io.LimitReader(statusReader, 2))
|
|
_ = statusReader.Close()
|
|
launch <- commandLaunchStatus{started: readErr == nil && len(data) == 0}
|
|
}()
|
|
wait := make(chan error, 1)
|
|
go func() {
|
|
wait <- cmd.Wait()
|
|
}()
|
|
return &commandProcess{
|
|
wait: wait, launch: launch, pid: cmd.Process.Pid,
|
|
exitCode: func() int32 {
|
|
if cmd.ProcessState == nil {
|
|
return -1
|
|
}
|
|
return int32(cmd.ProcessState.ExitCode())
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func runCommandShim() int {
|
|
status := os.NewFile(commandStatusFD, "workspace-command-status")
|
|
fail := func() int {
|
|
if status != nil {
|
|
_, _ = status.Write([]byte{'F'})
|
|
_ = status.Close()
|
|
}
|
|
return commandShimExit
|
|
}
|
|
if status == nil {
|
|
return commandShimExit
|
|
}
|
|
unix.CloseOnExec(commandStatusFD)
|
|
|
|
recordFile := os.NewFile(commandRecordFD, "workspace-command-record")
|
|
root := os.NewFile(commandRootFD, "workspace-root")
|
|
if recordFile == nil || root == nil {
|
|
return fail()
|
|
}
|
|
defer recordFile.Close()
|
|
defer root.Close()
|
|
encoded, err := io.ReadAll(io.LimitReader(recordFile, commandLaunchRecordLimit+1))
|
|
if err != nil || len(encoded) == 0 || len(encoded) > commandLaunchRecordLimit {
|
|
return fail()
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(encoded))
|
|
decoder.DisallowUnknownFields()
|
|
var record commandLaunchRecord
|
|
if err := decoder.Decode(&record); err != nil {
|
|
return fail()
|
|
}
|
|
if err := ensureJSONEOF(decoder); err != nil || !validLaunchRecord(record) {
|
|
return fail()
|
|
}
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstat(commandRootFD, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != record.Device || uint64(stat.Ino) != record.Inode {
|
|
return fail()
|
|
}
|
|
if err := unix.Fchdir(commandRootFD); err != nil {
|
|
return fail()
|
|
}
|
|
_ = recordFile.Close()
|
|
_ = root.Close()
|
|
argv := make([]string, 1, len(record.Args)+1)
|
|
argv[0] = record.Executable
|
|
argv = append(argv, record.Args...)
|
|
if err := unix.Exec(record.Executable, argv, record.Environment); err != nil {
|
|
return fail()
|
|
}
|
|
return commandShimExit
|
|
}
|
|
|
|
func ensureJSONEOF(decoder *json.Decoder) error {
|
|
var extra any
|
|
if err := decoder.Decode(&extra); err != io.EOF {
|
|
if err == nil {
|
|
return errors.New("workspace command record has trailing data")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validLaunchRecord(record commandLaunchRecord) bool {
|
|
if record.Version != commandLaunchVersion || !filepath.IsAbs(record.Executable) || filepath.Clean(record.Executable) != record.Executable || strings.IndexByte(record.Executable, 0) >= 0 {
|
|
return false
|
|
}
|
|
for _, arg := range record.Args {
|
|
if strings.IndexByte(arg, 0) >= 0 {
|
|
return false
|
|
}
|
|
}
|
|
seen := make(map[string]struct{}, len(record.Environment))
|
|
for _, item := range record.Environment {
|
|
name, value, ok := strings.Cut(item, "=")
|
|
if !ok || !validEnvironmentName(name) || name == commandShimEnvironment || strings.IndexByte(value, 0) >= 0 {
|
|
return false
|
|
}
|
|
if _, duplicate := seen[name]; duplicate {
|
|
return false
|
|
}
|
|
seen[name] = struct{}{}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func terminateProcessGroup(pid int) {
|
|
if pid <= 0 {
|
|
return
|
|
}
|
|
_ = syscall.Kill(-pid, syscall.SIGTERM)
|
|
_ = syscall.Kill(-pid, syscall.SIGKILL)
|
|
}
|