2020-08-11 04:38:42 +00:00
package docker
import (
"context"
"github.com/apex/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
2020-08-20 01:58:48 +00:00
"github.com/pterodactyl/wings/environment"
2020-08-11 04:38:42 +00:00
"os"
"strings"
"time"
)
// Run before the container starts and get the process configuration from the Panel.
// This is important since we use this to check configuration files as well as ensure
// we always have the latest version of an egg available for server processes.
//
// This process will also confirm that the server environment exists and is in a bootable
// state. This ensures that unexpected container deletion while Wings is running does
// not result in the server becoming unbootable.
func ( e * Environment ) OnBeforeStart ( ) error {
// Always destroy and re-create the server container to ensure that synced data from
// the Panel is usee.
if err := e . client . ContainerRemove ( context . Background ( ) , e . Id , types . ContainerRemoveOptions { RemoveVolumes : true } ) ; err != nil {
if ! client . IsErrNotFound ( err ) {
2020-08-28 03:28:29 +00:00
return errors . Wrap ( err , "failed to remove server docker container during pre-boot" )
2020-08-11 04:38:42 +00:00
}
}
// The Create() function will check if the container exists in the first place, and if
// so just silently return without an error. Otherwise, it will try to create the necessary
// container and data storage directory.
//
// This won't actually run an installation process however, it is just here to ensure the
2020-09-05 19:08:40 +00:00
// environment gets created properly if it is missing and the server is started. We're making
2020-08-11 04:38:42 +00:00
// an assumption that all of the files will still exist at this point.
if err := e . Create ( ) ; err != nil {
return err
}
return nil
}
// Starts the server environment and begins piping output to the event listeners for the
// console. If a container does not exist, or needs to be rebuilt that will happen in the
// call to OnBeforeStart().
func ( e * Environment ) Start ( ) error {
sawError := false
// If sawError is set to true there was an error somewhere in the pipeline that
// got passed up, but we also want to ensure we set the server to be offline at
// that point.
defer func ( ) {
if sawError {
// If we don't set it to stopping first, you'll trigger crash detection which
// we don't want to do at this point since it'll just immediately try to do the
// exact same action that lead to it crashing in the first place...
2020-11-07 05:53:00 +00:00
e . SetState ( environment . ProcessStoppingState )
e . SetState ( environment . ProcessOfflineState )
2020-08-11 04:38:42 +00:00
}
} ( )
if c , err := e . client . ContainerInspect ( context . Background ( ) , e . Id ) ; err != nil {
// Do nothing if the container is not found, we just don't want to continue
2020-09-05 19:08:40 +00:00
// to the next block of code here. This check was inlined here to guard against
2020-08-11 04:38:42 +00:00
// a nil-pointer when checking c.State below.
//
// @see https://github.com/pterodactyl/panel/issues/2000
if ! client . IsErrNotFound ( err ) {
return errors . WithStack ( err )
}
} else {
// If the server is running update our internal state and continue on with the attach.
if c . State . Running {
2020-11-07 05:53:00 +00:00
e . SetState ( environment . ProcessRunningState )
2020-08-11 04:38:42 +00:00
return e . Attach ( )
}
// Truncate the log file so we don't end up outputting a bunch of useless log information
// to the websocket and whatnot. Check first that the path and file exist before trying
// to truncate them.
if _ , err := os . Stat ( c . LogPath ) ; err == nil {
if err := os . Truncate ( c . LogPath , 0 ) ; err != nil {
return errors . WithStack ( err )
}
}
}
2020-11-07 05:53:00 +00:00
e . SetState ( environment . ProcessStartingState )
2020-08-11 04:38:42 +00:00
// Set this to true for now, we will set it to false once we reach the
// end of this chain.
sawError = true
// Run the before start function and wait for it to finish. This will validate that the container
// exists on the system, and rebuild the container if that is required for server booting to
// occur.
if err := e . OnBeforeStart ( ) ; err != nil {
return errors . WithStack ( err )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Second * 10 )
defer cancel ( )
if err := e . client . ContainerStart ( ctx , e . Id , types . ContainerStartOptions { } ) ; err != nil {
return errors . WithStack ( err )
}
// No errors, good to continue through.
sawError = false
return e . Attach ( )
}
2020-08-14 04:10:33 +00:00
// Stops the container that the server is running in. This will allow up to 30 seconds to pass
// before the container is forcefully terminated if we are trying to stop it without using a command
// sent into the instance.
//
// You most likely want to be using WaitForStop() rather than this function, since this will return
// as soon as the command is sent, rather than waiting for the process to be completed stopped.
2020-08-11 04:38:42 +00:00
func ( e * Environment ) Stop ( ) error {
e . mu . RLock ( )
s := e . meta . Stop
e . mu . RUnlock ( )
2020-10-17 19:06:47 +00:00
if s . Type == "" || s . Type == api . ProcessStopSignal {
if s . Type == "" {
2020-09-05 19:08:40 +00:00
log . WithField ( "container_id" , e . Id ) . Warn ( "no stop configuration detected for environment, using termination procedure" )
2020-08-11 04:38:42 +00:00
}
return e . Terminate ( os . Kill )
}
2020-08-19 03:43:28 +00:00
// 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.
2020-11-07 05:53:00 +00:00
if e . st . Load ( ) != environment . ProcessOfflineState {
e . SetState ( environment . ProcessStoppingState )
2020-08-19 03:43:28 +00:00
}
2020-08-11 04:38:42 +00:00
// 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.
if e . IsAttached ( ) && s . Type == api . ProcessStopCommand {
return e . SendCommand ( s . Value )
}
2020-08-14 04:10:33 +00:00
t := time . Second * 30
2020-08-11 04:38:42 +00:00
err := e . client . ContainerStop ( context . Background ( ) , e . Id , & t )
if err != nil {
// If the container does not exist just mark the process as stopped and return without
// an error.
if client . IsErrNotFound ( err ) {
e . SetStream ( nil )
2020-11-07 05:53:00 +00:00
e . SetState ( environment . ProcessOfflineState )
2020-08-11 04:38:42 +00:00
return nil
}
return err
}
return nil
}
2020-08-14 04:10:33 +00:00
// Attempts to gracefully stop a server using the defined stop command. If the server
2020-08-11 04:38:42 +00:00
// does not stop after seconds have passed, an error will be returned, or the instance
// will be terminated forcefully depending on the value of the second argument.
2020-08-14 04:10:33 +00:00
func ( e * Environment ) WaitForStop ( seconds uint , terminate bool ) error {
2020-08-11 04:38:42 +00:00
if err := e . Stop ( ) ; err != nil {
return errors . WithStack ( err )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Duration ( seconds ) * time . Second )
defer cancel ( )
// Block the return of this function until the container as been marked as no
// longer running. If this wait does not end by the time seconds have passed,
// attempt to terminate the container, or return an error.
ok , errChan := e . client . ContainerWait ( ctx , e . Id , container . WaitConditionNotRunning )
select {
case <- ctx . Done ( ) :
if ctxErr := ctx . Err ( ) ; ctxErr != nil {
if terminate {
2020-09-18 03:13:04 +00:00
log . WithField ( "container_id" , e . Id ) . Debug ( "server did not stop in time, executing process termination" )
return errors . WithStack ( e . Terminate ( os . Kill ) )
2020-08-11 04:38:42 +00:00
}
return errors . WithStack ( ctxErr )
}
case err := <- errChan :
if err != nil {
2020-09-18 03:13:04 +00:00
if terminate {
log . WithField ( "container_id" , e . Id ) . WithField ( "error" , errors . WithStack ( err ) ) . Warn ( "error while waiting for container stop, attempting process termination" )
return errors . WithStack ( e . Terminate ( os . Kill ) )
}
2020-08-11 04:38:42 +00:00
return errors . WithStack ( err )
}
case <- ok :
}
return nil
}
// Forcefully terminates the container using the signal passed through.
func ( e * Environment ) Terminate ( signal os . Signal ) error {
c , err := e . client . ContainerInspect ( context . Background ( ) , e . Id )
if err != nil {
return errors . WithStack ( err )
}
if ! c . State . Running {
2020-08-19 03:43:28 +00:00
// If the container is not running but we're not already in a stopped state go ahead
// and update things to indicate we should be completely stopped now. Set to stopping
// first so crash detection is not triggered.
2020-11-07 05:53:00 +00:00
if e . st . Load ( ) != environment . ProcessOfflineState {
e . SetState ( environment . ProcessStoppingState )
e . SetState ( environment . ProcessOfflineState )
2020-08-19 03:43:28 +00:00
}
2020-08-11 04:38:42 +00:00
return nil
}
2020-09-05 19:08:40 +00:00
// We set it to stopping than offline to prevent crash detection from being triggered.
2020-11-07 05:53:00 +00:00
e . SetState ( environment . ProcessStoppingState )
2020-08-11 04:38:42 +00:00
sig := strings . TrimSuffix ( strings . TrimPrefix ( signal . String ( ) , "signal " ) , "ed" )
if err := e . client . ContainerKill ( context . Background ( ) , e . Id , sig ) ; err != nil {
return err
}
2020-11-07 05:53:00 +00:00
e . SetState ( environment . ProcessOfflineState )
2020-08-11 04:38:42 +00:00
return nil
}