mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-07-22 20:47:45 +00:00
## Problem On host-mode Windows runners, a job can leave processes running after it finishes, and those leftovers hold file handles that block deletion of the workspace. Today a step's tree is only torn down when the step is *cancelled* (`process.Killer`). A step that completes leaves whatever it spawned alive, and the existing workspace scan in `terminateRunningProcesses` misses two shapes of leftover: orphans whose parent already exited (no tree to walk, and their executable often lives outside the workspace), and processes that merely *run in* the workspace but reference no path from it — `Win32_Process` exposes no working directory, so the scan cannot match them. ## Solution Two additions in `internal/pkg/process`, both no-ops outside Windows: - **`process.Group`** — a job-scoped Windows Job Object created with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. Step processes are assigned to it before they get their own `Killer`, so the step's job nests inside the job's: cancellation still kills exactly the step's tree, while `Remove` closing the group makes the kernel terminate everything still assigned, whatever its parentage. The kernel also drops the handle when the runner exits, so a crashed runner cannot strand processes. - **`process.KillProcessesWithCWDUnder`** — a best-effort net for processes that never joined the job (started via a service or scheduled task). It reads each process's working directory from its PEB and terminates those under a workspace dir. Processes it cannot open are skipped. --------- Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1080 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
87 lines
2.0 KiB
Go
87 lines
2.0 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package process
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// Group is a job-scoped Windows Job Object holding every process the job's steps
|
|
// start; closing it terminates whatever is still assigned, reaching orphans the
|
|
// per-step Killer misses (a completed step's leftover has no parent to walk from).
|
|
type Group struct {
|
|
mu sync.Mutex
|
|
job windows.Handle
|
|
}
|
|
|
|
// NewGroup creates the job object. Closing it is what terminates the leftovers,
|
|
// so the caller must Close it when the job ends.
|
|
func NewGroup() (*Group, error) {
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
|
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
|
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
|
},
|
|
}
|
|
if _, err := windows.SetInformationJobObject(
|
|
job,
|
|
windows.JobObjectExtendedLimitInformation,
|
|
uintptr(unsafe.Pointer(&info)),
|
|
uint32(unsafe.Sizeof(info)),
|
|
); err != nil {
|
|
_ = windows.CloseHandle(job)
|
|
return nil, err
|
|
}
|
|
|
|
return &Group{job: job}, nil
|
|
}
|
|
|
|
// Assign adds a started process to the job; anything it spawns afterwards joins
|
|
// too. Call before NewKiller so the step's job nests inside this one. Nil-safe.
|
|
func (g *Group) Assign(p *os.Process) error {
|
|
if g == nil || p == nil {
|
|
return nil
|
|
}
|
|
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.job == 0 {
|
|
return nil
|
|
}
|
|
|
|
h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(p.Pid))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = windows.CloseHandle(h) }()
|
|
|
|
return windows.AssignProcessToJobObject(g.job, h)
|
|
}
|
|
|
|
// Close drops the job handle, terminating every process still assigned to it.
|
|
// Nil-safe and idempotent.
|
|
func (g *Group) Close() error {
|
|
if g == nil {
|
|
return nil
|
|
}
|
|
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.job == 0 {
|
|
return nil
|
|
}
|
|
|
|
h := g.job
|
|
g.job = 0
|
|
return windows.CloseHandle(h)
|
|
}
|