210 lines
4.5 KiB
Go
210 lines
4.5 KiB
Go
//go:build windows
|
|
|
|
package proc
|
|
|
|
import (
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// TreeTracker records a process tree while a command is running. Windows Job
|
|
// Objects should own normal children, but Git Bash/MSYS launch chains can briefly
|
|
// expose grandchildren before or outside taskkill's live tree walk. Recording
|
|
// descendants gives cancellation a second chance to terminate those escapees.
|
|
type TreeTracker struct {
|
|
root uint32
|
|
done chan struct{}
|
|
once sync.Once
|
|
|
|
mu sync.Mutex
|
|
records map[uint32]processRecord
|
|
}
|
|
|
|
type processRecord struct {
|
|
pid uint32
|
|
parent uint32
|
|
exe string
|
|
created windows.Filetime
|
|
hasTimes bool
|
|
}
|
|
|
|
func TrackTree(cmd *exec.Cmd) *TreeTracker {
|
|
if cmd == nil || cmd.Process == nil {
|
|
return nil
|
|
}
|
|
t := &TreeTracker{
|
|
root: uint32(cmd.Process.Pid),
|
|
done: make(chan struct{}),
|
|
records: map[uint32]processRecord{},
|
|
}
|
|
t.record()
|
|
go t.loop()
|
|
return t
|
|
}
|
|
|
|
func (t *TreeTracker) Stop() {
|
|
if t == nil {
|
|
return
|
|
}
|
|
t.once.Do(func() { close(t.done) })
|
|
}
|
|
|
|
func (t *TreeTracker) Kill() int {
|
|
if t == nil {
|
|
return 0
|
|
}
|
|
t.record()
|
|
records := t.snapshot()
|
|
killed := 0
|
|
for _, rec := range records {
|
|
if rec.pid != t.root {
|
|
killed += terminateRecord(rec)
|
|
}
|
|
}
|
|
for _, rec := range records {
|
|
if rec.pid == t.root {
|
|
killed += terminateRecord(rec)
|
|
break
|
|
}
|
|
}
|
|
return killed
|
|
}
|
|
|
|
func (t *TreeTracker) loop() {
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
t.record()
|
|
case <-t.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *TreeTracker) record() {
|
|
if t == nil || t.root == 0 {
|
|
return
|
|
}
|
|
records := processSnapshot()
|
|
t.mu.Lock()
|
|
if root, ok := records[t.root]; ok {
|
|
t.records[t.root] = root
|
|
}
|
|
for _, rec := range descendantRecords(t.root, records) {
|
|
t.records[rec.pid] = rec
|
|
}
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *TreeTracker) snapshot() []processRecord {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
out := make([]processRecord, 0, len(t.records))
|
|
for _, rec := range t.records {
|
|
out = append(out, rec)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].pid < out[j].pid })
|
|
return out
|
|
}
|
|
|
|
func descendantRecords(root uint32, records map[uint32]processRecord) []processRecord {
|
|
if root == 0 {
|
|
return nil
|
|
}
|
|
children := map[uint32][]uint32{}
|
|
for _, rec := range records {
|
|
children[rec.parent] = append(children[rec.parent], rec.pid)
|
|
}
|
|
|
|
var out []processRecord
|
|
seen := map[uint32]bool{root: true}
|
|
var walk func(uint32)
|
|
walk = func(pid uint32) {
|
|
for _, child := range children[pid] {
|
|
if child == 0 || seen[child] {
|
|
continue
|
|
}
|
|
seen[child] = true
|
|
if rec, ok := records[child]; ok {
|
|
out = append(out, rec)
|
|
}
|
|
walk(child)
|
|
}
|
|
}
|
|
walk(root)
|
|
return out
|
|
}
|
|
|
|
func processSnapshot() map[uint32]processRecord {
|
|
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer func() { _ = windows.CloseHandle(snap) }()
|
|
|
|
records := map[uint32]processRecord{}
|
|
var pe windows.ProcessEntry32
|
|
pe.Size = uint32(unsafe.Sizeof(pe))
|
|
for err := windows.Process32First(snap, &pe); err == nil; err = windows.Process32Next(snap, &pe) {
|
|
rec := processRecord{
|
|
pid: pe.ProcessID,
|
|
parent: pe.ParentProcessID,
|
|
exe: strings.ToLower(windows.UTF16ToString(pe.ExeFile[:])),
|
|
}
|
|
rec.created, rec.hasTimes = processCreationTime(pe.ProcessID)
|
|
records[rec.pid] = rec
|
|
}
|
|
return records
|
|
}
|
|
|
|
func processCreationTime(pid uint32) (windows.Filetime, bool) {
|
|
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
|
if err != nil {
|
|
return windows.Filetime{}, false
|
|
}
|
|
defer func() { _ = windows.CloseHandle(h) }()
|
|
var created, exited, kernel, user windows.Filetime
|
|
if err := windows.GetProcessTimes(h, &created, &exited, &kernel, &user); err != nil {
|
|
return windows.Filetime{}, false
|
|
}
|
|
return created, true
|
|
}
|
|
|
|
func terminateRecord(rec processRecord) int {
|
|
if rec.pid == 0 {
|
|
return 0
|
|
}
|
|
current, ok := processSnapshot()[rec.pid]
|
|
if !ok || !sameProcessIdentity(rec, current) {
|
|
return 0
|
|
}
|
|
h, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, rec.pid)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer func() { _ = windows.CloseHandle(h) }()
|
|
_ = windows.TerminateProcess(h, 1)
|
|
return 1
|
|
}
|
|
|
|
func sameProcessIdentity(recorded, current processRecord) bool {
|
|
if recorded.pid != current.pid {
|
|
return false
|
|
}
|
|
if recorded.hasTimes && current.hasTimes {
|
|
return recorded.created == current.created
|
|
}
|
|
if recorded.exe != "" && current.exe != "" {
|
|
return strings.EqualFold(recorded.exe, current.exe)
|
|
}
|
|
return true
|
|
}
|