Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/reference/layer-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ services:
# Default is 5 seconds ("5s").
kill-delay: <duration>

# (Optional) Signal to send instead of SIGTERM when stopping service.
stop-signal: <signal>

# (Optional) A list of health checks managed by this configuration layer.
checks:

Expand Down
25 changes: 20 additions & 5 deletions internals/overlord/servstate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ var (
// failDelay is the duration given to services for shutting down when Pebble
// sends a SIGKILL signal.
failDelay = 5 * time.Second

// stopSignalDefault is the signal sent to a service when it's asked to stop.
stopSignalDefault = syscall.SIGTERM
)

const (
Expand Down Expand Up @@ -715,6 +718,16 @@ func (s *serviceData) killDelay() time.Duration {
return killDelayDefault
}

// stopSignal returns the signal sent to a service when it's asked to stop.
// The value returned will either be the service's pre-configured value, or
// the default stop signal if that is not set.
func (s *serviceData) stopSignal() (name string, signal syscall.Signal) {
if s.config.StopSignal.IsSet {
return s.config.StopSignal.Name, s.config.StopSignal.Value
}
return plan.SignalToString(stopSignalDefault), stopSignalDefault
}

// stop is called to stop a running (or backing off) service.
func (s *serviceData) stop() error {
s.manager.servicesLock.Lock()
Expand All @@ -726,11 +739,12 @@ func (s *serviceData) stop() error {
fallthrough

case stateRunning:
logger.Debugf("Attempting to stop service %q by sending SIGTERM", s.config.Name)
signalName, stopSignal := s.stopSignal()
logger.Debugf("Attempting to stop service %q by sending %s", s.config.Name, signalName)
// First send SIGTERM to try to terminate it gracefully.
err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGTERM)
err := syscall.Kill(-s.cmd.Process.Pid, stopSignal)
if err != nil {
logger.Noticef("Cannot send SIGTERM to process: %v", err)
logger.Noticef("Cannot send %s to process: %v", signalName, err)
}
s.transition(stateTerminating)
time.AfterFunc(s.killDelay(), func() { logError(s.terminateTimeElapsed()) })
Expand Down Expand Up @@ -857,11 +871,12 @@ func (s *serviceData) checkFailed(action plan.ServiceAction) {
case plan.ActionRestart:
switch s.state {
case stateRunning:
signalName, stopSignal := s.stopSignal()
logger.Noticef("Service %q %s action is %q, terminating process before restarting",
s.config.Name, onType, action)
err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGTERM)
err := syscall.Kill(-s.cmd.Process.Pid, stopSignal)
if err != nil {
logger.Noticef("Cannot send SIGTERM to process: %v", err)
logger.Noticef("Cannot send %s to process: %v", signalName, err)
}
s.transitionRestarting(stateTerminating, true)
time.AfterFunc(s.killDelay(), func() { logError(s.terminateTimeElapsed()) })
Expand Down
1 change: 1 addition & 0 deletions internals/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ type Service struct {
BackoffFactor OptionalFloat `yaml:"backoff-factor,omitempty"`
BackoffLimit OptionalDuration `yaml:"backoff-limit,omitempty"`
KillDelay OptionalDuration `yaml:"kill-delay,omitempty"`
StopSignal OptionalSyscallSignal `yaml:"stop-signal,omitempty"`
}

// Copy returns a deep copy of the service.
Expand Down
88 changes: 88 additions & 0 deletions internals/plan/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plan
import (
"fmt"
"strconv"
"syscall"
"time"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -65,3 +66,90 @@ func (o *OptionalFloat) UnmarshalYAML(value *yaml.Node) error {
o.IsSet = true
return nil
}

// OptionalSyscallSignal is a wrapper around syscall.Signal
type OptionalSyscallSignal struct {
Value syscall.Signal
Name string
IsSet bool
}

func (o OptionalSyscallSignal) IsZero() bool {
return !o.IsSet
}

func (o OptionalSyscallSignal) MarshalYAML() (any, error) {
if !o.IsSet {
return nil, nil
}
return o.Name, nil
}

func (o *OptionalSyscallSignal) UnmarshalYAML(value *yaml.Node) error {
if value.Kind != yaml.ScalarNode {
return fmt.Errorf("signal must be a YAML string")
}
signal, err := parseSignal(value.Value)
if err != nil {
return fmt.Errorf("invalid signal %q: %w", value.Value, err)
}
o.Value = signal
o.Name = value.Value
o.IsSet = true
return nil
}

// A selection of the most common signals.
var signals = map[string]syscall.Signal{
"SIGABRT": syscall.SIGABRT,
"SIGALRM": syscall.SIGALRM,
"SIGBUS": syscall.SIGBUS,
"SIGCHLD": syscall.SIGCHLD,
"SIGCLD": syscall.SIGCLD,
"SIGCONT": syscall.SIGCONT,
"SIGFPE": syscall.SIGFPE,
"SIGHUP": syscall.SIGHUP,
"SIGILL": syscall.SIGILL,
"SIGINT": syscall.SIGINT,
"SIGIO": syscall.SIGIO,
"SIGIOT": syscall.SIGIOT,
"SIGKILL": syscall.SIGKILL,
"SIGPIPE": syscall.SIGPIPE,
"SIGPOLL": syscall.SIGPOLL,
"SIGPROF": syscall.SIGPROF,
"SIGPWR": syscall.SIGPWR,
"SIGQUIT": syscall.SIGQUIT,
"SIGSEGV": syscall.SIGSEGV,
"SIGSTKFLT": syscall.SIGSTKFLT,
"SIGSTOP": syscall.SIGSTOP,
"SIGSYS": syscall.SIGSYS,
"SIGTERM": syscall.SIGTERM,
"SIGTRAP": syscall.SIGTRAP,
"SIGTSTP": syscall.SIGTSTP,
"SIGTTIN": syscall.SIGTTIN,
"SIGTTOU": syscall.SIGTTOU,
"SIGUNUSED": syscall.SIGUNUSED,
"SIGURG": syscall.SIGURG,
"SIGUSR1": syscall.SIGUSR1,
"SIGUSR2": syscall.SIGUSR2,
"SIGVTALRM": syscall.SIGVTALRM,
"SIGWINCH": syscall.SIGWINCH,
"SIGXCPU": syscall.SIGXCPU,
"SIGXFSZ": syscall.SIGXFSZ,
}

func parseSignal(s string) (syscall.Signal, error) {
if sig, ok := signals[s]; ok {
return sig, nil
}
return 0, fmt.Errorf("unknown signal %q", s)
}

func SignalToString(sig syscall.Signal) string {
for name, s := range signals {
if s == sig {
return name
}
}
return sig.String()
}