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>
100 lines
2.1 KiB
Go
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 ""
|
|
}
|