KernelBridge.swift 20 KB

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