├── .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 |
--------------------------------------------------------------------------------