// // 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 // ═══════════════════════════════════════════════════════════════════ // 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. import CSystem 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("Scanning \(servicesPath) for .serve files...", component: "ark.sys.service") guard let dir = ark_opendir(servicesPath) else { LogManager.log("Cannot open \(servicesPath)", level: .warn, component: "ark.sys.service") return } defer { ark_closedir(dir) } while let entry = ark_readdir(dir) { let nameString = String(cString: ark_dirent_name(entry)) if nameString.hasSuffix(".serve") { parseService(at: servicesPath + "/" + nameString) } } LogManager.log("Loaded \(services.count) service(s)", component: "ark.sys.service") } // ── 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("Cannot read \(path)", level: .error, component: "ark.sys.service") 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 ark_getline(&lineBuffer, &lineCap, filePointer) > 0 { guard let cString = lineBuffer else { continue } var line = String(cString: cString) if line.hasSuffix("\n") { line.removeLast() } if line.hasSuffix("\r") { line.removeLast() } // Skip empty lines and comments if line.isEmpty || line.hasPrefix("#") { continue } let parts = line.split(separator: "=", maxSplits: 1) .map { String($0) } 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) } default: break } } if let buffer = lineBuffer { free(buffer) } guard !name.isEmpty, !exec.isEmpty else { LogManager.log("Skipping invalid service at \(path)", level: .warn, component: "ark.sys.service") return } let service = ArkService( name: name, exec: exec, autostart: autostart, respawn: respawn, deps: deps, state: .stopped ) services[name] = service LogManager.log("Registered '\(name)' [exec=\(exec), autostart=\(autostart), respawn=\(respawn), deps=\(deps)]", component: "ark.sys.service") } // ── 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("'\(name)' depends on unknown service '\(dep)'", level: .warn, component: "ark.sys.service") } } } // 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("Circular dependency detected for: \(missing.sorted())", level: .error, component: "ark.sys.service") } 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("Unknown service '\(name)'", level: .error, component: "ark.sys.service") return } // Check dependencies are running for dep in service.deps { if let depService = services[dep], depService.state != .running { LogManager.log("Cannot start '\(name)' — dependency '\(dep)' is not running", level: .warn, component: "ark.sys.service") return } } service.state = .starting services[name] = service LogManager.log("Starting '\(name)' (\(service.exec))...", component: "ark.sys.service") let pid = fork() if pid == 0 { // ── Child Process ── // Redirect stdout/stderr to /boot/.log on FAT32 partition let logPath = "/boot/\(name).log" var logFd = ark_open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0o644) if logFd < 0 { logFd = ark_open("/var/log/\(name).log", O_WRONLY | O_CREAT | O_APPEND, 0o644) } if logFd < 0 { logFd = ark_open2("/dev/ttyAMA0", O_RDWR) } if logFd < 0 { logFd = ark_open2("/dev/serial0", O_RDWR) } if logFd < 0 { logFd = ark_open2("/dev/ttyS0", O_RDWR) } if logFd >= 0 { dup2(logFd, 1) dup2(logFd, 2) if logFd > 2 { close(logFd) } } // 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 env3 = strdup("XDG_RUNTIME_DIR=/run") let env4 = strdup("GTK_BACKEND=wayland") let env5 = strdup("GDK_BACKEND=wayland") let env6 = strdup("WAYLAND_DISPLAY=wayland-0") let env7 = strdup("LD_LIBRARY_PATH=/lib") let envp: [UnsafeMutablePointer?] = [env0, env1, env2, env3, env4, env5, env6, env7, nil] execve(cArgs[0]!, cArgs, envp) // execve only returns on failure let err = String(cString: strerror(ark_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("'\(name)' started with PID \(pid)", component: "ark.sys.service") } else { service.state = .failed services[name] = service LogManager.log("fork() failed for '\(name)'", level: .error, component: "ark.sys.service") } } /// 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("Stopping '\(name)' (PID \(service.pid))...", component: "ark.sys.service") 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("'\(name)' stopped gracefully", component: "ark.sys.service") 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("'\(name)' force-killed (SIGKILL)", level: .warn, component: "ark.sys.service") } // ── 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 termSig = status & 0x7F let exitCode = (status >> 8) & 0xFF services[name]?.pid = -1 services[name]?.lastExitCode = Int32(exitCode) removePidFile(name: name) if termSig != 0 { services[name]?.state = .failed LogManager.log("'\(name)' killed by signal \(termSig)", level: .error, component: "ark.sys.service") } else if exitCode == 0 { services[name]?.state = .stopped LogManager.log("'\(name)' exited cleanly", component: "ark.sys.service") } else { services[name]?.state = .failed LogManager.log("'\(name)' crashed (exit \(exitCode))", level: .error, component: "ark.sys.service") } if termSig != 0 || exitCode != 0 { // Respawn with backoff if configured if service.respawn && service.restartCount < maxRestartAttempts { let delay = baseRestartDelay * UInt32(service.restartCount + 1) LogManager.log("Respawning '\(name)' in \(delay)s (attempt \(service.restartCount + 1)/\(maxRestartAttempts))", component: "ark.sys.service") services[name]?.restartCount += 1 let serviceName = name ResourceController.executeOnCorePool { sleep(delay) ServiceManager.shared.startService(name: serviceName) } } else if service.restartCount >= maxRestartAttempts { LogManager.log("'\(name)' exceeded max restart attempts — giving up", level: .error, component: "ark.sys.service") } } } } } /// 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) } }