diff --git a/Sources/SecureXPC/Client/XPCClient.swift b/Sources/SecureXPC/Client/XPCClient.swift index 9ef339b..99e68ae 100644 --- a/Sources/SecureXPC/Client/XPCClient.swift +++ b/Sources/SecureXPC/Client/XPCClient.swift @@ -541,9 +541,10 @@ public class XPCClient { // ordering between the invocations of the connection's event handler and the reply handler for that // message, even if they are targeted to the same queue. // - // The net effect of this is we can't do much with the reply such as terminating the sequence because this - // reply can arrive before some of the out-of-band sends from the server to client do. (This was attempted - // and it caused unit tests to fail non-deterministically.) + // The net effect of this is we can't do much with the reply such as terminating the sequence or + // deregistering the handler because this reply will sometimes arrive before some of the out-of-band sends + // from the server to client are received. (This was attempted and it caused unit tests to fail + // non-deterministically.) // // But if this is an internal XPC error (for example because the server shut down), we can use this to // update the connection's state. diff --git a/Sources/SecureXPC/SequentialResult.swift b/Sources/SecureXPC/SequentialResult.swift index 5172a83..102302f 100644 --- a/Sources/SecureXPC/SequentialResult.swift +++ b/Sources/SecureXPC/SequentialResult.swift @@ -9,12 +9,17 @@ /// /// `SequentialResult` is similar to [`Result`](https://developer.apple.com/documentation/swift/result), but represents one of arbitrarily /// many results that are returned in response to a request. +/// +/// ## Topics +/// ### Representing a Sequential Result +/// - ``success(_:)`` +/// - ``failure(_:)`` +/// - ``finished`` +/// ### As a Throwing Expression +/// - ``get()`` +/// - ``SequentialResultFinishedError`` public enum SequentialResult where Failure: Error { - public struct SequentialResultFinishedError: Error { - fileprivate init() { } - } - /// This portion of the sequence was succesfully created and is available. case success(Success) /// The sequence has finished in failure, there will be no more results. @@ -22,7 +27,15 @@ public enum SequentialResult where Failure: Error { /// The sequence has finished succesfully, there will be no more results. case finished - func get() throws -> Success { + /// An error thrown when ``get()`` is called on a ``finished`` sequential result. + public struct SequentialResultFinishedError: Error { + fileprivate init() { } + } + + /// Returns the success value as a throwing expression. + /// + /// If this represents ``finished`` then ``SequentialResultFinishedError`` will be thrown. + public func get() throws -> Success { switch self { case .success(let success): return success diff --git a/Sources/SecureXPC/Server/SequentialResultProvider.swift b/Sources/SecureXPC/Server/SequentialResultProvider.swift index f06f7b6..eb0fa2c 100644 --- a/Sources/SecureXPC/Server/SequentialResultProvider.swift +++ b/Sources/SecureXPC/Server/SequentialResultProvider.swift @@ -15,10 +15,13 @@ import Foundation /// - Synchronous without a message — ``XPCServer/registerRoute(_:handler:)-7r1hv`` /// - Synchronous with a message — ``XPCServer/registerRoute(_:handler:)-qcox`` /// -/// > Important: It is valid to use an instance of this class outside of the closure it was provided to. Responses will be sent so long as the client remains connected. +/// It is valid to use an instance of this class outside of the closure it was provided to. Responses will be sent so long as the client remains connected. /// -/// Any errors generated while using this provider will be passed to the ``XPCServer``'s error handler. Once a sequence has been either explicitly finished or -/// finishes because of an encoding error, any subsequent operations will not be sent and ``XPCError/sequenceFinished`` will be passed to the error handler. +/// Any errors generated while using this provider will be passed to the ``XPCServer``'s error handler. +/// +/// Once a sequence has been either explicitly finished or finishes because of an encoding error, any subsequent operations will not be sent and +/// ``XPCError/sequenceFinished`` will be passed to the error handler. If a sequence was not already finished, it will be finished upon deinitialization of this +/// provider instance. /// /// While provider instances are thread-safe, attempting concurrent responses is likely to lead to inconsistent ordering on the client side. If exact ordering is /// necessary, it is recommended that callers synchronize access to a provider instance. @@ -53,7 +56,28 @@ public class SequentialResultProvider { self.serialQueue = DispatchQueue(label: "response-provider-\(request.requestID)") } + /// Finishes the sequence if it hasn't already been. + deinit { + // There's no need to run this on the serial queue as deinit does not run concurrently with anything else + if !self.isFinished, let connection = connection { + // This intentionally doesn't call finished() because that would run async and by the time ir ran + // deinitialization may have (and in practice typically will have) already completed + do { + var response = xpc_dictionary_create(nil, nil, 0) + try Response.encodeRequestID(self.request.requestID, intoReply: &response) + xpc_connection_send_message(connection, response) + } catch { + self.sendToServerErrorHandler(error) + + // There's no point trying to send the encoding error to the client because encoding the requestID + // failed and that's needed by the client in order to properly reassociate the error with the request + } + } + } + /// Responds to the client with the provided result. + /// + /// - Parameter result: The sequential result to respond with. public func respond(withResult result: SequentialResult) { switch result { case .success(let value): @@ -66,6 +90,8 @@ public class SequentialResultProvider { } /// Responds to the client with the provided value. + /// + /// - Parameter value: The value to be sent. public func success(value: S) { self.sendResponse(isFinished: false) { response in try Response.encodePayload(value, intoReply: &response) @@ -75,6 +101,8 @@ public class SequentialResultProvider { /// Responds to the client with the provided error and finishes the sequence. /// /// This error will also be passed to the ``XPCServer``'s error handler if one has been set. + /// + /// - Parameter error: The error to be sent to the client and passed to the server's error handler. public func failure(error: Error) { let handlerError = HandlerError(error: error) self.sendToServerErrorHandler(handlerError) @@ -85,6 +113,8 @@ public class SequentialResultProvider { } /// Responds to the client indicating the sequence is now finished. + /// + /// If a sequence was not already finished, it will be finished upon deinitialization of this provider. public func finished() { // An "empty" response indicates it's finished self.sendResponse(isFinished: true) { _ in }