feat: update dashboard and process manager for unified launcher
Actualiza el dashboard TUI y el process manager para el modelo de launcher unificado donde todos los agentes corren en un solo proceso. Dashboard (pkg/tui): - model.go: campos de estado del launcher (PID, uptime, memory, CPU, log size) - model.go: ServerMenuOptions(running) contextual, AgentActionOptions(enabled) - messages.go: MsgAgentsLoaded incluye estado del launcher, MsgServerActionDone/MsgRebuildDone simplificados - update.go: intents nuevos (Enable/Disable agent, Start/Stop/Restart/Kill launcher) - view.go: vista de servidor muestra stats del launcher, agentes muestran enabled/disabled Shell adapter (shell/tui): - adapter.go: reescrito para usar métodos unificados (StartUnified, StopUnified, ToggleEnabled, StatusAllUnified, UnifiedStats, UnifiedLogTail) Process manager (shell/process): - manager.go: métodos StartUnified, StopUnified, KillUnified, IsUnifiedRunning, UnifiedPID, UnifiedStats, UnifiedLogTail, StatusAllUnified, ToggleEnabled Los agentes ya no se inician/detienen individualmente desde el dashboard. Se habilitan/deshabilitan en config y se reinicia el launcher para aplicar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,8 @@ func (osProber) isAlive(pid int) bool {
|
||||
return syscall.Kill(pid, 0) == nil
|
||||
}
|
||||
|
||||
const unifiedID = "launcher" // PID/log file ID for the unified launcher
|
||||
|
||||
// Manager handles agent process lifecycle.
|
||||
type Manager struct {
|
||||
runDir string
|
||||
@@ -388,6 +390,184 @@ func (m *Manager) Build() (string, error) {
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// ── Unified launcher ─────────────────────────────────────────────────────
|
||||
// The unified launcher runs ALL enabled agents + orchestrator in a single
|
||||
// process. PID → run/launcher.pid, log → run/launcher.log.
|
||||
|
||||
// StartUnified launches the unified launcher (no -c flag → discovers all agents).
|
||||
func (m *Manager) StartUnified() error {
|
||||
if m.IsUnifiedRunning() {
|
||||
return fmt.Errorf("unified launcher is already running (PID %d)", m.readPID(unifiedID))
|
||||
}
|
||||
if err := os.MkdirAll(m.runDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create run dir: %w", err)
|
||||
}
|
||||
|
||||
logFile, err := os.OpenFile(m.logPath(unifiedID), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open log: %w", err)
|
||||
}
|
||||
|
||||
bin := m.resolvedBin()
|
||||
var cmd *exec.Cmd
|
||||
if strings.HasPrefix(bin, "go run") {
|
||||
cmd = exec.Command("go", "run", "-tags", "goolm", "./cmd/launcher", "--log-level", "info")
|
||||
} else {
|
||||
cmd = exec.Command(bin, "--log-level", "info")
|
||||
}
|
||||
|
||||
cmd.Env = m.buildEnv()
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return fmt.Errorf("exec: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(m.pidPath(unifiedID), []byte(strconv.Itoa(cmd.Process.Pid)), 0o644); err != nil {
|
||||
return fmt.Errorf("write PID: %w", err)
|
||||
}
|
||||
|
||||
go func() { _ = cmd.Wait() }()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopUnified stops the unified launcher process.
|
||||
func (m *Manager) StopUnified() error {
|
||||
return m.Stop(unifiedID)
|
||||
}
|
||||
|
||||
// KillUnified sends SIGKILL to the unified launcher.
|
||||
func (m *Manager) KillUnified() error {
|
||||
return m.Kill(unifiedID)
|
||||
}
|
||||
|
||||
// IsUnifiedRunning checks if the unified launcher is alive.
|
||||
func (m *Manager) IsUnifiedRunning() bool {
|
||||
pid := m.readPID(unifiedID)
|
||||
if pid > 0 && m.isAlive(pid) {
|
||||
return true
|
||||
}
|
||||
// Fallback: search for launcher running without -c flag
|
||||
pids := m.findUnifiedPIDs()
|
||||
return len(pids) > 0
|
||||
}
|
||||
|
||||
// UnifiedPID returns the PID of the running unified launcher, or 0.
|
||||
func (m *Manager) UnifiedPID() int {
|
||||
pid := m.readPID(unifiedID)
|
||||
if pid > 0 && m.isAlive(pid) {
|
||||
return pid
|
||||
}
|
||||
pids := m.findUnifiedPIDs()
|
||||
if len(pids) > 0 {
|
||||
// Repair PID file
|
||||
_ = os.WriteFile(m.pidPath(unifiedID), []byte(strconv.Itoa(pids[0])), 0o644)
|
||||
return pids[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// UnifiedStats returns resource usage for the unified launcher process.
|
||||
func (m *Manager) UnifiedStats() (ProcessStats, error) {
|
||||
pid := m.UnifiedPID()
|
||||
if pid == 0 {
|
||||
return ProcessStats{}, fmt.Errorf("unified launcher is not running")
|
||||
}
|
||||
return m.statsForPID(pid, unifiedID), nil
|
||||
}
|
||||
|
||||
// UnifiedLogTail returns the last N lines of the unified launcher log.
|
||||
func (m *Manager) UnifiedLogTail(lines int) ([]string, error) {
|
||||
return m.LogTail(unifiedID, lines)
|
||||
}
|
||||
|
||||
// StatusAllUnified returns status for all agents, deriving "running" from
|
||||
// whether the unified launcher is running + the agent is enabled.
|
||||
func (m *Manager) StatusAllUnified() ([]AgentStatus, error) {
|
||||
agents, err := m.Scan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
launcherRunning := m.IsUnifiedRunning()
|
||||
launcherPID := m.UnifiedPID()
|
||||
|
||||
statuses := make([]AgentStatus, len(agents))
|
||||
for i, a := range agents {
|
||||
running := launcherRunning && a.Enabled
|
||||
pid := 0
|
||||
instances := 0
|
||||
if running {
|
||||
pid = launcherPID
|
||||
instances = 1
|
||||
}
|
||||
statuses[i] = AgentStatus{
|
||||
AgentInfo: a,
|
||||
Running: running,
|
||||
PID: pid,
|
||||
Instances: instances,
|
||||
}
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// ToggleEnabled sets the enabled field in an agent's config.yaml.
|
||||
func (m *Manager) ToggleEnabled(id string, enabled bool) error {
|
||||
agents, err := m.Scan()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range agents {
|
||||
if a.ID == id {
|
||||
return m.setEnabledInConfig(a.ConfigPath, enabled)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("agent %q not found", id)
|
||||
}
|
||||
|
||||
// setEnabledInConfig rewrites the enabled field in a config.yaml.
|
||||
func (m *Manager) setEnabledInConfig(path string, enabled bool) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val := "false"
|
||||
if enabled {
|
||||
val = "true"
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "enabled:") {
|
||||
// Preserve indentation
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
lines[i] = indent + "enabled: " + val
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644)
|
||||
}
|
||||
|
||||
// findUnifiedPIDs finds launcher processes running without -c flag.
|
||||
func (m *Manager) findUnifiedPIDs() []int {
|
||||
// Search for launcher processes that do NOT have -c flag
|
||||
raw := m.prober.pgrepPIDs("launcher.*--log-level")
|
||||
var pids []int
|
||||
for _, p := range raw {
|
||||
comm := m.prober.processComm(p)
|
||||
if comm == "go" {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, p)
|
||||
}
|
||||
return pids
|
||||
}
|
||||
|
||||
// ── internal helpers ─────────────────────────────────────────────────────
|
||||
|
||||
func (m *Manager) pidPath(id string) string { return filepath.Join(m.runDir, id+".pid") }
|
||||
|
||||
Reference in New Issue
Block a user