diff --git a/http.go b/http.go index 7cc4cd9..f6e7c09 100644 --- a/http.go +++ b/http.go @@ -424,7 +424,7 @@ func (rt *Router) routeServerInstall(w http.ResponseWriter, r *http.Request, ps s := rt.GetServer(ps.ByName("server")) defer r.Body.Close() - go func (serv *server.Server) { + go func(serv *server.Server) { if err := serv.Install(); err != nil { zap.S().Errorw("failed to execute server installation process", zap.String("server", s.Uuid), zap.Error(err)) } @@ -479,6 +479,20 @@ func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, ps h w.WriteHeader(http.StatusAccepted) } +func (rt *Router) routeServerReinstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + s := rt.GetServer(ps.ByName("server")) + defer r.Body.Close() + + zap.S().Infow("beginning reinstall process for server", zap.String("server", s.Uuid)) + go func(server *server.Server) { + if err := server.Reinstall(); err != nil { + zap.S().Errorw("failed to complete server reinstall process", zap.String("server", server.Uuid), zap.Error(err)) + } + }(s) + + w.WriteHeader(http.StatusAccepted) +} + func (rt *Router) routeSystemInformation(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { defer r.Body.Close() @@ -573,6 +587,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router { router.POST("/api/servers/:server/files/delete", rt.AuthenticateRequest(rt.routeServerDeleteFile)) router.POST("/api/servers/:server/power", rt.AuthenticateRequest(rt.routeServerPower)) router.POST("/api/servers/:server/commands", rt.AuthenticateRequest(rt.routeServerSendCommand)) + router.POST("/api/servers/:server/reinstall", rt.AuthenticateRequest(rt.routeServerReinstall)) router.PATCH("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerUpdate)) router.DELETE("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerDelete)) diff --git a/server/environment.go b/server/environment.go index 18f2a9b..4573fdd 100644 --- a/server/environment.go +++ b/server/environment.go @@ -31,6 +31,11 @@ type Environment interface { // not be returned. Stop() error + // Waits for a server instance to stop gracefully. If the server is still detected + // as running after seconds, an error will be returned, or the server will be terminated + // depending on the value of the second argument. + WaitForStop(seconds int, terminate bool) error + // Determines if the server instance exists. For example, in a docker environment // this should confirm that the container is created and in a bootable state. In // a basic CLI environment this can probably just return true right away. diff --git a/server/environment_docker.go b/server/environment_docker.go index 76c19fe..7f9e91e 100644 --- a/server/environment_docker.go +++ b/server/environment_docker.go @@ -267,6 +267,44 @@ func (d *DockerEnvironment) Stop() error { return d.Client.ContainerStop(context.Background(), d.Server.Uuid, &t) } +// Attempts to gracefully stop a server using the defined stop command. If the server +// does not stop after seconds have passed, an error will be returned, or the instance +// will be terminated forcefully depending on the value of the second argument. +func (d *DockerEnvironment) WaitForStop(seconds int, terminate bool) error { + if d.Server.State == ProcessOfflineState { + return nil + } + + if err := d.Stop(); err != nil { + return errors.WithStack(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second) + defer cancel() + + // Block the return of this function until the container as been marked as no + // longer running. If this wait does not end by the time seconds have passed, + // attempt to terminate the container, or return an error. + ok, errChan := d.Client.ContainerWait(ctx, d.Server.Uuid, container.WaitConditionNotRunning) + select { + case <-ctx.Done(): + if ctxErr := ctx.Err(); ctxErr != nil { + if terminate { + return d.Terminate(os.Kill) + } + + return errors.WithStack(ctxErr) + } + case err := <-errChan: + if err != nil { + return errors.WithStack(err) + } + case <-ok: + } + + return nil +} + // Forcefully terminates the container using the signal passed through. func (d *DockerEnvironment) Terminate(signal os.Signal) error { ctx := context.Background() diff --git a/server/install.go b/server/install.go index 0f250b7..dd1206e 100644 --- a/server/install.go +++ b/server/install.go @@ -36,6 +36,19 @@ func (s *Server) Install() error { return err } +// Reinstalls a server's software by utilizing the install script for the server egg. This +// does not touch any existing files for the server, other than what the script modifies. +func (s *Server) Reinstall() error { + if s.State != ProcessOfflineState { + zap.S().Debugw("waiting for server instance to enter a stopped state", zap.String("server", s.Uuid)) + if err := s.Environment.WaitForStop(10, true); err != nil { + return err + } + } + + return s.Install() +} + // Internal installation function used to simplify reporting back to the Panel. func (s *Server) internalInstall() error { script, rerr, err := api.NewRequester().GetInstallationScript(s.Uuid)