How to debug communication between XPC service and client app in OSX
Asked Answered
L

1

8

I'm trying to write a simple pair of "client app" & "XPC service". I was able to launch xpc service from client (i.e I can see service running in the Activity monitor processes list), but when I try to send any request, that has a response block, I get an error: "Couldn’t communicate with a helper application."

The worst thing here is that error doesn't give me any info about what went wrong. And I'm also unable to debug the service properly. As I understand, the correct way to do this is to attach a debugger to process (Debug->Attach to process, also see here). I have both client and service projects in a single workspace.

When I run client from xcode and try to attach debugger to launched service, that ends with a "Could not attach to pid : X" error.

If I archive the client app run it from app file and then try to attach debugger to service the result is the same.

The only way to record something from the service I could imagine is to write a logger class, that would write data to some file. Haven't tried this approach yet, however that looks insane to me.

So my question is:

a) How to find out what went wrong, when receiving such non-informative response like: "Couldn’t communicate with a helper application"?

b) And also, what's the correct way to debug the xpc service in the first place? The link above is 5 years old from now, however I can see that some people were saying that "attach to debugger" wasn't working.

The code itself is fairly simple:

XPC service, listener implementation:

#import "ProcessorListener.h"

@implementation ProcessorListener

- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
{
    [newConnection setExportedInterface: [NSXPCInterface interfaceWithProtocol:@protocol(TestServiceProtocol)]];
    [newConnection setExportedObject: self];
    self.xpcConnection = newConnection;

    newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol: @protocol(Progress)];

    // connections start suspended by default, so resume and start receiving them
    [newConnection resume];

    return YES;
}

- (void) sendMessageWithResponse:(NSString *)receivedString reply:(void (^)(NSString *))reply
{
    reply = @"This is a response";
}

- (void) sendMessageWithNoResponse:(NSString *)mString
{
    // no response here, dummy method
    NSLog(@"%@", mString);
}

And the main file for service:

#import <Foundation/Foundation.h>
#import "TestService.h"

@interface ServiceDelegate : NSObject <NSXPCListenerDelegate>
@end

@implementation ServiceDelegate

- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
    // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection.

    // Configure the connection.
    // First, set the interface that the exported object implements.
    newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(TestServiceProtocol)];

    // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object.
    TestService *exportedObject = [TestService new];
    newConnection.exportedObject = exportedObject;

    // Resuming the connection allows the system to deliver more incoming messages.
    [newConnection resume];

    // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO.
    return YES;
}

@end

int main(int argc, const char *argv[])
{
//    [NSThread sleepForTimeInterval:10.0];
    // Create the delegate for the service.
    ServiceDelegate *delegate = [ServiceDelegate new];

    // Set up the one NSXPCListener for this service. It will handle all incoming connections.
    NSXPCListener *listener = [NSXPCListener serviceListener];
    listener.delegate = delegate;

    // Resuming the serviceListener starts this service. This method does not return.
    [listener resume];
    return 0;
}

For client app, the UI contains a bunch of buttons:

- (IBAction)buttonSendMessageTap:(id)sender {
    if ([daemonController running])
    {
        [self executeRemoteProcessWithName:@"NoResponse"];
    }
    else
    {
        [[self.labelMessageResult cell] setTitle: @"Error"];
    }
}

- (IBAction)buttonSendMessage2:(id)sender {
    if ([daemonController running])
    {
        [self executeRemoteProcessWithName:@"WithResponse"];
    }
    else
    {
        [[self.labelMessageResult cell] setTitle: @"Error"];
    }
}

- (void) executeRemoteProcessWithName: (NSString*) processName
    {
        // Create connection
        NSXPCInterface * myCookieInterface = [NSXPCInterface interfaceWithProtocol: @protocol(Processor)];

        NSXPCConnection * connection = [[NSXPCConnection alloc] initWithServiceName: @"bunldeID"]; // there's a correct bundle id there, really

        [connection setRemoteObjectInterface: myCookieInterface];

        connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(Progress)];
        connection.exportedObject = self;

        [connection resume];

        // NOTE that this error handling code is not called, when debugging client, i.e connection seems to be established
        id<Processor> theProcessor = [connection remoteObjectProxyWithErrorHandler:^(NSError *err)
                                      {
                                          NSAlert *alert = [[NSAlert alloc] init];
                                          [alert addButtonWithTitle: @"OK"];
                                          [alert setMessageText: err.localizedDescription];
                                          [alert setAlertStyle: NSAlertStyleWarning];

                                          [alert performSelectorOnMainThread: @selector(runModal) withObject: nil waitUntilDone: YES];
                                      }];

        if ([processName containsString:@"NoResponse"])
        {
            [theProcessor sendMessageWithNoResponse:@"message"];
        }
        else if ([processName containsString:@"WithResponse"])
        {
            [theProcessor sendMessageWithResponse:@"message" reply:^(NSString* replyString)
             {
                 [[self.labelMessageResult cell] setTitle: replyString];
             }];
        }
    }
Levee answered 13/3, 2017 at 13:16 Comment(0)
C
1

Jonathan Levin's XPoCe tool is helpful when you can't attach a debugger.

You can add logging NSLog() or fprintf(stderr,...) to your service and clients, specifically around the status codes. You just have to specify the path of the file to write stdout and stderr. <key>StandardErrorPath</key> <string>/tmp/mystderr.log</string>

There's a section on Debugging Daemons at this article on objc.io .

Chainsmoke answered 9/9, 2020 at 17:58 Comment(1)
Alex, thanks for the answer. Regrettably I cannot check whether it works or not, since the question is several years old and I no longer work with MacOS.Levee

© 2022 - 2024 — McMap. All rights reserved.