Files
sproutclaw-web/backend/internal/handlers/restart.go
shumengya bd2251e89a fix: restart backend under systemd and use .sproutclaw agent dir
Restart via systemctl when running as a service, set SPROUTCLAW_CODING_AGENT_DIR
for the RPC subprocess, and add a frontend poll timeout for failed restarts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:29:28 +08:00

100 lines
2.1 KiB
Go

package handlers
import (
"log"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/gin-gonic/gin"
)
const defaultSystemdUnit = "sproutclaw-web.service"
func handleRestart() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
go func() {
time.Sleep(300 * time.Millisecond)
if unit, underSystemd := systemdServiceUnit(); underSystemd {
log.Printf("[restart] restarting via systemctl: %s", unit)
cmd := exec.Command("systemctl", "restart", unit)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("[restart] systemctl restart failed: %v: %s", err, strings.TrimSpace(string(out)))
}
return
}
restartByReexec()
}()
}
}
func restartByReexec() {
exe, err := os.Executable()
if err != nil {
log.Printf("[restart] get executable: %v", err)
os.Exit(1)
}
cmd := exec.Command(exe, os.Args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Printf("[restart] start new process: %v", err)
os.Exit(1)
}
os.Exit(0)
}
func systemdServiceUnit() (string, bool) {
if runtime.GOOS != "linux" {
return "", false
}
if os.Getenv("INVOCATION_ID") == "" && !inSystemdCgroup() {
return "", false
}
if unit := strings.TrimSpace(os.Getenv("SPROUTCLAW_WEB_SYSTEMD_UNIT")); unit != "" {
return normalizeSystemdUnit(unit), true
}
if unit := readSystemdUnitFromCgroup(); unit != "" {
return unit, true
}
return defaultSystemdUnit, true
}
func normalizeSystemdUnit(name string) string {
if strings.HasSuffix(name, ".service") {
return name
}
return name + ".service"
}
func inSystemdCgroup() bool {
data, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
return false
}
return strings.Contains(string(data), ".service")
}
func readSystemdUnitFromCgroup() string {
data, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
idx := strings.LastIndex(line, "/")
if idx < 0 {
continue
}
name := line[idx+1:]
if strings.HasSuffix(name, ".service") {
return name
}
}
return ""
}