// // Copyright 2026 Aarav Ravindra Kharade // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation #if canImport(Glibc) import Glibc #endif // ═══════════════════════════════════════════════════════════════════ // ArkOS Service Manager // ═══════════════════════════════════════════════════════════════════ // Manages system services defined by .serve configuration files. // Handles service lifecycle: discovery → dependency resolution → // ordered startup → health monitoring → respawn on crash. // // Service files live at /system/services/*.serve and use a simple // key=value format: // // name=display // exec=/system/ui_daemon // autostart=true // respawn=true // deps= // // ═══════════════════════════════════════════════════════════════════ // ── Service State Machine ───────────────────────────────────────── /// Lifecycle states for a managed service. public enum ServiceState: String { case stopped = "STOPPED" // Not running, not scheduled case starting = "STARTING" // Fork issued, waiting for confirmation case running = "RUNNING" // Process is alive case failed = "FAILED" // Exited with non-zero status } // ── Service Definition ──────────────────────────────────────────── /// Represents a single ArkOS service parsed from a .serve config file. struct ArkService { var name: String // Unique service identifier var exec: String // Absolute path to the executable var autostart: Bool // Whether to start on boot var respawn: Bool // Whether to restart on crash var deps: [String] // Names of services this depends on var state: ServiceState // Current lifecycle state var pid: pid_t = -1 // PID of the running process (-1 = not running) var restartCount: Int = 0 // Number of times this service has been restarted var lastExitCode: Int32 = 0 // Last exit status (for diagnostics) } // ── Service Manager ─────────────────────────────────────────────── /// Singleton service manager that discovers, starts, monitors, and /// restarts all ArkOS system services. class ServiceManager { static let shared = ServiceManager() /// Path to the directory containing .serve files inside the initramfs. private let servicesPath = "/system/services" /// All loaded services, keyed by name. var services: [String: ArkService] = [:] /// Maximum restart attempts before marking a service as FAILED. private let maxRestartAttempts = 5 /// Delay between restart attempts (seconds). Increases with each /// failure to prevent rapid crash loops. private let baseRestartDelay: UInt32 = 1 private init() { loadServices() } // ── Service Discovery ───────────────────────────────────────── /// Scans the services directory for .serve files and parses each one. func loadServices() { LogManager.log("ServiceManager: Scanning \(servicesPath) for .serve files...") guard let dir = opendir(servicesPath) else { LogManager.log("ServiceManager: Cannot open \(servicesPath)", level: .warn) return } defer { closedir(dir) } while let entry = readdir(dir) { let nameBytes = withUnsafeBytes(of: entry.pointee.d_name) { $0.bindMemory(to: CChar.self).map { $0 } } let nameString = String(cString: nameBytes) if nameString.hasSuffix(".serve") { parseService(at: servicesPath + "/" + nameString) } } LogManager.log("ServiceManager: Loaded \(services.count) service(s)") } // ── Config File Parser ──────────────────────────────────────── /// Parses a single .serve configuration file into an ArkService. func parseService(at path: String) { guard let filePointer = fopen(path, "r") else { LogManager.log("ServiceManager: Cannot read \(path)", level: .error) return } defer { fclose(filePointer) } var name = "" var exec = "" var autostart = false var respawn = false var deps: [String] = [] var lineBuffer: UnsafeMutablePointer? = nil var lineCap: Int = 0 while getline(&lineBuffer, &lineCap, filePointer) > 0 { guard let cString = lineBuffer else { continue } let line = String(cString: cString) .trimmingCharacters(in: .whitespacesAndNewlines) // Skip empty lines and comments if line.isEmpty || line.hasPrefix("#") { continue } let parts = line.split(separator: "=", maxSplits: 1) .map { String($0).trimmingCharacters(in: .whitespaces) } guard parts.count == 2 else { continue } let key = parts[0].lowercased() let value = parts[1] switch key { case "name": name = value case "exec": exec = value case "autostart": autostart = (value.lowercased() == "true") case "respawn": respawn = (value.lowercased() == "true") case "deps": deps = value.split(separator: ",") .map { String($0).trimmingCharacters(in: .whitespaces) } default: break } } if let buffer = lineBuffer { free(buffer) } guard !name.isEmpty, !exec.isEmpty else { LogManager.log("ServiceManager: Skipping invalid service at \(path)", level: .warn) return } let service = ArkService( name: name, exec: exec, autostart: autostart, respawn: respawn, deps: deps, state: .stopped ) services[name] = service LogManager.log("ServiceManager: Registered '\(name)' [exec=\(exec), autostart=\(autostart), respawn=\(respawn), deps=\(deps)]") } // ── Dependency-Ordered Startup ──────────────────────────────── /// Starts all autostart services in dependency order. /// Uses topological sort to ensure dependencies are started first. func startAllServices() { let ordered = topologicalSort() for name in ordered { guard let service = services[name], service.autostart else { continue } startService(name: name) // Give the service a moment to initialize before starting dependents usleep(200_000) // 200ms } } /// Returns service names in dependency order (dependencies first). /// Uses Kahn's algorithm for topological sorting. private func topologicalSort() -> [String] { var inDegree: [String: Int] = [:] var adjacency: [String: [String]] = [:] // Initialize all services for name in services.keys { inDegree[name] = 0 adjacency[name] = [] } // Build the dependency graph for (name, service) in services { for dep in service.deps { if services[dep] != nil { adjacency[dep, default: []].append(name) inDegree[name, default: 0] += 1 } else { LogManager.log("ServiceManager: '\(name)' depends on unknown service '\(dep)'", level: .warn) } } } // Kahn's algorithm: start with nodes that have no dependencies var queue: [String] = inDegree.filter { $0.value == 0 }.map { $0.key }.sorted() var result: [String] = [] while !queue.isEmpty { let current = queue.removeFirst() result.append(current) for dependent in adjacency[current] ?? [] { inDegree[dependent, default: 0] -= 1 if inDegree[dependent] == 0 { queue.append(dependent) } } } // Any remaining services have circular dependencies — log and skip let missing = Set(services.keys).subtracting(result) if !missing.isEmpty { LogManager.log("ServiceManager: Circular dependency detected for: \(missing.sorted())", level: .error) } return result } // ── Service Lifecycle ───────────────────────────────────────── /// Starts a single service by name. Forks a child process, redirects /// I/O to /dev/ttyS0, and exec's the service binary. func startService(name: String) { guard var service = services[name] else { LogManager.log("ServiceManager: Unknown service '\(name)'", level: .error) return } // Check dependencies are running for dep in service.deps { if let depService = services[dep], depService.state != .running { LogManager.log("ServiceManager: Cannot start '\(name)' — dependency '\(dep)' is not running", level: .warn) return } } service.state = .starting services[name] = service LogManager.log("ServiceManager: Starting '\(name)' (\(service.exec))...") let pid = fork() if pid == 0 { // ── Child Process ── // Redirect stdout/stderr to /dev/ttyS0 for visibility let consoleFd = open("/dev/ttyS0", O_RDWR) if consoleFd >= 0 { dup2(consoleFd, 1) dup2(consoleFd, 2) if consoleFd > 2 { close(consoleFd) } } // Parse the exec line into arguments let args = service.exec.split(separator: " ").map { String($0) } var cArgs: [UnsafeMutablePointer?] = args.map { strdup($0) } cArgs.append(nil) let env0 = strdup("PATH=/bin:/usr/bin:/sbin:/system") let env1 = strdup("HOME=/") let env2 = strdup("TERM=linux") let envp: [UnsafeMutablePointer?] = [env0, env1, env2, nil] execve(cArgs[0]!, cArgs, envp) // execve only returns on failure let err = String(cString: strerror(errno)) print("ServiceManager: execve failed for \(service.exec): \(err)") _exit(127) } else if pid > 0 { // ── Parent Process ── service.pid = pid service.state = .running services[name] = service // Write PID file for external monitoring tools writePidFile(name: name, pid: pid) LogManager.log("ServiceManager: '\(name)' started with PID \(pid)") } else { service.state = .failed services[name] = service LogManager.log("ServiceManager: fork() failed for '\(name)'", level: .error) } } /// Stops a running service by sending SIGTERM, then SIGKILL after 3 seconds. func stopService(name: String) { guard var service = services[name], service.pid > 0 else { return } LogManager.log("ServiceManager: Stopping '\(name)' (PID \(service.pid))...") kill(service.pid, SIGTERM) // Wait up to 3 seconds for graceful shutdown for _ in 0..<6 { var status: Int32 = 0 let result = waitpid(service.pid, &status, WNOHANG) if result == service.pid { service.pid = -1 service.state = .stopped services[name] = service removePidFile(name: name) LogManager.log("ServiceManager: '\(name)' stopped gracefully") return } usleep(500_000) // 500ms } // Force kill if still running kill(service.pid, SIGKILL) var status: Int32 = 0 waitpid(service.pid, &status, 0) service.pid = -1 service.state = .stopped services[name] = service removePidFile(name: name) LogManager.log("ServiceManager: '\(name)' force-killed (SIGKILL)", level: .warn) } // ── Health Monitoring ───────────────────────────────────────── /// Checks all running services for unexpected exits and respawns /// them if configured. Called periodically from the main run loop. func monitorServices() { for (name, service) in services { guard service.pid > 0 else { continue } var status: Int32 = 0 let result = waitpid(service.pid, &status, WNOHANG) if result == service.pid { // Service exited let exitCode = (status >> 8) & 0xFF services[name]?.pid = -1 services[name]?.lastExitCode = Int32(exitCode) removePidFile(name: name) if exitCode == 0 { services[name]?.state = .stopped LogManager.log("ServiceManager: '\(name)' exited cleanly") } else { services[name]?.state = .failed LogManager.log("ServiceManager: '\(name)' crashed (exit \(exitCode))", level: .error) // Respawn with backoff if configured if service.respawn && service.restartCount < maxRestartAttempts { let delay = baseRestartDelay * UInt32(service.restartCount + 1) LogManager.log("ServiceManager: Respawning '\(name)' in \(delay)s (attempt \(service.restartCount + 1)/\(maxRestartAttempts))") services[name]?.restartCount += 1 sleep(delay) startService(name: name) } else if service.restartCount >= maxRestartAttempts { LogManager.log("ServiceManager: '\(name)' exceeded max restart attempts — giving up", level: .error) } } } } } /// Returns a human-readable status summary of all services. func getStatusReport() -> String { var lines: [String] = ["=== ArkOS Service Status ==="] for (name, service) in services.sorted(by: { $0.key < $1.key }) { let pidStr = service.pid > 0 ? "PID \(service.pid)" : "---" lines.append(" \(name): \(service.state.rawValue) [\(pidStr)] restarts=\(service.restartCount)") } return lines.joined(separator: "\n") } // ── PID File Management ─────────────────────────────────────── /// Write a PID file to /run/ for external monitoring. private func writePidFile(name: String, pid: pid_t) { let path = "/run/\(name).pid" if let fp = fopen(path, "w") { fputs("\(pid)\n", fp) fclose(fp) } } /// Remove the PID file when a service stops. private func removePidFile(name: String) { let path = "/run/\(name).pid" unlink(path) } }