Is possible to use Mac OS X XPC like IPC to exchange messages between processes? How?
Asked Answered
O

3

25

According to Apple, the new XPC Services API, introduced in Lion, provides a lightweight mechanism for basic interprocess communication integrated with Grand Central Dispatch (GCD) and launchd.

It seems possible to use this API as a kind of IPC, like the POSIX IPC, however, I cannot find how to do it.

I am trying to communicate two processes using the XPC API so I can pass messages between them but I always get a "XPC connection invalid" error in the server side.

I don't want an XPC Service, I just want to exchange messages using a client-server architecture.

I am using two BSD-like processes, so there is no Info.plist or whatever...

I have been following this discussion http://lists.macosforge.org/pipermail/launchd-dev/2011-November/000982.html but this topic seems a bit obscure and undocumented.

Thanks!

Oxfordshire answered 26/12, 2011 at 15:55 Comment(4)
It seems that someone has achieved to do this...#8491861Oxfordshire
However still don't know how to do it myself...Oxfordshire
If you really have a parent-child relationship, then XPC is for you, but if you have two independent processes, XPC is not the way to go. macOS bases on a Mach Microkernel and so it has a very powerful IPC mechanism, that is way faster than anything else: Mach Messages. It works a bit like sending data over sockets but you can also make it transfer data through shared memory for you (that will be copy on write). It's a bit poorly documented and concepts are complex at first, but it's worth learning. All other IPC in macOS is in fact implemented on top of Mach Messages.Tamworth
The main thing that many miss is the difference between "XPC Service" (capital S) and "XPC service". The first - is indeed designed as a temporary sub-process for the sole use of specific Cocoa Application (for separation of privileges, stability and sandboxing). but the second is a normal "Mac service" launched and maintained by MacOS launchd, that exposes an API via XPC protocol, to whom any process can connect and communicate.Vulgarism
G
20

Yes, that is possible, but not the way you'd expect.

You can not have a (non launchd) process vend a service. That is for security reasons, since it would make it easy to do man-in-the-middle attacks.

You can still achieve what you want, though: You have to set up a launchd service that vends an XPC / mach service. Both process A and B then connect to your launchd service. Process A can then create a so called anonymous connection and send that to the launchd service which will forward it to process B. Once that has happened, processes A and B can talk to each other directly through that connection (i.e. the launchd service can exit without the connection breaking).

This may seem round-about, but it's necessary for security reasons.

See the xpc_object(3) man page for details about anonymous connections.

It's a bit counter intuitive, because process A will create a listener object with xpc_connection_create(). A then creates an endpoint object from the listener with xpc_endpoint_create() and sends that endpoint across the wire (over XPC) to process B. B can then turn that object into a connection with xpc_connection_create_from_endpoint(). A's event handler for the listener will then receive a connection object matching the connection that B created with xpc_connection_create_from_endpoint(). This works similar to the way that the event handler of xpc_connection_create_mach_service() will receive connection objects when clients connect.

Glasses answered 7/2, 2012 at 20:54 Comment(6)
What if I don't want process B to connect to the service? Assume, for example, that B is a bash script or an already compiled C program, is it then possible to achieve "classic" IPC through XPC? Or, alternatively, may I bypass XPC in some way (using NSPipe or something like that)?Rag
You don't have to use XPC, but if you're using XPC, you're using XPC. You can write an XPC client that you can execute from the command line, though, if you need that. Hard to say what the best solution is without more details.Glasses
Maybe it's not nice to ask explicitly but here's my question if you mind to take a look: #9743437Rag
Okay I tried your technique with NSXPC APIs, it would certainly work if the two processes were actually connecting to the same instance of the service, but that's not the case. When I try to run my app which runs another process using NSTask, the two processes attempt to talk with the XPC Service, one to send the endPoint the other to retrieve it. The problem is that launchd creates a separate instance for each processes, so I end up with my two processes and two XPC processes running at once, and no communication between the branches. Do you know of a way to solve that problem?Mossgrown
Have you checked with ps(1) that you actually have two processes running? If so, you probably have something misconfigured in the plist file that configures your launchd process. The entire point of launchd is that it automatically launches that daemon and only keeps one instance around.Glasses
Yes I checked with Activity Monitor that there were actually two processes of the same service running. How do I configure it to make it work? What I understand from the documentation is that it's the expected behavior…Mossgrown
C
12

Here is how I am doing Bi-Directional IPC using XPC.

The Helper (login item) is the server or listener. The main app or any other app are considered clients.

I created the following manager:

Header:

@class CommXPCManager;

typedef NS_ENUM(NSUInteger, CommXPCErrorType) {

    CommXPCErrorInvalid     = 1,
    CommXPCErrorInterrupted = 2,
    CommXPCErrorTermination = 3
};

typedef void (^XPCErrorHandler)(CommXPCManager *mgrXPC, CommXPCErrorType errorType, NSError *error);
typedef void (^XPCMessageHandler)(CommXPCManager *mgrXPC, xpc_object_t event, NSDictionary *message);
typedef void (^XPCConnectionHandler)(CommXPCManager *peerConnection);

@interface CommXPCManager : NSObject

@property (readwrite, copy, nonatomic) XPCErrorHandler errorHandler;
@property (readwrite, copy, nonatomic) XPCMessageHandler messageHandler;
@property (readwrite, copy, nonatomic) XPCConnectionHandler connectionHandler;

@property (readonly, nonatomic) BOOL clientConnection;
@property (readonly, nonatomic) BOOL serverConnection;
@property (readonly, nonatomic) BOOL peerConnection;

@property (readonly, nonatomic) __attribute__((NSObject)) xpc_connection_t connection;

@property (readonly, strong, nonatomic) NSString *connectionName;
@property (readonly, strong, nonatomic) NSNumber *connectionEUID;
@property (readonly, strong, nonatomic) NSNumber *connectionEGID;
@property (readonly, strong, nonatomic) NSNumber *connectionProcessID;
@property (readonly, strong, nonatomic) NSString *connectionAuditSessionID;

- (id) initWithConnection:(xpc_connection_t)aConnection;
- (id) initAsClientWithBundleID:(NSString *)bundleID;
- (id) initAsServer;

- (void) suspendConnection;
- (void) resumeConnection;
- (void) cancelConnection;

- (void) sendMessage:(NSDictionary *)dict;
- (void) sendMessage:(NSDictionary *)dict reply:(void (^)(NSDictionary *replyDict, NSError *error))reply;
+ (void) sendReply:(NSDictionary *)dict forEvent:(xpc_object_t)event;

@end

Implementation:

@interface CommXPCManager ()
@property (readwrite, nonatomic) BOOL clientConnection;
@property (readwrite, nonatomic) BOOL serverConnection;
@property (readwrite, nonatomic) BOOL peerConnection;
@property (readwrite, strong, nonatomic) __attribute__((NSObject)) dispatch_queue_t dispatchQueue;
@end

@implementation CommXPCManager

@synthesize clientConnection, serverConnection, peerConnection;
@synthesize errorHandler, messageHandler, connectionHandler;
@synthesize connection    = _connection;
@synthesize dispatchQueue = _dispatchQueue;

#pragma mark - Message Methods:

- (void) sendMessage:(NSDictionary *)dict {

    dispatch_async( self.dispatchQueue, ^{

        xpc_object_t message = dict.xObject;
        xpc_connection_send_message( _connection, message );
        xpc_release( message );
    });
}

- (void) sendMessage:(NSDictionary *)dict reply:(void (^)(NSDictionary *replyDict, NSError *error))reply {

    dispatch_async( self.dispatchQueue, ^{

        xpc_object_t message = dict.xObject;
        xpc_connection_send_message_with_reply( _connection, message, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(xpc_object_t object) {

            xpc_type_t type = xpc_get_type( object );

            if ( type == XPC_TYPE_ERROR ) {

                /*! @discussion Reply: XPC Error */
                reply( [NSDictionary dictionary], [NSError errorFromXObject:object] );

            } else if ( type == XPC_TYPE_DICTIONARY ) {

                /*! @discussion Reply: XPC Dictionary */
                reply( [NSDictionary dictionaryFromXObject:object], nil );
            }
        }); xpc_release( message );
    });
}

+ (void) sendReply:(NSDictionary *)dict forEvent:(xpc_object_t)event {

    xpc_object_t message = [dict xObjectReply:event];
    xpc_connection_t replyConnection = xpc_dictionary_get_remote_connection( message );
    xpc_connection_send_message( replyConnection, message );
    xpc_release( message );
}

#pragma mark - Connection Methods:

- (void) suspendConnection {

    dispatch_async(self.dispatchQueue, ^{ xpc_connection_suspend( _connection ); });
}

- (void) resumeConnection {

    dispatch_async(self.dispatchQueue, ^{ xpc_connection_resume(_connection); });
}

- (void) cancelConnection {

    dispatch_async(self.dispatchQueue, ^{ xpc_connection_cancel(_connection); });
}

#pragma mark - Accessor Overrides:

- (void) setDispatchQueue:(dispatch_queue_t)queue {

    if ( queue ) dispatch_retain( queue );
    if ( _dispatchQueue ) dispatch_release( _dispatchQueue );
    _dispatchQueue = queue;

    xpc_connection_set_target_queue( self.connection, self.dispatchQueue );
}

#pragma mark - Getter Overrides:

- (NSString *) connectionName {

    __block char* name = NULL;
    dispatch_sync(self.dispatchQueue, ^{ name = (char*)xpc_connection_get_name( _connection ); });

    if(!name) return nil;
    return [NSString stringWithCString:name encoding:[NSString defaultCStringEncoding]];
}

- (NSNumber *) connectionEUID {

    __block uid_t uid = 0;
    dispatch_sync(self.dispatchQueue, ^{ uid = xpc_connection_get_euid( _connection ); });
    return [NSNumber numberWithUnsignedInt:uid];
}

- (NSNumber *) connectionEGID {

    __block gid_t egid = 0;
    dispatch_sync(self.dispatchQueue, ^{ egid = xpc_connection_get_egid( _connection ); });
    return [NSNumber numberWithUnsignedInt:egid];
}

- (NSNumber *) connectionProcessID {

    __block pid_t pid = 0;
    dispatch_sync(self.dispatchQueue, ^{ pid = xpc_connection_get_pid( _connection ); });
    return [NSNumber numberWithUnsignedInt:pid];
}

- (NSNumber *) connectionAuditSessionID{ 

    __block au_asid_t auasid = 0;
    dispatch_sync(self.dispatchQueue, ^{ auasid = xpc_connection_get_asid( _connection ); });
    return [NSNumber numberWithUnsignedInt:auasid];
}

#pragma mark - Setup Methods:

- (void) setupConnectionHandler:(xpc_connection_t)conn {

    __block CommXPCManager *this = self;

    xpc_connection_set_event_handler( conn, ^(xpc_object_t object) {

        xpc_type_t type = xpc_get_type( object );

        if ( type == XPC_TYPE_ERROR ) {

            /*! @discussion Client | Peer: XPC Error */

            NSError *xpcError = [NSError errorFromXObject:object];

            if ( object == XPC_ERROR_CONNECTION_INVALID ) {

                if ( this.errorHandler )
                    this.errorHandler( this, CommXPCErrorInvalid, xpcError );

            } else if ( object == XPC_ERROR_CONNECTION_INTERRUPTED ) {

                if ( this.errorHandler )
                    this.errorHandler( this, CommXPCErrorInterrupted, xpcError );

            } else if ( object == XPC_ERROR_TERMINATION_IMMINENT ) {

                if ( this.errorHandler )
                    this.errorHandler( this, CommXPCErrorTermination, xpcError );
            }

            xpcError = nil; return;

        } else if ( type == XPC_TYPE_CONNECTION ) {

            /*! @discussion XPC Server: XPC Connection */

            CommXPCManager *xpcPeer = [[CommXPCManager alloc] initWithConnection:object];

            if ( this.connectionHandler )
                this.connectionHandler( xpcPeer );

            xpcPeer = nil; return;

        } else if ( type == XPC_TYPE_DICTIONARY ) {

            /*! @discussion Client | Peer: XPC Dictionary */

            if ( this.messageHandler )
                this.messageHandler( this, object, [NSDictionary dictionaryFromXObject:object] );
        }

    });
}

- (void) setupDispatchQueue {

    dispatch_queue_t queue = dispatch_queue_create( xpc_connection_get_name(_connection), 0 );
    self.dispatchQueue = queue;
    dispatch_release( queue );
}

- (void) setupConnection:(xpc_connection_t)aConnection {

    _connection = xpc_retain( aConnection );

    [self setupConnectionHandler:aConnection];
    [self setupDispatchQueue];
    [self resumeConnection];
}

#pragma mark - Initialization:

- (id) initWithConnection:(xpc_connection_t)aConnection {

    if ( !aConnection ) return nil;

    if ( (self = [super init]) ) {

        self.peerConnection = YES;
        [self setupConnection:aConnection];

    } return self;
}

- (id) initAsClientWithBundleID:(NSString *)bundleID {

    xpc_connection_t xpcConnection = xpc_connection_create_mach_service( [bundleID UTF8String], nil, 0 );

    if ( (self = [super init]) ) {

        self.clientConnection = YES;
        [self setupConnection:xpcConnection];
    }

    xpc_release( xpcConnection );
    return self;
}

- (id) initAsServer {

    xpc_connection_t xpcConnection = xpc_connection_create_mach_service( [[[NSBundle mainBundle] bundleIdentifier] UTF8String],
                                                                         dispatch_get_main_queue(),
                                                                         XPC_CONNECTION_MACH_SERVICE_LISTENER );
    if ( (self = [super init]) ) {

        self.serverConnection = YES;
        [self setupConnection:xpcConnection];
    }

    xpc_release( xpcConnection );
    return self;
}

@end

Obviously, I am using some Category methods which are self explanatory. For example:

@implementation NSError (CategoryXPCMessage)
+ (NSError *) errorFromXObject:(xpc_object_t)xObject {

    char *description = xpc_copy_description( xObject );
    NSError *xpcError = [NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:@{
                         NSLocalizedDescriptionKey:
                        [NSString stringWithCString:description encoding:[NSString defaultCStringEncoding]] }];
    free( description );
    return xpcError;
}
@end

Okay, using this I set myself up an interface for both the client-side and server-side. The header looks like this:

@class CommXPCManager;

@protocol AppXPCErrorHandler <NSObject>
@required
- (void) handleXPCError:(NSError *)error forType:(CommXPCErrorType)errorType;
@end

static NSString* const kAppXPCKeyReturn = @"AppXPCInterfaceReturn";    // id returnObject
static NSString* const kAppXPCKeyReply  = @"AppXPCInterfaceReply";     // NSNumber: BOOL
static NSString* const kAppXPCKeySEL    = @"AppXPCInterfaceSelector";  // NSString
static NSString* const kAppXPCKeyArgs   = @"AppXPCInterfaceArguments"; // NSArray (Must be xObject compliant)

@interface AppXPCInterface : NSObject

@property (readonly, strong, nonatomic) CommXPCManager *managerXPC;
@property (readonly, strong, nonatomic) NSArray *peerConnections;

- (void) sendMessage:(SEL)aSelector withArgs:(NSArray *)args reply:(void (^)(NSDictionary *replyDict, NSError *error))reply;
- (void) sendMessageToPeers:(SEL)aSelector withArgs:(NSArray *)args reply:(void (^)(NSDictionary *replyDict, NSError *error))reply;

- (id) initWithBundleID:(NSString *)bundleID andDelegate:(id<AppXPCErrorHandler>)object forProtocol:(Protocol *)proto;
- (id) initListenerWithDelegate:(id<AppXPCErrorHandler>)object forProtocol:(Protocol *)proto;

- (void) observeListenerHello:(CommReceptionistNoteBlock)helloBlock;
- (void) removeListenerObserver;

- (void) startClientConnection;
- (void) startListenerConnection;
- (void) stopConnection;

@end

Here is the implementation to start the listener:

- (void) startListenerConnection {

    [self stopConnection];
    self.managerXPC = [[CommXPCManager alloc] initAsServer];

    __block AppXPCInterface *this = self;

    self.managerXPC.connectionHandler = ^(CommXPCManager *peerConnection) {

        [(NSMutableArray *)this.peerConnections addObject:peerConnection];

        peerConnection.messageHandler = ^(CommXPCManager *mgrXPC, xpc_object_t event, NSDictionary *message) {

            [this processMessage:message forEvent:event];
        };

        peerConnection.errorHandler = ^(CommXPCManager *peer, CommXPCErrorType errorType, NSError *error) {

            [this processError:error forErrorType:errorType];
            [(NSMutableArray *)this.peerConnections removeObject:peer];
        };
    };

    [CommReceptionist postGlobalNote:kAppXPCListenerNoteHello];
}

Here is the implementation to start the client:

- (void) startClientConnection {

    [self stopConnection];
    self.managerXPC = [[CommXPCManager alloc] initAsClientWithBundleID:self.identifierXPC];

    __block AppXPCInterface *this = self;

    self.managerXPC.messageHandler = ^(CommXPCManager *mgrXPC, xpc_object_t event, NSDictionary *message) {

        [this processMessage:message forEvent:event];
    };

    self.managerXPC.errorHandler = ^(CommXPCManager *mgrXPC, CommXPCErrorType errorType, NSError *error) {

        [this processError:error forErrorType:errorType];
    };
}

Now here is the order of things.

  1. Your main app starts its helper The helper starts listening using its bundleID <--- Important!
  2. The main app listens for a global notification and then sends a message
  3. When the client sends a message the connection is established

Now the server can send messages to the client and the client can send messages to the server (with or without a reply).

It's very fast, it works well, and is designed for OS X 10.7.3 or greater.

A few notes:

  • The name of the helper must be the same name as the bundle ID
  • The name must begin with your team ID
  • For sandboxing, both the Main app and Helper app application group setting must be start with prefix of the helper Bundle ID

e.g. Helper bundle id is: ABC123XYZ.CompanyName.GroupName.Helper App Group ID will be: ABC123XYZ.CompanyName.GroupName

There are additional details I left out so as not to bore anyone. But if it's still unclear just ask and I will answer.

Ok, hope this helps. Arvin

Catarina answered 30/8, 2012 at 19:7 Comment(2)
Hi Arvin, Im having lots of troubles with the bundles ids and the provisioning profiles, do you use two different app id and provisioning profiles one for main and one for helper app?Lifeanddeath
@Catarina Can you please provide complete implementation, because this reference looks like the most promising source on the web. Any github link to the project would be much helpful. I know with OSX 10.8 we have NSXPCConnection for which i can find good reference from apple, but my project requirement is to support 10.7.5 and later. Thanks.Ambi
B
9

Alright for anyone that has been struggling with this, I was finally able to 100% get communication working between two application processes, using NSXPCConnection

The key to note is that you can only create an NSXPCConnection to three things.

  1. An XPCService. You can connect to an XPCService strictly through a name
  2. A Mach Service. You can also connect to a Mach Service strictly through a name
  3. An NSXPCEndpoint. This is what we're looking for, to communicate between two application processes.

The problem being that we can't directly transfer an NSXPCListenerEndpoint from one application to another.

It involved creating a machservice Launch Agent (See this example for how to do that) that held an NSXPCListenerEndpoint property. One application can connect to the machservice, and set that property to it's own [NSXPCListener anonymousListener].endpoint

Then the other application can connect to the machservice, and ask for that endpoint.

Then using that endpoint, an NSXPCConnection can be created, which successfully established a bridge between the two applications. I have tested sending objects back and forth, and it all works as expected.

Note that if your application is sandboxed, you will have to create an XPCService, as a middle man between your Application and the Machservice

I'm pretty pumped that I got this working-- I'm fairly active in SO, so if anybody is interested in source code, just add a comment and I can go through the effort to post more details

Some hurdles I came across:

You have to launch your machservice, these are the lines:

   OSStatus                    err;
   AuthorizationExternalForm   extForm;

   err = AuthorizationCreate(NULL, NULL, 0, &self->_authRef);
   if (err == errAuthorizationSuccess) {
      NSLog(@"SUCCESS AUTHORIZING DAEMON");
   }
   assert(err == errAuthorizationSuccess);

   Boolean             success;
   CFErrorRef          error;

   success = SMJobBless(
                        kSMDomainSystemLaunchd,
                        CFSTR("DAEMON IDENTIFIER HERE"),
                        self->_authRef,
                        &error
                        );

Also, every time you rebuild your daemon, you have to unload the previous launch agent, with these bash commands:

sudo launchctl unload /Library/LaunchDaemons/com.example.apple-samplecode.EBAS.HelperTool.plist
sudo rm /Library/LaunchDaemons/com.example.apple-samplecode.EBAS.HelperTool.plist
sudo rm /Library/PrivilegedHelperTools/com.example.apple-samplecode.EBAS.HelperTool

(With your corresponding identifiers, of course)

Boabdil answered 6/2, 2017 at 22:39 Comment(13)
Hi A O, did you create a git repository or could you share a sample code with your implementation ? Thanks.Rudolf
Just tested it, it should build and run out of the box for youBoabdil
Thank you A O, I'm having the following error: FAILED TO LAUNCH DAEMON Error Domain=CFErrorDomainLaunchd Code=4 "(null)" I changed the code sign to use my identity and changed info in plist files. I tried the SMJobBlessUtil.py setreq but I get the error: KeyError: 'com.xxx.TestHelper.xpc' Any idea ?Rudolf
it's been a long time :\ but what I do remember, and recommend, is that you go into the SMJobBlessUtil script, and find the spot where it throws that error. then put up a bunch of print statements so you can debug the script, and see what exactly you're missing and why (through looking at the script and seeing what it's doing) Good luck and report back!Boabdil
@AO having the same issue, Investigated like 5 hours, still no luck. Will update asapTeddy
you should update the readme! it is mandatory to run the python script before running the app and daemon. it works now, thanks!Teddy
Great to hear you found success :) I remember spending weeks figuring it out-- no resources were helping, and finally getting it to work was greatBoabdil
Can't imagine. I wonder - how are others doing IPC on macOS with XPC, or what? @AO Because there are only a hand-full of questions on stack about this. BTW, i only get notified if you mention me with @ Stefan SzekeresTeddy
@Rudolf do the above. just a ping.Teddy
IIRC we ended up going with NSConnection, there were too many hoops to go through to get XPC to work @StefanSzekeresBoabdil
I'm going forward investigating with this new example i found: apple.co/2w4QeUM seems to work almost out of the box (info.plist update), and now i'm creating the UML diagrams to better understand the arch. Note, NSConnection is deprecated as of 10.13Teddy
Yeah @Teddy I remember that app, IIRC it just helps you setup XPC connection between an App and a Service, but not between an App and an App. However that was a long time ago now and I'm not confident in my memoryBoabdil
Finally made your XPClab example work, when I have the time I will send a pull request.Teddy

© 2022 - 2024 — McMap. All rights reserved.