Skip to content

Instantly share code, notes, and snippets.

@atr000
Created October 12, 2010 02:59
Show Gist options
  • Save atr000/621601 to your computer and use it in GitHub Desktop.
Save atr000/621601 to your computer and use it in GitHub Desktop.
/*
asynctask.m -- sample code that shows how to implement asynchronous stdin, stdout & stderr streams for processing data with NSTask
compile with:
gcc -Wall -O3 -x objective-c -fobjc-exceptions -framework Foundation -o asynctask asynctask.m
./asynctask
./asynctask > asynctask-output.txt 2>&1
for ((i=0; i < 20; i++)); do ./asynctask; done
open -e asynctask-output.txt
asynctask.m is based on and extends FRCommand.h and FRCommand.m by Torsten Curdt (Apache License, Version 2.0).
Being a GUI-less application (i.e. a Foundation-based command line tool), asynctask.m runs an NSRunLoop manually
to enable the use of asynchronous "waitForDataInBackgroundAndNotify" notifications. In addition, asynctask.m
uses pthread_create(3) and pthread_detach(3) for writing more than 64 KB to the stdin of an NSTask.
Source code:
- http://github.com/tcurdt/feedbackreporter (by Torsten Curdt; Apache License, Version 2.0)
- http://github.com/tcurdt/feedbackreporter/tree/master/Sources/Main/
- http://github.com/tcurdt/feedbackreporter/blob/443300c21f6b7c5b70298edd861b4b2017a97b00/Sources/Main/FRCommand.h
- http://github.com/tcurdt/feedbackreporter/blob/443300c21f6b7c5b70298edd861b4b2017a97b00/Sources/Main/FRCommand.m
# create testfile.txt
jot -b "line" 500 > testfile.txt
jot -b "line" 20000 > testfile.txt
jot -b "line" 1000000 > testfile.txt
du -h testfile.txt
*/
/*
* Copyright 2008, Torsten Curdt
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import <unistd.h>
#import <pthread.h>
#import <stdio.h>
// ADDED begin
static int empty_data_count;
static int task_exit_code;
struct arg_struct {
NSData *inData;
NSFileHandle *inHandle;
};
void *threadFunction(void *arguments)
{
[[NSAutoreleasePool alloc] init];
struct arg_struct *taskArgs = arguments;
[taskArgs -> inHandle writeData: taskArgs -> inData];
[taskArgs -> inHandle closeFile];
//[taskArgs -> inHandle release];
//[taskArgs -> inData release];
return nil;
}
// ADDED end
@interface FRCommand : NSObject {
NSTask *task;
NSString *path;
NSArray *args;
NSData *stdinData; // ADDED
NSMutableString *output;
NSMutableString *error;
BOOL terminated;
NSFileHandle *outFile;
NSFileHandle *errFile;
NSFileHandle *stdinHandle; // ADDED
}
-(NSData *) availableDataOrError: (NSFileHandle *)file;
- (id) initWithPath:(NSString*)path;
- (void) setArgs:(NSArray*)args;
- (void) setInput:(NSData*)stdinData; // ADDED
- (void) setError:(NSMutableString*)error;
- (void) setOutput:(NSMutableString*)output;
- (int) execute;
@end
//#import "FRCommand.h"
@implementation FRCommand
- (id) initWithPath:(NSString*)pPath
{
self = [super init];
if (self != nil)
{
task = [[NSTask alloc] init];
args = [NSArray array];
path = pPath;
stdinData = nil; // ADDED
error = nil;
output = nil;
terminated = NO;
}
return self;
}
// ADDED
// For "availableDataOrError:" see:
// - "NSTasks, NSPipes, and deadlocks when reading...",
// http://dev.notoptimal.net/2007/04/nstasks-nspipes-and-deadlocks-when.html
// - "NSTask stealth bug in readDataOfLength!! :(",
// http://www.cocoabuilder.com/archive/cocoa/173348-nstask-stealth-bug-in-readdataoflength.html#173647
-(NSData *) availableDataOrError: (NSFileHandle *)file {
for (;;)
{
@try {
return [file availableData];
} @catch (NSException *e) {
if ([[e name] isEqualToString:NSFileHandleOperationException])
{
if ([[e reason] isEqualToString: @"*** -[NSConcreteFileHandle availableData]: Interrupted system call"])
{
continue;
}
return nil;
}
@throw;
}
} // for
}
- (void) setArgs:(NSArray*)pArgs
{
args = pArgs;
}
- (void) setInput:(NSData*)pInput // ADDED
{
stdinData = pInput;
}
- (void) setError:(NSMutableString*)pError
{
error = pError;
}
- (void) setOutput:(NSMutableString*)pOutput
{
output = pOutput;
}
-(void) appendDataFrom:(NSFileHandle*)fileHandle to:(NSMutableString*)string
{
NSData *data = [self availableDataOrError: fileHandle];
//NSData *data = [fileHandle availableData];
if ([data length]) {
NSString *s = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding: NSUTF8StringEncoding];
//NSString *s = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding: NSASCIIStringEncoding];
[output appendString:s];
//NSLog(@"| %@", s);
[s release];
//[fileHandle waitForDataInBackgroundAndNotify];
}else{
empty_data_count += 1;
if (empty_data_count > 10)
{
//[task interrupt]; // failed to abort infinite NSRunLoop
//[task terminate]; // same
[[NSNotificationCenter defaultCenter] removeObserver:self]; // only way to abort infinite NSRunLoop ???
}
}
[fileHandle waitForDataInBackgroundAndNotify];
}
-(void) outData: (NSNotification *) notification
{
NSFileHandle *fileHandle = (NSFileHandle*) [notification object];
[self appendDataFrom:fileHandle to:output];
[fileHandle waitForDataInBackgroundAndNotify];
}
-(void) errData: (NSNotification *) notification
{
NSFileHandle *fileHandle = (NSFileHandle*) [notification object];
[self appendDataFrom:fileHandle to:output];
[fileHandle waitForDataInBackgroundAndNotify];
}
- (void) terminated: (NSNotification *)notification
{
NSLog(@"Task terminated");
[[NSNotificationCenter defaultCenter] removeObserver:self];
terminated = YES;
}
- (int) execute
{
empty_data_count = 0;
[task setLaunchPath:path];
[task setArguments:args];
NSPipe *outPipe = [NSPipe pipe];
NSPipe *errPipe = [NSPipe pipe];
[task setStandardOutput:outPipe];
[task setStandardError:errPipe];
[task setCurrentDirectoryPath:@"."]; // ADDED
//NSFileHandle *outFile = [outPipe fileHandleForReading]; // TROUBLEMAKER
//NSFileHandle *errFile = [errPipe fileHandleForReading];
outFile = [outPipe fileHandleForReading];
errFile = [errPipe fileHandleForReading];
// ADDED
// create inPipe after outPipe & errPipe
NSPipe *inPipe = [NSPipe pipe];
stdinHandle = [inPipe fileHandleForWriting];
[task setStandardInput:inPipe];
struct arg_struct taskArguments;
taskArguments.inData = stdinData;
taskArguments.inHandle = stdinHandle;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(terminated:)
name:NSTaskDidTerminateNotification
object:task];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(outData:)
name:NSFileHandleDataAvailableNotification
object:outFile];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(errData:)
name:NSFileHandleDataAvailableNotification
object:errFile];
[outFile waitForDataInBackgroundAndNotify];
[errFile waitForDataInBackgroundAndNotify];
[task launch];
// ADDED
pthread_t thread = nil;
if (pthread_create(&thread, nil, (void *(*)(void *))threadFunction, (void *)&taskArguments) != 0)
{
perror("pthread_create failed");
return 1;
}
if (pthread_detach(thread) != 0)
{
perror("pthread_detach failed");
return 1;
}
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
while(!terminated)
{
//if (![[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:100000]])
if (![[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])
{
break;
}
[pool release];
pool = [[NSAutoreleasePool alloc] init];
}
[pool release];
[self appendDataFrom:outFile to:output];
[self appendDataFrom:errFile to:error];
int result = [task terminationStatus];
task_exit_code = result;
return result;
}
-(void)dealloc
{
[task release];
[super dealloc];
}
@end
int main(int argc, const char *argv[])
{
NSAutoreleasePool *mainpool = [[NSAutoreleasePool alloc] init];
// Test 1: write data to stdin of NSTask for subsequent cat(1) or tail(1)
NSString *testfile = @"testfile.txt";
NSData *data = [NSData dataWithContentsOfFile: testfile];
FRCommand *newTask = [[[FRCommand alloc] initWithPath: @"/bin/cat"] autorelease];
//[newTask setArgs: [NSArray arrayWithObjects: @"-n", nil ]];
[newTask setArgs: [NSArray arrayWithObjects: @"-u", @"-n", nil ]];
//FRCommand *newTask = [[[FRCommand alloc] initWithPath: @"/usr/bin/tail"] autorelease];
//[newTask setArgs: [NSArray arrayWithObjects: @"-n", @"10", nil ]];
[newTask setInput: data];
// Test 2: do not write to stdin
/*
FRCommand *newTask = [[[FRCommand alloc] initWithPath: @"/bin/ls"] autorelease];
[newTask setArgs: [NSArray arrayWithObjects: @"-l", nil ]];
*/
//----------------------
NSMutableString *stdoutString = [NSMutableString string];
NSMutableString *stderrString = [NSMutableString string];
[newTask setOutput: stdoutString];
[newTask setError: stderrString];
[newTask execute];
NSLog(@"stdoutString:\n%@", stdoutString);
NSLog(@"stderrString:\n%@", stderrString);
[mainpool release];
printf("\nempty_data_count: %i\n", empty_data_count);
printf("\ntask_exit_code: %i\n\n", task_exit_code);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment