199 lines
5.6 KiB
Go
199 lines
5.6 KiB
Go
//go:build windows
|
|
|
|
package compose
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
func TestOwnedProcessUsesCreationTimeJobAttribute(t *testing.T) {
|
|
if procThreadAttributeJobList == 0 {
|
|
t.Fatal("owned Windows process must carry a creation-time job-list attribute")
|
|
}
|
|
cmd := exec.Command("cmd.exe")
|
|
configureOwnedProcess(cmd)
|
|
if cmd.SysProcAttr != nil && cmd.SysProcAttr.CreationFlags&windows.CREATE_SUSPENDED != 0 {
|
|
t.Fatal("owned Windows process must not rely on a post-start assignment race")
|
|
}
|
|
}
|
|
|
|
func TestOwnedJobPolicyKillsDescendantsOnClose(t *testing.T) {
|
|
if windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE == 0 {
|
|
t.Fatal("owned job must use kill-on-close policy")
|
|
}
|
|
}
|
|
|
|
// TestWindowsProcessHelper is both a test helper and the child process used by the runtime
|
|
// ownership regression below. The grandchild records its PID before the parent can exit. The
|
|
// parent then either exits, blocks for cancellation, or overflows stdout.
|
|
func TestWindowsProcessHelper(t *testing.T) {
|
|
mode := os.Getenv("THOTHCTL_WINDOWS_HELPER")
|
|
if mode == "" {
|
|
return
|
|
}
|
|
pidFile := os.Getenv("THOTHCTL_WINDOWS_PID_FILE")
|
|
releaseFile := os.Getenv("THOTHCTL_WINDOWS_RELEASE_FILE")
|
|
if mode == "grandchild" {
|
|
if err := os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for {
|
|
time.Sleep(time.Hour)
|
|
}
|
|
}
|
|
child := exec.Command(os.Args[0], "-test.run=TestWindowsProcessHelper", "--")
|
|
child.Env = append(os.Environ(),
|
|
"THOTHCTL_WINDOWS_HELPER=grandchild",
|
|
"THOTHCTL_WINDOWS_PID_FILE="+pidFile,
|
|
)
|
|
if err := child.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
waitForWindowsPIDFile(t, pidFile)
|
|
waitForWindowsReleaseFile(t, releaseFile)
|
|
if mode == "exit" {
|
|
return
|
|
}
|
|
if mode == "cancel" {
|
|
for {
|
|
time.Sleep(time.Hour)
|
|
}
|
|
}
|
|
for {
|
|
_, _ = os.Stdout.Write([]byte("overflow\n"))
|
|
}
|
|
}
|
|
|
|
func waitForWindowsPIDFile(t *testing.T, path string) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
contents, err := os.ReadFile(path)
|
|
if err == nil {
|
|
pid, parseErr := strconv.Atoi(strings.TrimSpace(string(contents)))
|
|
if parseErr == nil && pid > 0 {
|
|
return
|
|
}
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatalf("grandchild PID was not recorded in %s", path)
|
|
}
|
|
|
|
func waitForWindowsReleaseFile(t *testing.T, path string) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatalf("test did not release Windows helper via %s", path)
|
|
}
|
|
|
|
func openWindowsProcessForTest(t *testing.T, pid int) windows.Handle {
|
|
t.Helper()
|
|
const access = windows.SYNCHRONIZE | windows.PROCESS_QUERY_LIMITED_INFORMATION
|
|
handle, err := windows.OpenProcess(access, false, uint32(pid))
|
|
if err != nil {
|
|
t.Fatalf("OpenProcess(%d): %v", pid, err)
|
|
}
|
|
return handle
|
|
}
|
|
|
|
func assertWindowsProcessTerminated(t *testing.T, handle windows.Handle, pid int) {
|
|
t.Helper()
|
|
const stillActive = 259
|
|
status, err := windows.WaitForSingleObject(handle, uint32((5*time.Second)/time.Millisecond))
|
|
if err != nil {
|
|
t.Fatalf("WaitForSingleObject(%d): %v", pid, err)
|
|
}
|
|
if status != windows.WAIT_OBJECT_0 {
|
|
t.Fatalf("WaitForSingleObject(%d) = %#x, want signaled terminated process", pid, status)
|
|
}
|
|
var exitCode uint32
|
|
if err := windows.GetExitCodeProcess(handle, &exitCode); err != nil {
|
|
t.Fatalf("GetExitCodeProcess(%d): %v", pid, err)
|
|
}
|
|
if exitCode == stillActive {
|
|
t.Fatalf("GetExitCodeProcess(%d) still reports STILL_ACTIVE", pid)
|
|
}
|
|
}
|
|
|
|
func TestOwnedProcessTerminatesDescendantOnOverflowCancelAndParentExit(t *testing.T) {
|
|
for _, mode := range []string{"overflow", "cancel", "exit"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
pidFile, err := os.CreateTemp("", "thothctl-windows-grandchild-*.pid")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pidPath := pidFile.Name()
|
|
_ = pidFile.Close()
|
|
_ = os.Remove(pidPath)
|
|
defer os.Remove(pidPath)
|
|
releaseFile, err := os.CreateTemp("", "thothctl-windows-release-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
releasePath := releaseFile.Name()
|
|
_ = releaseFile.Close()
|
|
_ = os.Remove(releasePath)
|
|
defer os.Remove(releasePath)
|
|
t.Setenv("THOTHCTL_WINDOWS_HELPER", mode)
|
|
t.Setenv("THOTHCTL_WINDOWS_PID_FILE", pidPath)
|
|
t.Setenv("THOTHCTL_WINDOWS_RELEASE_FILE", releasePath)
|
|
ctx := context.Background()
|
|
var cancel context.CancelFunc
|
|
if mode == "cancel" {
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
defer cancel()
|
|
}
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
_, runErr := runBoundedWindows(ctx, os.Args[0], []string{"-test.run=TestWindowsProcessHelper", "--"}, nil, CaptureLimits{StdoutBytes: 1024, StderrBytes: 1024})
|
|
result <- runErr
|
|
}()
|
|
|
|
waitForWindowsPIDFile(t, pidPath)
|
|
contents, err := os.ReadFile(pidPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(contents)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handle := openWindowsProcessForTest(t, pid)
|
|
defer windows.CloseHandle(handle)
|
|
if err := os.WriteFile(releasePath, []byte("release"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mode == "cancel" {
|
|
cancel()
|
|
}
|
|
select {
|
|
case runErr := <-result:
|
|
if mode == "overflow" && !errors.Is(runErr, ErrOutputLimit) {
|
|
t.Fatalf("overflow error = %v, want ErrOutputLimit", runErr)
|
|
}
|
|
if mode == "cancel" && !errors.Is(runErr, context.Canceled) {
|
|
t.Fatalf("cancel error = %v, want context.Canceled", runErr)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("owned process did not terminate")
|
|
}
|
|
assertWindowsProcessTerminated(t, handle, pid)
|
|
})
|
|
}
|
|
}
|