Detailed changes
@@ -106,3 +106,22 @@ func (c *Client) VersionInfo() (*proto.VersionInfo, error) {
}
return &vi, nil
}
+
+// ShutdownServer sends a shutdown request to the server.
+func (c *Client) ShutdownServer() error {
+ req, err := http.NewRequest("POST", "http://localhost/v1/control", jsonBody(proto.ServerControl{
+ Command: "shutdown",
+ }))
+ if err != nil {
+ return err
+ }
+ rsp, err := c.h.Do(req)
+ if err != nil {
+ return err
+ }
+ defer rsp.Body.Close()
+ if rsp.StatusCode != http.StatusOK {
+ return fmt.Errorf("server shutdown failed: %s", rsp.Status)
+ }
+ return nil
+}
@@ -60,7 +60,7 @@ var serverCmd = &cobra.Command{
select {
case <-sigch:
slog.Info("Received interrupt signal...")
- case err := <-errch:
+ case err = <-errch:
if err != nil && !errors.Is(err, server.ErrServerClosed) {
_ = srv.Close()
slog.Error("Server error", "error", err)
@@ -68,6 +68,10 @@ var serverCmd = &cobra.Command{
}
}
+ if errors.Is(err, server.ErrServerClosed) {
+ return nil
+ }
+
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
defer cancel()
@@ -0,0 +1,5 @@
+package proto
+
+type ServerControl struct {
+ Command string `json:"command"`
+}
@@ -1,6 +1,7 @@
package server
import (
+ "context"
"encoding/json"
"fmt"
"log/slog"
@@ -36,6 +37,29 @@ func (c *controllerV1) handleGetVersion(w http.ResponseWriter, r *http.Request)
})
}
+func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
+ var req proto.ServerControl
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ c.logError(r, "failed to decode request", "error", err)
+ jsonError(w, http.StatusBadRequest, "failed to decode request")
+ return
+ }
+
+ switch req.Command {
+ case "shutdown":
+ go func() {
+ slog.Info("shutting down server...")
+ if err := c.Shutdown(context.Background()); err != nil {
+ c.logError(r, "failed to shutdown server", "error", err)
+ }
+ }()
+ default:
+ c.logError(r, "unknown command", "command", req.Command)
+ jsonError(w, http.StatusBadRequest, "unknown command")
+ return
+ }
+}
+
func (c *controllerV1) handleGetConfig(w http.ResponseWriter, r *http.Request) {
jsonEncode(w, c.cfg)
}
@@ -16,7 +16,7 @@ import (
)
// ErrServerClosed is returned when the server is closed.
-var ErrServerClosed = fmt.Errorf("server closed")
+var ErrServerClosed = http.ErrServerClosed
// InstanceState represents the state of a running [app.App] instance.
type InstanceState uint8
@@ -115,6 +115,7 @@ func NewServer(cfg *config.Config, network, address string) *Server {
mux.HandleFunc("GET /v1/health", c.handleGetHealth)
mux.HandleFunc("GET /v1/version", c.handleGetVersion)
mux.HandleFunc("GET /v1/config", c.handleGetConfig)
+ mux.HandleFunc("POST /v1/control", c.handlePostControl)
mux.HandleFunc("GET /v1/instances", c.handleGetInstances)
mux.HandleFunc("POST /v1/instances", c.handlePostInstances)
mux.HandleFunc("DELETE /v1/instances", c.handleDeleteInstances)