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 {
|
func New() *Config {
|
||||||
port := flag.Int("port", 19133, "HTTP port")
|
port := flag.Int("port", 19133, "HTTP port")
|
||||||
agentDir := flag.String("agent-dir", "", "pi agent directory (e.g. /path/to/.pi/agent)")
|
agentDir := flag.String("agent-dir", "", "SproutClaw agent directory (e.g. /path/to/.sproutclaw/agent)")
|
||||||
repoRoot := flag.String("repo-root", "", "pi repo root (pi CLI cwd); defaults to two levels above agent-dir")
|
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)")
|
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\")")
|
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)")
|
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 == "" {
|
if *agentDir == "" {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
*agentDir = filepath.Join(cwd, ".pi", "agent")
|
*agentDir = filepath.Join(cwd, ".sproutclaw", "agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
@@ -50,7 +50,7 @@ func New() *Config {
|
|||||||
RepoRoot: *repoRoot,
|
RepoRoot: *repoRoot,
|
||||||
DataDir: *dataDir,
|
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 == "" {
|
if cfg.RepoRoot == "" {
|
||||||
cfg.RepoRoot = filepath.Dir(filepath.Dir(*agentDir))
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -37,28 +35,6 @@ func RegisterSettings(r *gin.RouterGroup, cfg *config.Config, database *db.DB, p
|
|||||||
|
|
||||||
var processStartTime = time.Now()
|
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 {
|
func handleGetSettings(cfg *config.Config, database *db.DB) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
systemPrompt, _ := services.ReadSystemPrompt(cfg.SystemPromptFile)
|
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")
|
args := append(append([]string{}, piArgs...), "--mode", "rpc")
|
||||||
cmd := exec.Command(piCmd, args...)
|
cmd := exec.Command(piCmd, args...)
|
||||||
cmd.Dir = repoRoot
|
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)
|
log.Printf("[sproutclaw-web] launching pi RPC: %s %s (cwd=%s)", piCmd, strings.Join(args, " "), repoRoot)
|
||||||
|
|
||||||
|
|||||||
11
build.sh
Executable file
11
build.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
OUT="$SCRIPT_DIR/backend/dist/sproutclaw-web-backend"
|
||||||
|
|
||||||
|
echo "[build] compiling Go backend -> $OUT"
|
||||||
|
cd "$SCRIPT_DIR/backend"
|
||||||
|
/usr/local/go/bin/go build -o "$OUT" ./cmd/server
|
||||||
|
chmod +x "$OUT"
|
||||||
|
echo "[build] done: $OUT"
|
||||||
@@ -100,7 +100,7 @@ function extensionCategory(extension: ExtensionInfo): "local" | "npm" {
|
|||||||
}
|
}
|
||||||
if (extension.source?.startsWith("npm:")) return "npm";
|
if (extension.source?.startsWith("npm:")) return "npm";
|
||||||
const pathHint = `${extension.path || ""} ${extension.location || ""}`.replace(/\\/g, "/");
|
const pathHint = `${extension.path || ""} ${extension.location || ""}`.replace(/\\/g, "/");
|
||||||
if (pathHint.includes("/.pi/agent/npm/node_modules/")) return "npm";
|
if (pathHint.includes("/.sproutclaw/agent/npm/node_modules/")) return "npm";
|
||||||
return "local";
|
return "local";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,8 +701,13 @@ export function SettingsPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
// 后端重启时连接断开属正常现象,忽略错误
|
// 后端重启时连接断开属正常现象,忽略错误
|
||||||
}
|
}
|
||||||
// 轮询直到后端恢复
|
const deadline = Date.now() + 60_000;
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
setRestarting(false);
|
||||||
|
setStatus("后端重启超时,请检查 sproutclaw-web 服务状态", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await settingsApi.fetchSettings();
|
await settingsApi.fetchSettings();
|
||||||
setStatus("后端已重启,正在刷新...", "ok");
|
setStatus("后端已重启,正在刷新...", "ok");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ rem -------------------------------------------------------
|
|||||||
|
|
||||||
cd /d "%~dp0backend"
|
cd /d "%~dp0backend"
|
||||||
echo Starting Go backend on port %BACKEND_PORT%...
|
echo Starting Go backend on port %BACKEND_PORT%...
|
||||||
go run ./cmd/server --port %BACKEND_PORT% --agent-dir "%SPROUTCLAW_DIR%\.pi\agent" --pi-cmd "node %SPROUTCLAW_DIR%\packages\coding-agent\src\cli.ts"
|
go run ./cmd/server --port %BACKEND_PORT% --agent-dir "%SPROUTCLAW_DIR%\.sproutclaw\agent" --pi-cmd "node %SPROUTCLAW_DIR%\packages\coding-agent\src\cli.ts"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
pause >nul
|
pause >nul
|
||||||
|
|||||||
Reference in New Issue
Block a user