Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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];
             }];
        }
    }
like image 967
Olter Avatar asked Mar 13 '17 13:03

Olter


People also ask

What is XPC service on Mac?

macOS uses XPC services for basic inter-process communication between various processes, such as between the XPC Service daemon and third-party application privileged helper tools.

How do you debug a Mac?

Just open the source file you want to debug in Xcode, and click in the margin to the left of the line of code where you want to break. During the debugging session, each time that line is executed, the debugger will break there, and you will be able to debug it.

What is XPC listener?

XPC Services class NSXPCListener. A listener that waits for new incoming connections, configures them, and accepts or rejects them. protocol NSXPCListenerDelegate. The protocol that delegates to the XPC listener use to accept or reject new connections.

What is XPC service IOS?

The XPC Services API provides a lightweight mechanism for basic interprocess communication at the libSystem level. It allows you to create lightweight helper tools, called XPC services, that perform work on behalf of your app.


1 Answers

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 .

like image 70
Alex M Avatar answered Sep 27 '22 17:09

Alex M