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>
This commit is contained in:
@@ -32,8 +32,8 @@ type Config struct {
|
||||
|
||||
func New() *Config {
|
||||
port := flag.Int("port", 19133, "HTTP port")
|
||||
agentDir := flag.String("agent-dir", "", "pi agent directory (e.g. /path/to/.pi/agent)")
|
||||
repoRoot := flag.String("repo-root", "", "pi repo root (pi CLI cwd); defaults to two levels above agent-dir")
|
||||
agentDir := flag.String("agent-dir", "", "SproutClaw agent directory (e.g. /path/to/.sproutclaw/agent)")
|
||||
repoRoot := flag.String("repo-root", "", "SproutClaw repo root (CLI cwd); defaults to two levels above agent-dir")
|
||||
dataDir := flag.String("data-dir", "", "sproutclaw-web data dir for SQLite (defaults to ./data)")
|
||||
piCmd := flag.String("pi-cmd", "", "pi CLI command (may include args, e.g. \"node /path/tsx.mjs /path/cli.ts\")")
|
||||
frontendDist := flag.String("frontend-dist", "", "path to frontend/dist (defaults to ../frontend/dist relative to binary)")
|
||||
@@ -41,7 +41,7 @@ func New() *Config {
|
||||
|
||||
if *agentDir == "" {
|
||||
cwd, _ := os.Getwd()
|
||||
*agentDir = filepath.Join(cwd, ".pi", "agent")
|
||||
*agentDir = filepath.Join(cwd, ".sproutclaw", "agent")
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
@@ -50,7 +50,7 @@ func New() *Config {
|
||||
RepoRoot: *repoRoot,
|
||||
DataDir: *dataDir,
|
||||
}
|
||||
// repoRoot defaults to the directory two levels above agentDir (.pi/agent -> repo root)
|
||||
// repoRoot defaults to the directory two levels above agentDir (.sproutclaw/agent -> repo root)
|
||||
if cfg.RepoRoot == "" {
|
||||
cfg.RepoRoot = filepath.Dir(filepath.Dir(*agentDir))
|
||||
}
|
||||
|
||||
99
backend/internal/handlers/restart.go
Normal file
99
backend/internal/handlers/restart.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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 ""
|
||||
}
|
||||
@@ -2,10 +2,8 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -37,28 +35,6 @@ func RegisterSettings(r *gin.RouterGroup, cfg *config.Config, database *db.DB, p
|
||||
|
||||
var processStartTime = time.Now()
|
||||
|
||||
func handleRestart() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
go func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
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 handleGetSettings(cfg *config.Config, database *db.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
systemPrompt, _ := services.ReadSystemPrompt(cfg.SystemPromptFile)
|
||||
|
||||
@@ -80,7 +80,7 @@ func NewClient(piCmd string, piArgs []string, repoRoot, agentDir string, onExit
|
||||
args := append(append([]string{}, piArgs...), "--mode", "rpc")
|
||||
cmd := exec.Command(piCmd, args...)
|
||||
cmd.Dir = repoRoot
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PI_CODING_AGENT_DIR=%s", agentDir))
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SPROUTCLAW_CODING_AGENT_DIR=%s", agentDir))
|
||||
|
||||
log.Printf("[sproutclaw-web] launching pi RPC: %s %s (cwd=%s)", piCmd, strings.Join(args, " "), repoRoot)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user