package infra import ( "database/sql" "fmt" "net" "os/exec" "regexp" "strconv" "strings" "time" _ "github.com/mattn/go-sqlite3" ) // ServiceStatus holds the runtime status of a registered service app. type ServiceStatus struct { AppID string // e.g. "registry_api_go_infra" Name string // e.g. "registry_api" UnitName string // e.g. "registry_api.service" UnitActive string // "active", "inactive", "failed", "not-installed", "unknown" Port int // declared port parsed from notes/description, 0 if none PortListening bool // true if Port > 0 and 127.0.0.1:Port is accepting TCP connections HostMatch string // pc_id from ~/.fn_pc, or "" if unreadable } var portRe = regexp.MustCompile(`\b([1-9][0-9]{3,4})\b`) // ServicesStatus queries registry.db for apps tagged "service" and returns // their current systemd unit state and port reachability. func ServicesStatus(registryRoot string) ([]ServiceStatus, error) { dbPath := registryRoot + "/registry.db" db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&mode=ro") if err != nil { return nil, fmt.Errorf("services_status: open db: %w", err) } defer db.Close() rows, err := db.Query(`SELECT id, name, COALESCE(notes,''), COALESCE(description,'') FROM apps WHERE tags LIKE '%service%'`) if err != nil { return nil, fmt.Errorf("services_status: query: %w", err) } defer rows.Close() pcID, _ := readFnPC() var results []ServiceStatus for rows.Next() { var id, name, notes, description string if err := rows.Scan(&id, &name, ¬es, &description); err != nil { continue } unit := name + ".service" active := queryUnitActive(unit) port := parseFirstPort(notes + " " + description) listening := false if port > 0 { listening = tcpListening("127.0.0.1", port, 500*time.Millisecond) } results = append(results, ServiceStatus{ AppID: id, Name: name, UnitName: unit, UnitActive: active, Port: port, PortListening: listening, HostMatch: pcID, }) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("services_status: rows: %w", err) } return results, nil } // queryUnitActive runs systemctl is-active, trying --user first then system. func queryUnitActive(unit string) string { // try user scope out, err := exec.Command("systemctl", "--user", "is-active", unit).Output() if err == nil { return strings.TrimSpace(string(out)) } combined, _ := exec.Command("systemctl", "--user", "is-active", unit).CombinedOutput() if strings.Contains(string(combined), "could not be found") || strings.Contains(string(combined), "not found") || strings.Contains(string(combined), "No such") { // try system scope out2, err2 := exec.Command("systemctl", "is-active", unit).Output() if err2 == nil { return strings.TrimSpace(string(out2)) } combined2, _ := exec.Command("systemctl", "is-active", unit).CombinedOutput() if strings.Contains(string(combined2), "could not be found") || strings.Contains(string(combined2), "not found") || strings.Contains(string(combined2), "No such") { return "not-installed" } s := strings.TrimSpace(string(out2)) if s == "" { return "unknown" } return s } // systemctl returned non-zero but unit exists (e.g. "inactive", "failed") if len(out) > 0 { return strings.TrimSpace(string(out)) } return "unknown" } // parseFirstPort returns the first integer in [1024, 65535] found in text. func parseFirstPort(text string) int { for _, m := range portRe.FindAllString(text, -1) { n, err := strconv.Atoi(m) if err == nil && n >= 1024 && n <= 65535 { return n } } return 0 } // tcpListening attempts a TCP connection to addr:port with the given timeout. func tcpListening(host string, port int, timeout time.Duration) bool { conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), timeout) if err != nil { return false } conn.Close() return true }