KernelBridge.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import Foundation
  2. #if canImport(Glibc)
  3. import Glibc
  4. // Linux reboot syscall — used by PowerManager.shutdown()
  5. @_silgen_name("reboot")
  6. public func reboot(_ cmd: Int32) -> Int32
  7. // Linux reboot command constants
  8. public let LINUX_REBOOT_CMD_POWER_OFF: Int32 = 0x4321FEDC
  9. public let LINUX_REBOOT_CMD_RESTART: Int32 = 0x01234567
  10. #endif
  11. // ═══════════════════════════════════════════════════════════════════
  12. // ArkOS Kernel Bridge
  13. // ═══════════════════════════════════════════════════════════════════
  14. // Provides the lowest-level interface between the ArkOS runtime and
  15. // the Linux kernel. All hardware access, syscall wrappers, and
  16. // resource management primitives live here.
  17. //
  18. // Layers:
  19. // 1. Syscall — Safe wrappers around raw Linux syscalls
  20. // 2. ResourceController — CPU core gating and memory monitoring
  21. // 3. LogManager — Structured, leveled logging with file output
  22. // 4. PowerManager — Battery, AC adapter, shutdown/reboot
  23. // 5. NetworkManager — Real interface enumeration and state
  24. // 6. BluetoothManager — HCI device scanning via sysfs
  25. // 7. InputManager — Raw keystroke reading from stdin
  26. // 8. FileDescriptor — RAII-style file handle wrapper
  27. // ═══════════════════════════════════════════════════════════════════
  28. // ── Syscall Error Type ────────────────────────────────────────────
  29. /// Represents a failed Linux syscall with the errno code and message.
  30. public struct SyscallError: Error, CustomStringConvertible {
  31. public let code: Int32
  32. public let message: String
  33. public var description: String {
  34. return "SyscallError(\(code)): \(message)"
  35. }
  36. /// Create a SyscallError from the current value of errno.
  37. public static func fromErrno() -> SyscallError {
  38. return SyscallError(code: errno, message: String(cString: strerror(errno)))
  39. }
  40. }
  41. // ── Safe Syscall Wrappers ─────────────────────────────────────────
  42. /// Thin wrappers around read(2) and write(2) that throw on failure
  43. /// instead of returning -1. Prevents silent data corruption.
  44. public struct Syscall {
  45. public static func read(fd: Int32, buffer: UnsafeMutableRawPointer, count: Int) throws -> Int {
  46. let result = Glibc.read(fd, buffer, count)
  47. if result < 0 {
  48. throw SyscallError.fromErrno()
  49. }
  50. return result
  51. }
  52. public static func write(fd: Int32, buffer: UnsafeRawPointer, count: Int) throws -> Int {
  53. let result = Glibc.write(fd, buffer, count)
  54. if result < 0 {
  55. throw SyscallError.fromErrno()
  56. }
  57. return result
  58. }
  59. }
  60. // ── Resource Controller ───────────────────────────────────────────
  61. /// Manages CPU and memory resources for the ArkOS runtime.
  62. ///
  63. /// CPU gating: All background work is dispatched through a concurrent
  64. /// queue to prevent runaway thread creation. The queue width matches
  65. /// the number of available CPU cores detected at startup.
  66. ///
  67. /// Memory monitoring: Reads the process RSS from getrusage(2) and
  68. /// compares against the configured cap (default 2 GB).
  69. public struct ResourceController {
  70. /// Concurrent dispatch queue for background OS work.
  71. /// Width is unbounded but tasks are cooperative — the kernel
  72. /// scheduler handles core affinity.
  73. private static let cpuQueue = DispatchQueue(
  74. label: "ark.system.cpu-gate",
  75. qos: .default,
  76. attributes: .concurrent
  77. )
  78. /// Dispatch a unit of work onto the OS thread pool.
  79. public static func executeOnCorePool(_ work: @escaping () -> Void) {
  80. cpuQueue.async { work() }
  81. }
  82. /// Returns the current process peak RSS in bytes.
  83. /// On Linux, getrusage(2) reports ru_maxrss in kilobytes.
  84. public static func checkMemoryUsage() -> Int64 {
  85. #if canImport(Glibc)
  86. var usage = rusage()
  87. if getrusage(0, &usage) == 0 {
  88. return Int64(usage.ru_maxrss) * 1024
  89. }
  90. #endif
  91. return 0
  92. }
  93. /// Returns the number of online CPU cores by reading /proc/cpuinfo.
  94. public static func getCPUCoreCount() -> Int {
  95. #if canImport(Glibc)
  96. let count = sysconf(Int32(_SC_NPROCESSORS_ONLN))
  97. if count > 0 { return count }
  98. #endif
  99. return 1
  100. }
  101. /// Returns total system RAM in bytes by reading /proc/meminfo.
  102. public static func getTotalMemory() -> Int64 {
  103. if let data = try? String(contentsOfFile: "/proc/meminfo") {
  104. for line in data.split(separator: "\n") {
  105. if line.hasPrefix("MemTotal:") {
  106. let parts = line.split(separator: " ").compactMap { Int64($0) }
  107. if let kb = parts.first {
  108. return kb * 1024
  109. }
  110. }
  111. }
  112. }
  113. return 0
  114. }
  115. }
  116. // ── Log Manager ───────────────────────────────────────────────────
  117. /// Structured, leveled logging system for ArkOS.
  118. ///
  119. /// Design:
  120. /// - Uses a pre-allocated circular buffer (1024 entries) to avoid
  121. /// heap allocation pressure during logging.
  122. /// - Thread-safe via NSLock (not a mutex — NSLock is faster for
  123. /// short critical sections on Linux).
  124. /// - Writes to both stdout and /var/log/arkos.log when available.
  125. /// - Supports four severity levels: INFO, WARN, ERROR, FATAL.
  126. public struct LogManager {
  127. /// Log severity levels, ordered by increasing severity.
  128. public enum Level: String {
  129. case info = "INFO"
  130. case warn = "WARN"
  131. case error = "ERROR"
  132. case fatal = "FATAL"
  133. }
  134. private static let maxLogCount = 1024
  135. private static let logBuffer: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?> = {
  136. let ptr = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: maxLogCount)
  137. for i in 0..<maxLogCount { ptr[i] = nil }
  138. return ptr
  139. }()
  140. private static var writeIndex = 0
  141. private static let lock = NSLock()
  142. /// Path to the persistent log file (created after /var is mounted).
  143. private static let logFilePath = "/var/log/arkos.log"
  144. /// Log a message at the INFO level.
  145. public static func log(_ message: String) {
  146. log(message, level: .info)
  147. }
  148. /// Log a message at a specific severity level.
  149. public static func log(_ message: String, level: Level) {
  150. let timestamp = Date().description
  151. let formatted = "[\(timestamp)] [\(level.rawValue)] \(message)"
  152. ResourceController.executeOnCorePool {
  153. lock.lock()
  154. defer { lock.unlock() }
  155. // Recycle the oldest entry in the circular buffer
  156. if let old = logBuffer[writeIndex] {
  157. old.deallocate()
  158. }
  159. // Copy the formatted string into the raw buffer
  160. let cStr = formatted.utf8CString
  161. let ptr = UnsafeMutablePointer<CChar>.allocate(capacity: cStr.count)
  162. for j in 0..<cStr.count { ptr[j] = cStr[j] }
  163. logBuffer[writeIndex] = ptr
  164. writeIndex = (writeIndex + 1) % maxLogCount
  165. // Print to console
  166. print(formatted)
  167. // Append to persistent log file (best-effort, non-blocking)
  168. if let fp = fopen(logFilePath, "a") {
  169. fputs(formatted + "\n", fp)
  170. fclose(fp)
  171. }
  172. }
  173. }
  174. /// Dump all buffered log entries in chronological order.
  175. public static func dumpLogs() -> [String] {
  176. lock.lock()
  177. defer { lock.unlock() }
  178. var result: [String] = []
  179. result.reserveCapacity(maxLogCount)
  180. for i in 0..<maxLogCount {
  181. let idx = (writeIndex + i) % maxLogCount
  182. if let ptr = logBuffer[idx] {
  183. result.append(String(cString: ptr))
  184. }
  185. }
  186. return result
  187. }
  188. }
  189. // ── Power Manager ─────────────────────────────────────────────────
  190. /// Reads battery and AC adapter state from Linux sysfs, and provides
  191. /// shutdown/reboot primitives using the reboot(2) syscall.
  192. public struct PowerManager {
  193. /// Battery charge state reported by the kernel.
  194. public enum ChargeState: String {
  195. case charging = "Charging"
  196. case discharging = "Discharging"
  197. case full = "Full"
  198. case notCharging = "Not charging"
  199. case unknown = "Unknown"
  200. }
  201. /// Returns the current battery percentage (0–100).
  202. /// Falls back to 100 when running in QEMU (no battery sysfs node).
  203. public static func getBatteryPercentage() -> Int {
  204. let path = "/sys/class/power_supply"
  205. if let items = try? FileManager.default.contentsOfDirectory(atPath: path) {
  206. for item in items {
  207. let lowerItem = item.lowercased()
  208. if lowerItem.contains("bat") || lowerItem.contains("battery") {
  209. let capPath = "\(path)/\(item)/capacity"
  210. if let content = try? String(contentsOfFile: capPath),
  211. let val = Int(content.trimmingCharacters(in: .whitespacesAndNewlines)) {
  212. return val
  213. }
  214. }
  215. }
  216. }
  217. return 100 // QEMU fallback — no physical battery
  218. }
  219. /// Returns the current charging state by reading the battery status file.
  220. public static func getChargeState() -> ChargeState {
  221. let path = "/sys/class/power_supply"
  222. if let items = try? FileManager.default.contentsOfDirectory(atPath: path) {
  223. for item in items {
  224. let lowerItem = item.lowercased()
  225. if lowerItem.contains("bat") || lowerItem.contains("battery") {
  226. let statusPath = "\(path)/\(item)/status"
  227. if let content = try? String(contentsOfFile: statusPath) {
  228. let status = content.trimmingCharacters(in: .whitespacesAndNewlines)
  229. return ChargeState(rawValue: status) ?? .unknown
  230. }
  231. }
  232. }
  233. }
  234. return .full // QEMU fallback
  235. }
  236. /// Returns true if an AC adapter is connected.
  237. public static func isACConnected() -> Bool {
  238. let path = "/sys/class/power_supply"
  239. if let items = try? FileManager.default.contentsOfDirectory(atPath: path) {
  240. for item in items {
  241. let lowerItem = item.lowercased()
  242. if lowerItem.contains("ac") || lowerItem.contains("adapter") {
  243. let onlinePath = "\(path)/\(item)/online"
  244. if let content = try? String(contentsOfFile: onlinePath) {
  245. return content.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
  246. }
  247. }
  248. }
  249. }
  250. return true // QEMU fallback — always powered
  251. }
  252. /// Initiates a clean system shutdown via reboot(2).
  253. public static func shutdown() {
  254. LogManager.log("Initiating system shutdown...", level: .info)
  255. #if canImport(Glibc)
  256. sync() // Flush all filesystem buffers to disk
  257. _ = reboot(LINUX_REBOOT_CMD_POWER_OFF)
  258. #endif
  259. }
  260. /// Initiates a system reboot via reboot(2).
  261. public static func restart() {
  262. LogManager.log("Initiating system restart...", level: .info)
  263. #if canImport(Glibc)
  264. sync()
  265. _ = reboot(LINUX_REBOOT_CMD_RESTART)
  266. #endif
  267. }
  268. }
  269. // ── Network Manager ───────────────────────────────────────────────
  270. /// Enumerates and queries network interfaces via Linux sysfs and
  271. /// the getifaddrs(3) API. Provides real interface state, IP addresses,
  272. /// MAC addresses, and carrier detection.
  273. public struct NetworkManager {
  274. /// Represents the operational state of a network interface.
  275. public struct InterfaceInfo {
  276. public let name: String
  277. public let isUp: Bool
  278. public let hasCarrier: Bool
  279. public let ipAddress: String?
  280. public let macAddress: String?
  281. }
  282. /// Returns the IP address of the first non-loopback IPv4 interface.
  283. public static func getInterfaceIP() -> String {
  284. #if canImport(Glibc)
  285. var ifaddr: UnsafeMutablePointer<ifaddrs>?
  286. guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else {
  287. return "127.0.0.1"
  288. }
  289. defer { freeifaddrs(ifaddr) }
  290. var ptr: UnsafeMutablePointer<ifaddrs>? = firstAddr
  291. while ptr != nil {
  292. let flags = Int32(ptr!.pointee.ifa_flags)
  293. let family = ptr!.pointee.ifa_addr.pointee.sa_family
  294. // Skip loopback (IFF_LOOPBACK = 8) and match IPv4 (AF_INET = 2)
  295. if (flags & 8) == 0, family == 2 {
  296. var ipBuf = [CChar](repeating: 0, count: 16)
  297. if let ipPtr = ptr!.pointee.ifa_addr {
  298. let sin = UnsafeMutableRawPointer(ipPtr).assumingMemoryBound(to: sockaddr_in.self)
  299. if inet_ntop(2, &sin.pointee.sin_addr, &ipBuf, 16) != nil {
  300. return String(cString: ipBuf)
  301. }
  302. }
  303. }
  304. ptr = ptr!.pointee.ifa_next
  305. }
  306. #endif
  307. return "10.0.2.15" // QEMU user-mode networking default
  308. }
  309. /// Returns a list of all network interface names (excluding loopback).
  310. public static func getInterfaceNames() -> [String] {
  311. if let items = try? FileManager.default.contentsOfDirectory(atPath: "/sys/class/net") {
  312. return items.filter { $0 != "lo" }.sorted()
  313. }
  314. return []
  315. }
  316. /// Returns detailed info about all non-loopback network interfaces.
  317. public static func getAllInterfaces() -> [InterfaceInfo] {
  318. var interfaces: [InterfaceInfo] = []
  319. let basePath = "/sys/class/net"
  320. guard let items = try? FileManager.default.contentsOfDirectory(atPath: basePath) else {
  321. return interfaces
  322. }
  323. for name in items where name != "lo" {
  324. let ifPath = "\(basePath)/\(name)"
  325. // Read operational state (up/down)
  326. let operstate = (try? String(contentsOfFile: "\(ifPath)/operstate"))?
  327. .trimmingCharacters(in: .whitespacesAndNewlines) ?? "unknown"
  328. let isUp = (operstate == "up")
  329. // Read carrier state (cable plugged / associated)
  330. let carrierStr = (try? String(contentsOfFile: "\(ifPath)/carrier"))?
  331. .trimmingCharacters(in: .whitespacesAndNewlines) ?? "0"
  332. let hasCarrier = (carrierStr == "1")
  333. // Read MAC address
  334. let mac = (try? String(contentsOfFile: "\(ifPath)/address"))?
  335. .trimmingCharacters(in: .whitespacesAndNewlines)
  336. // Get IP address for this specific interface
  337. var ipAddress: String? = nil
  338. #if canImport(Glibc)
  339. var ifaddr: UnsafeMutablePointer<ifaddrs>?
  340. if getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr {
  341. var ptr: UnsafeMutablePointer<ifaddrs>? = firstAddr
  342. while ptr != nil {
  343. let family = ptr!.pointee.ifa_addr.pointee.sa_family
  344. if family == 2 { // AF_INET
  345. let ifName = String(cString: ptr!.pointee.ifa_name)
  346. if ifName == name {
  347. var ipBuf = [CChar](repeating: 0, count: 16)
  348. let sin = UnsafeMutableRawPointer(ptr!.pointee.ifa_addr)
  349. .assumingMemoryBound(to: sockaddr_in.self)
  350. if inet_ntop(2, &sin.pointee.sin_addr, &ipBuf, 16) != nil {
  351. ipAddress = String(cString: ipBuf)
  352. }
  353. }
  354. }
  355. ptr = ptr!.pointee.ifa_next
  356. }
  357. freeifaddrs(ifaddr)
  358. }
  359. #endif
  360. interfaces.append(InterfaceInfo(
  361. name: name, isUp: isUp, hasCarrier: hasCarrier,
  362. ipAddress: ipAddress, macAddress: mac
  363. ))
  364. }
  365. return interfaces
  366. }
  367. /// Returns true if any non-loopback interface has an IP address.
  368. public static func isConnected() -> Bool {
  369. return getAllInterfaces().contains { $0.ipAddress != nil && $0.isUp }
  370. }
  371. /// Scans for wireless interfaces by checking /sys/class/net/*/wireless.
  372. public static func getWirelessInterfaces() -> [String] {
  373. let basePath = "/sys/class/net"
  374. guard let items = try? FileManager.default.contentsOfDirectory(atPath: basePath) else {
  375. return []
  376. }
  377. return items.filter { name in
  378. let wirelessPath = "\(basePath)/\(name)/wireless"
  379. return access(wirelessPath, F_OK) == 0
  380. }
  381. }
  382. }
  383. // ── Bluetooth Manager ─────────────────────────────────────────────
  384. /// Scans for Bluetooth HCI adapters and paired devices via Linux sysfs.
  385. /// Returns real hardware data when a BT adapter is present, empty
  386. /// results when running in QEMU (which has no Bluetooth hardware).
  387. public struct BluetoothManager {
  388. /// Returns true if at least one Bluetooth HCI adapter is present.
  389. public static func isEnabled() -> Bool {
  390. let path = "/sys/class/bluetooth"
  391. if let items = try? FileManager.default.contentsOfDirectory(atPath: path) {
  392. return !items.isEmpty
  393. }
  394. return false
  395. }
  396. /// Returns the names of all detected Bluetooth HCI adapters.
  397. public static func getAdapters() -> [String] {
  398. let path = "/sys/class/bluetooth"
  399. if let items = try? FileManager.default.contentsOfDirectory(atPath: path) {
  400. return items.sorted()
  401. }
  402. return []
  403. }
  404. }
  405. // ── Input Manager ─────────────────────────────────────────────────
  406. /// Reads raw keystrokes from stdin (fd 0). Used for console input
  407. /// before the display server is running.
  408. public struct InputManager {
  409. /// Reads a single byte from stdin and returns it as a Character.
  410. /// Returns nil if no input is available or read fails.
  411. public static func readKeyStroke() -> Character? {
  412. let fd: Int32 = 0
  413. var buf: UInt8 = 0
  414. if let bytesRead = try? Syscall.read(fd: fd, buffer: &buf, count: 1),
  415. bytesRead == 1 {
  416. return Character(UnicodeScalar(buf))
  417. }
  418. return nil
  419. }
  420. }
  421. // ── File Descriptor Wrapper ───────────────────────────────────────
  422. /// RAII-style wrapper for Linux file descriptors. Ensures the fd is
  423. /// valid on creation and provides explicit cleanup via closeFd().
  424. public struct FileDescriptor {
  425. public let raw: Int32
  426. /// Opens a file at the given path with the specified mode flags.
  427. /// Throws SyscallError if open(2) fails.
  428. public init(path: String, mode: Int32) throws {
  429. let fd = open(path, mode)
  430. if fd < 0 {
  431. throw SyscallError.fromErrno()
  432. }
  433. self.raw = fd
  434. }
  435. /// Closes the underlying file descriptor.
  436. public func closeFd() {
  437. close(raw)
  438. }
  439. }