-
Notifications
You must be signed in to change notification settings - Fork 15
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
Allows for async functions/closures to be registered as routes #64
Conversation
Adds tests to match
@amomchilov For your review. I dislike how much code duplication this introduces, but I couldn't figure out a way to reduce it due to the cascading nature of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look at this in more depth over the weekend.
Until then, could you help me understand when a callback should be async
?
// MARK: Route registration | ||
|
||
private func checkUniqueRoute(route: XPCRoute) throws { | ||
if self.routes.keys.contains(route) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IDK how much it matters, but these two operations (checking it's not already there, and setting the new value) could be done in a single hash look-up with https://developer.apple.com/documentation/swift/dictionary/3127179-updatevalue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh yeah I don't think any efficiency gains matter here, I'd rather consolidate the error generation into just one place.
When the API user wants to create a callback which uses |
A realistic example could be an XPC service with a route provided a URL to a PDF. The service needs to download and then parse the PDF to extract some data. PDFs are a notorious exploit vector, so breaking out this component into an XPC service would be prudent. Using |
Ah yes, that makes perfect sense, thanks for the explanation |
@@ -137,11 +149,21 @@ public class XPCServer { | |||
/// - Throws: If this route has already been registered. | |||
public func registerRoute(_ route: XPCRouteWithoutMessageWithoutReply, | |||
handler: @escaping () throws -> Void) throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In retrospect, I don't think throwing here was a good idea (introduced in #28).
These errors can't reasonably recovered:
- "Do nothing" => the route doesn't act the way you expect
- "Name it something different" => makes it useless to the callers that expect a given name
Fatal erroring here might be more appropriate, so that route-registration doesn't need a try
(removing try
for brevity is a non-goal for me, but in some cases an unexpected try
raises a suspicion error was thrown that can't really be caught)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me think about this. If I make this change, will do so as part of a separate PR.
self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithoutReplySync(handler: handler) | ||
} | ||
|
||
/// Registers a route that has a message and can't receive a reply. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use more context-specific terminology:
/// Registers a route that sends a message and doesn't expect a reply.
This is similar to how below you say "expects a reply" rather than "has a reply"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this was my attempt to be specific. It's not that it doesn't expect a reply, it very much cannot have one - it's quite literally impossible with the way the code is written.
@@ -219,15 +271,10 @@ public class XPCServer { | |||
|
|||
private func handleEvent(connection: xpc_connection_t, event: xpc_object_t) { | |||
if xpc_get_type(event) == XPC_TYPE_DICTIONARY { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another case where my guard clause instinct tingles:
private func handleEvent(connection: xpc_connection_t, event: xpc_object_t) {
// Only dictionary types (and not anything which will be interpreted as an error) should be checked for
// message acceptance as errors generated by libxpc can fail to meet the acceptor's criteria
guard xpc_get_type(event) == XPC_TYPE_DICTIONARY else {
self.errorHandler?(XPCError.fromXPCObject(event))
return
}
guard self.messageAcceptor.acceptMessage(connection: connection, message: event) else {
self.errorHandler?(.insecure)
return
}
handleMessage(connection: connection, message: event)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm maybe it's just my relative lack of usage of guard
, but I find this a bit harder to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, I do prefer this. Have updated the comment to make a bit more sense with this control flow.
var reply = xpc_dictionary_create_reply(event) | ||
do { | ||
try handleMessage(connection: connection, message: event, reply: &reply) | ||
} catch { | ||
let error = XPCError.asXPCError(error: error, expectingOtherError: true) | ||
self.errorHandler?(error) | ||
self.replyWithErrorIfPossible(error, connection: connection, reply: &reply) | ||
} | ||
handleMessage(connection: connection, message: event) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nice 🧹
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧼
} | ||
} | ||
} else { | ||
fatalError("Server handler for route \(request.route.pathComponents) cannot be run: \(handler)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you clarify this case?
By my reading, it happens if you have a XPCHandlerAsync
registered, but don't pass the #available(macOS 10.15.0, *)
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. It shouldn't be possible, but seemed prudent to explicitly fail if it was encountered. I can improve the error message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just explain why it doesn't makes sense, along the lines of "The route wasn't an sync route, even that's the only kind that should be possible for this system"
self.handleError(error, expectingOtherError: true, connection: connection, reply: &reply) | ||
} | ||
|
||
} else if #available(macOS 10.15.0, *), let handler = handler as? XPCHandlerAsync { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh cool, I didn't know these compile-time and run-time checks could be combined into one statement like this. Neat!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's very convenient!
// If a dictionary reply exists, then the message expects a reply to be sent back | ||
if let reply = reply { | ||
xpc_connection_send_message(connection, reply) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be moved out of this do
block and after the catch below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NVM, I think my bias towards minimal do
blocks comes from Java/Ruby/etc. where any one of the lines can be the source of the thrown exception, and it's unclear which ones you intend to catch.
I have a new-found appreciation for try
. It makes this so clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be moved out of this
do
block and after the catch below?
I know you said never mind, but FWIW it needs to be in the do
block as it should only run if the handler succeeds. Otherwise both this logic and the logic in the catch
block would run resulting in two sends to the same reply
instance which would result in XPC API Misuse.
Adds tests to match