Skip to content

Commit

Permalink
Merge pull request #86 from trilemma-dev/sequential-improvements
Browse files Browse the repository at this point in the history
Provider finishes sequence on deinit, improved documentation
  • Loading branch information
jakaplan authored Apr 8, 2022
2 parents b587133 + 965bbb0 commit 318bb69
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 11 deletions.
7 changes: 4 additions & 3 deletions Sources/SecureXPC/Client/XPCClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 18 additions & 5 deletions Sources/SecureXPC/SequentialResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,33 @@
///
/// `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<Success, Failure> 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.
case failure(Failure)
/// 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
Expand Down
36 changes: 33 additions & 3 deletions Sources/SecureXPC/Server/SequentialResultProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -53,7 +56,28 @@ public class SequentialResultProvider<S: Encodable> {
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<S, Error>) {
switch result {
case .success(let value):
Expand All @@ -66,6 +90,8 @@ public class SequentialResultProvider<S: Encodable> {
}

/// 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)
Expand All @@ -75,6 +101,8 @@ public class SequentialResultProvider<S: Encodable> {
/// 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)
Expand All @@ -85,6 +113,8 @@ public class SequentialResultProvider<S: Encodable> {
}

/// 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 }
Expand Down

0 comments on commit 318bb69

Please sign in to comment.