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>
42 lines
1.3 KiB
Go
42 lines
1.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package process
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestPathWithin(t *testing.T) {
|
|
base := filepath.Join("base", "job1")
|
|
|
|
cases := []struct {
|
|
name string
|
|
dir string
|
|
target string
|
|
fold bool
|
|
want bool
|
|
}{
|
|
{"same dir", base, base, false, true},
|
|
{"direct child", base, filepath.Join(base, "app.exe"), false, true},
|
|
{"deep child", base, filepath.Join(base, "sub", "sub", "app.exe"), false, true},
|
|
{"parent is not within child", filepath.Join(base, "sub"), base, false, false},
|
|
{"name-prefix sibling spared", base, filepath.Join("base", "job10", "app.exe"), false, false},
|
|
{"unrelated", base, filepath.Join("other", "app.exe"), false, false},
|
|
{"empty dir", "", base, false, false},
|
|
{"empty target", base, "", false, false},
|
|
{"dot dir", ".", base, false, false},
|
|
{"case-sensitive miss", base, filepath.Join("base", "JOB1", "app.exe"), false, false},
|
|
{"case-insensitive hit", base, filepath.Join("base", "JOB1", "app.exe"), true, true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := pathWithin(filepath.Clean(tc.dir), filepath.Clean(tc.target), tc.fold)
|
|
if got != tc.want {
|
|
t.Fatalf("pathWithin(%q, %q, fold=%v) = %v, want %v", tc.dir, tc.target, tc.fold, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|