ServiceManager.swift 16 KB

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