├── .gitignore ├── Example App ├── Example App.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Example App.xcscheme │ └── project.pbxproj ├── Example App │ ├── Example_App.swift │ ├── Example_App.entitlements │ ├── Errors.swift │ ├── MessageSender.swift │ └── ContentView.swift └── Example XPC Service │ ├── Info.plist │ ├── CommandSet.swift │ └── XPCService.swift ├── Sources ├── TestShared │ ├── HelperID.swift │ ├── JokeMessage.swift │ ├── ProcessIDs.swift │ ├── DataInfo.swift │ └── CommandSet.swift ├── SwiftyXPC │ ├── XPCNull.swift │ ├── Extensions │ │ └── String+SwiftyXPC.swift │ ├── XPCEndpoint.swift │ ├── XPCFileDescriptor.swift │ ├── XPCError.swift │ ├── XPCType.swift │ ├── XPCErrorRegistry.swift │ ├── XPCListener.swift │ ├── XPCEncoder.swift │ ├── XPCConnection.swift │ └── XPCDecoder.swift └── TestHelper │ └── TestHelper.swift ├── .swift-format ├── .swiftpm ├── SwiftyXPC-Package.xctestplan └── xcode │ └── xcshareddata │ └── xcschemes │ ├── SwiftyXPC.xcscheme │ ├── TestHelper.xcscheme │ └── SwiftyXPC-Package.xcscheme ├── Package.swift ├── LICENSE.md ├── README.md └── Tests └── SwiftyXPCTests ├── HelperLauncher.swift └── SwiftyXPCTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | swiftpm 4 | /Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Example App/Example App.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/TestShared/HelperID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelperID.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 10/12/23. 6 | // 7 | 8 | // swift-format-ignore: AllPublicDeclarationsHaveDocumentation 9 | public let helperID = "com.charlessoft.SwiftyXPC.TestHelper" 10 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCNull.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCNull.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 12/27/21. 6 | // 7 | 8 | /// A class representing a null value in XPC. 9 | public struct XPCNull: Codable, Sendable { 10 | /// The shared `XPCNull` instance. 11 | public static let shared = Self() 12 | } 13 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 125, 4 | "indentation": { 5 | "spaces": 4 6 | }, 7 | "maximumBlankLines": 1, 8 | "rules": { 9 | "AllPublicDeclarationsHaveDocumentation": true, 10 | "BeginDocumentationCommentWithOneLineSummary": true, 11 | "ValidateDocumentationComments": true, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example App/Example App/Example_App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Example_App.swift 3 | // Example App 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct Example_App: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example App/Example App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example App/Example XPC Service/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XPCService 6 | 7 | ServiceType 8 | Application 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Example App/Example App/Example_App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example App/Example XPC Service/CommandSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandSet.swift 3 | // Example XPC Service 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | struct CommandSet { 9 | static let capitalizeString = "com.charlessoft.SwiftyXPC.Example-App.CapitalizeString" 10 | static let longRunningTask = "com.charlessoft.SwiftyXPC.Example-App.LongRunningTask" 11 | } 12 | 13 | struct LongRunningTaskMessage { 14 | static let progressNotification = "com.charlessoft.SwiftyXPC.Example-App.LongRunningTask.Progress" 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TestShared/JokeMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JokeMessage.swift 3 | // 4 | // Created by Charles Srstka on 10/13/23. 5 | // 6 | 7 | // swift-format-ignore: AllPublicDeclarationsHaveDocumentation 8 | public struct JokeMessage { 9 | public struct NotAKnockKnockJoke: Error, Codable { 10 | public let complaint: String 11 | public init(complaint: String) { 12 | self.complaint = complaint 13 | } 14 | } 15 | 16 | public static let askForJoke = "ask-for-joke" 17 | public static let whosThere = "who's-there" 18 | public static let who = "who" 19 | public static let groan = "groan" 20 | } 21 | -------------------------------------------------------------------------------- /Sources/TestShared/ProcessIDs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessIDs.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 10/14/23. 6 | // 7 | 8 | import Darwin 9 | import SwiftyXPC 10 | import System 11 | 12 | // swift-format-ignore: AllPublicDeclarationsHaveDocumentation 13 | public struct ProcessIDs: Codable, Sendable { 14 | public let pid: pid_t 15 | public let effectiveUID: uid_t 16 | public let effectiveGID: gid_t 17 | public let auditSessionID: au_asid_t 18 | 19 | public init(connection: XPCConnection) throws { 20 | self.pid = getpid() 21 | self.effectiveUID = geteuid() 22 | self.effectiveGID = getegid() 23 | self.auditSessionID = connection.auditSessionIdentifier 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/TestShared/DataInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataInfo.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 2/16/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DataInfo: Codable, Sendable { 11 | public struct DataError: LocalizedError, Codable { 12 | public let failureReason: String? 13 | public init(failureReason: String) { self.failureReason = failureReason } 14 | } 15 | 16 | public init(characterName: Data, playedBy: Data, otherCharacters: [Data]) { 17 | self.characterName = characterName 18 | self.playedBy = playedBy 19 | self.otherCharacters = otherCharacters 20 | } 21 | 22 | public let characterName: Data 23 | public let playedBy: Data 24 | public let otherCharacters: [Data] 25 | } 26 | -------------------------------------------------------------------------------- /Sources/TestShared/CommandSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandSet.swift 3 | // Example XPC Service 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | // swift-format-ignore: AllPublicDeclarationsHaveDocumentation 9 | public struct CommandSet { 10 | public static let reportIDs = "com.charlessoft.SwiftyXPC.Tests.ReportIDs" 11 | public static let capitalizeString = "com.charlessoft.SwiftyXPC.Tests.CapitalizeString" 12 | public static let multiplyBy5 = "com.charlessoft.SwiftyXPC.Tests.MultiplyBy5" 13 | public static let transportData = "com.charlessoft.SwiftyXPC.Tests.TransportData" 14 | public static let tellAJoke = "com.charlessoft.SwiftyXPC.Tests.TellAJoke" 15 | public static let pauseOneSecond = "com.charlessoft.SwiftyXPC.Tests.PauseOneSecond" 16 | } 17 | -------------------------------------------------------------------------------- /.swiftpm/SwiftyXPC-Package.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "FCC01CE3-8BB6-4831-B6FF-9EA386D3CC52", 5 | "name" : "Test Scheme Action", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "codeCoverage" : { 13 | "targets" : [ 14 | { 15 | "containerPath" : "container:", 16 | "identifier" : "SwiftyXPC", 17 | "name" : "SwiftyXPC" 18 | } 19 | ] 20 | }, 21 | "targetForVariableExpansion" : { 22 | "containerPath" : "container:", 23 | "identifier" : "TestHelper", 24 | "name" : "TestHelper" 25 | } 26 | }, 27 | "testTargets" : [ 28 | { 29 | "target" : { 30 | "containerPath" : "container:", 31 | "identifier" : "SwiftyXPCTests", 32 | "name" : "SwiftyXPCTests" 33 | } 34 | } 35 | ], 36 | "version" : 1 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/Extensions/String+SwiftyXPC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+SwiftyXPC.swift 3 | // SwiftyXPC 4 | // 5 | // Created by Charles Srstka on 7/29/21. 6 | // 7 | 8 | import XPC 9 | 10 | extension String { 11 | /// Create a `String` from an `xpc_object_t`. 12 | /// 13 | /// - Parameter xpcObject: An `xpc_object_t` wrapping a string. 14 | public init?(_ xpcObject: xpc_object_t) { 15 | guard let ptr = xpc_string_get_string_ptr(xpcObject) else { return nil } 16 | let length = xpc_string_get_length(xpcObject) 17 | 18 | self = UnsafeBufferPointer(start: ptr, count: length).withMemoryRebound(to: UInt8.self) { 19 | String(decoding: $0, as: UTF8.self) 20 | } 21 | } 22 | 23 | /// Convert a `String` to an `xpc_object_t`. 24 | /// 25 | /// - Returns: An `xpc_object_t` wrapping the receiver's string. 26 | public func toXPCObject() -> xpc_object_t? { xpc_string_create(self) } 27 | } 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftyXPC", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .macCatalyst(.v13), 11 | ], 12 | products: [ 13 | .library( 14 | name: "SwiftyXPC", 15 | targets: ["SwiftyXPC"] 16 | ) 17 | ], 18 | dependencies: [], 19 | targets: [ 20 | .target( 21 | name: "SwiftyXPC", 22 | dependencies: [] 23 | ), 24 | .target( 25 | name: "TestShared", 26 | dependencies: ["SwiftyXPC"] 27 | ), 28 | .executableTarget( 29 | name: "TestHelper", 30 | dependencies: ["SwiftyXPC", "TestShared"] 31 | ), 32 | .testTarget( 33 | name: "SwiftyXPCTests", 34 | dependencies: ["SwiftyXPC", "TestHelper", "TestShared"] 35 | ), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Copyright © 2013-2021 Charles J. Srstka 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Example App/Example App/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // Example App 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import SwiftyXPC 11 | 12 | // just to make error reporting a little nicer 13 | extension XPCError: LocalizedError { 14 | /// Implementation of `LocalizedError`. 15 | public var errorDescription: String? { 16 | switch self { 17 | case .connectionInvalid: 18 | return NSLocalizedString("Invalid XPC Connection", comment: "Invalid XPC Connection") 19 | case .connectionInterrupted: 20 | return NSLocalizedString("XPC Connection Interrupted", comment: "XPC Connection Interrupted") 21 | case .invalidCodeSignatureRequirement: 22 | return NSLocalizedString("Bad Code Signature Requirement", comment: "Bad Code Signature Requirement") 23 | case .terminationImminent: 24 | return NSLocalizedString("XPC Service Termination Imminent", comment: "XPC Service Termination Imminent") 25 | case .unknown(let code): 26 | return "Error \(code)" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example App/Example XPC Service/XPCService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceDelegate.swift 3 | // Example XPC Service 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | import SwiftyXPC 9 | 10 | @main 11 | class XPCService { 12 | static func main() { 13 | do { 14 | let xpcService = XPCService() 15 | 16 | // In an actual product, you should always set a real code signing requirement here, for security 17 | let requirement: String? = nil 18 | 19 | let serviceListener = try XPCListener(type: .service, codeSigningRequirement: requirement) 20 | 21 | serviceListener.setMessageHandler(name: CommandSet.capitalizeString, handler: xpcService.capitalizeString) 22 | serviceListener.setMessageHandler(name: CommandSet.longRunningTask, handler: xpcService.longRunningTask) 23 | 24 | serviceListener.activate() 25 | fatalError("Should never get here") 26 | } catch { 27 | fatalError("Error while setting up XPC service: \(error)") 28 | } 29 | } 30 | 31 | private func capitalizeString(_: XPCConnection, string: String) async throws -> String { 32 | return string.uppercased() 33 | } 34 | 35 | private func longRunningTask(_: XPCConnection, endpoint: XPCEndpoint) async throws { 36 | let remoteConnection = try XPCConnection( 37 | type: .remoteServiceFromEndpoint(endpoint), 38 | codeSigningRequirement: nil 39 | ) 40 | 41 | remoteConnection.activate() 42 | 43 | for i in 0...100 { 44 | try await Task.sleep(for: .milliseconds(500)) 45 | 46 | try remoteConnection.sendOnewayMessage( 47 | name: LongRunningTaskMessage.progressNotification, 48 | message: Double(i) / 100.0 49 | ) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCEndpoint.swift 3 | // SwiftyXPC 4 | // 5 | // Created by Charles Srstka on 7/24/21. 6 | // 7 | 8 | import XPC 9 | 10 | /// A reference to an `XPCListener` object. 11 | /// 12 | /// An `XPCEndpoint` can be passed over an active XPC connection, allowing the process on the other end to initialize a new `XPCConnection` 13 | /// to communicate with it. 14 | public struct XPCEndpoint: Codable, @unchecked Sendable { 15 | private struct CanOnlyBeDecodedByXPCDecoder: Error { 16 | var localizedDescription: String { "XPCEndpoint can only be decoded via XPCDecoder." } 17 | } 18 | 19 | private struct CanOnlyBeEncodedByXPCEncoder: Error { 20 | var localizedDescription: String { "XPCEndpoint can only be encoded via XPCEncoder." } 21 | } 22 | 23 | internal let endpoint: xpc_endpoint_t 24 | 25 | internal init(connection: xpc_connection_t) { 26 | self.endpoint = xpc_endpoint_create(connection) 27 | } 28 | 29 | internal init(endpoint: xpc_endpoint_t) { 30 | self.endpoint = endpoint 31 | } 32 | 33 | internal func makeConnection() -> xpc_connection_t { 34 | xpc_connection_create_from_endpoint(self.endpoint) 35 | } 36 | 37 | /// Required method for the purpose of conforming to the `Decodable` protocol. 38 | /// 39 | /// - Throws: Trying to decode this object from any decoder type other than `XPCDecoder` will result in an error. 40 | public init(from decoder: Decoder) throws { 41 | throw CanOnlyBeDecodedByXPCDecoder() 42 | } 43 | 44 | /// Required method for the purpose of conforming to the `Encodable` protocol. 45 | /// 46 | /// - Throws: Trying to encode this object from any encoder type other than `XPCEncoder` will result in an error. 47 | public func encode(to encoder: Encoder) throws { 48 | throw CanOnlyBeEncodedByXPCEncoder() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example App/Example App/MessageSender.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageSender.swift 3 | // Example App 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftyXPC 10 | import os 11 | 12 | actor MessageSender { 13 | static let shared = try! MessageSender() 14 | 15 | private let connection: XPCConnection 16 | @Published var messageSendInProgress = false 17 | 18 | private init() throws { 19 | let connection = try XPCConnection(type: .remoteService(bundleID: "com.charlessoft.SwiftyXPC.Example-App.xpc")) 20 | 21 | let logger = Logger() 22 | 23 | connection.errorHandler = { _, error in 24 | logger.error("The connection to the XPC service received an error: \(error.localizedDescription)") 25 | } 26 | 27 | connection.resume() 28 | self.connection = connection 29 | } 30 | 31 | func capitalize(string: String) async throws -> String { 32 | self.messageSendInProgress = true 33 | defer { self.messageSendInProgress = false } 34 | 35 | return try await self.connection.sendMessage(name: CommandSet.capitalizeString, request: string) 36 | } 37 | 38 | func startLongRunningTask(callback: @escaping (Double?) -> Void) async throws { 39 | self.messageSendInProgress = true 40 | defer { self.messageSendInProgress = false } 41 | 42 | let listener = try XPCListener(type: .anonymous, codeSigningRequirement: nil) // don't actually use nil 43 | 44 | listener.setMessageHandler(name: LongRunningTaskMessage.progressNotification) { (_, progress: Double) in 45 | callback(progress) 46 | } 47 | 48 | listener.activate() 49 | listener.errorHandler = { 50 | callback(nil) 51 | print("something went wrong: \($1)") 52 | } 53 | 54 | try await self.connection.sendMessage(name: CommandSet.longRunningTask, request: listener.endpoint) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftyXPC 2 | 3 | ## Hmm, what’s this? 4 | 5 | SwiftyXPC is a wrapper for Apple’s XPC interprocess communication library that gives it an easy-to-use, idiomatic Swift interface. 6 | 7 | ## But there’s already `NSXPCConnection`! 8 | 9 | Well, yes, but with its protocol-and-proxy-based interface, it’s far better suited to Objective-C than Swift. 10 | Using `NSXPCConnection` from Swift has always felt somewhat awkward, and with the advent of Swift Concurrency, it’s even worse, given that everything has to be wrapped in `withCheckedThrowingContinuation` blocks. 11 | `NSXPCConnection` has also tended to be behind `libxpc` in certain important ways—notably, in the ability to verify the code signature of a remote process via an audit token. 12 | 13 | By contrast, SwiftyXPC: 14 | - Offers a fully Swift Concurrency-aware interface. Use `try` and `await` to call your helper code with no closures necessary. 15 | - Gives you a straightforward interface for your helper functions; take an argument, return a value async. No fussing around with Objective-C selectors and reply blocks. 16 | - Built around the `Codable` protocol, so you can use any types you want for the argument and return value, as long as both types conform to `Codable`, and it Just Works™. Error types are preserved as well, if they are `Codable` and you register their domains with the shared `XPCErrorRegistry` object. 17 | - Only links against XPC and Security (the latter for the code signature validation), so there’s no need to link Foundation into your app, deal with Objective-C bridging magic, or involve the Objective-C runtime at all (excluding any places where XPC or the Security framework may be using it internally). 18 | 19 | ## But I want to support older macOS versions! Using Swift Concurrency means that it requires macOS 12! 20 | 21 | Actually, it turns out that’s not true anymore! With Xcode 13.2, Swift concurrency now works all the way back to macOS 10.15 “Catalina”, and consequently, so does this library. 22 | 23 | ## What’s the license on this? 24 | 25 | MIT. 26 | -------------------------------------------------------------------------------- /Example App/Example App/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Example App 4 | // 5 | // Created by Charles Srstka on 5/5/22. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftyXPC 10 | 11 | struct ContentView: View { 12 | @State var request = "hello world" 13 | @State var response = "" 14 | @State var progress: Double? = nil 15 | 16 | var body: some View { 17 | VStack { 18 | HStack { 19 | VStack { 20 | Text("Request:").fixedSize().padding() 21 | Text("Response:").fixedSize().padding() 22 | } 23 | VStack { 24 | TextField("Request", text: self.$request).frame(minWidth: 200).padding() 25 | Text(self.response).frame(maxWidth: .infinity, alignment: .leading).padding() 26 | } 27 | } 28 | Button("Capitalize") { 29 | Task { 30 | do { 31 | self.response = try await MessageSender.shared.capitalize(string: self.request) 32 | } catch { 33 | self.response = "Error: \(error.localizedDescription)" 34 | } 35 | } 36 | } 37 | Button("Start Long-Running-Task") { 38 | Task { 39 | do { 40 | self.response = NSLocalizedString("Please Wait…", comment: "Please Wait...") 41 | 42 | try await MessageSender.shared.startLongRunningTask() { 43 | self.progress = $0 44 | } 45 | 46 | self.response = NSLocalizedString("Done!", comment: "Done message") 47 | } catch { 48 | self.response = "Error: \(error.localizedDescription)" 49 | } 50 | } 51 | }.padding() 52 | 53 | ProgressView(value: self.progress, total: 1.0) 54 | .progressViewStyle(.linear) 55 | .opacity(self.progress != nil ? 1.0 : 0.0) 56 | }.padding() 57 | } 58 | } 59 | 60 | struct ContentView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | ContentView() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCFileDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCFileDescriptorWrapper.swift 3 | // SwiftyXPC 4 | // 5 | // Created by Charles Srstka on 7/24/21. 6 | // 7 | 8 | import Darwin 9 | import System 10 | 11 | /// An owned wrapper around a file descriptor, which can be embedded within XPC messages to send file descriptors to other processes. 12 | /// 13 | /// This wrapper takes ownership of the file descriptor so the user doesn't need to close it manually. 14 | /// 15 | /// This wrapper serves two purposes: 16 | /// 1. To provide compatibility with macOS 10.15, which does not support the `FileDescriptor` structure. 17 | /// 2. To ensure that the file descriptor is closed when the message is embedded. 18 | /// 19 | /// On versions of macOS greater than 11.0, you can also simply use a `FileDescriptor` from the `System` module. However, the `FileDescriptor` 20 | /// is not owned, so you will need to make sure the file descriptor is not closed in the sender process before the message is embedded 21 | /// in an XPC message. 22 | public final class XPCFileDescriptor: Codable { 23 | internal let fileDescriptor: Int32 24 | 25 | /// Create an `XPCFileDescriptor` from a raw file descriptor and take the ownership of it. 26 | /// The file descriptor will be closed automatically when this instance is deinitialized. 27 | public init(fileDescriptor: Int32) { 28 | self.fileDescriptor = fileDescriptor 29 | } 30 | 31 | /// Create an `XPCFileDescriptor` from a `FileDescriptor` and take the ownership of it. 32 | /// The file descriptor will be closed automatically when this instance is deinitialized. 33 | @available(macOS 11.0, *) 34 | public init(fileDescriptor: FileDescriptor) { 35 | self.fileDescriptor = fileDescriptor.rawValue 36 | } 37 | 38 | /// Duplicate the file descriptor. The caller is responsible for closing the returned file descriptor. 39 | public func dup() throws -> Int32 { 40 | let fd = Darwin.dup(fileDescriptor) 41 | 42 | if fd < 0 { 43 | if #available(macOS 11.0, *) { 44 | throw Errno(rawValue: errno) 45 | } else { 46 | throw XPCErrorRegistry.BoxedError(domain: "NSPOSIXErrorDomain", code: Int(errno)) 47 | } 48 | } 49 | 50 | return fd 51 | } 52 | 53 | /// Duplicate the file descriptor. The caller is responsible for closing the returned `FileDescriptor`. 54 | @available(macOS 11.0, *) 55 | public func duplicate() throws -> FileDescriptor { 56 | if #available(macOS 12.0, *) { 57 | return try FileDescriptor(rawValue: self.fileDescriptor).duplicate() 58 | } else { 59 | return try FileDescriptor(rawValue: self.dup()) 60 | } 61 | } 62 | 63 | deinit { 64 | close(self.fileDescriptor) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/SwiftyXPCTests/HelperLauncher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelperLauncher.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 10/12/23. 6 | // 7 | 8 | import Foundation 9 | import System 10 | import TestShared 11 | 12 | class HelperLauncher { 13 | let codeSigningRequirement: String 14 | private let plistURL: URL 15 | 16 | private struct LoadError: Error {} 17 | 18 | init() throws { 19 | let bundleURL = Bundle(for: Self.self).bundleURL 20 | let helperURL = bundleURL.deletingLastPathComponent().appending(path: "TestHelper") 21 | 22 | self.plistURL = try Self.writeLaunchdPlist(helperURL: helperURL) 23 | self.codeSigningRequirement = try Self.getCodeSigningRequirement(url: helperURL) 24 | } 25 | 26 | func startHelper() throws { 27 | do { 28 | try self.runLaunchctl(verb: "load") 29 | } catch is LoadError { 30 | try self.runLaunchctl(verb: "unload") 31 | try self.runLaunchctl(verb: "load") 32 | } 33 | } 34 | 35 | func stopHelper() throws { 36 | try self.runLaunchctl(verb: "unload") 37 | } 38 | 39 | private func runLaunchctl(verb: String) throws { 40 | let process = Process() 41 | let stderrPipe = Pipe() 42 | let stderrHandle = stderrPipe.fileHandleForReading 43 | 44 | process.executableURL = URL(filePath: "/bin/launchctl") 45 | process.arguments = [verb, self.plistURL.path] 46 | process.standardError = stderrPipe 47 | 48 | try process.run() 49 | process.waitUntilExit() 50 | 51 | if let stderrData = try stderrHandle.readToEnd(), 52 | let stderr = String(data: stderrData, encoding: .utf8), 53 | stderr.contains("Load failed:") 54 | { 55 | throw LoadError() 56 | } 57 | } 58 | 59 | private static func writeLaunchdPlist(helperURL: URL) throws -> URL { 60 | let plist: [String: Any] = [ 61 | "KeepAlive": true, 62 | "Label": helperID, 63 | "MachServices": [helperID: true], 64 | "Program": helperURL.path, 65 | "RunAtLoad": true, 66 | ] 67 | 68 | let plistURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).plist") 69 | try (plist as NSDictionary).write(to: plistURL) 70 | 71 | return plistURL 72 | } 73 | 74 | private static func getCodeSigningRequirement(url: URL) throws -> String { 75 | var staticCode: SecStaticCode? = nil 76 | var err = SecStaticCodeCreateWithPath(url as CFURL, [], &staticCode) 77 | if err != errSecSuccess { throw Errno(rawValue: err) } 78 | 79 | var req: SecRequirement? = nil 80 | err = SecCodeCopyDesignatedRequirement(staticCode!, [], &req) 81 | if err != errSecSuccess { throw Errno(rawValue: err) } 82 | 83 | var string: CFString? = nil 84 | err = SecRequirementCopyString(req!, [], &string) 85 | if err != errSecSuccess { throw Errno(rawValue: err) } 86 | 87 | return string! as String 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SwiftyXPC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example App/Example App.xcodeproj/xcshareddata/xcschemes/Example App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCError.swift: -------------------------------------------------------------------------------- 1 | import XPC 2 | 3 | /// An XPC-related communication error. 4 | /// 5 | /// To avoid dependencies on Foundation, this does not formally adopt the `LocalizedError` protocol, but it does implement its methods, 6 | /// so you can simply make this class conform to `LocalizedError` with an empty implementation to allow Foundation clients to easily get 7 | /// user-facing error description strings: 8 | /// 9 | /// extension XPCError: LocalizedError {} 10 | /// 11 | /// However, this default implementation is not guaranteed to be correctly localized. 12 | public enum XPCError: Error, Codable, Sendable { 13 | /// Will be delivered to the connection’s error handler if the remote service exited. 14 | /// The connection is still live even in this case, and resending a message will cause the service to be launched on-demand. 15 | /// This error serves as a client’s indication that it should resynchronize any state that it had given the service. 16 | case connectionInterrupted 17 | /// An error that sends to the connection's error handler to indicate that the connection is no longer usable. 18 | case connectionInvalid 19 | /// An error that sends to a peer connection’s error handler when the XPC runtime determines that the program needs to exit 20 | /// and that all outstanding transactions must wind down. 21 | case terminationImminent 22 | /// A code signature requirement passed to a connection or listener was not able to be parsed. 23 | case invalidCodeSignatureRequirement 24 | /// An unknown error. The string parameter represents the description coming from the XPC system. 25 | case unknown(String) 26 | 27 | internal init(error: xpc_object_t) { 28 | if error === XPC_ERROR_CONNECTION_INTERRUPTED { 29 | self = .connectionInterrupted 30 | } else if error === XPC_ERROR_CONNECTION_INVALID { 31 | self = .connectionInvalid 32 | } else if error === XPC_ERROR_TERMINATION_IMMINENT { 33 | self = .terminationImminent 34 | } else { 35 | let errorString = Self.errorString(error: error) 36 | 37 | self = .unknown(errorString) 38 | } 39 | } 40 | 41 | /// A description of the error, intended to be a default implementation for the `LocalizedError` protocol. 42 | /// 43 | /// Is not guaranteed to be localized. 44 | public var errorDescription: String? { self.failureReason } 45 | 46 | /// A string describing the reason that the error occurred, intended to be a default implementation for the `LocalizedError` protocol. 47 | /// 48 | /// Is not guaranteed to be localized. 49 | public var failureReason: String? { 50 | switch self { 51 | case .connectionInterrupted: 52 | return Self.errorString(error: XPC_ERROR_CONNECTION_INTERRUPTED) 53 | case .connectionInvalid: 54 | return Self.errorString(error: XPC_ERROR_CONNECTION_INVALID) 55 | case .terminationImminent: 56 | return Self.errorString(error: XPC_ERROR_TERMINATION_IMMINENT) 57 | case .invalidCodeSignatureRequirement: 58 | return "Invalid Code Signature Requirement" 59 | case .unknown(let failureReason): 60 | return failureReason 61 | } 62 | } 63 | 64 | private static func errorString(error: xpc_object_t) -> String { 65 | if let rawString = xpc_dictionary_get_string(error, XPC_ERROR_KEY_DESCRIPTION) { 66 | return String(cString: rawString) 67 | } else { 68 | return "(unknown error)" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/TestHelper/TestHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelper.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 10/12/23. 6 | // 7 | 8 | import Foundation 9 | import Dispatch 10 | import SwiftyXPC 11 | import TestShared 12 | 13 | @main 14 | @available(macOS 13.0, *) 15 | final class XPCService: Sendable { 16 | static func main() { 17 | do { 18 | let xpcService = XPCService() 19 | 20 | let listener = try XPCListener(type: .machService(name: helperID), codeSigningRequirement: nil) 21 | 22 | listener.setMessageHandler(name: CommandSet.reportIDs, handler: xpcService.reportIDs) 23 | listener.setMessageHandler(name: CommandSet.capitalizeString, handler: xpcService.capitalizeString) 24 | listener.setMessageHandler(name: CommandSet.multiplyBy5, handler: xpcService.multiplyBy5) 25 | listener.setMessageHandler(name: CommandSet.transportData, handler: xpcService.transportData) 26 | listener.setMessageHandler(name: CommandSet.tellAJoke, handler: xpcService.tellAJoke) 27 | listener.setMessageHandler(name: CommandSet.pauseOneSecond, handler: xpcService.pauseOneSecond) 28 | 29 | listener.activate() 30 | dispatchMain() 31 | } catch { 32 | fatalError("Error while setting up XPC service: \(error)") 33 | } 34 | } 35 | 36 | private func reportIDs(connection: XPCConnection) async throws -> ProcessIDs { 37 | try ProcessIDs(connection: connection) 38 | } 39 | 40 | private func capitalizeString(_: XPCConnection, string: String) async throws -> String { 41 | string.uppercased() 42 | } 43 | 44 | private func multiplyBy5(_: XPCConnection, number: Double) async throws -> Double { 45 | number * 5.0 46 | } 47 | 48 | private func transportData(_: XPCConnection, data: Data) async throws -> DataInfo { 49 | guard String(data: data, encoding: .utf8) == "One to beam up" else { 50 | throw DataInfo.DataError(failureReason: "fluctuation in the positronic matrix") 51 | } 52 | 53 | return DataInfo( 54 | characterName: "Lt. Cmdr. Data".data(using: .utf8)!, 55 | playedBy: "Brent Spiner".data(using: .utf8)!, 56 | otherCharacters: [ 57 | "Lore".data(using: .utf8)!, 58 | "B4".data(using: .utf8)!, 59 | "Noonien Soong".data(using: .utf8)!, 60 | "Arik Soong".data(using: .utf8)!, 61 | "Altan Soong".data(using: .utf8)!, 62 | "Adam Soong".data(using: .utf8)! 63 | ] 64 | ) 65 | } 66 | 67 | private func tellAJoke(_: XPCConnection, endpoint: XPCEndpoint) async throws { 68 | let remoteConnection = try XPCConnection( 69 | type: .remoteServiceFromEndpoint(endpoint), 70 | codeSigningRequirement: nil 71 | ) 72 | 73 | remoteConnection.activate() 74 | 75 | let opening: String = try await remoteConnection.sendMessage(name: JokeMessage.askForJoke, request: "Tell me a joke") 76 | 77 | guard opening == "Knock knock" else { 78 | throw JokeMessage.NotAKnockKnockJoke(complaint: "That was not a knock knock joke!") 79 | } 80 | 81 | let whosThere: String = try await remoteConnection.sendMessage(name: JokeMessage.whosThere, request: "Who's there?") 82 | 83 | try await remoteConnection.sendMessage(name: JokeMessage.who, request: "\(whosThere) who?") 84 | 85 | try remoteConnection.sendOnewayMessage(name: JokeMessage.groan, message: "That was awful!") 86 | } 87 | 88 | private func pauseOneSecond(_: XPCConnection) async throws { 89 | try await Task.sleep(for: .seconds(1)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCType.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 12/20/21. 6 | // 7 | 8 | import XPC 9 | 10 | /// Enum representing the type of an XPC object. 11 | public enum XPCType: Codable, Sendable { 12 | case activity 13 | case array 14 | case bool 15 | case connection 16 | case data 17 | case date 18 | case dictionary 19 | case double 20 | case endpoint 21 | case error 22 | case fileDescriptor 23 | case null 24 | case sharedMemory 25 | case string 26 | case signedInteger 27 | case unsignedInteger 28 | case uuid 29 | case unknown(String) 30 | 31 | init(rawType: xpc_type_t) { 32 | switch rawType { 33 | case XPC_TYPE_ACTIVITY: 34 | self = .activity 35 | case XPC_TYPE_ARRAY: 36 | self = .array 37 | case XPC_TYPE_BOOL: 38 | self = .bool 39 | case XPC_TYPE_CONNECTION: 40 | self = .connection 41 | case XPC_TYPE_DATA: 42 | self = .data 43 | case XPC_TYPE_DATE: 44 | self = .date 45 | case XPC_TYPE_DICTIONARY: 46 | self = .dictionary 47 | case XPC_TYPE_DOUBLE: 48 | self = .double 49 | case XPC_TYPE_ENDPOINT: 50 | self = .endpoint 51 | case XPC_TYPE_ERROR: 52 | self = .error 53 | case XPC_TYPE_FD: 54 | self = .fileDescriptor 55 | case XPC_TYPE_NULL: 56 | self = .null 57 | case XPC_TYPE_STRING: 58 | self = .string 59 | case XPC_TYPE_INT64: 60 | self = .signedInteger 61 | case XPC_TYPE_UINT64: 62 | self = .unsignedInteger 63 | case XPC_TYPE_UUID: 64 | self = .uuid 65 | default: 66 | self = .unknown(String(cString: xpc_type_get_name(rawType))) 67 | } 68 | } 69 | 70 | /// The name of this type, which can be useful for debugging purposes. 71 | public var name: String { 72 | switch self { 73 | case .activity: 74 | return String(cString: xpc_type_get_name(XPC_TYPE_ACTIVITY)) 75 | case .array: 76 | return String(cString: xpc_type_get_name(XPC_TYPE_ARRAY)) 77 | case .bool: 78 | return String(cString: xpc_type_get_name(XPC_TYPE_BOOL)) 79 | case .connection: 80 | return String(cString: xpc_type_get_name(XPC_TYPE_CONNECTION)) 81 | case .data: 82 | return String(cString: xpc_type_get_name(XPC_TYPE_DATA)) 83 | case .date: 84 | return String(cString: xpc_type_get_name(XPC_TYPE_DATE)) 85 | case .dictionary: 86 | return String(cString: xpc_type_get_name(XPC_TYPE_DICTIONARY)) 87 | case .double: 88 | return String(cString: xpc_type_get_name(XPC_TYPE_DOUBLE)) 89 | case .endpoint: 90 | return String(cString: xpc_type_get_name(XPC_TYPE_ENDPOINT)) 91 | case .error: 92 | return String(cString: xpc_type_get_name(XPC_TYPE_ERROR)) 93 | case .fileDescriptor: 94 | return String(cString: xpc_type_get_name(XPC_TYPE_FD)) 95 | case .null: 96 | return String(cString: xpc_type_get_name(XPC_TYPE_NULL)) 97 | case .sharedMemory: 98 | return String(cString: xpc_type_get_name(XPC_TYPE_SHMEM)) 99 | case .string: 100 | return String(cString: xpc_type_get_name(XPC_TYPE_STRING)) 101 | case .signedInteger: 102 | return String(cString: xpc_type_get_name(XPC_TYPE_INT64)) 103 | case .unsignedInteger: 104 | return String(cString: xpc_type_get_name(XPC_TYPE_UINT64)) 105 | case .uuid: 106 | return String(cString: xpc_type_get_name(XPC_TYPE_UUID)) 107 | case .unknown(let name): 108 | return name 109 | } 110 | } 111 | } 112 | 113 | extension xpc_object_t { 114 | /// The type of a raw `xpc_object_t`, represented as an `XPCType`. 115 | var type: XPCType { XPCType(rawType: xpc_get_type(self)) } 116 | } 117 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TestHelper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 70 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SwiftyXPC-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 62 | 63 | 64 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 87 | 88 | 94 | 95 | 96 | 97 | 103 | 104 | 110 | 111 | 112 | 113 | 115 | 116 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /Tests/SwiftyXPCTests/SwiftyXPCTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import SwiftyXPC 4 | import System 5 | import TestShared 6 | 7 | // swift-format-ignore: AllPublicDeclarationsHaveDocumentation 8 | final class SwiftyXPCTests: XCTestCase { 9 | var helperLauncher: HelperLauncher? 10 | 11 | override func setUp() async throws { 12 | self.helperLauncher = try HelperLauncher() 13 | try self.helperLauncher?.startHelper() 14 | } 15 | 16 | override func tearDown() async throws { 17 | try self.helperLauncher?.stopHelper() 18 | } 19 | 20 | func testProcessIDs() async throws { 21 | let conn = try self.openConnection() 22 | 23 | let ids: ProcessIDs = try await conn.sendMessage(name: CommandSet.reportIDs) 24 | 25 | XCTAssertEqual(ids.pid, conn.processIdentifier) 26 | XCTAssertEqual(ids.effectiveUID, conn.effectiveUserIdentifier) 27 | XCTAssertEqual(ids.effectiveGID, conn.effectiveGroupIdentifier) 28 | XCTAssertEqual(ids.auditSessionID, conn.auditSessionIdentifier) 29 | } 30 | 31 | func testCodeSignatureVerification() async throws { 32 | let goodConn = try self.openConnection(codeSigningRequirement: self.helperLauncher!.codeSigningRequirement) 33 | 34 | let response: String = try await goodConn.sendMessage(name: CommandSet.capitalizeString, request: "Testing 1 2 3") 35 | XCTAssertEqual(response, "TESTING 1 2 3") 36 | 37 | let badConn = try self.openConnection(codeSigningRequirement: "identifier \"com.apple.true\" and anchor apple") 38 | let failsSignatureVerification = self.expectation( 39 | description: "Fails to send message because of code signature mismatch" 40 | ) 41 | 42 | do { 43 | try await badConn.sendMessage(name: CommandSet.capitalizeString, request: "Testing 1 2 3") 44 | } catch let error as XPCError { 45 | if case .unknown(let errorDesc) = error, errorDesc == "Peer Forbidden" { 46 | failsSignatureVerification.fulfill() 47 | } else { 48 | throw error 49 | } 50 | } 51 | 52 | let failsConnectionInitialization = self.expectation( 53 | description: "Fails to initialize connection because of bad code signing requirement" 54 | ) 55 | 56 | do { 57 | _ = try self.openConnection(codeSigningRequirement: "") 58 | } catch XPCError.invalidCodeSignatureRequirement { 59 | failsConnectionInitialization.fulfill() 60 | } 61 | 62 | await fulfillment(of: [failsSignatureVerification, failsConnectionInitialization], timeout: 10.0) 63 | } 64 | 65 | func testSimpleRequestAndResponse() async throws { 66 | let conn = try self.openConnection() 67 | 68 | let stringResponse: String = try await conn.sendMessage(name: CommandSet.capitalizeString, request: "hi there") 69 | XCTAssertEqual(stringResponse, "HI THERE") 70 | 71 | let doubleResponse: Double = try await conn.sendMessage(name: CommandSet.multiplyBy5, request: 3.7) 72 | XCTAssertEqual(doubleResponse, 18.5, accuracy: 0.001) 73 | } 74 | 75 | func testDataTransport() async throws { 76 | let conn = try self.openConnection() 77 | 78 | let dataInfo: DataInfo = try await conn.sendMessage( 79 | name: CommandSet.transportData, 80 | request: "One to beam up".data(using: .utf8)! 81 | ) 82 | 83 | XCTAssertEqual(String(data: dataInfo.characterName, encoding: .utf8), "Lt. Cmdr. Data") 84 | XCTAssertEqual(String(data: dataInfo.playedBy, encoding: .utf8), "Brent Spiner") 85 | XCTAssertEqual( 86 | dataInfo.otherCharacters.map { String(data: $0, encoding: .utf8) }, 87 | ["Lore", "B4", "Noonien Soong", "Arik Soong", "Altan Soong", "Adam Soong"] 88 | ) 89 | 90 | XPCErrorRegistry.shared.registerDomain(forErrorType: DataInfo.DataError.self) 91 | let failsToSendBadData = self.expectation(description: "Fails to send bad data") 92 | 93 | do { 94 | try await conn.sendMessage(name: CommandSet.transportData, request: "It's Lore being sneaky".data(using: .utf8)!) 95 | } catch let error as DataInfo.DataError { 96 | XCTAssertEqual(error.failureReason, "fluctuation in the positronic matrix") 97 | failsToSendBadData.fulfill() 98 | } 99 | 100 | await fulfillment(of: [failsToSendBadData], timeout: 10.0) 101 | } 102 | 103 | func testTwoWayCommunication() async throws { 104 | let conn = try self.openConnection() 105 | 106 | let listener = try XPCListener(type: .anonymous, codeSigningRequirement: nil) 107 | 108 | let asksForJoke = self.expectation(description: "We will get asked for a joke") 109 | let saysWhosThere = self.expectation(description: "The task will ask who's there") 110 | let asksWho = self.expectation(description: "The task will respond to our query and add 'who?'") 111 | let groans = self.expectation(description: "The task will not appreciate the joke") 112 | let expectations = [asksForJoke, saysWhosThere, asksWho, groans] 113 | expectations.forEach { $0.assertForOverFulfill = true } 114 | 115 | listener.setMessageHandler(name: JokeMessage.askForJoke) { _, response in 116 | XCTAssertEqual(response, "Tell me a joke") 117 | asksForJoke.fulfill() 118 | return "Knock knock" 119 | } 120 | 121 | listener.setMessageHandler(name: JokeMessage.whosThere) { _, response in 122 | XCTAssertEqual(response, "Who's there?") 123 | saysWhosThere.fulfill() 124 | return "Orange" 125 | } 126 | 127 | listener.setMessageHandler(name: JokeMessage.who) { _, response in 128 | XCTAssertEqual(response, "Orange who?") 129 | asksWho.fulfill() 130 | return "Orange you glad this example is so silly?" 131 | } 132 | 133 | listener.setMessageHandler(name: JokeMessage.groan) { _, response in 134 | XCTAssertEqual(response, "That was awful!") 135 | groans.fulfill() 136 | } 137 | 138 | listener.errorHandler = { _, error in 139 | if case .connectionInvalid = error as? XPCError { 140 | // connection can go down once we've received the last message 141 | return 142 | } 143 | 144 | DispatchQueue.main.async { 145 | XCTFail(error.localizedDescription) 146 | } 147 | } 148 | 149 | listener.activate() 150 | 151 | try await conn.sendMessage(name: CommandSet.tellAJoke, request: listener.endpoint) 152 | 153 | await self.fulfillment(of: expectations, timeout: 10.0, enforceOrder: true) 154 | } 155 | 156 | func testTwoWayCommunicationWithError() async throws { 157 | XPCErrorRegistry.shared.registerDomain(forErrorType: JokeMessage.NotAKnockKnockJoke.self) 158 | let conn = try self.openConnection() 159 | 160 | let listener = try XPCListener(type: .anonymous, codeSigningRequirement: nil) 161 | 162 | listener.setMessageHandler(name: JokeMessage.askForJoke) { _, response in 163 | XCTAssertEqual(response, "Tell me a joke") 164 | return "A `foo` walks into a `bar`" 165 | } 166 | 167 | listener.errorHandler = { _, error in 168 | if case .connectionInvalid = error as? XPCError { 169 | // connection can go down once we've received the last message 170 | return 171 | } 172 | 173 | DispatchQueue.main.async { 174 | XCTFail(error.localizedDescription) 175 | } 176 | } 177 | 178 | listener.activate() 179 | 180 | let failsToSendInvalidJoke = self.expectation(description: "Fails to send non-knock-knock joke") 181 | 182 | do { 183 | try await conn.sendMessage(name: CommandSet.tellAJoke, request: listener.endpoint) 184 | } catch let error as JokeMessage.NotAKnockKnockJoke { 185 | XCTAssertEqual(error.complaint, "That was not a knock knock joke!") 186 | failsToSendInvalidJoke.fulfill() 187 | } 188 | 189 | await fulfillment(of: [failsToSendInvalidJoke], timeout: 10.0) 190 | } 191 | 192 | func testOnewayVsTwoWay() async throws { 193 | let conn = try self.openConnection() 194 | 195 | var date = Date.now 196 | try await conn.sendMessage(name: CommandSet.pauseOneSecond) 197 | XCTAssertGreaterThanOrEqual(Date.now.timeIntervalSince(date), 1.0) 198 | 199 | date = Date.now 200 | try conn.sendOnewayMessage(name: CommandSet.pauseOneSecond, message: XPCNull()) 201 | XCTAssertLessThan(Date.now.timeIntervalSince(date), 0.5) 202 | } 203 | 204 | func testCancelConnection() async throws { 205 | let conn = try self.openConnection() 206 | 207 | let response: String = try await conn.sendMessage(name: CommandSet.capitalizeString, request: "will work") 208 | XCTAssertEqual(response, "WILL WORK") 209 | 210 | conn.cancel() 211 | 212 | let err: Error? 213 | do { 214 | _ = try await conn.sendMessage(name: CommandSet.capitalizeString, request: "won't work") as String 215 | err = nil 216 | } catch { 217 | err = error 218 | } 219 | 220 | guard case .connectionInvalid = err as? XPCError else { 221 | XCTFail("Sending message to cancelled connection should throw XPCError.connectionInvalid") 222 | return 223 | } 224 | } 225 | 226 | private func openConnection(codeSigningRequirement: String? = nil) throws -> XPCConnection { 227 | let conn = try XPCConnection( 228 | type: .remoteMachService(serviceName: helperID, isPrivilegedHelperTool: false), 229 | codeSigningRequirement: codeSigningRequirement ?? self.helperLauncher?.codeSigningRequirement 230 | ) 231 | conn.activate() 232 | 233 | return conn 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCErrorRegistry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCErrorRegistry.swift 3 | // 4 | // 5 | // Created by Charles Srstka on 12/19/21. 6 | // 7 | 8 | import Synchronization 9 | import XPC 10 | 11 | /// A registry which facilitates decoding error types that are sent over an XPC connection. 12 | /// 13 | /// If an error is received, it will be looked up in the registry by its domain. 14 | /// If a matching error type exists, that type is used to decode the error using `XPCDecoder`. 15 | /// However, if the error domain is not registered, it will be encapsulated in a `BoxedError` which resembles Foundation's `NSError` class. 16 | /// 17 | /// Use this registry to communicate rich error information without being beholden to `Foundation` user info dictionaries. 18 | /// 19 | /// In the example below, any `MyError`s which are received over the wire will be converted back to a `MyError` enum, allowing handler functions to check for them: 20 | /// 21 | /// enum MyError: Error, Codable { 22 | /// case foo(Int) 23 | /// case bar(String) 24 | /// } 25 | /// 26 | /// // then, at app startup time: 27 | /// 28 | /// func someAppStartFunction() { 29 | /// XPCErrorRegistry.shared.registerDomain(forErrorType: MyError.self) 30 | /// } 31 | /// 32 | /// // and later you can: 33 | /// 34 | /// do { 35 | /// try await connection.sendMessage(name: someName) 36 | /// } catch let error as MyError { 37 | /// switch error { 38 | /// case .foo(let foo): 39 | /// print("got foo: \(foo)") 40 | /// case .bar(let bar): 41 | /// print("got bar: \(bar)") 42 | /// } 43 | /// } catch { 44 | /// print("got some other error") 45 | /// } 46 | public final class XPCErrorRegistry: Sendable { 47 | /// The shared `XPCErrorRegistry` instance. 48 | public static let shared = XPCErrorRegistry() 49 | 50 | @available(macOS 15.0, macCatalyst 18.0, *) 51 | private final class MutexWrapper: Sendable { 52 | let mutex: Mutex<[String: (Error & Codable).Type]> 53 | init(dict: [String: (Error & Codable).Type]) { self.mutex = Mutex(dict) } 54 | } 55 | 56 | private final class LegacyWrapper: @unchecked Sendable { 57 | let sema = DispatchSemaphore(value: 1) 58 | var dict: [String: (Error & Codable).Type] 59 | init(dict: [String: (Error & Codable).Type]) { self.dict = dict } 60 | } 61 | 62 | private let errorDomainMapWrapper: any Sendable = { 63 | let errorDomainMap: [String: (Error & Codable).Type] = [ 64 | String(reflecting: XPCError.self): XPCError.self, 65 | String(reflecting: XPCConnection.Error.self): XPCConnection.Error.self, 66 | ] 67 | 68 | if #available(macOS 15.0, macCatalyst 18.0, *) { 69 | return MutexWrapper(dict: errorDomainMap) 70 | } else { 71 | return LegacyWrapper(dict: errorDomainMap) 72 | } 73 | }() 74 | 75 | private func withLock(closure: (inout [String: (Error & Codable).Type]) throws -> T) rethrows -> T { 76 | if #available(macOS 15.0, macCatalyst 18.0, *) { 77 | return try (self.errorDomainMapWrapper as! MutexWrapper).mutex.withLock { try closure(&$0) } 78 | } else { 79 | let wrapper = self.errorDomainMapWrapper as! LegacyWrapper 80 | wrapper.sema.wait() 81 | defer { wrapper.sema.signal() } 82 | 83 | return try closure(&wrapper.dict) 84 | } 85 | } 86 | 87 | /// Register an error type. 88 | /// 89 | /// - Parameters: 90 | /// - domain: An `NSError`-style domain string to associate with this error type. In most cases, you will just pass `nil` for this parameter, in which case the default value of `String(reflecting: errorType)` will be used instead. 91 | /// - errorType: An error type to register. This type must conform to `Codable`. 92 | public func registerDomain(_ domain: String? = nil, forErrorType errorType: (Error & Codable).Type) { 93 | self.withLock { $0[domain ?? String(reflecting: errorType)] = errorType } 94 | } 95 | 96 | internal func encodeError(_ error: Error, domain: String? = nil) throws -> xpc_object_t { 97 | try self.withLock { _ in 98 | try XPCEncoder().encode(BoxedError(error: error, domain: domain)) 99 | } 100 | } 101 | 102 | internal func decodeError(_ error: xpc_object_t) throws -> Error { 103 | let boxedError = try XPCDecoder().decode(type: BoxedError.self, from: error) 104 | 105 | return boxedError.encodedError ?? boxedError 106 | } 107 | 108 | internal func errorType(forDomain domain: String) -> (any (Error & Codable).Type)? { 109 | self.withLock { $0[domain] } 110 | } 111 | 112 | /// An error type representing errors for which we have an `NSError`-style domain and code, but do not know the exact error class. 113 | /// 114 | /// To avoid requiring Foundation, this type does not formally adopt the `CustomNSError` protocol, but implements methods which 115 | /// can be used as a default implementation of the protocol. Foundation clients may want to add an empty implementation as in the example below. 116 | /// 117 | /// extension XPCErrorRegistry.BoxedError: CustomNSError {} 118 | public struct BoxedError: Error, Codable { 119 | private enum Storage { 120 | case codable(Error & Codable) 121 | case uncodable(code: Int) 122 | } 123 | 124 | private enum Key: CodingKey { 125 | case domain 126 | case code 127 | case encodedError 128 | } 129 | 130 | private let storage: Storage 131 | 132 | /// An `NSError`-style error domain. 133 | public let errorDomain: String 134 | 135 | /// An `NSError`-style error code. 136 | public var errorCode: Int { 137 | switch self.storage { 138 | case .codable(let error): 139 | return error._code 140 | case .uncodable(let code): 141 | return code 142 | } 143 | } 144 | 145 | /// An `NSError`-style user info dictionary. 146 | public var errorUserInfo: [String: Any] { [:] } 147 | 148 | /// Hacky default implementation for internal `Error` requirements. 149 | /// 150 | /// This isn't great, but it allows this class to have basic functionality without depending on Foundation. 151 | /// 152 | /// Give `BoxedError` a default implementation of `CustomNSError` in Foundation clients to avoid this being called. 153 | public var _domain: String { self.errorDomain } 154 | 155 | /// Hacky default implementation for internal `Error` requirements. 156 | /// 157 | /// This isn't great, but it allows this class to have basic functionality without depending on Foundation. 158 | /// 159 | /// Give `BoxedError` a default implementation of `CustomNSError` to avoid this being called. 160 | public var _code: Int { self.errorCode } 161 | 162 | fileprivate var encodedError: Error? { 163 | switch self.storage { 164 | case .codable(let error): 165 | return error 166 | case .uncodable: 167 | return nil 168 | } 169 | } 170 | 171 | internal init(domain: String, code: Int) { 172 | self.errorDomain = domain 173 | self.storage = .uncodable(code: code) 174 | } 175 | 176 | internal init(error: Error, domain: String? = nil) { 177 | self.errorDomain = domain ?? error._domain 178 | 179 | if let codableError = error as? (Error & Codable) { 180 | self.storage = .codable(codableError) 181 | } else { 182 | self.storage = .uncodable(code: error._code) 183 | } 184 | } 185 | 186 | /// Included for `Decodable` conformance. 187 | /// 188 | /// - Parameter decoder: A decoder. 189 | /// 190 | /// - Throws: Any errors that come up in the process of decoding the error. 191 | public init(from decoder: Decoder) throws { 192 | let container = try decoder.container(keyedBy: Key.self) 193 | 194 | self.errorDomain = try container.decode(String.self, forKey: .domain) 195 | let code = try container.decode(Int.self, forKey: .code) 196 | 197 | if let codableType = XPCErrorRegistry.shared.errorType(forDomain: self.errorDomain), 198 | let codableError = try codableType.decodeIfPresent(from: container, key: .encodedError) 199 | { 200 | self.storage = .codable(codableError) 201 | } else { 202 | self.storage = .uncodable(code: code) 203 | } 204 | } 205 | 206 | /// Included for `Encodable` conformance. 207 | /// 208 | /// - Parameter encoder: An encoder. 209 | /// 210 | /// - Throws: Any errors that come up in the process of encoding the error. 211 | public func encode(to encoder: Encoder) throws { 212 | var container = encoder.container(keyedBy: Key.self) 213 | 214 | try container.encode(self.errorDomain, forKey: .domain) 215 | try container.encode(self.errorCode, forKey: .code) 216 | 217 | if case .codable(let error) = self.storage { 218 | try error.encode(into: &container, forKey: .encodedError) 219 | } 220 | } 221 | } 222 | } 223 | 224 | extension Error where Self: Codable { 225 | fileprivate static func decode(from error: xpc_object_t, using decoder: XPCDecoder) throws -> Error { 226 | try decoder.decode(type: self, from: error) 227 | } 228 | 229 | fileprivate static func decodeIfPresent(from keyedContainer: KeyedDecodingContainer, key: Key) throws -> Self? 230 | { 231 | try keyedContainer.decodeIfPresent(Self.self, forKey: key) 232 | } 233 | 234 | fileprivate func encode(using encoder: XPCEncoder) throws -> xpc_object_t { 235 | try encoder.encode(self) 236 | } 237 | 238 | fileprivate func encode(into keyedContainer: inout KeyedEncodingContainer, forKey key: Key) throws { 239 | try keyedContainer.encode(self, forKey: key) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCListener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCListener.swift 3 | // SwiftyXPC 4 | // 5 | // Created by Charles Srstka on 8/8/21. 6 | // 7 | 8 | import System 9 | import XPC 10 | import os 11 | 12 | /// A listener that waits for new incoming connections, configures them, and accepts or rejects them. 13 | /// 14 | /// Each XPC service, launchd agent, or launchd daemon typically has at least one `XPCListener` object that listens for connections to a specified service name. 15 | /// 16 | /// Create an `XPCListener` via `init(type:codeSigningRequirement:)`. 17 | /// 18 | /// Use the `setMessageHandler` family of functions to set up handler functions to receive messages. 19 | /// 20 | /// A listener must receive `.activate()` before it can receive any messages. 21 | public final class XPCListener { 22 | /// The type of the listener. 23 | public enum ListenerType { 24 | /// An anonymous listener connection. This can be passed to other processes by embedding its `endpoint` in an XPC message. 25 | case anonymous 26 | /// A service listener used to listen for incoming connections in an embedded XPC service. Requires the service’s `Info.plist` to be configured correctly. 27 | case service 28 | /// A service listener used to listen for incoming connections in a Mach service, advertised to the system by the given `name`. 29 | case machService(name: String) 30 | } 31 | 32 | private enum Backing { 33 | case xpcMain 34 | case connection(connection: XPCConnection, isMulti: Bool) 35 | } 36 | 37 | /// The type of this listener. 38 | public let type: ListenerType 39 | private let backing: Backing 40 | 41 | /// Returns an endpoint object that may be sent over an existing connection. 42 | /// 43 | /// The receiver of the endpoint can use this object to create a new connection to this `XPCListener` object. 44 | /// The resulting `XPCEndpoint` object uniquely names this listener object across connections. 45 | public var endpoint: XPCEndpoint { 46 | switch self.backing { 47 | case .xpcMain: 48 | fatalError("Can't get endpoint for main service listener") 49 | case .connection(let connection, _): 50 | return connection.makeEndpoint() 51 | } 52 | } 53 | 54 | // because xpc_main takes a C function that can't capture any context, we need to store the xpc_main listener globally 55 | private class XPCMainListenerStorage: @unchecked Sendable { 56 | var xpcMainListener: XPCListener? = nil 57 | } 58 | private static let xpcMainListenerStorage = XPCMainListenerStorage() 59 | 60 | private var _messageHandlers: [String: XPCConnection.MessageHandler] = [:] 61 | private var messageHandlers: [String: XPCConnection.MessageHandler] { 62 | if case .connection(let connection, let isMulti) = self.backing, !isMulti { 63 | return connection.messageHandlers 64 | } 65 | 66 | return self._messageHandlers 67 | } 68 | 69 | /// Set a message handler for an incoming message, identified by the `name` parameter, without taking any arguments or returning any value. 70 | /// 71 | /// - Parameters: 72 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:)`. 73 | /// - handler: Pass a function that processes the message, optionally throwing an error. 74 | public func setMessageHandler(name: String, handler: @escaping (XPCConnection) async throws -> Void) { 75 | self.setMessageHandler(name: name) { (connection: XPCConnection, _: XPCNull) async throws -> XPCNull in 76 | try await handler(connection) 77 | return XPCNull.shared 78 | } 79 | } 80 | 81 | /// Set a message handler for an incoming message, identified by the `name` parameter, taking an argument but not returning any value. 82 | /// 83 | /// - Parameters: 84 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:request:)`. 85 | /// - handler: Pass a function that processes the message, optionall throwing an error. The request value must 86 | /// conform to the `Codable` protocol, and will automatically be type-checked by `XPCConnection` upon receiving a message. 87 | public func setMessageHandler( 88 | name: String, 89 | handler: @escaping (XPCConnection, Request) async throws -> Void 90 | ) { 91 | self.setMessageHandler(name: name) { (connection: XPCConnection, request: Request) async throws -> XPCNull in 92 | try await handler(connection, request) 93 | return XPCNull.shared 94 | } 95 | } 96 | 97 | /// Set a message handler for an incoming message, identified by the `name` parameter, without taking any arguments but returning a value. 98 | /// 99 | /// - Parameters: 100 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:)`. 101 | /// - handler: Pass a function that processes the message and either returns a value or throws an error. The return value must 102 | /// conform to the `Codable` protocol. 103 | public func setMessageHandler( 104 | name: String, 105 | handler: @escaping (XPCConnection) async throws -> Response 106 | ) { 107 | self.setMessageHandler(name: name) { (connection: XPCConnection, _: XPCNull) async throws -> Response in 108 | try await handler(connection) 109 | } 110 | } 111 | 112 | /// Set a message handler for an incoming message, identified by the `name` parameter, taking an argument and returning a value. 113 | /// 114 | /// Example usage: 115 | /// 116 | /// listener.setMessageHandler( 117 | /// name: "com.example.SayHello", 118 | /// handler: self.sayHello 119 | /// ) 120 | /// 121 | /// // ... later in the same class ... 122 | /// 123 | /// func sayHello( 124 | /// connection: XPCConnection, 125 | /// message: String 126 | /// ) async throws -> String { 127 | /// self.logger.notice("Caller sent message: \(message)") 128 | /// 129 | /// if message == "Hello, World!" { 130 | /// return "Hello back!" 131 | /// } else { 132 | /// throw RudeCallerError(message: "You didn't say hello!") 133 | /// } 134 | /// } 135 | /// 136 | /// - Parameters: 137 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:request:)`. 138 | /// - handler: Pass a function that processes the message and either returns a value or throws an error. Both the request and return values must conform to the `Codable` protocol. The types are automatically type-checked by `XPCConnection` upon receiving a message. 139 | public func setMessageHandler( 140 | name: String, 141 | handler: @escaping (XPCConnection, Request) async throws -> Response 142 | ) { 143 | if case .connection(let connection, let isMulti) = self.backing, !isMulti { 144 | connection.setMessageHandler(name: name, handler: handler) 145 | return 146 | } 147 | 148 | self._messageHandlers[name] = XPCConnection.MessageHandler(closure: handler) 149 | } 150 | 151 | private let _codeSigningRequirement: String? 152 | 153 | private var _errorHandler: XPCConnection.ErrorHandler? = nil 154 | 155 | /// A handler that will be called if a communication error occurs. 156 | public var errorHandler: XPCConnection.ErrorHandler? { 157 | get { 158 | if case .connection(let connection, let isMulti) = self.backing, !isMulti { 159 | return connection.errorHandler 160 | } 161 | 162 | return self._errorHandler 163 | } 164 | set { 165 | if case .connection(let connection, let isMulti) = self.backing, !isMulti { 166 | connection.errorHandler = newValue 167 | } else { 168 | self._errorHandler = newValue 169 | } 170 | } 171 | } 172 | 173 | /// Create a new `XPCListener`. 174 | /// 175 | /// - Parameters: 176 | /// - type: The type of listener to create. Check the documentation for `ListenerType` for possible values. 177 | /// - requirement: An optional code signing requirement. If specified, the listener will reject all messages from processes that do not meet the specified requirement. 178 | /// 179 | /// - Throws: Any errors that come up in the process of creating the listener. 180 | public init(type: ListenerType, codeSigningRequirement requirement: String?) throws { 181 | self.type = type 182 | 183 | switch type { 184 | case .anonymous: 185 | self._codeSigningRequirement = nil 186 | self.backing = .connection( 187 | connection: try XPCConnection.makeAnonymousListenerConnection(codeSigningRequirement: requirement), 188 | isMulti: false 189 | ) 190 | case .service: 191 | self.backing = .xpcMain 192 | self._codeSigningRequirement = requirement 193 | case .machService(let name): 194 | let connection = try XPCConnection( 195 | machServiceName: name, 196 | flags: XPC_CONNECTION_MACH_SERVICE_LISTENER, 197 | codeSigningRequirement: requirement 198 | ) 199 | 200 | self._codeSigningRequirement = nil 201 | self.backing = .connection(connection: connection, isMulti: true) 202 | } 203 | 204 | self.setUpConnection(requirement: requirement) 205 | } 206 | 207 | private func setUpConnection(requirement: String?) { 208 | switch self.backing { 209 | case .xpcMain: 210 | Self.xpcMainListenerStorage.xpcMainListener = self 211 | case .connection(let connection, _): 212 | connection.customEventHandler = { [weak self] in 213 | do { 214 | guard case .connection = $0.type else { 215 | preconditionFailure("XPCListener is required to have connection backing when run as a Mach service") 216 | } 217 | 218 | let newConnection = try XPCConnection(connection: $0, codeSigningRequirement: requirement) 219 | 220 | newConnection.messageHandlers = self?.messageHandlers ?? [:] 221 | newConnection.errorHandler = self?.errorHandler 222 | 223 | newConnection.activate() 224 | } catch { 225 | self?.errorHandler?(connection, error) 226 | } 227 | } 228 | } 229 | } 230 | 231 | /// Cancels the listener and ensures that its event handler doesn't fire again. 232 | /// 233 | /// After this call, any messages that have not yet been sent will be discarded, and the connection will be unwound. 234 | /// If there are messages that are awaiting replies, they will receive the `XPCError.connectionInvalid` error. 235 | public func cancel() { 236 | switch self.backing { 237 | case .xpcMain: 238 | fatalError("XPC service listener cannot be cancelled") 239 | case .connection(let connection, _): 240 | connection.cancel() 241 | } 242 | } 243 | 244 | /// Activate the connection. 245 | /// 246 | /// Listeners start in an inactive state, so you must call `activate()` on a connection before it will send or receive any messages. 247 | public func activate() { 248 | switch self.backing { 249 | case .xpcMain: 250 | xpc_main { 251 | do { 252 | let xpcMainListener = XPCListener.xpcMainListenerStorage.xpcMainListener! 253 | 254 | let connection = try XPCConnection( 255 | connection: $0, 256 | codeSigningRequirement: xpcMainListener._codeSigningRequirement 257 | ) 258 | 259 | connection.messageHandlers = xpcMainListener._messageHandlers 260 | connection.errorHandler = xpcMainListener._errorHandler 261 | 262 | connection.activate() 263 | } catch { 264 | os_log(.fault, "Can’t initialize incoming XPC connection!") 265 | } 266 | } 267 | case .connection(let connection, _): 268 | connection.activate() 269 | } 270 | } 271 | 272 | /// Suspends the listener so that the event handler block doesn't fire and the listener doesn't attempt to send any messages it has in its queue. 273 | /// 274 | /// All calls to `suspend()` must be balanced with calls to `resume()` before releasing the last reference to the listener. 275 | /// 276 | /// Suspension is asynchronous and non-preemptive, and therefore this method will not interrupt the execution of an already-running event handler block. 277 | /// If the event handler is executing at the time of this call, it will finish, and then the listener will be suspended before the next scheduled invocation of 278 | /// the event handler. The XPC runtime guarantees this non-preemptiveness even for concurrent target queues. 279 | /// 280 | /// Listeners initialized with the `.service` type cannot be suspended, so calling `.suspend()` on these listeners is considered an error. 281 | public func suspend() { 282 | switch self.backing { 283 | case .xpcMain: 284 | fatalError("XPC service listener cannot be suspended") 285 | case .connection(let connection, _): 286 | connection.suspend() 287 | } 288 | } 289 | 290 | /// Resumes a suspended listener. 291 | /// 292 | /// In order for a listener to become live, every call to `suspend()` must be balanced with a call to `resume()`. 293 | /// Calling `resume()` more times than `suspend()` has been called is considered an error. 294 | /// 295 | /// Listeners initialized with the `.service` type cannot be suspended, so calling `.resume()` on these listeners is considered an error. 296 | public func resume() { 297 | switch self.backing { 298 | case .xpcMain: 299 | fatalError("XPC service listener cannot be resumed") 300 | case .connection(let connection, _): 301 | connection.resume() 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /Example App/Example App.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 288C50152AD8C2A7000DD185 /* SwiftyXPC in Frameworks */ = {isa = PBXBuildFile; productRef = 288C50142AD8C2A7000DD185 /* SwiftyXPC */; }; 11 | 28AD8F6A2ADA5DBB00EAB83A /* SwiftyXPC in Frameworks */ = {isa = PBXBuildFile; productRef = 28AD8F692ADA5DBB00EAB83A /* SwiftyXPC */; }; 12 | 28F54817282486160069100A /* Example_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F54816282486160069100A /* Example_App.swift */; }; 13 | 28F54819282486160069100A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F54818282486160069100A /* ContentView.swift */; }; 14 | 28F54837282486B60069100A /* Example XPC Service.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 28F5482C282486B60069100A /* Example XPC Service.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 15 | 28F54840282487910069100A /* XPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F5483F282487910069100A /* XPCService.swift */; }; 16 | 28F54843282489E60069100A /* CommandSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F54842282489E60069100A /* CommandSet.swift */; }; 17 | 28F5484428248D3D0069100A /* CommandSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F54842282489E60069100A /* CommandSet.swift */; }; 18 | 28F5484628248E7B0069100A /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F5484528248E7B0069100A /* MessageSender.swift */; }; 19 | 28F54848282490950069100A /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F54847282490950069100A /* Errors.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 28F54835282486B60069100A /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 28F5480B282486160069100A /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 28F5482B282486B60069100A; 28 | remoteInfo = "Example XPC Service"; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXCopyFilesBuildPhase section */ 33 | 28F5483B282486B60069100A /* Embed XPC Services */ = { 34 | isa = PBXCopyFilesBuildPhase; 35 | buildActionMask = 2147483647; 36 | dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; 37 | dstSubfolderSpec = 16; 38 | files = ( 39 | 28F54837282486B60069100A /* Example XPC Service.xpc in Embed XPC Services */, 40 | ); 41 | name = "Embed XPC Services"; 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXCopyFilesBuildPhase section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 28F54813282486160069100A /* Example App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 28F54816282486160069100A /* Example_App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example_App.swift; sourceTree = ""; }; 49 | 28F54818282486160069100A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50 | 28F5481F282486180069100A /* Example_App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example_App.entitlements; sourceTree = ""; }; 51 | 28F5482C282486B60069100A /* Example XPC Service.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "Example XPC Service.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 28F54834282486B60069100A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 28F5483F282487910069100A /* XPCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCService.swift; sourceTree = ""; }; 54 | 28F54842282489E60069100A /* CommandSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandSet.swift; sourceTree = ""; }; 55 | 28F5484528248E7B0069100A /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = ""; }; 56 | 28F54847282490950069100A /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 28F54810282486160069100A /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | 28AD8F6A2ADA5DBB00EAB83A /* SwiftyXPC in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | 28F54829282486B60069100A /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | 288C50152AD8C2A7000DD185 /* SwiftyXPC in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | 28F5480A282486160069100A = { 80 | isa = PBXGroup; 81 | children = ( 82 | 28F54815282486160069100A /* Example App */, 83 | 28F5482D282486B60069100A /* Example XPC Service */, 84 | 28F54814282486160069100A /* Products */, 85 | 28F5483C282486D20069100A /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 28F54814282486160069100A /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 28F54813282486160069100A /* Example App.app */, 93 | 28F5482C282486B60069100A /* Example XPC Service.xpc */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 28F54815282486160069100A /* Example App */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 28F54816282486160069100A /* Example_App.swift */, 102 | 28F54818282486160069100A /* ContentView.swift */, 103 | 28F5484528248E7B0069100A /* MessageSender.swift */, 104 | 28F54847282490950069100A /* Errors.swift */, 105 | 28F5481F282486180069100A /* Example_App.entitlements */, 106 | ); 107 | path = "Example App"; 108 | sourceTree = ""; 109 | }; 110 | 28F5482D282486B60069100A /* Example XPC Service */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 28F54842282489E60069100A /* CommandSet.swift */, 114 | 28F5483F282487910069100A /* XPCService.swift */, 115 | 28F54834282486B60069100A /* Info.plist */, 116 | ); 117 | path = "Example XPC Service"; 118 | sourceTree = ""; 119 | }; 120 | 28F5483C282486D20069100A /* Frameworks */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | ); 124 | name = Frameworks; 125 | sourceTree = ""; 126 | }; 127 | /* End PBXGroup section */ 128 | 129 | /* Begin PBXNativeTarget section */ 130 | 28F54812282486160069100A /* Example App */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = 28F54822282486180069100A /* Build configuration list for PBXNativeTarget "Example App" */; 133 | buildPhases = ( 134 | 28F5480F282486160069100A /* Sources */, 135 | 28F54810282486160069100A /* Frameworks */, 136 | 28F54811282486160069100A /* Resources */, 137 | 28F5483B282486B60069100A /* Embed XPC Services */, 138 | ); 139 | buildRules = ( 140 | ); 141 | dependencies = ( 142 | 28F54836282486B60069100A /* PBXTargetDependency */, 143 | ); 144 | name = "Example App"; 145 | packageProductDependencies = ( 146 | 28AD8F692ADA5DBB00EAB83A /* SwiftyXPC */, 147 | ); 148 | productName = "Example App"; 149 | productReference = 28F54813282486160069100A /* Example App.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | 28F5482B282486B60069100A /* Example XPC Service */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 28F54838282486B60069100A /* Build configuration list for PBXNativeTarget "Example XPC Service" */; 155 | buildPhases = ( 156 | 28F54828282486B60069100A /* Sources */, 157 | 28F54829282486B60069100A /* Frameworks */, 158 | 28F5482A282486B60069100A /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | 28AD8F682ADA5DA500EAB83A /* PBXTargetDependency */, 164 | ); 165 | name = "Example XPC Service"; 166 | packageProductDependencies = ( 167 | 288C50142AD8C2A7000DD185 /* SwiftyXPC */, 168 | ); 169 | productName = "Example XPC Service"; 170 | productReference = 28F5482C282486B60069100A /* Example XPC Service.xpc */; 171 | productType = "com.apple.product-type.xpc-service"; 172 | }; 173 | /* End PBXNativeTarget section */ 174 | 175 | /* Begin PBXProject section */ 176 | 28F5480B282486160069100A /* Project object */ = { 177 | isa = PBXProject; 178 | attributes = { 179 | BuildIndependentTargetsInParallel = 1; 180 | LastSwiftUpdateCheck = 1330; 181 | LastUpgradeCheck = 1330; 182 | TargetAttributes = { 183 | 28F54812282486160069100A = { 184 | CreatedOnToolsVersion = 13.3.1; 185 | }; 186 | 28F5482B282486B60069100A = { 187 | CreatedOnToolsVersion = 13.3.1; 188 | LastSwiftMigration = 1330; 189 | }; 190 | }; 191 | }; 192 | buildConfigurationList = 28F5480E282486160069100A /* Build configuration list for PBXProject "Example App" */; 193 | compatibilityVersion = "Xcode 13.0"; 194 | developmentRegion = en; 195 | hasScannedForEncodings = 0; 196 | knownRegions = ( 197 | en, 198 | Base, 199 | ); 200 | mainGroup = 28F5480A282486160069100A; 201 | packageReferences = ( 202 | 28AD8F642ADA5D8F00EAB83A /* XCRemoteSwiftPackageReference "SwiftyXPC" */, 203 | ); 204 | productRefGroup = 28F54814282486160069100A /* Products */; 205 | projectDirPath = ""; 206 | projectRoot = ""; 207 | targets = ( 208 | 28F54812282486160069100A /* Example App */, 209 | 28F5482B282486B60069100A /* Example XPC Service */, 210 | ); 211 | }; 212 | /* End PBXProject section */ 213 | 214 | /* Begin PBXResourcesBuildPhase section */ 215 | 28F54811282486160069100A /* Resources */ = { 216 | isa = PBXResourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | 28F5482A282486B60069100A /* Resources */ = { 223 | isa = PBXResourcesBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXResourcesBuildPhase section */ 230 | 231 | /* Begin PBXSourcesBuildPhase section */ 232 | 28F5480F282486160069100A /* Sources */ = { 233 | isa = PBXSourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 28F5484628248E7B0069100A /* MessageSender.swift in Sources */, 237 | 28F54819282486160069100A /* ContentView.swift in Sources */, 238 | 28F5484428248D3D0069100A /* CommandSet.swift in Sources */, 239 | 28F54848282490950069100A /* Errors.swift in Sources */, 240 | 28F54817282486160069100A /* Example_App.swift in Sources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | 28F54828282486B60069100A /* Sources */ = { 245 | isa = PBXSourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 28F54843282489E60069100A /* CommandSet.swift in Sources */, 249 | 28F54840282487910069100A /* XPCService.swift in Sources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | /* End PBXSourcesBuildPhase section */ 254 | 255 | /* Begin PBXTargetDependency section */ 256 | 28AD8F682ADA5DA500EAB83A /* PBXTargetDependency */ = { 257 | isa = PBXTargetDependency; 258 | productRef = 28AD8F672ADA5DA500EAB83A /* SwiftyXPC */; 259 | }; 260 | 28F54836282486B60069100A /* PBXTargetDependency */ = { 261 | isa = PBXTargetDependency; 262 | target = 28F5482B282486B60069100A /* Example XPC Service */; 263 | targetProxy = 28F54835282486B60069100A /* PBXContainerItemProxy */; 264 | }; 265 | /* End PBXTargetDependency section */ 266 | 267 | /* Begin XCBuildConfiguration section */ 268 | 28F54820282486180069100A /* Debug */ = { 269 | isa = XCBuildConfiguration; 270 | buildSettings = { 271 | ALWAYS_SEARCH_USER_PATHS = NO; 272 | CLANG_ANALYZER_NONNULL = YES; 273 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 274 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 275 | CLANG_ENABLE_MODULES = YES; 276 | CLANG_ENABLE_OBJC_ARC = YES; 277 | CLANG_ENABLE_OBJC_WEAK = YES; 278 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 279 | CLANG_WARN_BOOL_CONVERSION = YES; 280 | CLANG_WARN_COMMA = YES; 281 | CLANG_WARN_CONSTANT_CONVERSION = YES; 282 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 283 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 284 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 285 | CLANG_WARN_EMPTY_BODY = YES; 286 | CLANG_WARN_ENUM_CONVERSION = YES; 287 | CLANG_WARN_INFINITE_RECURSION = YES; 288 | CLANG_WARN_INT_CONVERSION = YES; 289 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 290 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 291 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 293 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 294 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 295 | CLANG_WARN_STRICT_PROTOTYPES = YES; 296 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 297 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 298 | CLANG_WARN_UNREACHABLE_CODE = YES; 299 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 300 | COPY_PHASE_STRIP = NO; 301 | DEBUG_INFORMATION_FORMAT = dwarf; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | ENABLE_TESTABILITY = YES; 304 | GCC_C_LANGUAGE_STANDARD = gnu11; 305 | GCC_DYNAMIC_NO_PIC = NO; 306 | GCC_NO_COMMON_BLOCKS = YES; 307 | GCC_OPTIMIZATION_LEVEL = 0; 308 | GCC_PREPROCESSOR_DEFINITIONS = ( 309 | "DEBUG=1", 310 | "$(inherited)", 311 | ); 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | MACOSX_DEPLOYMENT_TARGET = 13.0; 319 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 320 | MTL_FAST_MATH = YES; 321 | ONLY_ACTIVE_ARCH = YES; 322 | SDKROOT = macosx; 323 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 324 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 325 | }; 326 | name = Debug; 327 | }; 328 | 28F54821282486180069100A /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ALWAYS_SEARCH_USER_PATHS = NO; 332 | CLANG_ANALYZER_NONNULL = YES; 333 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 335 | CLANG_ENABLE_MODULES = YES; 336 | CLANG_ENABLE_OBJC_ARC = YES; 337 | CLANG_ENABLE_OBJC_WEAK = YES; 338 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 339 | CLANG_WARN_BOOL_CONVERSION = YES; 340 | CLANG_WARN_COMMA = YES; 341 | CLANG_WARN_CONSTANT_CONVERSION = YES; 342 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 343 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 344 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INFINITE_RECURSION = YES; 348 | CLANG_WARN_INT_CONVERSION = YES; 349 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 350 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 351 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 353 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 354 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 355 | CLANG_WARN_STRICT_PROTOTYPES = YES; 356 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 357 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 358 | CLANG_WARN_UNREACHABLE_CODE = YES; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | COPY_PHASE_STRIP = NO; 361 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 362 | ENABLE_NS_ASSERTIONS = NO; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | GCC_C_LANGUAGE_STANDARD = gnu11; 365 | GCC_NO_COMMON_BLOCKS = YES; 366 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 367 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 368 | GCC_WARN_UNDECLARED_SELECTOR = YES; 369 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 370 | GCC_WARN_UNUSED_FUNCTION = YES; 371 | GCC_WARN_UNUSED_VARIABLE = YES; 372 | MACOSX_DEPLOYMENT_TARGET = 13.0; 373 | MTL_ENABLE_DEBUG_INFO = NO; 374 | MTL_FAST_MATH = YES; 375 | SDKROOT = macosx; 376 | SWIFT_COMPILATION_MODE = wholemodule; 377 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 378 | }; 379 | name = Release; 380 | }; 381 | 28F54823282486180069100A /* Debug */ = { 382 | isa = XCBuildConfiguration; 383 | buildSettings = { 384 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 386 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 387 | CODE_SIGN_ENTITLEMENTS = "Example App/Example_App.entitlements"; 388 | CODE_SIGN_STYLE = Automatic; 389 | COMBINE_HIDPI_IMAGES = YES; 390 | CURRENT_PROJECT_VERSION = 1; 391 | DEVELOPMENT_TEAM = HRLUCP7QP4; 392 | ENABLE_HARDENED_RUNTIME = YES; 393 | ENABLE_PREVIEWS = YES; 394 | GENERATE_INFOPLIST_FILE = YES; 395 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 396 | LD_RUNPATH_SEARCH_PATHS = ( 397 | "$(inherited)", 398 | "@executable_path/../Frameworks", 399 | ); 400 | MARKETING_VERSION = 1.0; 401 | PRODUCT_BUNDLE_IDENTIFIER = "com.charlessoft.Example-App"; 402 | PRODUCT_NAME = "$(TARGET_NAME)"; 403 | SWIFT_EMIT_LOC_STRINGS = YES; 404 | SWIFT_VERSION = 5.0; 405 | }; 406 | name = Debug; 407 | }; 408 | 28F54824282486180069100A /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 412 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 413 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 414 | CODE_SIGN_ENTITLEMENTS = "Example App/Example_App.entitlements"; 415 | CODE_SIGN_STYLE = Automatic; 416 | COMBINE_HIDPI_IMAGES = YES; 417 | CURRENT_PROJECT_VERSION = 1; 418 | DEVELOPMENT_TEAM = HRLUCP7QP4; 419 | ENABLE_HARDENED_RUNTIME = YES; 420 | ENABLE_PREVIEWS = YES; 421 | GENERATE_INFOPLIST_FILE = YES; 422 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 423 | LD_RUNPATH_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "@executable_path/../Frameworks", 426 | ); 427 | MARKETING_VERSION = 1.0; 428 | PRODUCT_BUNDLE_IDENTIFIER = "com.charlessoft.Example-App"; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | SWIFT_EMIT_LOC_STRINGS = YES; 431 | SWIFT_VERSION = 5.0; 432 | }; 433 | name = Release; 434 | }; 435 | 28F54839282486B60069100A /* Debug */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | CLANG_ENABLE_MODULES = YES; 439 | CODE_SIGN_STYLE = Automatic; 440 | COMBINE_HIDPI_IMAGES = YES; 441 | CURRENT_PROJECT_VERSION = 1; 442 | DEVELOPMENT_TEAM = HRLUCP7QP4; 443 | ENABLE_HARDENED_RUNTIME = YES; 444 | GENERATE_INFOPLIST_FILE = YES; 445 | INFOPLIST_FILE = "Example XPC Service/Info.plist"; 446 | INFOPLIST_KEY_CFBundleDisplayName = "Example XPC Service"; 447 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 448 | LD_RUNPATH_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "@executable_path/../Frameworks", 451 | "@loader_path/../Frameworks", 452 | ); 453 | MARKETING_VERSION = 1.0; 454 | PRODUCT_BUNDLE_IDENTIFIER = "com.charlessoft.SwiftyXPC.Example-App.xpc"; 455 | PRODUCT_NAME = "$(TARGET_NAME)"; 456 | SKIP_INSTALL = YES; 457 | SWIFT_EMIT_LOC_STRINGS = YES; 458 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 459 | SWIFT_VERSION = 5.0; 460 | }; 461 | name = Debug; 462 | }; 463 | 28F5483A282486B60069100A /* Release */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | CLANG_ENABLE_MODULES = YES; 467 | CODE_SIGN_STYLE = Automatic; 468 | COMBINE_HIDPI_IMAGES = YES; 469 | CURRENT_PROJECT_VERSION = 1; 470 | DEVELOPMENT_TEAM = HRLUCP7QP4; 471 | ENABLE_HARDENED_RUNTIME = YES; 472 | GENERATE_INFOPLIST_FILE = YES; 473 | INFOPLIST_FILE = "Example XPC Service/Info.plist"; 474 | INFOPLIST_KEY_CFBundleDisplayName = "Example XPC Service"; 475 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 476 | LD_RUNPATH_SEARCH_PATHS = ( 477 | "$(inherited)", 478 | "@executable_path/../Frameworks", 479 | "@loader_path/../Frameworks", 480 | ); 481 | MARKETING_VERSION = 1.0; 482 | PRODUCT_BUNDLE_IDENTIFIER = "com.charlessoft.SwiftyXPC.Example-App.xpc"; 483 | PRODUCT_NAME = "$(TARGET_NAME)"; 484 | SKIP_INSTALL = YES; 485 | SWIFT_EMIT_LOC_STRINGS = YES; 486 | SWIFT_VERSION = 5.0; 487 | }; 488 | name = Release; 489 | }; 490 | /* End XCBuildConfiguration section */ 491 | 492 | /* Begin XCConfigurationList section */ 493 | 28F5480E282486160069100A /* Build configuration list for PBXProject "Example App" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 28F54820282486180069100A /* Debug */, 497 | 28F54821282486180069100A /* Release */, 498 | ); 499 | defaultConfigurationIsVisible = 0; 500 | defaultConfigurationName = Release; 501 | }; 502 | 28F54822282486180069100A /* Build configuration list for PBXNativeTarget "Example App" */ = { 503 | isa = XCConfigurationList; 504 | buildConfigurations = ( 505 | 28F54823282486180069100A /* Debug */, 506 | 28F54824282486180069100A /* Release */, 507 | ); 508 | defaultConfigurationIsVisible = 0; 509 | defaultConfigurationName = Release; 510 | }; 511 | 28F54838282486B60069100A /* Build configuration list for PBXNativeTarget "Example XPC Service" */ = { 512 | isa = XCConfigurationList; 513 | buildConfigurations = ( 514 | 28F54839282486B60069100A /* Debug */, 515 | 28F5483A282486B60069100A /* Release */, 516 | ); 517 | defaultConfigurationIsVisible = 0; 518 | defaultConfigurationName = Release; 519 | }; 520 | /* End XCConfigurationList section */ 521 | 522 | /* Begin XCRemoteSwiftPackageReference section */ 523 | 28AD8F642ADA5D8F00EAB83A /* XCRemoteSwiftPackageReference "SwiftyXPC" */ = { 524 | isa = XCRemoteSwiftPackageReference; 525 | repositoryURL = "https://github.com/CharlesJS/SwiftyXPC"; 526 | requirement = { 527 | kind = upToNextMajorVersion; 528 | minimumVersion = 0.5.1; 529 | }; 530 | }; 531 | /* End XCRemoteSwiftPackageReference section */ 532 | 533 | /* Begin XCSwiftPackageProductDependency section */ 534 | 288C50142AD8C2A7000DD185 /* SwiftyXPC */ = { 535 | isa = XCSwiftPackageProductDependency; 536 | productName = SwiftyXPC; 537 | }; 538 | 28AD8F672ADA5DA500EAB83A /* SwiftyXPC */ = { 539 | isa = XCSwiftPackageProductDependency; 540 | package = 28AD8F642ADA5D8F00EAB83A /* XCRemoteSwiftPackageReference "SwiftyXPC" */; 541 | productName = SwiftyXPC; 542 | }; 543 | 28AD8F692ADA5DBB00EAB83A /* SwiftyXPC */ = { 544 | isa = XCSwiftPackageProductDependency; 545 | package = 28AD8F642ADA5D8F00EAB83A /* XCRemoteSwiftPackageReference "SwiftyXPC" */; 546 | productName = SwiftyXPC; 547 | }; 548 | /* End XCSwiftPackageProductDependency section */ 549 | }; 550 | rootObject = 28F5480B282486160069100A /* Project object */; 551 | } 552 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCDecoder.swift 3 | // 4 | // Created by Charles Srstka on 11/2/21. 5 | // 6 | 7 | import System 8 | import XPC 9 | 10 | private protocol XPCEncodingContainer { 11 | var childContainers: [XPCEncodingContainer] { get } 12 | var childEncoders: [XPCEncoder._XPCEncoder] { get } 13 | func finalize() 14 | } 15 | 16 | extension XPCEncodingContainer { 17 | fileprivate func encodeNil() -> xpc_object_t { xpc_null_create() } 18 | fileprivate func encodeBool(_ flag: Bool) -> xpc_object_t { xpc_bool_create(flag) } 19 | fileprivate func encodeInteger(_ i: I) -> xpc_object_t { xpc_int64_create(Int64(i)) } 20 | fileprivate func encodeInteger(_ i: I) -> xpc_object_t { xpc_uint64_create(UInt64(i)) } 21 | fileprivate func encodeFloat(_ f: F) -> xpc_object_t { xpc_double_create(Double(f)) } 22 | fileprivate func encodeString(_ string: String) -> xpc_object_t { xpc_string_create(string) } 23 | 24 | fileprivate func finalize() {} 25 | } 26 | 27 | /// An implementation of `Encoder` that can encode values to be sent over an XPC connection. 28 | public class XPCEncoder { 29 | internal enum Key: CodingKey { 30 | case arrayIndex(Int) 31 | case `super` 32 | 33 | var stringValue: String { 34 | switch self { 35 | case .arrayIndex(let int): 36 | return "Index: \(int)" 37 | case .super: 38 | return "super" 39 | } 40 | } 41 | 42 | var intValue: Int? { 43 | switch self { 44 | case .arrayIndex(let int): 45 | return int 46 | case .super: 47 | return nil 48 | } 49 | } 50 | 51 | init?(stringValue: String) { return nil } 52 | init(intValue: Int) { self = .arrayIndex(intValue) } 53 | } 54 | 55 | internal struct UnkeyedContainerDictionaryKeys { 56 | static let contents = "Contents" 57 | static let `super` = "Super" 58 | } 59 | 60 | private class KeyedContainer: KeyedEncodingContainerProtocol, XPCEncodingContainer { 61 | let dict: xpc_object_t 62 | var codingPath: [CodingKey] = [] 63 | var childContainers: [XPCEncodingContainer] = [] 64 | var childEncoders: [_XPCEncoder] = [] 65 | 66 | init(wrapping dict: xpc_object_t, codingPath: [CodingKey]) { 67 | precondition(xpc_get_type(dict) == XPC_TYPE_DICTIONARY, "Keyed container is not wrapping a dictionary") 68 | 69 | self.dict = dict 70 | self.codingPath = codingPath 71 | } 72 | 73 | private func encode(xpcValue: xpc_object_t, for key: Key) { 74 | key.stringValue.withCString { 75 | precondition(xpc_dictionary_get_value(self.dict, $0) == nil, "Value already keyed for \(key)") 76 | 77 | xpc_dictionary_set_value(self.dict, $0, xpcValue) 78 | } 79 | } 80 | 81 | func encodeNil(forKey key: Key) throws { self.encode(xpcValue: self.encodeNil(), for: key) } 82 | func encode(_ value: Bool, forKey key: Key) throws { self.encode(xpcValue: self.encodeBool(value), for: key) } 83 | func encode(_ value: String, forKey key: Key) throws { self.encode(xpcValue: self.encodeString(value), for: key) } 84 | func encode(_ value: Double, forKey key: Key) throws { self.encode(xpcValue: self.encodeFloat(value), for: key) } 85 | func encode(_ value: Float, forKey key: Key) throws { self.encode(xpcValue: self.encodeFloat(value), for: key) } 86 | func encode(_ value: Int, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 87 | func encode(_ value: Int8, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 88 | func encode(_ value: Int16, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 89 | func encode(_ value: Int32, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 90 | func encode(_ value: Int64, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 91 | func encode(_ value: UInt, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 92 | func encode(_ value: UInt8, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 93 | func encode(_ value: UInt16, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 94 | func encode(_ value: UInt32, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 95 | func encode(_ value: UInt64, forKey key: Key) throws { self.encode(xpcValue: self.encodeInteger(value), for: key) } 96 | 97 | func encode(_ value: T, forKey key: Key) throws { 98 | if let fileDescriptor = value as? XPCFileDescriptor, let xpc = xpc_fd_create(fileDescriptor.fileDescriptor) { 99 | self.encode(xpcValue: xpc, for: key) 100 | } else if #available(macOS 11.0, *), 101 | let fileDescriptor = value as? FileDescriptor, 102 | let xpc = xpc_fd_create(fileDescriptor.rawValue) 103 | { 104 | self.encode(xpcValue: xpc, for: key) 105 | } else if let endpoint = value as? XPCEndpoint { 106 | self.encode(xpcValue: endpoint.endpoint, for: key) 107 | } else if value is XPCNull { 108 | self.encode(xpcValue: xpc_null_create(), for: key) 109 | } else { 110 | let encoder = _XPCEncoder(parentXPC: self.dict, codingPath: self.codingPath + [key]) 111 | 112 | self.childEncoders.append(encoder) 113 | 114 | try value.encode(to: encoder) 115 | } 116 | } 117 | 118 | func nestedContainer( 119 | keyedBy keyType: NestedKey.Type, 120 | forKey key: Key 121 | ) -> KeyedEncodingContainer { 122 | let dict = xpc_dictionary_create(nil, nil, 0) 123 | self.encode(xpcValue: dict, for: key) 124 | 125 | let container = KeyedContainer(wrapping: dict, codingPath: self.codingPath + [key]) 126 | 127 | self.childContainers.append(container) 128 | 129 | return KeyedEncodingContainer(container) 130 | } 131 | 132 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 133 | let dictionary = xpc_dictionary_create(nil, nil, 0) 134 | self.encode(xpcValue: dictionary, for: key) 135 | 136 | let container = UnkeyedContainer(wrapping: dictionary, codingPath: self.codingPath + [key]) 137 | 138 | self.childContainers.append(container) 139 | 140 | return container 141 | } 142 | 143 | func superEncoder() -> Encoder { 144 | let encoder = _XPCEncoder(parentXPC: self.dict, codingPath: self.codingPath + [XPCEncoder.Key.super]) 145 | 146 | self.childEncoders.append(encoder) 147 | 148 | return encoder 149 | } 150 | 151 | func superEncoder(forKey key: Key) -> Encoder { 152 | let encoder = _XPCEncoder(parentXPC: self.dict, codingPath: self.codingPath + [key]) 153 | 154 | self.childEncoders.append(encoder) 155 | 156 | return encoder 157 | } 158 | } 159 | 160 | private class UnkeyedContainer: UnkeyedEncodingContainer, XPCEncodingContainer { 161 | private enum Storage { 162 | class ByteStorage { 163 | var bytes: ContiguousArray 164 | let isSigned: Bool 165 | 166 | init(bytes: ContiguousArray) { 167 | self.bytes = bytes 168 | self.isSigned = false 169 | } 170 | 171 | init(bytes: ContiguousArray) { 172 | self.bytes = ContiguousArray.init(unsafeUninitializedCapacity: bytes.count) { buffer, count in 173 | _ = buffer.withMemoryRebound(to: Int8.self) { $0.initialize(fromContentsOf: bytes) } 174 | count = bytes.count 175 | } 176 | 177 | self.isSigned = true 178 | } 179 | } 180 | 181 | case empty 182 | case array(xpc_object_t) 183 | case bytes(ByteStorage) 184 | case finalized(Int) 185 | } 186 | 187 | var childContainers: [XPCEncodingContainer] = [] 188 | var childEncoders: [XPCEncoder._XPCEncoder] = [] 189 | 190 | private let dict: xpc_object_t 191 | private var storage: Storage 192 | 193 | let codingPath: [CodingKey] 194 | var count: Int { 195 | switch self.storage { 196 | case .empty: 197 | return 0 198 | case .array(let array): 199 | return xpc_array_get_count(array) 200 | case .bytes(let byteStorage): 201 | return byteStorage.bytes.count 202 | case .finalized(let count): 203 | return count 204 | } 205 | } 206 | 207 | init(wrapping dict: xpc_object_t, codingPath: [CodingKey]) { 208 | precondition(xpc_get_type(dict) == XPC_TYPE_DICTIONARY, "Unkeyed container is not wrapping dictionary") 209 | 210 | self.dict = dict 211 | self.storage = .empty 212 | self.codingPath = codingPath 213 | } 214 | 215 | private func encode(xpcValue: xpc_object_t) { 216 | switch self.storage { 217 | case .empty: 218 | var value = xpcValue 219 | self.storage = .array(xpc_array_create(&value, 1)) 220 | case .array(let array): 221 | xpc_array_append_value(array, xpcValue) 222 | case .bytes(let byteStorage): 223 | var byteArray: [xpc_object_t] 224 | if byteStorage.isSigned { 225 | byteArray = byteStorage.bytes.map { self.encodeInteger(Int8(bitPattern: $0)) } 226 | } else { 227 | byteArray = byteStorage.bytes.map { self.encodeInteger($0) } 228 | } 229 | 230 | byteArray.append(xpcValue) 231 | 232 | self.storage = .array(byteArray.withUnsafeBufferPointer { xpc_array_create($0.baseAddress, $0.count) }) 233 | case .finalized: 234 | preconditionFailure("UnkeyedContainer encoded to after being finalized") 235 | } 236 | } 237 | 238 | private func encodeByte(_ byte: Int8) { 239 | switch self.storage { 240 | case .empty: 241 | self.storage = .bytes(.init(bytes: [byte])) 242 | case .bytes(let byteStorage) where byteStorage.isSigned: 243 | byteStorage.bytes.append(UInt8(bitPattern: byte)) 244 | default: 245 | self.encode(xpcValue: self.encodeInteger(byte)) 246 | } 247 | } 248 | 249 | private func encodeByte(_ byte: UInt8) { 250 | switch self.storage { 251 | case .empty: 252 | self.storage = .bytes(.init(bytes: [byte])) 253 | case .bytes(let byteStorage) where !byteStorage.isSigned: 254 | byteStorage.bytes.append(byte) 255 | default: 256 | self.encode(xpcValue: self.encodeInteger(byte)) 257 | } 258 | } 259 | 260 | func encodeNil() { self.encode(xpcValue: self.encodeNil()) } 261 | func encode(_ value: Bool) throws { self.encode(xpcValue: self.encodeBool(value)) } 262 | func encode(_ value: String) throws { self.encode(xpcValue: self.encodeString(value)) } 263 | func encode(_ value: Double) throws { self.encode(xpcValue: self.encodeFloat(value)) } 264 | func encode(_ value: Float) throws { self.encode(xpcValue: self.encodeFloat(value)) } 265 | func encode(_ value: Int) throws { self.encode(xpcValue: self.encodeInteger(value)) } 266 | func encode(_ value: Int8) throws { self.encodeByte(value) } 267 | func encode(_ value: Int16) throws { self.encode(xpcValue: self.encodeInteger(value)) } 268 | func encode(_ value: Int32) throws { self.encode(xpcValue: self.encodeInteger(value)) } 269 | func encode(_ value: Int64) throws { self.encode(xpcValue: self.encodeInteger(value)) } 270 | func encode(_ value: UInt) throws { self.encode(xpcValue: self.encodeInteger(value)) } 271 | func encode(_ value: UInt8) throws { self.encodeByte(value) } 272 | func encode(_ value: UInt16) throws { self.encode(xpcValue: self.encodeInteger(value)) } 273 | func encode(_ value: UInt32) throws { self.encode(xpcValue: self.encodeInteger(value)) } 274 | func encode(_ value: UInt64) throws { self.encode(xpcValue: self.encodeInteger(value)) } 275 | 276 | func encode(contentsOf sequence: some Sequence) throws { 277 | switch self.storage { 278 | case .empty: 279 | self.storage = .bytes(.init(bytes: ContiguousArray(sequence))) 280 | case .bytes(let byteStorage) where !byteStorage.isSigned: 281 | byteStorage.bytes.append(contentsOf: sequence) 282 | default: 283 | for eachByte in sequence { 284 | self.encode(xpcValue: self.encodeInteger(eachByte)) 285 | } 286 | } 287 | } 288 | 289 | func encode(_ value: T) throws { 290 | let codingPath = self.nextCodingPath() 291 | 292 | if let fileDescriptor = value as? XPCFileDescriptor, let xpc = xpc_fd_create(fileDescriptor.fileDescriptor) { 293 | self.encode(xpcValue: xpc) 294 | } else if #available(macOS 11.0, *), 295 | let fileDescriptor = value as? FileDescriptor, 296 | let xpc = xpc_fd_create(fileDescriptor.rawValue) 297 | { 298 | self.encode(xpcValue: xpc) 299 | } else if let endpoint = value as? XPCEndpoint { 300 | self.encode(xpcValue: endpoint.endpoint) 301 | } else if value is XPCNull { 302 | self.encode(xpcValue: xpc_null_create()) 303 | } else if let byte = value as? Int8 { 304 | self.encodeByte(byte) 305 | } else if let byte = value as? UInt8 { 306 | self.encodeByte(byte) 307 | } else { 308 | self.encodeNil() // leave placeholder which will be overwritten later 309 | 310 | guard case .array(let array) = self.storage else { 311 | preconditionFailure("encodeNil() should have converted storage to array") 312 | } 313 | 314 | let encoder = _XPCEncoder(parentXPC: array, codingPath: codingPath) 315 | 316 | self.childEncoders.append(encoder) 317 | 318 | try value.encode(to: encoder) 319 | } 320 | } 321 | 322 | func nestedContainer( 323 | keyedBy keyType: NestedKey.Type 324 | ) -> KeyedEncodingContainer { 325 | let dict = xpc_dictionary_create(nil, nil, 0) 326 | self.encode(xpcValue: dict) 327 | 328 | let container = KeyedContainer(wrapping: dict, codingPath: self.nextCodingPath()) 329 | 330 | self.childContainers.append(container) 331 | 332 | return KeyedEncodingContainer(container) 333 | } 334 | 335 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 336 | let dict = xpc_dictionary_create(nil, nil, 0) 337 | self.encode(xpcValue: dict) 338 | 339 | let container = UnkeyedContainer(wrapping: dict, codingPath: self.nextCodingPath()) 340 | 341 | self.childContainers.append(container) 342 | 343 | return container 344 | } 345 | 346 | func superEncoder() -> Encoder { 347 | let encoder = _XPCEncoder(parentXPC: self.dict, codingPath: self.codingPath + [XPCEncoder.Key.super]) 348 | 349 | self.childEncoders.append(encoder) 350 | 351 | return encoder 352 | } 353 | 354 | func finalize() { 355 | let value: xpc_object_t? 356 | switch self.storage { 357 | case .empty: 358 | value = xpc_array_create(nil, 0) 359 | case .array(let array): 360 | value = array 361 | case .bytes(let byteStorage): 362 | value = byteStorage.bytes.withUnsafeBytes { xpc_data_create($0.baseAddress, $0.count) } 363 | case .finalized: 364 | preconditionFailure("UnkeyedContainer finalized twice") 365 | } 366 | 367 | xpc_dictionary_set_value(self.dict, UnkeyedContainerDictionaryKeys.contents, value) 368 | } 369 | 370 | private func nextCodingPath() -> [CodingKey] { 371 | self.codingPath + [XPCEncoder.Key.arrayIndex(self.count)] 372 | } 373 | } 374 | 375 | private class SingleValueContainer: SingleValueEncodingContainer, XPCEncodingContainer { 376 | // We can use `unowned` here, because `SingleValueContainer` should only be created under these circumstances: 377 | // 1. Created by `XPCEncoder.encode(_:)`, which keeps the encoder alive until after encoding is done, and: 378 | // 2. Created by an `Encodable` in its implementation of `encode(to:)`, during which the encoder will remain 379 | // alive for the duration of the method, after which the `SingleValueContainer` will no longer be used. 380 | unowned let encoder: _XPCEncoder 381 | var hasBeenEncoded = false 382 | 383 | var codingPath: [CodingKey] { self.encoder.codingPath } 384 | var childContainers: [XPCEncodingContainer] { [] } 385 | var childEncoders: [XPCEncoder._XPCEncoder] = [] 386 | 387 | init(encoder: _XPCEncoder) { 388 | self.encoder = encoder 389 | } 390 | 391 | private func encode(xpcValue: xpc_object_t) { 392 | precondition(!self.hasBeenEncoded, "Cannot encode to SingleValueContainer twice") 393 | defer { self.hasBeenEncoded = true } 394 | 395 | self.encoder.setEncodedValue(value: xpcValue) 396 | } 397 | 398 | func encodeNil() throws { self.encode(xpcValue: self.encodeNil()) } 399 | func encode(_ value: Bool) throws { self.encode(xpcValue: self.encodeBool(value)) } 400 | func encode(_ value: String) throws { self.encode(xpcValue: self.encodeString(value)) } 401 | func encode(_ value: Double) throws { self.encode(xpcValue: self.encodeFloat(value)) } 402 | func encode(_ value: Float) throws { self.encode(xpcValue: self.encodeFloat(value)) } 403 | func encode(_ value: Int) throws { self.encode(xpcValue: self.encodeInteger(value)) } 404 | func encode(_ value: Int8) throws { self.encode(xpcValue: self.encodeInteger(value)) } 405 | func encode(_ value: Int16) throws { self.encode(xpcValue: self.encodeInteger(value)) } 406 | func encode(_ value: Int32) throws { self.encode(xpcValue: self.encodeInteger(value)) } 407 | func encode(_ value: Int64) throws { self.encode(xpcValue: self.encodeInteger(value)) } 408 | func encode(_ value: UInt) throws { self.encode(xpcValue: self.encodeInteger(value)) } 409 | func encode(_ value: UInt8) throws { self.encode(xpcValue: self.encodeInteger(value)) } 410 | func encode(_ value: UInt16) throws { self.encode(xpcValue: self.encodeInteger(value)) } 411 | func encode(_ value: UInt32) throws { self.encode(xpcValue: self.encodeInteger(value)) } 412 | func encode(_ value: UInt64) throws { self.encode(xpcValue: self.encodeInteger(value)) } 413 | 414 | func encode(_ value: T) throws { 415 | if let fileDescriptor = value as? XPCFileDescriptor, let xpc = xpc_fd_create(fileDescriptor.fileDescriptor) { 416 | self.encode(xpcValue: xpc) 417 | } else if #available(macOS 11.0, *), 418 | let fileDescriptor = value as? FileDescriptor, 419 | let xpc = xpc_fd_create(fileDescriptor.rawValue) 420 | { 421 | self.encode(xpcValue: xpc) 422 | } else if let endpoint = value as? XPCEndpoint { 423 | self.encode(xpcValue: endpoint.endpoint) 424 | } else if value is XPCNull { 425 | self.encode(xpcValue: xpc_null_create()) 426 | } else { 427 | let encoder = _XPCEncoder(parentXPC: nil, codingPath: self.codingPath) 428 | try value.encode(to: encoder) 429 | 430 | self.childEncoders.append(encoder) 431 | 432 | guard let encoded = encoder.encodedValue else { 433 | preconditionFailure("XPCEncoder did not set encoded value") 434 | } 435 | 436 | self.encode(xpcValue: encoded) 437 | } 438 | } 439 | } 440 | 441 | fileprivate class _XPCEncoder: Encoder { 442 | let codingPath: [CodingKey] 443 | var userInfo: [CodingUserInfoKey: Any] { [:] } 444 | let original: xpc_object_t? 445 | private(set) var encodedValue: xpc_object_t? = nil 446 | 447 | private let parentXPC: xpc_object_t? 448 | private var topLevelContainer: XPCEncodingContainer? = nil 449 | 450 | init(parentXPC: xpc_object_t?, codingPath: [CodingKey], replyingTo original: xpc_object_t? = nil) { 451 | self.parentXPC = parentXPC 452 | self.codingPath = codingPath 453 | self.original = original 454 | } 455 | 456 | func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { 457 | precondition(self.topLevelContainer == nil, "Can only have one top-level container") 458 | 459 | let dict: xpc_object_t 460 | 461 | if let original = self.original, let replyDict = xpc_dictionary_create_reply(original) { 462 | dict = replyDict 463 | } else { 464 | dict = xpc_dictionary_create(nil, nil, 0) 465 | } 466 | 467 | self.setEncodedValue(value: dict) 468 | 469 | let container = KeyedContainer(wrapping: dict, codingPath: self.codingPath) 470 | 471 | self.topLevelContainer = container 472 | 473 | return KeyedEncodingContainer(container) 474 | } 475 | 476 | func unkeyedContainer() -> UnkeyedEncodingContainer { 477 | precondition(self.topLevelContainer == nil, "Can only have one top-level container") 478 | precondition(self.original == nil, "Message replies must use keyed containers") 479 | 480 | let dict = xpc_dictionary_create(nil, nil, 0) 481 | self.setEncodedValue(value: dict) 482 | 483 | let container = UnkeyedContainer(wrapping: dict, codingPath: self.codingPath) 484 | 485 | self.topLevelContainer = container 486 | 487 | return container 488 | } 489 | 490 | func singleValueContainer() -> SingleValueEncodingContainer { 491 | precondition(self.topLevelContainer == nil, "Can only have one top-level container") 492 | precondition(self.original == nil, "Message replies must use keyed containers") 493 | 494 | let container = SingleValueContainer(encoder: self) 495 | 496 | self.topLevelContainer = container 497 | 498 | return container 499 | } 500 | 501 | func setEncodedValue(value: xpc_object_t) { 502 | self.encodedValue = value 503 | 504 | if let parentXPC = self.parentXPC { 505 | guard let key = self.codingPath.last else { 506 | preconditionFailure("No coding key with parent XPC object") 507 | } 508 | 509 | if let specialKey = key as? XPCEncoder.Key { 510 | switch specialKey { 511 | case .arrayIndex(let index): 512 | precondition(xpc_get_type(parentXPC) == XPC_TYPE_ARRAY, "Index passed to non-array") 513 | 514 | xpc_array_set_value(parentXPC, index, value) 515 | default: 516 | preconditionFailure("Invalid key '\(specialKey.stringValue)'") 517 | } 518 | } else { 519 | precondition(xpc_get_type(parentXPC) == XPC_TYPE_DICTIONARY, "Key passed to non-dictionary") 520 | 521 | key.stringValue.withCString { xpc_dictionary_set_value(parentXPC, $0, value) } 522 | } 523 | } 524 | } 525 | 526 | func finalize() { 527 | if let container = self.topLevelContainer { 528 | container.finalize() 529 | container.childContainers.forEach { $0.finalize() } 530 | container.childEncoders.forEach { $0.finalize() } 531 | } 532 | } 533 | } 534 | 535 | private let original: xpc_object_t? 536 | 537 | /// Create an `XPCEncoder`. 538 | /// 539 | /// - Parameter original: An optional incoming XPC event object to which the encoded value should be a reply. This event must be a dictionary. 540 | public init(replyingTo original: xpc_object_t? = nil) { 541 | if let original = original { 542 | precondition(xpc_get_type(original) == XPC_TYPE_DICTIONARY, "XPC replies must be to dictionaries") 543 | } 544 | 545 | self.original = original 546 | } 547 | 548 | /// Encode a value to an XPC object. 549 | /// 550 | /// - Parameter value: The value to be encoded. 551 | /// 552 | /// - Returns: The encoded value, as an `xpc_object_t`. 553 | /// 554 | /// - Throws: Any errors that come up in the process of encoding the value. 555 | public func encode(_ value: T) throws -> xpc_object_t { 556 | let encoder = _XPCEncoder(parentXPC: nil, codingPath: [], replyingTo: self.original) 557 | 558 | // Everything has to go through containers so that custom catchers in the container classes will catch things like 559 | // file descriptors. 560 | 561 | var container = encoder.singleValueContainer() 562 | try container.encode(value) 563 | 564 | encoder.finalize() 565 | 566 | guard let encoded = encoder.encodedValue else { 567 | preconditionFailure("XPCEncoder did not set encoded value") 568 | } 569 | 570 | return encoded 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCConnection.swift: -------------------------------------------------------------------------------- 1 | import Security 2 | import System 3 | import XPC 4 | 5 | /// A bidirectional communication channel between two processes. 6 | /// 7 | /// Use this class’s `init(type:codeSigningRequirement:)` initializer to connect to an `XPCListener` instance in another process. 8 | /// 9 | /// Use the `sendMessage` family of functions to send messages to the remote process, and `setMessageHandler(name:handler:)` to receive them. 10 | /// 11 | /// The connection must receive `.activate()` before it can send or receive any messages. 12 | public class XPCConnection: @unchecked Sendable { 13 | /// Errors specific to `XPCConnection`. 14 | public enum Error: Swift.Error, Codable { 15 | /// An XPC message was missing its name. 16 | case missingMessageName 17 | /// An XPC message was missing its request and/or response body. 18 | case missingMessageBody 19 | /// Received an unhandled XPC message. 20 | case unexpectedMessage 21 | /// A message contained data of the wrong type. 22 | case typeMismatch(expected: XPCType, actual: XPCType) 23 | /// Only used on macOS versions prior to 12.0. 24 | case callerFailedCredentialCheck(OSStatus) 25 | } 26 | 27 | private struct MessageKeys { 28 | static let name = "com.charlessoft.SwiftyXPC.XPCEventHandler.Name" 29 | static let body = "com.charlessoft.SwiftyXPC.XPCEventHandler.Body" 30 | static let error = "com.charlessoft.SwiftyXPC.XPCEventHandler.Error" 31 | } 32 | 33 | /// Represents the various types of connection that can be created. 34 | public enum ConnectionType { 35 | /// Connect to an embedded XPC service inside the current application’s bundle. Pass the XPC service’s bundle ID as the `bundleID` parameter. 36 | case remoteService(bundleID: String) 37 | /// Create a connection from a passed-in endpoint, which typically will come embedded in an XPC message. 38 | case remoteServiceFromEndpoint(XPCEndpoint) 39 | /// Connect to a remote Mach service by its service name. 40 | case remoteMachService(serviceName: String, isPrivilegedHelperTool: Bool) 41 | } 42 | 43 | /// A handler that will be called if a communication error occurs. 44 | public typealias ErrorHandler = (XPCConnection, Swift.Error) -> Void 45 | 46 | internal class MessageHandler { 47 | typealias RawHandler = ((XPCConnection, xpc_object_t) async throws -> xpc_object_t) 48 | let closure: RawHandler 49 | let requestType: Codable.Type 50 | let responseType: Codable.Type 51 | 52 | init(closure: @escaping (XPCConnection, Request) async throws -> Response) { 53 | self.requestType = Request.self 54 | self.responseType = Response.self 55 | 56 | self.closure = { connection, event in 57 | guard let body = xpc_dictionary_get_value(event, MessageKeys.body) else { 58 | throw Error.missingMessageBody 59 | } 60 | 61 | let request = try XPCDecoder().decode(type: Request.self, from: body) 62 | let response = try await closure(connection, request) 63 | 64 | return try XPCEncoder().encode(response) 65 | } 66 | } 67 | } 68 | 69 | private let connection: xpc_connection_t 70 | 71 | @available(macOS, obsoleted: 12.0) 72 | private let codeSigningRequirement: String? 73 | 74 | internal static func makeAnonymousListenerConnection(codeSigningRequirement: String?) throws -> XPCConnection { 75 | try .init(connection: xpc_connection_create(nil, nil), codeSigningRequirement: codeSigningRequirement) 76 | } 77 | 78 | /// Initialize a new `XPCConnection`. 79 | /// 80 | /// - Parameters: 81 | /// - type: The type of connection to create. See the documentation for `ConnectionType` for possible values. 82 | /// - requirement: An optional code signing requirement. If specified, the connection will reject all messages from processes that do not meet the specified requirement. 83 | /// 84 | /// - Throws: Any errors that come up in the process of initializing the connection. 85 | public convenience init(type: ConnectionType, codeSigningRequirement requirement: String? = nil) throws { 86 | switch type { 87 | case .remoteService(let bundleID): 88 | try self.init(connection: xpc_connection_create(bundleID, nil), codeSigningRequirement: requirement) 89 | case .remoteServiceFromEndpoint(let endpoint): 90 | try self.init(connection: endpoint.makeConnection(), codeSigningRequirement: requirement) 91 | case .remoteMachService(serviceName: let name, isPrivilegedHelperTool: let isPrivileged): 92 | let flags: Int32 = isPrivileged ? XPC_CONNECTION_MACH_SERVICE_PRIVILEGED : 0 93 | try self.init(machServiceName: name, flags: flags, codeSigningRequirement: requirement) 94 | } 95 | } 96 | 97 | internal convenience init(machServiceName: String, flags: Int32, codeSigningRequirement: String? = nil) throws { 98 | let connection = xpc_connection_create_mach_service(machServiceName, nil, UInt64(flags)) 99 | 100 | do { 101 | try self.init(connection: connection, codeSigningRequirement: codeSigningRequirement) 102 | } catch { 103 | // To avoid xpc_api_misuse errors from the connection being released without having been fully initialized 104 | xpc_connection_set_event_handler(connection) { _ in } 105 | xpc_connection_activate(connection) 106 | xpc_connection_cancel(connection) 107 | throw error 108 | } 109 | } 110 | 111 | internal init(connection: xpc_connection_t, codeSigningRequirement: String?) throws { 112 | self.connection = connection 113 | self.codeSigningRequirement = codeSigningRequirement 114 | 115 | if #available(macOS 12.0, *), let requirement = codeSigningRequirement { 116 | guard xpc_connection_set_peer_code_signing_requirement(self.connection, requirement) == 0 else { 117 | throw XPCError.invalidCodeSignatureRequirement 118 | } 119 | } 120 | 121 | xpc_connection_set_event_handler(self.connection, self.handleEvent) 122 | } 123 | 124 | internal var messageHandlers: [String: MessageHandler] = [:] 125 | 126 | /// A handler that will be called if a communication error occurs. 127 | public var errorHandler: ErrorHandler? = nil 128 | 129 | internal var customEventHandler: xpc_handler_t? = nil 130 | 131 | internal func getMessageHandler(forName name: String) -> MessageHandler.RawHandler? { 132 | self.messageHandlers[name]?.closure 133 | } 134 | 135 | /// Set a message handler for an incoming message, identified by the `name` parameter, without taking any arguments or returning any value. 136 | /// 137 | /// - Parameters: 138 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:)`. 139 | /// - handler: Pass a function that processes the message, optionally throwing an error. 140 | public func setMessageHandler(name: String, handler: @escaping (XPCConnection) async throws -> Void) { 141 | self.setMessageHandler(name: name) { (connection: XPCConnection, _: XPCNull) async throws -> XPCNull in 142 | try await handler(connection) 143 | return XPCNull.shared 144 | } 145 | } 146 | 147 | /// Set a message handler for an incoming message, identified by the `name` parameter, taking an argument but not returning any value. 148 | /// 149 | /// - Parameters: 150 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:request:)`. 151 | /// - handler: Pass a function that processes the message, optionall throwing an error. The request value must 152 | /// conform to the `Codable` protocol, and will automatically be type-checked by `XPCConnection` upon receiving a message. 153 | public func setMessageHandler( 154 | name: String, 155 | handler: @escaping (XPCConnection, Request) async throws -> Void 156 | ) { 157 | self.setMessageHandler(name: name) { (connection: XPCConnection, request: Request) async throws -> XPCNull in 158 | try await handler(connection, request) 159 | return XPCNull.shared 160 | } 161 | } 162 | 163 | /// Set a message handler for an incoming message, identified by the `name` parameter, without taking any arguments but returning a value. 164 | /// 165 | /// - Parameters: 166 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:)`. 167 | /// - handler: Pass a function that processes the message and either returns a value or throws an error. The return value must 168 | /// conform to the `Codable` protocol. 169 | public func setMessageHandler( 170 | name: String, 171 | handler: @escaping (XPCConnection) async throws -> Response 172 | ) { 173 | self.setMessageHandler(name: name) { (connection: XPCConnection, _: XPCNull) in 174 | try await handler(connection) 175 | } 176 | } 177 | 178 | /// Set a message handler for an incoming message, identified by the `name` parameter, taking an argument and returning a value. 179 | /// 180 | /// Example usage: 181 | /// 182 | /// connection.setMessageHandler( 183 | /// name: "com.example.SayHello", 184 | /// handler: self.sayHello 185 | /// ) 186 | /// 187 | /// // ... later in the same class ... 188 | /// 189 | /// func sayHello( 190 | /// connection: XPCConnection, 191 | /// message: String 192 | /// ) async throws -> String { 193 | /// self.logger.notice("Caller sent message: \(message)") 194 | /// 195 | /// if message == "Hello, World!" { 196 | /// return "Hello back!" 197 | /// } else { 198 | /// throw RudeCallerError(message: "You didn't say hello!") 199 | /// } 200 | /// } 201 | /// 202 | /// - Parameters: 203 | /// - name: A name uniquely identifying the message. This must match the name that the sending process passes to `sendMessage(name:request:)`. 204 | /// - handler: Pass a function that processes the message and either returns a value or throws an error. Both the request and return values must conform to the `Codable` protocol. The types are automatically type-checked by `XPCConnection` upon receiving a message. 205 | public func setMessageHandler( 206 | name: String, 207 | handler: @escaping (XPCConnection, Request) async throws -> Response 208 | ) { 209 | self.messageHandlers[name] = MessageHandler(closure: handler) 210 | } 211 | 212 | /// The audit session identifier associated with the remote process. 213 | public var auditSessionIdentifier: au_asid_t { 214 | xpc_connection_get_asid(self.connection) 215 | } 216 | 217 | /// The effective group identifier associated with the remote process. 218 | public var effectiveGroupIdentifier: gid_t { 219 | xpc_connection_get_egid(self.connection) 220 | } 221 | 222 | /// The effective user identifier associated with the remote process. 223 | public var effectiveUserIdentifier: uid_t { 224 | xpc_connection_get_euid(self.connection) 225 | } 226 | 227 | /// The process ID of the remote process. 228 | public var processIdentifier: pid_t { 229 | xpc_connection_get_pid(self.connection) 230 | } 231 | 232 | /// Activate the connection. 233 | /// 234 | /// Connections start in an inactive state, so you must call `activate()` on a connection before it will send or receive any messages. 235 | public func activate() { 236 | xpc_connection_activate(self.connection) 237 | } 238 | 239 | /// Suspends the connection so that the event handler block doesn't fire and the connection doesn't attempt to send any messages it has in its queue. 240 | /// 241 | /// All calls to `suspend()` must be balanced with calls to `resume()` before releasing the last reference to the connection. 242 | /// 243 | /// Suspension is asynchronous and non-preemptive, and therefore this method will not interrupt the execution of an already-running event handler block. 244 | /// If the event handler is executing at the time of this call, it will finish, and then the connection will be suspended before the next scheduled invocation of 245 | /// the event handler. The XPC runtime guarantees this non-preemptiveness even for concurrent target queues. 246 | public func suspend() { 247 | xpc_connection_suspend(self.connection) 248 | } 249 | 250 | /// Resumes a suspended connection. 251 | /// 252 | /// In order for a connection to become live, every call to `suspend()` must be balanced with a call to `resume()`. 253 | /// Calling `resume()` more times than `suspend()` has been called is considered an error. 254 | public func resume() { 255 | xpc_connection_resume(self.connection) 256 | } 257 | 258 | /// Cancels the connection and ensures that its event handler doesn't fire again. 259 | /// 260 | /// After this call, any messages that have not yet been sent will be discarded, and the connection will be unwound. 261 | /// If there are messages that are awaiting replies, they will receive the `XPCError.connectionInvalid` error. 262 | public func cancel() { 263 | xpc_connection_cancel(self.connection) 264 | } 265 | 266 | internal func makeEndpoint() -> XPCEndpoint { 267 | XPCEndpoint(connection: self.connection) 268 | } 269 | 270 | /// Send a message to an `XPCConnection` in another process without any parameters. 271 | /// 272 | /// - Parameter name: A name uniquely identifying the message. This must match the name that the receiving connection has passed to `setMessage(name:handler:)`. 273 | /// 274 | /// - Throws: Throws an error if the receiving connection throws an error in its handler, or if a communication error occurs. 275 | public func sendMessage(name: String) async throws { 276 | try await self.sendMessage(name: name, request: XPCNull.shared) 277 | } 278 | 279 | /// Send a message to an `XPCConnection` in another process that takes a parameter. 280 | /// 281 | /// - Parameters: 282 | /// - name: A name uniquely identifying the message. This must match the name that the receiving connection has passed to `setMessage(name:handler:)`. 283 | /// - request: A parameter that will be passed to the receiving connection’s handler function. The type of the request must match the type specified by the receiving connection. 284 | /// 285 | /// - Throws: Throws an error the `request` parameter does not match the type specified by the receiving connection’s handler function, 286 | /// if the receiving connection throws an error in its handler, or if a communication error occurs. 287 | public func sendMessage(name: String, request: Request) async throws { 288 | _ = try await self.sendMessage(name: name, request: request) as XPCNull 289 | } 290 | 291 | /// Send a message to an `XPCConnection` in another process that does not take a parameter, and receives a response. 292 | /// 293 | /// - Parameter name: A name uniquely identifying the message. This must match the name that the receiving connection has passed to `setMessage(name:handler:)`. 294 | /// 295 | /// - Returns: The value returned by the receiving connection's helper function. 296 | /// 297 | /// - Throws: Throws an error if the receiving connection throws an error in its handler, or if a communication error occurs. 298 | public func sendMessage(name: String) async throws -> Response { 299 | try await self.sendMessage(name: name, request: XPCNull.shared) 300 | } 301 | 302 | /// Send a message to an `XPCConnection` in another process that takes a parameter. 303 | /// 304 | /// - Parameters: 305 | /// - name: A name uniquely identifying the message. This must match the name that the receiving connection has passed to `setMessage(name:handler:)`. 306 | /// - request: A parameter that will be passed to the receiving connection’s handler function. The type of the request must match the type specified by the receiving connection. 307 | /// 308 | /// - Returns: The value returned by the receiving connection's helper function. 309 | /// 310 | /// - Throws: Throws an error the `request` parameter does not match the type specified by the receiving connection’s handler function, 311 | /// if the receiving connection throws an error in its handler, or if a communication error occurs. 312 | public func sendMessage(name: String, request: some Codable) async throws -> Response { 313 | let body = try XPCEncoder().encode(request) 314 | 315 | return try await withCheckedThrowingContinuation { continuation in 316 | let message = xpc_dictionary_create(nil, nil, 0) 317 | 318 | xpc_dictionary_set_string(message, MessageKeys.name, name) 319 | xpc_dictionary_set_value(message, MessageKeys.body, body) 320 | 321 | xpc_connection_send_message_with_reply(self.connection, message, nil) { event in 322 | do { 323 | switch event.type { 324 | case .dictionary: break 325 | case .error: throw XPCError(error: event) 326 | default: throw Error.typeMismatch(expected: .dictionary, actual: event.type) 327 | } 328 | 329 | if let error = xpc_dictionary_get_value(event, MessageKeys.error) { 330 | throw try XPCErrorRegistry.shared.decodeError(error) 331 | } 332 | 333 | guard let body = xpc_dictionary_get_value(event, MessageKeys.body) else { 334 | throw Error.missingMessageBody 335 | } 336 | 337 | let response: Response = if Response.self == XPCNull.self { 338 | XPCNull() as! Response 339 | } else { 340 | try XPCDecoder().decode(type: Response.self, from: body) 341 | } 342 | 343 | continuation.resume(returning: response) 344 | } catch { 345 | continuation.resume(throwing: error) 346 | } 347 | } 348 | } 349 | } 350 | 351 | /// Send a message, and do not wait for a reply. 352 | /// 353 | /// - Parameters: 354 | /// - message: A parameter that will be passed to the receiving connection’s handler function. This must match the type specified in the receiving connection’s handler function. 355 | /// - name: A name uniquely identifying the message. This must match the name that the receiving connection has passed to `setMessage(name:handler:)`. 356 | /// 357 | /// - Throws: Any communication errors that occur in the process of sending the message. 358 | public func sendOnewayMessage(name: String? = nil, message: Message) throws { 359 | try self.sendOnewayRawMessage(name: name, body: XPCEncoder().encode(message), key: MessageKeys.body, asReplyTo: nil) 360 | } 361 | 362 | private func sendOnewayError(error: Swift.Error, asReplyTo original: xpc_object_t?) throws { 363 | try self.sendOnewayRawMessage( 364 | name: nil, 365 | body: XPCErrorRegistry.shared.encodeError(error), 366 | key: MessageKeys.error, 367 | asReplyTo: original 368 | ) 369 | } 370 | 371 | private func sendOnewayRawMessage( 372 | name: String?, 373 | body: xpc_object_t, 374 | key: String, 375 | asReplyTo original: xpc_object_t? 376 | ) throws { 377 | let xpcMessage: xpc_object_t 378 | if let original = original, let reply = xpc_dictionary_create_reply(original) { 379 | xpcMessage = reply 380 | } else { 381 | xpcMessage = xpc_dictionary_create(nil, nil, 0) 382 | } 383 | 384 | if let name = name { 385 | xpc_dictionary_set_string(xpcMessage, MessageKeys.name, name) 386 | } 387 | 388 | xpc_dictionary_set_value(xpcMessage, key, body) 389 | 390 | xpc_connection_send_message(self.connection, xpcMessage) 391 | } 392 | 393 | /// Issues a barrier against the connection's message-send activity. 394 | /// 395 | /// - Parameter barrier: The barrier block to issue. 396 | /// This barrier prevents concurrent message-send activity on the connection. 397 | /// No messages will be sent while the barrier block is executing. 398 | /// 399 | /// XPC guarantees that, even if the connection's target queue is a concurrent queue, there are no other messages being sent concurrently 400 | /// while the barrier block is executing. 401 | /// XPC does not guarantee that the receipt of messages (either through the connection's event handler or through reply handlers) will be 402 | /// suspended while the barrier is executing. 403 | /// 404 | /// A barrier is issued relative to the message-send queue. So, if you call `sendMessage(name:request:)` five times and then call `sendBarrier(_:)`, 405 | /// the barrier will be invoked after the fifth message has been sent and its memory disposed of. 406 | /// You may safely cancel a connection from within a barrier block. 407 | /// 408 | /// If a barrier is issued after sending a message which expects a reply, the behavior is the same as described above. 409 | /// The receipt of a reply message will not influence when the barrier runs. 410 | /// 411 | /// A barrier block can be useful for throttling resource consumption on the connected side of a connection. 412 | /// For example, if your connection sends many large messages, you can use a barrier to limit the number of messages that are inflight at any given time. 413 | /// This can be particularly useful for messages that contain kernel resources (like file descriptors) which have a systemwide limit. 414 | /// 415 | /// If a barrier is issued on a canceled connection, it will be invoked immediately. 416 | /// If a connection has been canceled and still has outstanding barriers, those barriers will be invoked as part of the connection's unwinding process. 417 | /// 418 | /// It is important to note that a barrier block's execution order is not guaranteed with respect to other blocks that have been scheduled on the 419 | /// target queue of the connection. Or said differently, `sendBarrier(_:)` is not equivalent to `DispatchQueue.async`. 420 | public func sendBarrier(_ barrier: @escaping () -> Void) { 421 | xpc_connection_send_barrier(self.connection, barrier) 422 | } 423 | 424 | private func handleEvent(_ event: xpc_object_t) { 425 | if #available(macOS 12.0, *) { 426 | // On Monterey and later, we are relying on xpc's built-in functionality for checking code signatures instead 427 | } else { 428 | do { 429 | try self.checkCallerCredentials(event: event) 430 | } catch { 431 | self.errorHandler?(self, error) 432 | return 433 | } 434 | } 435 | 436 | if let customEventHandler = self.customEventHandler { 437 | customEventHandler(event) 438 | return 439 | } 440 | 441 | do { 442 | switch event.type { 443 | case .dictionary: 444 | if let error = xpc_dictionary_get_value(event, MessageKeys.error) { 445 | throw try XPCErrorRegistry.shared.decodeError(error) 446 | } 447 | 448 | self.respond(to: event) 449 | case .error: 450 | throw XPCError(error: event) 451 | default: 452 | throw Error.typeMismatch(expected: .dictionary, actual: event.type) 453 | } 454 | } catch { 455 | self.errorHandler?(self, error) 456 | return 457 | } 458 | } 459 | 460 | @available(macOS, obsoleted: 12.0) 461 | private func checkCallerCredentials(event: xpc_object_t) throws { 462 | guard let requirementString = self.codeSigningRequirement else { return } 463 | 464 | var code: SecCode? = nil 465 | var err: OSStatus 466 | 467 | if #available(macOS 11.0, *) { 468 | err = SecCodeCreateWithXPCMessage(event, [], &code) 469 | } else { 470 | var keyCB = kCFTypeDictionaryKeyCallBacks 471 | var valueCB = kCFTypeDictionaryValueCallBacks 472 | let key = kSecGuestAttributePid 473 | var pid = Int64(xpc_connection_get_pid(xpc_dictionary_get_remote_connection(event)!)) 474 | let value = CFNumberCreate(kCFAllocatorDefault, .sInt64Type, &pid) 475 | let attributes = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &keyCB, &valueCB) 476 | 477 | CFDictionarySetValue( 478 | attributes, 479 | unsafeBitCast(key, to: UnsafeRawPointer.self), 480 | unsafeBitCast(value, to: UnsafeRawPointer.self) 481 | ) 482 | 483 | err = SecCodeCopyGuestWithAttributes(nil, attributes, [], &code) 484 | } 485 | 486 | guard err == errSecSuccess else { 487 | throw Error.callerFailedCredentialCheck(err) 488 | } 489 | 490 | let cfRequirementString = requirementString.withCString { 491 | CFStringCreateWithCString(kCFAllocatorDefault, $0, CFStringBuiltInEncodings.UTF8.rawValue) 492 | } 493 | 494 | var requirement: SecRequirement? = nil 495 | err = SecRequirementCreateWithString(cfRequirementString!, [], &requirement) 496 | guard err == errSecSuccess else { 497 | throw Error.callerFailedCredentialCheck(err) 498 | } 499 | 500 | err = SecCodeCheckValidity(code!, [], requirement) 501 | guard err == errSecSuccess else { 502 | throw Error.callerFailedCredentialCheck(err) 503 | } 504 | } 505 | 506 | private func respond(to event: xpc_object_t) { 507 | struct SendableWrapper: @unchecked Sendable { 508 | let event: xpc_object_t 509 | } 510 | 511 | let wrapper = SendableWrapper(event: event) 512 | 513 | Task { 514 | let event = wrapper.event 515 | 516 | let messageHandler: MessageHandler.RawHandler 517 | 518 | do { 519 | guard let name = xpc_dictionary_get_value(event, MessageKeys.name).flatMap({ String($0) }) else { 520 | throw Error.missingMessageName 521 | } 522 | 523 | guard let _messageHandler = self.getMessageHandler(forName: name) else { 524 | throw Error.unexpectedMessage 525 | } 526 | 527 | messageHandler = _messageHandler 528 | } catch { 529 | self.errorHandler?(self, error) 530 | return 531 | } 532 | 533 | let response: xpc_object_t 534 | 535 | do { 536 | response = try await messageHandler(self, event) 537 | } catch { 538 | try self.sendOnewayError(error: error, asReplyTo: event) 539 | return 540 | } 541 | 542 | do { 543 | try self.sendOnewayRawMessage(name: nil, body: response, key: MessageKeys.body, asReplyTo: event) 544 | } catch { 545 | self.errorHandler?(self, error) 546 | } 547 | } 548 | } 549 | 550 | @available(*, deprecated, message: "Use sendOnewayMessage(name:message:) instead") 551 | public func sendOnewayMessage(message: Message, name: String?) throws { 552 | try self.sendOnewayMessage(name: name, message: message) 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /Sources/SwiftyXPC/XPCDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCEncoder.swift 3 | // 4 | // Created by Charles Srstka on 11/2/21. 5 | // 6 | 7 | import System 8 | import XPC 9 | 10 | private protocol XPCDecodingContainer { 11 | var codingPath: [CodingKey] { get } 12 | var error: Error? { get } 13 | } 14 | extension XPCDecodingContainer { 15 | fileprivate func makeErrorContext(description: String, underlyingError: Error? = nil) -> DecodingError.Context { 16 | DecodingError.Context(codingPath: self.codingPath, debugDescription: description, underlyingError: underlyingError) 17 | } 18 | 19 | fileprivate func checkType(xpcType: xpc_type_t, swiftType: Any.Type, xpc: xpc_object_t) throws { 20 | if xpc_get_type(xpc) != xpcType { 21 | let expectedTypeName = String(cString: xpc_type_get_name(xpcType)) 22 | let actualTypeName = String(cString: xpc_type_get_name(xpc_get_type(xpc))) 23 | 24 | let context = self.makeErrorContext( 25 | description: "Incorrect XPC type; want \(expectedTypeName), got \(actualTypeName)" 26 | ) 27 | 28 | throw DecodingError.typeMismatch(swiftType, context) 29 | } 30 | } 31 | 32 | fileprivate func decodeNil(xpc: xpc_object_t) throws { 33 | try self.checkType(xpcType: XPC_TYPE_NULL, swiftType: Any?.self, xpc: xpc) 34 | } 35 | 36 | fileprivate func decodeBool(xpc: xpc_object_t) throws -> Bool { 37 | try self.checkType(xpcType: XPC_TYPE_BOOL, swiftType: Bool.self, xpc: xpc) 38 | 39 | return xpc_bool_get_value(xpc) 40 | } 41 | 42 | fileprivate func decodeInteger(xpc: xpc_object_t) throws -> I { 43 | try self.checkType(xpcType: XPC_TYPE_INT64, swiftType: I.self, xpc: xpc) 44 | let int = xpc_int64_get_value(xpc) 45 | 46 | if let i = I(exactly: int) { 47 | return i 48 | } else { 49 | let context = self.makeErrorContext(description: "Integer overflow; \(int) out of bounds") 50 | throw DecodingError.dataCorrupted(context) 51 | } 52 | } 53 | 54 | fileprivate func decodeInteger(xpc: xpc_object_t) throws -> I { 55 | try self.checkType(xpcType: XPC_TYPE_UINT64, swiftType: I.self, xpc: xpc) 56 | let int = xpc_uint64_get_value(xpc) 57 | 58 | if let i = I(exactly: int) { 59 | return i 60 | } else { 61 | let context = self.makeErrorContext(description: "Integer overflow; \(int) out of bounds") 62 | throw DecodingError.dataCorrupted(context) 63 | } 64 | } 65 | 66 | fileprivate func decodeFloatingPoint(xpc: xpc_object_t) throws -> F { 67 | try self.checkType(xpcType: XPC_TYPE_DOUBLE, swiftType: F.self, xpc: xpc) 68 | 69 | return F(xpc_double_get_value(xpc)) 70 | } 71 | 72 | fileprivate func decodeString(xpc: xpc_object_t) throws -> String { 73 | try self.checkType(xpcType: XPC_TYPE_STRING, swiftType: String.self, xpc: xpc) 74 | 75 | let length = xpc_string_get_length(xpc) 76 | let pointer = xpc_string_get_string_ptr(xpc) 77 | 78 | return withExtendedLifetime(xpc) { 79 | UnsafeBufferPointer(start: pointer, count: length).withMemoryRebound(to: UInt8.self) { 80 | String(decoding: $0, as: UTF8.self) 81 | } 82 | } 83 | } 84 | } 85 | 86 | /// An implementation of `Decoder` that can decode values sent over an XPC connection. 87 | public final class XPCDecoder { 88 | private final class KeyedContainer: KeyedDecodingContainerProtocol, XPCDecodingContainer { 89 | let dict: xpc_object_t 90 | let codingPath: [CodingKey] 91 | var error: Error? { nil } 92 | 93 | private var checkedType = false 94 | 95 | var allKeys: [Key] { 96 | var keys: [Key] = [] 97 | 98 | xpc_dictionary_apply(self.dict) { cKey, _ in 99 | let stringKey = String(cString: cKey) 100 | guard let key = Key(stringValue: stringKey) else { 101 | preconditionFailure("Couldn't convert string '\(stringKey)' into key") 102 | } 103 | 104 | keys.append(key) 105 | return true 106 | } 107 | 108 | return keys 109 | } 110 | 111 | init(wrapping dict: xpc_object_t, codingPath: [CodingKey]) { 112 | self.dict = dict 113 | self.codingPath = codingPath 114 | } 115 | 116 | func contains(_ key: Key) -> Bool { (try? self.getValue(for: key)) != nil } 117 | 118 | private func getValue(for key: CodingKey, allowNull: Bool = false) throws -> xpc_object_t { 119 | guard let value = try self.getOptionalValue(for: key, allowNull: allowNull) else { 120 | let context = self.makeErrorContext(description: "No value for key '\(key.stringValue)'") 121 | throw DecodingError.valueNotFound(Any.self, context) 122 | } 123 | 124 | return value 125 | } 126 | 127 | private func getOptionalValue(for key: CodingKey, allowNull: Bool = false) throws -> xpc_object_t? { 128 | try key.stringValue.withCString { 129 | if !self.checkedType { 130 | guard xpc_get_type(dict) == XPC_TYPE_DICTIONARY else { 131 | let type = String(cString: xpc_type_get_name(xpc_get_type(dict))) 132 | let desc = "Unexpected type for KeyedContainer wrapped object: expected dictionary, got \(type)" 133 | let context = self.makeErrorContext(description: desc) 134 | 135 | throw DecodingError.typeMismatch([String: Any].self, context) 136 | } 137 | 138 | self.checkedType = true 139 | } 140 | 141 | let value = xpc_dictionary_get_value(self.dict, $0) 142 | 143 | if !allowNull, let value = value, case .null = value.type { 144 | return nil 145 | } 146 | 147 | return value 148 | } 149 | } 150 | 151 | func decodeNil(forKey key: Key) throws -> Bool { 152 | xpc_get_type(try self.getValue(for: key, allowNull: true)) == XPC_TYPE_NULL 153 | } 154 | 155 | func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { 156 | try self.decodeBool(xpc: self.getValue(for: key)) 157 | } 158 | 159 | func decode(_ type: String.Type, forKey key: Key) throws -> String { 160 | try self.decodeString(xpc: self.getValue(for: key)) 161 | } 162 | 163 | func decode(_ type: Double.Type, forKey key: Key) throws -> Double { 164 | try self.decodeFloatingPoint(xpc: self.getValue(for: key)) 165 | } 166 | 167 | func decode(_ type: Float.Type, forKey key: Key) throws -> Float { 168 | try self.decodeFloatingPoint(xpc: self.getValue(for: key)) 169 | } 170 | 171 | func decode(_ type: Int.Type, forKey key: Key) throws -> Int { 172 | try self.decodeInteger(xpc: self.getValue(for: key)) 173 | } 174 | 175 | func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { 176 | try self.decodeInteger(xpc: self.getValue(for: key)) 177 | } 178 | 179 | func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { 180 | try self.decodeInteger(xpc: self.getValue(for: key)) 181 | } 182 | 183 | func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { 184 | try self.decodeInteger(xpc: self.getValue(for: key)) 185 | } 186 | 187 | func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { 188 | try self.decodeInteger(xpc: self.getValue(for: key)) 189 | } 190 | 191 | func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { 192 | try self.decodeInteger(xpc: self.getValue(for: key)) 193 | } 194 | 195 | func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { 196 | try self.decodeInteger(xpc: self.getValue(for: key)) 197 | } 198 | 199 | func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { 200 | try self.decodeInteger(xpc: self.getValue(for: key)) 201 | } 202 | 203 | func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { 204 | try self.decodeInteger(xpc: self.getValue(for: key)) 205 | } 206 | 207 | func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { 208 | try self.decodeInteger(xpc: self.getValue(for: key)) 209 | } 210 | 211 | func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? { 212 | try self.getOptionalValue(for: key).map { try self.decodeBool(xpc: $0) } 213 | } 214 | 215 | func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? { 216 | try self.getOptionalValue(for: key).map { try self.decodeString(xpc: $0) } 217 | } 218 | 219 | func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? { 220 | try self.getOptionalValue(for: key).map { try self.decodeFloatingPoint(xpc: $0) } 221 | } 222 | 223 | func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? { 224 | try self.getOptionalValue(for: key).map { try self.decodeFloatingPoint(xpc: $0) } 225 | } 226 | 227 | func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? { 228 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 229 | } 230 | 231 | func decodeIfPresent(_ type: Int8.Type, forKey key: Key) throws -> Int8? { 232 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 233 | } 234 | 235 | func decodeIfPresent(_ type: Int16.Type, forKey key: Key) throws -> Int16? { 236 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 237 | } 238 | 239 | func decodeIfPresent(_ type: Int32.Type, forKey key: Key) throws -> Int32? { 240 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 241 | } 242 | 243 | func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? { 244 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 245 | } 246 | 247 | func decodeIfPresent(_ type: UInt.Type, forKey key: Key) throws -> UInt? { 248 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 249 | } 250 | 251 | func decodeIfPresent(_ type: UInt8.Type, forKey key: Key) throws -> UInt8? { 252 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 253 | } 254 | 255 | func decodeIfPresent(_ type: UInt16.Type, forKey key: Key) throws -> UInt16? { 256 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 257 | } 258 | 259 | func decodeIfPresent(_ type: UInt32.Type, forKey key: Key) throws -> UInt32? { 260 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 261 | } 262 | 263 | func decodeIfPresent(_ type: UInt64.Type, forKey key: Key) throws -> UInt64? { 264 | try self.getOptionalValue(for: key).map { try self.decodeInteger(xpc: $0) } 265 | } 266 | 267 | func decode(_ type: T.Type, forKey key: Key) throws -> T { 268 | let xpc = try self.getValue(for: key, allowNull: true) 269 | let codingPath = self.codingPath + [key] 270 | 271 | if type == XPCFileDescriptor.self { 272 | try checkType(xpcType: XPC_TYPE_FD, swiftType: XPCFileDescriptor.self, xpc: xpc) 273 | 274 | return XPCFileDescriptor(fileDescriptor: xpc_fd_dup(xpc)) as! T 275 | } else if #available(macOS 11.0, *), type == FileDescriptor.self { 276 | try checkType(xpcType: XPC_TYPE_FD, swiftType: FileDescriptor.self, xpc: xpc) 277 | 278 | return FileDescriptor(rawValue: xpc_fd_dup(xpc)) as! T 279 | } else if type == XPCEndpoint.self { 280 | try checkType(xpcType: XPC_TYPE_ENDPOINT, swiftType: XPCEndpoint.self, xpc: xpc) 281 | 282 | return XPCEndpoint(endpoint: xpc) as! T 283 | } else if type == XPCNull.self { 284 | try checkType(xpcType: XPC_TYPE_NULL, swiftType: XPCNull.self, xpc: xpc) 285 | 286 | return XPCNull.shared as! T 287 | } else { 288 | return try _XPCDecoder(xpc: xpc, codingPath: codingPath).decodeTopLevelObject() 289 | } 290 | } 291 | 292 | func nestedContainer( 293 | keyedBy type: NestedKey.Type, 294 | forKey key: Key 295 | ) throws -> KeyedDecodingContainer { 296 | let value = try self.getValue(for: key) 297 | let codingPath = self.codingPath + [key] 298 | 299 | return KeyedDecodingContainer(KeyedContainer(wrapping: value, codingPath: codingPath)) 300 | } 301 | 302 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 303 | let value = try self.getValue(for: key) 304 | let codingPath = self.codingPath + [key] 305 | 306 | return UnkeyedContainer(wrapping: value, codingPath: codingPath) 307 | } 308 | 309 | func superDecoder() throws -> Decoder { 310 | let xpc = try self.getValue(for: XPCEncoder.Key.super) 311 | 312 | return _XPCDecoder(xpc: xpc, codingPath: self.codingPath + [XPCEncoder.Key.super]) 313 | } 314 | 315 | func superDecoder(forKey key: Key) throws -> Decoder { 316 | let xpc = try self.getValue(for: XPCEncoder.Key.super) 317 | 318 | return _XPCDecoder(xpc: xpc, codingPath: self.codingPath + [key]) 319 | } 320 | } 321 | 322 | private final class UnkeyedContainer: UnkeyedDecodingContainer, XPCDecodingContainer { 323 | private enum Storage { 324 | case array(xpc_object_t) 325 | case data(ContiguousArray) 326 | case error(Error) 327 | } 328 | 329 | let dict: xpc_object_t 330 | private let storage: Storage 331 | 332 | let codingPath: [CodingKey] 333 | var count: Int? { 334 | switch self.storage { 335 | case .array(let array): 336 | return xpc_array_get_count(array) 337 | case .data(let data): 338 | return data.count 339 | case .error: 340 | return nil 341 | } 342 | } 343 | 344 | var isAtEnd: Bool { self.currentIndex >= (self.count ?? 0) } 345 | private(set) var currentIndex: Int 346 | 347 | var error: Error? { 348 | switch self.storage { 349 | case .error(let error): 350 | return error 351 | default: 352 | return nil 353 | } 354 | } 355 | 356 | init(wrapping dict: xpc_object_t, codingPath: [CodingKey]) { 357 | self.dict = dict 358 | self.codingPath = codingPath 359 | self.currentIndex = 0 360 | 361 | do { 362 | guard xpc_get_type(dict) == XPC_TYPE_DICTIONARY else { 363 | let type = String(cString: xpc_type_get_name(xpc_get_type(dict))) 364 | let description = "Expected dictionary, got \(type))" 365 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 366 | 367 | throw DecodingError.typeMismatch([String: Any].self, context) 368 | } 369 | 370 | guard let xpc = xpc_dictionary_get_value(dict, XPCEncoder.UnkeyedContainerDictionaryKeys.contents) else { 371 | let description = "Missing contents for unkeyed container" 372 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 373 | 374 | throw DecodingError.dataCorrupted(context) 375 | } 376 | 377 | switch xpc_get_type(xpc) { 378 | case XPC_TYPE_ARRAY: 379 | self.storage = .array(xpc) 380 | case XPC_TYPE_DATA: 381 | let length = xpc_data_get_length(xpc) 382 | let bytes = ContiguousArray(unsafeUninitializedCapacity: length) { buffer, count in 383 | if let ptr = buffer.baseAddress { 384 | count = xpc_data_get_bytes(xpc, ptr, 0, length) 385 | } else { 386 | count = 0 387 | } 388 | } 389 | 390 | if bytes.count != length { 391 | let description = "Couldn't read data for unknown reason" 392 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 393 | 394 | throw DecodingError.dataCorrupted(context) 395 | } 396 | 397 | self.storage = .data(bytes) 398 | default: 399 | let type = String(cString: xpc_type_get_name(xpc_get_type(xpc))) 400 | let description = "Invalid XPC type for unkeyed container: \(type)" 401 | let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: description) 402 | 403 | throw DecodingError.typeMismatch(Any.self, context) 404 | } 405 | } catch { 406 | self.storage = .error(error) 407 | } 408 | } 409 | 410 | private func readNext(xpcType: xpc_type_t?, swiftType: Any.Type) throws -> xpc_object_t { 411 | if self.isAtEnd { 412 | let context = self.makeErrorContext(description: "Premature end of array data") 413 | throw DecodingError.dataCorrupted(context) 414 | } 415 | 416 | switch self.storage { 417 | case .array(let array): 418 | defer { self.currentIndex += 1 } 419 | 420 | let value = xpc_array_get_value(array, self.currentIndex) 421 | 422 | if let xpcType = xpcType { 423 | try self.checkType(xpcType: xpcType, swiftType: swiftType, xpc: value) 424 | } 425 | 426 | return value 427 | case .data: 428 | throw DecodingError.dataCorruptedError( 429 | in: self, 430 | debugDescription: "Tried to read non-byte value from data" 431 | ) 432 | case .error(let error): 433 | throw error 434 | } 435 | } 436 | 437 | private func decodeFloatingPoint() throws -> F { 438 | try self.decodeFloatingPoint(xpc: self.readNext(xpcType: XPC_TYPE_DOUBLE, swiftType: F.self)) 439 | } 440 | 441 | private func decodeInteger() throws -> I { 442 | try self.decodeInteger(xpc: self.readNext(xpcType: nil, swiftType: I.self)) 443 | } 444 | 445 | private func decodeInteger() throws -> I { 446 | try self.decodeInteger(xpc: self.readNext(xpcType: nil, swiftType: I.self)) 447 | } 448 | 449 | private func decodeByte() throws -> UInt8 { 450 | if case .data(let bytes) = self.storage { 451 | if self.currentIndex > bytes.count { 452 | let context = self.makeErrorContext(description: "Read past end of data buffer") 453 | throw DecodingError.dataCorrupted(context) 454 | } 455 | 456 | defer { self.currentIndex += 1 } 457 | 458 | return bytes[self.currentIndex] 459 | } else { 460 | return try self.decodeInteger() 461 | } 462 | } 463 | 464 | private func decodeByte() throws -> Int8 { 465 | return Int8(bitPattern: try self.decodeByte()) 466 | } 467 | 468 | func decodeNil() throws -> Bool { 469 | _ = try self.readNext(xpcType: XPC_TYPE_NULL, swiftType: Any.self) 470 | return true 471 | } 472 | 473 | func decode(_ type: Bool.Type) throws -> Bool { 474 | try self.decodeBool(xpc: self.readNext(xpcType: XPC_TYPE_BOOL, swiftType: type)) 475 | } 476 | 477 | func decode(_ type: String.Type) throws -> String { 478 | try self.decodeString(xpc: try self.readNext(xpcType: XPC_TYPE_STRING, swiftType: type)) 479 | } 480 | 481 | func decode(_ type: Double.Type) throws -> Double { try self.decodeFloatingPoint() } 482 | func decode(_ type: Float.Type) throws -> Float { try self.decodeFloatingPoint() } 483 | func decode(_ type: Int.Type) throws -> Int { try self.decodeInteger() } 484 | func decode(_ type: Int8.Type) throws -> Int8 { try self.decodeByte() } 485 | func decode(_ type: Int16.Type) throws -> Int16 { try self.decodeInteger() } 486 | func decode(_ type: Int32.Type) throws -> Int32 { try self.decodeInteger() } 487 | func decode(_ type: Int64.Type) throws -> Int64 { try self.decodeInteger() } 488 | func decode(_ type: UInt.Type) throws -> UInt { try self.decodeInteger() } 489 | func decode(_ type: UInt8.Type) throws -> UInt8 { try self.decodeByte() } 490 | func decode(_ type: UInt16.Type) throws -> UInt16 { try self.decodeInteger() } 491 | func decode(_ type: UInt32.Type) throws -> UInt32 { try self.decodeInteger() } 492 | func decode(_ type: UInt64.Type) throws -> UInt64 { try self.decodeInteger() } 493 | 494 | func decode(_ type: T.Type) throws -> T { 495 | if type == Bool.self { 496 | return try self.decode(Bool.self) as! T 497 | } else if type == String.self { 498 | return try self.decode(String.self) as! T 499 | } else if type == Double.self { 500 | return try self.decode(Double.self) as! T 501 | } else if type == Float.self { 502 | return try self.decode(Float.self) as! T 503 | } else if type == Int.self { 504 | return try self.decode(Int.self) as! T 505 | } else if type == Int8.self { 506 | return try self.decode(Int8.self) as! T 507 | } else if type == Int16.self { 508 | return try self.decode(Int16.self) as! T 509 | } else if type == Int32.self { 510 | return try self.decode(Int32.self) as! T 511 | } else if type == Int64.self { 512 | return try self.decode(Int64.self) as! T 513 | } else if type == UInt.self { 514 | return try self.decode(UInt.self) as! T 515 | } else if type == UInt8.self { 516 | return try self.decode(UInt8.self) as! T 517 | } else if type == UInt16.self { 518 | return try self.decode(UInt16.self) as! T 519 | } else if type == UInt32.self { 520 | return try self.decode(UInt32.self) as! T 521 | } else if type == UInt64.self { 522 | return try self.decode(UInt64.self) as! T 523 | } else if type == XPCFileDescriptor.self { 524 | let xpc = try self.readNext(xpcType: XPC_TYPE_FD, swiftType: type) 525 | 526 | return XPCFileDescriptor(fileDescriptor: xpc_fd_dup(xpc)) as! T 527 | } else if #available(macOS 11.0, *), type == FileDescriptor.self { 528 | let xpc = try self.readNext(xpcType: XPC_TYPE_FD, swiftType: type) 529 | 530 | return FileDescriptor(rawValue: xpc_fd_dup(xpc)) as! T 531 | } else if type == XPCEndpoint.self { 532 | let xpc = try self.readNext(xpcType: XPC_TYPE_ENDPOINT, swiftType: type) 533 | 534 | return XPCEndpoint(endpoint: xpc) as! T 535 | } else if type == XPCNull.self { 536 | _ = try self.readNext(xpcType: XPC_TYPE_NULL, swiftType: XPCNull.self) 537 | 538 | return XPCNull.shared as! T 539 | } else { 540 | let codingPath = self.nextCodingPath() 541 | let xpc = try self.readNext(xpcType: nil, swiftType: type) 542 | 543 | return try _XPCDecoder(xpc: xpc, codingPath: codingPath).decodeTopLevelObject() 544 | } 545 | } 546 | 547 | func nestedContainer( 548 | keyedBy type: NestedKey.Type 549 | ) throws -> KeyedDecodingContainer { 550 | let codingPath = self.nextCodingPath() 551 | let xpc = try self.readNext(xpcType: nil, swiftType: Any.self) 552 | 553 | return KeyedDecodingContainer(KeyedContainer(wrapping: xpc, codingPath: codingPath)) 554 | } 555 | 556 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 557 | let codingPath = self.nextCodingPath() 558 | let xpc = try self.readNext(xpcType: nil, swiftType: Any.self) 559 | 560 | return UnkeyedContainer(wrapping: xpc, codingPath: codingPath) 561 | } 562 | 563 | func superDecoder() throws -> Decoder { 564 | let key = XPCEncoder.Key.super 565 | 566 | guard let xpc = xpc_dictionary_get_value(self.dict, key.stringValue) else { 567 | let context = self.makeErrorContext(description: "No encoded value for super") 568 | throw DecodingError.valueNotFound(Any.self, context) 569 | } 570 | 571 | return _XPCDecoder(xpc: xpc, codingPath: self.codingPath + [key]) 572 | } 573 | 574 | private func nextCodingPath() -> [CodingKey] { 575 | self.codingPath + [XPCEncoder.Key.arrayIndex(self.currentIndex)] 576 | } 577 | } 578 | 579 | private final class SingleValueContainer: SingleValueDecodingContainer, XPCDecodingContainer { 580 | let codingPath: [CodingKey] 581 | let xpc: xpc_object_t 582 | var error: Error? { nil } 583 | 584 | init(wrapping xpc: xpc_object_t, codingPath: [CodingKey]) { 585 | self.codingPath = codingPath 586 | self.xpc = xpc 587 | } 588 | 589 | func decodeNil() -> Bool { 590 | do { 591 | try self.decodeNil(xpc: self.xpc) 592 | return true 593 | } catch { 594 | return false 595 | } 596 | } 597 | 598 | func decode(_ type: Bool.Type) throws -> Bool { try self.decodeBool(xpc: self.xpc) } 599 | func decode(_ type: String.Type) throws -> String { try self.decodeString(xpc: self.xpc) } 600 | func decode(_ type: Double.Type) throws -> Double { try self.decodeFloatingPoint(xpc: self.xpc) } 601 | func decode(_ type: Float.Type) throws -> Float { try self.decodeFloatingPoint(xpc: self.xpc) } 602 | func decode(_ type: Int.Type) throws -> Int { try self.decodeInteger(xpc: self.xpc) } 603 | func decode(_ type: Int8.Type) throws -> Int8 { try self.decodeInteger(xpc: self.xpc) } 604 | func decode(_ type: Int16.Type) throws -> Int16 { try self.decodeInteger(xpc: self.xpc) } 605 | func decode(_ type: Int32.Type) throws -> Int32 { try self.decodeInteger(xpc: self.xpc) } 606 | func decode(_ type: Int64.Type) throws -> Int64 { try self.decodeInteger(xpc: self.xpc) } 607 | func decode(_ type: UInt.Type) throws -> UInt { try self.decodeInteger(xpc: self.xpc) } 608 | func decode(_ type: UInt8.Type) throws -> UInt8 { try self.decodeInteger(xpc: self.xpc) } 609 | func decode(_ type: UInt16.Type) throws -> UInt16 { try self.decodeInteger(xpc: self.xpc) } 610 | func decode(_ type: UInt32.Type) throws -> UInt32 { try self.decodeInteger(xpc: self.xpc) } 611 | func decode(_ type: UInt64.Type) throws -> UInt64 { try self.decodeInteger(xpc: self.xpc) } 612 | 613 | func decode(_ type: T.Type) throws -> T { 614 | if type == XPCFileDescriptor.self { 615 | try checkType(xpcType: XPC_TYPE_FD, swiftType: XPCFileDescriptor.self, xpc: self.xpc) 616 | 617 | return XPCFileDescriptor(fileDescriptor: xpc_fd_dup(self.xpc)) as! T 618 | } else if #available(macOS 11.0, *), type == FileDescriptor.self { 619 | try checkType(xpcType: XPC_TYPE_FD, swiftType: XPCFileDescriptor.self, xpc: self.xpc) 620 | 621 | return FileDescriptor(rawValue: xpc_fd_dup(self.xpc)) as! T 622 | } else if type == XPCEndpoint.self { 623 | try checkType(xpcType: XPC_TYPE_ENDPOINT, swiftType: type, xpc: self.xpc) 624 | 625 | return XPCEndpoint(endpoint: self.xpc) as! T 626 | } else if type == XPCNull.self { 627 | try checkType(xpcType: XPC_TYPE_NULL, swiftType: XPCNull.self, xpc: self.xpc) 628 | 629 | return XPCNull.shared as! T 630 | } else { 631 | return try _XPCDecoder(xpc: self.xpc, codingPath: self.codingPath).decodeTopLevelObject() 632 | } 633 | } 634 | } 635 | 636 | private final class _XPCDecoder: Decoder { 637 | let xpc: xpc_object_t 638 | let codingPath: [CodingKey] 639 | var userInfo: [CodingUserInfoKey: Any] { [:] } 640 | var topLevelContainer: XPCDecodingContainer? = nil 641 | 642 | init(xpc: xpc_object_t, codingPath: [CodingKey]) { 643 | self.xpc = xpc 644 | self.codingPath = codingPath 645 | } 646 | 647 | func decodeTopLevelObject() throws -> T { 648 | if #available(macOS 13.0, *), 649 | xpc_get_type(self.xpc) == XPC_TYPE_DICTIONARY, 650 | let content = xpc_dictionary_get_value(self.xpc, XPCEncoder.UnkeyedContainerDictionaryKeys.contents), 651 | xpc_get_type(content) == XPC_TYPE_DATA, 652 | let bytes = T.self as? any RangeReplaceableCollection.Type { 653 | let buffer = UnsafeRawBufferPointer( 654 | start: xpc_data_get_bytes_ptr(content), 655 | count: xpc_data_get_length(content) 656 | ) 657 | 658 | return bytes.init(buffer) as! T 659 | } 660 | 661 | let value = try T.init(from: self) 662 | 663 | if let error = self.topLevelContainer?.error { 664 | throw error 665 | } 666 | 667 | return value 668 | } 669 | 670 | func container(keyedBy type: Key.Type) -> KeyedDecodingContainer { 671 | precondition(self.topLevelContainer == nil, "Can only have one top-level container") 672 | 673 | let container = KeyedContainer(wrapping: self.xpc, codingPath: self.codingPath) 674 | self.topLevelContainer = container 675 | 676 | return KeyedDecodingContainer(container) 677 | } 678 | 679 | func unkeyedContainer() -> UnkeyedDecodingContainer { 680 | precondition(self.topLevelContainer == nil, "Can only have one top-level container") 681 | 682 | let container = UnkeyedContainer(wrapping: self.xpc, codingPath: self.codingPath) 683 | self.topLevelContainer = container 684 | 685 | return container 686 | } 687 | 688 | func singleValueContainer() -> SingleValueDecodingContainer { 689 | precondition(self.topLevelContainer == nil, "Can only have one top-level container") 690 | 691 | let container = SingleValueContainer(wrapping: self.xpc, codingPath: self.codingPath) 692 | self.topLevelContainer = container 693 | 694 | return container 695 | } 696 | } 697 | 698 | /// Create an `XPCDecoder`. 699 | public init() {} 700 | 701 | /// Decode an XPC object originating from a remote connection. 702 | /// 703 | /// - Parameters: 704 | /// - type: The expected type of the decoded object. 705 | /// - xpcObject: The XPC object to decode. 706 | /// 707 | /// - Returns: The decoded value. 708 | /// 709 | /// - Throws: Any errors that come up in the process of decoding the XPC object. 710 | public func decode(type: T.Type, from xpcObject: xpc_object_t) throws -> T { 711 | let decoder = _XPCDecoder(xpc: xpcObject, codingPath: []) 712 | let container = decoder.singleValueContainer() 713 | 714 | return try container.decode(type) 715 | } 716 | } 717 | --------------------------------------------------------------------------------