Skip to content

Commit

Permalink
* Optimize behavior of processGroup kill also for windows and align i…
Browse files Browse the repository at this point in the history
…t for linux - Default is not: send signals to processGroup
  • Loading branch information
blaubaer committed Mar 13, 2016
1 parent c4b40fe commit 5e83cce
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 30 deletions.
6 changes: 3 additions & 3 deletions service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ type Config struct {
// See: {@ref #AutoRestart autoRestart}
SuccessExitCodes values.ExitCodes `json:"successExitCodes" yaml:"successExitCodes,flow"`

// @default "TERM" (on Unix like systems) - "KILL" (on Windows)
// @default "TERM"
//
// Signal which will be send to the service when a stop is requested.
// You can use the signal number here and also names like ``"TERM"`` or ``"KILL"``.
StopSignal values.Signal `json:"stopSignal" yaml:"stopSignal"`

// @default "process"
// @default "processGroup"
//
// Defines who have to receive the stop signal.
//
Expand Down Expand Up @@ -232,7 +232,7 @@ func (instance *Config) init() {
(*instance).RestartDelayInSeconds = values.NonNegativeInteger(5)
(*instance).SuccessExitCodes = values.ExitCodes{values.ExitCode(0)}
(*instance).StopSignal = defaultStopSignal()
(*instance).StopSignalTarget = values.Process
(*instance).StopSignalTarget = values.ProcessGroup
(*instance).StopCommand = []values.String{}
(*instance).StopWaitInSeconds = values.NonNegativeInteger(30)
(*instance).User = values.String("")
Expand Down
2 changes: 1 addition & 1 deletion service/config_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (
)

func defaultStopSignal() values.Signal {
return values.KILL
return values.TERM
}
4 changes: 2 additions & 2 deletions service/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,9 @@ func (instance *Execution) Stop() {
}
defer instance.doUnlock()
if instance.status != Down {
instance.logger.Log(logger.Debug, "Stopping '%s'...", instance.Name())
instance.sendStop()
if instance.status != Down {
instance.logger.Log(logger.Debug, "Stopping '%s'...", instance.Name())
instance.condition.Wait(time.Duration(instance.service.config.StopWaitInSeconds) * time.Second)
if instance.status != Down {
instance.logger.Log(logger.Warning, "Service '%s' does not respond after %d seconds. Going to kill it now...", instance.Name(), instance.service.config.StopWaitInSeconds)
Expand Down Expand Up @@ -405,7 +405,7 @@ func (instance *Execution) sendSignal(s values.Signal) error {
return nil
}
if s != values.NOOP {
return sendSignalToService((*instance).service, process, s)
return sendSignalToService((*instance).service, process, s, instance.service.config.StopSignalTarget)
}
return nil
}
Expand Down
16 changes: 14 additions & 2 deletions service/execution_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,20 @@ func serviceHandleUsersFor(service *Service, cmd *exec.Cmd) {
}
}

func sendSignalToService(service *Service, process *os.Process, what values.Signal) error {
if service.config.StopSignalTarget == values.ProcessGroup || what == values.KILL || what == values.STOP {
func sendSignalToService(service *Service, process *os.Process, what values.Signal, signalTarget values.SignalTarget) error {
switch signalTarget {
case values.Process:
return sendSignalToProcess(process, what, false)
case values.ProcessGroup:
return sendSignalToProcess(process, what, true)
case values.Mixed:
return sendSignalToProcess(process, what, what == values.KILL || what == values.STOP)
}
return errors.New("Could not signal '%v' (#%v) %v because signalTarget %v is not supported.", service, process.Pid, what, signalTarget)
}

func sendSignalToProcess(process *os.Process, what values.Signal, tryGroup bool) error {
if tryGroup {
pgid, err := syscall.Getpgid(process.Pid)
if err == nil {
if syscall.Kill(-pgid, syscall.Signal(what)) == nil {
Expand Down
104 changes: 82 additions & 22 deletions service/execution_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,114 @@ package service

import (
"github.com/echocat/caretakerd/errors"
"github.com/echocat/caretakerd/logger"
"github.com/echocat/caretakerd/panics"
"github.com/echocat/caretakerd/values"
"os"
"os/exec"
"syscall"
"unsafe"
)

const processAllAccess = 0x001F0FFF

func serviceHandleUsersFor(service *Service, cmd *exec.Cmd) {
if !(*service).config.User.IsTrimmedEmpty() {
panics.New("Could not handle users under windows. Please remove it from service '%s'.", service.Name()).Throw()
}
}

func sendSignalToService(service *Service, process *os.Process, what values.Signal) error {
if service.config.StopSignalTarget != values.Process {
return errors.New("On windows only stopSignalTarget == 'process' is supported but got: %v", service.config.StopSignalTarget)
}
if what == values.TERM {
sendSpecialSignal(process, syscall.CTRL_BREAK_EVENT)
} else if what == values.INT {
sendSpecialSignal(process, syscall.CTRL_C_EVENT)
} else {
sSignal := syscall.Signal(what)
service.Logger().Log(logger.Debug, "Send %v to %v with PID %d", sSignal, service, process.Pid)
if err := process.Signal(sSignal); err != nil {
ignore := false
func sendSignalToService(service *Service, process *os.Process, what values.Signal, signalTarget values.SignalTarget) error {
switch signalTarget {
case values.Process:
return sendSignalToProcess(process.Pid, service, what)
case values.ProcessGroup:
return sendSignalToProcessGroup(process.Pid, service, what)
case values.Mixed:
if what == values.KILL || what == values.STOP {
return sendSignalToProcessGroup(process.Pid, service, what)
} else {
return sendSignalToProcess(process.Pid, service, what)
}
}
return errors.New("Could not signal '%v' (#%v) %v because signalTarget %v is not supported.", service, process.Pid, what, signalTarget)
}

func sendSignalToProcess(pid int, service *Service, what values.Signal) error {
switch what {
case values.TERM:
return sendSpecialSignal(pid, syscall.CTRL_BREAK_EVENT)
case values.INT:
return sendSpecialSignal(pid, syscall.CTRL_C_EVENT)
case values.STOP:
fallthrough
case values.KILL:
processHandle, err := syscall.OpenProcess(processAllAccess, false, uint32(pid))
if err != nil {
if se, ok := err.(syscall.Errno); ok {
ignore = se.Error() == "invalid argument"
if se.Error() != "invalid argument" {
return errors.New("Could not signal '%v' (#%v): %v", service, pid, what).CausedBy(err)
}
} else {
return errors.New("Could not signal '%v' (#%v): %v", service, pid, what).CausedBy(err)
}
if !ignore {
return errors.New("Could not signal '%v': %v", service, what).CausedBy(err)
}
if err == nil {
err := syscall.TerminateProcess(processHandle, 1)
if err != nil {
if se, ok := err.(syscall.Errno); ok {
if se.Error() != "invalid argument" {
return errors.New("Could not signal '%v' (#%v): %v", service, pid, what).CausedBy(err)
}
} else {
return errors.New("Could not signal '%v' (#%v): %v", service, pid, what).CausedBy(err)
}
}
syscall.CloseHandle(processHandle)
}
return nil
}
return nil
return errors.New("Could not signal '%v' (#%v): %v ... because this signal is not supported on widows.", service, pid, what)
}

func sendSignalToProcessGroup(pid int, service *Service, what values.Signal) error {
pe := syscall.ProcessEntry32{}
pe.Size = uint32(unsafe.Sizeof(pe))

hSnap, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
if err != nil {
return errors.New("Could iterate over children of service %v (#%v).", service, pid).CausedBy(err)
}

err = syscall.Process32First(hSnap, &pe)
if err != nil {
return err
}

tryNext := true
for tryNext {
if pe.ParentProcessID == uint32(pid) {
err := sendSignalToProcess(int(pe.ProcessID), service, what)
if err != nil {
return err
}
}
tryNext = syscall.Process32Next(hSnap, &pe) == nil
}

return sendSignalToProcess(pid, service, what)
}

func sendSpecialSignal(process *os.Process, what uintptr) {
func sendSpecialSignal(pid int, what uintptr) error {
d, e := syscall.LoadDLL("kernel32.dll")
if e != nil {
panics.New("Could not set terminate to process %v.", process).CausedBy(e).Throw()
panics.New("Could not set terminate to process #%v.", pid).CausedBy(e).Throw()
}
p, e := d.FindProc("GenerateConsoleCtrlEvent")
if e != nil {
panics.New("Could not set terminate to process %v.", process).CausedBy(e).Throw()
panics.New("Could not set terminate to process #%v.", pid).CausedBy(e).Throw()
}
p.Call(what, uintptr(process.Pid))
_, _, err := p.Call(what, uintptr(pid))
return err
}

func (instance *Service) createSysProcAttr() *syscall.SysProcAttr {
Expand Down
5 changes: 5 additions & 0 deletions values/signalTarget.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ const (
//
// Send a signal to the whole process group.
ProcessGroup SignalTarget = 2
// @id mixed
//
// Send every signal only to a process - except KILL and STOP. This are send to the processGroup.
Mixed SignalTarget = 3
)

// AllSignalTargets contains all possible variants of SignalTarget.
var AllSignalTargets = []SignalTarget{
Process,
ProcessGroup,
Mixed,
}

func (instance SignalTarget) String() string {
Expand Down

0 comments on commit 5e83cce

Please sign in to comment.