package utils import ( "errors" "fmt" "net" "os" "os/exec" "path/filepath" "strings" "syscall" ) var ErrServiceNotFound = errors.New("service not found") // PortSpec defines a port and its name for checking availability type PortSpec struct { Name string Port int } var ServicePorts = map[string][]PortSpec{ "debros-gateway": { {Name: "Gateway API", Port: 6001}, }, "debros-olric": { {Name: "Olric HTTP", Port: 3320}, {Name: "Olric Memberlist", Port: 3322}, }, "debros-node": { {Name: "RQLite HTTP", Port: 5001}, {Name: "RQLite Raft", Port: 7001}, }, "debros-ipfs": { {Name: "IPFS API", Port: 4501}, {Name: "IPFS Gateway", Port: 8080}, {Name: "IPFS Swarm", Port: 4101}, }, "debros-ipfs-cluster": { {Name: "IPFS Cluster API", Port: 9094}, }, } // DefaultPorts is used for fresh installs/upgrades before unit files exist. func DefaultPorts() []PortSpec { return []PortSpec{ {Name: "IPFS Swarm", Port: 4001}, {Name: "IPFS API", Port: 4501}, {Name: "IPFS Gateway", Port: 8080}, {Name: "Gateway API", Port: 6001}, {Name: "RQLite HTTP", Port: 5001}, {Name: "RQLite Raft", Port: 7001}, {Name: "IPFS Cluster API", Port: 9094}, {Name: "Olric HTTP", Port: 3320}, {Name: "Olric Memberlist", Port: 3322}, } } // ResolveServiceName resolves service aliases to actual systemd service names func ResolveServiceName(alias string) ([]string, error) { // Service alias mapping (unified - no bootstrap/node distinction) aliases := map[string][]string{ "node": {"debros-node"}, "ipfs": {"debros-ipfs"}, "cluster": {"debros-ipfs-cluster"}, "ipfs-cluster": {"debros-ipfs-cluster"}, "gateway": {"debros-gateway"}, "olric": {"debros-olric"}, "rqlite": {"debros-node"}, // RQLite logs are in node logs } // Check if it's an alias if serviceNames, ok := aliases[strings.ToLower(alias)]; ok { // Filter to only existing services var existing []string for _, svc := range serviceNames { unitPath := filepath.Join("/etc/systemd/system", svc+".service") if _, err := os.Stat(unitPath); err == nil { existing = append(existing, svc) } } if len(existing) == 0 { return nil, fmt.Errorf("no services found for alias %q", alias) } return existing, nil } // Check if it's already a full service name unitPath := filepath.Join("/etc/systemd/system", alias+".service") if _, err := os.Stat(unitPath); err == nil { return []string{alias}, nil } // Try without .service suffix if !strings.HasSuffix(alias, ".service") { unitPath = filepath.Join("/etc/systemd/system", alias+".service") if _, err := os.Stat(unitPath); err == nil { return []string{alias}, nil } } return nil, fmt.Errorf("service %q not found. Use: node, ipfs, cluster, gateway, olric, or full service name", alias) } // IsServiceActive checks if a systemd service is currently active (running) func IsServiceActive(service string) (bool, error) { cmd := exec.Command("systemctl", "is-active", "--quiet", service) if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { switch exitErr.ExitCode() { case 3: return false, nil case 4: return false, ErrServiceNotFound } } return false, err } return true, nil } // IsServiceEnabled checks if a systemd service is enabled to start on boot func IsServiceEnabled(service string) (bool, error) { cmd := exec.Command("systemctl", "is-enabled", "--quiet", service) if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { switch exitErr.ExitCode() { case 1: return false, nil // Service is disabled case 4: return false, ErrServiceNotFound } } return false, err } return true, nil } // IsServiceMasked checks if a systemd service is masked func IsServiceMasked(service string) (bool, error) { cmd := exec.Command("systemctl", "is-enabled", service) output, err := cmd.CombinedOutput() if err != nil { outputStr := string(output) if strings.Contains(outputStr, "masked") { return true, nil } return false, err } return false, nil } // GetProductionServices returns a list of all DeBros production service names that exist, // including both global services and namespace-specific services func GetProductionServices() []string { // Global/default service names globalServices := []string{ "debros-gateway", "debros-node", "debros-olric", "debros-ipfs-cluster", "debros-ipfs", "debros-anyone-client", "debros-anyone-relay", } var existing []string // Add existing global services for _, svc := range globalServices { unitPath := filepath.Join("/etc/systemd/system", svc+".service") if _, err := os.Stat(unitPath); err == nil { existing = append(existing, svc) } } // Discover namespace service instances from the namespaces data directory. // We can't rely on scanning /etc/systemd/system because that only contains // template files (e.g. debros-namespace-gateway@.service) with no instance name. // Restarting a template without an instance is a no-op. // Instead, scan the data directory where each subdirectory is a provisioned namespace. namespacesDir := "/home/debros/.orama/data/namespaces" nsEntries, err := os.ReadDir(namespacesDir) if err == nil { serviceTypes := []string{"rqlite", "olric", "gateway"} for _, nsEntry := range nsEntries { if !nsEntry.IsDir() { continue } ns := nsEntry.Name() for _, svcType := range serviceTypes { // Only add if the env file exists (service was provisioned) envFile := filepath.Join(namespacesDir, ns, svcType+".env") if _, err := os.Stat(envFile); err == nil { svcName := fmt.Sprintf("debros-namespace-%s@%s", svcType, ns) existing = append(existing, svcName) } } } } return existing } // CollectPortsForServices returns a list of ports used by the specified services func CollectPortsForServices(services []string, skipActive bool) ([]PortSpec, error) { seen := make(map[int]PortSpec) for _, svc := range services { if skipActive { active, err := IsServiceActive(svc) if err != nil { return nil, fmt.Errorf("unable to check %s: %w", svc, err) } if active { continue } } for _, spec := range ServicePorts[svc] { if _, ok := seen[spec.Port]; !ok { seen[spec.Port] = spec } } } ports := make([]PortSpec, 0, len(seen)) for _, spec := range seen { ports = append(ports, spec) } return ports, nil } // EnsurePortsAvailable checks if the specified ports are available. // If a port is in use, it identifies the process and gives actionable guidance. func EnsurePortsAvailable(action string, ports []PortSpec) error { var conflicts []string for _, spec := range ports { ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", spec.Port)) if err != nil { if errors.Is(err, syscall.EADDRINUSE) || strings.Contains(err.Error(), "address already in use") { processInfo := identifyPortProcess(spec.Port) conflicts = append(conflicts, fmt.Sprintf(" - %s (port %d): %s", spec.Name, spec.Port, processInfo)) continue } return fmt.Errorf("%s cannot continue: failed to inspect %s (port %d): %w", action, spec.Name, spec.Port, err) } _ = ln.Close() } if len(conflicts) > 0 { msg := fmt.Sprintf("%s cannot continue: the following ports are already in use:\n%s\n\n", action, strings.Join(conflicts, "\n")) msg += "Please stop the conflicting services before running this command.\n" msg += "Common fixes:\n" msg += " - Docker: sudo systemctl stop docker docker.socket\n" msg += " - Old IPFS: sudo systemctl stop ipfs\n" msg += " - systemd-resolved: already handled by installer (port 53)\n" msg += " - Other services: sudo kill or sudo systemctl stop " return fmt.Errorf("%s", msg) } return nil } // identifyPortProcess uses ss/lsof to find what process is using a port func identifyPortProcess(port int) string { // Try ss first (available on most Linux) out, err := exec.Command("ss", "-tlnp", fmt.Sprintf("sport = :%d", port)).CombinedOutput() if err == nil { lines := strings.Split(strings.TrimSpace(string(out)), "\n") for _, line := range lines { if strings.Contains(line, "users:") { // Extract process info from ss output like: users:(("docker-proxy",pid=2049,fd=4)) if idx := strings.Index(line, "users:"); idx != -1 { return strings.TrimSpace(line[idx:]) } } } } // Fallback: try lsof out, err = exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN", "-n", "-P").CombinedOutput() if err == nil { lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) > 1 { return strings.TrimSpace(lines[1]) // first data line after header } } return "unknown process" }