Upstream pterodactyl/wings state of the touched files before 9cea6aae7b
This commit is contained in:
1 parent
0fffe4679c
commit
1bb0c10f5e
2 files changed
+486
No files matched your search
@@ -0,0 +1,265 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/transfer"
|
||||
)
|
||||
|
||||
// Returns a single server from the collection of servers.
|
||||
func getServer(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, ExtractServer(c).ToAPIResponse())
|
||||
}
|
||||
|
||||
// Returns the logs for a given server instance.
|
||||
func getServerLogs(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
l, _ := strconv.Atoi(c.DefaultQuery("size", "100"))
|
||||
if l <= 0 {
|
||||
l = 100
|
||||
} else if l > 100 {
|
||||
l = 100
|
||||
}
|
||||
|
||||
out, err := s.ReadLogfile(l)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||
}
|
||||
|
||||
// Handles a request to control the power state of a server. If the action being passed
|
||||
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
|
||||
// and the actual power action is run asynchronously so that we don't have to block the
|
||||
// request until a potentially slow operation completes.
|
||||
//
|
||||
// This is done because for the most part the Panel is using websockets to determine when
|
||||
// things are happening, so theres no reason to sit and wait for a request to finish. We'll
|
||||
// just see over the socket if something isn't working correctly.
|
||||
func postServerPower(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
var data struct {
|
||||
Action server.PowerAction `json:"action"`
|
||||
WaitSeconds int `json:"wait_seconds"`
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !data.Action.IsValid() {
|
||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||
"error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Because we route all of the actual bootup process to a separate thread we need to
|
||||
// check the suspension status here, otherwise the user will hit the endpoint and then
|
||||
// just sit there wondering why it returns a success but nothing actually happens.
|
||||
//
|
||||
// We don't really care about any of the other actions at this point, they'll all result
|
||||
// in the process being stopped, which should have happened anyways if the server is suspended.
|
||||
if (data.Action == server.PowerActionStart || data.Action == server.PowerActionRestart) && s.IsSuspended() {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Cannot start or restart a server that is suspended.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Pass the actual heavy processing off to a separate thread to handle so that
|
||||
// we can immediately return a response from the server. Some of these actions
|
||||
// can take quite some time, especially stopping or restarting.
|
||||
go func(s *server.Server) {
|
||||
if data.WaitSeconds < 0 || data.WaitSeconds > 300 {
|
||||
data.WaitSeconds = 30
|
||||
}
|
||||
if err := s.HandlePowerAction(data.Action, data.WaitSeconds); err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
s.Log().WithField("action", data.Action).WithField("error", err).Warn("could not process server power action")
|
||||
} else if errors.Is(err, server.ErrIsRunning) {
|
||||
// Do nothing, this isn't something we care about for logging,
|
||||
} else {
|
||||
s.Log().WithFields(log.Fields{"action": data.Action, "wait_seconds": data.WaitSeconds, "error": err}).
|
||||
Error("encountered error processing a server power action in the background")
|
||||
}
|
||||
}
|
||||
}(s)
|
||||
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// Sends an array of commands to a running server instance.
|
||||
func postServerCommands(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
return
|
||||
} else if !running {
|
||||
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
|
||||
"error": "Cannot send commands to a stopped server instance.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Commands []string `json:"commands"`
|
||||
}
|
||||
// BindJSON sends 400 if the request fails, all we need to do is return
|
||||
if err := c.BindJSON(&data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, command := range data.Commands {
|
||||
if err := s.Environment.SendCommand(command); err != nil {
|
||||
s.Log().WithFields(log.Fields{"command": command, "error": err}).Warn("failed to send command to server instance")
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if err := s.Sync(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
} else {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// Performs a server installation in a background thread.
|
||||
func postServerInstall(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
go func(s *server.Server) {
|
||||
s.Log().Info("syncing server state with remote source before executing installation process")
|
||||
if err := s.Sync(); err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to sync server state with Panel")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Install(); err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to execute server installation process")
|
||||
}
|
||||
}(s)
|
||||
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// Reinstalls a server.
|
||||
func postServerReinstall(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
if s.ExecutingPowerAction() {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||
"error": "Cannot execute server reinstall event while another power action is running.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
go func(s *server.Server) {
|
||||
if err := s.Reinstall(); err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to complete server re-install process")
|
||||
}
|
||||
}(s)
|
||||
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// Deletes a server from the wings daemon and dissociate its objects.
|
||||
func deleteServer(c *gin.Context) {
|
||||
s := middleware.ExtractServer(c)
|
||||
|
||||
// Immediately suspend the server to prevent a user from attempting
|
||||
// to start it while this process is running.
|
||||
s.Config().SetSuspended(true)
|
||||
|
||||
// Notify all websocket clients that the server is being deleted.
|
||||
// This is useful for two reasons, one to tell clients not to bother
|
||||
// retrying to connect to the websocket. And two, for transfers when
|
||||
// the server has been successfully transferred to another node, and
|
||||
// the client needs to switch to the new node.
|
||||
if s.IsTransferring() {
|
||||
s.Events().Publish(server.TransferStatusEvent, transfer.StatusCompleted)
|
||||
}
|
||||
s.Events().Publish(server.DeletedEvent, nil)
|
||||
|
||||
s.CleanupForDestroy()
|
||||
|
||||
// Remove any pending remote file downloads for the server.
|
||||
for _, dl := range downloader.ByServer(s.ID()) {
|
||||
dl.Cancel()
|
||||
}
|
||||
|
||||
// Destroy the environment; in Docker this will handle a running container and
|
||||
// forcibly terminate it before removing the container, so we do not need to handle
|
||||
// that here.
|
||||
if err := s.Environment.Destroy(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Once the environment is terminated, remove the server files from the system. This is
|
||||
// done in a separate process since failure is not the end of the world and can be
|
||||
// manually cleaned up after the fact.
|
||||
//
|
||||
// In addition, servers with large amounts of files can take some time to finish deleting,
|
||||
// so we don't want to block the HTTP call while waiting on this.
|
||||
go func(s *server.Server) {
|
||||
fs := s.Filesystem()
|
||||
p := fs.Path()
|
||||
_ = fs.UnixFS().Close()
|
||||
if err := os.RemoveAll(p); err != nil {
|
||||
log.WithFields(log.Fields{"path": p, "error": err}).Warn("failed to remove server files during deletion process")
|
||||
}
|
||||
}(s)
|
||||
|
||||
middleware.ExtractManager(c).Remove(func(server *server.Server) bool {
|
||||
return server.ID() == s.ID()
|
||||
})
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Adds any of the JTIs passed through in the body to the deny list for the websocket
|
||||
// preventing any JWT generated before the current time from being used to connect to
|
||||
// the socket or send along commands.
|
||||
//
|
||||
// deprecated: prefer /api/deauthorize-user
|
||||
func postServerDenyWSTokens(c *gin.Context) {
|
||||
var data struct {
|
||||
JTIs []string `json:"jtis"`
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, jti := range data.JTIs {
|
||||
tokens.DenyJTI(jti)
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
type PowerAction string
|
||||
|
||||
// The power actions that can be performed for a given server. This taps into the given server
|
||||
// environment and performs them in a way that prevents a race condition from occurring. For
|
||||
// example, sending two "start" actions back to back will not process the second action until
|
||||
// the first action has been completed.
|
||||
//
|
||||
// This utilizes a workerpool with a limit of one worker so that all the actions execute
|
||||
// in a sync manner.
|
||||
const (
|
||||
PowerActionStart = "start"
|
||||
PowerActionStop = "stop"
|
||||
PowerActionRestart = "restart"
|
||||
PowerActionTerminate = "kill"
|
||||
)
|
||||
|
||||
// IsValid checks if the power action being received is valid.
|
||||
func (pa PowerAction) IsValid() bool {
|
||||
return pa == PowerActionStart ||
|
||||
pa == PowerActionStop ||
|
||||
pa == PowerActionTerminate ||
|
||||
pa == PowerActionRestart
|
||||
}
|
||||
|
||||
func (pa PowerAction) IsStart() bool {
|
||||
return pa == PowerActionStart || pa == PowerActionRestart
|
||||
}
|
||||
|
||||
// ExecutingPowerAction checks if there is currently a power action being
|
||||
// processed for the server.
|
||||
func (s *Server) ExecutingPowerAction() bool {
|
||||
return s.powerLock.IsLocked()
|
||||
}
|
||||
|
||||
// HandlePowerAction is a helper function that can receive a power action and then process the
|
||||
// actions that need to occur for it. This guards against someone calling Start() twice at the
|
||||
// same time, or trying to restart while another restart process is currently running.
|
||||
//
|
||||
// However, the code design for the daemon does depend on the user correctly calling this
|
||||
// function rather than making direct calls to the start/stop/restart functions on the
|
||||
// environment struct.
|
||||
func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error {
|
||||
if s.IsInstalling() || s.IsTransferring() || s.IsRestoring() {
|
||||
if s.IsRestoring() {
|
||||
return ErrServerIsRestoring
|
||||
} else if s.IsTransferring() {
|
||||
return ErrServerIsTransferring
|
||||
}
|
||||
return ErrServerIsInstalling
|
||||
}
|
||||
|
||||
lockId, _ := uuid.NewUUID()
|
||||
log := s.Log().WithField("lock_id", lockId.String()).WithField("action", action)
|
||||
|
||||
cleanup := func() {
|
||||
log.Info("releasing exclusive lock for power action")
|
||||
s.powerLock.Release()
|
||||
}
|
||||
|
||||
var wait int
|
||||
if len(waitSeconds) > 0 && waitSeconds[0] > 0 {
|
||||
wait = waitSeconds[0]
|
||||
}
|
||||
|
||||
log.WithField("wait_seconds", wait).Debug("acquiring power action lock for instance")
|
||||
// Only attempt to acquire a lock on the process if this is not a termination event. We want to
|
||||
// just allow those events to pass right through for good reason. If a server is currently trying
|
||||
// to process a power action but has gotten stuck you still should be able to pass through the
|
||||
// terminate event. The good news here is that doing that oftentimes will get the stuck process to
|
||||
// move again, and naturally continue through the process.
|
||||
if action != PowerActionTerminate {
|
||||
// Determines if we should wait for the lock or not. If a value greater than 0 is passed
|
||||
// into this function we will wait that long for a lock to be acquired.
|
||||
if wait > 0 {
|
||||
ctx, cancel := context.WithTimeout(s.ctx, time.Second*time.Duration(wait))
|
||||
defer cancel()
|
||||
|
||||
// Attempt to acquire a lock on the power action lock for up to 30 seconds. If more
|
||||
// time than that passes an error will be propagated back up the chain and this
|
||||
// request will be aborted.
|
||||
if err := s.powerLock.TryAcquire(ctx); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("could not acquire lock on power action after %d seconds", wait))
|
||||
}
|
||||
} else {
|
||||
// If no wait duration was provided we will attempt to immediately acquire the lock
|
||||
// and bail out with a context deadline error if it is not acquired immediately.
|
||||
if err := s.powerLock.Acquire(); err != nil {
|
||||
return errors.Wrap(err, "failed to acquire exclusive lock for power actions")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("acquired exclusive lock on power actions, processing event...")
|
||||
defer cleanup()
|
||||
} else {
|
||||
// Still try to acquire the lock if terminating, and it is available, just so that
|
||||
// other power actions are blocked until it has completed. However, if it cannot be
|
||||
// acquired we won't stop the entire process.
|
||||
//
|
||||
// If we did successfully acquire the lock, make sure we release it once we're done
|
||||
// executiong the power actions.
|
||||
if err := s.powerLock.Acquire(); err == nil {
|
||||
log.Info("acquired exclusive lock on power actions, processing event...")
|
||||
defer cleanup()
|
||||
} else if errors.Is(err, system.ErrLockerDestroyed) {
|
||||
// The server is being deleted.
|
||||
return err
|
||||
} else {
|
||||
log.Warn("failed to acquire exclusive lock, ignoring failure for termination event")
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
case PowerActionStart:
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
return ErrIsRunning
|
||||
}
|
||||
|
||||
// Run the pre-boot logic for the server before processing the environment start.
|
||||
if err := s.onBeforeStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Environment.Start(s.Context())
|
||||
case PowerActionStop:
|
||||
fallthrough
|
||||
case PowerActionRestart:
|
||||
// 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.
|
||||
if err := s.Environment.WaitForStop(s.Context(), time.Minute*10, true); err != nil {
|
||||
// Even timeout errors should be bubbled back up the stack. If the process didn't stop
|
||||
// nicely, but the terminate argument was passed then the server is stopped without an
|
||||
// error being returned.
|
||||
//
|
||||
// However, if terminate is not passed you'll get a context deadline error. We could
|
||||
// probably handle that nicely here, but I'd rather just pass it back up the stack for now.
|
||||
// Either way, any type of error indicates we should not attempt to start the server back
|
||||
// up.
|
||||
return err
|
||||
}
|
||||
|
||||
if action == PowerActionStop {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now actually try to start the process by executing the normal pre-boot logic.
|
||||
if err := s.onBeforeStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Environment.Start(s.Context())
|
||||
case PowerActionTerminate:
|
||||
return s.Environment.Terminate(s.Context(), "SIGKILL")
|
||||
}
|
||||
|
||||
return errors.New("attempting to handle unknown power action")
|
||||
}
|
||||
|
||||
// Execute a few functions before actually calling the environment start commands. This ensures
|
||||
// that everything is ready to go for environment booting, and that the server can even be started.
|
||||
func (s *Server) onBeforeStart() error {
|
||||
// Sync also applies the new environment variables and process resource limits
|
||||
// to the environment.
|
||||
s.Log().Info("syncing server configuration with panel")
|
||||
if err := s.Sync(); err != nil {
|
||||
return errors.WithMessage(err, "unable to sync server data from Panel instance")
|
||||
}
|
||||
|
||||
// Disallow start & restart if the server is suspended. Do this check after performing a sync
|
||||
// action with the Panel to ensure that we have the most up-to-date information for that server.
|
||||
if s.IsSuspended() {
|
||||
return ErrSuspended
|
||||
}
|
||||
|
||||
// If a server has unlimited disk space, we don't care enough to block the startup to check remaining.
|
||||
// However, we should trigger a size anyway, as it'd be good to kick it off for other processes.
|
||||
if s.DiskSpace() <= 0 {
|
||||
s.Filesystem().HasSpaceAvailable(true)
|
||||
} else {
|
||||
s.PublishConsoleOutputFromDaemon("Checking server disk space usage, this could take a few seconds...")
|
||||
if err := s.Filesystem().HasSpaceErr(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update the configuration files defined for the server before beginning the boot process.
|
||||
// This process executes a bunch of parallel updates, so we just block until that process
|
||||
// is complete. Any errors as a result of this will just be bubbled out in the logger,
|
||||
// we don't need to actively do anything about it at this point, worse comes to worst the
|
||||
// server starts in a weird state and the user can manually adjust.
|
||||
s.PublishConsoleOutputFromDaemon("Updating process configuration files...")
|
||||
s.Log().Debug("updating server configuration files...")
|
||||
s.UpdateConfigurationFiles()
|
||||
s.Log().Debug("updated server configuration files")
|
||||
|
||||
if config.Get().System.CheckPermissionsOnBoot {
|
||||
s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...")
|
||||
// Ensure all the server file permissions are set correctly before booting the process.
|
||||
s.Log().Debug("chowning server root directory...")
|
||||
if err := s.Filesystem().Chown("/"); err != nil {
|
||||
return errors.WithMessage(err, "failed to chown root server directory during pre-boot process")
|
||||
}
|
||||
}
|
||||
|
||||
s.Log().Info("completed server preflight, starting boot process...")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user