Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35ba6d7524 | ||
|
|
fb0e769306 | ||
|
|
0676a82a21 | ||
|
|
a0ae5fd131 | ||
|
|
4b244e96fb | ||
|
|
488884fdee | ||
|
|
cfa338108f | ||
|
|
16b0ca3a8e | ||
|
|
f57c24002e | ||
|
|
8dfd494eaf | ||
|
|
2e0496c1f9 | ||
|
|
f85ee1aa73 | ||
|
|
d4b63bef39 | ||
|
|
4c3b497652 | ||
|
|
ff62d16085 | ||
|
|
202ca922ad | ||
|
|
76b7967fef | ||
|
|
1b1eaa3171 | ||
|
|
87f0b11078 | ||
|
|
b448310a33 | ||
|
|
f1b85ef0ab | ||
|
|
bec6a6112d | ||
|
|
b691b8f06f | ||
|
|
31127620e5 | ||
|
|
5e7316e09a | ||
|
|
52fcf1e37f | ||
|
|
0c17e240f4 | ||
|
|
471886dd34 | ||
|
|
b63a491b5e |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,6 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## v1.3.0
|
||||
## v1.4.0
|
||||
### Fixed
|
||||
* **[Breaking]** Fixes `/api/servers` and `/api/servers/:server` not properly returning all of the relevant server information and resource usage.
|
||||
* Fixes Wings improperly reading `WINGS_UID` and not `WINGS_GID` when running in containerized environments.
|
||||
* Fixes a panic encountered when returning the contents of a file that is actively being written to by another process.
|
||||
* Corrected the handling of files that are being decompressed to properly support `.rar` files.
|
||||
* Fixes the error message returned when a server has run out of disk space to properly indicate such, rather than indicating that the file is a directory.
|
||||
|
||||
### Changed
|
||||
* Improved the error handling and output when an error is encountered while pulling an image for a server.
|
||||
* Improved robustness of code handling value replacement in configuration files to not potentially panic if a non-string value is encountered as the replacement type.
|
||||
* Improves error handling throughout the server filesystem.
|
||||
|
||||
### Added
|
||||
* Adds the ability to set the internal name of the application in response output from the console using the `app_name` key in the `config.yml` file.
|
||||
|
||||
## v1.3.2
|
||||
### Fixed
|
||||
* Correctly sets the internal state of the server as restoring when a restore is being performed to avoid any accidental booting.
|
||||
|
||||
## v1.3.1
|
||||
### Fixed
|
||||
* Fixes an error being returned to the client when attempting to restart a server when the container no longer exists on the machine.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FROM golang:1.15-alpine3.12 AS builder
|
||||
|
||||
ARG VERSION
|
||||
RUN apk add --update --no-cache git=2.26.2-r0 make=4.3-r0 upx=3.96-r0
|
||||
RUN apk add --update --no-cache git make upx
|
||||
WORKDIR /app/
|
||||
COPY go.mod go.sum /app/
|
||||
RUN go mod download
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Dane Everitt <dane@daneeveritt.com>
|
||||
Copyright (c) 2018 - 2021 Dane Everitt <dane@daneeveritt.com> and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -19,14 +19,19 @@ I would like to extend my sincere thanks to the following sponsors for helping f
|
||||
| Company | About |
|
||||
| ------- | ----- |
|
||||
| [**WISP**](https://wisp.gg) | Extra features. |
|
||||
| [**MixmlHosting**](https://mixmlhosting.com) | MixmlHosting provides high quality Virtual Private Servers along with game servers, all at a affordable price. |
|
||||
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
||||
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
|
||||
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
|
||||
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
|
||||
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
|
||||
| [**XCORE**](https://xcore-server.de/) | XCORE offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
|
||||
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHosting’s reliable servers and network. Easy to use, provisioned in a couple of minutes. |
|
||||
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. |
|
||||
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims for inexpensive services on quality servers. Premium i9-9900K processors will run your game like a dream. |
|
||||
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
|
||||
| [**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*! |
|
||||
|
||||
## Documentation
|
||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||
|
||||
@@ -58,7 +58,7 @@ var versionCommand = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints the current executable version and exits.",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
fmt.Printf("wings v%s\nCopyright © 2018 - 2021 Dane Everitt & Contributors\n", system.Version)
|
||||
fmt.Printf("wings v%s\nCopyright © 2018 - %d Dane Everitt & Contributors\n", system.Version, time.Now().Year())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ __ [blue][bold]Pterodactyl[reset] _____/___/_______ _______ ______
|
||||
\___/\___/___/___/___/___ /______/
|
||||
/_______/ [bold]%s[reset]
|
||||
|
||||
Copyright © 2018 - 2021 Dane Everitt & Contributors
|
||||
Copyright © 2018 - %d Dane Everitt & Contributors
|
||||
|
||||
Website: https://pterodactyl.io
|
||||
Source: https://github.com/pterodactyl/wings
|
||||
@@ -408,7 +408,7 @@ License: https://github.com/pterodactyl/wings/blob/develop/LICENSE
|
||||
|
||||
This software is made available under the terms of the MIT license.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.%s`), system.Version, "\n\n")
|
||||
in all copies or substantial portions of the Software.%s`), system.Version, time.Now().Year(), "\n\n")
|
||||
}
|
||||
|
||||
func exitWithConfigurationNotice() {
|
||||
|
||||
@@ -247,6 +247,8 @@ type Configuration struct {
|
||||
// if the debug flag is passed through the command line arguments.
|
||||
Debug bool
|
||||
|
||||
AppName string `default:"Pterodactyl" json:"app_name" yaml:"app_name"`
|
||||
|
||||
// A unique identifier for this node in the Panel.
|
||||
Uuid string
|
||||
|
||||
@@ -395,7 +397,7 @@ func EnsurePterodactylUser() error {
|
||||
if sysName == "busybox" {
|
||||
_config.System.Username = system.FirstNotEmpty(os.Getenv("WINGS_USERNAME"), "pterodactyl")
|
||||
_config.System.User.Uid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988"))
|
||||
_config.System.User.Gid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988"))
|
||||
_config.System.User.Gid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_GID"), "988"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -149,12 +149,12 @@ func (e *Environment) Create() error {
|
||||
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
|
||||
return nil
|
||||
} else if !client.IsErrNotFound(err) {
|
||||
return err
|
||||
return errors.Wrap(err, "environment/docker: failed to inspect container")
|
||||
}
|
||||
|
||||
// Try to pull the requested image before creating the container.
|
||||
if err := e.ensureImageExists(e.meta.Image); err != nil {
|
||||
return err
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
a := e.Configuration.Allocations()
|
||||
@@ -230,7 +230,7 @@ func (e *Environment) Create() error {
|
||||
}
|
||||
|
||||
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, nil, e.Id); err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "environment/docker: failed to create container")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -420,7 +420,7 @@ func (e *Environment) ensureImageExists(image string) error {
|
||||
if ierr != nil {
|
||||
// Well damn, something has gone really wrong here, just go ahead and abort there
|
||||
// isn't much anything we can do to try and self-recover from this.
|
||||
return ierr
|
||||
return errors.Wrap(ierr, "environment/docker: failed to list images")
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
@@ -441,7 +441,7 @@ func (e *Environment) ensureImageExists(image string) error {
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return errors.Wrapf(err, "environment/docker: failed to pull \"%s\" image for server", image)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ func (e *Environment) OnBeforeStart() 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 !client.IsErrNotFound(err) {
|
||||
return errors.WithMessage(err, "failed to remove server docker container during pre-boot")
|
||||
return errors.WrapIf(err, "environment/docker: failed to remove container during pre-boot")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (e *Environment) Start() error {
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2000
|
||||
if !client.IsErrNotFound(err) {
|
||||
return err
|
||||
return errors.WrapIf(err, "environment/docker: failed to inspect container")
|
||||
}
|
||||
} else {
|
||||
// If the server is running update our internal state and continue on with the attach.
|
||||
@@ -86,7 +86,7 @@ func (e *Environment) Start() error {
|
||||
// to truncate them.
|
||||
if _, err := os.Stat(c.LogPath); err == nil {
|
||||
if err := os.Truncate(c.LogPath, 0); err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "environment/docker: failed to truncate instance logs")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,14 +101,14 @@ func (e *Environment) Start() error {
|
||||
// exists on the system, and rebuild the container if that is required for server booting to
|
||||
// occur.
|
||||
if err := e.OnBeforeStart(); err != nil {
|
||||
return err
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
|
||||
return err
|
||||
return errors.WrapIf(err, "environment/docker: failed to start container")
|
||||
}
|
||||
|
||||
// No errors, good to continue through.
|
||||
|
||||
124
parser/parser.go
124
parser/parser.go
@@ -30,6 +30,45 @@ const (
|
||||
Xml = "xml"
|
||||
)
|
||||
|
||||
type ReplaceValue struct {
|
||||
value []byte
|
||||
valueType jsonparser.ValueType
|
||||
}
|
||||
|
||||
// Value returns the underlying value of the replacement. Be aware that this
|
||||
// can include escaped UTF-8 sequences that will need to be handled by the caller
|
||||
// in order to avoid accidentally injecting invalid sequences into the running
|
||||
// process.
|
||||
//
|
||||
// For example the expected value may be "§Foo" but you'll be working directly
|
||||
// with "\u00a7FOo" for this value. This will cause user pain if not solved since
|
||||
// that is clearly not the value they were expecting to be using.
|
||||
func (cv *ReplaceValue) Value() []byte {
|
||||
return cv.value
|
||||
}
|
||||
|
||||
// Type returns the underlying data type for the Value field.
|
||||
func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
||||
return cv.valueType
|
||||
}
|
||||
|
||||
// String returns the value as a string representation. This will automatically
|
||||
// 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>"
|
||||
}
|
||||
str, err := jsonparser.ParseString(cv.value)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "parser: could not parse value"))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
type ConfigurationParser string
|
||||
|
||||
func (cp ConfigurationParser) String() string {
|
||||
@@ -77,15 +116,16 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Defines a single find/replace instance for a given server configuration file.
|
||||
// ConfigurationFileReplacement defines a single find/replace instance for a
|
||||
// given server configuration file.
|
||||
type ConfigurationFileReplacement struct {
|
||||
Match string `json:"match"`
|
||||
IfValue string `json:"if_value"`
|
||||
ReplaceWith ReplaceValue `json:"replace_with"`
|
||||
}
|
||||
|
||||
// Handles unmarshaling the JSON representation into a struct that provides more useful
|
||||
// data to this functionality.
|
||||
// UnmarshalJSON handles unmarshaling the JSON representation into a struct that
|
||||
// provides more useful data to this functionality.
|
||||
func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
||||
m, err := jsonparser.GetString(data, "match")
|
||||
if err != nil {
|
||||
@@ -410,48 +450,66 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses a properties file and updates the values within it to match those that
|
||||
// are passed. Writes the file once completed.
|
||||
// parsePropertiesFile parses a properties file and updates the values within it
|
||||
// to match those that are passed. Once completed the new file is written to the
|
||||
// disk. This will cause comments not present at the head of the file to be
|
||||
// removed unfortunately.
|
||||
//
|
||||
// Any UTF-8 value will be written back to the disk as their escaped value rather
|
||||
// than the raw value There is no winning with this logic. This fixes a bug where
|
||||
// users with hand rolled UTF-8 escape sequences would have all sorts of pain in
|
||||
// their configurations because we were writing the UTF-8 literal characters which
|
||||
// their games could not actually handle.
|
||||
//
|
||||
// However, by adding this fix to only store the escaped UTF-8 sequence we
|
||||
// unwittingly introduced a "regression" that causes _other_ games to have issues
|
||||
// because they can only handle the unescaped representations. I cannot think of
|
||||
// a simple approach to this problem that doesn't just lead to more complicated
|
||||
// cases and problems.
|
||||
//
|
||||
// So, if your game cannot handle parsing UTF-8 sequences that are escaped into
|
||||
// the string, well, sucks. There are fewer of those games than there are games
|
||||
// that have issues parsing the raw UTF-8 sequence into a string? Also how does
|
||||
// one really know what the user intended at this point? We'd need to know if
|
||||
// the value was escaped or not to begin with before setting it, which I suppose
|
||||
// can work but jesus that is going to be some annoyingly complicated logic?
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2308 (original)
|
||||
// @see https://github.com/pterodactyl/panel/issues/3009 ("bug" introduced as result)
|
||||
func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
// Open the file.
|
||||
f2, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
|
||||
// Get any header comments from the file.
|
||||
scanner := bufio.NewScanner(f2)
|
||||
// Open the file and attempt to load any comments that currenty exist at the start
|
||||
// of the file. This is kind of a hack, but should work for a majority of users for
|
||||
// the time being.
|
||||
if fd, err := os.Open(path); err != nil {
|
||||
return errors.Wrap(err, "parser: could not open file for reading")
|
||||
} else {
|
||||
scanner := bufio.NewScanner(fd)
|
||||
// Scan until we hit a line that is not a comment that actually has content
|
||||
// on it. Keep appending the comments until that time.
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
if len(text) > 0 && text[0] != '#' {
|
||||
break
|
||||
}
|
||||
|
||||
s.WriteString(text)
|
||||
s.WriteString("\n")
|
||||
s.WriteString(text + "\n")
|
||||
}
|
||||
|
||||
// Close the file.
|
||||
_ = f2.Close()
|
||||
|
||||
// Handle any scanner errors.
|
||||
_ = fd.Close()
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the properties file.
|
||||
p, err := properties.LoadFile(path, properties.UTF8)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "parser: could not load properties file for configuration update")
|
||||
}
|
||||
|
||||
// Replace any values that need to be replaced.
|
||||
for _, replace := range f.Replace {
|
||||
data, err := f.LookupConfigurationValue(replace)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "parser: failed to lookup configuration value")
|
||||
}
|
||||
|
||||
v, ok := p.Get(replace.Match)
|
||||
@@ -463,7 +521,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
}
|
||||
|
||||
if _, _, err := p.Set(replace.Match, data); err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "parser: failed to set replacement value")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,11 +531,11 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
s.WriteString(key)
|
||||
s.WriteByte('=')
|
||||
s.WriteString(strings.Trim(strconv.QuoteToASCII(value), `"`))
|
||||
s.WriteString("\n")
|
||||
// This escape is intentional!
|
||||
//
|
||||
// See the docblock for this function for more details, do not change this
|
||||
// or you'll cause a flood of new issue reports no one wants to deal with.
|
||||
s.WriteString(key + "=" + strings.Trim(strconv.QuoteToASCII(value), "\"") + "\n")
|
||||
}
|
||||
|
||||
// Open the file for writing.
|
||||
@@ -489,7 +547,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
|
||||
// Write the data to the file.
|
||||
if _, err := w.Write([]byte(s.String())); err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/buger/jsonparser"
|
||||
)
|
||||
|
||||
type ReplaceValue struct {
|
||||
value []byte
|
||||
valueType jsonparser.ValueType `json:"-"`
|
||||
}
|
||||
|
||||
func (cv *ReplaceValue) Value() []byte {
|
||||
return cv.value
|
||||
}
|
||||
|
||||
func (cv *ReplaceValue) String() string {
|
||||
str, _ := jsonparser.ParseString(cv.value)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
||||
return cv.valueType
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func (e *RequestError) getAsFilesystemError() (int, string) {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
|
||||
}
|
||||
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeDiskSpace) || strings.Contains(e.err.Error(), "filesystem: not enough disk space") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
|
||||
return http.StatusBadRequest, "Cannot perform that action: not enough disk space available."
|
||||
}
|
||||
if strings.HasSuffix(e.err.Error(), "file name too long") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
|
||||
|
||||
@@ -16,19 +16,9 @@ import (
|
||||
"github.com/pterodactyl/wings/server"
|
||||
)
|
||||
|
||||
type serverProcData struct {
|
||||
server.ResourceUsage
|
||||
Suspended bool `json:"suspended"`
|
||||
}
|
||||
|
||||
// Returns a single server from the collection of servers.
|
||||
func getServer(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
c.JSON(http.StatusOK, serverProcData{
|
||||
ResourceUsage: s.Proc(),
|
||||
Suspended: s.IsSuspended(),
|
||||
})
|
||||
c.JSON(http.StatusOK, ExtractServer(c).ToAPIResponse())
|
||||
}
|
||||
|
||||
// Returns the logs for a given server instance.
|
||||
|
||||
@@ -63,7 +63,7 @@ func postServerBackup(c *gin.Context) {
|
||||
// This endpoint will block until the backup is fully restored allowing for a
|
||||
// spinner to be displayed in the Panel UI effectively.
|
||||
//
|
||||
// TODO: stop the server if it is running; internally mark it as suspended
|
||||
// TODO: stop the server if it is running
|
||||
func postServerRestoreBackup(c *gin.Context) {
|
||||
s := middleware.ExtractServer(c)
|
||||
client := middleware.ExtractApiClient(c)
|
||||
@@ -84,9 +84,19 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
s.SetRestoring(true)
|
||||
hasError := true
|
||||
defer func() {
|
||||
if !hasError {
|
||||
return
|
||||
}
|
||||
|
||||
s.SetRestoring(false)
|
||||
}()
|
||||
|
||||
logger.Info("processing server backup restore request")
|
||||
if data.TruncateDirectory {
|
||||
logger.Info(`recieved "truncate_directory" flag in request: deleting server files`)
|
||||
logger.Info("received \"truncate_directory\" flag in request: deleting server files")
|
||||
if err := s.Filesystem().TruncateRootDirectory(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
return
|
||||
@@ -109,7 +119,9 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||
s.Events().Publish(server.DaemonMessageEvent, "Completed server restoration from local backup.")
|
||||
s.Events().Publish(server.BackupRestoreCompletedEvent, "")
|
||||
logger.Info("completed server restoration from local backup")
|
||||
s.SetRestoring(false)
|
||||
}(s, b, logger)
|
||||
hasError = false
|
||||
c.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
@@ -136,7 +148,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||
}
|
||||
// Don't allow content types that we know are going to give us problems.
|
||||
if res.Header.Get("Content-Type") == "" || !strings.Contains("application/x-gzip application/gzip", res.Header.Get("Content-Type")) {
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": "The provided backup link is not a supported content type. \"" + res.Header.Get("Content-Type") + "\" is not application/x-gzip.",
|
||||
})
|
||||
@@ -151,8 +163,10 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||
s.Events().Publish(server.DaemonMessageEvent, "Completed server restoration from S3 backup.")
|
||||
s.Events().Publish(server.BackupRestoreCompletedEvent, "")
|
||||
logger.Info("completed server restoration from S3 backup")
|
||||
s.SetRestoring(false)
|
||||
}(s, c.Param("backup"), logger)
|
||||
|
||||
hasError = false
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -43,8 +44,16 @@ func getServerFileContents(c *gin.Context) {
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
}
|
||||
defer c.Writer.Flush()
|
||||
_, err = bufio.NewReader(f).WriteTo(c.Writer)
|
||||
if err != nil {
|
||||
// If you don't do a limited reader here you will trigger a panic on write when
|
||||
// a different server process writes content to the file after you've already
|
||||
// determined the file size. This could lead to some weird content output but
|
||||
// it would technically be accurate based on the content at the time of the request.
|
||||
//
|
||||
// "http: wrote more than the declared Content-Length"
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/3131
|
||||
r := io.LimitReader(f, st.Size())
|
||||
if _, err = bufio.NewReader(r).WriteTo(c.Writer); err != nil {
|
||||
// Pretty sure this will unleash chaos on the response, but its a risk we can
|
||||
// take since a panic will at least be recovered and this should be incredibly
|
||||
// rare?
|
||||
@@ -374,8 +383,6 @@ func postServerCompressFiles(c *gin.Context) {
|
||||
// of unpacking an archive that exists on the server into the provided RootPath
|
||||
// for the server.
|
||||
func postServerDecompressFiles(c *gin.Context) {
|
||||
s := middleware.ExtractServer(c)
|
||||
lg := middleware.ExtractLogger(c)
|
||||
var data struct {
|
||||
RootPath string `json:"root"`
|
||||
File string `json:"file"`
|
||||
@@ -384,7 +391,8 @@ func postServerDecompressFiles(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
lg = lg.WithFields(log.Fields{"root_path": data.RootPath, "file": data.File})
|
||||
s := middleware.ExtractServer(c)
|
||||
lg := middleware.ExtractLogger(c).WithFields(log.Fields{"root_path": data.RootPath, "file": data.File})
|
||||
lg.Debug("checking if space is available for file decompression")
|
||||
err := s.Filesystem().SpaceAvailableForDecompression(data.RootPath, data.File)
|
||||
if err != nil {
|
||||
@@ -403,7 +411,7 @@ func postServerDecompressFiles(c *gin.Context) {
|
||||
// much we specifically can do. They'll need to stop the running server process in order to overwrite
|
||||
// a file like this.
|
||||
if strings.Contains(err.Error(), "text file busy") {
|
||||
lg.WithField("error", err).Warn("failed to decompress file: text file busy")
|
||||
lg.WithField("error", errors.WithStackIf(err)).Warn("failed to decompress file: text file busy")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": "One or more files this archive is attempting to overwrite are currently in use by another process. Please try again.",
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/installer"
|
||||
"github.com/pterodactyl/wings/router/middleware"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
@@ -28,7 +29,12 @@ func getSystemInformation(c *gin.Context) {
|
||||
// Returns all of the servers that are registered and configured correctly on
|
||||
// this wings instance.
|
||||
func getAllServers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, middleware.ExtractManager(c).All())
|
||||
servers := middleware.ExtractManager(c).All()
|
||||
out := make([]server.APIResponse, len(servers), len(servers))
|
||||
for i, v := range servers {
|
||||
out[i] = v.ToAPIResponse()
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// Creates a new server on the wings daemon and begins the installation process
|
||||
|
||||
@@ -2,13 +2,13 @@ package backup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
type LocalBackup struct {
|
||||
@@ -78,10 +78,6 @@ func (b *LocalBackup) Restore(_ io.Reader, callback RestoreCallback) error {
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
name, err := system.ExtractArchiveSourceName(f, "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return callback(name, f)
|
||||
return callback(f.Name(), f)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type EggConfiguration struct {
|
||||
// The internal UUID of the Egg on the Panel.
|
||||
ID string
|
||||
ID string `json:"id"`
|
||||
|
||||
// Maintains a list of files that are blacklisted for opening/editing/downloading
|
||||
// or basically any type of access on the server by any user. This is NOT the same
|
||||
@@ -43,7 +43,6 @@ type Configuration struct {
|
||||
Build environment.Limits `json:"build"`
|
||||
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
|
||||
Mounts []Mount `json:"mounts"`
|
||||
Resources ResourceUsage `json:"resources"`
|
||||
Egg EggConfiguration `json:"egg,omitempty"`
|
||||
|
||||
Container struct {
|
||||
|
||||
@@ -13,6 +13,11 @@ import (
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
// appName is a local cache variable to avoid having to make expensive copies of
|
||||
// the configuration every time we need to send output along to the websocket for
|
||||
// a server.
|
||||
var appName string
|
||||
|
||||
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
|
||||
|
||||
type ConsoleThrottler struct {
|
||||
@@ -122,11 +127,14 @@ func (s *Server) Throttler() *ConsoleThrottler {
|
||||
return s.throttler
|
||||
}
|
||||
|
||||
// Sends output to the server console formatted to appear correctly as being sent
|
||||
// from Wings.
|
||||
// PublishConsoleOutputFromDaemon sends output to the server console formatted
|
||||
// to appear correctly as being sent from Wings.
|
||||
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
|
||||
if appName == "" {
|
||||
appName = config.Get().AppName
|
||||
}
|
||||
s.Events().Publish(
|
||||
ConsoleOutputEvent,
|
||||
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
|
||||
colorstring.Color(fmt.Sprintf("[yellow][bold][%s Daemon]:[default] %s", appName, data)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ var (
|
||||
ErrSuspended = errors.New("server is currently in a suspended state")
|
||||
ErrServerIsInstalling = errors.New("server is currently installing")
|
||||
ErrServerIsTransferring = errors.New("server is currently being transferred")
|
||||
ErrServerIsRestoring = errors.New("server is currently being restored")
|
||||
)
|
||||
|
||||
type crashTooFrequent struct {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
// CompressFiles compresses all of the files matching the given paths in the
|
||||
@@ -86,13 +86,13 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) er
|
||||
// Walk over the archive and figure out just how large the final output would be from unarchiving it.
|
||||
err = archiver.Walk(source, func(f archiver.File) error {
|
||||
if atomic.AddInt64(&size, f.Size())+dirSize > fs.MaxDisk() {
|
||||
return &Error{code: ErrCodeDiskSpace}
|
||||
return newFilesystemError(ErrCodeDiskSpace, nil)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "format ") {
|
||||
return &Error{code: ErrCodeUnknownArchive}
|
||||
if IsUnknownArchiveFormatError(err) {
|
||||
return newFilesystemError(ErrCodeUnknownArchive, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
||||
}
|
||||
// Ensure that the source archive actually exists on the system.
|
||||
if _, err := os.Stat(source); err != nil {
|
||||
return err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Walk all of the files in the archiver file and write them to the disk. If any
|
||||
@@ -121,23 +121,19 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
name, err := system.ExtractArchiveSourceName(f, dir)
|
||||
if err != nil {
|
||||
return WrapError(err, filepath.Join(dir, f.Name()))
|
||||
}
|
||||
p := filepath.Join(dir, name)
|
||||
p := filepath.Join(dir, f.Name())
|
||||
// If it is ignored, just don't do anything with the file and skip over it.
|
||||
if err := fs.IsIgnored(p); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := fs.Writefile(p, f); err != nil {
|
||||
return &Error{code: ErrCodeUnknownError, err: err, resolved: source}
|
||||
return wrapError(err, source)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "format ") {
|
||||
return &Error{code: ErrCodeUnknownArchive}
|
||||
if IsUnknownArchiveFormatError(err) {
|
||||
return newFilesystemError(ErrCodeUnknownArchive, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/karrick/godirwalk"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/karrick/godirwalk"
|
||||
)
|
||||
|
||||
type SpaceCheckingOpts struct {
|
||||
@@ -48,7 +49,7 @@ func (fs *Filesystem) SetDiskLimit(i int64) {
|
||||
// no space, rather than a boolean value.
|
||||
func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
|
||||
if !fs.HasSpaceAvailable(allowStaleValue) {
|
||||
return &Error{code: ErrCodeDiskSpace}
|
||||
return newFilesystemError(ErrCodeDiskSpace, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -200,16 +201,13 @@ func (fs *Filesystem) HasSpaceFor(size int64) error {
|
||||
if fs.MaxDisk() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s, err := fs.DiskUsage(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (s + size) > fs.MaxDisk() {
|
||||
return &Error{code: ErrCodeDiskSpace}
|
||||
return newFilesystemError(ErrCodeDiskSpace, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
@@ -34,6 +35,14 @@ type Error struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// newFilesystemError returns a new error instance with a stack trace associated.
|
||||
func newFilesystemError(code ErrorCode, err error) error {
|
||||
if err != nil {
|
||||
return errors.WithStackDepth(&Error{code: code, err: err}, 1)
|
||||
}
|
||||
return errors.WithStackDepth(&Error{code: code}, 1)
|
||||
}
|
||||
|
||||
// Code returns the ErrorCode for this specific error instance.
|
||||
func (e *Error) Code() ErrorCode {
|
||||
return e.code
|
||||
@@ -63,13 +72,13 @@ func (e *Error) Error() string {
|
||||
case ErrCodeUnknownError:
|
||||
fallthrough
|
||||
default:
|
||||
return fmt.Sprintf("filesystem: an error occurred: %s", e.Cause())
|
||||
return fmt.Sprintf("filesystem: an error occurred: %s", e.Unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of this filesystem error. In some causes
|
||||
// Unwrap returns the underlying cause of this filesystem error. In some causes
|
||||
// there may not be a cause present, in which case nil will be returned.
|
||||
func (e *Error) Cause() error {
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
@@ -113,20 +122,26 @@ func IsErrorCode(err error, code ErrorCode) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NewBadPathResolution returns a new BadPathResolution error.
|
||||
func NewBadPathResolution(path string, resolved string) *Error {
|
||||
return &Error{code: ErrCodePathResolution, path: path, resolved: resolved}
|
||||
// IsUnknownArchiveFormatError checks if the error is due to the archive being
|
||||
// in an unexpected file format.
|
||||
func IsUnknownArchiveFormatError(err error) bool {
|
||||
if err != nil && strings.HasPrefix(err.Error(), "format ") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WrapError wraps the provided error as a Filesystem error and attaches the
|
||||
// NewBadPathResolution returns a new BadPathResolution error.
|
||||
func NewBadPathResolution(path string, resolved string) error {
|
||||
return errors.WithStackDepth(&Error{code: ErrCodePathResolution, path: path, resolved: resolved}, 1)
|
||||
}
|
||||
|
||||
// wrapError wraps the provided error as a Filesystem error and attaches the
|
||||
// provided resolved source to it. If the error is already a Filesystem error
|
||||
// no action is taken.
|
||||
func WrapError(err error, resolved string) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
func wrapError(err error, resolved string) error {
|
||||
if err == nil || IsFilesystemError(err) {
|
||||
return err
|
||||
}
|
||||
if IsFilesystemError(err) {
|
||||
return err.(*Error)
|
||||
}
|
||||
return &Error{code: ErrCodeUnknownError, err: err, resolved: resolved}
|
||||
return errors.WithStackDepth(&Error{code: ErrCodeUnknownError, err: err, resolved: resolved}, 1)
|
||||
}
|
||||
@@ -1,13 +1,45 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
. "github.com/franela/goblin"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"emperror.dev/errors"
|
||||
. "github.com/franela/goblin"
|
||||
)
|
||||
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
func TestFilesystem_PathResolutionError(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
|
||||
g.Describe("NewFilesystemError", func() {
|
||||
g.It("includes a stack trace for the error", func() {
|
||||
err := newFilesystemError(ErrCodeUnknownError, nil)
|
||||
|
||||
_, ok := err.(stackTracer)
|
||||
g.Assert(ok).IsTrue()
|
||||
})
|
||||
|
||||
g.It("properly wraps the underlying error cause", func() {
|
||||
underlying := io.EOF
|
||||
err := newFilesystemError(ErrCodeUnknownError, underlying)
|
||||
|
||||
_, ok := err.(stackTracer)
|
||||
g.Assert(ok).IsTrue()
|
||||
|
||||
_, ok = err.(*Error)
|
||||
g.Assert(ok).IsFalse()
|
||||
|
||||
fserr, ok := errors.Unwrap(err).(*Error)
|
||||
g.Assert(ok).IsTrue()
|
||||
g.Assert(fserr.Unwrap()).IsNotNil()
|
||||
g.Assert(fserr.Unwrap()).Equal(underlying)
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("NewBadPathResolutionError", func() {
|
||||
g.It("is can detect itself as an error correctly", func() {
|
||||
err := NewBadPathResolution("foo", "bar")
|
||||
@@ -18,6 +50,7 @@ func TestFilesystem_PathResolutionError(t *testing.T) {
|
||||
|
||||
g.It("returns <empty> if no destination path is provided", func() {
|
||||
err := NewBadPathResolution("foo", "")
|
||||
g.Assert(err).IsNotNil()
|
||||
g.Assert(err.Error()).Equal("filesystem: server path [foo] resolves to a location outside the server root: <empty>")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,7 +67,7 @@ func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
|
||||
return nil, Stat{}, err
|
||||
}
|
||||
if st.IsDir() {
|
||||
return nil, Stat{}, &Error{code: ErrCodeIsDirectory}
|
||||
return nil, Stat{}, newFilesystemError(ErrCodeIsDirectory, nil)
|
||||
}
|
||||
f, err := os.Open(cleaned)
|
||||
if err != nil {
|
||||
@@ -144,7 +144,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
||||
return errors.Wrap(err, "server/filesystem: writefile: failed to stat file")
|
||||
} else if err == nil {
|
||||
if stat.IsDir() {
|
||||
return &Error{code: ErrCodeIsDirectory, resolved: cleaned}
|
||||
return errors.WithStack(&Error{code: ErrCodeIsDirectory, resolved: cleaned})
|
||||
}
|
||||
currentSize = stat.Size()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func (fs *Filesystem) IsIgnored(paths ...string) error {
|
||||
return err
|
||||
}
|
||||
if fs.denylist.MatchesPath(sp) {
|
||||
return &Error{code: ErrCodeDenylistFile, path: p, resolved: sp}
|
||||
return errors.WithStack(&Error{code: ErrCodeDenylistFile, path: p, resolved: sp})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -2,11 +2,12 @@ package filesystem
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"emperror.dev/errors"
|
||||
. "github.com/franela/goblin"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"emperror.dev/errors"
|
||||
. "github.com/franela/goblin"
|
||||
)
|
||||
|
||||
func TestFilesystem_Path(t *testing.T) {
|
||||
|
||||
@@ -151,6 +151,14 @@ func (s *Server) SetTransferring(state bool) {
|
||||
s.transferring.Store(state)
|
||||
}
|
||||
|
||||
func (s *Server) IsRestoring() bool {
|
||||
return s.restoring.Load()
|
||||
}
|
||||
|
||||
func (s *Server) SetRestoring(state bool) {
|
||||
s.restoring.Store(state)
|
||||
}
|
||||
|
||||
// Removes the installer container for the server.
|
||||
func (ip *InstallationProcess) RemoveContainer() error {
|
||||
err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", types.ContainerRemoveOptions{
|
||||
|
||||
@@ -70,6 +70,10 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
||||
return ErrServerIsTransferring
|
||||
}
|
||||
|
||||
if s.IsRestoring() {
|
||||
return ErrServerIsRestoring
|
||||
}
|
||||
|
||||
if s.powerLock == nil {
|
||||
s.powerLock = semaphore.NewWeighted(1)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ type Server struct {
|
||||
// installer process is still running.
|
||||
installing *system.AtomicBool
|
||||
transferring *system.AtomicBool
|
||||
restoring *system.AtomicBool
|
||||
|
||||
// The console throttler instance used to control outputs.
|
||||
throttler *ConsoleThrottler
|
||||
@@ -79,6 +80,7 @@ func New(client remote.Client) (*Server, error) {
|
||||
client: client,
|
||||
installing: system.NewAtomicBool(false),
|
||||
transferring: system.NewAtomicBool(false),
|
||||
restoring: system.NewAtomicBool(false),
|
||||
}
|
||||
if err := defaults.Set(&s); err != nil {
|
||||
return nil, errors.Wrap(err, "server: could not set default values for struct")
|
||||
@@ -141,12 +143,12 @@ func (s *Server) Log() *log.Entry {
|
||||
return log.WithField("server", s.Id())
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Sync 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.
|
||||
//
|
||||
// This also means mass actions can be performed against servers on the Panel and they
|
||||
// will automatically sync with Wings when the server is started.
|
||||
// This also means mass actions can be performed against servers on the Panel
|
||||
// and they will automatically sync with Wings when the server is started.
|
||||
func (s *Server) Sync() error {
|
||||
cfg, err := s.client.GetServerConfiguration(s.Context(), s.Id())
|
||||
if err != nil {
|
||||
@@ -303,3 +305,24 @@ func (s *Server) IsRunning() bool {
|
||||
|
||||
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
||||
}
|
||||
|
||||
// APIResponse is a type returned when requesting details about a single server
|
||||
// instance on Wings. This includes the information needed by the Panel in order
|
||||
// to show resource utilization and the current state on this system.
|
||||
type APIResponse struct {
|
||||
State string `json:"state"`
|
||||
IsSuspended bool `json:"is_suspended"`
|
||||
Utilization ResourceUsage `json:"utilization"`
|
||||
Configuration Configuration `json:"configuration"`
|
||||
}
|
||||
|
||||
// ToAPIResponse returns the server struct as an API object that can be consumed
|
||||
// by callers.
|
||||
func (s *Server) ToAPIResponse() APIResponse {
|
||||
return APIResponse{
|
||||
State: s.Environment.State(),
|
||||
IsSuspended: s.IsSuspended(),
|
||||
Utilization: s.Proc(),
|
||||
Configuration: *s.Config(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
||||
var cr = []byte(" \r")
|
||||
@@ -41,22 +36,6 @@ func MustInt(v string) int {
|
||||
return i
|
||||
}
|
||||
|
||||
// ExtractArchiveSourceName looks for the provided archiver.File's name if it is
|
||||
// a type that is supported, otherwise it returns an error to the caller.
|
||||
func ExtractArchiveSourceName(f archiver.File, dir string) (name string, err error) {
|
||||
switch s := f.Sys().(type) {
|
||||
case *tar.Header:
|
||||
name = s.Name
|
||||
case *gzip.Header:
|
||||
name = s.Name
|
||||
case *zip.FileHeader:
|
||||
name = s.Name
|
||||
default:
|
||||
err = errors.New(fmt.Sprintf("could not parse underlying data source with type: %s", reflect.TypeOf(s).String()))
|
||||
}
|
||||
return name, err
|
||||
}
|
||||
|
||||
func ScanReader(r io.Reader, callback func(line string)) error {
|
||||
br := bufio.NewReader(r)
|
||||
// Avoid constantly re-allocating memory when we're flooding lines through this
|
||||
|
||||
Reference in New Issue
Block a user