使用Mac OS X XPC作为IPC在进程之间交换消息是否可行?如何实现?

24
根据苹果公司所说,Lion中引入的新XPC服务API提供了一种轻量级机制,用于基本的进程间通信,并与Grand Central Dispatch(GCD)和launchd集成。
似乎可以像POSIX IPC一样使用此API作为IPC,但我无法找到如何做到这一点。
我正在尝试使用XPC API来通信两个进程,以便我可以在它们之间传递消息,但是服务器端总是出现“XPC连接无效”的错误。
我不想要一个XPC服务,我只想使用客户端-服务器架构交换消息。
我正在使用两个类似BSD的进程,因此没有Info.plist或其他文件...
我一直在关注这个讨论http://lists.macosforge.org/pipermail/launchd-dev/2011-November/000982.html 但是这个主题似乎有点晦涩和未记录。

似乎有人已经成功做到了这一点... http://stackoverflow.com/questions/8491361/exc-bad-instruction-when-sending-message-to-xpc-service - poorDeveloper
然而我仍然不知道如何自己做... - poorDeveloper
1
如果你真的有父子关系,那么XPC适合你,但如果你有两个独立的进程,XPC不是最好的选择。macOS基于Mach微内核,因此它具有非常强大的IPC机制,比任何其他机制都要快得多:Mach消息。它的工作方式有点像通过套接字发送数据,但你也可以让它为你传输共享内存中的数据(这将是写时复制)。它的文档有点贫乏,概念一开始可能很复杂,但学习它是值得的。macOS中的所有其他IPC实际上都是在Mach消息之上实现的。 - Mecki
许多人忽略的主要问题是“XPC Service”(大写S)和“XPC service”之间的区别。前者确实被设计为特定Cocoa应用程序的临时子进程(用于权限分离、稳定性和沙盒),但后者是由MacOS launchd启动和维护的普通“Mac服务”,通过XPC协议公开API,任何进程都可以连接和通信。 - Motti Shneor
3个回答

20

是的,这是有可能的,但不是您期望的方式。

您无法让一个(非launchd)进程出售服务。这是出于安全原因,因为这样很容易进行中间人攻击。

不过,您仍然可以实现您想要的:您需要设置一个出售XPC / mach服务的launchd服务。进程A和B都连接到您的launchd服务。进程A可以创建所谓的匿名连接并将其发送到launchd服务,后者将其转发给进程B。一旦发生这种情况,进程A和B就可以通过该连接直接交流(即使launchd服务退出,连接也不会断开)。

这可能看起来有点绕,但出于安全原因是必要的。

有关匿名连接的详细信息,请参见xpc_object(3)手册页面。

这有点反直觉,因为进程A将使用xpc_connection_create()创建一个侦听器对象。然后,A使用xpc_endpoint_create()从侦听器创建一个端点对象,并将该端点通过线路(通过XPC)发送到进程B。然后,B可以使用xpc_connection_create_from_endpoint()将该对象转换为连接。当B使用xpc_connection_create_from_endpoint()创建连接时,A的事件处理程序将接收与B创建的连接相匹配的连接对象。这类似于当客户端连接时,xpc_connection_create_mach_service()的事件处理程序将接收连接对象。


如果我不想让进程B连接到服务怎么办?例如,假设B是一个bash脚本或已编译的C程序,那么是否可以通过XPC实现“经典”的IPC?或者,我是否可以通过某种方式绕过XPC(使用NSPipe或类似的东西)? - Manlio
你不必使用XPC,但如果你正在使用XPC,那么你就是在使用XPC。如果需要的话,你可以编写一个XPC客户端,可以从命令行执行。没有更多细节很难说出最佳解决方案。 - Daniel Eggert
也许直接问不太好,但这是我的问题,如果您愿意看一下:http://stackoverflow.com/questions/9742937/interprocess-communication-on-macosx-lion - Manlio
好的,我尝试了你提供的使用NSXPC API的技术,如果两个进程实际上连接到同一个服务实例,那么它肯定会起作用,但事实并非如此。当我尝试运行我的应用程序时,它会使用NSTask运行另一个进程,这两个进程尝试与XPC服务通信,一个发送endPoint,另一个检索它。问题在于launchd为每个进程创建了单独的实例,因此我最终得到了两个进程和两个XPC进程同时运行,并且分支之间没有通信。你知道解决这个问题的方法吗? - Psycho
你是否已经通过ps(1)检查了实际上运行了两个进程?如果是这样,那么您可能在配置launchd进程的plist文件中出现了一些配置错误。launchd的整个目的在于自动启动守护程序并仅保留一个实例。 - Daniel Eggert
是的,我用活动监视器检查了一下,发现实际上有两个相同服务的进程在运行。我该如何配置它才能使其正常工作?根据文档,我理解这是预期的行为... - Psycho

12

我是如何使用XPC进行双向IPC的。

Helper(登录项)是服务器或监听器。主要应用程序或任何其他应用程序都被视为客户端。

我创建了以下管理器:

头文件:


@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

实现:

@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

显然,我正在使用一些类别方法,这些方法本身就很易于理解。例如:

@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

好的,使用这个工具,我为客户端和服务器端都设置了一个界面。头文件如下:

@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

以下是启动监听器的实现:

- (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];
}

这里是启动客户端的实现:

- (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];
    };
}
现在事情的顺序如下:
1. 主应用程序启动其帮助程序,帮助程序使用其bundleID进行侦听<---非常重要! 2. 主应用程序侦听全局通知,然后发送消息。 3. 当客户端发送消息时,建立连接。
现在,服务器可以向客户端发送消息,客户端也可以向服务器发送消息(带有或不带有回复)。
它非常快速,工作得很好,并且设计用于OS X 10.7.3或更高版本。
一些注释: - 帮助程序的名称必须与bundle ID相同。 - 名称必须以您的团队ID开头。 - 对于沙盒,主应用程序和帮助程序应用程序组设置都必须以helper Bundle ID的前缀开头。
例如: Helper bundle id是: ABC123XYZ.CompanyName.GroupName.Helper 应用程序组ID将为: ABC123XYZ.CompanyName.GroupName 还有一些我省略的额外细节,以免使任何人感到无聊。但如果仍然不清楚,请询问,我会回答。
好了,希望这可以帮助你。 Arvin

嗨,Arvin,我在处理bundles ids和provisioning profiles时遇到了很多麻烦,你是否使用两个不同的app id和provisioning profiles,一个用于主应用程序,另一个用于辅助应用程序? - IturPablo
2
@Arvin,能否提供完整的实现呢?因为这个参考资料看起来是网络上最有前途的来源。如果有项目的Github链接将非常有帮助。我知道在OSX 10.8中我们有NSXPCConnection,可以从苹果公司找到很好的参考资料,但我的项目要求支持10.7.5及更高版本。谢谢。 - john fedric

9

如果你一直在努力解决这个问题,我终于成功地使用NSXPCConnection在两个应用程序进程之间实现了100%的通信。

需要注意的关键点是,你只能创建三种类型的NSXPCConnection

  1. XPC服务。你只能通过名称连接到XPC服务。
  2. Mach服务。你也可以通过名称连接到Mach服务。
  3. NSXPCEndpoint。这就是我们要寻找的,用于在两个应用程序进程之间进行通信的对象。

问题在于,我们不能直接将一个NSXPCListenerEndpoint从一个应用程序传输到另一个应用程序。

需要创建一个Mach服务启动代理(参见此示例),其中包含一个NSXPCListenerEndpoint属性。一个应用程序可以连接到Mach服务,并将该属性设置为自己的[NSXPCListener anonymousListener].endpoint

然后,另一个应用程序可以连接到Mach服务,并请求该端点。

然后,使用该端点可以创建一个NSXPCConnection,成功地建立了两个应用程序之间的桥梁。我已经测试了来回发送对象,一切都按预期工作。

请注意,如果你的应用程序是沙箱化的,你必须创建一个XPCService,作为你的应用程序和Mach服务之间的中间人。

我很高兴我成功了--我在SO上非常活跃,所以如果有人对源代码感兴趣,只需添加一条评论,我就可以费心发布更多详细信息。

我遇到的一些障碍:

你必须启动你的Mach服务,下面是这些行:

   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
                        );

此外,每次重建守护进程时,都需要使用以下bash命令卸载先前的启动代理:
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

(当然要使用相应的标识符)

2
刚刚测试过了,它应该可以直接构建和运行。 - A O
2
已经很久没做这个了,但我记得并且建议你进入SMJobBlessUtil脚本,并找到它抛出错误的位置。然后放置一堆“print”语句以便你可以调试脚本,并查看你缺少什么以及为什么(通过查看脚本并查看它正在执行的操作)。祝你好运并回报! - A O
2
很高兴听到你取得了成功 :) 我记得花了几周时间去弄明白它--没有任何资源能够帮助,最终让它工作起来真是太好了。 - A O
2
如果我没记错的话,我们最终选择了 NSConnection,因为使用 XPC 需要跨越太多障碍。@StefanSzekeres - A O
2
是的 @StefanS,我记得那个应用程序,如果我没记错的话,它只是帮助你在应用程序和服务之间建立XPC连接,而不是在应用程序和应用程序之间建立连接。然而,那已经是很久以前的事了,我对我的记忆不太有信心。 - A O
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接