This commit is contained in:
@@ -3,14 +3,22 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"nadir/internal/oscmd"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
)
|
||||
|
||||
// selfUnit is nadir's own systemd unit name. Acting on it via the normal
|
||||
// synchronous path would have systemd SIGTERM the very process serving the
|
||||
// request, so the client sees a dropped connection / 500 even though the
|
||||
// action succeeded. We detach those calls into a Setsid subprocess instead.
|
||||
const selfUnit = "nadir"
|
||||
|
||||
const tagServices = "Services"
|
||||
|
||||
var (
|
||||
@@ -136,6 +144,9 @@ func registerServices(api huma.API) {
|
||||
if err := ensureExists(in.Unit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isSelf(in.Unit) {
|
||||
return runDetached(c.action, in.Unit)
|
||||
}
|
||||
if _, err := oscmd.Run("systemctl", c.action, "--", in.Unit); err != nil {
|
||||
return nil, huma.Error500InternalServerError("systemctl "+c.action+" failed", err)
|
||||
}
|
||||
@@ -144,6 +155,27 @@ func registerServices(api huma.API) {
|
||||
}
|
||||
}
|
||||
|
||||
// isSelf reports whether unit names nadir's own service, with or without the
|
||||
// .service suffix.
|
||||
func isSelf(unit string) bool {
|
||||
return unit == selfUnit || unit == selfUnit+".service"
|
||||
}
|
||||
|
||||
// runDetached fires systemctl in a new session so a "systemctl restart nadir"
|
||||
// (or stop) doesn't kill its own caller before the HTTP response is written.
|
||||
// Returns success once the subprocess has *started* — the actual systemd
|
||||
// operation may complete after the response is sent, which is the whole point.
|
||||
func runDetached(action, unit string) (*oscmd.StatusOutput, error) {
|
||||
cmd := exec.Command("systemctl", action, "--", unit)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, huma.Error500InternalServerError("could not start detached systemctl", err)
|
||||
}
|
||||
// Reap in the background so the child doesn't become a zombie.
|
||||
go cmd.Wait()
|
||||
return oscmd.OK(), nil
|
||||
}
|
||||
|
||||
// validateUnit guards against empty, flag-like, or malformed unit names.
|
||||
func validateUnit(unit string) error {
|
||||
if unit == "" || strings.HasPrefix(unit, "-") || !unitNameRe.MatchString(unit) {
|
||||
|
||||
@@ -19,3 +19,22 @@ func TestValidateUnit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsSelf pins the dispatch that detaches stop/restart-of-self into a
|
||||
// Setsid subprocess. Both "nadir" and "nadir.service" must match; anything
|
||||
// else (including substrings) must not, or unrelated services would also get
|
||||
// detached and bypass the synchronous error path.
|
||||
func TestIsSelf(t *testing.T) {
|
||||
yes := []string{"nadir", "nadir.service"}
|
||||
for _, u := range yes {
|
||||
if !isSelf(u) {
|
||||
t.Errorf("isSelf(%q) = false, want true", u)
|
||||
}
|
||||
}
|
||||
no := []string{"", "sshd.service", "nadir-something.service", "nadir.timer", "not-nadir.service"}
|
||||
for _, u := range no {
|
||||
if isSelf(u) {
|
||||
t.Errorf("isSelf(%q) = true, want false", u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user