diff --git a/go.mod b/go.mod index 05e388f..49cb55a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,13 @@ module github.com/arduino/go-paths-helper -go 1.21 +go 1.23.0 -require github.com/stretchr/testify v1.8.4 +toolchain go1.23.2 + +require ( + github.com/stretchr/testify v1.8.4 + golang.org/x/sys v0.32.0 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fa4b6e6..bf9063e 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/process.go b/process.go index 2c014a1..fe6166f 100644 --- a/process.go +++ b/process.go @@ -56,7 +56,7 @@ func NewProcess(extraEnv []string, args ...string) (*Process, error) { } p.cmd.Env = append(os.Environ(), extraEnv...) tellCommandNotToSpawnShell(p.cmd) // windows specific - tellCommandToStartOnNewProcessGroup(p.cmd) // linux specific + tellCommandToStartOnNewProcessGroup(p.cmd) // linux and macosx specific // This is required because some tools detects if the program is running // from terminal by looking at the stdin/out bindings. @@ -67,6 +67,8 @@ func NewProcess(extraEnv []string, args ...string) (*Process, error) { // TellCommandNotToSpawnShell avoids that the specified Cmd display a small // command prompt while runnning on Windows. It has no effects on other OS. +// +// Deprecated: TellCommandNotToSpawnShell is now always applied by default, there is no need to call it anymore. func (p *Process) TellCommandNotToSpawnShell() { tellCommandNotToSpawnShell(p.cmd) } diff --git a/process_darwin.go b/process_darwin.go new file mode 100644 index 0000000..237b894 --- /dev/null +++ b/process_darwin.go @@ -0,0 +1,62 @@ +// +// This file is part of PathsHelper library. +// +// Copyright 2023 Arduino AG (http://www.arduino.cc/) +// +// PathsHelper library is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// +// As a special exception, you may use this file as part of a free software +// library without restriction. Specifically, if other files instantiate +// templates or use macros or inline functions from this file, or you compile +// this file and link it with other files to produce an executable, this +// file does not by itself cause the resulting executable to be covered by +// the GNU General Public License. This exception does not however +// invalidate any other reasons why the executable file might be covered by +// the GNU General Public License. +// + +package paths + +import ( + "os/exec" + "syscall" +) + +func tellCommandNotToSpawnShell(_ *exec.Cmd) { + // no op +} + +func tellCommandToStartOnNewProcessGroup(oscmd *exec.Cmd) { + // https://groups.google.com/g/golang-nuts/c/XoQ3RhFBJl8 + + // Start the process in a new process group. + // This is needed to kill the process and its children + // if we need to kill the process. + if oscmd.SysProcAttr == nil { + oscmd.SysProcAttr = &syscall.SysProcAttr{} + } + oscmd.SysProcAttr.Setpgid = true +} + +func kill(oscmd *exec.Cmd) error { + // https://groups.google.com/g/golang-nuts/c/XoQ3RhFBJl8 + + // Kill the process group + pgid, err := syscall.Getpgid(oscmd.Process.Pid) + if err != nil { + return err + } + return syscall.Kill(-pgid, syscall.SIGKILL) +} diff --git a/process_others.go b/process_others.go index 70b6236..0aca003 100644 --- a/process_others.go +++ b/process_others.go @@ -27,7 +27,7 @@ // the GNU General Public License. // -//go:build !windows && !linux +//go:build !windows && !linux && !darwin package paths diff --git a/process_windows.go b/process_windows.go index 00cd6a3..e8971c0 100644 --- a/process_windows.go +++ b/process_windows.go @@ -30,8 +30,12 @@ package paths import ( + "fmt" "os/exec" "syscall" + "unsafe" + + "golang.org/x/sys/windows" ) func tellCommandNotToSpawnShell(oscmd *exec.Cmd) { @@ -46,5 +50,54 @@ func tellCommandToStartOnNewProcessGroup(_ *exec.Cmd) { } func kill(oscmd *exec.Cmd) error { - return oscmd.Process.Kill() + parentProcessMap, err := createParentProcessSnapshot() + if err != nil { + return err + } + return killPidTree(uint32(oscmd.Process.Pid), parentProcessMap) +} + +// createParentProcessSnapshot returns a map that correlate a process +// with its parent process: childPid -> parentPid +func createParentProcessSnapshot() (map[uint32]uint32, error) { + // Inspired by: https://stackoverflow.com/a/36089871/1655275 + + // Make a snapshot of the current running processes + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if err != nil { + return nil, fmt.Errorf("getting running processes snapshot: %w", err) + } + defer windows.CloseHandle(snapshot) + + // Iterate the result and extract the parent-child relationship + processParentMap := map[uint32]uint32{} + var processEntry windows.ProcessEntry32 + processEntry.Size = uint32(unsafe.Sizeof(processEntry)) + hasData := (windows.Process32First(snapshot, &processEntry) == nil) + for hasData { + processParentMap[processEntry.ProcessID] = processEntry.ParentProcessID + hasData = (windows.Process32Next(snapshot, &processEntry) == nil) + } + return processParentMap, nil +} + +func killPidTree(pid uint32, parentProcessMap map[uint32]uint32) error { + for childPid, parentPid := range parentProcessMap { + if parentPid == pid { + // Descend process tree + if err := killPidTree(childPid, parentProcessMap); err != nil { + return fmt.Errorf("error killing child process: %w", err) + } + } + } + return killPid(pid) +} + +func killPid(pid uint32) error { + process, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, pid) + if err != nil { + return fmt.Errorf("opening process for kill: %w", err) + } + defer windows.CloseHandle(process) + return windows.TerminateProcess(process, 128) }