#if os(macOS) import Foundation import os private let logger = Logger(category: "RootHelperXPC") @objc public class ConnectionOwnerResult: NSObject, NSSecureCoding { public static let supportsSecureCoding = true @objc public var userId: Int32 @objc public var userName: String @objc public var processPath: String public init(userId: Int32, userName: String, processPath: String) { self.userId = userId self.userName = userName self.processPath = processPath } public required init?(coder: NSCoder) { userId = coder.decodeInt32(forKey: "userId") userName = coder.decodeObject(of: NSString.self, forKey: "userName") as? String ?? "" processPath = coder.decodeObject(of: NSString.self, forKey: "processPath") as? String ?? "" } public func encode(with coder: NSCoder) { coder.encode(userId, forKey: "userId") coder.encode(userName as NSString, forKey: "userName") coder.encode(processPath as NSString, forKey: "processPath") } } @objc public protocol RootHelperProtocol { func findConnectionOwner( ipProtocol: Int32, sourceAddress: String, sourcePort: Int32, destinationAddress: String, destinationPort: Int32, reply: @escaping (ConnectionOwnerResult?, NSError?) -> Void ) func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void) func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void) } public enum RootHelperXPC { public static func configureInterface(_ interface: NSXPCInterface) { let resultClasses = NSSet(array: [ConnectionOwnerResult.self, NSString.self]) as! Set interface.setClasses( resultClasses, for: #selector(RootHelperProtocol.findConnectionOwner(ipProtocol:sourceAddress:sourcePort:destinationAddress:destinationPort:reply:)), argumentIndex: 0, ofReply: true ) } } public class RootHelperClient { public static let shared = RootHelperClient() private var connection: NSXPCConnection? private let connectionLock = NSLock() private init() {} private func getConnection() -> NSXPCConnection { connectionLock.lock() defer { connectionLock.unlock() } if let existing = connection { return existing } let newConnection = NSXPCConnection(machServiceName: AppConfiguration.rootHelperMachService) let remoteInterface = NSXPCInterface(with: RootHelperProtocol.self) RootHelperXPC.configureInterface(remoteInterface) newConnection.remoteObjectInterface = remoteInterface newConnection.invalidationHandler = { [weak self] in guard let self else { return } connectionLock.lock() connection = nil connectionLock.unlock() } newConnection.resume() connection = newConnection return newConnection } private func performXPCCall( _ operation: String, call: (RootHelperProtocol, @escaping (T?, NSError?) -> Void) -> Void ) throws -> T { let semaphore = DispatchSemaphore(value: 0) var result: T? var resultError: NSError? let conn = getConnection() guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in logger.error("\(operation) XPC error: \(error.localizedDescription)") resultError = error as NSError semaphore.signal() }) as? RootHelperProtocol else { connectionLock.lock() connection = nil connectionLock.unlock() conn.invalidate() throw NSError(domain: "RootHelper", code: -1, userInfo: [ NSLocalizedDescriptionKey: "Failed to get RootHelper proxy", ]) } call(proxy) { value, error in result = value resultError = error semaphore.signal() } let timeout = DispatchTime.now() + .seconds(5) if semaphore.wait(timeout: timeout) == .timedOut { let error = NSError(domain: "RootHelper", code: -1, userInfo: [ NSLocalizedDescriptionKey: "\(operation) request timeout", ]) logger.error("\(operation): timeout") throw error } if let error = resultError { logger.error("\(operation) error: \(error.localizedDescription)") throw error } guard let value = result else { let error = NSError(domain: "RootHelper", code: -1, userInfo: [ NSLocalizedDescriptionKey: "\(operation) returned nil", ]) throw error } return value } private func performXPCCallVoid( _ operation: String, call: (RootHelperProtocol, @escaping (NSError?) -> Void) -> Void ) throws { let semaphore = DispatchSemaphore(value: 0) var resultError: NSError? let conn = getConnection() guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in logger.error("\(operation) XPC error: \(error.localizedDescription)") resultError = error as NSError semaphore.signal() }) as? RootHelperProtocol else { connectionLock.lock() connection = nil connectionLock.unlock() conn.invalidate() throw NSError(domain: "RootHelper", code: -1, userInfo: [ NSLocalizedDescriptionKey: "Failed to get RootHelper proxy", ]) } call(proxy) { error in resultError = error semaphore.signal() } let timeout = DispatchTime.now() + .seconds(5) if semaphore.wait(timeout: timeout) == .timedOut { let error = NSError(domain: "RootHelper", code: -1, userInfo: [ NSLocalizedDescriptionKey: "\(operation) request timeout", ]) logger.error("\(operation): timeout") throw error } if let error = resultError { logger.error("\(operation) error: \(error.localizedDescription)") throw error } } public func findConnectionOwner( ipProtocol: Int32, sourceAddress: String, sourcePort: Int32, destinationAddress: String, destinationPort: Int32 ) throws -> ConnectionOwnerResult { try performXPCCall("findConnectionOwner") { proxy, reply in proxy.findConnectionOwner( ipProtocol: ipProtocol, sourceAddress: sourceAddress, sourcePort: sourcePort, destinationAddress: destinationAddress, destinationPort: destinationPort, reply: reply ) } } public func getWorkingDirectorySize() throws -> Int64 { try performXPCCall("getWorkingDirectorySize") { proxy, reply in proxy.getWorkingDirectorySize { size, error in reply(size as Int64?, error) } } } public func cleanWorkingDirectory() throws { try performXPCCallVoid("cleanWorkingDirectory") { proxy, reply in proxy.cleanWorkingDirectory(reply: reply) } } } #endif