Compare commits

..

18 Commits

Author SHA1 Message Date
Dane Everitt
90cdea83d4 Bump for release 2020-01-19 15:18:00 -08:00
Dane Everitt
ab54d2c416 Avoid triggering crash detection on container destroy 2020-01-19 14:00:59 -08:00
Dane Everitt
59299d3cda Add code to notify panel when install is completed 2020-01-19 13:30:54 -08:00
Dane Everitt
7533e38543 Add support for running installation process when creating a server 2020-01-19 13:05:49 -08:00
Dane Everitt
99a11f81c3 Improve event emitter/subscription abilities 2020-01-18 14:04:26 -08:00
Dane Everitt
c6fcd8cabb Send installation data over socket when running 2020-01-18 13:05:44 -08:00
Dane Everitt
5c3823de9a Only dump pull status in debug 2019-12-28 15:14:56 -08:00
Dane Everitt
5350a2d5a5 Don't block the HTTP request while waiting on the installation 2019-12-28 15:12:12 -08:00
Dane Everitt
6ef2773c01 Very, very basic server installation process 2019-12-28 14:57:19 -08:00
Dane Everitt
853d215b1d Less problematic handling for time drift in the socket 2019-12-28 12:27:21 -08:00
Dane Everitt
3bbb8a3769 Relicense 2019-12-25 14:33:02 -08:00
Dane Everitt
a51cb79d3b Create server data directory when installing
closes pterodactyl/panel#1790
2019-12-24 17:13:03 -08:00
Dane Everitt
beec5723e6 Update SFTP server dependency 2019-12-24 17:11:54 -08:00
Dane Everitt
d7bd10fcee Better approach to timezone handling by using a TZ environment variable 2019-12-24 16:57:22 -08:00
Dane Everitt
06f495682c When mounting timezone data, check for the path first to avoid a fatal error 2019-12-24 16:49:08 -08:00
Dane Everitt
32e389db21 General code cleanup 2019-12-24 16:40:51 -08:00
Dane Everitt
d9ceb9601d Do not set the default values in the struct we're unmarshaling into
closes pterodactyl/panel#1786

If you set the defaults, you'll override a bunch of values for the server with the original values. For example this code caused servers in a running state to be changed to a non-running state, thus leading to stats not sending.

Just merge in any new values, leave everything else as empty.
2019-12-24 16:35:43 -08:00
Dane Everitt
ea867d16a5 None of these constants are actually used 2019-12-24 15:30:25 -08:00
20 changed files with 793 additions and 252 deletions

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 Jakob Schrettenbrunner <dev@schrej.net>
Copyright (c) 2019 Dane Everitt <dane@daneeveritt.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -41,6 +41,14 @@ type ProcessConfiguration struct {
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
}
// Defines installation script information for a server process. This is used when
// a server is installed for the first time, and when a server is marked for re-installation.
type InstallationScript struct {
ContainerImage string `json:"container_image"`
Entrypoint string `json:"entrypoint"`
Script string `json:"script"`
}
// Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
@@ -64,3 +72,53 @@ func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfiguration
return res, nil, nil
}
// Fetches installation information for the server process.
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
res := InstallationScript{}
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
if err != nil {
return res, nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return res, r.Error(), nil
}
b, _ := r.ReadBody()
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
}
return res, nil, nil
}
type installRequest struct {
Successful bool `json:"successful"`
}
// Marks a server as being installed successfully or unsuccessfully on the panel.
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(installRequest{Successful: successful})
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}

View File

@@ -166,7 +166,7 @@ type DockerConfiguration struct {
// Defines the location of the timezone file on the host system that should
// be mounted into the created containers so that they all use the same time.
TimezonePath string `yaml:"timezone_path"`
TimezonePath string `default:"/etc/timezone" yaml:"timezone_path"`
}
// Defines the configuration for the internal API that is exposed by the

View File

@@ -1,29 +1,6 @@
package main
import "os"
const (
Version = "1.0.0-alpha.1"
// DefaultFilePerms are the file perms used for created files.
DefaultFilePerms os.FileMode = 0644
// DefaultFolderPerms are the file perms used for created folders.
DefaultFolderPerms os.FileMode = 0755
// ServersPath is the path of the servers within the configured DataPath.
ServersPath = "servers"
// ServerConfigFile is the filename of the server config file.
ServerConfigFile = "server.json"
// ServerDataPath is the path of the data of a single server.
ServerDataPath = "data"
// DockerContainerPrefix is the prefix used for naming Docker containers.
// It's also used to prefix the hostnames of the docker containers.
DockerContainerPrefix = "ptdl-"
// WSMaxMessages is the maximum number of messages that are sent in one transfer.
WSMaxMessages = 10
// The current version of this software.
Version = "1.0.0-alpha.2"
)

4
data/.gitignore vendored
View File

@@ -1 +1,3 @@
servers/*.yml
servers/*.yml
!install_logs/.gitkeep
install_logs/*

View File

2
go.mod
View File

@@ -44,7 +44,7 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.8.1
github.com/pkg/sftp v1.10.1 // indirect
github.com/pterodactyl/sftp-server v1.1.0
github.com/pterodactyl/sftp-server v1.1.1
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
github.com/sirupsen/logrus v1.0.5 // indirect
go.uber.org/atomic v1.5.1 // indirect

2
go.sum
View File

@@ -102,6 +102,8 @@ github.com/pterodactyl/sftp-server v1.0.4 h1:hPUaUQvA6U/R8/bybQFDMBDcZaqqj+kufGB
github.com/pterodactyl/sftp-server v1.0.4/go.mod h1:0LKDl+f1qY2TH9+B5jxdROktW0+10UM1qJ472iWbyvQ=
github.com/pterodactyl/sftp-server v1.1.0 h1:NcYh+UqEH8pfvFsee6yt7eb08RLLidw6q+cNOCdh/V0=
github.com/pterodactyl/sftp-server v1.1.0/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
github.com/pterodactyl/sftp-server v1.1.1 h1:IjuOy21BNZxfejKnXG1RgLxXAYylDqBVpbKZ6+fG5FQ=
github.com/pterodactyl/sftp-server v1.1.1/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

27
http.go
View File

@@ -420,6 +420,19 @@ func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request,
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeServerInstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
go func (serv *server.Server) {
if err := serv.Install(); err != nil {
zap.S().Errorw("failed to execute server installation process", zap.String("server", s.Uuid), zap.Error(err))
}
}(s)
w.WriteHeader(http.StatusAccepted)
}
func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
@@ -451,11 +464,17 @@ func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, ps h
// requests from here-on out.
server.GetServers().Add(inst.Server())
zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid()))
// Begin the installation process in the background to not block the request
// cycle. If there are any errors they will be logged and communicated back
// to the Panel where a reinstall may take place.
zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid()))
go inst.Execute()
go func(i *installer.Installer) {
i.Execute()
if err := i.Server().Install(); err != nil {
zap.S().Errorw("failed to run install process for server", zap.String("server", i.Uuid()), zap.Error(err))
}
}(inst)
w.WriteHeader(http.StatusAccepted)
}
@@ -482,6 +501,7 @@ func (rt *Router) routeServerDelete(w http.ResponseWriter, r *http.Request, ps h
// to start it while this process is running.
s.Suspended = true
zap.S().Infow("processing server deletion request", zap.String("server", s.Uuid))
// Destroy the environment; in Docker this will handle a running container and
// forcibly terminate it before removing the container, so we do not need to handle
// that here.
@@ -545,7 +565,8 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead))
router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory))
router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile))
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer));
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer))
router.POST("/api/servers/:server/install", rt.AuthenticateRequest(rt.routeServerInstall))
router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile))
router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile))
router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory))

View File

@@ -8,6 +8,8 @@ import (
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
"os"
"path"
)
type Installer struct {
@@ -102,6 +104,18 @@ func (i *Installer) Server() *server.Server {
// associated installation process based on the parameters passed through for
// the server instance.
func (i *Installer) Execute() {
zap.S().Debugw("creating required server data directory", zap.String("server", i.Uuid()))
if err := os.MkdirAll(path.Join(config.Get().System.Data, i.Uuid()), 0755); err != nil {
zap.S().Errorw("failed to create server data directory", zap.String("server", i.Uuid()), zap.Error(errors.WithStack(err)))
return
}
if err := os.Chown(path.Join(config.Get().System.Data, i.Uuid()), config.Get().System.User.Uid, config.Get().System.User.Gid); err != nil {
zap.S().Errorw("failed to chown server data directory", zap.String("server", i.Uuid()), zap.Error(errors.WithStack(err)))
return
}
zap.S().Debugw("creating required environment for server instance", zap.String("server", i.Uuid()))
if err := i.server.Environment.Create(); err != nil {
zap.S().Errorw("failed to create environment for server", zap.String("server", i.Uuid()), zap.Error(err))

View File

@@ -1,9 +1,13 @@
package server
import "io"
import (
"fmt"
"github.com/mitchellh/colorstring"
"io"
)
type Console struct {
Server *Server
Server *Server
HandlerFunc *func(string)
}
@@ -18,4 +22,13 @@ func (c Console) Write(b []byte) (int, error) {
}
return len(b), nil
}
}
// Sends output to the server console formatted to appear correctly as being sent
// from Wings.
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
s.Events().Publish(
ConsoleOutputEvent,
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
)
}

View File

@@ -37,7 +37,7 @@ func (s *Server) handleServerCrash() error {
if !s.CrashDetection.Enabled {
zap.S().Debugw("server triggered crash detection but handler is disabled for server process", zap.String("server", s.Uuid))
s.SendConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
s.PublishConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
}
return nil
@@ -56,15 +56,15 @@ func (s *Server) handleServerCrash() error {
return nil
}
s.SendConsoleOutputFromDaemon("---------- Detected server process in a crashed state! ----------")
s.SendConsoleOutputFromDaemon(fmt.Sprintf("Exit code: %d", exitCode))
s.SendConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
s.PublishConsoleOutputFromDaemon("---------- Detected server process in a crashed state! ----------")
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Exit code: %d", exitCode))
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
c := s.CrashDetection.lastCrash
// If the last crash time was within the last 60 seconds we do not want to perform
// an automatic reboot of the process. Return an error that can be handled.
if !c.IsZero() && c.Add(time.Second * 60).After(time.Now()) {
s.SendConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
return &crashTooFrequent{}
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap"
"io"
"os"
@@ -26,13 +27,6 @@ import (
type DockerEnvironment struct {
Server *Server
// The user ID that containers should be running as.
User int
// Defines the configuration for the Docker instance that will allow us to connect
// and create and modify containers.
TimezonePath string
// The Docker client being used for this instance.
Client *client.Client
@@ -51,22 +45,18 @@ type DockerEnvironment struct {
}
// Creates a new base Docker environment. A server must still be attached to it.
func NewDockerEnvironment(opts ...func(*DockerEnvironment)) (*DockerEnvironment, error) {
func NewDockerEnvironment(server *Server) error {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
return err
}
env := &DockerEnvironment{
User: 1000,
server.Environment = &DockerEnvironment{
Server: server,
Client: cli,
}
for _, opt := range opts {
opt(env)
}
return env, nil
return nil
}
// Ensure that the Docker environment is always implementing all of the methods
@@ -210,11 +200,8 @@ func (d *DockerEnvironment) Start() error {
// No reason to try starting a container that is already running.
if c.State.Running {
d.Server.SetState(ProcessRunningState)
if !d.attached {
return d.Attach()
}
return nil
return d.Attach()
}
d.Server.SetState(ProcessStartingState)
@@ -305,6 +292,9 @@ func (d *DockerEnvironment) Terminate(signal os.Signal) error {
func (d *DockerEnvironment) Destroy() error {
ctx := context.Background()
// Avoid crash detection firing off.
d.Server.SetState(ProcessStoppingState)
return d.Client.ContainerRemove(ctx, d.Server.Uuid, types.ContainerRemoveOptions{
RemoveVolumes: true,
RemoveLinks: false,
@@ -354,8 +344,12 @@ func (d *DockerEnvironment) Attach() error {
Server: d.Server,
}
d.EnableResourcePolling()
d.attached = true
go func() {
if err := d.EnableResourcePolling(); err != nil {
zap.S().Warnw("failed to enabled resource polling on server", zap.String("server", d.Server.Uuid), zap.Error(errors.WithStack(err)))
}
}()
go func() {
defer d.stream.Close()
@@ -397,7 +391,7 @@ func (d *DockerEnvironment) FollowConsoleOutput() error {
s := bufio.NewScanner(r)
for s.Scan() {
d.Server.Emit(ConsoleOutputEvent, s.Text())
d.Server.Events().Publish(ConsoleOutputEvent, s.Text())
}
if err := s.Err(); err != nil {
@@ -430,7 +424,10 @@ func (d *DockerEnvironment) EnableResourcePolling() error {
var v *types.StatsJSON
if err := dec.Decode(&v); err != nil {
zap.S().Warnw("encountered error processing server stats; stopping collection", zap.Error(err))
if err != io.EOF {
zap.S().Warnw("encountered error processing server stats; stopping collection", zap.Error(err))
}
d.DisableResourcePolling()
return
}
@@ -456,7 +453,7 @@ func (d *DockerEnvironment) EnableResourcePolling() error {
}
b, _ := json.Marshal(s.Resources)
s.Emit(StatsEvent, string(b))
s.Events().Publish(StatsEvent, string(b))
}
}(d.Server)
@@ -537,7 +534,7 @@ func (d *DockerEnvironment) Create() error {
conf := &container.Config{
Hostname: "container",
User: strconv.Itoa(d.User),
User: strconv.Itoa(config.Get().System.User.Uid),
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
@@ -550,7 +547,8 @@ func (d *DockerEnvironment) Create() error {
Env: d.environmentVariables(),
Labels: map[string]string{
"Service": "Pterodactyl",
"Service": "Pterodactyl",
"ContainerType": "server_process",
},
}
@@ -558,9 +556,7 @@ func (d *DockerEnvironment) Create() error {
PortBindings: d.portBindings(),
// Configure the mounts for this container. First mount the server data directory
// into the container as a r/w bind. Additionally mount the host timezone data into
// the container as a readonly bind so that software running in the container uses
// the same time as the host system.
// into the container as a r/w bind.
Mounts: []mount.Mount{
{
Target: "/home/container",
@@ -568,12 +564,6 @@ func (d *DockerEnvironment) Create() error {
Type: mount.TypeBind,
ReadOnly: false,
},
{
Target: d.TimezonePath,
Source: d.TimezonePath,
Type: mount.TypeBind,
ReadOnly: true,
},
},
// Configure the /tmp folder mapping in containers. This is necessary for some
@@ -610,6 +600,17 @@ func (d *DockerEnvironment) Create() error {
NetworkMode: "pterodactyl_nw",
}
// Pretty sure TZ=X in the environment variables negates the need for this
// to happen. Leaving it until I can confirm that works for everything.
//
// if err := mountTimezoneData(hostConf); err != nil {
// if os.IsNotExist(err) {
// zap.S().Warnw("the timezone data path configured does not exist on the system", zap.Error(errors.WithStack(err)))
// } else {
// zap.S().Warnw("failed to mount timezone data into container", zap.Error(errors.WithStack(err)))
// }
// }
if _, err := cli.ContainerCreate(ctx, conf, hostConf, nil, d.Server.Uuid); err != nil {
return errors.WithStack(err)
}
@@ -617,6 +618,30 @@ func (d *DockerEnvironment) Create() error {
return nil
}
// Given a host configuration mount, also mount the timezone data into it.
func mountTimezoneData(c *container.HostConfig) error {
p := config.Get().System.TimezonePath
// Check for the timezone file, if it exists use it assuming it isn't also a directory,
// otherwise bubble the error back up the stack.
if s, err := os.Stat(p); err != nil {
return err
} else {
if s.IsDir() {
return errors.New("attempting to mount directory as timezone file")
}
}
c.Mounts = append(c.Mounts, mount.Mount{
Target: p,
Source: p,
Type: mount.TypeBind,
ReadOnly: true,
})
return nil
}
// Sends the specified command to the stdin of the running container instance. There is no
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
func (d *DockerEnvironment) SendCommand(c string) error {
@@ -708,7 +733,10 @@ func (d *DockerEnvironment) parseLogToStrings(b []byte) ([]string, error) {
// Returns the environment variables for a server in KEY="VALUE" form.
func (d *DockerEnvironment) environmentVariables() []string {
zone, _ := time.Now().In(time.Local).Zone()
var out = []string{
fmt.Sprintf("TZ=%s", zone),
fmt.Sprintf("STARTUP=%s", d.Server.Invocation),
fmt.Sprintf("SERVER_MEMORY=%d", d.Server.Build.MemoryLimit),
fmt.Sprintf("SERVER_IP=%s", d.Server.Allocations.DefaultMapping.Ip),

View File

@@ -1,63 +1,76 @@
package server
import (
"fmt"
"github.com/mitchellh/colorstring"
"sync"
)
type EventListeners map[string][]EventListenerFunction
type EventListenerFunction *func(string)
// Defines all of the possible output events for a server.
// noinspection GoNameStartsWithPackageName
const (
DaemonMessageEvent = "daemon message"
InstallOutputEvent = "install output"
ConsoleOutputEvent = "console output"
StatusEvent = "status"
StatsEvent = "stats"
)
// Adds an event listener for the server instance.
func (s *Server) AddListener(event string, f EventListenerFunction) {
if s.listeners == nil {
s.listeners = make(map[string][]EventListenerFunction)
type Event struct {
Data string
Topic string
}
type EventBus struct {
subscribers map[string][]chan Event
mu sync.Mutex
}
// Returns the server's emitter instance.
func (s *Server) Events() *EventBus {
if s.emitter == nil {
s.emitter = &EventBus{
subscribers: map[string][]chan Event{},
}
}
if _, ok := s.listeners[event]; ok {
s.listeners[event] = append(s.listeners[event], f)
} else {
s.listeners[event] = []EventListenerFunction{f}
return s.emitter
}
// Publish data to a given topic.
func (e *EventBus) Publish(topic string, data string) {
e.mu.Lock()
defer e.mu.Unlock()
if ch, ok := e.subscribers[topic]; ok {
go func(data Event, cs []chan Event) {
for _, channel := range cs {
channel <- data
}
}(Event{Data: data, Topic: topic}, ch)
}
}
// Removes the event listener for the server instance.
func (s *Server) RemoveListener(event string, f EventListenerFunction) {
if _, ok := s.listeners[event]; ok {
for i := range s.listeners[event] {
if s.listeners[event][i] == f {
s.listeners[event] = append(s.listeners[event][:i], s.listeners[event][i+1:]...)
break
// Subscribe to an emitter topic using a channel.
func (e *EventBus) Subscribe(topic string, ch chan Event) {
e.mu.Lock()
defer e.mu.Unlock()
if p, ok := e.subscribers[topic]; ok {
e.subscribers[topic] = append(p, ch)
} else {
e.subscribers[topic] = append([]chan Event{}, ch)
}
}
// Unsubscribe a channel from a topic.
func (e *EventBus) Unsubscribe(topic string, ch chan Event) {
e.mu.Lock()
defer e.mu.Unlock()
if _, ok := e.subscribers[topic]; ok {
for i := range e.subscribers[topic] {
if ch == e.subscribers[topic][i] {
e.subscribers[topic] = append(e.subscribers[topic][:i], e.subscribers[topic][i+1:]...)
}
}
}
}
// Emits an event to all of the active listeners for a server.
func (s *Server) Emit(event string, data string) {
if _, ok := s.listeners[event]; ok {
for _, handler := range s.listeners[event] {
go func(f EventListenerFunction, d string) {
(*f)(d)
}(handler, data)
}
}
}
// Sends output to the server console formatted to appear correctly as being sent
// from Wings.
func (s *Server) SendConsoleOutputFromDaemon(data string) {
s.Emit(
ConsoleOutputEvent,
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
)
}
}

410
server/install.go Normal file
View File

@@ -0,0 +1,410 @@
package server
import (
"bufio"
"bytes"
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"go.uber.org/zap"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"
)
// Executes the installation stack for a server process. Bubbles any errors up to the calling
// function which should handle contacting the panel to notify it of the server state.
func (s *Server) Install() error {
err := s.internalInstall()
zap.S().Debugw("notifying panel of server install state", zap.String("server", s.Uuid))
if serr := s.SyncInstallState(err == nil); serr != nil {
zap.S().Warnw(
"failed to notify panel of server install state",
zap.String("server", s.Uuid),
zap.Bool("was_successful", err == nil),
zap.Error(serr),
)
}
return err
}
// Internal installation function used to simplify reporting back to the Panel.
func (s *Server) internalInstall() error {
script, rerr, err := api.NewRequester().GetInstallationScript(s.Uuid)
if err != nil || rerr != nil {
if err != nil {
return err
}
return errors.New(rerr.String())
}
p, err := NewInstallationProcess(s, &script)
if err != nil {
return errors.WithStack(err)
}
zap.S().Infow("beginning installation process for server", zap.String("server", s.Uuid))
if err := p.Run(); err != nil {
return err
}
zap.S().Infow("completed installation process for server", zap.String("server", s.Uuid))
return nil
}
type InstallationProcess struct {
Server *Server
Script *api.InstallationScript
client *client.Client
mutex *sync.Mutex
}
// Generates a new installation process struct that will be used to create containers,
// and otherwise perform installation commands for a server.
func NewInstallationProcess(s *Server, script *api.InstallationScript) (*InstallationProcess, error) {
proc := &InstallationProcess{
Script: script,
Server: s,
mutex: &sync.Mutex{},
}
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
return nil, errors.WithStack(err)
} else {
proc.client = c
}
return proc, nil
}
// Runs the installation process, this is done as a backgrounded thread. This will configure
// the required environment, and then spin up the installation container.
//
// Once the container finishes installing the results will be stored in an installation
// log in the server's configuration directory.
func (ip *InstallationProcess) Run() error {
installPath, err := ip.BeforeExecute()
if err != nil {
return err
}
cid, err := ip.Execute(installPath)
if err != nil {
return err
}
// If this step fails, log a warning but don't exit out of the process. This is completely
// internal to the daemon's functionality, and does not affect the status of the server itself.
if err := ip.AfterExecute(cid); err != nil {
zap.S().Warnw("failed to complete after-execute step of installation process", zap.String("server", ip.Server.Uuid), zap.Error(err))
}
return nil
}
// Writes the installation script to a temporary file on the host machine so that it
// can be properly mounted into the installation container and then executed.
func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
d, err := ioutil.TempDir("", "pterodactyl")
if err != nil {
return "", errors.WithStack(err)
}
f, err := os.OpenFile(filepath.Join(d, "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return "", errors.WithStack(err)
}
defer f.Close()
w := bufio.NewWriter(f)
scanner := bufio.NewScanner(bytes.NewReader([]byte(ip.Script.Script)))
for scanner.Scan() {
w.WriteString(scanner.Text() + "\n")
}
if err := scanner.Err(); err != nil {
return "", errors.WithStack(err)
}
w.Flush()
return d, nil
}
// Pulls the docker image to be used for the installation container.
func (ip *InstallationProcess) pullInstallationImage() error {
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, types.ImagePullOptions{})
if err != nil {
return errors.WithStack(err)
}
// Block continuation until the image has been pulled successfully.
scanner := bufio.NewScanner(r)
for scanner.Scan() {
zap.S().Debugw(scanner.Text())
}
if err := scanner.Err(); err != nil {
return errors.WithStack(err)
}
return nil
}
// Runs before the container is executed. This pulls down the required docker container image
// as well as writes the installation script to the disk. This process is executed in an async
// manner, if either one fails the error is returned.
func (ip *InstallationProcess) BeforeExecute() (string, error) {
wg := sync.WaitGroup{}
wg.Add(3)
var e []error
var fileName string
go func() {
defer wg.Done()
name, err := ip.writeScriptToDisk()
if err != nil {
e = append(e, err)
return
}
fileName = name
}()
go func() {
defer wg.Done()
if err := ip.pullInstallationImage(); err != nil {
e = append(e, err)
}
}()
go func() {
defer wg.Done()
opts := types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}
if err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", opts); err != nil {
if !client.IsErrNotFound(err) {
e = append(e, err)
}
}
}()
wg.Wait()
// Maybe a better way to handle this, but if there is at least one error
// just bail out of the process now.
if len(e) > 0 {
return "", errors.WithStack(e[0])
}
return fileName, nil
}
// Cleans up after the execution of the installation process. This grabs the logs from the
// process to store in the server configuration directory, and then destroys the associated
// installation container.
func (ip *InstallationProcess) AfterExecute(containerId string) error {
ctx := context.Background()
zap.S().Debugw("pulling installation logs for server", zap.String("server", ip.Server.Uuid), zap.String("container_id", containerId))
reader, err := ip.client.ContainerLogs(ctx, containerId, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: false,
})
if err != nil && !client.IsErrNotFound(err) {
return errors.WithStack(err)
}
f, err := os.OpenFile(filepath.Join("data/install_logs/", ip.Server.Uuid+".log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
// We write the contents of the container output to a more "permanent" file so that they
// can be referenced after this container is deleted.
if _, err := io.Copy(f, reader); err != nil {
return errors.WithStack(err)
}
zap.S().Debugw("removing server installation container", zap.String("server", ip.Server.Uuid), zap.String("container_id", containerId))
rErr := ip.client.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{
RemoveVolumes: true,
RemoveLinks: false,
Force: true,
})
if rErr != nil && !client.IsErrNotFound(rErr) {
return errors.WithStack(rErr)
}
return nil
}
// Executes the installation process inside a specially created docker container.
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
ctx := context.Background()
zap.S().Debugw(
"creating server installer container",
zap.String("server", ip.Server.Uuid),
zap.String("script_path", installPath+"/install.sh"),
)
conf := &container.Config{
Hostname: "installer",
AttachStdout: true,
AttachStderr: true,
AttachStdin: true,
OpenStdin: true,
Tty: true,
Cmd: []string{ip.Script.Entrypoint, "./mnt/install/install.sh"},
Image: ip.Script.ContainerImage,
Env: ip.Server.GetEnvironmentVariables(),
Labels: map[string]string{
"Service": "Pterodactyl",
"ContainerType": "server_installer",
},
}
hostConf := &container.HostConfig{
Mounts: []mount.Mount{
{
Target: "/mnt/server",
Source: ip.Server.Filesystem.Path(),
Type: mount.TypeBind,
ReadOnly: false,
},
{
Target: "/mnt/install",
Source: installPath,
Type: mount.TypeBind,
ReadOnly: false,
},
},
Tmpfs: map[string]string{
"/tmp": "rw,exec,nosuid,size=50M",
},
DNS: []string{"1.1.1.1", "8.8.8.8"},
LogConfig: container.LogConfig{
Type: "local",
Config: map[string]string{
"max-size": "5m",
"max-file": "1",
"compress": "false",
},
},
Privileged: true,
NetworkMode: "pterodactyl_nw",
}
zap.S().Infow("creating installer container for server process", zap.String("server", ip.Server.Uuid))
r, err := ip.client.ContainerCreate(ctx, conf, hostConf, nil, ip.Server.Uuid+"_installer")
if err != nil {
return "", errors.WithStack(err)
}
zap.S().Infow(
"running installation script for server in container",
zap.String("server", ip.Server.Uuid),
zap.String("container_id", r.ID),
)
if err := ip.client.ContainerStart(ctx, r.ID, types.ContainerStartOptions{}); err != nil {
return "", err
}
go func(id string) {
ip.Server.Events().Publish(DaemonMessageEvent, "Starting installation process, this could take a few minutes...")
if err := ip.StreamOutput(id); err != nil {
zap.S().Errorw(
"error handling streaming output for server install process",
zap.String("container_id", id),
zap.Error(err),
)
}
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
}(r.ID)
sChann, eChann := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning)
select {
case err := <-eChann:
if err != nil {
return "", errors.WithStack(err)
}
case <-sChann:
}
return r.ID, nil
}
// Streams the output of the installation process to a log file in the server configuration
// directory, as well as to a websocket listener so that the process can be viewed in
// the panel by administrators.
func (ip *InstallationProcess) StreamOutput(id string) error {
reader, err := ip.client.ContainerLogs(context.Background(), id, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
})
if err != nil {
return errors.WithStack(err)
}
defer reader.Close()
s := bufio.NewScanner(reader)
for s.Scan() {
ip.Server.Events().Publish(InstallOutputEvent, s.Text())
}
if err := s.Err(); err != nil {
zap.S().Warnw(
"error processing scanner line in installation output for server",
zap.String("server", ip.Server.Uuid),
zap.String("container_id", id),
zap.Error(errors.WithStack(err)),
)
}
return nil
}
// Makes a HTTP request to the Panel instance notifying it that the server has
// completed the installation process, and what the state of the server is. A boolean
// value of "true" means everything was successful, "false" means something went
// wrong and the server must be deleted and re-created.
func (s *Server) SyncInstallState(successful bool) error {
r := api.NewRequester()
rerr, err := r.SendInstallationStatus(s.Uuid, successful)
if rerr != nil || err != nil {
if err != nil {
return errors.WithStack(err)
}
return errors.New(rerr.String())
}
return nil
}

View File

@@ -6,40 +6,41 @@ import (
"strings"
)
// Adds all of the internal event listeners we want to use for a server.
func (s *Server) AddEventListeners() {
s.AddListener(ConsoleOutputEvent, s.onConsoleOutput())
}
consoleChannel := make(chan Event)
s.Events().Subscribe(ConsoleOutputEvent, consoleChannel)
var onConsoleOutputListener func(string)
go func() {
for {
select {
case data := <-consoleChannel:
s.onConsoleOutput(data.Data)
}
}
}()
}
// Custom listener for console output events that will check if the given line
// of output matches one that should mark the server as started or not.
func (s *Server) onConsoleOutput() *func(string) {
if onConsoleOutputListener == nil {
onConsoleOutputListener = func (data string) {
// If the specific line of output is one that would mark the server as started,
// set the server to that state. Only do this if the server is not currently stopped
// or stopping.
if s.State == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
zap.S().Debugw(
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
)
func (s *Server) onConsoleOutput(data string) {
// If the specific line of output is one that would mark the server as started,
// set the server to that state. Only do this if the server is not currently stopped
// or stopping.
if s.State == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
zap.S().Debugw(
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
)
s.SetState(ProcessRunningState)
}
// If the command sent to the server is one that should stop the server we will need to
// set the server to be in a stopping state, otherwise crash detection will kick in and
// cause the server to unexpectedly restart on the user.
if s.State == ProcessStartingState || s.State == ProcessRunningState {
if s.processConfiguration.Stop.Type == api.ProcessStopCommand && data == s.processConfiguration.Stop.Value {
s.SetState(ProcessStoppingState)
}
}
}
s.SetState(ProcessRunningState)
}
return &onConsoleOutputListener
}
// If the command sent to the server is one that should stop the server we will need to
// set the server to be in a stopping state, otherwise crash detection will kick in and
// cause the server to unexpectedly restart on the user.
if s.State == ProcessStartingState || s.State == ProcessRunningState {
if s.processConfiguration.Stop.Type == api.ProcessStopCommand && data == s.processConfiguration.Stop.Value {
s.SetState(ProcessStoppingState)
}
}
}

View File

@@ -64,8 +64,8 @@ type Server struct {
// certain long operations return faster. For example, FS disk space usage.
Cache *cache.Cache `json:"-" yaml:"-"`
// All of the registered event listeners for this server instance.
listeners EventListeners
// Events emitted by the server instance.
emitter *EventBus
// Defines the process configuration for the server instance. This is dynamically
// fetched from the Pterodactyl Server instance each time the server process is
@@ -199,7 +199,6 @@ func LoadDirectory(dir string, cfg *config.SystemConfiguration) error {
// Initializes the default required internal struct components for a Server.
func (s *Server) Init() {
s.listeners = make(map[string][]EventListenerFunction)
s.mutex = &sync.Mutex{}
}
@@ -221,23 +220,13 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
s.AddEventListeners()
withConfiguration := func(e *DockerEnvironment) {
e.User = cfg.User.Uid
e.TimezonePath = cfg.TimezonePath
e.Server = s
}
// Right now we only support a Docker based environment, so I'm going to hard code
// this logic in. When we're ready to support other environment we'll need to make
// some modifications here obviously.
var env Environment
if t, err := NewDockerEnvironment(withConfiguration); err == nil {
env = t
} else {
if err := NewDockerEnvironment(s); err != nil {
return nil, err
}
s.Environment = env
s.Cache = cache.New(time.Minute*10, time.Minute*15)
s.Filesystem = Filesystem{
Configuration: cfg,
@@ -257,6 +246,33 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
return s, nil
}
// Returns all of the environment variables that should be assigned to a running
// server instance.
func (s *Server) GetEnvironmentVariables() []string {
zone, _ := time.Now().In(time.Local).Zone()
var out = []string{
fmt.Sprintf("TZ=%s", zone),
fmt.Sprintf("STARTUP=%s", s.Invocation),
fmt.Sprintf("SERVER_MEMORY=%d", s.Build.MemoryLimit),
fmt.Sprintf("SERVER_IP=%s", s.Allocations.DefaultMapping.Ip),
fmt.Sprintf("SERVER_PORT=%d", s.Allocations.DefaultMapping.Port),
}
eloop:
for k, v := range s.EnvVars {
for _, e := range out {
if strings.HasPrefix(e, strings.ToUpper(k)) {
continue eloop
}
}
out = append(out, fmt.Sprintf("%s=%s", strings.ToUpper(k), v))
}
return out
}
// Syncs the state of the server on the Panel with Wings. This ensures that we're always
// using the state of the server from the Panel and allows us to not require successful
// API calls to Wings to do things.
@@ -337,10 +353,10 @@ func (s *Server) SetState(state string) error {
}
}(s)
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", state))
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", s.State))
// Emit the event to any listeners that are currently registered.
s.Emit(StatusEvent, s.State)
s.Events().Publish(StatusEvent, s.State)
// If server was in an online state, and is now in an offline state we should handle
// that as a crash event. In that scenario, check the last crash time, and the crash

View File

@@ -3,7 +3,6 @@ package server
import (
"encoding/json"
"github.com/buger/jsonparser"
"github.com/creasty/defaults"
"github.com/imdario/mergo"
"github.com/pkg/errors"
"go.uber.org/zap"
@@ -30,11 +29,6 @@ func (s *Server) UpdateDataStructure(data []byte, background bool) error {
return errors.New("attempting to merge a data stack with an invalid UUID")
}
// Set the default values in the interface that we unmarshaled into.
if err := defaults.Set(src); err != nil {
return errors.WithStack(err)
}
// Merge the new data object that we have received with the existing server data object
// and then save it to the disk so it is persistent.
if err := mergo.Merge(s, src, mergo.WithOverride); err != nil {

View File

@@ -62,10 +62,11 @@ type WebsocketTokenPayload struct {
}
const (
PermissionConnect = "connect"
PermissionSendCommand = "send-command"
PermissionSendPower = "send-power"
PermissionReceiveErrors = "receive-errors"
PermissionConnect = "connect"
PermissionSendCommand = "send-command"
PermissionSendPower = "send-power"
PermissionReceiveErrors = "receive-errors"
PermissionReceiveInstall = "receive-install"
)
// Checks if the given token payload has a permission string.
@@ -92,24 +93,17 @@ func ParseJWT(token []byte) (*WebsocketTokenPayload, error) {
alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken))
}
_, err := jwt.Verify(token, alg, &payload)
now := time.Now()
verifyOptions := jwt.ValidatePayload(
&payload.Payload,
jwt.ExpirationTimeValidator(now),
)
_, err := jwt.Verify(token, alg, &payload, verifyOptions)
if err != nil {
return nil, err
}
// Check the time of the JWT becoming valid does not exceed more than 15 seconds
// compared to the system time. This accounts for clock drift to some degree.
if time.Now().Unix()-payload.NotBefore.Unix() <= -15 {
return nil, errors.New("jwt violates nbf")
}
// Compare the expiration time of the token to the current system time. Include
// up to 15 seconds of clock drift, and if it has expired return an error and
// do not process the action.
if time.Now().Unix()-payload.ExpirationTime.Unix() > 15 {
return nil, errors.New("jwt violates exp")
}
if !payload.HasPermission(PermissionConnect) {
return nil, errors.New("not authorized to connect to this socket")
}
@@ -123,8 +117,8 @@ func (wsh *WebsocketHandler) TokenValid() error {
return errors.New("no jwt present")
}
if time.Now().Unix()-wsh.JWT.ExpirationTime.Unix() > 15 {
return errors.New("jwt violates nbf")
if err := jwt.ExpirationTimeValidator(time.Now())(&wsh.JWT.Payload); err != nil {
return err
}
if !wsh.JWT.HasPermission(PermissionConnect) {
@@ -170,35 +164,39 @@ func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps http
JWT: nil,
}
handleOutput := func(data string) {
handler.SendJson(&WebsocketMessage{
Event: server.ConsoleOutputEvent,
Args: []string{data},
})
events := []string{
server.StatsEvent,
server.StatusEvent,
server.ConsoleOutputEvent,
server.InstallOutputEvent,
server.DaemonMessageEvent,
}
handleServerStatus := func(data string) {
handler.SendJson(&WebsocketMessage{
Event: server.StatusEvent,
Args: []string{data},
})
eventChannel := make(chan server.Event)
for _, event := range events {
s.Events().Subscribe(event, eventChannel)
}
handleResourceUse := func(data string) {
handler.SendJson(&WebsocketMessage{
Event: server.StatsEvent,
Args: []string{data},
})
}
defer func() {
for _, event := range events {
s.Events().Unsubscribe(event, eventChannel)
}
s.AddListener(server.StatusEvent, &handleServerStatus)
defer s.RemoveListener(server.StatusEvent, &handleServerStatus)
close(eventChannel)
}()
s.AddListener(server.ConsoleOutputEvent, &handleOutput)
defer s.RemoveListener(server.ConsoleOutputEvent, &handleOutput)
s.AddListener(server.StatsEvent, &handleResourceUse)
defer s.RemoveListener(server.StatsEvent, &handleResourceUse)
// Listen for different events emitted by the server and respond to them appropriately.
go func() {
for {
select {
case d := <-eventChannel:
handler.SendJson(&WebsocketMessage{
Event: d.Topic,
Args: []string{d.Data},
})
}
}
}()
// Sit here and check the time to expiration on the JWT every 30 seconds until
// the token has expired. If we are within 3 minutes of the token expiring, send
@@ -257,13 +255,22 @@ func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps http
// Perform a blocking send operation on the websocket since we want to avoid any
// concurrent writes to the connection, which would cause a runtime panic and cause
// the program to crash out.
func (wsh *WebsocketHandler) SendJson(v interface{}) error {
func (wsh *WebsocketHandler) SendJson(v *WebsocketMessage) error {
// Do not send JSON down the line if the JWT on the connection is not
// valid!
if err := wsh.TokenValid(); err != nil {
return nil
}
// If we're sending installation output but the user does not have the required
// permissions to see the output, don't send it down the line.
if v.Event == server.InstallOutputEvent {
zap.S().Debugf("%+v", v.Args)
if wsh.JWT != nil && !wsh.JWT.HasPermission(PermissionReceiveInstall) {
return nil
}
}
return wsh.unsafeSendJson(v)
}
@@ -351,7 +358,7 @@ func (wsh *WebsocketHandler) HandleInbound(m WebsocketMessage) error {
// On every authentication event, send the current server status back
// to the client. :)
wsh.Server.Emit(server.StatusEvent, wsh.Server.State)
wsh.Server.Events().Publish(server.StatusEvent, wsh.Server.State)
wsh.unsafeSendJson(WebsocketMessage{
Event: AuthenticationSuccessEvent,

View File

@@ -106,48 +106,33 @@ func main() {
zap.S().Errorw("failed to create an environment for server", zap.String("server", s.Uuid), zap.Error(err))
}
if r, err := s.Environment.IsRunning(); err != nil {
r, err := s.Environment.IsRunning()
if err != nil {
zap.S().Errorw("error checking server environment status", zap.String("server", s.Uuid), zap.Error(err))
} else if r {
// If the server is currently running on Docker, mark the process as being in that state.
// We never want to stop an instance that is currently running external from Wings since
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
zap.S().Infow("detected server is running, re-attaching to process", zap.String("server", s.Uuid))
if err := s.Sync(); err != nil {
zap.S().Errorw("failed to sync server state, cannot mark as running", zap.String("server", s.Uuid), zap.Error(errors.WithStack(err)))
} else {
s.SetState(server.ProcessRunningState)
// If we cannot attach to the environment go ahead and mark the processs as being offline.
if err := s.Environment.Attach(); err != nil {
zap.S().Warnw("error attaching to server environment", zap.String("server", s.Uuid), zap.Error(err))
s.SetState(server.ProcessOfflineState)
}
}
} else if !r {
// If the server is not in a running state right now but according to the configuration it
// should be, we want to go ahead and restart the instance.
if s.State == server.ProcessRunningState || s.State == server.ProcessStartingState {
zap.S().Infow(
"server state does not match last recorded state in configuration, starting instance now",
zap.String("server", s.Uuid),
)
if err := s.Environment.Start(); err != nil {
zap.S().Warnw(
"failed to put server instance back in running state",
zap.String("server", s.Uuid),
zap.Error(errors.WithStack(err)),
)
}
} else {
if s.State == "" {
// Addresses potentially invalid data in the stored file that can cause Wings to lose
// track of what the actual server state is.
s.SetState(server.ProcessOfflineState)
}
}
}
// If the server is currently running on Docker, mark the process as being in that state.
// We never want to stop an instance that is currently running external from Wings since
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
//
// This will also validate that a server process is running if the last tracked state we have
// is that it was running, but we see that the container process is not currently running.
if r || (!r && (s.State == server.ProcessRunningState || s.State == server.ProcessStartingState)) {
zap.S().Infow("detected server is running, re-attaching to process", zap.String("server", s.Uuid))
if err := s.Environment.Start(); err != nil {
zap.S().Warnw(
"failed to properly start server detected as already running",
zap.String("server", s.Uuid),
zap.Error(errors.WithStack(err)),
)
}
return
}
// Addresses potentially invalid data in the stored file that can cause Wings to lose
// track of what the actual server state is.
s.SetState(server.ProcessOfflineState)
}(serv)
}