Mike Crawford
Mike Crawford

Reputation: 2278

How can I return a value from a block?

I am quite humbled to admit that I have no clue about blocks.

I am writing a GUI uninstaller app that uses a privileged helper tool delete all of my product's files and directories.

Because the helper is a single file, I embed its Info.plist into the executable by creating a TEXT segment then copying Info.plist into it.

So far I can successfully use SMJobBless() to install that helper tool in /Library/PrivilegedHelperTools and its launchd property list in /Library/LaunchDaemons.

Apple recommends using XPC to ensure that the GUI app and the tool have binary-compatible versions. The EvenBetterAuthorizationSample uses a block to call into the helper tool. I need my getVersion function to pass back an NSMutableString to verifyVersionCompatibility.

- (uint32_t)getVersion: (NSMutableString**) helperVersionPtr
    // Called when the user clicks the Get Version button.
    // This is the simplest form of
    // NSXPCConnection request because it doesn't require any authorization.
{
    assert( helperVersionPtr != NULL );
    assert( *helperVersionPtr != NULL );

    [self connectAndExecuteCommandBlock:^(NSError * connectError) {
        if (connectError != nil) {
            [self logError:connectError];
            *helperVersionPtr = NULL;

            DBG_MSG(( "%s connectAndExecuteCommandBlock failed connectError: %s\n", __func__, [[connectError description] UTF8String] ));

        } else {
            [[self.helperToolConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) {
                [self logError:proxyError];
            }] getVersionWithReply:^(NSString *version) {

                [self logWithFormat:@"version = %@\n", version];

                [*helperVersionPtr setString: version];  // Pass the version back
            }];
        }
    }];

    return 0;
}

The block gets executed here:

- (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock
    // Connects to the helper tool and then executes 
    // the supplied command block on the 
    // main thread, passing it an error indicating 
    // if the connection was successful.
{
    assert([NSThread isMainThread]);

    // Ensure that there's a helper tool connection in place.

    [self connectToHelperTool];

    // Run the command block.  
    // Note that we never error in this case because, if there is 
    // an error connecting to the helper tool, it will be delivered 
    // to the error handler 
    // passed to -remoteObjectProxyWithErrorHandler:.
    // However, I maintain the possibility 
    // of an error here to allow for future expansion.

    commandBlock(nil);
}

The helper tool's getVersionWithReply:

- (void)getVersionWithReply:(void(^)(NSString * version))reply
    // Part of the HelperToolProtocol.
    // Returns the version number of the tool.  Note that never
    // requires authorization.
{
    // We specifically don't check for authorization here.
    // Everyone is always allowed to get
    // the version of the helper tool.

    reply([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]);

}

Upvotes: 1

Views: 1237

Answers (1)

James Bucanek
James Bucanek

Reputation: 3439

I think what you're asking is how can -getVersion: synchronously obtain the string value obtained via the XPC reply function and return it to the caller.

The problem is that all XPC messages/handlers execute asynchronously and on random threads.

If you really must have a synchronous call, you can use semaphores to block until the reply is received:

- (NSString*)synchronousExample
{
    NSConditionLock* barrierLock = [[NSConditionLock alloc] initWithCondition:NO];
    id<YourXPCServiceMessaging> proxy = self.serviceProxy;  // get the proxy object of the XPC connection

    // Send the XPC message that requests a response
    __block NSString* replyString = nil;
    [proxy someMessageWithStringReply:^(NSString* string){
        // The XPC service has responded with the value; squirrel it away and let the original thread know it's done
        replyString = string;
        [barrierLock lock];
        [barrierLock unlockWithCondition:YES];
        }];
    // Wait for the reply block to execute
    [barrierLock lockWhenCondition:YES];
    [barrierLock unlock];

    return replyString;
}

A simpler approach, if you can reorganize your code, is to make an asynchronous request and then continue once the reply is received:

- (void)startVerifyCheck
{
    id<YourXPCServiceMessaging> proxy = self.serviceProxy;  // get the proxy object of the XPC connection
    [proxy someMessageWithStringReply:^(NSString* string){
        // The XPC service has responded with the value
        if ([string isEqualToString:@"the expected value"])
            {
            // Verify successful: continue with the next step
            dispatch_async(dispatch_get_main_queue(), ^{
                [self verifyComplete];
                });
            }
        }];
    // Reply block will continue once the reply is received
}

- (void)verifyComplete
{
    // do the next step here...
}

Upvotes: 2

Related Questions