Skip to content

Commit 08cf9b4

Browse files
authored
Merge pull request #34 from arduino/kill_process_darwin
`Kill` now kills all the childrens in the process-tree on MacOS and Windows.
2 parents 73c658f + 8d1e1d5 commit 08cf9b4

File tree

6 files changed

+129
-5
lines changed

6 files changed

+129
-5
lines changed

go.mod

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
module github.com/arduino/go-paths-helper
22

3-
go 1.21
3+
go 1.23.0
44

5-
require github.com/stretchr/testify v1.8.4
5+
toolchain go1.23.2
6+
7+
require (
8+
github.com/stretchr/testify v1.8.4
9+
golang.org/x/sys v0.32.0
10+
)
611

712
require (
813
github.com/davecgh/go-spew v1.1.1 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
44
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
55
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
66
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
8+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
79
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
810
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
911
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

process.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func NewProcess(extraEnv []string, args ...string) (*Process, error) {
5656
}
5757
p.cmd.Env = append(os.Environ(), extraEnv...)
5858
tellCommandNotToSpawnShell(p.cmd) // windows specific
59-
tellCommandToStartOnNewProcessGroup(p.cmd) // linux specific
59+
tellCommandToStartOnNewProcessGroup(p.cmd) // linux and macosx specific
6060

6161
// This is required because some tools detects if the program is running
6262
// from terminal by looking at the stdin/out bindings.
@@ -67,6 +67,8 @@ func NewProcess(extraEnv []string, args ...string) (*Process, error) {
6767

6868
// TellCommandNotToSpawnShell avoids that the specified Cmd display a small
6969
// command prompt while runnning on Windows. It has no effects on other OS.
70+
//
71+
// Deprecated: TellCommandNotToSpawnShell is now always applied by default, there is no need to call it anymore.
7072
func (p *Process) TellCommandNotToSpawnShell() {
7173
tellCommandNotToSpawnShell(p.cmd)
7274
}

process_darwin.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// This file is part of PathsHelper library.
3+
//
4+
// Copyright 2023 Arduino AG (http://www.arduino.cc/)
5+
//
6+
// PathsHelper library is free software; you can redistribute it and/or modify
7+
// it under the terms of the GNU General Public License as published by
8+
// the Free Software Foundation; either version 2 of the License, or
9+
// (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU General Public License
17+
// along with this program; if not, write to the Free Software
18+
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19+
//
20+
// As a special exception, you may use this file as part of a free software
21+
// library without restriction. Specifically, if other files instantiate
22+
// templates or use macros or inline functions from this file, or you compile
23+
// this file and link it with other files to produce an executable, this
24+
// file does not by itself cause the resulting executable to be covered by
25+
// the GNU General Public License. This exception does not however
26+
// invalidate any other reasons why the executable file might be covered by
27+
// the GNU General Public License.
28+
//
29+
30+
package paths
31+
32+
import (
33+
"os/exec"
34+
"syscall"
35+
)
36+
37+
func tellCommandNotToSpawnShell(_ *exec.Cmd) {
38+
// no op
39+
}
40+
41+
func tellCommandToStartOnNewProcessGroup(oscmd *exec.Cmd) {
42+
// https://groups.google.com/g/golang-nuts/c/XoQ3RhFBJl8
43+
44+
// Start the process in a new process group.
45+
// This is needed to kill the process and its children
46+
// if we need to kill the process.
47+
if oscmd.SysProcAttr == nil {
48+
oscmd.SysProcAttr = &syscall.SysProcAttr{}
49+
}
50+
oscmd.SysProcAttr.Setpgid = true
51+
}
52+
53+
func kill(oscmd *exec.Cmd) error {
54+
// https://groups.google.com/g/golang-nuts/c/XoQ3RhFBJl8
55+
56+
// Kill the process group
57+
pgid, err := syscall.Getpgid(oscmd.Process.Pid)
58+
if err != nil {
59+
return err
60+
}
61+
return syscall.Kill(-pgid, syscall.SIGKILL)
62+
}

process_others.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
// the GNU General Public License.
2828
//
2929

30-
//go:build !windows && !linux
30+
//go:build !windows && !linux && !darwin
3131

3232
package paths
3333

process_windows.go

+54-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030
package paths
3131

3232
import (
33+
"fmt"
3334
"os/exec"
3435
"syscall"
36+
"unsafe"
37+
38+
"golang.org/x/sys/windows"
3539
)
3640

3741
func tellCommandNotToSpawnShell(oscmd *exec.Cmd) {
@@ -46,5 +50,54 @@ func tellCommandToStartOnNewProcessGroup(_ *exec.Cmd) {
4650
}
4751

4852
func kill(oscmd *exec.Cmd) error {
49-
return oscmd.Process.Kill()
53+
parentProcessMap, err := createParentProcessSnapshot()
54+
if err != nil {
55+
return err
56+
}
57+
return killPidTree(uint32(oscmd.Process.Pid), parentProcessMap)
58+
}
59+
60+
// createParentProcessSnapshot returns a map that correlate a process
61+
// with its parent process: childPid -> parentPid
62+
func createParentProcessSnapshot() (map[uint32]uint32, error) {
63+
// Inspired by: https://stackoverflow.com/a/36089871/1655275
64+
65+
// Make a snapshot of the current running processes
66+
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
67+
if err != nil {
68+
return nil, fmt.Errorf("getting running processes snapshot: %w", err)
69+
}
70+
defer windows.CloseHandle(snapshot)
71+
72+
// Iterate the result and extract the parent-child relationship
73+
processParentMap := map[uint32]uint32{}
74+
var processEntry windows.ProcessEntry32
75+
processEntry.Size = uint32(unsafe.Sizeof(processEntry))
76+
hasData := (windows.Process32First(snapshot, &processEntry) == nil)
77+
for hasData {
78+
processParentMap[processEntry.ProcessID] = processEntry.ParentProcessID
79+
hasData = (windows.Process32Next(snapshot, &processEntry) == nil)
80+
}
81+
return processParentMap, nil
82+
}
83+
84+
func killPidTree(pid uint32, parentProcessMap map[uint32]uint32) error {
85+
for childPid, parentPid := range parentProcessMap {
86+
if parentPid == pid {
87+
// Descend process tree
88+
if err := killPidTree(childPid, parentProcessMap); err != nil {
89+
return fmt.Errorf("error killing child process: %w", err)
90+
}
91+
}
92+
}
93+
return killPid(pid)
94+
}
95+
96+
func killPid(pid uint32) error {
97+
process, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, pid)
98+
if err != nil {
99+
return fmt.Errorf("opening process for kill: %w", err)
100+
}
101+
defer windows.CloseHandle(process)
102+
return windows.TerminateProcess(process, 128)
50103
}

0 commit comments

Comments
 (0)