fix: overhaul docker container termination signals (#192)

Fixes https://github.com/pterodactyl/panel/issues/4783

Requires https://github.com/pterodactyl/panel/pull/5132 to work
This commit is contained in:
Daniel Barton 2024-06-30 02:31:36 +08:00 committed by GitHub
parent 5a15612754
commit 29e4425e21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 34 deletions

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"os" "os"
"strings" "strings"
"syscall"
"time" "time"
"emperror.dev/errors" "emperror.dev/errors"
@ -143,42 +142,49 @@ func (e *Environment) Stop(ctx context.Context) error {
s := e.meta.Stop s := e.meta.Stop
e.mu.RUnlock() e.mu.RUnlock()
// A native "stop" as the Type field value will just skip over all of this
// logic and end up only executing the container stop command (which may or
// may not work as expected).
if s.Type == "" || s.Type == remote.ProcessStopSignal {
if s.Type == "" {
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
}
signal := os.Kill
// Handle a few common cases, otherwise just fall through and just pass along
// the os.Kill signal to the process.
switch strings.ToUpper(s.Value) {
case "SIGABRT":
signal = syscall.SIGABRT
case "SIGINT":
signal = syscall.SIGINT
case "SIGTERM":
signal = syscall.SIGTERM
}
return e.Terminate(ctx, signal)
}
// If the process is already offline don't switch it back to stopping. Just leave it how // If the process is already offline don't switch it back to stopping. Just leave it how
// it is and continue through to the stop handling for the process. // it is and continue through to the stop handling for the process.
if e.st.Load() != environment.ProcessOfflineState { if e.st.Load() != environment.ProcessOfflineState {
e.SetState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
} }
// Handle signal based actions
if s.Type == remote.ProcessStopSignal {
log.WithField("signal_value", s.Value).Debug("stopping server using signal")
// Handle some common signals - Default to SIGKILL
signal := "SIGKILL"
switch strings.ToUpper(s.Value) {
case "SIGABRT":
signal = "SIGABRT"
case "SIGINT", "C":
signal = "SIGINT"
case "SIGTERM":
signal = "SIGTERM"
case "SIGKILL":
signal = "SIGKILL"
default:
log.Info("Unrecognised signal requested, defaulting to SIGKILL")
}
return e.SignalContainer(ctx, signal)
}
// Handle command based stops
// Only attempt to send the stop command to the instance if we are actually attached to // Only attempt to send the stop command to the instance if we are actually attached to
// the instance. If we are not for some reason, just send the container stop event. // the instance. If we are not for some reason, just send the container stop event.
if e.IsAttached() && s.Type == remote.ProcessStopCommand { if e.IsAttached() && s.Type == remote.ProcessStopCommand {
return e.SendCommand(s.Value) return e.SendCommand(s.Value)
} }
// Allow the stop action to run for however long it takes, similar to executing a command if s.Type == "" {
// and using a different logic pathway to wait for the container to stop successfully. log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using native docker stop")
}
// Fallback to a native docker stop. As we aren't passing a signal to ContainerStop docker will
// attempt to stop the container using the default stop signal, SIGTERM, unless
// another signal was specified in the Dockerfile
// //
// Using a negative timeout here will allow the container to stop gracefully, // Using a negative timeout here will allow the container to stop gracefully,
// rather than forcefully terminating it. Value is in seconds, but -1 is // rather than forcefully terminating it. Value is in seconds, but -1 is
@ -224,7 +230,7 @@ func (e *Environment) WaitForStop(ctx context.Context, duration time.Duration, t
doTermination := func(s string) error { doTermination := func(s string) error {
e.log().WithField("step", s).WithField("duration", duration).Warn("container stop did not complete in time, terminating process...") e.log().WithField("step", s).WithField("duration", duration).Warn("container stop did not complete in time, terminating process...")
return e.Terminate(ctx, os.Kill) return e.Terminate(ctx, "SIGKILL")
} }
// We pass through the timed context for this stop action so that if one of the // We pass through the timed context for this stop action so that if one of the
@ -268,8 +274,8 @@ func (e *Environment) WaitForStop(ctx context.Context, duration time.Duration, t
return nil return nil
} }
// Terminate forcefully terminates the container using the signal provided. // Sends the specified signal to the container in an attempt to stop it.
func (e *Environment) Terminate(ctx context.Context, signal os.Signal) error { func (e *Environment) SignalContainer(ctx context.Context, signal string) error {
c, err := e.ContainerInspect(ctx) c, err := e.ContainerInspect(ctx)
if err != nil { if err != nil {
// Treat missing containers as an okay error state, means it is obviously // Treat missing containers as an okay error state, means it is obviously
@ -294,11 +300,27 @@ func (e *Environment) Terminate(ctx context.Context, signal os.Signal) error {
// We set it to stopping than offline to prevent crash detection from being triggered. // We set it to stopping than offline to prevent crash detection from being triggered.
e.SetState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed") if err := e.client.ContainerKill(ctx, e.Id, signal); err != nil && !client.IsErrNotFound(err) {
if err := e.client.ContainerKill(ctx, e.Id, sig); err != nil && !client.IsErrNotFound(err) {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil
}
// Terminate forcefully terminates the container using the signal provided.
// then sets its state to stopped.
func (e *Environment) Terminate(ctx context.Context, signal string) error {
// Send the signal to the container to kill it
if err := e.SignalContainer(ctx, signal); err != nil {
return errors.WithStack(err)
}
// We expect Terminate to instantly kill the container
// so go ahead and mark it as dead and clean up
e.SetState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return nil return nil
} }

View File

@ -2,7 +2,6 @@ package environment
import ( import (
"context" "context"
"os"
"time" "time"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
@ -72,7 +71,7 @@ type ProcessEnvironment interface {
// Terminate stops a running server instance using the provided signal. This function // Terminate stops a running server instance using the provided signal. This function
// is a no-op if the server is already stopped. // is a no-op if the server is already stopped.
Terminate(ctx context.Context, signal os.Signal) error Terminate(ctx context.Context, signal string) error
// Destroys the environment removing any containers that were created (in Docker // Destroys the environment removing any containers that were created (in Docker
// environments at least). // environments at least).

View File

@ -3,7 +3,6 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"time" "time"
"emperror.dev/errors" "emperror.dev/errors"
@ -161,7 +160,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
return s.Environment.Start(s.Context()) return s.Environment.Start(s.Context())
case PowerActionTerminate: case PowerActionTerminate:
return s.Environment.Terminate(s.Context(), os.Kill) return s.Environment.Terminate(s.Context(), "SIGKILL")
} }
return errors.New("attempting to handle unknown power action") return errors.New("attempting to handle unknown power action")