| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- //
- // 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=<comma-separated dependency names>
- //
- // ═══════════════════════════════════════════════════════════════════
- // ── 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<CChar>? = 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<CChar>?] = 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<CChar>?] = [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)
- }
- }
|