239 lines
6.9 KiB
Go
239 lines
6.9 KiB
Go
//go:build windows
|
|
|
|
package compose
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// PROC_THREAD_ATTRIBUTE_JOB_LIST is intentionally kept here because x/sys does
|
|
// not expose this newer SDK constant yet. Supplying it to CreateProcess makes
|
|
// job ownership atomic with process creation; CREATE_SUSPENDED plus a later
|
|
// AssignProcessToJobObject is not sufficient (the parent can die in between).
|
|
const procThreadAttributeJobList = 0x0002000d
|
|
|
|
// The generic exec.Cmd path is not used on Windows: runBoundedPlatform launches
|
|
// with CreateProcess and a creation-time job-list attribute. These hooks remain
|
|
// for the platform-neutral runner's compile-time shape only.
|
|
func configureOwnedProcess(*exec.Cmd) {}
|
|
func registerOwnedProcess(*exec.Cmd) error {
|
|
return errors.New("Windows owned processes require creation-time job ownership")
|
|
}
|
|
func releaseOwnedProcess(*exec.Cmd) {}
|
|
func terminateOwnedProcess(c *exec.Cmd) {
|
|
if c.Process != nil {
|
|
_ = c.Process.Kill()
|
|
}
|
|
}
|
|
|
|
func runBoundedPlatform(ctx context.Context, runner Runner, args []string, stdin io.Reader, limits CaptureLimits) (Result, bool, error) {
|
|
if limits.StdoutBytes < 0 || limits.StderrBytes < 0 {
|
|
return Result{}, true, ErrOutputLimit
|
|
}
|
|
result, err := runBoundedWindows(ctx, runner.binary, args, stdin, limits)
|
|
return result, true, err
|
|
}
|
|
|
|
type windowsCollector struct {
|
|
buf []byte
|
|
n int64
|
|
overflow bool
|
|
}
|
|
|
|
func runBoundedWindows(ctx context.Context, binary string, args []string, stdin io.Reader, limits CaptureLimits) (Result, error) {
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
closeJob := true
|
|
defer func() {
|
|
if closeJob {
|
|
windows.CloseHandle(job)
|
|
}
|
|
}()
|
|
limitsInfo := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{}
|
|
limitsInfo.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
if _, err = windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limitsInfo)), uint32(unsafe.Sizeof(limitsInfo))); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
stdinRead, stdinWrite, err := os.Pipe()
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
stdoutRead, stdoutWrite, err := os.Pipe()
|
|
if err != nil {
|
|
_ = stdinRead.Close()
|
|
_ = stdinWrite.Close()
|
|
return Result{}, err
|
|
}
|
|
stderrRead, stderrWrite, err := os.Pipe()
|
|
if err != nil {
|
|
_ = stdinRead.Close()
|
|
_ = stdinWrite.Close()
|
|
_ = stdoutRead.Close()
|
|
_ = stdoutWrite.Close()
|
|
return Result{}, err
|
|
}
|
|
closeAll := func() {
|
|
_ = stdinRead.Close()
|
|
_ = stdinWrite.Close()
|
|
_ = stdoutRead.Close()
|
|
_ = stdoutWrite.Close()
|
|
_ = stderrRead.Close()
|
|
_ = stderrWrite.Close()
|
|
}
|
|
defer closeAll()
|
|
childHandles := []windows.Handle{windows.Handle(stdinRead.Fd()), windows.Handle(stdoutWrite.Fd()), windows.Handle(stderrWrite.Fd())}
|
|
for _, h := range childHandles {
|
|
if err := windows.SetHandleInformation(h, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT); err != nil {
|
|
return Result{}, err
|
|
}
|
|
}
|
|
// The attribute list limits inheritance to exactly these standard handles.
|
|
attributes, err := windows.NewProcThreadAttributeList(2)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
defer attributes.Delete()
|
|
jobList := []windows.Handle{job}
|
|
if err := attributes.Update(procThreadAttributeJobList, unsafe.Pointer(&jobList[0]), unsafe.Sizeof(jobList[0])); err != nil {
|
|
return Result{}, err
|
|
}
|
|
if err := attributes.Update(windows.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, unsafe.Pointer(&childHandles[0]), uintptr(len(childHandles))*unsafe.Sizeof(childHandles[0])); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
command := syscall.EscapeArg(binary)
|
|
for _, arg := range args {
|
|
command += " " + syscall.EscapeArg(arg)
|
|
}
|
|
commandLine, err := windows.UTF16FromString(command)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
startup := windows.StartupInfoEx{
|
|
StartupInfo: windows.StartupInfo{
|
|
Cb: uint32(unsafe.Sizeof(windows.StartupInfoEx{})),
|
|
Flags: windows.STARTF_USESTDHANDLES,
|
|
StdInput: childHandles[0],
|
|
StdOutput: childHandles[1],
|
|
StdErr: childHandles[2],
|
|
},
|
|
ProcThreadAttributeList: attributes.List(),
|
|
}
|
|
var process windows.ProcessInformation
|
|
creationFlags := uint32(windows.CREATE_UNICODE_ENVIRONMENT | windows.CREATE_NEW_PROCESS_GROUP | windows.EXTENDED_STARTUPINFO_PRESENT)
|
|
if err := windows.CreateProcess(nil, &commandLine[0], nil, nil, true, creationFlags, nil, nil, (*windows.StartupInfo)(unsafe.Pointer(&startup)), &process); err != nil {
|
|
return Result{ExitCode: 127}, err
|
|
}
|
|
// The child owns duplicate pipe handles; parent copies use the opposite ends.
|
|
_ = stdinRead.Close()
|
|
_ = stdoutWrite.Close()
|
|
_ = stderrWrite.Close()
|
|
_ = windows.CloseHandle(process.Thread)
|
|
|
|
var inputDone sync.WaitGroup
|
|
inputDone.Add(1)
|
|
go func() {
|
|
defer inputDone.Done()
|
|
defer stdinWrite.Close()
|
|
if stdin != nil {
|
|
_, _ = io.Copy(stdinWrite, stdin)
|
|
}
|
|
}()
|
|
out := windowsCollector{buf: make([]byte, limits.StdoutBytes)}
|
|
er := windowsCollector{buf: make([]byte, limits.StderrBytes)}
|
|
overflow := make(chan struct{}, 1)
|
|
var readers sync.WaitGroup
|
|
readers.Add(2)
|
|
read := func(file *os.File, maximum int64, collector *windowsCollector) {
|
|
defer readers.Done()
|
|
chunk := make([]byte, 32*1024)
|
|
for {
|
|
n, readErr := file.Read(chunk)
|
|
if n > 0 {
|
|
remain := maximum - collector.n
|
|
if remain > 0 {
|
|
take := int64(n)
|
|
if take > remain {
|
|
take = remain
|
|
}
|
|
copy(collector.buf[collector.n:collector.n+take], chunk[:take])
|
|
collector.n += take
|
|
}
|
|
if int64(n) > remain {
|
|
collector.overflow = true
|
|
select {
|
|
case overflow <- struct{}{}:
|
|
default:
|
|
}
|
|
return
|
|
}
|
|
}
|
|
if readErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
go read(stdoutRead, limits.StdoutBytes, &out)
|
|
go read(stderrRead, limits.StderrBytes, &er)
|
|
waited := make(chan uint32, 1)
|
|
go func() {
|
|
_, waitErr := windows.WaitForSingleObject(process.Process, windows.INFINITE)
|
|
if waitErr != nil {
|
|
waited <- uint32(0x103)
|
|
} else {
|
|
var code uint32
|
|
if windows.GetExitCodeProcess(process.Process, &code) != nil {
|
|
code = 1
|
|
}
|
|
waited <- code
|
|
}
|
|
}()
|
|
var exitCode uint32
|
|
var cancelled error
|
|
select {
|
|
case exitCode = <-waited:
|
|
// Closing the job handle kills any descendants before waiting on pipe readers.
|
|
_ = windows.CloseHandle(job)
|
|
closeJob = false
|
|
case <-overflow:
|
|
_ = windows.TerminateJobObject(job, 1)
|
|
exitCode = <-waited
|
|
_ = windows.CloseHandle(job)
|
|
closeJob = false
|
|
case <-ctx.Done():
|
|
cancelled = ctx.Err()
|
|
_ = windows.TerminateJobObject(job, 1)
|
|
exitCode = <-waited
|
|
_ = windows.CloseHandle(job)
|
|
closeJob = false
|
|
}
|
|
_ = windows.CloseHandle(process.Process)
|
|
_ = stdinWrite.Close()
|
|
readers.Wait()
|
|
inputDone.Wait()
|
|
result := Result{Stdout: string(out.buf[:out.n]), Stderr: string(er.buf[:er.n]), ExitCode: int(exitCode)}
|
|
if out.overflow || er.overflow {
|
|
return result, ErrOutputLimit
|
|
}
|
|
if cancelled != nil {
|
|
return result, cancelled
|
|
}
|
|
if exitCode == 0 {
|
|
return result, nil
|
|
}
|
|
return result, fmt.Errorf("child exited with code %d", exitCode)
|
|
}
|