No One Can Stop Me Now
This commit is contained in:
380
services/server-manager/internal/tui/view_service.go
Normal file
380
services/server-manager/internal/tui/view_service.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// ── Service Definitions ─────────────────────────────────────────────
|
||||
|
||||
type serviceInfo struct {
|
||||
Name string
|
||||
Unit string // systemd unit name
|
||||
Desc string
|
||||
Binary string // path to check
|
||||
}
|
||||
|
||||
var managedServices = []serviceInfo{
|
||||
{"AUTARCH Web", "autarch-web", "Web dashboard (Flask)", "autarch_web.py"},
|
||||
{"AUTARCH DNS", "autarch-dns", "DNS server (Go)", "autarch-dns"},
|
||||
{"AUTARCH Autonomy", "autarch-autonomy", "Autonomous AI daemon", ""},
|
||||
}
|
||||
|
||||
// ── Rendering ───────────────────────────────────────────────────────
|
||||
|
||||
func (a App) renderServiceMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(styleTitle.Render("SERVICE MANAGEMENT"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(a.renderHR())
|
||||
b.WriteString("\n")
|
||||
|
||||
// Show service statuses — checks both systemd and raw processes
|
||||
svcChecks := []struct {
|
||||
Info serviceInfo
|
||||
Process string // process name to pgrep for
|
||||
}{
|
||||
{managedServices[0], "autarch_web.py"},
|
||||
{managedServices[1], "autarch-dns"},
|
||||
{managedServices[2], "autonomy"},
|
||||
}
|
||||
|
||||
for _, sc := range svcChecks {
|
||||
status, running := getProcessStatus(sc.Info.Unit, sc.Process)
|
||||
|
||||
indicator := styleStatusOK.Render("● running")
|
||||
if !running {
|
||||
indicator = styleStatusBad.Render("○ stopped")
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
||||
indicator,
|
||||
lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(sc.Info.Name),
|
||||
))
|
||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
||||
styleDim.Render(sc.Info.Desc),
|
||||
styleDim.Render("("+status+")"),
|
||||
))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(a.renderHR())
|
||||
b.WriteString("\n")
|
||||
b.WriteString(styleKey.Render(" [1]") + " Start/Stop AUTARCH Web\n")
|
||||
b.WriteString(styleKey.Render(" [2]") + " Start/Stop AUTARCH DNS\n")
|
||||
b.WriteString(styleKey.Render(" [3]") + " Start/Stop Autonomy Daemon\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(styleKey.Render(" [r]") + " Restart all running services\n")
|
||||
b.WriteString(styleKey.Render(" [e]") + " Enable all services on boot\n")
|
||||
b.WriteString(styleKey.Render(" [i]") + " Install/update systemd unit files\n")
|
||||
b.WriteString(styleKey.Render(" [l]") + " View service logs (journalctl)\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(styleDim.Render(" esc back"))
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Key Handling ────────────────────────────────────────────────────
|
||||
|
||||
func (a App) handleServiceMenu(key string) (tea.Model, tea.Cmd) {
|
||||
switch key {
|
||||
case "1":
|
||||
return a.toggleService(0)
|
||||
case "2":
|
||||
return a.toggleService(1)
|
||||
case "3":
|
||||
return a.toggleService(2)
|
||||
case "r":
|
||||
return a.restartAllServices()
|
||||
case "e":
|
||||
return a.enableAllServices()
|
||||
case "i":
|
||||
return a.installServiceUnits()
|
||||
case "l":
|
||||
return a.viewServiceLogs()
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ── Commands ────────────────────────────────────────────────────────
|
||||
|
||||
func (a App) loadServiceStatus() tea.Cmd {
|
||||
return nil // Services are checked live in render
|
||||
}
|
||||
|
||||
func getServiceStatus(unit string) (string, bool) {
|
||||
out, err := exec.Command("systemctl", "is-active", unit).Output()
|
||||
status := strings.TrimSpace(string(out))
|
||||
if err != nil || status != "active" {
|
||||
// Check if unit exists
|
||||
_, existErr := exec.Command("systemctl", "cat", unit).Output()
|
||||
if existErr != nil {
|
||||
return "not installed", false
|
||||
}
|
||||
return status, false
|
||||
}
|
||||
return status, true
|
||||
}
|
||||
|
||||
// getProcessStatus checks both systemd and direct process for a service.
|
||||
// Returns (status description, isRunning).
|
||||
func getProcessStatus(unitName, processName string) (string, bool) {
|
||||
// First try systemd
|
||||
status, running := getServiceStatus(unitName)
|
||||
if running {
|
||||
return "systemd: " + status, true
|
||||
}
|
||||
|
||||
// Fall back to process detection (pgrep)
|
||||
out, err := exec.Command("pgrep", "-f", processName).Output()
|
||||
if err == nil && strings.TrimSpace(string(out)) != "" {
|
||||
pids := strings.Fields(strings.TrimSpace(string(out)))
|
||||
return fmt.Sprintf("running (PID %s)", pids[0]), true
|
||||
}
|
||||
|
||||
return status, false
|
||||
}
|
||||
|
||||
func (a App) toggleService(idx int) (App, tea.Cmd) {
|
||||
if idx < 0 || idx >= len(managedServices) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
svc := managedServices[idx]
|
||||
processNames := []string{"autarch_web.py", "autarch-dns", "autonomy"}
|
||||
procName := processNames[idx]
|
||||
|
||||
_, sysRunning := getServiceStatus(svc.Unit)
|
||||
_, procRunning := getProcessStatus(svc.Unit, procName)
|
||||
isRunning := sysRunning || procRunning
|
||||
|
||||
return a, func() tea.Msg {
|
||||
dir := findAutarchDir()
|
||||
|
||||
if isRunning {
|
||||
// Stop — try systemd first, then kill process
|
||||
if sysRunning {
|
||||
cmd := exec.Command("systemctl", "stop", svc.Unit)
|
||||
cmd.CombinedOutput()
|
||||
}
|
||||
// Also kill any direct processes
|
||||
exec.Command("pkill", "-f", procName).Run()
|
||||
return ResultMsg{
|
||||
Title: "Service " + svc.Name,
|
||||
Lines: []string{svc.Name + " stopped."},
|
||||
}
|
||||
}
|
||||
|
||||
// Start — try systemd first, fall back to direct launch
|
||||
if _, err := exec.Command("systemctl", "cat", svc.Unit).Output(); err == nil {
|
||||
cmd := exec.Command("systemctl", "start", svc.Unit)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ResultMsg{
|
||||
Title: "Service Error",
|
||||
Lines: []string{"systemctl start failed:", string(out), err.Error(), "", "Trying direct launch..."},
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
return ResultMsg{
|
||||
Title: "Service " + svc.Name,
|
||||
Lines: []string{svc.Name + " started via systemd."},
|
||||
}
|
||||
}
|
||||
|
||||
// Direct launch (no systemd unit installed)
|
||||
var startCmd *exec.Cmd
|
||||
switch idx {
|
||||
case 0: // Web
|
||||
venvPy := dir + "/venv/bin/python3"
|
||||
startCmd = exec.Command(venvPy, dir+"/autarch_web.py")
|
||||
case 1: // DNS
|
||||
binary := dir + "/services/dns-server/autarch-dns"
|
||||
configFile := dir + "/data/dns/config.json"
|
||||
startCmd = exec.Command(binary, "--config", configFile)
|
||||
case 2: // Autonomy
|
||||
venvPy := dir + "/venv/bin/python3"
|
||||
startCmd = exec.Command(venvPy, "-c",
|
||||
"import sys; sys.path.insert(0,'"+dir+"'); from core.autonomy import AutonomyDaemon; AutonomyDaemon().run()")
|
||||
}
|
||||
|
||||
if startCmd != nil {
|
||||
startCmd.Dir = dir
|
||||
// Detach process so it survives manager exit
|
||||
startCmd.Stdout = nil
|
||||
startCmd.Stderr = nil
|
||||
if err := startCmd.Start(); err != nil {
|
||||
return ResultMsg{
|
||||
Title: "Service Error",
|
||||
Lines: []string{"Failed to start " + svc.Name + ":", err.Error()},
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
// Release so it runs independently
|
||||
go startCmd.Wait()
|
||||
|
||||
return ResultMsg{
|
||||
Title: "Service " + svc.Name,
|
||||
Lines: []string{
|
||||
svc.Name + " started directly (PID " + fmt.Sprintf("%d", startCmd.Process.Pid) + ").",
|
||||
"",
|
||||
styleDim.Render("Tip: Install systemd units with [i] for persistent service management."),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return ResultMsg{
|
||||
Title: "Error",
|
||||
Lines: []string{"No start method available for " + svc.Name},
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) restartAllServices() (App, tea.Cmd) {
|
||||
return a, func() tea.Msg {
|
||||
var lines []string
|
||||
for _, svc := range managedServices {
|
||||
_, running := getServiceStatus(svc.Unit)
|
||||
if running {
|
||||
cmd := exec.Command("systemctl", "restart", svc.Unit)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
lines = append(lines, styleError.Render("✘ "+svc.Name+": "+strings.TrimSpace(string(out))))
|
||||
} else {
|
||||
lines = append(lines, styleSuccess.Render("✔ "+svc.Name+": restarted"))
|
||||
}
|
||||
} else {
|
||||
lines = append(lines, styleDim.Render("- "+svc.Name+": not running, skipped"))
|
||||
}
|
||||
}
|
||||
return ResultMsg{Title: "Restart Services", Lines: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) enableAllServices() (App, tea.Cmd) {
|
||||
return a, func() tea.Msg {
|
||||
var lines []string
|
||||
for _, svc := range managedServices {
|
||||
cmd := exec.Command("systemctl", "enable", svc.Unit)
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
lines = append(lines, styleWarning.Render("⚠ "+svc.Name+": could not enable (unit may not exist)"))
|
||||
} else {
|
||||
lines = append(lines, styleSuccess.Render("✔ "+svc.Name+": enabled on boot"))
|
||||
}
|
||||
}
|
||||
return ResultMsg{Title: "Enable Services", Lines: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) installServiceUnits() (App, tea.Cmd) {
|
||||
return a, func() tea.Msg {
|
||||
dir := findAutarchDir()
|
||||
var lines []string
|
||||
|
||||
// Web service unit
|
||||
webUnit := fmt.Sprintf(`[Unit]
|
||||
Description=AUTARCH Web Dashboard
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s/venv/bin/python3 %s/autarch_web.py
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, dir, dir, dir)
|
||||
|
||||
// DNS service unit
|
||||
dnsUnit := fmt.Sprintf(`[Unit]
|
||||
Description=AUTARCH DNS Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s/services/dns-server/autarch-dns --config %s/data/dns/config.json
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, dir, dir, dir)
|
||||
|
||||
// Autonomy daemon unit
|
||||
autoUnit := fmt.Sprintf(`[Unit]
|
||||
Description=AUTARCH Autonomy Daemon
|
||||
After=network.target autarch-web.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s/venv/bin/python3 -c "from core.autonomy import AutonomyDaemon; AutonomyDaemon().run()"
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, dir, dir)
|
||||
|
||||
units := map[string]string{
|
||||
"autarch-web.service": webUnit,
|
||||
"autarch-dns.service": dnsUnit,
|
||||
"autarch-autonomy.service": autoUnit,
|
||||
}
|
||||
|
||||
for name, content := range units {
|
||||
path := "/etc/systemd/system/" + name
|
||||
if err := writeFileAtomic(path, []byte(content)); err != nil {
|
||||
lines = append(lines, styleError.Render("✘ "+name+": "+err.Error()))
|
||||
} else {
|
||||
lines = append(lines, styleSuccess.Render("✔ "+name+": installed"))
|
||||
}
|
||||
}
|
||||
|
||||
// Reload systemd
|
||||
exec.Command("systemctl", "daemon-reload").Run()
|
||||
lines = append(lines, "", styleSuccess.Render("✔ systemctl daemon-reload"))
|
||||
|
||||
return ResultMsg{Title: "Service Units Installed", Lines: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) viewServiceLogs() (App, tea.Cmd) {
|
||||
return a, func() tea.Msg {
|
||||
var lines []string
|
||||
for _, svc := range managedServices {
|
||||
out, _ := exec.Command("journalctl", "-u", svc.Unit, "-n", "10", "--no-pager").Output()
|
||||
lines = append(lines, styleKey.Render("── "+svc.Name+" ──"))
|
||||
logLines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
for _, l := range logLines {
|
||||
lines = append(lines, " "+l)
|
||||
}
|
||||
lines = append(lines, "")
|
||||
}
|
||||
return ResultMsg{Title: "Service Logs (last 10 entries)", Lines: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func writeFileAtomic(path string, data []byte) error {
|
||||
tmp := path + ".tmp"
|
||||
if err := writeFile(tmp, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return renameFile(tmp, path)
|
||||
}
|
||||
Reference in New Issue
Block a user