-
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
Add Endpoint APIs #23
Conversation
// MARK: Implementation | ||
|
||
internal let serviceName: String? | ||
private var connection: xpc_connection_t? = nil | ||
|
||
/// Creates a client which will attempt to send messages to the specified mach service. | ||
/// | ||
/// - Parameters: | ||
/// - serviceName: The name of the XPC service; no validation is performed on this. |
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 documentation is getting confusing as it references both mach services and XPC service. Additionally both parameters can be nil
but if both are is that actually valid? Although this is internal, some more documentation would be good.
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.
Ah yes, this doc comment is a mess.
Additionally both parameters can be nil but if both are is that actually valid?
Yes.
The serviceName
can be nil
for anonymous services. For regular (named) XPC services, and Mach services, it needs to be non-nil. Perhaps serviceName
should be an abstract property, and the subclasses can store their own name variable (which can be non-nil
for mach services.
When creating a client from an XPCEndpoint
, the connection is already created, and so the client just uses that. In other cases, connection
will be nil
, and the client will create a new connection for itself from the service name.
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.
Actually looking at this in more details, looks like there's no need for XPCClient
to have a serviceName
instance variable. This can purely be an internal implementation detail of the XPCMachClient
and XPCServiceClient
subclasses and a future XPCAnonymousClient
(or whatever it's called) subclasses wouldn't have one.
Similarly there doesn't appear to be any benefit to passing the connection into the initializer as it'll get created (or return an existing one for an XPCEndpoint
) as part of the createConnection()
function. Is that right?
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.
Actually looking at this in more details, looks like there's no need for
XPCClient
to have aserviceName
instance variable. This can purely be an internal implementation detail of theXPCMachClient
andXPCServiceClient
subclasses and a futureXPCAnonymousClient
(or whatever it's called) subclasses wouldn't have one.
Errrr I disagree. NSXPCConnection and the C APIs both take this approach, where connections are just these magical opaque things with very little exposed API. They happen to print their service name (if known) in their description
, but they're still annoying to deal with, because e.g. their name isn't visible from the object view in the debugger.
I think having the service name be exposed is easy enough that we should have it. It's just a nice convenience for development. (not that anybody should ever need to like, switch
over it, or anything like that)
Similarly there doesn't appear to be any benefit to passing the connection into the initializer as it'll get created (or return an existing one for an XPCEndpoint) as part of the createConnection() function. Is that right?
How will "or return an existing one for an XPCEndpoint" work if the connection isn't passed in?
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 still have to update the docs here (and elsewhere, probably), but could yo uplease look over my last comment?
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.
NSXPCConnection and the C APIs both take this approach, where connections are just these magical opaque things with very little exposed API. They happen to print their service name (if known) in their description, but they're still annoying to deal with, because e.g. their name isn't visible from the object view in the debugger.
Hmm, I think we just have a difference of opinion when it comes to API design here and I think Apple took the right approach. As far as I see it, this is why CustomDebugStringConvertible
exists. Unless I'm mistaken the value of debugDescription
can be visible in the debugger so long as it's not a computed property?
How will "or return an existing one for an XPCEndpoint" work if the connection isn't passed in?
It'd get passed into the initializer of a concrete subclass.
} | ||
/// Creates and returns a connection for the Mach service represented by this client. | ||
internal override func createConnection() -> xpc_connection_t { | ||
xpc_connection_create_mach_service(self.serviceName!, nil, 0) |
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.
Having the self.serviceName!
strikes me as a bit convoluted / code smell. Seems like this should be a private variable of this class which it takes into its initializer. It'd result in a bit more code, but would be cleaner.
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.
Are you thinking perhaps having out own machServiceName: String
and then having an override var serviceName: String? { machServiceName }
?
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 don't believe there's any need for the super class to have access to serviceName
. It appears to never be used. So yes to the first part, but no to the second.
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.
It would be used in debug descriptions of the object, and xpc endpoints (in the future when I add the ability to export a client as an endpoint)
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.
Is there a reason not to take the same approach as XPCServer
where each subclass actually implements this and the superclass just has:
public var endpoint: XPCServerEndpoint {
fatalError("Abstract Method")
}
In regards to debug descriptions, I'd think there'd be advantages to having the subclass do that because each subclass has the most comprehensive set of contextual information.
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 regards to debug descriptions, I'd think there'd be advantages to having the subclass do that because each subclass has the most comprehensive set of contextual information.
To clarify, I didn't necessarily mean CustomDebugStringConvertible
(although we should implement that, too), but the breakdown of the value when displayed in the debugger. Stored properties are nice because they're shown there directly, without needing po obj
or the like.
|
||
import Foundation | ||
|
||
public enum XPCConnectionType { |
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.
Since this is such a small enum and only ever used from XPServerEndpoint
would it be clearer if it was nested inside?
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 think I originally had more methods on it. I'll fold it in.
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.
Actually, it's used in the client and endpoint files, too. I think it should stay standalone. However, the cases could use better names (they shouldn't be client-specific)
|
||
import Foundation | ||
|
||
// TODO: make this codable so it can be sent over XPC. |
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.
Indeed, that's important :)
// Technically, an `xpc_endpoint_t` is sufficient to create a new connection, on its own. However, it's useful to | ||
// be able to communicate the kind of connection, and its name, so we also store those, separately. | ||
internal let kind: XPCConnectionType | ||
internal let serviceName: String? |
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 what circumstances is it valid for the name to be nil
?
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.
iff it's an anonymous service
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 think we should structure this as part of the XPCConnectionType
enum with an associated value such that when a connection must have a connection name, it has one, and when it can't then it's not possible to specify one. That'll let the compiler ensure we've implemented the invariant correctly. Thoughts?
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 play around with it and let you know how it goes!
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 I remember why I didn’t do this:
- it complicates the structure of the way this value gets encoded (IIRC Swift added the ability to auth-synthesize the codable conformance for an enumeration with associated values, but I haven’t played with that yet)
- It would make it impossible to build an XPCEndpoint out of a plain
xpc_endpoint_t
.
Whether the latter should be allowed at all comes down to a matter of design philosophy. My opinion is that we should allow people to access endpoints, connections, audit tokens, etc, if they want to. I don’t think a framework called “SecureXPC” should be afraid of exposing the fact that is uses XPC under the hood (it’s not a secret ;) ). NSXPCConnection was designed as a more opaque wrapper where everything should “just work”, but as you’ve probably seen, that adds a bit too much smoke and mirrors sometimes.
I think having “escape hatches” can be useful, particularly when looking for help. E.g. when working on the anonymous connection code, I ran into a fair number of difficulties that I only managed to solve by “inlining” the abstractions of this framework. This allowed me to more easily compare my work against the existing body of literature that covers the C and ObjC APIs.
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 regards to #1, if all of the associated values are Codable
then conformance is automatically generated by the compiler. XPCError
make use of this; several of its cases have String
associated values.
In regards to #2, I consider it an intentional design choice of SecureXPC that it does not expose the C API it uses. I don't have any issue with NSXPCConnection
in regards to its abstractions except that it's an Objective-C API which maps very awkwardly to Swift. My view is escape hatches cause many issues and should only be done as a last resort. Fundamentally I don't view SecureXPC as a wrapper of the XPC C API, I view it as its own XPC API which internally makes use of the XPC C API. (This is quite different than the Blessed framework I open sourced which is intentionally a Swift-friendly wrapper of the Authorization Services and Service Management frameworks.) Happy to discuss this in more depth in the Discussions section.
2480d40
to
75cd7be
Compare
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 think I've addressed most (all?) of your feedback. Wanna have another look?
// MARK: Implementation | ||
|
||
internal let serviceName: String? | ||
private var connection: xpc_connection_t? = nil | ||
|
||
/// Creates a client which will attempt to send messages to the specified mach service. | ||
/// | ||
/// - Parameters: | ||
/// - serviceName: The name of the XPC service; no validation is performed on this. |
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 still have to update the docs here (and elsewhere, probably), but could yo uplease look over my last comment?
} | ||
/// Creates and returns a connection for the Mach service represented by this client. | ||
internal override func createConnection() -> xpc_connection_t { | ||
xpc_connection_create_mach_service(self.serviceName!, nil, 0) |
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 regards to debug descriptions, I'd think there'd be advantages to having the subclass do that because each subclass has the most comprehensive set of contextual information.
To clarify, I didn't necessarily mean CustomDebugStringConvertible
(although we should implement that, too), but the breakdown of the value when displayed in the debugger. Stored properties are nice because they're shown there directly, without needing po obj
or the like.
@@ -193,6 +196,19 @@ internal class XPCMachServer: XPCServer { | |||
return accept | |||
} | |||
|
|||
public override var endpoint: XPCServerEndpoint { | |||
guard let connection = self.serviceListenerConection else { | |||
fatalError("You can only create an `endpoint` for an XPCMachServer after starting it with `start()`.") |
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've made the message more clear, LMK what you think.
xpc_connection_set_event_handler(connection, { (event: xpc_object_t) in | ||
fatalError("Is should be impossible for this connection to recieve an event.") | ||
}) | ||
xpc_connection_resume(connection) |
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.
FIXME: we shouldn't resume the client here. We should probably defer it until getConnection()
.
guard let connection = self.serviceListenerConnection else { | ||
fatalError(""" | ||
An XPCServer for a Mach service can only have its `connection` exported to an `endpoint` after | ||
a `connection` has been created by calling `start()`. |
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.
Remove the backticks since they'll just literally print as backticks?
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.
The concept of a connection
isn't part of the public API and I don't believe the fact this is for a Mach service is relevant (is it?) so probably better to say something like:
An XPCServer's endpoint can only be retrieved after start() has been called on it.
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.
(is it?)
I don't think that's something that could be reasonably withheld. This framework doesn't exist in isolation: I'd bet almost everyone looking to use it will be cross-referencing against the body of existing literature on XPC, where the existence of the connection is a core concept.
Try to hide the notion of a connection is also a leaky abstraction. Its existence leaks through the errors (e.g. .connectionInvalidated
), and it's explicitly mentioned in docc docs, in several places. In addition, the message latency is a side channel that hints towards the existing of a connection, and its currency status.
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 should've been more explicit in my feedback. As written you have connection
which to me made it sound like that was a property that was actually exposed to the end user. Users never directly create connections or have access to one so depending on how it's worded it could be confusing and just be easier to say it needs to be started, but I have no issue with a connection being mentioned.
In terms of it being for a Mach service, since all of the servers will need to be started it's probably just not very helpful to add the detail that a server for a Mach service needs to be started. This isn't about withholding anything, just that it doesn't seem relevant and therefore may cause the reader to read something into it that isn't applicable.
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 I see, that makes sense.
@@ -333,6 +333,14 @@ public class XPCServer { | |||
internal func acceptMessage(connection: xpc_connection_t, message: xpc_object_t) -> Bool { | |||
fatalError("Abstract Method") | |||
} | |||
|
|||
public var serviceName: String? { |
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.
Looks like XPCMachServer
doesn't override this.
Separately is this useful for an API user to know? Excluding anonymous servers there's likely to only ever be one server running, and anonymous servers don't have a service name.
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 use it in my logger in my project.
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.
Is logging that any more useful than logging the bundle id?
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.
Nah, but that's kind of an implementation detail. The "service name" is what I'm looking to log, and the bundle ID just happens to be the source for it at the moment.
75cd7be
to
20f75a9
Compare
It will be `nil` for anonymous connections.
7170cf8
to
bc4f6eb
Compare
@jakaplan I think I've addressed almost all the comments. Do you feel comfortable merging as-is and iterating on the remaining stuff? Stale PRs become increasingly hard to work on with time |
fatalError("An XPCServer's endpoint can only be retrieved after start() has been called on it.") | ||
} | ||
|
||
let endpoint = xpc_endpoint_create(connection) |
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'm assuming this actually works? I don't have any good way to test it, but just realized this is rather unintuitive because the connection being serialized into an endpoint here is the one that accepts new connections, not one which can be used to receive events.
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.
IIRC, it's "smart" and doesn't encode the service listener connection itself. When a connection it created from this, it's able to talk to the mach service.
However, this isn't something I can test right now, either
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.
Ah, what's stopping you from trying this out? I assume it doesn't actually need to be sent across processes in order to be tested (although normally not doing so would be pointless).
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.
How do you think we should proceed? I'm moving to a new house soon, so I won't be able to build out a test bed for this any time soon. I see two options:
- Yolo ship it, follow up on it later (with an issue to track it)
- Fatal-error this code path to say that it's untested, and treat it as effectively not-yet-implemented.
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's go with option 2 for now. I'd volunteer to try testing it out, but between end of year busyness at work and heading off on holiday soon I just won't have time before early January. Can revisit it then.
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.
Yes, pending an answer to whether the endpoint for |
bc4f6eb
to
7884d7b
Compare
This PR builds on top of the code introduced in #21. Do not ship until #21 is merged.
Add a new
XPCEndpoint
wrapper aroundxpc_endpoint_t
.