Files
tyto/backend/internal/collectors/systemd.go
vikingowl a2504c1327 feat: rename project to Tyto with owl branding
- Rename project from system-monitor to Tyto (barn owl themed)
- Update Go module name and all import paths
- Update Docker container names (tyto-backend, tyto-frontend)
- Update localStorage keys (tyto-settings, tyto-hosts)
- Create barn owl SVG favicon and PWA icons (192, 512)
- Update header with owl logo icon
- Update manifest.json and app.html with Tyto branding

Named after Tyto alba, the barn owl — nature's silent, watchful guardian

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 06:36:01 +01:00

143 lines
3.1 KiB
Go

package collectors
import (
"context"
"strings"
"time"
"github.com/godbus/dbus/v5"
"tyto/internal/models"
)
const dbusTimeout = 2 * time.Second
type SystemdCollector struct {
conn *dbus.Conn
available bool
}
func NewSystemdCollector() *SystemdCollector {
c := &SystemdCollector{}
c.connect()
return c
}
func (c *SystemdCollector) connect() {
// Try to connect to the system bus
conn, err := dbus.SystemBus()
if err != nil {
c.available = false
return
}
c.conn = conn
c.available = true
}
func (c *SystemdCollector) Collect() (models.SystemdStats, error) {
stats := models.SystemdStats{
Available: c.available,
Services: []models.ServiceStatus{},
}
if !c.available || c.conn == nil {
return stats, nil
}
// Create a context with timeout for the D-Bus call
ctx, cancel := context.WithTimeout(context.Background(), dbusTimeout)
defer cancel()
// Call ListUnits on systemd manager with timeout
obj := c.conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1")
call := obj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnits", 0)
if call.Err != nil {
// Connection might have been lost or timed out, mark as unavailable
c.available = false
return stats, nil // Return empty stats instead of error
}
// ListUnits returns array of structs:
// (name, description, load_state, active_state, sub_state, following, unit_path, job_id, job_type, job_path)
var units [][]interface{}
if err := call.Store(&units); err != nil {
return stats, nil // Return empty stats instead of error
}
for _, unit := range units {
if len(unit) < 5 {
continue
}
name, ok := unit[0].(string)
if !ok {
continue
}
// Only process .service units
if !strings.HasSuffix(name, ".service") {
continue
}
// Remove .service suffix for cleaner display
name = strings.TrimSuffix(name, ".service")
// Skip template services
if strings.Contains(name, "@") && !strings.Contains(name, "@.") {
continue
}
load, _ := unit[2].(string)
active, _ := unit[3].(string)
sub, _ := unit[4].(string)
service := models.ServiceStatus{
Name: name,
Load: load,
Active: active,
Sub: sub,
}
// Count by status
switch active {
case "active":
stats.Active++
case "inactive":
stats.Inactive++
case "failed":
stats.Failed++
}
stats.Services = append(stats.Services, service)
}
stats.Total = len(stats.Services)
// Sort failed services first, then active running, then other active
// Limit to 50 most relevant services
sortedServices := make([]models.ServiceStatus, 0, 50)
// Add failed first
for _, s := range stats.Services {
if s.Active == "failed" && len(sortedServices) < 50 {
sortedServices = append(sortedServices, s)
}
}
// Add active running
for _, s := range stats.Services {
if s.Active == "active" && s.Sub == "running" && len(sortedServices) < 50 {
sortedServices = append(sortedServices, s)
}
}
// Add other active
for _, s := range stats.Services {
if s.Active == "active" && s.Sub != "running" && len(sortedServices) < 50 {
sortedServices = append(sortedServices, s)
}
}
stats.Services = sortedServices
return stats, nil
}