Files
silverwind dcba546cb3 fix: align process handling and fix Windows crashes (#1376)
The intermittent Windows test crashes came from step errors formatting a nil `rc.Run`. `fmt` recovers the nil dereference, but on CPUs with AMX the recovered fault overflows into the heap (https://github.com/golang/go/issues/81238). The errors now name the job, and the Windows test VM hides AMX and runs on UTC, so cached boots no longer jump back 8 hours mid-test.

Windows commands now start suspended and resume once they are in their job objects, so no child escapes them. That drops the process scan reading other processes' memory and the last `unsafe` code, which `depguard` now denies. Step processes outlive a crashed runner, and commands are no longer looked up in the runner's working directory, both as with GitHub's runner.

Like GitHub's runner, host jobs on Linux and macOS now end the processes carrying the job's `RUNNER_TRACKING_ID`, and image pulls and Dockerfile action builds are retried.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1376
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
2026-11-04 16:32:22 +00:00

160 lines
3.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package process
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestNewTreeKillConfiguresCommand(t *testing.T) {
cmd := exec.CommandContext(t.Context(), "sleep", "1")
tk := NewTreeKill(cmd)
require.NotNil(t, tk)
require.NotNil(t, cmd.Cancel)
require.Equal(t, treeKillWaitDelay, cmd.WaitDelay)
require.NoError(t, cmd.Cancel())
}
func TestTreeKillWaitWithLeftoverHoldingOutput(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
for _, tc := range []struct {
name string
wantErr bool
}{
{name: "succeeds"},
{name: "fails once the context is done", wantErr: true},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", "sleep 30 &")
cmd.Stdout = io.Discard
cmd.SysProcAttr = SysProcAttr("", false)
tk := NewTreeKill(cmd)
cmd.WaitDelay = 50 * time.Millisecond
require.NoError(t, cmd.Start())
killer, err := tk.Capture(cmd.Process)
require.NoError(t, err)
t.Cleanup(func() { _ = killer.Kill() })
if tc.wantErr {
cancel()
}
err = tk.Wait(ctx, cmd)
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestTreeKillCaptureStoresKiller(t *testing.T) {
cmd, ready := newTestProcess(t, "child", false)
tk := NewTreeKill(cmd)
require.NoError(t, cmd.Start())
killer, err := tk.Capture(cmd.Process)
require.NoError(t, err)
require.NotNil(t, killer)
ready()
require.NoError(t, cmd.Cancel())
require.NoError(t, killer.Close())
}
func newTestProcess(t *testing.T, mode string, copiedOutput bool) (*exec.Cmd, func() int) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
t.Cleanup(cancel)
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestProcessHelper$")
cmd.Env = append(os.Environ(), "GITEA_PROCESS_HELPER="+mode)
cmd.SysProcAttr = SysProcAttr("", false)
cmd.Stderr = os.Stderr
input, err := cmd.StdinPipe()
require.NoError(t, err)
var output io.ReadCloser
if copiedOutput {
var writer *io.PipeWriter
output, writer = io.Pipe()
cmd.Stdout = writer
} else {
output, err = cmd.StdoutPipe()
require.NoError(t, err)
}
stop := context.AfterFunc(ctx, func() {
_ = input.Close()
_ = output.Close()
})
t.Cleanup(func() {
stop()
_ = input.Close()
_ = output.Close()
if cmd.Process != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}
})
return cmd, func() int {
t.Helper()
_, err := input.Write([]byte{1})
require.NoError(t, err)
var childPID int
_, err = fmt.Fscan(output, &childPID)
require.NoError(t, err, "process should report its PID")
require.Positive(t, childPID)
return childPID
}
}
func TestProcessHelper(t *testing.T) {
mode := os.Getenv("GITEA_PROCESS_HELPER")
switch mode {
case "":
return
case "orphan-child":
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
_, err := fmt.Fprintln(os.Stdout, os.Getpid())
require.NoError(t, err)
<-interrupt
case "child":
_, err := fmt.Fprintln(os.Stdout, os.Getpid())
require.NoError(t, err)
_, err = io.Copy(io.Discard, os.Stdin)
require.NoError(t, err)
default:
var start [1]byte
_, err := io.ReadFull(os.Stdin, start[:])
require.NoError(t, err)
child := exec.Command(os.Args[0], "-test.run=^TestProcessHelper$")
child.Env = append(os.Environ(), "GITEA_PROCESS_HELPER=child")
if mode == "orphan" {
child.Env = append(os.Environ(), "GITEA_PROCESS_HELPER=orphan-child")
}
child.Stdin = os.Stdin
child.Stdout = os.Stdout
child.Stderr = os.Stderr
require.NoError(t, child.Start())
if mode == "orphan" {
os.Exit(0)
}
require.NoError(t, child.Wait())
}
os.Exit(0)
}