Skip to content

Commit

Permalink
Use windows console groups for process management
Browse files Browse the repository at this point in the history
  • Loading branch information
lox committed Dec 20, 2018
1 parent fd34201 commit 2325800
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 33 deletions.
53 changes: 20 additions & 33 deletions process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"io/ioutil"
"os"
"os/exec"
"runtime"
"strconv"
"sync"
"syscall"
"time"
Expand Down Expand Up @@ -38,7 +36,7 @@ func (p *Process) Start() error {
return fmt.Errorf("Process is already running")
}

p.command = exec.Command(p.Script[0], p.Script[1:]...)
p.command = createCommand(p.Script[0], p.Script[1:]...)

// Create channels for signalling started and done
p.mu.Lock()
Expand Down Expand Up @@ -186,17 +184,24 @@ func (p *Process) Started() <-chan struct{} {
// Kill the process. If supported by the operating system it is initially signaled for termination
// and then forcefully killed after the provided grace period.
func (p *Process) Kill(gracePeriod time.Duration) error {
var err error
if runtime.GOOS == "windows" {
// Sending Interrupt on Windows is not implemented.
// https://golang.org/src/os/exec.go?s=3842:3884#L110
err = exec.Command("CMD", "/C", "TASKKILL", "/F", "/T", "/PID", strconv.Itoa(p.Pid)).Run()
} else {
// Send a sigterm
err = p.signal(syscall.SIGTERM)
p.mu.Lock()

if p.command == nil || p.command.Process == nil {
logger.Debug("[Process] No process to signal yet")
p.mu.Unlock()
return nil
}
if err != nil {
return err

p.mu.Unlock()

// First we interrupt the process (ctrl-c or SIGINT)
if err := interruptProcess(p.command.Process); err != nil {
logger.Error("[Process] Failed to interrupt process %d: %v", p.Pid, err)

// Fallback to terminating if we get an error
if termErr := terminateProcess(p.command.Process); termErr != nil {
return termErr
}
}

select {
Expand All @@ -208,27 +213,8 @@ func (p *Process) Kill(gracePeriod time.Duration) error {
// Forcefully kill the process after grace period expires
case <-time.After(gracePeriod):
logger.Debug("[Process] Process %d didn't terminate within %v, killing.", p.Pid, gracePeriod)
return p.signal(syscall.SIGKILL)
}
}

func (p *Process) signal(sig os.Signal) error {
p.mu.Lock()
defer p.mu.Unlock()

if p.command != nil && p.command.Process != nil {
logger.Debug("[Process] Sending signal: %s to PID: %d", sig.String(), p.Pid)

err := p.command.Process.Signal(sig)
if err != nil {
logger.Error("[Process] Failed to send signal: %s to PID: %d (%T: %v)", sig.String(), p.Pid, err, err)
return err
}
} else {
logger.Debug("[Process] No process to signal yet")
return terminateProcess(p.command.Process)
}

return nil
}

// https://github.com/hnakamur/commango/blob/fe42b1cf82bf536ce7e24dceaef6656002e03743/os/executil/executil.go#L29
Expand All @@ -239,6 +225,7 @@ func getExitStatus(waitResult error) string {
if waitResult != nil {
if err, ok := waitResult.(*exec.ExitError); ok {
if s, ok := err.Sys().(syscall.WaitStatus); ok {
logger.Debug("[Process] Got wait status: %#v", s)
exitStatus = s.ExitStatus()
} else {
logger.Error("[Process] Unimplemented for system where exec.ExitError.Sys() is not syscall.WaitStatus.")
Expand Down
13 changes: 13 additions & 0 deletions process/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ package process
import (
"os"
"os/exec"
"syscall"

"github.com/kr/pty"
)

func StartPTY(c *exec.Cmd) (*os.File, error) {
return pty.Start(c)
}

func createCommand(cmd string, args ...string) *exec.Cmd {
return exec.Command(cmd, args...)
}

func terminateProcess(p *os.Process) error {
return p.Signal(syscall.SIGKILL)
}

func interruptProcess(p *os.Process) error {
return p.Signal(syscall.SIGTERM)
}
46 changes: 46 additions & 0 deletions process/utils_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,54 @@ import (
"errors"
"os"
"os/exec"
"syscall"
)

func StartPTY(c *exec.Cmd) (*os.File, error) {
return nil, errors.New("PTY is not supported on Windows")
}

// Windows has no concept of parent/child processes or signals. The best we can do
// is create processes inside a "console group" and then send break / ctrl-c events
// to that group. This is superior to walking a process tree to kill each process
// because that relies on each process in that chain still being active.

// See https://docs.microsoft.com/en-us/windows/console/generateconsolectrlevent


var (
libkernel32 = syscall.MustLoadDLL("kernel32")
procSetConsoleCtrlHandler = libkernel32.MustFindProc("SetConsoleCtrlHandler")
procGenerateConsoleCtrlEvent = libkernel32.MustFindProc("GenerateConsoleCtrlEvent")
)

const (

createNewProcessGroupFlag = 0x00000200
)

func createCommand(cmd string, args ...string) *exec.Cmd {
execCmd := exec.Command(cmd, args...)
execCmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_UNICODE_ENVIRONMENT | createNewProcessGroupFlag,
}
return execCmd
}

func terminateProcess(p *os.Process) error {
return p.Kill()
}

func interruptProcess(p *os.Process) error {
procSetConsoleCtrlHandler.Call(0, 1)
defer procSetConsoleCtrlHandler.Call(0, 0)
r1, _, err := procGenerateConsoleCtrlEvent.Call(syscall.CTRL_BREAK_EVENT, uintptr(p.Pid))
if r1 == 0 {
return err
}
r1, _, err = procGenerateConsoleCtrlEvent.Call(syscall.CTRL_C_EVENT, uintptr(p.Pid))
if r1 == 0 {
return err
}
return nil
}

0 comments on commit 2325800

Please sign in to comment.