Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provider finishes sequence on deinit, improved documentation #86

Merged
merged 1 commit into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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