From 576f57bf517ebf16664d71df2b91c4c218fb7e61 Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Tue, 2 Jul 2024 16:01:07 -0700 Subject: [PATCH 1/9] add message name to unexpectedMessage error --- Sources/SwiftyXPC/XPCConnection.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftyXPC/XPCConnection.swift b/Sources/SwiftyXPC/XPCConnection.swift index 11b5878..59bda93 100644 --- a/Sources/SwiftyXPC/XPCConnection.swift +++ b/Sources/SwiftyXPC/XPCConnection.swift @@ -17,7 +17,7 @@ public class XPCConnection: @unchecked Sendable { /// An XPC message was missing its request and/or response body. case missingMessageBody /// Received an unhandled XPC message. - case unexpectedMessage + case unexpectedMessage(String) /// A message contained data of the wrong type. case typeMismatch(expected: XPCType, actual: XPCType) /// Only used on macOS versions prior to 12.0. @@ -519,7 +519,7 @@ public class XPCConnection: @unchecked Sendable { } guard let _messageHandler = self.getMessageHandler(forName: name) else { - throw Error.unexpectedMessage + throw Error.unexpectedMessage(name) } messageHandler = _messageHandler From 7ba2ed931bd19cd38aeb0a23ad38b6adedc88536 Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Mon, 15 Jul 2024 14:09:46 -0700 Subject: [PATCH 2/9] add connection state tracking --- Sources/SwiftyXPC/XPCConnection.swift | 31 +++++++++++++- Sources/SwiftyXPC/XPCListener.swift | 17 +++++++- Sources/TestHelper/TestHelper.swift | 12 +++++- Sources/TestShared/CommandSet.swift | 1 + Tests/SwiftyXPCTests/SwiftyXPCTests.swift | 52 ++++++++++++++++------- 5 files changed, 93 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftyXPC/XPCConnection.swift b/Sources/SwiftyXPC/XPCConnection.swift index 59bda93..a801c35 100644 --- a/Sources/SwiftyXPC/XPCConnection.swift +++ b/Sources/SwiftyXPC/XPCConnection.swift @@ -29,6 +29,11 @@ public class XPCConnection: @unchecked Sendable { static let body = "com.charlessoft.SwiftyXPC.XPCEventHandler.Body" static let error = "com.charlessoft.SwiftyXPC.XPCEventHandler.Error" } + + private struct StateMessages { + static let activate = "com.charlessoft.SwiftyXPC.XPCConnectionState.Activate" + static let deactivate = "com.charlessoft.SwiftyXPC.XPCConnectionState.Deactivate" + } /// Represents the various types of connection that can be created. public enum ConnectionType { @@ -42,6 +47,9 @@ public class XPCConnection: @unchecked Sendable { /// A handler that will be called if a communication error occurs. public typealias ErrorHandler = (XPCConnection, Swift.Error) -> Void + + /// A handler that will be called when the connection is cancelled. + public typealias CancelHandler = () -> Void internal class MessageHandler { typealias RawHandler = ((XPCConnection, xpc_object_t) async throws -> xpc_object_t) @@ -125,11 +133,21 @@ public class XPCConnection: @unchecked Sendable { /// A handler that will be called if a communication error occurs. public var errorHandler: ErrorHandler? = nil + + /// A handler that will be called when the connection is cancelled. + public var cancelHandler: CancelHandler? = nil internal var customEventHandler: xpc_handler_t? = nil internal func getMessageHandler(forName name: String) -> MessageHandler.RawHandler? { - self.messageHandlers[name]?.closure + switch name { + case StateMessages.activate: return { _, _ in try XPCEncoder().encode(XPCNull.shared) } + case StateMessages.deactivate: return { [cancelHandler] _, _ in + cancelHandler?() + return try XPCEncoder().encode(XPCNull.shared) + } + default: return self.messageHandlers[name]?.closure + } } /// Set a message handler for an incoming message, identified by the `name` parameter, without taking any arguments or returning any value. @@ -232,6 +250,12 @@ public class XPCConnection: @unchecked Sendable { /// Activate the connection. /// /// Connections start in an inactive state, so you must call `activate()` on a connection before it will send or receive any messages. + + public func activate() async throws { + xpc_connection_activate(self.connection) + try await sendMessage(name: StateMessages.activate) + } + public func activate() { xpc_connection_activate(self.connection) } @@ -262,6 +286,11 @@ public class XPCConnection: @unchecked Sendable { public func cancel() { xpc_connection_cancel(self.connection) } + + public func cancel() async throws { + try await sendMessage(name: StateMessages.deactivate) + xpc_connection_cancel(self.connection) + } internal func makeEndpoint() -> XPCEndpoint { XPCEndpoint(connection: self.connection) diff --git a/Sources/SwiftyXPC/XPCListener.swift b/Sources/SwiftyXPC/XPCListener.swift index 7f106f1..e855056 100644 --- a/Sources/SwiftyXPC/XPCListener.swift +++ b/Sources/SwiftyXPC/XPCListener.swift @@ -169,6 +169,14 @@ public final class XPCListener { } } } + + /// A handler that will be called when a new connection activates. + public typealias ActivatedConnectionHandler = (XPCConnection) -> Void + public var activatedConnectionHandler: ActivatedConnectionHandler? = nil + + /// A handler that will be called when a connection cancels. + public typealias CanceledConnectionHandler = (XPCConnection) -> Void + public var canceledConnectionHandler: CanceledConnectionHandler? = nil /// Create a new `XPCListener`. /// @@ -219,8 +227,13 @@ public final class XPCListener { newConnection.messageHandlers = self?.messageHandlers ?? [:] newConnection.errorHandler = self?.errorHandler + newConnection.cancelHandler = { + self?.canceledConnectionHandler?(newConnection) + } newConnection.activate() + + self?.activatedConnectionHandler?(newConnection) } catch { self?.errorHandler?(connection, error) } @@ -232,12 +245,12 @@ public final class XPCListener { /// /// After this call, any messages that have not yet been sent will be discarded, and the connection will be unwound. /// If there are messages that are awaiting replies, they will receive the `XPCError.connectionInvalid` error. - public func cancel() { + public func cancel() async throws { switch self.backing { case .xpcMain: fatalError("XPC service listener cannot be cancelled") case .connection(let connection, _): - connection.cancel() + try await connection.cancel() } } diff --git a/Sources/TestHelper/TestHelper.swift b/Sources/TestHelper/TestHelper.swift index 064909f..e4001b1 100644 --- a/Sources/TestHelper/TestHelper.swift +++ b/Sources/TestHelper/TestHelper.swift @@ -19,12 +19,22 @@ class XPCService { let listener = try XPCListener(type: .machService(name: helperID), codeSigningRequirement: nil) + var connectionCount: Int = 0 + listener.activatedConnectionHandler = { _ in + connectionCount += 1 + } + listener.canceledConnectionHandler = { _ in + connectionCount -= 1 + } listener.setMessageHandler(name: CommandSet.reportIDs, handler: xpcService.reportIDs) listener.setMessageHandler(name: CommandSet.capitalizeString, handler: xpcService.capitalizeString) listener.setMessageHandler(name: CommandSet.multiplyBy5, handler: xpcService.multiplyBy5) listener.setMessageHandler(name: CommandSet.transportData, handler: xpcService.transportData) listener.setMessageHandler(name: CommandSet.tellAJoke, handler: xpcService.tellAJoke) listener.setMessageHandler(name: CommandSet.pauseOneSecond, handler: xpcService.pauseOneSecond) + listener.setMessageHandler(name: CommandSet.countConnections, handler: { _ in + connectionCount + }) listener.activate() dispatchMain() @@ -70,7 +80,7 @@ class XPCService { codeSigningRequirement: nil ) - remoteConnection.activate() + try await remoteConnection.activate() let opening: String = try await remoteConnection.sendMessage(name: JokeMessage.askForJoke, request: "Tell me a joke") diff --git a/Sources/TestShared/CommandSet.swift b/Sources/TestShared/CommandSet.swift index 164e9b9..2593b29 100644 --- a/Sources/TestShared/CommandSet.swift +++ b/Sources/TestShared/CommandSet.swift @@ -13,4 +13,5 @@ public struct CommandSet { public static let transportData = "com.charlessoft.SwiftyXPC.Tests.TransportData" public static let tellAJoke = "com.charlessoft.SwiftyXPC.Tests.TellAJoke" public static let pauseOneSecond = "com.charlessoft.SwiftyXPC.Tests.PauseOneSecond" + public static let countConnections = "com.charlessoft.SwiftyXPC.Tests.CountConnections" } diff --git a/Tests/SwiftyXPCTests/SwiftyXPCTests.swift b/Tests/SwiftyXPCTests/SwiftyXPCTests.swift index 8ffa6b4..b113372 100644 --- a/Tests/SwiftyXPCTests/SwiftyXPCTests.swift +++ b/Tests/SwiftyXPCTests/SwiftyXPCTests.swift @@ -18,7 +18,7 @@ final class SwiftyXPCTests: XCTestCase { } func testProcessIDs() async throws { - let conn = try self.openConnection() + let conn = try await self.openConnection() let ids: ProcessIDs = try await conn.sendMessage(name: CommandSet.reportIDs) @@ -29,17 +29,17 @@ final class SwiftyXPCTests: XCTestCase { } func testCodeSignatureVerification() async throws { - let goodConn = try self.openConnection(codeSigningRequirement: self.helperLauncher!.codeSigningRequirement) + let goodConn = try await self.openConnection(codeSigningRequirement: self.helperLauncher!.codeSigningRequirement) let response: String = try await goodConn.sendMessage(name: CommandSet.capitalizeString, request: "Testing 1 2 3") XCTAssertEqual(response, "TESTING 1 2 3") - let badConn = try self.openConnection(codeSigningRequirement: "identifier \"com.apple.true\" and anchor apple") let failsSignatureVerification = self.expectation( description: "Fails to send message because of code signature mismatch" ) do { + let badConn = try await self.openConnection(codeSigningRequirement: "identifier \"com.apple.true\" and anchor apple") try await badConn.sendMessage(name: CommandSet.capitalizeString, request: "Testing 1 2 3") } catch let error as XPCError { if case .unknown(let errorDesc) = error, errorDesc == "Peer Forbidden" { @@ -54,7 +54,7 @@ final class SwiftyXPCTests: XCTestCase { ) do { - _ = try self.openConnection(codeSigningRequirement: "") + _ = try await self.openConnection(codeSigningRequirement: "") } catch XPCError.invalidCodeSignatureRequirement { failsConnectionInitialization.fulfill() } @@ -63,7 +63,7 @@ final class SwiftyXPCTests: XCTestCase { } func testSimpleRequestAndResponse() async throws { - let conn = try self.openConnection() + let conn = try await self.openConnection() let stringResponse: String = try await conn.sendMessage(name: CommandSet.capitalizeString, request: "hi there") XCTAssertEqual(stringResponse, "HI THERE") @@ -73,7 +73,7 @@ final class SwiftyXPCTests: XCTestCase { } func testDataTransport() async throws { - let conn = try self.openConnection() + let conn = try await self.openConnection() let dataInfo: DataInfo = try await conn.sendMessage( name: CommandSet.transportData, @@ -101,7 +101,7 @@ final class SwiftyXPCTests: XCTestCase { } func testTwoWayCommunication() async throws { - let conn = try self.openConnection() + let conn = try await self.openConnection() let listener = try XPCListener(type: .anonymous, codeSigningRequirement: nil) @@ -147,15 +147,18 @@ final class SwiftyXPCTests: XCTestCase { } listener.activate() - - try await conn.sendMessage(name: CommandSet.tellAJoke, request: listener.endpoint) + do { + try await conn.sendMessage(name: CommandSet.tellAJoke, request: listener.endpoint) + } catch { + print("ERRRR", error) + } await self.fulfillment(of: expectations, timeout: 10.0, enforceOrder: true) } func testTwoWayCommunicationWithError() async throws { XPCErrorRegistry.shared.registerDomain(forErrorType: JokeMessage.NotAKnockKnockJoke.self) - let conn = try self.openConnection() + let conn = try await self.openConnection() let listener = try XPCListener(type: .anonymous, codeSigningRequirement: nil) @@ -190,7 +193,7 @@ final class SwiftyXPCTests: XCTestCase { } func testOnewayVsTwoWay() async throws { - let conn = try self.openConnection() + let conn = try await self.openConnection() var date = Date.now try await conn.sendMessage(name: CommandSet.pauseOneSecond) @@ -202,12 +205,12 @@ final class SwiftyXPCTests: XCTestCase { } func testCancelConnection() async throws { - let conn = try self.openConnection() + let conn = try await self.openConnection() let response: String = try await conn.sendMessage(name: CommandSet.capitalizeString, request: "will work") XCTAssertEqual(response, "WILL WORK") - conn.cancel() + try await conn.cancel() let err: Error? do { @@ -222,13 +225,30 @@ final class SwiftyXPCTests: XCTestCase { return } } - - private func openConnection(codeSigningRequirement: String? = nil) throws -> XPCConnection { + + func testSimpleConnectionCounting() async throws { + let conn = try await self.openConnection() + + var count: Int = try await conn.sendMessage(name: CommandSet.countConnections) + XCTAssertEqual(count, 1) + + let conn2 = try await self.openConnection() + + count = try await conn.sendMessage(name: CommandSet.countConnections) + XCTAssertEqual(count, 2) + + try await conn2.cancel() + + count = try await conn.sendMessage(name: CommandSet.countConnections) + XCTAssertEqual(count, 1) + } + + private func openConnection(codeSigningRequirement: String? = nil) async throws -> XPCConnection { let conn = try XPCConnection( type: .remoteMachService(serviceName: helperID, isPrivilegedHelperTool: false), codeSigningRequirement: codeSigningRequirement ?? self.helperLauncher?.codeSigningRequirement ) - conn.activate() + try await conn.activate() return conn } From a5fdead8d3eabf5ce22e848495946189520c9b72 Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Mon, 15 Jul 2024 14:25:16 -0700 Subject: [PATCH 3/9] listener activate async too --- Sources/SwiftyXPC/XPCListener.swift | 4 ++-- Sources/TestHelper/TestHelper.swift | 6 ++++-- Tests/SwiftyXPCTests/SwiftyXPCTests.swift | 8 ++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftyXPC/XPCListener.swift b/Sources/SwiftyXPC/XPCListener.swift index e855056..64fca36 100644 --- a/Sources/SwiftyXPC/XPCListener.swift +++ b/Sources/SwiftyXPC/XPCListener.swift @@ -257,7 +257,7 @@ public final class XPCListener { /// Activate the connection. /// /// Listeners start in an inactive state, so you must call `activate()` on a connection before it will send or receive any messages. - public func activate() { + public func activate() async throws { switch self.backing { case .xpcMain: xpc_main { @@ -278,7 +278,7 @@ public final class XPCListener { } } case .connection(let connection, _): - connection.activate() + try await connection.activate() } } diff --git a/Sources/TestHelper/TestHelper.swift b/Sources/TestHelper/TestHelper.swift index e4001b1..78bd5f1 100644 --- a/Sources/TestHelper/TestHelper.swift +++ b/Sources/TestHelper/TestHelper.swift @@ -13,7 +13,7 @@ import TestShared @main @available(macOS 13.0, *) class XPCService { - static func main() { + static func main() async throws { do { let xpcService = XPCService() @@ -36,7 +36,9 @@ class XPCService { connectionCount }) - listener.activate() + Task { + try await listener.activate() + } dispatchMain() } catch { fatalError("Error while setting up XPC service: \(error)") diff --git a/Tests/SwiftyXPCTests/SwiftyXPCTests.swift b/Tests/SwiftyXPCTests/SwiftyXPCTests.swift index b113372..ffe4118 100644 --- a/Tests/SwiftyXPCTests/SwiftyXPCTests.swift +++ b/Tests/SwiftyXPCTests/SwiftyXPCTests.swift @@ -146,7 +146,9 @@ final class SwiftyXPCTests: XCTestCase { } } - listener.activate() + Task { + try await listener.activate() + } do { try await conn.sendMessage(name: CommandSet.tellAJoke, request: listener.endpoint) } catch { @@ -178,7 +180,9 @@ final class SwiftyXPCTests: XCTestCase { } } - listener.activate() + Task { + try await listener.activate() + } let failsToSendInvalidJoke = self.expectation(description: "Fails to send non-knock-knock joke") From 56f4199a70f7dd25edfeb1ceee74ef7612f03fae Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Mon, 15 Jul 2024 14:37:56 -0700 Subject: [PATCH 4/9] activate from listener should not send activation message because will block up the connection --- Sources/SwiftyXPC/XPCListener.swift | 4 ++-- Tests/SwiftyXPCTests/SwiftyXPCTests.swift | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftyXPC/XPCListener.swift b/Sources/SwiftyXPC/XPCListener.swift index 64fca36..e855056 100644 --- a/Sources/SwiftyXPC/XPCListener.swift +++ b/Sources/SwiftyXPC/XPCListener.swift @@ -257,7 +257,7 @@ public final class XPCListener { /// Activate the connection. /// /// Listeners start in an inactive state, so you must call `activate()` on a connection before it will send or receive any messages. - public func activate() async throws { + public func activate() { switch self.backing { case .xpcMain: xpc_main { @@ -278,7 +278,7 @@ public final class XPCListener { } } case .connection(let connection, _): - try await connection.activate() + connection.activate() } } diff --git a/Tests/SwiftyXPCTests/SwiftyXPCTests.swift b/Tests/SwiftyXPCTests/SwiftyXPCTests.swift index ffe4118..b113372 100644 --- a/Tests/SwiftyXPCTests/SwiftyXPCTests.swift +++ b/Tests/SwiftyXPCTests/SwiftyXPCTests.swift @@ -146,9 +146,7 @@ final class SwiftyXPCTests: XCTestCase { } } - Task { - try await listener.activate() - } + listener.activate() do { try await conn.sendMessage(name: CommandSet.tellAJoke, request: listener.endpoint) } catch { @@ -180,9 +178,7 @@ final class SwiftyXPCTests: XCTestCase { } } - Task { - try await listener.activate() - } + listener.activate() let failsToSendInvalidJoke = self.expectation(description: "Fails to send non-knock-knock joke") From 2bf47a489749ff1742f8e4cd933d367a2b116bbf Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Mon, 15 Jul 2024 14:42:58 -0700 Subject: [PATCH 5/9] fixup unused task wrapper --- Sources/TestHelper/TestHelper.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/TestHelper/TestHelper.swift b/Sources/TestHelper/TestHelper.swift index 78bd5f1..146f379 100644 --- a/Sources/TestHelper/TestHelper.swift +++ b/Sources/TestHelper/TestHelper.swift @@ -36,9 +36,7 @@ class XPCService { connectionCount }) - Task { - try await listener.activate() - } + listener.activate() dispatchMain() } catch { fatalError("Error while setting up XPC service: \(error)") From 2b576bfbbb0d2fa988753cb8b5758b50a89d177d Mon Sep 17 00:00:00 2001 From: Charles Srstka Date: Mon, 2 Sep 2024 18:25:42 -0400 Subject: [PATCH 6/9] Clean up concurrency for Swift 6 --- Sources/SwiftyXPC/XPCConnection.swift | 12 +++--- Sources/SwiftyXPC/XPCErrorRegistry.swift | 55 ++++++++++++++++++++---- Sources/TestHelper/TestHelper.swift | 14 +++--- Sources/TestShared/DataInfo.swift | 2 +- Sources/TestShared/ProcessIDs.swift | 2 +- 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/Sources/SwiftyXPC/XPCConnection.swift b/Sources/SwiftyXPC/XPCConnection.swift index a801c35..3f56206 100644 --- a/Sources/SwiftyXPC/XPCConnection.swift +++ b/Sources/SwiftyXPC/XPCConnection.swift @@ -324,7 +324,7 @@ public class XPCConnection: @unchecked Sendable { /// - Returns: The value returned by the receiving connection's helper function. /// /// - Throws: Throws an error if the receiving connection throws an error in its handler, or if a communication error occurs. - public func sendMessage(name: String) async throws -> Response { + public func sendMessage(name: String) async throws -> Response { try await self.sendMessage(name: name, request: XPCNull.shared) } @@ -338,7 +338,7 @@ public class XPCConnection: @unchecked Sendable { /// /// - Throws: Throws an error the `request` parameter does not match the type specified by the receiving connection’s handler function, /// if the receiving connection throws an error in its handler, or if a communication error occurs. - public func sendMessage(name: String, request: Request) async throws -> Response { + public func sendMessage(name: String, request: some Codable) async throws -> Response { let body = try XPCEncoder().encode(request) return try await withCheckedThrowingContinuation { continuation in @@ -363,11 +363,13 @@ public class XPCConnection: @unchecked Sendable { throw Error.missingMessageBody } - if Response.self == XPCNull.self { - continuation.resume(returning: XPCNull() as! Response) + let response: Response = if Response.self == XPCNull.self { + XPCNull() as! Response } else { - continuation.resume(returning: try XPCDecoder().decode(type: Response.self, from: body)) + try XPCDecoder().decode(type: Response.self, from: body) } + + continuation.resume(returning: response) } catch { continuation.resume(throwing: error) } diff --git a/Sources/SwiftyXPC/XPCErrorRegistry.swift b/Sources/SwiftyXPC/XPCErrorRegistry.swift index b1219b7..721ce7a 100644 --- a/Sources/SwiftyXPC/XPCErrorRegistry.swift +++ b/Sources/SwiftyXPC/XPCErrorRegistry.swift @@ -5,6 +5,7 @@ // Created by Charles Srstka on 12/19/21. // +import Synchronization import XPC /// A registry which facilitates decoding error types that are sent over an XPC connection. @@ -42,14 +43,46 @@ import XPC /// } catch { /// print("got some other error") /// } -public class XPCErrorRegistry { +public final class XPCErrorRegistry: Sendable { /// The shared `XPCErrorRegistry` instance. public static let shared = XPCErrorRegistry() - private var errorDomainMap: [String: (Error & Codable).Type] = [ - String(reflecting: XPCError.self): XPCError.self, - String(reflecting: XPCConnection.Error.self): XPCConnection.Error.self, - ] + @available(macOS 15.0, macCatalyst 18.0, *) + private final class MutexWrapper: Sendable { + let mutex: Mutex<[String: (Error & Codable).Type]> + init(dict: [String: (Error & Codable).Type]) { self.mutex = Mutex(dict) } + } + + private final class LegacyWrapper: @unchecked Sendable { + let sema = DispatchSemaphore(value: 1) + var dict: [String: (Error & Codable).Type] + init(dict: [String: (Error & Codable).Type]) { self.dict = dict } + } + + private let errorDomainMapWrapper: any Sendable = { + let errorDomainMap: [String: (Error & Codable).Type] = [ + String(reflecting: XPCError.self): XPCError.self, + String(reflecting: XPCConnection.Error.self): XPCConnection.Error.self, + ] + + if #available(macOS 15.0, macCatalyst 18.0, *) { + return MutexWrapper(dict: errorDomainMap) + } else { + return LegacyWrapper(dict: errorDomainMap) + } + }() + + private func withLock(closure: (inout [String: (Error & Codable).Type]) throws -> T) rethrows -> T { + if #available(macOS 15.0, macCatalyst 18.0, *) { + return try (self.errorDomainMapWrapper as! MutexWrapper).mutex.withLock { try closure(&$0) } + } else { + let wrapper = self.errorDomainMapWrapper as! LegacyWrapper + wrapper.sema.wait() + defer { wrapper.sema.signal() } + + return try closure(&wrapper.dict) + } + } /// Register an error type. /// @@ -57,11 +90,13 @@ public class XPCErrorRegistry { /// - 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. /// - errorType: An error type to register. This type must conform to `Codable`. public func registerDomain(_ domain: String? = nil, forErrorType errorType: (Error & Codable).Type) { - errorDomainMap[domain ?? String(reflecting: errorType)] = errorType + self.withLock { $0[domain ?? String(reflecting: errorType)] = errorType } } internal func encodeError(_ error: Error, domain: String? = nil) throws -> xpc_object_t { - try XPCEncoder().encode(BoxedError(error: error, domain: domain)) + try self.withLock { _ in + try XPCEncoder().encode(BoxedError(error: error, domain: domain)) + } } internal func decodeError(_ error: xpc_object_t) throws -> Error { @@ -70,6 +105,10 @@ public class XPCErrorRegistry { return boxedError.encodedError ?? boxedError } + internal func errorType(forDomain domain: String) -> (any (Error & Codable).Type)? { + self.withLock { $0[domain] } + } + /// An error type representing errors for which we have an `NSError`-style domain and code, but do not know the exact error class. /// /// To avoid requiring Foundation, this type does not formally adopt the `CustomNSError` protocol, but implements methods which @@ -155,7 +194,7 @@ public class XPCErrorRegistry { self.errorDomain = try container.decode(String.self, forKey: .domain) let code = try container.decode(Int.self, forKey: .code) - if let codableType = XPCErrorRegistry.shared.errorDomainMap[self.errorDomain], + if let codableType = XPCErrorRegistry.shared.errorType(forDomain: self.errorDomain), let codableError = try codableType.decodeIfPresent(from: container, key: .encodedError) { self.storage = .codable(codableError) diff --git a/Sources/TestHelper/TestHelper.swift b/Sources/TestHelper/TestHelper.swift index 146f379..6343f78 100644 --- a/Sources/TestHelper/TestHelper.swift +++ b/Sources/TestHelper/TestHelper.swift @@ -12,8 +12,8 @@ import TestShared @main @available(macOS 13.0, *) -class XPCService { - static func main() async throws { +final class XPCService: Sendable { + static func main() { do { let xpcService = XPCService() @@ -32,9 +32,11 @@ class XPCService { listener.setMessageHandler(name: CommandSet.transportData, handler: xpcService.transportData) listener.setMessageHandler(name: CommandSet.tellAJoke, handler: xpcService.tellAJoke) listener.setMessageHandler(name: CommandSet.pauseOneSecond, handler: xpcService.pauseOneSecond) - listener.setMessageHandler(name: CommandSet.countConnections, handler: { _ in - connectionCount - }) + listener.setMessageHandler( + name: CommandSet.countConnections, + handler: { _ in + connectionCount + }) listener.activate() dispatchMain() @@ -69,7 +71,7 @@ class XPCService { "Noonien Soong".data(using: .utf8)!, "Arik Soong".data(using: .utf8)!, "Altan Soong".data(using: .utf8)!, - "Adam Soong".data(using: .utf8)! + "Adam Soong".data(using: .utf8)!, ] ) } diff --git a/Sources/TestShared/DataInfo.swift b/Sources/TestShared/DataInfo.swift index d616fbd..c49c453 100644 --- a/Sources/TestShared/DataInfo.swift +++ b/Sources/TestShared/DataInfo.swift @@ -7,7 +7,7 @@ import Foundation -public struct DataInfo: Codable { +public struct DataInfo: Codable, Sendable { public struct DataError: LocalizedError, Codable { public let failureReason: String? public init(failureReason: String) { self.failureReason = failureReason } diff --git a/Sources/TestShared/ProcessIDs.swift b/Sources/TestShared/ProcessIDs.swift index 42b866a..9d0c9a0 100644 --- a/Sources/TestShared/ProcessIDs.swift +++ b/Sources/TestShared/ProcessIDs.swift @@ -10,7 +10,7 @@ import SwiftyXPC import System // swift-format-ignore: AllPublicDeclarationsHaveDocumentation -public struct ProcessIDs: Codable { +public struct ProcessIDs: Codable, Sendable { public let pid: pid_t public let effectiveUID: uid_t public let effectiveGID: gid_t From 266fff4e416f5d9d9cc0c23f6f51444fd540c4f6 Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Thu, 12 Jun 2025 13:37:33 -0700 Subject: [PATCH 7/9] make XPCConnection cancel handler async so service can cleanup --- Sources/SwiftyXPC/XPCConnection.swift | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftyXPC/XPCConnection.swift b/Sources/SwiftyXPC/XPCConnection.swift index 3f56206..0facc55 100644 --- a/Sources/SwiftyXPC/XPCConnection.swift +++ b/Sources/SwiftyXPC/XPCConnection.swift @@ -29,7 +29,7 @@ public class XPCConnection: @unchecked Sendable { static let body = "com.charlessoft.SwiftyXPC.XPCEventHandler.Body" static let error = "com.charlessoft.SwiftyXPC.XPCEventHandler.Error" } - + private struct StateMessages { static let activate = "com.charlessoft.SwiftyXPC.XPCConnectionState.Activate" static let deactivate = "com.charlessoft.SwiftyXPC.XPCConnectionState.Deactivate" @@ -47,9 +47,9 @@ public class XPCConnection: @unchecked Sendable { /// A handler that will be called if a communication error occurs. public typealias ErrorHandler = (XPCConnection, Swift.Error) -> Void - + /// A handler that will be called when the connection is cancelled. - public typealias CancelHandler = () -> Void + public typealias CancelHandler = () async -> Void internal class MessageHandler { typealias RawHandler = ((XPCConnection, xpc_object_t) async throws -> xpc_object_t) @@ -133,7 +133,7 @@ public class XPCConnection: @unchecked Sendable { /// A handler that will be called if a communication error occurs. public var errorHandler: ErrorHandler? = nil - + /// A handler that will be called when the connection is cancelled. public var cancelHandler: CancelHandler? = nil @@ -142,10 +142,11 @@ public class XPCConnection: @unchecked Sendable { internal func getMessageHandler(forName name: String) -> MessageHandler.RawHandler? { switch name { case StateMessages.activate: return { _, _ in try XPCEncoder().encode(XPCNull.shared) } - case StateMessages.deactivate: return { [cancelHandler] _, _ in - cancelHandler?() - return try XPCEncoder().encode(XPCNull.shared) - } + case StateMessages.deactivate: + return { [cancelHandler] _, _ in + await cancelHandler?() + return try XPCEncoder().encode(XPCNull.shared) + } default: return self.messageHandlers[name]?.closure } } @@ -250,12 +251,12 @@ public class XPCConnection: @unchecked Sendable { /// Activate the connection. /// /// Connections start in an inactive state, so you must call `activate()` on a connection before it will send or receive any messages. - + public func activate() async throws { xpc_connection_activate(self.connection) try await sendMessage(name: StateMessages.activate) } - + public func activate() { xpc_connection_activate(self.connection) } @@ -286,7 +287,7 @@ public class XPCConnection: @unchecked Sendable { public func cancel() { xpc_connection_cancel(self.connection) } - + public func cancel() async throws { try await sendMessage(name: StateMessages.deactivate) xpc_connection_cancel(self.connection) @@ -363,11 +364,12 @@ public class XPCConnection: @unchecked Sendable { throw Error.missingMessageBody } - let response: Response = if Response.self == XPCNull.self { - XPCNull() as! Response - } else { - try XPCDecoder().decode(type: Response.self, from: body) - } + let response: Response = + if Response.self == XPCNull.self { + XPCNull() as! Response + } else { + try XPCDecoder().decode(type: Response.self, from: body) + } continuation.resume(returning: response) } catch { From 6d44d5711050361104598e18e4e7894c2d8ce5d6 Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Thu, 12 Jun 2025 13:42:54 -0700 Subject: [PATCH 8/9] make XPCConnection error handler async so service can cleanup --- Sources/SwiftyXPC/XPCConnection.swift | 14 +++++++++----- Sources/SwiftyXPC/XPCListener.swift | 10 ++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftyXPC/XPCConnection.swift b/Sources/SwiftyXPC/XPCConnection.swift index 0facc55..8108de5 100644 --- a/Sources/SwiftyXPC/XPCConnection.swift +++ b/Sources/SwiftyXPC/XPCConnection.swift @@ -46,7 +46,7 @@ public class XPCConnection: @unchecked Sendable { } /// A handler that will be called if a communication error occurs. - public typealias ErrorHandler = (XPCConnection, Swift.Error) -> Void + public typealias ErrorHandler = (XPCConnection, Swift.Error) async -> Void /// A handler that will be called when the connection is cancelled. public typealias CancelHandler = () async -> Void @@ -459,7 +459,9 @@ public class XPCConnection: @unchecked Sendable { do { try self.checkCallerCredentials(event: event) } catch { - self.errorHandler?(self, error) + Task { + await self.errorHandler?(self, error) + } return } } @@ -483,7 +485,9 @@ public class XPCConnection: @unchecked Sendable { throw Error.typeMismatch(expected: .dictionary, actual: event.type) } } catch { - self.errorHandler?(self, error) + Task { + await self.errorHandler?(self, error) + } return } } @@ -557,7 +561,7 @@ public class XPCConnection: @unchecked Sendable { messageHandler = _messageHandler } catch { - self.errorHandler?(self, error) + await self.errorHandler?(self, error) return } @@ -573,7 +577,7 @@ public class XPCConnection: @unchecked Sendable { do { try self.sendOnewayRawMessage(name: nil, body: response, key: MessageKeys.body, asReplyTo: event) } catch { - self.errorHandler?(self, error) + await self.errorHandler?(self, error) } } } diff --git a/Sources/SwiftyXPC/XPCListener.swift b/Sources/SwiftyXPC/XPCListener.swift index e855056..a1f6b4e 100644 --- a/Sources/SwiftyXPC/XPCListener.swift +++ b/Sources/SwiftyXPC/XPCListener.swift @@ -169,11 +169,11 @@ public final class XPCListener { } } } - + /// A handler that will be called when a new connection activates. public typealias ActivatedConnectionHandler = (XPCConnection) -> Void public var activatedConnectionHandler: ActivatedConnectionHandler? = nil - + /// A handler that will be called when a connection cancels. public typealias CanceledConnectionHandler = (XPCConnection) -> Void public var canceledConnectionHandler: CanceledConnectionHandler? = nil @@ -232,10 +232,12 @@ public final class XPCListener { } newConnection.activate() - + self?.activatedConnectionHandler?(newConnection) } catch { - self?.errorHandler?(connection, error) + Task { + await self?.errorHandler?(connection, error) + } } } } From 088af8219009a07716a9678603be0278c19b5ef5 Mon Sep 17 00:00:00 2001 From: Zane Shannon Date: Thu, 12 Jun 2025 14:15:58 -0700 Subject: [PATCH 9/9] make XPCListener activated/canceled handlers async so service can cleanup --- Sources/SwiftyXPC/XPCConnection.swift | 6 ++++-- Sources/SwiftyXPC/XPCListener.swift | 12 +++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftyXPC/XPCConnection.swift b/Sources/SwiftyXPC/XPCConnection.swift index 8108de5..c1bf1ed 100644 --- a/Sources/SwiftyXPC/XPCConnection.swift +++ b/Sources/SwiftyXPC/XPCConnection.swift @@ -137,7 +137,7 @@ public class XPCConnection: @unchecked Sendable { /// A handler that will be called when the connection is cancelled. public var cancelHandler: CancelHandler? = nil - internal var customEventHandler: xpc_handler_t? = nil + internal var customEventHandler: ((xpc_object_t) async -> Void)? = nil internal func getMessageHandler(forName name: String) -> MessageHandler.RawHandler? { switch name { @@ -467,7 +467,9 @@ public class XPCConnection: @unchecked Sendable { } if let customEventHandler = self.customEventHandler { - customEventHandler(event) + Task { + await customEventHandler(event) + } return } diff --git a/Sources/SwiftyXPC/XPCListener.swift b/Sources/SwiftyXPC/XPCListener.swift index a1f6b4e..6fbed7b 100644 --- a/Sources/SwiftyXPC/XPCListener.swift +++ b/Sources/SwiftyXPC/XPCListener.swift @@ -171,11 +171,11 @@ public final class XPCListener { } /// A handler that will be called when a new connection activates. - public typealias ActivatedConnectionHandler = (XPCConnection) -> Void + public typealias ActivatedConnectionHandler = (XPCConnection) async -> Void public var activatedConnectionHandler: ActivatedConnectionHandler? = nil /// A handler that will be called when a connection cancels. - public typealias CanceledConnectionHandler = (XPCConnection) -> Void + public typealias CanceledConnectionHandler = (XPCConnection) async -> Void public var canceledConnectionHandler: CanceledConnectionHandler? = nil /// Create a new `XPCListener`. @@ -228,12 +228,10 @@ public final class XPCListener { newConnection.messageHandlers = self?.messageHandlers ?? [:] newConnection.errorHandler = self?.errorHandler newConnection.cancelHandler = { - self?.canceledConnectionHandler?(newConnection) + await self?.canceledConnectionHandler?(newConnection) } - - newConnection.activate() - - self?.activatedConnectionHandler?(newConnection) + await self?.activatedConnectionHandler?(newConnection) + try await newConnection.activate() } catch { Task { await self?.errorHandler?(connection, error)