Refactor environment handling logic to separate a server from the environment handler itself
This change makes the environment handling logic execute independent of the server itself and should make it much easier for people to contribute changes and additional environment handlers down the road without polluting the server object even more. There is still a lot of work to do on this front to make things easier to work with, and there are some questionable design decisions at play I'm sure. Welcome to additional modifications and cleanup to make this code easier to reason about and work with.
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
package server
|
||||
|
||||
// Defines the allocations available for a given server. When using the Docker environment
|
||||
// driver these correspond to mappings for the container that allow external connections.
|
||||
type Allocations struct {
|
||||
// Defines the default allocation that should be used for this server. This is
|
||||
// what will be used for {SERVER_IP} and {SERVER_PORT} when modifying configuration
|
||||
// files or the startup arguments for a server.
|
||||
DefaultMapping struct {
|
||||
Ip string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
} `json:"default"`
|
||||
|
||||
// Mappings contains all of the ports that should be assigned to a given server
|
||||
// attached to the IP they correspond to.
|
||||
Mappings map[string][]int `json:"mappings"`
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package server
|
||||
|
||||
import "math"
|
||||
|
||||
// The build settings for a given server that impact docker container creation and
|
||||
// resource limits for a server instance.
|
||||
type BuildSettings struct {
|
||||
// The total amount of memory in megabytes that this server is allowed to
|
||||
// use on the host system.
|
||||
MemoryLimit int64 `json:"memory_limit"`
|
||||
|
||||
// The amount of additional swap space to be provided to a container instance.
|
||||
Swap int64 `json:"swap"`
|
||||
|
||||
// The relative weight for IO operations in a container. This is relative to other
|
||||
// containers on the system and should be a value between 10 and 1000.
|
||||
IoWeight uint16 `json:"io_weight"`
|
||||
|
||||
// The percentage of CPU that this instance is allowed to consume relative to
|
||||
// the host. A value of 200% represents complete utilization of two cores. This
|
||||
// should be a value between 1 and THREAD_COUNT * 100.
|
||||
CpuLimit int64 `json:"cpu_limit"`
|
||||
|
||||
// The amount of disk space in megabytes that a server is allowed to use.
|
||||
DiskSpace int64 `json:"disk_space"`
|
||||
|
||||
// Sets which CPU threads can be used by the docker instance.
|
||||
Threads string `json:"threads"`
|
||||
}
|
||||
|
||||
func (s *Server) Build() *BuildSettings {
|
||||
return &s.Config().Build
|
||||
}
|
||||
|
||||
// Converts the CPU limit for a server build into a number that can be better understood
|
||||
// by the Docker environment. If there is no limit set, return -1 which will indicate to
|
||||
// Docker that it has unlimited CPU quota.
|
||||
func (b *BuildSettings) ConvertedCpuLimit() int64 {
|
||||
if b.CpuLimit == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return b.CpuLimit * 1000
|
||||
}
|
||||
|
||||
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
|
||||
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
|
||||
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
|
||||
func (b *BuildSettings) MemoryOverheadMultiplier() float64 {
|
||||
if b.MemoryLimit <= 2048 {
|
||||
return 1.15
|
||||
} else if b.MemoryLimit <= 4096 {
|
||||
return 1.10
|
||||
}
|
||||
|
||||
return 1.05
|
||||
}
|
||||
|
||||
func (b *BuildSettings) BoundedMemoryLimit() int64 {
|
||||
return int64(math.Round(float64(b.MemoryLimit) * b.MemoryOverheadMultiplier() * 1_000_000))
|
||||
}
|
||||
|
||||
// Returns the amount of swap available as a total in bytes. This is returned as the amount
|
||||
// of memory available to the server initially, PLUS the amount of additional swap to include
|
||||
// which is the format used by Docker.
|
||||
func (b *BuildSettings) ConvertedSwap() int64 {
|
||||
if b.Swap < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return (b.Swap * 1_000_000) + b.BoundedMemoryLimit()
|
||||
}
|
||||
@@ -1,41 +1,10 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EnvironmentVariables map[string]interface{}
|
||||
|
||||
// Ugly hacky function to handle environment variables that get passed through as not-a-string
|
||||
// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
|
||||
// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
|
||||
// try to handle the most likely values from the JSON and hope for the best.
|
||||
func (ev EnvironmentVariables) Get(key string) string {
|
||||
val, ok := ev[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch val.(type) {
|
||||
case int:
|
||||
return strconv.Itoa(val.(int))
|
||||
case int32:
|
||||
return strconv.FormatInt(val.(int64), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(val.(int64), 10)
|
||||
case float32:
|
||||
return fmt.Sprintf("%f", val.(float32))
|
||||
case float64:
|
||||
return fmt.Sprintf("%f", val.(float64))
|
||||
case bool:
|
||||
return strconv.FormatBool(val.(bool))
|
||||
}
|
||||
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
@@ -53,20 +22,17 @@ type Configuration struct {
|
||||
|
||||
// An array of environment variables that should be passed along to the running
|
||||
// server process.
|
||||
EnvVars EnvironmentVariables `json:"environment"`
|
||||
EnvVars environment.Variables `json:"environment"`
|
||||
|
||||
Allocations Allocations `json:"allocations"`
|
||||
Build BuildSettings `json:"build"`
|
||||
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
|
||||
Mounts []Mount `json:"mounts"`
|
||||
Resources ResourceUsage `json:"resources"`
|
||||
Allocations environment.Allocations `json:"allocations"`
|
||||
Build environment.Limits `json:"build"`
|
||||
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
|
||||
Mounts []Mount `json:"mounts"`
|
||||
Resources ResourceUsage `json:"resources"`
|
||||
|
||||
Container struct {
|
||||
// Defines the Docker image that will be used for this server
|
||||
Image string `json:"image,omitempty"`
|
||||
// If set to true, OOM killer will be disabled on the server's Docker container.
|
||||
// If not present (nil) we will default to disabling it.
|
||||
OomDisabled bool `default:"true" json:"oom_disabled"`
|
||||
} `json:"container,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,27 +3,100 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"io"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Console struct {
|
||||
Server *Server
|
||||
HandlerFunc *func(string)
|
||||
type ConsoleThrottler struct {
|
||||
sync.RWMutex
|
||||
config.ConsoleThrottles
|
||||
|
||||
// The total number of activations that have occurred thus far.
|
||||
activations uint64
|
||||
|
||||
// The total number of lines processed so far during the given time period.
|
||||
lines uint64
|
||||
|
||||
lastIntervalTime *time.Time
|
||||
lastDecayTime *time.Time
|
||||
}
|
||||
|
||||
var _ io.Writer = Console{}
|
||||
// Increments the number of activations for a server.
|
||||
func (ct *ConsoleThrottler) AddActivation() uint64 {
|
||||
ct.Lock()
|
||||
defer ct.Unlock()
|
||||
|
||||
func (c Console) Write(b []byte) (int, error) {
|
||||
if c.HandlerFunc != nil {
|
||||
l := make([]byte, len(b))
|
||||
copy(l, b)
|
||||
ct.activations += 1
|
||||
|
||||
(*c.HandlerFunc)(string(l))
|
||||
return ct.activations
|
||||
}
|
||||
|
||||
// Decrements the number of activations for a server.
|
||||
func (ct *ConsoleThrottler) RemoveActivation() uint64 {
|
||||
ct.Lock()
|
||||
defer ct.Unlock()
|
||||
|
||||
if ct.activations == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
ct.activations -= 1
|
||||
|
||||
return ct.activations
|
||||
}
|
||||
|
||||
// Increment the total count of lines that we have processed so far.
|
||||
func (ct *ConsoleThrottler) IncrementLineCount() uint64 {
|
||||
return atomic.AddUint64(&ct.lines, 1)
|
||||
}
|
||||
|
||||
// Reset the line count to zero.
|
||||
func (ct *ConsoleThrottler) ResetLineCount() {
|
||||
atomic.SwapUint64(&ct.lines, 0)
|
||||
}
|
||||
|
||||
// Handles output from a server's console. This code ensures that a server is not outputting
|
||||
// an excessive amount of data to the console that could indicate a malicious or run-away process
|
||||
// and lead to performance issues for other users.
|
||||
//
|
||||
// This was much more of a problem for the NodeJS version of the daemon which struggled to handle
|
||||
// large volumes of output. However, this code is much more performant so I generally feel a lot
|
||||
// better about it's abilities.
|
||||
//
|
||||
// However, extreme output is still somewhat of a DoS attack vector against this software since we
|
||||
// are still logging it to the disk temporarily and will want to avoid dumping a huge amount of
|
||||
// data all at once. These values are all configurable via the wings configuration file, however the
|
||||
// defaults have been in the wild for almost two years at the time of this writing, so I feel quite
|
||||
// confident in them.
|
||||
func (ct *ConsoleThrottler) Handle() {
|
||||
|
||||
}
|
||||
|
||||
// Returns the throttler instance for the server or creates a new one.
|
||||
func (s *Server) Throttler() *ConsoleThrottler {
|
||||
s.throttleLock.RLock()
|
||||
|
||||
if s.throttler == nil {
|
||||
// Release the read lock so that we can acquire a normal lock on the process and
|
||||
// make modifications to the throttler.
|
||||
s.throttleLock.RUnlock()
|
||||
|
||||
s.throttleLock.Lock()
|
||||
s.throttler = &ConsoleThrottler{
|
||||
ConsoleThrottles: config.Get().Throttles,
|
||||
}
|
||||
s.throttleLock.Unlock()
|
||||
|
||||
return s.throttler
|
||||
} else {
|
||||
defer s.throttleLock.RUnlock()
|
||||
return s.throttler
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sends output to the server console formatted to appear correctly as being sent
|
||||
// from Wings.
|
||||
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Defines the basic interface that all environments need to implement so that
|
||||
// a server can be properly controlled.
|
||||
type Environment interface {
|
||||
// Returns the name of the environment.
|
||||
Type() string
|
||||
|
||||
// Determines if the environment is currently active and running a server process
|
||||
// for this specific server instance.
|
||||
IsRunning() (bool, error)
|
||||
|
||||
// Performs an update of server resource limits without actually stopping the server
|
||||
// process. This only executes if the environment supports it, otherwise it is
|
||||
// a no-op.
|
||||
InSituUpdate() error
|
||||
|
||||
// Runs before the environment is started. If an error is returned starting will
|
||||
// not occur, otherwise proceeds as normal.
|
||||
OnBeforeStart() error
|
||||
|
||||
// Starts a server instance. If the server instance is not in a state where it
|
||||
// can be started an error should be returned.
|
||||
Start() error
|
||||
|
||||
// Stops a server instance. If the server is already stopped an error should
|
||||
// not be returned.
|
||||
Stop() error
|
||||
|
||||
// Restart a server instance. If already stopped the process will be started. This function
|
||||
// will return an error if the server is already performing a restart process as to avoid
|
||||
// unnecessary double/triple/quad looping issues if multiple people press restart or spam the
|
||||
// button to restart.
|
||||
Restart() error
|
||||
|
||||
// Waits for a server instance to stop gracefully. If the server is still detected
|
||||
// as running after seconds, an error will be returned, or the server will be terminated
|
||||
// depending on the value of the second argument.
|
||||
WaitForStop(seconds int, terminate bool) error
|
||||
|
||||
// Determines if the server instance exists. For example, in a docker environment
|
||||
// this should confirm that the container is created and in a bootable state. In
|
||||
// a basic CLI environment this can probably just return true right away.
|
||||
Exists() (bool, error)
|
||||
|
||||
// Terminates a running server instance using the provided signal. If the server
|
||||
// is not running no error should be returned.
|
||||
Terminate(signal os.Signal) error
|
||||
|
||||
// Destroys the environment removing any containers that were created (in Docker
|
||||
// environments at least).
|
||||
Destroy() error
|
||||
|
||||
// Returns the exit state of the process. The first result is the exit code, the second
|
||||
// determines if the process was killed by the system OOM killer.
|
||||
ExitState() (uint32, bool, error)
|
||||
|
||||
// Creates the necessary environment for running the server process. For example,
|
||||
// in the Docker environment create will create a new container instance for the
|
||||
// server.
|
||||
Create() error
|
||||
|
||||
// Attaches to the server console environment and allows piping the output to a
|
||||
// websocket or other internal tool to monitor output. Also allows you to later
|
||||
// send data into the environment's stdin.
|
||||
Attach() error
|
||||
|
||||
// Follows the output from the server console and will begin piping the output to
|
||||
// the server's emitter.
|
||||
FollowConsoleOutput() error
|
||||
|
||||
// Sends the provided command to the running server instance.
|
||||
SendCommand(string) error
|
||||
|
||||
// Reads the log file for the process from the end backwards until the provided
|
||||
// number of bytes is met.
|
||||
Readlog(int64) ([]string, error)
|
||||
|
||||
// Polls the given environment for resource usage of the server when the process
|
||||
// is running.
|
||||
EnableResourcePolling() error
|
||||
|
||||
// Disables the polling operation for resource usage and sets the required values
|
||||
// to 0 in the server resource usage struct.
|
||||
DisableResourcePolling() error
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
102
server/events.go
102
server/events.go
@@ -1,9 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
)
|
||||
|
||||
// Defines all of the possible output events for a server.
|
||||
@@ -19,108 +17,14 @@ const (
|
||||
BackupCompletedEvent = "backup completed"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Data string
|
||||
Topic string
|
||||
}
|
||||
|
||||
type EventBus struct {
|
||||
sync.RWMutex
|
||||
|
||||
subscribers map[string]map[chan Event]struct{}
|
||||
}
|
||||
|
||||
// Returns the server's emitter instance.
|
||||
func (s *Server) Events() *EventBus {
|
||||
func (s *Server) Events() *events.EventBus {
|
||||
s.emitterLock.Lock()
|
||||
defer s.emitterLock.Unlock()
|
||||
|
||||
if s.emitter == nil {
|
||||
s.emitter = &EventBus{
|
||||
subscribers: make(map[string]map[chan Event]struct{}),
|
||||
}
|
||||
s.emitter = events.New()
|
||||
}
|
||||
|
||||
return s.emitter
|
||||
}
|
||||
|
||||
// Publish data to a given topic.
|
||||
func (e *EventBus) Publish(topic string, data string) {
|
||||
t := topic
|
||||
// Some of our topics for the socket support passing a more specific namespace,
|
||||
// such as "backup completed:1234" to indicate which specific backup was completed.
|
||||
//
|
||||
// In these cases, we still need to the send the event using the standard listener
|
||||
// name of "backup completed".
|
||||
if strings.Contains(topic, ":") {
|
||||
parts := strings.SplitN(topic, ":", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
t = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire a read lock and loop over all of the channels registered for the topic. This
|
||||
// avoids a panic crash if the process tries to unregister the channel while this routine
|
||||
// is running.
|
||||
go func() {
|
||||
e.RLock()
|
||||
defer e.RUnlock()
|
||||
|
||||
if ch, ok := e.subscribers[t]; ok {
|
||||
for channel := range ch {
|
||||
channel <- Event{Data: data, Topic: topic}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Publish(topic, string(b))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe to an emitter topic using a channel.
|
||||
func (e *EventBus) Subscribe(topic string, ch chan Event) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
|
||||
if _, exists := e.subscribers[topic]; !exists {
|
||||
e.subscribers[topic] = make(map[chan Event]struct{})
|
||||
}
|
||||
|
||||
// Only set the channel if there is not currently a matching one for this topic. This
|
||||
// avoids registering two identical listeners for the same topic and causing pain in
|
||||
// the unsubscribe functionality as well.
|
||||
if _, exists := e.subscribers[topic][ch]; !exists {
|
||||
e.subscribers[topic][ch] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe a channel from a given topic.
|
||||
func (e *EventBus) Unsubscribe(topic string, ch chan Event) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
|
||||
if _, exists := e.subscribers[topic][ch]; exists {
|
||||
delete(e.subscribers[topic], ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Removes all of the event listeners for the server. This is used when a server
|
||||
// is being deleted to avoid a bunch of de-reference errors cropping up. Obviously
|
||||
// should also check elsewhere and handle a server reference going nil, but this
|
||||
// won't hurt.
|
||||
func (e *EventBus) UnsubscribeAll() {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
|
||||
// Reset the entire struct into an empty map.
|
||||
e.subscribers = make(map[string]map[chan Event]struct{})
|
||||
}
|
||||
|
||||
@@ -3,19 +3,34 @@ package server
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Adds all of the internal event listeners we want to use for a server.
|
||||
func (s *Server) AddEventListeners() {
|
||||
consoleChannel := make(chan Event)
|
||||
s.Events().Subscribe(ConsoleOutputEvent, consoleChannel)
|
||||
func (s *Server) StartEventListeners() {
|
||||
consoleChannel := make(chan events.Event)
|
||||
stateChannel := make(chan events.Event)
|
||||
|
||||
s.Environment.Events().Subscribe(environment.ConsoleOutputEvent, consoleChannel)
|
||||
s.Environment.Events().Subscribe(environment.StateChangeEvent, stateChannel)
|
||||
|
||||
// TODO: this is leaky I imagine since the routines aren't destroyed when the server is?
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data := <-consoleChannel:
|
||||
// Immediately emit this event back over the server event stream since it is
|
||||
// being called from the environment event stream and things probably aren't
|
||||
// listening to that event.
|
||||
s.Events().Publish(ConsoleOutputEvent, data.Data)
|
||||
|
||||
// Also pass the data along to the console output channel.
|
||||
s.onConsoleOutput(data.Data)
|
||||
case data := <-stateChannel:
|
||||
s.SetState(data.Data)
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/environment/docker"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
@@ -87,21 +89,24 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.AddEventListeners()
|
||||
s.cache = cache.New(time.Minute*10, time.Minute*15)
|
||||
s.Archiver = Archiver{Server: s}
|
||||
s.Filesystem = Filesystem{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.
|
||||
if err := NewDockerEnvironment(s); err != nil {
|
||||
return nil, err
|
||||
envCfg := environment.NewConfiguration(s.Mounts(), s.cfg.Allocations, s.cfg.Build, s.cfg.EnvVars)
|
||||
meta := docker.Metadata{
|
||||
Invocation: s.Config().Invocation,
|
||||
Image: s.Config().Container.Image,
|
||||
}
|
||||
|
||||
s.cache = cache.New(time.Minute*10, time.Minute*15)
|
||||
s.Archiver = Archiver{
|
||||
Server: s,
|
||||
}
|
||||
s.Filesystem = Filesystem{
|
||||
Server: s,
|
||||
if env, err := docker.New(s.Id(), &meta, envCfg); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
s.Environment = env
|
||||
s.StartEventListeners()
|
||||
}
|
||||
|
||||
// Forces the configuration to be synced with the panel.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package server
|
||||
|
||||
// Mount represents a Server Mount.
|
||||
type Mount struct {
|
||||
Target string `json:"target"`
|
||||
Source string `json:"source"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
98
server/mounts.go
Normal file
98
server/mounts.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// To avoid confusion when working with mounts, assume that a server.Mount has not been properly
|
||||
// cleaned up and had the paths set. An environment.Mount should only be returned with valid paths
|
||||
// that have been checked.
|
||||
type Mount environment.Mount
|
||||
|
||||
// Returns the default container mounts for the server instance. This includes the data directory
|
||||
// for the server as well as any timezone related files if they exist on the host system so that
|
||||
// servers running within the container will use the correct time.
|
||||
func (s *Server) Mounts() []environment.Mount {
|
||||
var m []environment.Mount
|
||||
|
||||
m = append(m, environment.Mount{
|
||||
Default: true,
|
||||
Target: "/home/container",
|
||||
Source: s.Filesystem.Path(),
|
||||
ReadOnly: false,
|
||||
})
|
||||
|
||||
// Try to mount in /etc/localtime and /etc/timezone if they exist on the host system.
|
||||
if _, err := os.Stat("/etc/localtime"); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/localtime due to an error")
|
||||
}
|
||||
} else {
|
||||
m = append(m, environment.Mount{
|
||||
Target: "/etc/localtime",
|
||||
Source: "/etc/localtime",
|
||||
ReadOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/etc/timezone"); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/timezone due to an error")
|
||||
}
|
||||
} else {
|
||||
m = append(m, environment.Mount{
|
||||
Target: "/etc/timezone",
|
||||
Source: "/etc/timezone",
|
||||
ReadOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Also include any of this server's custom mounts when returning them.
|
||||
return append(m, s.customMounts()...)
|
||||
}
|
||||
|
||||
// Returns the custom mounts for a given server after verifying that they are within a list of
|
||||
// allowed mount points for the node.
|
||||
func (s *Server) customMounts() []environment.Mount {
|
||||
var mounts []environment.Mount
|
||||
|
||||
// TODO: probably need to handle things trying to mount directories that do not exist.
|
||||
for _, m := range s.Config().Mounts {
|
||||
source := filepath.Clean(m.Source)
|
||||
target := filepath.Clean(m.Target)
|
||||
|
||||
logger := s.Log().WithFields(log.Fields{
|
||||
"source_path": source,
|
||||
"target_path": target,
|
||||
"read_only": m.ReadOnly,
|
||||
})
|
||||
|
||||
mounted := false
|
||||
for _, allowed := range config.Get().AllowedMounts {
|
||||
if !strings.HasPrefix(source, allowed) {
|
||||
continue
|
||||
}
|
||||
|
||||
mounted = true
|
||||
mounts = append(mounts, environment.Mount{
|
||||
Source: source,
|
||||
Target: target,
|
||||
ReadOnly: m.ReadOnly,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if !mounted {
|
||||
logger.Warn("skipping custom server mount, not in list of allowed mount points")
|
||||
}
|
||||
}
|
||||
|
||||
return mounts
|
||||
}
|
||||
@@ -32,6 +32,10 @@ func (pa PowerAction) IsValid() bool {
|
||||
pa == PowerActionRestart
|
||||
}
|
||||
|
||||
func (pa PowerAction) IsStart() bool {
|
||||
return pa == PowerActionStart || pa == PowerActionRestart
|
||||
}
|
||||
|
||||
// Helper function that can receive a power action and then process the actions that need
|
||||
// to occur for it. This guards against someone calling Start() twice at the same time, or
|
||||
// trying to restart while another restart process is currently running.
|
||||
@@ -40,6 +44,11 @@ func (pa PowerAction) IsValid() bool {
|
||||
// function rather than making direct calls to the start/stop/restart functions on the
|
||||
// environment struct.
|
||||
func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error {
|
||||
// Disallow start & restart if the server is suspended.
|
||||
if action.IsStart() && s.IsSuspended() {
|
||||
return new(suspendedError)
|
||||
}
|
||||
|
||||
if s.powerLock == nil {
|
||||
s.powerLock = semaphore.NewWeighted(1)
|
||||
}
|
||||
@@ -65,6 +74,17 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
||||
// Release the lock once the process being requested has finished executing.
|
||||
defer s.powerLock.Release(1)
|
||||
|
||||
if action.IsStart() {
|
||||
s.Log().Info("syncing server configuration with panel")
|
||||
if err := s.Sync(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !s.Filesystem.HasSpaceAvailable() {
|
||||
return errors.New("cannot start server, not enough disk space available")
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
case PowerActionStart:
|
||||
return s.Environment.Start()
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/environment/docker"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,8 +21,9 @@ type Server struct {
|
||||
// Internal mutex used to block actions that need to occur sequentially, such as
|
||||
// writing the configuration to the disk.
|
||||
sync.RWMutex
|
||||
emitterLock sync.Mutex
|
||||
powerLock *semaphore.Weighted
|
||||
emitterLock sync.Mutex
|
||||
powerLock *semaphore.Weighted
|
||||
throttleLock sync.RWMutex
|
||||
|
||||
// Maintains the configuration for the server. This is the data that gets returned by the Panel
|
||||
// such as build settings and container images.
|
||||
@@ -29,16 +33,16 @@ type Server struct {
|
||||
crasher CrashHandler
|
||||
|
||||
resources ResourceUsage
|
||||
Archiver Archiver `json:"-"`
|
||||
Environment Environment `json:"-"`
|
||||
Filesystem Filesystem `json:"-"`
|
||||
Archiver Archiver `json:"-"`
|
||||
Environment environment.ProcessEnvironment `json:"-"`
|
||||
Filesystem Filesystem `json:"-"`
|
||||
|
||||
// Server cache used to store frequently requested information in memory and make
|
||||
// certain long operations return faster. For example, FS disk space usage.
|
||||
cache *cache.Cache
|
||||
|
||||
// Events emitted by the server instance.
|
||||
emitter *EventBus
|
||||
emitter *events.EventBus
|
||||
|
||||
// Defines the process configuration for the server instance. This is dynamically
|
||||
// fetched from the Pterodactyl Server instance each time the server process is
|
||||
@@ -50,6 +54,9 @@ type Server struct {
|
||||
// installation process, for example when a server is deleted from the panel while the
|
||||
// installer process is still running.
|
||||
installer InstallerDetails
|
||||
|
||||
// The console throttler instance used to control outputs.
|
||||
throttler *ConsoleThrottler
|
||||
}
|
||||
|
||||
type InstallerDetails struct {
|
||||
@@ -131,6 +138,13 @@ func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) err
|
||||
s.procConfig = cfg.ProcessConfiguration
|
||||
s.Unlock()
|
||||
|
||||
// If this is a Docker environment we need to sync the stop configuration with it so that
|
||||
// the process isn't just terminated when a user requests it be stopped.
|
||||
if e, ok := s.Environment.(*docker.Environment); ok {
|
||||
s.Log().Debug("syncing stop configuration with configured docker environment")
|
||||
e.SetStopConfiguration(&cfg.ProcessConfiguration.Stop)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -150,6 +164,11 @@ func (s *Server) IsBootable() bool {
|
||||
// Initalizes a server instance. This will run through and ensure that the environment
|
||||
// for the server is setup, and that all of the necessary files are created.
|
||||
func (s *Server) CreateEnvironment() error {
|
||||
// Ensure the data directory exists before getting too far through this process.
|
||||
if err := s.Filesystem.EnsureDataDirectory(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return s.Environment.Create()
|
||||
}
|
||||
|
||||
@@ -162,3 +181,7 @@ func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *a
|
||||
func (s *Server) IsSuspended() bool {
|
||||
return s.Config().Suspended
|
||||
}
|
||||
|
||||
func (s *Server) Build() *environment.Limits {
|
||||
return &s.Config().Build
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (s *Server) UpdateDataStructure(data []byte, background bool) error {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
c.Container.OomDisabled = v
|
||||
c.Build.OOMDisabled = v
|
||||
}
|
||||
|
||||
// Mergo also cannot handle this boolean value.
|
||||
|
||||
Reference in New Issue
Block a user