Skip to content

Commit 6444974

Browse files
authored
Merge pull request #31 from arduino/kill_improvements
Improvements to Process.Kill() method.
2 parents 4ac75f3 + 2288ccb commit 6444974

File tree

5 files changed

+109
-5
lines changed

5 files changed

+109
-5
lines changed

process.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ func NewProcess(extraEnv []string, args ...string) (*Process, error) {
5555
cmd: exec.Command(args[0], args[1:]...),
5656
}
5757
p.cmd.Env = append(os.Environ(), extraEnv...)
58-
p.TellCommandNotToSpawnShell()
58+
tellCommandNotToSpawnShell(p.cmd) // windows specific
59+
tellCommandToStartOnNewProcessGroup(p.cmd) // linux specific
5960

6061
// This is required because some tools detects if the program is running
6162
// from terminal by looking at the stdin/out bindings.
@@ -146,7 +147,7 @@ func (p *Process) Signal(sig os.Signal) error {
146147
// actually exited. This only kills the Process itself, not any other processes it may
147148
// have started.
148149
func (p *Process) Kill() error {
149-
return p.cmd.Process.Kill()
150+
return kill(p.cmd)
150151
}
151152

152153
// SetDir sets the working directory of the command. If Dir is the empty string, Run

process_linux.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
//go:build !windows
31+
32+
package paths
33+
34+
import (
35+
"os/exec"
36+
"syscall"
37+
)
38+
39+
func tellCommandNotToSpawnShell(_ *exec.Cmd) {
40+
// no op
41+
}
42+
43+
func tellCommandToStartOnNewProcessGroup(oscmd *exec.Cmd) {
44+
// https://groups.google.com/g/golang-nuts/c/XoQ3RhFBJl8
45+
46+
// Start the process in a new process group.
47+
// This is needed to kill the process and its children
48+
// if we need to kill the process.
49+
if oscmd.SysProcAttr == nil {
50+
oscmd.SysProcAttr = &syscall.SysProcAttr{}
51+
}
52+
oscmd.SysProcAttr.Setpgid = true
53+
}
54+
55+
func kill(oscmd *exec.Cmd) error {
56+
// https://groups.google.com/g/golang-nuts/c/XoQ3RhFBJl8
57+
58+
// Kill the process group
59+
pgid, err := syscall.Getpgid(oscmd.Process.Pid)
60+
if err != nil {
61+
return err
62+
}
63+
return syscall.Kill(-pgid, syscall.SIGKILL)
64+
}

process_others.go

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

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

3232
package paths
3333

34-
import "os/exec"
34+
import (
35+
"os/exec"
36+
)
3537

3638
func tellCommandNotToSpawnShell(_ *exec.Cmd) {
3739
// no op
3840
}
41+
42+
func tellCommandToStartOnNewProcessGroup(_ *exec.Cmd) {
43+
// no op
44+
}
45+
46+
func kill(oscmd *exec.Cmd) error {
47+
return oscmd.Process.Kill()
48+
}

process_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ package paths
3131

3232
import (
3333
"context"
34+
"runtime"
3435
"testing"
3536
"time"
3637

@@ -54,3 +55,20 @@ func TestProcessWithinContext(t *testing.T) {
5455
require.Less(t, time.Since(start), 500*time.Millisecond)
5556
cancel()
5657
}
58+
59+
func TestKillProcessGroupOnLinux(t *testing.T) {
60+
if runtime.GOOS != "linux" {
61+
t.Skip("skipping test on non-linux system")
62+
}
63+
64+
p, err := NewProcess(nil, "bash", "-c", "sleep 5 ; echo -n 5")
65+
require.NoError(t, err)
66+
start := time.Now()
67+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
68+
defer cancel()
69+
70+
_, _, err = p.RunAndCaptureOutput(ctx)
71+
require.EqualError(t, err, "signal: killed")
72+
// Assert that the process was killed within the timeout
73+
require.Less(t, time.Since(start), 2*time.Second)
74+
}

process_windows.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,16 @@ import (
3535
)
3636

3737
func tellCommandNotToSpawnShell(oscmd *exec.Cmd) {
38-
oscmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
38+
if oscmd.SysProcAttr == nil {
39+
oscmd.SysProcAttr = &syscall.SysProcAttr{}
40+
}
41+
oscmd.SysProcAttr.HideWindow = true
42+
}
43+
44+
func tellCommandToStartOnNewProcessGroup(_ *exec.Cmd) {
45+
// no op
46+
}
47+
48+
func kill(oscmd *exec.Cmd) error {
49+
return oscmd.Process.Kill()
3950
}

0 commit comments

Comments
 (0)