import Foundation /// JSON dict from the bridge; immutable after parse, safe to pass across isolation. struct BridgeReply: @unchecked Sendable { let dict: [String: Any] subscript(_ key: String) -> Any? { dict[key] } } /// Actor owning the persistent python bridge process (line-JSON over stdio). actor BridgeTV { enum BridgeError: Error, LocalizedError { case spawn(String) case timeout case closed var errorDescription: String? { switch self { case .spawn(let why): return "bridge start failed: \(why)" case .timeout: return "bridge timeout" case .closed: return "bridge closed" } } } static let shared = BridgeTV() private var process: Process? private var nextID = 1 private var waiters: [Int: CheckedContinuation] = [:] private var buffer = Data() private var eventHandler: (@Sendable (BridgeReply) -> Void)? private let venvSite = "/Users/ahmedmohammed/Documents/rae/.venv/lib/python3.9/site-packages" private let python = "/Users/ahmedmohammed/Documents/rae/.venv/bin/python3" private var scriptURL: URL { if let bundled = Bundle.main.url(forResource: "bridge", withExtension: "py") { return bundled } return URL(fileURLWithPath: "/Users/ahmedmohammed/Documents/rae/HisenseBar/bridge.py") } func setEventHandler(_ handler: @escaping @Sendable (BridgeReply) -> Void) { eventHandler = handler } private func ensureProcess() throws -> Process { if let p = process, p.isRunning { return p } waiters.values.forEach { $0.resume(throwing: BridgeError.closed) } waiters.removeAll() buffer.removeAll() let p = Process() p.executableURL = URL(fileURLWithPath: python) p.arguments = [scriptURL.path, UserDefaults.standard.string(forKey: "hisensebar.ip") ?? "192.168.1.146", UserDefaults.standard.string(forKey: "hisensebar.mac") ?? "", NSString(string: "~/Library/Application Support/HisenseBar/tokens.json").expandingTildeInPath] var env = ProcessInfo.processInfo.environment env["VIDAA_SITE"] = venvSite env["PYTHONUNBUFFERED"] = "1" p.environment = env let inPipe = Pipe(), outPipe = Pipe(), errPipe = Pipe() p.standardInput = inPipe p.standardOutput = outPipe p.standardError = errPipe do { try p.run() } catch { throw BridgeError.spawn("\(error)") } process = p outPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in let data = handle.availableData guard !data.isEmpty else { handle.readabilityHandler = nil Task { await self?.handleClosed() } return } Task { await self?.feed(data) } } return p } private func feed(_ data: Data) { buffer.append(data) while let nl = buffer.firstIndex(of: UInt8(ascii: "\n")) { let lineData = buffer.subdata(in: buffer.startIndex.. BridgeReply { let p = try ensureProcess() let id = nextID nextID += 1 var msg = cmd msg["id"] = id let payload = try JSONSerialization.data(withJSONObject: msg) + Data("\n".utf8) return try await withCheckedThrowingContinuation { cont in waiters[id] = cont guard let pipe = p.standardInput as? Pipe else { waiters.removeValue(forKey: id) cont.resume(throwing: BridgeError.closed) return } do { try pipe.fileHandleForWriting.write(contentsOf: payload) } catch { waiters.removeValue(forKey: id) cont.resume(throwing: BridgeError.closed) } Task { try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) await self.timeoutID(id) } } } private func timeoutID(_ id: Int) { if let cont = waiters.removeValue(forKey: id) { cont.resume(throwing: BridgeError.timeout) } } }