/* | |
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