Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
Good morning to all,
May I ask your input in understanding a couple of issues ( I am sure there are probably more) relating to NSTask.

Some background. I am working on writing/understanding a Hash class, so in the process wrote a Foundation command line app that tries to illustrate to me how it works.

So, there are three stages in the code. ( I have included the entire code in case someone actually wishes to run it).

First stage...simply runs an MD5 command line app using NSTask. The arguments are supplied with NSTask's setArguments, and the stdOutput is used to display the result.


Next...I use a pipe to read the result from the StdOutput

Finally, I try and pipe "in" the arguments and read the output.


There are 2 issues that I would like to understand.


1) Even though I have added the "Terminal" class as an observer, it does not trigger the notification as I would have expected. What am I missing?
2) The output from the 3rd version is different from the first 2. ( In the first 2 iterations, I supply the switch "-s" and the string to be hashed as arguments in setArguments. In the Final iteration, I supply the switch and argument as a single string. I suspect I might not fully understand the intricacies of setArgument vs "piping" in the switch and argument as one. I had tried supplying the argument "-s" with setArgument and the string solely in the pipe, but this would not work. Only an "inpipe" with both got any sort of result.

Thanks for our input.

Here is the full code. It's a foundation tool app. I apologize for all the code but feel that it's all pretty relevant. Hope no-one is offended, and will gladly take advice on this for future reference. Just , that I have hit a wall for most of the week and feel it's too big a knowledge gap to just move on to the next issue before at least understanding this.

Thanks as always in advance.
Please be kind :confused:


Program name: HashClassImplementation

HashClassImplementation.m
Code:
#import <Foundation/Foundation.h>
//#import <Cocoa/Cocoa.h>
//#import "HashValue.h"
#import "Terminal.h"

#define MD5PATH @"/sbin/md5"
#define SHA256PATH @"/usr/bin/shasum"
#define HMAC256PATH @"/Developer/SDKs/MacOSX10.6.sdk/usr/include/CommonCrypto/CommonHMAC/CCHmac"


 


int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
	
	NSFileHandle *fileHandleForReading = nil;
	NSFileHandle *fileHandleForWriting = nil;
	
	
	/*Enter test string here*/
	
	NSString *testString = @"Some data value.";
	NSLog(@"Test against a standard using terminal");
	NSLog(@"Test of MD5 hash algorithm");
	NSLog(@"Test string is: \"%@\"", testString);
	
	
	
	NSLog(@"\n\nFirst test will simply write the hash to the terminal");
	
	Terminal *terminal = [[ Terminal alloc]initWithPath:MD5PATH];
	[terminal setArgsOfTask: @"-s" arg2:testString arg3:nil];
	[terminal launch];
	NSLog(@"Task's Current directory: %@", [terminal currenWorkingirectory] );
	[terminal terminate];
	[terminal terminateObserverStatus];
	
	NSLog(@"-->\n\nNext test will pipe result to be read by a string and displayed");
	
	terminal = [[ Terminal alloc]initWithPath:MD5PATH];
	[terminal setArgsOfTask: @"-s" arg2:testString arg3:nil];
	[terminal setPipeOut];
	fileHandleForReading = [terminal outFileHandle];
	[terminal launch];
	NSData *outData = [fileHandleForReading readDataToEndOfFile];
	NSLog(@"Data collected through pipe: %@", outData);
	NSString * readData = [[NSString alloc]initWithData:outData encoding:NSUTF8StringEncoding];
	NSLog(@"Result as string: %@", readData);
	[readData release];
	[terminal terminate];
	[fileHandleForReading release];
	[terminal terminateObserverStatus];
	
	
	NSLog(@"-->\n\nNext test will pipe in the string and the  result to be read by a string and displayed");
	
	terminal = [[ Terminal alloc]initWithPath:MD5PATH];
	//[terminal setArgsOfTask: @"-s" arg2:nil arg3:nil];
	[terminal setPipeOut];
	[terminal setPipeIn];
	fileHandleForReading = [terminal outFileHandle];
	fileHandleForWriting = [terminal inFileHandle];
	
	NSString * fullCommandLine = [@"-s" stringByAppendingString:testString];
	NSData * stringAsData = [fullCommandLine dataUsingEncoding:NSUTF8StringEncoding];
	[fileHandleForWriting writeData:stringAsData];
	[fileHandleForWriting closeFile];
	
	[terminal launch];
	
	NSData *outData2 = [fileHandleForReading readDataToEndOfFile];
	NSString * readData2 = [[NSString alloc]initWithData:outData2 encoding:NSUTF8StringEncoding];
	NSLog(@"Result as string:\n%@", readData2);
	[terminal terminate];
	[fileHandleForReading release];
	[terminal terminateObserverStatus];
	
					
    [pool drain];
    return 0;
}



Terminal.h

Code:
#import <Foundation/Foundation.h>


@interface Terminal : NSObject {
	
	NSString * string;
	NSString * digest;
	NSTask *_task;
	NSFileHandle *_inHandle;
	NSFileHandle *_outHandle;
	

}

@property(readwrite, retain) NSString *string;
@property(readwrite, retain) NSString *digest;
@property(readwrite, retain) NSTask *task;
@property(readwrite, retain) NSFileHandle * inFileHandle;
@property(readwrite, retain)NSFileHandle * outFileHandle;


-(id) initWithPath: (NSString *) aPath;
-(void) setArgsOfTask:(NSString *)arg1
				 arg2:(NSString*) arg2
				 arg3:(NSString *) arg3;
-(void) setPipeOut;
-(void) setPipeIn;
-(NSFileHandle *) outFileHandle;
-(NSFileHandle *) inFileHandle;
-(void) launch;
-(void) terminate;
-(void) terminateObserverStatus;
-(NSString *) currenWorkingirectory;
-(void)waitUntilExit;


@end


Terminal.m
Code:
#import "Terminal.h"


typedef enum
{
	ATASK_SUCCESS_VALUE = 0
}
SUCCESS_STATUS;


@implementation Terminal

@synthesize string, digest, task = _task, outFileHandle = _outFileHandle, inFileHandle = _inFileHandle;

#pragma mark  initializers

-(void) dealloc
{
	[super dealloc];
}




-(id) initWithPath: (NSString *) aPath
{
	self = [super init];
	
	if (self)
	{
		[self setTask:[[NSTask alloc] init]];
		[[self task] setLaunchPath:aPath];
		
		[[NSNotificationCenter defaultCenter] addObserver:self
												 selector:@selector(checkATaskStatus:)
													 name:NSTaskDidTerminateNotification
												   object:nil];
		/* make @class terminal a delegate of Application? */
		
		
		
		
		 }
	
	return self;
}

-(void) terminateObserverStatus
{
	[[NSNotificationCenter defaultCenter] removeObserver:self];
}




#pragma mark  task methods

-(void) setArgsOfTask:(NSString *)arg1
				 arg2:(NSString*) arg2
				 arg3:(NSString *) arg3
{
	NSArray * arr = nil;
	
	if (arg2 == nil && arg3 == nil) {
		
		arr = [NSArray arrayWithObject:arg1];
	}
	
	else if ( arg3 == nil) 
	
	{
		
		arr = [NSArray arrayWithObjects:arg1, arg2, nil];
	}
	
	else {
		
		arr = [NSArray arrayWithObjects:arg1, arg2, arg3, nil];
	}

	
	[[self task] setArguments:arr];

}

#pragma mark  setThePipes

-(void) setPipeOut
{
	
	NSPipe *pipeOut = [NSPipe pipe];
	[[self task] setStandardOutput:pipeOut];
	
	
}

-(void) setPipeIn
{
	
	NSPipe *pipeIn = [NSPipe pipe];
	[[self task] setStandardInput:pipeIn];
	
	
}

-(NSFileHandle *) outFileHandle
{
	NSPipe * stdOut = [[self task] standardOutput];
	return [stdOut fileHandleForReading];
}


-(NSFileHandle *) inFileHandle
{
	NSPipe *stdIn = [[ self task] standardInput];
	return [stdIn fileHandleForWriting];
}


#pragma mark  launch methods

-(void) launch
{
	[[self task] launch];
}


- (void)checkATaskStatus:(NSNotification *)aNotification {
		NSLog(@"Notification: %@",[ aNotification name]);
		int status = [[aNotification object] terminationStatus];
		if (status == ATASK_SUCCESS_VALUE)
			NSLog(@"Task succeeded.");
		else
			NSLog(@"Task failed.");
	}
	
-(void) terminate
{
	[[self task ]terminate];
	[self setTask:nil];
}

-(NSString *) currenWorkingirectory
{
	return [ [self task ] currentDirectoryPath];
}

-(void)waitUntilExit
{
	[[self task] waitUntilExit];
}


#pragma mark  setFileHandles



@end
 

chown33

Moderator
Staff member
Aug 9, 2009
10,750
8,422
A sea of green
Did you try running the 'md5' command in Terminal first, using the same inputs as you wrote code for?

I ask because it seems you don't understand how command-lines work.

Answering your second question first, your 3rd example has a different hash output because it's hashing a different input. Here's why.

Referring to the man page for md5, we see it has three modes of obtaining input:
1. -s string
2. file [...]
3. <nothing>

In mode 1, it hashes only the string. In mode 2, it reads and hashes the contents of the file, or of multiple files if multiple filenames are given. In mode 3, i.e. the absence of -s string or a filename, it reads its stdin.

So when you use this command-line form:
Code:
md5 -s "Some data value."
The command hashes only the command-line arg that comes after the -s. It doesn't read any files, nor does it read its stdin. It also doesn't hash the -s, because that would be silly. So the hashed data begins with "So" and ends with "ue.".

An example that reads stdin would be:
Code:
md5
[COLOR="RoyalBlue"]Some data value.[control-D][/COLOR]
You type in the blue-hilited text, without a newline, then press control-D. The command will then print the hash of the data it read. I recommend that you actually do this, and also do the other command, so you can see how they work.

If you want a version that reads stdin, but avoids typing the same text every time:
Code:
echo -n "Some data value." >in1.txt
md5 <in1.txt
You only need to run the first command once. Its output is stored in the file "in1.txt". You can then run the md5 command any number of times, redirecting its stdin to the unchanging file. If you don't know what 'echo -n' does, you should read 'man echo'.


Referring to the code for your 3rd run, you produce its input data like this:
Code:
	NSString * fullCommandLine = [@"-s" stringByAppendingString:testString];
	NSData * stringAsData = [fullCommandLine dataUsingEncoding:NSUTF8StringEncoding];
	[fileHandleForWriting writeData:stringAsData];
This prepends "-s" to the string you want hashed, which makes no sense. The md5 command does not read switches, i.e. the -s, from its stdin. Few if any commands do that, and md5 certainly doesn't.

By prepending "-s" to the hashable data, you have changed the input. As a result, we should expect to see a different hash output, and your question shows that this is exactly what happens.


Regarding your first question, about the notification. You don't see the notification because you never wait for one. Notifications are only delivered if you run a run-loop. If you don't run a run-loop, then no notifications are delivered.

Finally, this code is wrong:
Code:
	[terminal launch];
	NSLog(@"Task's Current directory: %@", [terminal currenWorkingirectory] );
	[terminal terminate];
If it works, it works by sheer luck.

Launching a task spawns a process that runs asynchronously in parallel. That is, the code that calls launch does not wait for the process to finish. Depending on a whole lot of things you have no control over, that process might or might not finish before your code reaches the NSLog or the terminate. If the process hasn't finished by the time you reach terminate, then you're terminating it before it has finished. You may even be terminating it before it has run anything.

This is different from your 2nd launch, which is synchronous because it waits for all the output to appear. So if you're thinking that it works in the 2nd case, you're right. But if you're also thinking that the 2nd case working means the 1st case also works, then you're wrong, because you're not accounting for the difference in how the processes are run.


I didn't do any deeper examination of your code. It might have leaks, over-releases, or other bugs.

It's also unclear to me why you'd have to use NSTask at all. You said you had to write a tool that in turn runs a command, to illustrate how that command works. While that would work, as long as you use NSTask and NSPipe correctly, it is vastly more work than needed. As I've shown above, very simple shell command-lines are perfectly capable of showing how a command works, and they are much simpler to use than configuring and spawning an NSTask.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
Did you try running the 'md5' command in Terminal first, using the same inputs as you wrote code for?

The answer is "YES". And that is how I verified the correct answer I was getting for parts 1 and 2.

I ask because it seems you don't understand how command-lines work.

Uh..oh! That bad? :)

Referring to the man page for md5, we see it has three modes of obtaining input:
1. -s string
2. file [...]
3. <nothing>

OK....I went back to the source...terminal ( DUH) ! and found your reference. Tks

Chown...as always...thanks for taking the time to answer...always! appreciated.

I am going to comb through your reply in much more detail, but to the question of **WHY** this way? I wanted to check the results of the "Hash" class against something, and the easiest was doing the same calculation in terminal. ....((and of course...as usual...it all started in Hillegass...where an example is given to interrogate Amazon's Server, and which is now defunct ( the example, not Amazon...so as usual this is a **VERY LONG** way around to getting that to work, and getting more understanding along the way)). As an extension of that, I thought it might be **fun** :D to run the terminal command from within cocoa. There is a lot of stuff about this, and it seemed this would add some new knowledge to my understanding of Cocoa/Obj C, hence this little bit of code. Turns out it is quite challenging, but getting it working "somewhat" has been rewarding, albeit it a very minor, and as you say, lucky reward.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,750
8,422
A sea of green
So, may I gently ask, **where** you found that definitive description of MD5, so that I can use those refs for the future?

Under Xcode's Help menu is a menu-item: Open man Page...

Choose that, then enter md5. It should display a page similar to this on-line man page:
http://developer.apple.com/Mac/library/documentation/Darwin/Reference/ManPages/man1/md5.1.html

This assumes you're running 10.6 or 10.4. 10.5 has a different man page for the md5 command (or it does on my machine, which may not be a stock configuration at this point).

Other ways to read man pages:

1. Enter the command in Terminal: man md5

2. Use Bwana and your browser:
http://www.bruji.com/bwana/index.html

You'd enter man:md5 into the browser's URL bar, or enter this command in Terminal: open man:md5

Personally, I use Bwana, even from the command-line. Sometimes I'll use the online man page, googling for: mac os man page K, where K is the name of the command. It depends on what I might be looking for.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
Personally, I use Bwana, even from the command-line. Sometimes I'll use the online man page, googling for: mac os man page K, where K is the name of the command. It depends on what I might be looking for.

Chown33 Tks....
Well...my first insight...and sadly it often only happens when you run into something like this...is realizing the difference between -s and the stdin. ( I am unable to get terminal to accept Control-D and then enter the string...but I am working on it. At least I see the difference. Also, part 3 correlates with parts 1 and 2 when the arguments and inputs are corrected..but just wanted to say tks and delving into your answer.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
An example that reads stdin would be:
Code:
md5
[COLOR="RoyalBlue"]Some data value.[control-D][/COLOR]
You type in the blue-hilited text, without a newline, then press control-D. The command will then print the hash of the data it read. I recommend that you actually do this, and also do the other command, so you can see how they work.

I read that it's etiqutette to at the very least thank those who are helping you...( which I normally try and do) but to at least show you have learnt something from this.

So, let me say, YES indeed your code was very instructive, and after some slight messing ( including entering Control-D twice) the output was exactly the same as the -s switch.

Code:
If you want a version that reads stdin, but avoids typing the same text every time:
 ***
echo -n "Some data value." >in1.txt

***
md5 <in1.txt


Yes...Lee had used that too, and the -n switch is a very useful tip to know...thanks.

Code:
Referring to the code for your 3rd run, you produce its input data like this:
	
******

NSString * fullCommandLine = [@"-s" stringByAppendingString:testString];
	NSData * stringAsData = [fullCommandLine dataUsingEncoding:NSUTF8StringEncoding];
	[fileHandleForWriting writeData:stringAsData];


******

Lets not even go there. Not sure **what** I was thinking!

Regarding your first question, about the notification. You don't see the notification because you never wait for one. Notifications are only delivered if you run a run-loop. If you don't run a run-loop, then no notifications are delivered.

I think I see this...and this is truly a little further than I need to go, so I am going to table that in the back of my mind, for when I really need this.


Finally, this code is wrong:
*****

[terminal launch];
NSLog(@"Task's Current directory: %@", [terminal currentWorkingDirectory] );
[terminal terminate];


******
If it works, it works by sheer luck.

chown33, I wonder if this is not that wrong after all? Other than the spelling error (which I have corrected), the method is actually *my* method which calls this in my "Terminal class"

Code:
-(NSString *) currentWorkingDirectory
{
	return [ [self task ] currentDirectoryPath];  

// here task is initialized and allocated in a previous call
}

Finally, I used the switch "-p" to "pipe" in the data in part 3 thus.

Code:
[terminal3 setArgsOfTask: @"-p" arg2:nil arg3:nil];
	..............
	NSData * stringAsData = [testString dataUsingEncoding:NSUTF8StringEncoding];
	[fileHandleForWriting writeData:stringAsData];
	[fileHandleForWriting closeFile];
[/CODE]

The man file for the -p says this.

Echo stdin to stdout and append the checksum to stdout.

Actual output:
Some data value.db7116c8634ad7fe3bd90bee94274ee0"
which matches the 2 previous outputs.

Chown33...even though it may seem tough sledding to you, you have made my day! Thanks again.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,750
8,422
A sea of green
chown33, I wonder if this is not that wrong after all? Other than the spelling error (which I have corrected), the method is actually *my* method which calls this in my "Terminal class"

Code:
-(NSString *) currentWorkingDirectory
{
	return [ [self task ] currentDirectoryPath];  

// here task is initialized and allocated in a previous call
}

I wasn't pointing to currentWorkingDirectory specifically. My point was that this sequence is wrong, or at best it has unpredictable outcomes:
Code:
[terminal launch];
..anything that doesn't involve waiting for the process or data...
[terminal terminate];
Telling a process to launch and then quickly terminate has unpredictable results because there is no guarantee that the process has reached any given point. "Any given point" includes a normal finish.

When launch returns, the only thing certain is that a process was created. (Actually, not even that is certain, because the process launch may have failed, and your posted code has no real error handling.)

When you call terminate, you are forcibly terminating the process, at whatever point it's at, at that moment. This may or may not have produced any results. If it has produced results, it's only by chance that the process has gotten that far, because both your launching process and the child process are executing their own code, with no coordination between them. If you had some kind of coordination, as you do in the other cases, that's different. But in this particular case, there's no telling what might happen.

In general, when you start a process doing something, you either have to wait for it to finish, or wait for it to produce results, or otherwise coordinate between the two. It's the same thing when working with threads: execution occurs asynchronously, and you have no guarantee that it reaches any particular point unless you have some way of independently identifying that point, such as it's producing a certain output value or some other externally visible work product, such as a file.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
I wasn't pointing to currentWorkingDirectory specifically. My point was that this sequence is wrong, or at best it has unpredictable outcomes:
............
In general, when you start a process doing something, you either have to wait for it to finish, or wait for it to produce results, or otherwise coordinate between the two. It's the same thing when working with threads:

I see what you are saying...thanks again. I'll leave you alone now!
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
..........My point was that this sequence is wrong, or at best it has unpredictable outcomes:
...........
In general, when you start a process doing something, you either have to wait for it to finish, or wait for it to produce results <snip>

I realize that by glibly ignoring this advice, I will miss out on the opportunity to gain some really good insight into another aspect this little utility..so...I have put aside the need to get results with the need to gain better understanding of the programming style.

I've spent the last few days voraciously reading the docs and the "google-sphere" about this. Right now, my heads a-spinning!! :D

So, if I may, to summarize the issue here.
I have a command line tool that creates an (NSTask) to carry out some computation. ( Hash, MD5, whatever). Somehow, I need to **check into** or have the Task **check in** to my command line tool ie the ??main thread??.... to give the status and report the result.
The approaches that I have seen ...in no particular order....have included
-- using a timer which blocks the main thread until the task is complete

I think the closest I found to this was this correspondence from Chris Kane at Apple

If I interpret your explanation of the previous code correctly and it was similar to the code above, the problem here, in a sense, is more your desire to block until the task is finished or a timeout expires. Go back to the main thread. Setup a oneshot NSTimer for the timeout period. Setup a notification handler to listen for the NSTaskDidTerminateNotification. If the timer fires first, kill the task, unregister the notification handler, etc. If the notification happens first, invalidate the timer, unregister the notification handler, etc. Don't run the run loop yourself. Let your code be event-driven.

A run loop can be run re-entrantly, but as Jens said, usually it is done in a private mode, to prevent (say) the default mode stuff from happening at times which are surprising to that stuff (breaking assumptions or requirements it has), and possibly surprising to your app (breaking assumptions or requirements it has). However in this case, the task death notification, if you need that, requires the default run loop mode to be run to get delivered.



-- **create** a runloop in the command line tool and let it respond to the NSTask object once it has completed it's work.

My question is a broad one. Which approach would be the most instructive and most rewarding in actually getting the results I want? Anyone care to put this into a little more perspective? I don't mind doing the heavy lifting, but knowing **what** to lift and why helps a lot.

Thanks as always for your input.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
I wasn't pointing to currentWorkingDirectory specifically. My point was that this sequence is wrong, or at best it has unpredictable outcomes:


I think I found a solution...took a while to start dissecting the forest from the trees...but this is some code that calls methods in a "Terminal" class, which provides ( hopefully) self explanatory input.

This code runs in the command line.

Code:
terminal = [[ Terminal alloc]initWithPath:MD5PATH];// terminal has a "task" object
	[terminal setArgsOfTask: @"-s" arg2:testString arg3:nil];
	NSLog(@"Initial \"taskOutcome\" status: %i", [terminal taskOutcome]);  // check to see that initial status is not 0
	[terminal launch];
	[terminal waitUntilExit];
	NSInteger taskOutcome = [terminal taskOutcome];  // ivar "taskOutcome" is set in the method called by NSTaskDidTerminateNotificaion
	if ( taskOutcome == 0)
	{
		NSLog(@"Task completed");
	}
	else {
		
		NSLog(@"Task failed to complete");
		return RETURN_ON_FAILURE;  // defined as -1
	}

	[terminal removeObserver];
	[terminal release];

If this is anything close to being correct, then chown33, I thank you. :)
 

chown33

Moderator
Staff member
Aug 9, 2009
10,750
8,422
A sea of green
If this is anything close to being correct, then chown33, I thank you. :)

I can't tell if it's correct or not. You didn't post any of the new Terminal code.

Looking at the sequence of calls, it looks overly detailed to me. Too many little methods must be called in a specific sequence. I'm not sure what you gain by doing this, that you don't already have in NSTask. Especially since the series of calls you make effectively runs the task synchronously (i.e. waits for it to end) on the main thread. I see no advantage to that.

And I really see no reason for an observer in this situation, again because it's all being done synchronously. What do you gain by that addition, other than having to make an additional call to remove it?

If you're going to encapsulate NSTask, I think you'd be trying to simplify its usage and API, not add more complexity and more required method calls.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
I can't tell if it's correct or not. You didn't post any of the new Terminal code.

Looking at the sequence of calls, it looks overly detailed to me. Too many little methods must be called in a specific sequence. I'm not sure what you gain by doing this, that you don't already have in NSTask. Especially since the series of calls you make effectively runs the task synchronously (i.e. waits for it to end) on the main thread. I see no advantage to that.

Yep....you are correct. I guess the one thing I got from this was actually doing it and using those calls to find out what they did. But, the key aspect that you keep coming back to ( correctly, clearly) is that , **I think** you would prefer to see NSTask act in a (truly) asynchronous manner. The problem I had with that approach, and it may well have been simply missing somethings in all the stuff I read, was how to indicate to the main thread that the task, once instantiated and started and completed, was done. Which was the issue right at the beginning of our conversation. Again, as an exercise in learning, I would really like to implement that, but so far, have been unable to do this in the command line set up that I have. A push from you or anyone in the right direction would be greatly appreciated. ( I have included the full code of the "terminal" class below)


And I really see no reason for an observer in this situation, again because it's all being done synchronously. What do you gain by that addition, other than having to make an additional call to remove it?

I agree....it's more of a confirmation that NSTask actually is done, more than anything else.

Code:
#import <Foundation/Foundation.h>


@interface Terminal : NSObject {
	
	NSString * string;
	NSString * digest;
	NSTask *_task;
	NSFileHandle *_inHandle;
	NSFileHandle *_outHandle;
	NSInteger _taskOutcome;
	

}

@property(readwrite, retain) NSString *string;
@property(readwrite, retain) NSString *digest;
@property(readwrite, retain) NSTask *task;
@property(readwrite, retain) NSFileHandle * inFileHandle;
@property(readwrite, retain)NSFileHandle * outFileHandle;
@property(readwrite) NSInteger taskOutcome;


-(id) initWithPath: (NSString *) aPath;
-(void) setArgsOfTask:(NSString *)arg1
				 arg2:(NSString*) arg2
				 arg3:(NSString *) arg3;
-(void) setPipeOut;
-(void) setPipeIn;
-(NSFileHandle *) outFileHandle;
-(NSFileHandle *) inFileHandle;
-(void) launch;
-(void) terminate;
-(void) terminateObserverStatus;
-(NSString *) currentWorkingDirectory;
-(void) waitUntilExit;
-(BOOL) taskIsRunning;
-(void) removeObserver;



@end

Code:
#import "Terminal.h"


typedef enum
{
	ATASK_SUCCESS_VALUE = 0
}
SUCCESS_STATUS;

#define UNUSED_TASK_OUTCOME -999


@implementation Terminal

@synthesize string, digest, task = _task, outFileHandle = _outFileHandle, inFileHandle = _inFileHandle, taskOutcome = _taskOutcome;

#pragma mark  initializers

-(void) dealloc
{
	[super dealloc];
}




-(id) initWithPath: (NSString *) aPath
{
	self = [super init];
	
	if (self)
	{
		[self setTask:[[NSTask alloc] init]];
		[[self task] setLaunchPath:aPath];
		[[NSNotificationCenter defaultCenter] addObserver:self
								 selector:@selector(checkATaskStatus:)
									 name:NSTaskDidTerminateNotification
								   object:nil];
		[self setTaskOutcome:UNUSED_TASK_OUTCOME];
		
				
		}
	
	return self;
}

-(void) terminateObserverStatus
{
	[[NSNotificationCenter defaultCenter] removeObserver:self];
}




#pragma mark  task methods

-(void) setArgsOfTask:(NSString *)arg1
				 arg2:(NSString*) arg2
				 arg3:(NSString *) arg3
{
	NSArray * arr = nil;
	
	if (arg2 == nil && arg3 == nil) {
		
		arr = [NSArray arrayWithObject:arg1];
	}
	
	else if ( arg3 == nil) 
	
	{
		
		arr = [NSArray arrayWithObjects:arg1, arg2, nil];
	}
	
	else {
		
		arr = [NSArray arrayWithObjects:arg1, arg2, arg3, nil];
	}

	
	[[self task] setArguments:arr];

}

#pragma mark  setThePipes

-(void) setPipeOut
{
	
	NSPipe *pipeOut = [NSPipe pipe];
	[[self task] setStandardOutput:pipeOut];
	
	
}

-(void) setPipeIn
{
	
	NSPipe *pipeIn = [NSPipe pipe];
	[[self task] setStandardInput:pipeIn];
	
	
}

-(NSFileHandle *) outFileHandle
{
	NSPipe * stdOut = [[self task] standardOutput];
	return [stdOut fileHandleForReading];
}


-(NSFileHandle *) inFileHandle
{
	NSPipe *stdIn = [[ self task] standardInput];
	return [stdIn fileHandleForWriting];
}


#pragma mark  launch methods

-(void) launch
{
	[[self task] launch];
}


- (void)checkATaskStatus:(NSNotification *)aNotification {
		
	[self setTaskOutcome: [[aNotification object] terminationStatus]];
		
	}
	
-(void) terminate
{
	[[self task ]terminate];
	[self setTask:nil];
}

-(NSString *) currentWorkingDirectory
{
	return [ [self task ] currentDirectoryPath];
}

-(void) waitUntilExit
{
	[[self task ]waitUntilExit];
}

-(BOOL) taskIsRunning
{
	return [[ self task] isRunning];
}

-(void) removeObserver
{
	[[NSNotificationCenter defaultCenter] removeObserver: self];
}
@end
 

chown33

Moderator
Staff member
Aug 9, 2009
10,750
8,422
A sea of green
Yep....you are correct. I guess the one thing I got from this was actually doing it and using those calls to find out what they did. But, the key aspect that you keep coming back to ( correctly, clearly) is that , **I think** you would prefer to see NSTask act in a (truly) asynchronous manner.

I have no preference one way or the other. Each one has uses, in different circumstances.

Going back to your original post, the 1st launch was completely asynchronous, and the 2nd launch was completely synchronous.

The 3rd was semi-synchronous, in that the parent process provided input data via a pipe, but the only thing it did while the child process was running was feed data. No other actions were permitted, so it was not fully asynchronous.

In all 3 cases, the task should have been synchronous, i.e. waiting for a result. 2 out of 3 already did this. Only 1 didn't.

You could have made the other one synchronous simply by waiting for the task to exit. None of the asynchronous features were needed here, so it's unclear to me why you kept adding them.

Given the context, a command-line tool that runs other tools, it seems appropriate to me that the main thread run those other tools synchronously. It should terminate only when its sub-tasks have finished. So again, it's unclear what adding asynchronous features is giving you here.

The problem I had with that approach, and it may well have been simply missing somethings in all the stuff I read, was how to indicate to the main thread that the task, once instantiated and started and completed, was done.
An NSTask is done when it exits. After it exits, it is no longer running, so isRunning will return false.

After exit, terminationStatus returns a value; before exit it throws an exception.

However, since you're running the task synchronously in the first 2 cases, simply waiting for the task to exit would suffice. This might not always work, if the process produces a lot of output on a pipe, but hashing always produces a fixed amount of output, but its very nature, so this isn't an issue here.

In the 3rd case, you also wait for exit or read the output pipe until EOF.


Your goal appears to be a rough emulation of shell commands:
Code:
md5 -s someData >/dev/null

collectedOutput=`md5 -s someData`

collectedOutput=`echo -n someData | md5`
Those seem relatively simple to me. They all run synchronously, i.e. they wait for the command to finish running, either collecting output or ignoring it, but always awaiting completion.

My point about the design of Terminal's API is its complexity. Since you're encapsulating NSTask, one would hope for an API that is simpler to use when doing simple things, such as launching and collecting output, or launching and ignoring output. But that's not what you have. Instead you've added methods and doubled the number of things a caller must coordinate.

I would have designed it to have a launchAndCollectOutput method, and a launchAndIgnoreOutput method, both of which waited for exit. The output-collecting method might need an NSStringEncoding parameter, or it could use a sensible default. The output-ignoring method needs no parameters.

You could also make the launchAndXX methods take parameters representing the command and its args, to reduce the API even further.

Consider another method that adds an optional NSData for feeding to the task as its stdin. Any use of pipes for providing input or collecting output is purely internal, not exposed as API.
 

mdeh

macrumors 6502
Original poster
Jan 3, 2009
345
2
chown33..let me first just say that this thread has been incredibly instructive to me....and let me try and explain the differences in approach in the various attempts to get this done.

I have no preference one way or the other. Each one has uses, in different circumstances.

Good....as I **thought** that you were in fact in favor of using the asynchronous mode...as in the first launch.

Going back to your original post, the 1st launch was completely asynchronous, and the 2nd launch was completely synchronous..... The 3rd was semi-synchronous....in all 3 cases, the task should have been synchronous, i.e. waiting for a result. 2 out of 3 already did this. Only 1 didn't.

and

You could have made the other one synchronous simply by waiting for the task to exit. None of the asynchronous features were needed here, so it's unclear to me why you kept adding them.........Given the context, a command-line tool that runs other tools, it seems appropriate to me that the main thread run those other tools synchronously.

Part of the problem here was you assuming, perhaps, that I knew what I was doing! :) I took from your input that I was using NSTask incorrectly ie not quite understanding the essence, ( as I now do) of synchronous vs asynchronous....which in fact was true. My goal to try and use NSTask was to understand how it worked, and in the process, with your help, have a much broader understanding of synchronicity. When I had gone back to the docs and all the examples I could find, it became apparent that this code ought to be done synchronously ( now that I understood it). Please understand that even though it may not seem like it, your input has been invaluable.



However, since you're running the task synchronously in the first 2 cases, simply waiting for the task to exit would suffice.

Agree completely. But, at this stage I simply wanted to set up the notification as a confirmatory action that it had finished, as well as the knowledge it added in using notifications...when not coming out of Hillegass's book :)


Your goal appears to be a rough emulation of shell commands:
Code:
md5 -s someData >/dev/null

collectedOutput=`md5 -s someData`

collectedOutput=`echo -n someData | md5`

Precisely...
My goal was to create a class that I could use in Hillegass's book , but at the same time, I thought it would be nice to verify the results agains a standard that I could rely on ie doing the same in terminal. So, why not simply run it alone in terminal. Simply, in the end, to learn about NSTask. At some point I do want to write a daemon that shuts down one of my backup computers - (in the case of a power failure) - running a program called Retrospect, which has the nasty habit of not wishing to quit and thus stopping the whole shutdown process. And...my UPS will eventually fail too...so believe it or not...there really is some goal behind all this madness.


Since you're encapsulating NSTask, one would hope for an API that is simpler to use when doing simple things, such as launching and collecting output, or launching and ignoring output. But that's not what you have. Instead you've added methods and doubled the number of things a caller must coordinate.

I understand exactly what you are implying. The "caller" in this case is yours truly, but the gist of what you say remains, nontheless, true. When I go back and "refactor" it, this is what I will aim for. In fact, when I write the actual "Hash class" (which) was the idea behind this when I started it, it will incorporate those exact principles as you outline here (well, I will certainly try). I hope I am not being obtuse and appear not to comprehend what you are saying, but the extra steps are more of a learning tool for me, than anything else. Your help to me has been invaluable, as I am sure , many others feel on this board.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.