Compare commits

...

15 Commits

Author SHA1 Message Date
Dane Everitt
9861286f96 Update CHANGELOG.md 2021-09-12 11:24:31 -07:00
Dane Everitt
09e1ba6f34 Use the request context for cancelation, not a background context
This also fixes an improperly written server deletion listener to look at the correct context cancelation.

Theoretically this should help address the issues in pterodactyl/panel#3596 but I'm not really sure how that happens, and theres no steps for reproduction.
2021-09-12 11:14:00 -07:00
Dane Everitt
ee91224eb6 add context timeouts to avoid hanging wings boot process if docker has a hiccup; closes pterodactyl/panel#3358 2021-09-11 14:13:19 -07:00
Matthew Penner
5cd43dd4c9 archive: keep timestamps when extracting 2021-09-01 09:54:41 -06:00
Dane Everitt
3b5e042ccc Simplify logic when creating a new installer; no longer requires an entire server object be passed. 2021-08-29 14:08:01 -07:00
Dane Everitt
7321c6aa45 Remove unused and complicated installer logic 2021-08-29 13:52:19 -07:00
Dane Everitt
354e69b976 Merge branch 'develop' of github.com:pterodactyl/wings into develop 2021-08-29 13:49:44 -07:00
Dane Everitt
d2cfa6cd51 Update CHANGELOG.md 2021-08-29 13:49:34 -07:00
Dane Everitt
5764894a5e Cleanup server sync logic to work in a single consistent format (#101)
* Cleanup server sync logic to work in a single consistent format

Previously we had a mess of a function trying to update server details from a patch request. This change just centralizes everything to a single Sync() call when a server needs to update itself.

We can also eventually update the panel (in V2) to not hit the patch endpoint, rather it can just be a generic endpoint that is hit after a server is updated on the Panel that tells Wings to re-sync the data to get the environment changes on the fly.

The changes I made to the patch function currently act like that, with a slightly fragile 2 second wait to let the panel persist the changes since I don't want this to be a breaking change on that end.

* Remove legacy server patch endpoint; replace with simpler sync endpoint
2021-08-29 13:37:18 -07:00
Matthew Penner
d4a8f25cc6 parser: bug fixes (#102)
* parser: remove unnecessary type convertions
* parser: properly pass number and boolean values
* parser: set values if they don't exist
2021-08-24 17:05:02 -06:00
Matthew Penner
a0a54749d7 upgrade to go1.17 2021-08-24 13:28:17 -06:00
Dane Everitt
88caafa3f5 Update README.md 2021-08-15 18:32:54 -07:00
Johannes
4ee7f367e7 Expose 8080 so that reverse-proxies like jwilder/nginx-proxy can pick… (#97)
* Expose 8080 so that reverse-proxies like jwilder/nginx-proxy can pick up on it.

* Now actually patching the right image....

Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2021-08-15 18:31:11 -07:00
Dane Everitt
c279d28c5d Correctly set the egg values to avoid allowing blocked files to be edited; closes pterodactyl/panel#3536 2021-08-15 17:53:54 -07:00
Dane Everitt
f7c8571f46 Fix race condition when setting app name in console output 2021-08-15 16:46:55 -07:00
33 changed files with 311 additions and 400 deletions

View File

@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-20.04 ]
go: [ '^1.16' ]
go: [ '^1.17' ]
goos: [ linux ]
goarch: [ amd64, arm64 ]
runs-on: ${{ matrix.os }}

View File

@@ -11,7 +11,7 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.16'
go-version: '^1.17'
- name: Build
env:
REF: ${{ github.ref }}

View File

@@ -1,5 +1,26 @@
# Changelog
## v1.5.0
### Fixed
* Fixes a race condition when setting the application name in the console output for a server.
* Fixes a server being reinstalled causing the `file_denylist` parameter for an Egg to be ignored until Wings is restarted.
* Fixes YAML file parser not correctly setting boolean values.
* Fixes potential issue where the underlying websocket connection is closed but the parent request context is not yet canceled causing a write over a closed connection.
* Fixes race condition when closing all active websocket connections when a server is deleted.
* Fixes logic to determine if a server's context is closed out and send a websocket close message to connected clients. Previously this fired off whenever the request itself was closed, and not when the server context was closed.
### Added
* Exposes `8080` in the default Docker setup to better support proxy tools.
### Changed
* Releases are now built using `Go 1.17` — the minimum version required to build Wings remains `Go 1.16`.
* Simplifed the logic powering server updates to only pull information from the Panel rather than trying to accept updated values. All parts of Wings needing the most up-to-date server details should call `Server#Sync()` to fetch the latest stored build information.
* `Installer#New()` no longer requires passing all of the server data as a byte slice, rather a new `Installer#ServerDetails` struct is exposed which can be passed and accepts a UUID and if the server should be started after the installer finishes.
### Removed
* Removes complicated (and unused) logic during the server installation process that was a hold-over from legacy Wings architectures.
* Removes the `PATCH /api/servers/:server` endpoint — if you were previously using this API call it should be replaced with `POST /api/servers/:server/sync`.
## v1.4.7
### Fixed
* SFTP access is now properly denied if a server is suspended.

View File

@@ -1,5 +1,5 @@
# Stage 1 (Build)
FROM --platform=$BUILDPLATFORM golang:1.16-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS builder
ARG VERSION
RUN apk add --update --no-cache git make upx
@@ -19,5 +19,8 @@ RUN echo "ID=\"distroless\"" > /etc/os-release
# Stage 2 (Final)
FROM gcr.io/distroless/static:latest
COPY --from=builder /etc/os-release /etc/os-release
COPY --from=builder /app/wings /usr/bin/
CMD [ "/usr/bin/wings", "--config", "/etc/pterodactyl/config.yml" ]
EXPOSE 8080

View File

@@ -32,6 +32,9 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. |
| [**Capitol Hosting Solutions**](https://capitolsolutions.cloud/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! |
| [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
| [**RocketNode**](https://rocketnode.net) | RocketNode is a VPS and Game Server provider that offers the best performing VPS and Game hosting Solutions at affordable prices! |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View File

@@ -1,6 +1,7 @@
package cmd
import (
"context"
"crypto/tls"
"errors"
"fmt"
@@ -206,7 +207,17 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
st = state
}
r, err := s.Environment.IsRunning()
// Use a timed context here to avoid booting issues where Docker hangs for a
// specific container that would cause Wings to be un-bootable until the entire
// machine is rebooted. It is much better for us to just have a single failed
// server instance than an entire offline node.
//
// @see https://github.com/pterodactyl/panel/issues/2475
// @see https://github.com/pterodactyl/panel/issues/3358
ctx, cancel := context.WithTimeout(cmd.Context(), time.Second * 30)
defer cancel()
r, err := s.Environment.IsRunning(ctx)
// We ignore missing containers because we don't want to actually block booting of wings at this
// point. If we didn't do this, and you pruned all the images and then started wings you could
// end up waiting a long period of time for all the images to be re-pulled on Wings boot rather
@@ -235,7 +246,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
s.Log().Info("detected server is running, re-attaching to process...")
s.Environment.SetState(environment.ProcessRunningState)
if err := s.Environment.Attach(); err != nil {
if err := s.Environment.Attach(ctx); err != nil {
s.Log().WithField("error", err).Warn("failed to attach to running server environment")
}
} else {

View File

@@ -45,7 +45,7 @@ func (nw noopWriter) Write(b []byte) (int, error) {
// Calling this function will poll resources for the container in the background
// until the provided context is canceled by the caller. Failure to cancel said
// context will cause background memory leaks as the goroutine will not exit.
func (e *Environment) Attach() error {
func (e *Environment) Attach(ctx context.Context) error {
if e.IsAttached() {
return nil
}
@@ -62,14 +62,17 @@ func (e *Environment) Attach() error {
}
// Set the stream again with the container.
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
return err
} else {
e.SetStream(&st)
}
go func() {
ctx, cancel := context.WithCancel(context.Background())
// Don't use the context provided to the function, that'll cause the polling to
// exit unexpectedly. We want a custom context for this, the one passed to the
// function is to avoid a hang situation when trying to attach to a container.
pollCtx, cancel := context.WithCancel(context.Background())
defer cancel()
defer e.stream.Close()
defer func() {
@@ -78,7 +81,7 @@ func (e *Environment) Attach() error {
}()
go func() {
if err := e.pollResources(ctx); err != nil {
if err := e.pollResources(pollCtx); err != nil {
if !errors.Is(err, context.Canceled) {
e.log().WithField("error", err).Error("error during environment resource polling")
} else {

View File

@@ -128,20 +128,20 @@ func (e *Environment) Exists() (bool, error) {
return true, nil
}
// Determines if the server's docker container is currently running. If there is no container
// present, an error will be raised (since this shouldn't be a case that ever happens under
// correctly developed circumstances).
// IsRunning determines if the server's docker container is currently running.
// If there is no container present, an error will be raised (since this
// shouldn't be a case that ever happens under correctly developed
// circumstances).
//
// You can confirm if the instance wasn't found by using client.IsErrNotFound from the Docker
// API.
// You can confirm if the instance wasn't found by using client.IsErrNotFound
// from the Docker API.
//
// @see docker/client/errors.go
func (e *Environment) IsRunning() (bool, error) {
c, err := e.client.ContainerInspect(context.Background(), e.Id)
func (e *Environment) IsRunning(ctx context.Context) (bool, error) {
c, err := e.client.ContainerInspect(ctx, e.Id)
if err != nil {
return false, err
}
return c.State.Running, nil
}

View File

@@ -17,16 +17,17 @@ import (
"github.com/pterodactyl/wings/remote"
)
// 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.
// OnBeforeStart 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 un-bootable.
func (e *Environment) OnBeforeStart() error {
// 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 un-bootable.
func (e *Environment) OnBeforeStart(ctx context.Context) error {
// Always destroy and re-create the server container to ensure that synced data from the Panel is used.
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if err := e.client.ContainerRemove(ctx, e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if !client.IsErrNotFound(err) {
return errors.WrapIf(err, "environment/docker: failed to remove container during pre-boot")
}
@@ -46,10 +47,10 @@ func (e *Environment) OnBeforeStart() error {
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 {
// Start will start 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(ctx context.Context) error {
sawError := false
// If sawError is set to true there was an error somewhere in the pipeline that
@@ -65,7 +66,7 @@ func (e *Environment) Start() error {
}
}()
if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
if c, err := e.client.ContainerInspect(ctx, e.Id); err != nil {
// Do nothing if the container is not found, we just don't want to continue
// to the next block of code here. This check was inlined here to guard against
// a nil-pointer when checking c.State below.
@@ -79,7 +80,7 @@ func (e *Environment) Start() error {
if c.State.Running {
e.SetState(environment.ProcessRunningState)
return e.Attach()
return e.Attach(ctx)
}
// Truncate the log file, so we don't end up outputting a bunch of useless log information
@@ -101,21 +102,23 @@ func (e *Environment) Start() error {
// 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 {
if err := e.OnBeforeStart(ctx); err != nil {
return errors.WithStackIf(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
// If we cannot start & attach to the container in 30 seconds something has gone
// quite sideways and we should stop trying to avoid a hanging situation.
actx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {
return errors.WrapIf(err, "environment/docker: failed to start container")
}
// No errors, good to continue through.
sawError = false
return e.Attach()
return e.Attach(actx)
}
// Stop stops the container that the server is running in. This will allow up to

View File

@@ -1,6 +1,7 @@
package environment
import (
"context"
"os"
"github.com/pterodactyl/wings/events"
@@ -41,9 +42,9 @@ type ProcessEnvironment interface {
// a basic CLI environment this can probably just return true right away.
Exists() (bool, error)
// Determines if the environment is currently active and running a server process
// for this specific server instance.
IsRunning() (bool, error)
// IsRunning determines if the environment is currently active and running
// a server process for this specific server instance.
IsRunning(ctx context.Context) (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
@@ -52,11 +53,11 @@ type ProcessEnvironment interface {
// Runs before the environment is started. If an error is returned starting will
// not occur, otherwise proceeds as normal.
OnBeforeStart() error
OnBeforeStart(ctx context.Context) 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
Start(ctx context.Context) error
// Stops a server instance. If the server is already stopped an error should
// not be returned.
@@ -84,10 +85,10 @@ type ProcessEnvironment interface {
// 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
// Attach 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
Attach(ctx context.Context) error
// Sends the provided command to the running server instance.
SendCommand(string) error

View File

@@ -2,72 +2,32 @@ package installer
import (
"context"
"encoding/json"
"emperror.dev/errors"
"github.com/asaskevich/govalidator"
"github.com/buger/jsonparser"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server"
)
type Installer struct {
server *server.Server
StartOnCompletion bool
}
type ServerDetails struct {
UUID string `json:"uuid"`
StartOnCompletion bool `json:"start_on_completion"`
}
// New validates the received data to ensure that all the required fields
// have been passed along in the request. This should be manually run before
// calling Execute().
func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, error) {
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
func New(ctx context.Context, manager *server.Manager, details ServerDetails) (*Installer, error) {
if !govalidator.IsUUIDv4(details.UUID) {
return nil, NewValidationError("uuid provided was not in a valid format")
}
cfg := &server.Configuration{
Uuid: getString(data, "uuid"),
Suspended: false,
Invocation: getString(data, "invocation"),
SkipEggScripts: getBoolean(data, "skip_egg_scripts"),
StartOnCompletion: getBoolean(data, "start_on_completion"),
Build: environment.Limits{
MemoryLimit: getInt(data, "build", "memory"),
Swap: getInt(data, "build", "swap"),
IoWeight: uint16(getInt(data, "build", "io")),
CpuLimit: getInt(data, "build", "cpu"),
DiskSpace: getInt(data, "build", "disk"),
Threads: getString(data, "build", "threads"),
},
CrashDetectionEnabled: true,
}
cfg.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip")
cfg.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port"))
// Unmarshal the environment variables from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
return nil, errors.WithStackIf(err)
} else {
cfg.EnvVars = make(environment.Variables)
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
return nil, errors.WrapIf(err, "installer: could not unmarshal environment variables for server")
}
}
// Unmarshal the allocation mappings from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
return nil, errors.WithStackIf(err)
} else {
cfg.Allocations.Mappings = make(map[string][]int)
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
return nil, errors.Wrap(err, "installer: could not unmarshal allocation mappings")
}
}
cfg.Container.Image = getString(data, "container", "image")
c, err := manager.Client().GetServerConfiguration(ctx, cfg.Uuid)
c, err := manager.Client().GetServerConfiguration(ctx, details.UUID)
if err != nil {
if !remote.IsRequestError(err) {
return nil, errors.WithStackIf(err)
@@ -81,35 +41,11 @@ func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer,
if err != nil {
return nil, errors.WrapIf(err, "installer: could not init server instance")
}
return &Installer{server: s}, nil
}
// Uuid returns the UUID associated with this installer instance.
func (i *Installer) Uuid() string {
return i.server.ID()
i := Installer{server: s, StartOnCompletion: details.StartOnCompletion}
return &i, nil
}
// Server returns the server instance.
func (i *Installer) Server() *server.Server {
return i.server
}
// Returns a string value from the JSON data provided.
func getString(data []byte, key ...string) string {
value, _ := jsonparser.GetString(data, key...)
return value
}
// Returns an int value from the JSON data provided.
func getInt(data []byte, key ...string) int64 {
value, _ := jsonparser.GetInt(data, key...)
return value
}
func getBoolean(data []byte, key ...string) bool {
value, _ := jsonparser.GetBoolean(data, key...)
return value
}

View File

@@ -48,19 +48,19 @@ func readFileBytes(path string) ([]byte, error) {
}
// Gets the value of a key based on the value type defined.
func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} {
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
v, _ := strconv.ParseBool(string(value))
v, _ := strconv.ParseBool(value)
return v
}
// Try to parse into an int, if this fails just ignore the error and continue
// through, returning the string.
if v, err := strconv.Atoi(string(value)); err == nil {
if v, err := strconv.Atoi(value); err == nil {
return v
}
return string(value)
return value
}
// Iterate over an unstructured JSON/YAML/etc. interface and set all of the required
@@ -97,22 +97,21 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
// If the child is a null value, nothing will happen. Seems reasonable as of the
// time this code is being written.
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil {
if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.WithMessage(err, "failed to set config value of array child")
}
}
} else {
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
if err := v.SetAtPathway(parsed, v.Match, value); err != nil {
if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
}
}
@@ -132,13 +131,10 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
var err error
matches := checkForArrayElement.FindStringSubmatch(path)
if len(matches) < 3 {
// Only update the value if the pathway actually exists in the configuration, otherwise
// do nothing.
if c.ExistsP(path) {
_, err = c.SetP(value, path)
}
// Check if we are **NOT** updating an array element.
if len(matches) < 3 {
_, err = c.SetP(value, path)
return err
}
@@ -196,32 +192,34 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
// Sets the value at a specific pathway, but checks if we were looking for a specific
// value or not before doing it.
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value string) error {
if cfr.IfValue == "" {
return setValueAtPath(c, path, cfr.getKeyValue(value))
}
// If this is a regex based matching, we need to get a little more creative since
// we're only going to replacing part of the string, and not the whole thing.
if c.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
// We're doing some regex here.
// Check if we are replacing instead of overwriting.
if strings.HasPrefix(cfr.IfValue, "regex:") {
// Doing a regex replacement requires an existing value.
// TODO: Do we try passing an empty string to the regex?
if c.ExistsP(path) {
return gabs.ErrNotFound
}
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
if err != nil {
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
Warn("configuration if_value using invalid regexp, cannot perform replacement")
return nil
}
// If the path exists and there is a regex match, go ahead and attempt the replacement
// using the value we got from the key. This will only replace the one match.
v := strings.Trim(string(c.Path(path).Bytes()), "\"")
v := strings.Trim(c.Path(path).String(), "\"")
if r.Match([]byte(v)) {
return setValueAtPath(c, path, r.ReplaceAllString(v, string(value)))
return setValueAtPath(c, path, r.ReplaceAllString(v, value))
}
return nil
}
return nil
} else if !c.ExistsP(path) || (c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
if c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue)) {
return nil
}

View File

@@ -57,17 +57,22 @@ func (cv *ReplaceValue) Type() jsonparser.ValueType {
// handle casting the UTF-8 sequence into the expected value, switching something
// like "\u00a7Foo" into "§Foo".
func (cv *ReplaceValue) String() string {
if cv.Type() != jsonparser.String {
if cv.Type() == jsonparser.Null {
return "<nil>"
}
return "<invalid>"
}
switch cv.Type() {
case jsonparser.String:
str, err := jsonparser.ParseString(cv.value)
if err != nil {
panic(errors.Wrap(err, "parser: could not parse value"))
}
return str
case jsonparser.Null:
return "<nil>"
case jsonparser.Boolean:
return string(cv.value)
case jsonparser.Number:
return string(cv.value)
default:
return "<invalid>"
}
}
type ConfigurationParser string

View File

@@ -6,11 +6,11 @@ import (
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/router/middleware"
"github.com/pterodactyl/wings/server"
wserver "github.com/pterodactyl/wings/server"
)
// Configure configures the routing infrastructure for this daemon instance.
func Configure(m *server.Manager, client remote.Client) *gin.Engine {
func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
gin.SetMode("release")
router := gin.New()
@@ -63,7 +63,6 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
server.Use(middleware.RequireAuthorization(), middleware.ServerExists())
{
server.GET("", getServer)
server.PATCH("", patchServer)
server.DELETE("", deleteServer)
server.GET("/logs", getServerLogs)
@@ -71,6 +70,7 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
server.POST("/commands", postServerCommands)
server.POST("/install", postServerInstall)
server.POST("/reinstall", postServerReinstall)
server.POST("/sync", postServerSync)
server.POST("/ws/deny", postServerDenyWSTokens)
// This archive request causes the archive to start being created

View File

@@ -1,7 +1,6 @@
package router
import (
"bytes"
"context"
"net/http"
"os"
@@ -10,7 +9,6 @@ import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/downloader"
"github.com/pterodactyl/wings/router/middleware"
"github.com/pterodactyl/wings/router/tokens"
@@ -103,7 +101,7 @@ func postServerPower(c *gin.Context) {
func postServerCommands(c *gin.Context) {
s := ExtractServer(c)
if running, err := s.Environment.IsRunning(); err != nil {
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
NewServerError(err, s).Abort(c)
return
} else if !running {
@@ -130,21 +128,18 @@ func postServerCommands(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// Updates information about a server internally.
func patchServer(c *gin.Context) {
// postServerSync will accept a POST request and trigger a re-sync of the given
// server against the Panel. This can be manually triggered when needed by an
// external system, or triggered by the Panel itself when modifications are made
// to the build of a server internally.
func postServerSync(c *gin.Context) {
s := ExtractServer(c)
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
NewServerError(err, s).Abort(c)
return
}
s.SyncWithEnvironment()
if err := s.Sync(); err != nil {
WithError(c, err)
} else {
c.Status(http.StatusNoContent)
}
}
// Performs a server installation in a background thread.

View File

@@ -12,6 +12,14 @@ import (
"github.com/pterodactyl/wings/router/websocket"
)
var expectedCloseCodes = []int{
ws.CloseGoingAway,
ws.CloseAbnormalClosure,
ws.CloseNormalClosure,
ws.CloseNoStatusReceived,
ws.CloseServiceRestart,
}
// Upgrades a connection to a websocket and passes events along between.
func getServerWebsocket(c *gin.Context) {
manager := middleware.ExtractManager(c)
@@ -24,8 +32,10 @@ func getServerWebsocket(c *gin.Context) {
defer handler.Connection.Close()
// Create a context that can be canceled when the user disconnects from this
// socket that will also cancel listeners running in separate threads.
ctx, cancel := context.WithCancel(context.Background())
// socket that will also cancel listeners running in separate threads. If the
// connection itself is terminated listeners using this context will also be
// closed.
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
// Track this open connection on the server so that we can close them all programmatically
@@ -33,22 +43,19 @@ func getServerWebsocket(c *gin.Context) {
s.Websockets().Push(handler.Uuid(), &cancel)
defer s.Websockets().Remove(handler.Uuid())
// Listen for the context being canceled and then close the websocket connection. This normally
// just happens because you're disconnecting from the socket in the browser, however in some
// cases we close the connections programmatically (e.g. deleting the server) and need to send
// a close message to the websocket so it disconnects.
go func(ctx context.Context, c *ws.Conn) {
ListenerLoop:
for {
// If the server is deleted we need to send a close message to the connected client
// so that they disconnect since there will be no more events sent along. Listen for
// the request context being closed to break this loop, otherwise this routine will
// be left hanging in the background.
go func() {
select {
case <-ctx.Done():
break
case <-s.Context().Done():
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
// A break right here without defining the specific loop would only break the select
// and not actually break the for loop, thus causing this routine to stick around forever.
break ListenerLoop
break
}
}
}(ctx, handler.Connection)
}()
go handler.ListenForServerEvents(ctx)
go handler.ListenForExpiration(ctx)
@@ -58,14 +65,7 @@ func getServerWebsocket(c *gin.Context) {
_, p, err := handler.Connection.ReadMessage()
if err != nil {
if !ws.IsCloseError(
err,
ws.CloseNormalClosure,
ws.CloseGoingAway,
ws.CloseNoStatusReceived,
ws.CloseServiceRestart,
ws.CloseAbnormalClosure,
) {
if ws.IsUnexpectedCloseError(err, expectedCloseCodes...) {
s.Log().WithField("error", err).Warn("error handling websocket message for server")
}
break

View File

@@ -1,7 +1,6 @@
package router
import (
"bytes"
"context"
"errors"
"net/http"
@@ -44,10 +43,13 @@ func getAllServers(c *gin.Context) {
// for it.
func postCreateServer(c *gin.Context) {
manager := middleware.ExtractManager(c)
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
install, err := installer.New(c.Request.Context(), manager, buf.Bytes())
details := installer.ServerDetails{}
if err := c.BindJSON(&details); err != nil {
return
}
install, err := installer.New(c.Request.Context(), manager, details)
if err != nil {
if installer.IsValidationError(err) {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
@@ -74,24 +76,21 @@ func postCreateServer(c *gin.Context) {
}
if err := i.Server().Install(false); err != nil {
log.WithFields(log.Fields{"server": i.Uuid(), "error": err}).Error("failed to run install process for server")
log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server")
return
}
if i.Server().Config().StartOnCompletion {
if i.StartOnCompletion {
log.WithField("server_id", i.Server().ID()).Debug("starting server after successful installation")
if err := i.Server().HandlePowerAction(server.PowerActionStart, 30); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).
Warn("could not acquire a lock while attempting to perform a power action")
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).Warn("could not acquire a lock while attempting to perform a power action")
} else {
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).
Error("encountered error processing a server power action in the background")
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).Error("encountered error processing a server power action in the background")
}
}
} else {
log.WithField("server_id", i.Server().ID()).
Debug("skipping automatic start after successful server installation")
log.WithField("server_id", i.Server().ID()).Debug("skipping automatic start after successful server installation")
}
}(install)

View File

@@ -5,7 +5,6 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -50,7 +49,7 @@ type serverTransferRequest struct {
ServerID string `binding:"required" json:"server_id"`
URL string `binding:"required" json:"url"`
Token string `binding:"required" json:"token"`
Server json.RawMessage `json:"server"`
Server installer.ServerDetails `json:"server"`
}
func getArchivePath(sID string) string {

View File

@@ -8,16 +8,14 @@ import (
"github.com/pterodactyl/wings/server"
)
// Checks 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 a notice over
// the socket that it is expiring soon. If it has expired, send that notice as well.
// ListenForExpiration checks 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 a notice over the socket that it is expiring soon. If it has expired,
// send that notice as well.
func (h *Handler) ListenForExpiration(ctx context.Context) {
// Make a ticker and completion channel that is used to continuously poll the
// JWT stored in the session to send events to the socket when it is expiring.
ticker := time.NewTicker(time.Second * 30)
// Whenever this function is complete, end the ticker, close out the channel,
// and then close the websocket connection.
defer ticker.Stop()
for {
@@ -51,8 +49,9 @@ var e = []string{
server.TransferStatusEvent,
}
// Listens for different events happening on a server and sends them along
// to the connected websocket.
// ListenForServerEvents will listen for different events happening on a server
// and send them along to the connected websocket client. This function will
// block until the context provided to it is canceled.
func (h *Handler) ListenForServerEvents(ctx context.Context) {
h.server.Log().Debug("listening for server events over websocket")
callback := func(e events.Event) {
@@ -67,13 +66,10 @@ func (h *Handler) ListenForServerEvents(ctx context.Context) {
h.server.Events().On(evt, &callback)
}
go func(ctx context.Context) {
select {
case <-ctx.Done():
// Once this context is stopped, de-register all of the listeners that have been registered.
<-ctx.Done()
// Block until the context is stopped and then de-register all of the event listeners
// that we registered earlier.
for _, evt := range e {
h.server.Events().Off(evt, &callback)
}
}
}(ctx)
}

View File

@@ -368,7 +368,9 @@ func (h *Handler) HandleInbound(m Message) error {
}
case SendServerLogsEvent:
{
if running, _ := h.server.Environment.IsRunning(); !running {
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
defer cancel()
if running, _ := h.server.Environment.IsRunning(ctx); !running {
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"io/fs"
"io/ioutil"
"os"
"time"
"emperror.dev/errors"
"github.com/apex/log"
@@ -152,12 +153,15 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) (
// Attempt to restore the backup to the server by running through each entry
// in the file one at a time and writing them to the disk.
s.Log().Debug("starting file writing process for backup restoration")
err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode) error {
err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error {
s.Events().Publish(DaemonMessageEvent, "(restoring): "+file)
if err := s.Filesystem().Writefile(file, r); err != nil {
return err
}
return s.Filesystem().Chmod(file, mode)
if err := s.Filesystem().Chmod(file, mode); err != nil {
return err
}
return s.Filesystem().Chtimes(file, atime, mtime)
})
return errors.WithStackIf(err)

View File

@@ -8,6 +8,7 @@ import (
"io/fs"
"os"
"path"
"time"
"emperror.dev/errors"
"github.com/apex/log"
@@ -26,7 +27,7 @@ const (
// RestoreCallback is a generic restoration callback that exists for both local
// and remote backups allowing the files to be restored.
type RestoreCallback func(file string, r io.Reader, mode fs.FileMode) error
type RestoreCallback func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error
// noinspection GoNameStartsWithPackageName
type BackupInterface interface {

View File

@@ -88,7 +88,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore
if f.IsDir() {
return nil
}
return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode())
return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode(), f.ModTime(), f.ModTime())
}
})
}

View File

@@ -116,7 +116,7 @@ func (s *S3Backup) Restore(ctx context.Context, r io.Reader, callback RestoreCal
return err
}
if header.Typeflag == tar.TypeReg {
if err := callback(header.Name, tr, header.FileInfo().Mode()); err != nil {
if err := callback(header.Name, tr, header.FileInfo().Mode(), header.AccessTime, header.ModTime); err != nil {
return err
}
}

View File

@@ -35,8 +35,6 @@ type Configuration struct {
// server, specific installation scripts will be skipped for the server process.
SkipEggScripts bool `json:"skip_egg_scripts"`
StartOnCompletion bool `json:"start_on_completion"`
// An array of environment variables that should be passed along to the running
// server process.
EnvVars environment.Variables `json:"environment"`

View File

@@ -19,6 +19,8 @@ import (
// a server.
var appName string
var appNameSync sync.Once
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
type ConsoleThrottler struct {
@@ -131,9 +133,9 @@ func (s *Server) Throttler() *ConsoleThrottler {
// PublishConsoleOutputFromDaemon sends output to the server console formatted
// to appear correctly as being sent from Wings.
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
if appName == "" {
appNameSync.Do(func() {
appName = config.Get().AppName
}
})
s.Events().Publish(
ConsoleOutputEvent,
colorstring.Color(fmt.Sprintf("[yellow][bold][%s Daemon]:[default] %s", appName, data)),

View File

@@ -136,6 +136,10 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
if err := fs.Chmod(p, f.Mode()); err != nil {
return wrapError(err, source)
}
// Update the file modification time to the one set in the archive.
if err := fs.Chtimes(p, f.ModTime(), f.ModTime()); err != nil {
return wrapError(err, source)
}
return nil
})
if err != nil {

View File

@@ -528,3 +528,20 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
return out, nil
}
func (fs *Filesystem) Chtimes(path string, atime, mtime time.Time) error {
cleaned, err := fs.SafePath(path)
if err != nil {
return err
}
if fs.isTest {
return nil
}
if err := os.Chtimes(cleaned, atime, mtime); err != nil {
return err
}
return nil
}

View File

@@ -172,8 +172,11 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
if err != nil {
return nil, err
}
if err := s.UpdateDataStructure(data.Settings); err != nil {
return nil, err
// Setup the base server configuration data which will be used for all of the
// remaining functionality in this call.
if err := s.SyncWithConfiguration(data); err != nil {
return nil, errors.WithStackIf(err)
}
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.ID()), s.DiskSpace(), s.Config().Egg.FileDenylist)
@@ -200,11 +203,6 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
s.Throttler().StartTimer(s.Context())
}
// Forces the configuration to be synced with the panel.
if err := s.SyncWithConfiguration(data); err != nil {
return nil, err
}
// If the server's data directory exists, force disk usage calculation.
if _, err := os.Stat(s.Filesystem().Path()); err == nil {
s.Filesystem().HasSpaceAvailable(true)

View File

@@ -128,7 +128,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
return err
}
return s.Environment.Start()
return s.Environment.Start(s.Context())
case PowerActionStop:
// We're specifically waiting for the process to be stopped here, otherwise the lock is released
// too soon, and you can rack up all sorts of issues.
@@ -151,7 +151,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
return err
}
return s.Environment.Start()
return s.Environment.Start(s.Context())
case PowerActionTerminate:
return s.Environment.Terminate(os.Kill)
}

View File

@@ -2,6 +2,7 @@ package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
@@ -15,7 +16,6 @@ import (
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/events"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server/filesystem"
@@ -167,31 +167,48 @@ func (s *Server) Sync() error {
}
return errors.WithStackIf(err)
}
return s.SyncWithConfiguration(cfg)
if err := s.SyncWithConfiguration(cfg); err != nil {
return errors.WithStackIf(err)
}
// Update the disk space limits for the server whenever the configuration for
// it changes.
s.fs.SetDiskLimit(s.DiskSpace())
s.SyncWithEnvironment()
return nil
}
// SyncWithConfiguration accepts a configuration object for a server and will
// sync all of the values with the existing server state. This only replaces the
// existing configuration and process configuration for the server. The
// underlying environment will not be affected. This is because this function
// can be called from scoped where the server may not be fully initialized,
// therefore other things like the filesystem and environment may not exist yet.
func (s *Server) SyncWithConfiguration(cfg remote.ServerConfigurationResponse) error {
// Update the data structure and persist it to the disk.
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
return err
c := Configuration{}
if err := json.Unmarshal(cfg.Settings, &c); err != nil {
return errors.WithStackIf(err)
}
s.cfg.mu.Lock()
defer s.cfg.mu.Unlock()
// Lock the new configuration. Since we have the defered Unlock above we need
// to make sure that the NEW configuration object is already locked since that
// defer is running on the memory address for "s.cfg.mu" which we're explcitly
// changing on the next line.
c.mu.Lock()
//goland:noinspection GoVetCopyLock
s.cfg = c
s.Lock()
s.procConfig = cfg.ProcessConfiguration
s.Unlock()
// Update the disk space limits for the server whenever the configuration
// for it changes.
s.fs.SetDiskLimit(s.DiskSpace())
// 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.SetImage(s.Config().Container.Image)
e.SetStopConfiguration(cfg.ProcessConfiguration.Stop)
}
return nil
}

View File

@@ -1,146 +1,41 @@
package server
import (
"encoding/json"
"emperror.dev/errors"
"github.com/buger/jsonparser"
"github.com/imdario/mergo"
"github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/environment"
)
// UpdateDataStructure merges data passed through in JSON form into the existing
// server object. Any changes to the build settings will apply immediately in
// the environment if the environment supports it.
// SyncWithEnvironment updates the environment for the server to match any of
// the changed data. This pushes new settings and environment variables to the
// environment. In addition, the in-situ update method is called on the
// environment which will allow environments that make use of it (such as Docker)
// to immediately apply some settings without having to wait on a server to
// restart.
//
// The server will be marked as requiring a rebuild on the next boot sequence,
// it is up to the specific environment to determine what needs to happen when
// that is the case.
func (s *Server) UpdateDataStructure(data []byte) error {
src := new(Configuration)
if err := json.Unmarshal(data, src); err != nil {
return errors.Wrap(err, "server/update: could not unmarshal source data into Configuration struct")
}
// Don't allow obviously corrupted data to pass through into this function. If the UUID
// doesn't match something has gone wrong and the API is attempting to meld this server
// instance into a totally different one, which would be bad.
if src.Uuid != "" && s.ID() != "" && src.Uuid != s.ID() {
return errors.New("server/update: attempting to merge a data stack with an invalid UUID")
}
// Grab a copy of the configuration to work on.
c := *s.Config()
// Lock our copy of the configuration since the deferred unlock will end up acting upon this
// new memory address rather than the old one. If we don't lock this, the deferred unlock will
// cause a panic when it goes to run. However, since we only update s.cfg at the end, if there
// is an error before that point we'll still properly unlock the original configuration for the
// server.
c.mu.Lock()
// Lock the server configuration while we're doing this merge to avoid anything
// trying to overwrite it or make modifications while we're sorting out what we
// need to do.
s.cfg.mu.Lock()
defer s.cfg.mu.Unlock()
// 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(&c, src, mergo.WithOverride); err != nil {
return errors.WithStack(err)
}
// Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value
// so it won't override the value we've passed through in the API call. However, we can
// safely assume that we're passing through valid data structures here. I foresee this
// backfiring at some point, but until then...
c.Build = src.Build
// Mergo can't quite handle this boolean value correctly, so for now we'll just
// handle this edge case manually since none of the other data passed through in this
// request is going to be boolean. Allegedly.
if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil {
if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err)
}
} else {
c.Build.OOMDisabled = v
}
// Mergo also cannot handle this boolean value.
if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil {
if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err)
}
} else {
c.Suspended = v
}
if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil {
if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err)
}
} else {
c.SkipEggScripts = v
}
if v, err := jsonparser.GetBoolean(data, "start_on_completion"); err != nil {
if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err)
}
} else {
c.StartOnCompletion = v
}
if v, err := jsonparser.GetBoolean(data, "crash_detection_enabled"); err != nil {
if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err)
}
// Enable crash detection by default.
c.CrashDetectionEnabled = true
} else {
c.CrashDetectionEnabled = v
}
// Environment and Mappings should be treated as a full update at all times, never a
// true patch, otherwise we can't know what we're passing along.
if src.EnvVars != nil && len(src.EnvVars) > 0 {
c.EnvVars = src.EnvVars
}
if src.Allocations.Mappings != nil && len(src.Allocations.Mappings) > 0 {
c.Allocations.Mappings = src.Allocations.Mappings
}
if src.Mounts != nil && len(src.Mounts) > 0 {
c.Mounts = src.Mounts
}
// Update the configuration once we have a lock on the configuration object.
s.cfg = c
return nil
}
// Updates the environment for the server to match any of the changed data. This pushes new settings and
// environment variables to the environment. In addition, the in-situ update method is called on the
// environment which will allow environments that make use of it (such as Docker) to immediately apply
// some settings without having to wait on a server to restart.
//
// This functionality allows a server's resources limits to be modified on the fly and have them apply
// right away allowing for dynamic resource allocation and responses to abusive server processes.
// This functionality allows a server's resources limits to be modified on the
// fly and have them apply right away allowing for dynamic resource allocation
// and responses to abusive server processes.
func (s *Server) SyncWithEnvironment() {
s.Log().Debug("syncing server settings with environment")
cfg := s.Config()
// Update the environment settings using the new information from this server.
s.Environment.Config().SetSettings(environment.Settings{
Mounts: s.Mounts(),
Allocations: s.Config().Allocations,
Limits: s.Config().Build,
Allocations: cfg.Allocations,
Limits: cfg.Build,
})
// For Docker specific environments we also want to update the configured image
// and stop configuration.
if e, ok := s.Environment.(*docker.Environment); ok {
s.Log().Debug("syncing stop configuration with configured docker environment")
e.SetImage(cfg.Container.Image)
e.SetStopConfiguration(s.ProcessConfiguration().Stop)
}
// If build limits are changed, environment variables also change. Plus, any modifications to
// the startup command also need to be properly propagated to this environment.
//

View File

@@ -44,11 +44,11 @@ func (w *WebsocketBag) Remove(u uuid.UUID) {
w.mu.Unlock()
}
// CancelAll cancels all the stored cancel functions which has the effect of disconnecting
// every listening websocket for the server.
// CancelAll cancels all the stored cancel functions which has the effect of
// disconnecting every listening websocket for the server.
func (w *WebsocketBag) CancelAll() {
w.mu.Lock()
w.mu.Unlock()
defer w.mu.Unlock()
if w.conns != nil {
for _, cancel := range w.conns {