mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 06:45:44 +00:00
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]>
85 lines
2.3 KiB
Go
85 lines
2.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package process
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// Killer terminates a started process together with its entire descendant tree
|
|
// via a Windows Job Object.
|
|
//
|
|
// Background: a process (a step or a post-task script) often launches a process
|
|
// tree (a shell that starts a child which in turn spawns further GUI or
|
|
// background processes). The default exec.CommandContext cancellation only kills
|
|
// the direct child, so cancelling left the rest of the tree running. Because
|
|
// those orphans inherited the parent's stdout/stderr pipe, cmd.Wait() also
|
|
// blocked forever and the runner hung.
|
|
//
|
|
// Assigning the process to a Job Object lets us kill the whole tree atomically
|
|
// on cancellation (TerminateJobObject), which also closes the inherited pipe
|
|
// handles so cmd.Wait() can return.
|
|
type Killer struct {
|
|
mu sync.Mutex
|
|
job windows.Handle
|
|
}
|
|
|
|
// NewKiller creates a Job Object and assigns p (an already-started process) to
|
|
// it. Children spawned by p afterwards are automatically part of the job. The
|
|
// job does NOT use JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, so closing the handle on
|
|
// normal completion does not kill legitimate background processes; the tree is
|
|
// only torn down by an explicit Kill (cancellation).
|
|
func NewKiller(p *os.Process) (*Killer, error) {
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(p.Pid))
|
|
if err != nil {
|
|
_ = windows.CloseHandle(job)
|
|
return nil, err
|
|
}
|
|
defer func() { _ = windows.CloseHandle(h) }()
|
|
|
|
if err := windows.AssignProcessToJobObject(job, h); err != nil {
|
|
_ = windows.CloseHandle(job)
|
|
return nil, err
|
|
}
|
|
|
|
return &Killer{job: job}, nil
|
|
}
|
|
|
|
// Kill terminates every process currently assigned to the job (the process and
|
|
// all of its descendants).
|
|
func (k *Killer) Kill() error {
|
|
if k == nil {
|
|
return nil
|
|
}
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if k.job == 0 {
|
|
return nil
|
|
}
|
|
return windows.TerminateJobObject(k.job, 1)
|
|
}
|
|
|
|
// Close releases the job handle. It does not terminate the processes.
|
|
func (k *Killer) Close() error {
|
|
if k == nil {
|
|
return nil
|
|
}
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if k.job == 0 {
|
|
return nil
|
|
}
|
|
h := k.job
|
|
k.job = 0
|
|
return windows.CloseHandle(h)
|
|
}
|