Acquire exclusive lock when installing a server
Also allows aborting a server install mid-process when the server is deleted before the process finishes.
This commit is contained in:
parent
073247e4e1
commit
1f6789cba3
|
@ -164,6 +164,11 @@ func deleteServer(c *gin.Context) {
|
||||||
// to start it while this process is running.
|
// to start it while this process is running.
|
||||||
s.Suspended = true
|
s.Suspended = true
|
||||||
|
|
||||||
|
// If the server is currently installing, abort it.
|
||||||
|
if s.IsInstalling() {
|
||||||
|
s.AbortInstallation()
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the server's archive if it exists. We intentionally don't return
|
// Delete the server's archive if it exists. We intentionally don't return
|
||||||
// here, if the archive fails to delete, the server can still be removed.
|
// here, if the archive fails to delete, the server can still be removed.
|
||||||
if err := s.Archiver.DeleteIfExists(); err != nil {
|
if err := s.Archiver.DeleteIfExists(); err != nil {
|
||||||
|
|
|
@ -12,12 +12,14 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executes the installation stack for a server process. Bubbles any errors up to the calling
|
// Executes the installation stack for a server process. Bubbles any errors up to the calling
|
||||||
|
@ -85,7 +87,7 @@ type InstallationProcess struct {
|
||||||
Script *api.InstallationScript
|
Script *api.InstallationScript
|
||||||
|
|
||||||
client *client.Client
|
client *client.Client
|
||||||
mutex *sync.Mutex
|
context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a new installation process struct that will be used to create containers,
|
// Generates a new installation process struct that will be used to create containers,
|
||||||
|
@ -94,21 +96,70 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
|
||||||
proc := &InstallationProcess{
|
proc := &InstallationProcess{
|
||||||
Script: script,
|
Script: script,
|
||||||
Server: s,
|
Server: s,
|
||||||
mutex: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
s.installer.cancel = &cancel
|
||||||
|
|
||||||
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
} else {
|
} else {
|
||||||
proc.client = c
|
proc.client = c
|
||||||
|
proc.context = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
return proc, nil
|
return proc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to obtain an exclusive lock on the installation process for the server. Waits up to 10
|
||||||
|
// seconds before aborting with a context timeout.
|
||||||
|
func (s *Server) acquireInstallationLock() error {
|
||||||
|
if s.installer.sem == nil {
|
||||||
|
s.installer.sem = semaphore.NewWeighted(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
|
||||||
|
return s.installer.sem.Acquire(ctx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the server is actively running the installation process by checking the status
|
||||||
|
// of the semaphore lock.
|
||||||
|
func (s *Server) IsInstalling() bool {
|
||||||
|
if s.installer.sem == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.installer.sem.TryAcquire(1) {
|
||||||
|
// If we made it into this block it means we were able to obtain an exclusive lock
|
||||||
|
// on the semaphore. In that case, go ahead and release that lock immediately, and
|
||||||
|
// return false.
|
||||||
|
s.installer.sem.Release(1)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aborts the server installation process by calling the cancel function on the installer
|
||||||
|
// context.
|
||||||
|
func (s *Server) AbortInstallation() {
|
||||||
|
if !s.IsInstalling() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.installer.cancel != nil {
|
||||||
|
cancel := *s.installer.cancel
|
||||||
|
|
||||||
|
s.Log().Warn("aborting running installation process")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Removes the installer container for the server.
|
// Removes the installer container for the server.
|
||||||
func (ip *InstallationProcess) RemoveContainer() {
|
func (ip *InstallationProcess) RemoveContainer() {
|
||||||
err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", types.ContainerRemoveOptions{
|
err := ip.client.ContainerRemove(ip.context, ip.Server.Uuid+"_installer", types.ContainerRemoveOptions{
|
||||||
RemoveVolumes: true,
|
RemoveVolumes: true,
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
|
@ -124,6 +175,20 @@ func (ip *InstallationProcess) RemoveContainer() {
|
||||||
// Once the container finishes installing the results will be stored in an installation
|
// Once the container finishes installing the results will be stored in an installation
|
||||||
// log in the server's configuration directory.
|
// log in the server's configuration directory.
|
||||||
func (ip *InstallationProcess) Run() error {
|
func (ip *InstallationProcess) Run() error {
|
||||||
|
ip.Server.Log().Debug("acquiring installation process lock")
|
||||||
|
if err := ip.Server.acquireInstallationLock(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We now have an exclusive lock on this installation process. Ensure that whenever this
|
||||||
|
// process is finished that the semaphore is released so that other processes and be executed
|
||||||
|
// without encounting a wait timeout.
|
||||||
|
defer func() {
|
||||||
|
ip.Server.Log().Debug("releasing installation process lock")
|
||||||
|
ip.Server.installer.sem.Release(1)
|
||||||
|
ip.Server.installer.cancel = nil
|
||||||
|
}()
|
||||||
|
|
||||||
installPath, err := ip.BeforeExecute()
|
installPath, err := ip.BeforeExecute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -183,7 +248,7 @@ func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
||||||
|
|
||||||
// Pulls the docker image to be used for the installation container.
|
// Pulls the docker image to be used for the installation container.
|
||||||
func (ip *InstallationProcess) pullInstallationImage() error {
|
func (ip *InstallationProcess) pullInstallationImage() error {
|
||||||
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, types.ImagePullOptions{})
|
r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -237,7 +302,7 @@ func (ip *InstallationProcess) BeforeExecute() (string, error) {
|
||||||
Force: true,
|
Force: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", opts); err != nil {
|
if err := ip.client.ContainerRemove(ip.context, ip.Server.Uuid+"_installer", opts); err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
e = append(e, err)
|
e = append(e, err)
|
||||||
}
|
}
|
||||||
|
@ -264,11 +329,10 @@ func (ip *InstallationProcess) GetLogPath() string {
|
||||||
// process to store in the server configuration directory, and then destroys the associated
|
// process to store in the server configuration directory, and then destroys the associated
|
||||||
// installation container.
|
// installation container.
|
||||||
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||||
ctx := context.Background()
|
|
||||||
defer ip.RemoveContainer()
|
defer ip.RemoveContainer()
|
||||||
|
|
||||||
ip.Server.Log().WithField("container_id", containerId).Debug("pulling installation logs for server")
|
ip.Server.Log().WithField("container_id", containerId).Debug("pulling installation logs for server")
|
||||||
reader, err := ip.client.ContainerLogs(ctx, containerId, types.ContainerLogsOptions{
|
reader, err := ip.client.ContainerLogs(ip.context, containerId, types.ContainerLogsOptions{
|
||||||
ShowStdout: true,
|
ShowStdout: true,
|
||||||
ShowStderr: true,
|
ShowStderr: true,
|
||||||
Follow: false,
|
Follow: false,
|
||||||
|
@ -295,8 +359,6 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||||
|
|
||||||
// Executes the installation process inside a specially created docker container.
|
// Executes the installation process inside a specially created docker container.
|
||||||
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
conf := &container.Config{
|
conf := &container.Config{
|
||||||
Hostname: "installer",
|
Hostname: "installer",
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
|
@ -345,13 +407,13 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ip.Server.Log().WithField("install_script", installPath+"/install.sh").Info("creating install container for server process")
|
ip.Server.Log().WithField("install_script", installPath+"/install.sh").Info("creating install container for server process")
|
||||||
r, err := ip.client.ContainerCreate(ctx, conf, hostConf, nil, ip.Server.Uuid+"_installer")
|
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Uuid+"_installer")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
|
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
|
||||||
if err := ip.client.ContainerStart(ctx, r.ID, types.ContainerStartOptions{}); err != nil {
|
if err := ip.client.ContainerStart(ip.context, r.ID, types.ContainerStartOptions{}); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +425,7 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||||
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
||||||
}(r.ID)
|
}(r.ID)
|
||||||
|
|
||||||
sChann, eChann := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning)
|
sChann, eChann := ip.client.ContainerWait(ip.context, r.ID, container.WaitConditionNotRunning)
|
||||||
select {
|
select {
|
||||||
case err := <-eChann:
|
case err := <-eChann:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -379,7 +441,7 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||||
// directory, as well as to a websocket listener so that the process can be viewed in
|
// directory, as well as to a websocket listener so that the process can be viewed in
|
||||||
// the panel by administrators.
|
// the panel by administrators.
|
||||||
func (ip *InstallationProcess) StreamOutput(id string) error {
|
func (ip *InstallationProcess) StreamOutput(id string) error {
|
||||||
reader, err := ip.client.ContainerLogs(context.Background(), id, types.ContainerLogsOptions{
|
reader, err := ip.client.ContainerLogs(ip.context, id, types.ContainerLogsOptions{
|
||||||
ShowStdout: true,
|
ShowStdout: true,
|
||||||
ShowStderr: true,
|
ShowStderr: true,
|
||||||
Follow: true,
|
Follow: true,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -71,11 +73,27 @@ type Server struct {
|
||||||
// started, and then cached here.
|
// started, and then cached here.
|
||||||
processConfiguration *api.ProcessConfiguration
|
processConfiguration *api.ProcessConfiguration
|
||||||
|
|
||||||
|
// Tracks the installation process for this server and prevents a server from running
|
||||||
|
// two installer processes at the same time. This also allows us to cancel a running
|
||||||
|
// installation process, for example when a server is deleted from the panel while the
|
||||||
|
// installer process is still running.
|
||||||
|
installer InstallerDetails
|
||||||
|
|
||||||
// Internal mutex used to block actions that need to occur sequentially, such as
|
// Internal mutex used to block actions that need to occur sequentially, such as
|
||||||
// writing the configuration to the disk.
|
// writing the configuration to the disk.
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstallerDetails struct {
|
||||||
|
// The cancel function for the installer. This will be a non-nil value while there
|
||||||
|
// is an installer running for the server.
|
||||||
|
cancel *context.CancelFunc
|
||||||
|
|
||||||
|
// Installer lock. You should obtain an exclusive lock on this context while running
|
||||||
|
// the installation process and release it when finished.
|
||||||
|
sem *semaphore.Weighted
|
||||||
|
}
|
||||||
|
|
||||||
// The build settings for a given server that impact docker container creation and
|
// The build settings for a given server that impact docker container creation and
|
||||||
// resource limits for a server instance.
|
// resource limits for a server instance.
|
||||||
type BuildSettings struct {
|
type BuildSettings struct {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user