mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 14:53:42 +08:00
fix: restart the active systemd unit after update (#36)
Co-authored-by: Meng Meng <[email protected]>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectSystemdUnitUsesExplicitOverride(t *testing.T) {
|
||||
t.Setenv("VOCAT_SYSTEMD_UNIT", "vocat-test.service")
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
if got := detectSystemdUnit(logger); got != "vocat-test.service" {
|
||||
t.Fatalf("detectSystemdUnit() = %q, want vocat-test.service", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemdUnitFromCgroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
want string
|
||||
}{
|
||||
{name: "cgroup v2", data: "0::/system.slice/vocat-test.service\n", want: "vocat-test.service"},
|
||||
{name: "legacy", data: "1:name=systemd:/system.slice/[email protected]\n", want: "[email protected]"},
|
||||
{name: "no service", data: "0::/user.slice/user-1000.slice/session-1.scope\n", want: ""},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := systemdUnitFromCgroup(test.data); got != test.want {
|
||||
t.Fatalf("systemdUnitFromCgroup() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidSystemdUnitRejectsArgumentsAndPaths(t *testing.T) {
|
||||
for _, value := range []string{"vocat", "../vocat.service", "vocat.service --now", "vocat.service/other"} {
|
||||
if validSystemdUnit.MatchString(value) {
|
||||
t.Fatalf("validSystemdUnit unexpectedly accepted %q", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -320,17 +322,63 @@ func RestartService(logger *slog.Logger) error {
|
||||
if _, err := exec.LookPath("systemctl"); err != nil {
|
||||
return fmt.Errorf("neither /etc/init.d/vocat nor systemctl is available")
|
||||
}
|
||||
unit := detectSystemdUnit(logger)
|
||||
// Queue the restart and let systemctl exit before systemd stops this unit.
|
||||
// A blocking restart command becomes part of vocat.service's own cgroup and
|
||||
// waits for that same cgroup to terminate, creating a stop-timeout cycle.
|
||||
cmd := exec.Command("systemctl", "restart", "--no-block", "vocat")
|
||||
cmd := exec.Command("systemctl", "restart", "--no-block", unit)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
logger.Warn("systemctl restart failed", "error", err, "output", string(out))
|
||||
return fmt.Errorf("systemctl restart vocat: %w", err)
|
||||
return fmt.Errorf("systemctl restart %s: %w", unit, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var validSystemdUnit = regexp.MustCompile(`^[A-Za-z0-9_.@:-]+\.service$`)
|
||||
|
||||
func detectSystemdUnit(logger *slog.Logger) string {
|
||||
if configured := strings.TrimSpace(os.Getenv("VOCAT_SYSTEMD_UNIT")); validSystemdUnit.MatchString(configured) {
|
||||
return configured
|
||||
}
|
||||
if data, err := os.ReadFile("/proc/self/cgroup"); err == nil {
|
||||
if unit := systemdUnitFromCgroup(string(data)); unit != "" {
|
||||
return unit
|
||||
}
|
||||
}
|
||||
// Some cgroup namespaces hide the unit name. Query loaded services and
|
||||
// identify the unit whose MainPID is this process before falling back.
|
||||
list := exec.Command("systemctl", "list-units", "--type=service", "--all", "--no-legend", "--plain")
|
||||
if output, err := list.Output(); err == nil {
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 || !validSystemdUnit.MatchString(fields[0]) {
|
||||
continue
|
||||
}
|
||||
show := exec.Command("systemctl", "show", fields[0], "--property=MainPID", "--value")
|
||||
if value, showErr := show.Output(); showErr == nil && strings.TrimSpace(string(value)) == pid {
|
||||
return fields[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Warn("could not identify the current systemd unit; using vocat.service", "hint", "set VOCAT_SYSTEMD_UNIT for a custom unit")
|
||||
}
|
||||
return "vocat.service"
|
||||
}
|
||||
|
||||
func systemdUnitFromCgroup(data string) string {
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
for _, part := range strings.Split(line, "/") {
|
||||
part = strings.TrimSpace(part)
|
||||
if validSystemdUnit.MatchString(part) {
|
||||
return part
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// resolveDefaultTarget returns the conventional install path when present,
|
||||
// falling back to the running executable. This lets `vocat update` "just work"
|
||||
// on the standard systemd host without flags.
|
||||
|
||||
Reference in New Issue
Block a user