April 18 2010
Communicating with a Privileged Tool
In writing Multicast Ping for the Mac, I needed a two-way communication pipe between the interface (parent process) and the privileged tool (child process) and a clean method for terminating the tool through that pipe. NSTask provides setStandardInput and setStandardOutput for just this reason. However, I cannot launch a privileged tool with NSTask; instead, I have to use AuthorizationExecuteWithPrivileges with its lower-level file handle mechanism.
With a bit of help, I implemented a two-way pipe at a high-level and utilize that pipe to cleanly terminate the child process with a simple EOF signal. Apple’s BetterAuthorizationSample sample project touches on this communication pipe but does so at a low-level and only one-way. Matt Gallagher from Cocoa With Love also uses a pipe in invoking other processes when he creates an Open File Killer app, but the pipe is still only used one direction. Thanks to both those tutorials as well as a post from Caius Theory for helping me along. See my GitHub multicast_ping repo for the code in context.
Interface: setup the pipe to the tool
// Execute the command with privileges from SFAuthorizationView.
FILE *handle;
OSErr processError = AuthorizationExecuteWithPrivileges([[authView authorization] authorizationRef], [helperPath UTF8String],
kAuthorizationFlagDefaults, (char *const *)argv, &handle);
free(argv);
if (processError != 0) {
NSLog(@"helper tool failed (%d)", processError);
return NO;
}
// Setup the two-way pipe.
helperHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileno(handle)];
[helperHandle waitForDataInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTaskOutput:) name:NSFileHandleDataAvailableNotification object:helperHandle];
Interface: wait for data from the tool
- (void)handleTaskOutput:(NSNotification *)notification {
// Get the new data.
NSFileHandle *handle = (NSFileHandle *)[notification object];
NSData *data = [handle availableData];
if ([data length] > 0) {
// Convert the data into a string.
NSString *dataString = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding];
if ([dataString isEqual:@"port in use"]) {
// Tool failed.
[self handleToolFailure];
[dataString release];
return;
}
// Process the string, expecting "address,port,message".
NSArray *array = [dataString componentsSeparatedByString:@","];
[dataString release];
if ([array count] == 3) {
// Get attributes.
NSString *address = [array objectAtIndex:0];
int port = [[array objectAtIndex:1] intValue];
NSString *message = [array objectAtIndex:2];
// Create new message.
Message *newMessage = [[Message alloc] initWithAddress:address andPort:port andMessage:message];
[messages addObject:newMessage];
[newMessage release];
// Update view.
[listView reloadData];
}
// Prepare for more data.
[handle waitForDataInBackgroundAndNotify];
}
else {
// No data means tool failed.
[self handleToolFailure];
}
}
Interface: terminate the tool
// Close pipe to terminate the helper tool.
[helperHandle closeFile];
helperHandle = nil;
Tool: send data to the interface
// Write out the new message to the pipe.
NSString *line = [NSString stringWithFormat:@"%@,%d,%@", sentHost, sentPort, receivedMessage];
[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:[line dataUsingEncoding:NSUTF8StringEncoding]];
Tool: listen for terminate from the interface
// Wait for parent process to close the pipe. That will send the EOF signal.
[[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile];