1
0

ServiceManager.swift 17 KB

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