ServiceManager.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import Foundation
  2. #if canImport(Glibc)
  3. import Glibc
  4. #endif
  5. // ═══════════════════════════════════════════════════════════════════
  6. // ArkOS Service Manager
  7. // ═══════════════════════════════════════════════════════════════════
  8. // Manages system services defined by .serve configuration files.
  9. // Handles service lifecycle: discovery → dependency resolution →
  10. // ordered startup → health monitoring → respawn on crash.
  11. //
  12. // Service files live at /system/services/*.serve and use a simple
  13. // key=value format:
  14. //
  15. // name=display
  16. // exec=/system/ui_daemon
  17. // autostart=true
  18. // respawn=true
  19. // deps=<comma-separated dependency names>
  20. //
  21. // ═══════════════════════════════════════════════════════════════════
  22. // ── Service State Machine ─────────────────────────────────────────
  23. /// Lifecycle states for a managed service.
  24. public enum ServiceState: String {
  25. case stopped = "STOPPED" // Not running, not scheduled
  26. case starting = "STARTING" // Fork issued, waiting for confirmation
  27. case running = "RUNNING" // Process is alive
  28. case failed = "FAILED" // Exited with non-zero status
  29. }
  30. // ── Service Definition ────────────────────────────────────────────
  31. /// Represents a single ArkOS service parsed from a .serve config file.
  32. struct ArkService {
  33. var name: String // Unique service identifier
  34. var exec: String // Absolute path to the executable
  35. var autostart: Bool // Whether to start on boot
  36. var respawn: Bool // Whether to restart on crash
  37. var deps: [String] // Names of services this depends on
  38. var state: ServiceState // Current lifecycle state
  39. var pid: pid_t = -1 // PID of the running process (-1 = not running)
  40. var restartCount: Int = 0 // Number of times this service has been restarted
  41. var lastExitCode: Int32 = 0 // Last exit status (for diagnostics)
  42. }
  43. // ── Service Manager ───────────────────────────────────────────────
  44. /// Singleton service manager that discovers, starts, monitors, and
  45. /// restarts all ArkOS system services.
  46. class ServiceManager {
  47. static let shared = ServiceManager()
  48. /// Path to the directory containing .serve files inside the initramfs.
  49. private let servicesPath = "/system/services"
  50. /// All loaded services, keyed by name.
  51. var services: [String: ArkService] = [:]
  52. /// Maximum restart attempts before marking a service as FAILED.
  53. private let maxRestartAttempts = 5
  54. /// Delay between restart attempts (seconds). Increases with each
  55. /// failure to prevent rapid crash loops.
  56. private let baseRestartDelay: UInt32 = 1
  57. private init() {
  58. loadServices()
  59. }
  60. // ── Service Discovery ─────────────────────────────────────────
  61. /// Scans the services directory for .serve files and parses each one.
  62. func loadServices() {
  63. LogManager.log("ServiceManager: Scanning \(servicesPath) for .serve files...")
  64. guard let dir = opendir(servicesPath) else {
  65. LogManager.log("ServiceManager: Cannot open \(servicesPath)", level: .warn)
  66. return
  67. }
  68. defer { closedir(dir) }
  69. while let entry = readdir(dir) {
  70. let nameBytes = withUnsafeBytes(of: entry.pointee.d_name) {
  71. $0.bindMemory(to: CChar.self).map { $0 }
  72. }
  73. let nameString = String(cString: nameBytes)
  74. if nameString.hasSuffix(".serve") {
  75. parseService(at: servicesPath + "/" + nameString)
  76. }
  77. }
  78. LogManager.log("ServiceManager: Loaded \(services.count) service(s)")
  79. }
  80. // ── Config File Parser ────────────────────────────────────────
  81. /// Parses a single .serve configuration file into an ArkService.
  82. func parseService(at path: String) {
  83. guard let filePointer = fopen(path, "r") else {
  84. LogManager.log("ServiceManager: Cannot read \(path)", level: .error)
  85. return
  86. }
  87. defer { fclose(filePointer) }
  88. var name = ""
  89. var exec = ""
  90. var autostart = false
  91. var respawn = false
  92. var deps: [String] = []
  93. var lineBuffer: UnsafeMutablePointer<CChar>? = nil
  94. var lineCap: Int = 0
  95. while getline(&lineBuffer, &lineCap, filePointer) > 0 {
  96. guard let cString = lineBuffer else { continue }
  97. let line = String(cString: cString)
  98. .trimmingCharacters(in: .whitespacesAndNewlines)
  99. // Skip empty lines and comments
  100. if line.isEmpty || line.hasPrefix("#") { continue }
  101. let parts = line.split(separator: "=", maxSplits: 1)
  102. .map { String($0).trimmingCharacters(in: .whitespaces) }
  103. guard parts.count == 2 else { continue }
  104. let key = parts[0].lowercased()
  105. let value = parts[1]
  106. switch key {
  107. case "name": name = value
  108. case "exec": exec = value
  109. case "autostart": autostart = (value.lowercased() == "true")
  110. case "respawn": respawn = (value.lowercased() == "true")
  111. case "deps":
  112. deps = value.split(separator: ",")
  113. .map { String($0).trimmingCharacters(in: .whitespaces) }
  114. default: break
  115. }
  116. }
  117. if let buffer = lineBuffer { free(buffer) }
  118. guard !name.isEmpty, !exec.isEmpty else {
  119. LogManager.log("ServiceManager: Skipping invalid service at \(path)", level: .warn)
  120. return
  121. }
  122. let service = ArkService(
  123. name: name, exec: exec, autostart: autostart,
  124. respawn: respawn, deps: deps, state: .stopped
  125. )
  126. services[name] = service
  127. LogManager.log("ServiceManager: Registered '\(name)' [exec=\(exec), autostart=\(autostart), respawn=\(respawn), deps=\(deps)]")
  128. }
  129. // ── Dependency-Ordered Startup ────────────────────────────────
  130. /// Starts all autostart services in dependency order.
  131. /// Uses topological sort to ensure dependencies are started first.
  132. func startAllServices() {
  133. let ordered = topologicalSort()
  134. for name in ordered {
  135. guard let service = services[name], service.autostart else { continue }
  136. startService(name: name)
  137. // Give the service a moment to initialize before starting dependents
  138. usleep(200_000) // 200ms
  139. }
  140. }
  141. /// Returns service names in dependency order (dependencies first).
  142. /// Uses Kahn's algorithm for topological sorting.
  143. private func topologicalSort() -> [String] {
  144. var inDegree: [String: Int] = [:]
  145. var adjacency: [String: [String]] = [:]
  146. // Initialize all services
  147. for name in services.keys {
  148. inDegree[name] = 0
  149. adjacency[name] = []
  150. }
  151. // Build the dependency graph
  152. for (name, service) in services {
  153. for dep in service.deps {
  154. if services[dep] != nil {
  155. adjacency[dep, default: []].append(name)
  156. inDegree[name, default: 0] += 1
  157. } else {
  158. LogManager.log("ServiceManager: '\(name)' depends on unknown service '\(dep)'", level: .warn)
  159. }
  160. }
  161. }
  162. // Kahn's algorithm: start with nodes that have no dependencies
  163. var queue: [String] = inDegree.filter { $0.value == 0 }.map { $0.key }.sorted()
  164. var result: [String] = []
  165. while !queue.isEmpty {
  166. let current = queue.removeFirst()
  167. result.append(current)
  168. for dependent in adjacency[current] ?? [] {
  169. inDegree[dependent, default: 0] -= 1
  170. if inDegree[dependent] == 0 {
  171. queue.append(dependent)
  172. }
  173. }
  174. }
  175. // Any remaining services have circular dependencies — log and skip
  176. let missing = Set(services.keys).subtracting(result)
  177. if !missing.isEmpty {
  178. LogManager.log("ServiceManager: Circular dependency detected for: \(missing.sorted())", level: .error)
  179. }
  180. return result
  181. }
  182. // ── Service Lifecycle ─────────────────────────────────────────
  183. /// Starts a single service by name. Forks a child process, redirects
  184. /// I/O to /dev/ttyS0, and exec's the service binary.
  185. func startService(name: String) {
  186. guard var service = services[name] else {
  187. LogManager.log("ServiceManager: Unknown service '\(name)'", level: .error)
  188. return
  189. }
  190. // Check dependencies are running
  191. for dep in service.deps {
  192. if let depService = services[dep], depService.state != .running {
  193. LogManager.log("ServiceManager: Cannot start '\(name)' — dependency '\(dep)' is not running", level: .warn)
  194. return
  195. }
  196. }
  197. service.state = .starting
  198. services[name] = service
  199. LogManager.log("ServiceManager: Starting '\(name)' (\(service.exec))...")
  200. let pid = fork()
  201. if pid == 0 {
  202. // ── Child Process ──
  203. // Redirect stdout/stderr to /dev/ttyS0 for visibility
  204. let consoleFd = open("/dev/ttyS0", O_RDWR)
  205. if consoleFd >= 0 {
  206. dup2(consoleFd, 1)
  207. dup2(consoleFd, 2)
  208. if consoleFd > 2 { close(consoleFd) }
  209. }
  210. // Parse the exec line into arguments
  211. let args = service.exec.split(separator: " ").map { String($0) }
  212. var cArgs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
  213. cArgs.append(nil)
  214. let env0 = strdup("PATH=/bin:/usr/bin:/sbin:/system")
  215. let env1 = strdup("HOME=/")
  216. let env2 = strdup("TERM=linux")
  217. let envp: [UnsafeMutablePointer<CChar>?] = [env0, env1, env2, nil]
  218. execve(cArgs[0]!, cArgs, envp)
  219. // execve only returns on failure
  220. let err = String(cString: strerror(errno))
  221. print("ServiceManager: execve failed for \(service.exec): \(err)")
  222. _exit(127)
  223. } else if pid > 0 {
  224. // ── Parent Process ──
  225. service.pid = pid
  226. service.state = .running
  227. services[name] = service
  228. // Write PID file for external monitoring tools
  229. writePidFile(name: name, pid: pid)
  230. LogManager.log("ServiceManager: '\(name)' started with PID \(pid)")
  231. } else {
  232. service.state = .failed
  233. services[name] = service
  234. LogManager.log("ServiceManager: fork() failed for '\(name)'", level: .error)
  235. }
  236. }
  237. /// Stops a running service by sending SIGTERM, then SIGKILL after 3 seconds.
  238. func stopService(name: String) {
  239. guard var service = services[name], service.pid > 0 else { return }
  240. LogManager.log("ServiceManager: Stopping '\(name)' (PID \(service.pid))...")
  241. kill(service.pid, SIGTERM)
  242. // Wait up to 3 seconds for graceful shutdown
  243. for _ in 0..<6 {
  244. var status: Int32 = 0
  245. let result = waitpid(service.pid, &status, WNOHANG)
  246. if result == service.pid {
  247. service.pid = -1
  248. service.state = .stopped
  249. services[name] = service
  250. removePidFile(name: name)
  251. LogManager.log("ServiceManager: '\(name)' stopped gracefully")
  252. return
  253. }
  254. usleep(500_000) // 500ms
  255. }
  256. // Force kill if still running
  257. kill(service.pid, SIGKILL)
  258. var status: Int32 = 0
  259. waitpid(service.pid, &status, 0)
  260. service.pid = -1
  261. service.state = .stopped
  262. services[name] = service
  263. removePidFile(name: name)
  264. LogManager.log("ServiceManager: '\(name)' force-killed (SIGKILL)", level: .warn)
  265. }
  266. // ── Health Monitoring ─────────────────────────────────────────
  267. /// Checks all running services for unexpected exits and respawns
  268. /// them if configured. Called periodically from the main run loop.
  269. func monitorServices() {
  270. for (name, service) in services {
  271. guard service.pid > 0 else { continue }
  272. var status: Int32 = 0
  273. let result = waitpid(service.pid, &status, WNOHANG)
  274. if result == service.pid {
  275. // Service exited
  276. let exitCode = (status >> 8) & 0xFF
  277. services[name]?.pid = -1
  278. services[name]?.lastExitCode = Int32(exitCode)
  279. removePidFile(name: name)
  280. if exitCode == 0 {
  281. services[name]?.state = .stopped
  282. LogManager.log("ServiceManager: '\(name)' exited cleanly")
  283. } else {
  284. services[name]?.state = .failed
  285. LogManager.log("ServiceManager: '\(name)' crashed (exit \(exitCode))", level: .error)
  286. // Respawn with backoff if configured
  287. if service.respawn && service.restartCount < maxRestartAttempts {
  288. let delay = baseRestartDelay * UInt32(service.restartCount + 1)
  289. LogManager.log("ServiceManager: Respawning '\(name)' in \(delay)s (attempt \(service.restartCount + 1)/\(maxRestartAttempts))")
  290. services[name]?.restartCount += 1
  291. sleep(delay)
  292. startService(name: name)
  293. } else if service.restartCount >= maxRestartAttempts {
  294. LogManager.log("ServiceManager: '\(name)' exceeded max restart attempts — giving up", level: .error)
  295. }
  296. }
  297. }
  298. }
  299. }
  300. /// Returns a human-readable status summary of all services.
  301. func getStatusReport() -> String {
  302. var lines: [String] = ["=== ArkOS Service Status ==="]
  303. for (name, service) in services.sorted(by: { $0.key < $1.key }) {
  304. let pidStr = service.pid > 0 ? "PID \(service.pid)" : "---"
  305. lines.append(" \(name): \(service.state.rawValue) [\(pidStr)] restarts=\(service.restartCount)")
  306. }
  307. return lines.joined(separator: "\n")
  308. }
  309. // ── PID File Management ───────────────────────────────────────
  310. /// Write a PID file to /run/ for external monitoring.
  311. private func writePidFile(name: String, pid: pid_t) {
  312. let path = "/run/\(name).pid"
  313. if let fp = fopen(path, "w") {
  314. fputs("\(pid)\n", fp)
  315. fclose(fp)
  316. }
  317. }
  318. /// Remove the PID file when a service stops.
  319. private func removePidFile(name: String) {
  320. let path = "/run/\(name).pid"
  321. unlink(path)
  322. }
  323. }