« Wrapping UNIX Commands Part II

Posted by Andy Monitzer on July 05, 2001 [Feedback (6) & TrackBack (0)]

// What's This Tutorial About?

In my "Wrapping UNIX Commands" tutorial, I wrote the following:

Maybe you should implement a log window so that people can watch something while it's downloading (see NSTask's setStandardOutput:).

Well, people kept bugging me about how to do this. So, here's an explanation:

 

// The ls-Wrapper

I won't focus on UI, since we already covered it in the aforementioned "Wrapping UNIX Commands" tutorial. Instead, we'll jump right into some code. There's one outlet in the header, textview (where the output is written; it's wise to make it read-only/selectable):

#import <Cocoa/Cocoa.h>

@interface lswrapper : NSObject
{
    IBOutlet id textview;
}
@end

In the .m file, I'll put everything in the applicationDidFinishLaunching method. Note that the class has to be the application's delegate to get this to work. Usually you'll want to insert the code into some action method (triggered by a button, for example).

#import "lswrapper.h"

@implementation lswrapper

- (void)applicationDidFinishLaunching:(NSNotification*)notification {
    NSTask *ls=[[NSTask alloc] init];
    
    [ls setLaunchPath:@"/bin/ls"];
    [ls setArguments:[NSArray arrayWithObjects:@"-l",@"/System",nil]];
    [ls launch];
    
    [ls release];
}

@end

This code should work without a hitch. Try to compile and run it. The following should show up in Project Builder's console:

total 0
drwxr-xr-x  44 root  wheel  1452 Jul  4 04:18 Library

 

Moving On...

The next step is to redirect the output to the textview:

#import "lswrapper.h"

@implementation lswrapper

- (void)applicationDidFinishLaunching:(NSNotification*)notification {
    NSTask *ls=[[NSTask alloc] init];
    NSPipe *pipe=[[NSPipe alloc] init];
    NSFileHandle *handle;
    NSString *string;
    
    [ls setLaunchPath:@"/bin/ls"];
    [ls setArguments:[NSArray arrayWithObjects:@"-l",@"/System",nil]];
    [ls setStandardOutput:pipe];
    handle=[pipe fileHandleForReading];
    
    [ls launch];
    
    string=[[NSString alloc] initWithData:[handle readDataToEndOfFile]
        encoding:NSASCIIStringEncoding]; // convert NSData -> NSString
    
    [textview setString:string];
    
    [string release];
    [pipe release];
    [ls release];
}

@end

Ok, that's it! Well, not really. This code is nice for ls, but has three problems for most other uses:

  1. It blocks until it gets all the data (readDataToEndOfFile). This means that the rainbow cursor is spinning during that period. Blech.
  2. The complete output is written at once, which isn't usable for things like wget which update their state regulary.
  3. It's not interactive. Note that this feature is very complicated, it's better to tell Terminal.app to execute that commands in this case.

 

// Problem Number One

The first problem can be solved by moving the copying into another thread:

#import "lswrapper.h"

@implementation lswrapper

- (void)applicationDidFinishLaunching:(NSNotification*)notification {
    NSTask *ls=[[NSTask alloc] init];
    NSPipe *pipe=[[NSPipe alloc] init];
    NSFileHandle *handle;
    
    [ls setLaunchPath:@"/bin/ls"];
    [ls setArguments:[NSArray arrayWithObjects:@"-l",@"/System",nil]];
    [ls setStandardOutput:pipe];
    handle=[pipe fileHandleForReading];
    
    [ls launch];
    
    [NSThread detachNewThreadSelector:@selector(copyData:)
        toTarget:self withObject:handle];
    
    [pipe release];
    [ls release];
}

- (void)copyData:(NSFileHandle*)handle {
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
    NSString *string=[[NSString alloc] initWithData:
          [handle readDataToEndOfFile] encoding:NSASCIIStringEncoding];
    
    [textview setString:string];
    
    [string release];
    [pool release];
}

@end

Note: AppKit is not thread-safe, so this code might do harm to your computer, make you unpopular and/or make your wife/husband pregnant. Use at your own risk (and I'd not recommend to). Look at distributed objects if you want to release an app utilizing this snippet. Since Mac OS X 10.2, you can also use -performSelectorOnMainThread:object:waitUntilDone:, which is easier to implement.

The main thread (which executes applicationDidFinishLaunching:) creates another thread (copyData:) which does all the blocking work.

Note that an autorelease pool is needed in the new thread since it's not a part of a runloop (if you don't know what that means don't worry, Project Builder's console will tell you when you have to create one).

 

// Problem Number Two

This one is more complicated, you have to constantly poll for new data (only copyData: needs changes). To understand the following code, you have to read the explanation of NSFileHandle's availableData from the reference:

Returns the data available through the receiver. [...] If the receiver is a communications channel, reads up to a buffer of data and returns it; if no data is available, the method blocks. Returns an empty NSData if the end of file is reached.

- (void)copyData:(NSFileHandle*)handle {
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
    NSData *data;

    while([data=[handle availableData] length]) { // until EOF (check reference)
        NSString *string=[[NSString alloc] initWithData:data
                encoding:NSASCIIStringEncoding];
        NSRange theEnd=NSMakeRange([[textview string] length],0);

        [textview replaceCharactersInRange:theEnd
             withString:string]; // append new string to the end
        theEnd.location+=[string length]; // the end has moved
        [textview scrollRangeToVisible:theEnd];

        [string release];
    }

    [pool release];
}

As always, the above is not the only way to solve the problem, but that should be enough to get started. Which solution you should use depends on the application.

Have fun!


Comments

I don't think the code in the "Moving on..." section works... Seems like the standard out is not getting redirected correctly. Its showing up in the application instead of going to the pipe. I'm not really a newbie - I'm pretty sure I'm not doing anything trivial incorrectly...

Posted by: Josh on February 23, 2003 11:33 PM

Interesting article but the concepts are presented far too quickly and without enough context.

If the author is assuming that the readers know this information then what is the point of the article? Explain what you are doing and why you are doing it and this would have been far more useful.

Posted by: Zac on June 4, 2003 03:49 AM

I'll try to explain the things I'm doing in the "Moving On..."-section in more detail when I got some time, but keep in mind that I can't replace reading Apple's Cocoa reference.

Posted by: andy on June 4, 2003 06:11 AM

I'm currently having a go at the following:

1) telneting to a certain IP
2) logging in with a user and password
3) choosing an option from a menu

However, I have no idea as how to go about doing it. How do I for instance send strings (username, password, menuchoice) to the task that I have launched?

Further, how would I go about checking for specific words or phrases (ie. 'Login:','Password' etc.)

Can anyone tell me how to go about doing stuff like this?

Thank you very much in advance.

Sincerely,

Thomas

Posted by: Thomas Pilgaard Nielsen on July 19, 2003 04:45 PM

Thomas:

Take a look at expect. It is _the_ way to go about scripting interactive applications, which sounds like what you want. Even if using expect isn't acceptable for your project, exposing yourself to it might give you some insight on how to accomplish something similar in Objective-C.

Posted by: Shane Celis on September 14, 2003 05:05 AM

What is expect?

What I'm trying to do is make a cocoa gui that will authenticate the user and then run a few perl scripts, which I intend to include in the bundle as resources. Any suggestions?

Posted by: Cory Forsyth on November 14, 2003 10:06 AM
Post a comment