Cocoa Memory Management NSArray with Objects - cocoa

I'm having troubles releasing objects.. To explain it better I have included my code below.
NSTask *task = [NSTask new];
NSTask *grep = [NSTask new];
NSPipe *pipe = [NSPipe new];
[task setStandardError: pipe];
[grep setStandardInput: pipe];
[pipe release];
pipe = [NSPipe new];
[grep setStandardOutput: pipe];
[task launch];
[grep launch];
NSString *string = [[[[[[NSString alloc] initWithData: [[[grep standardOutput] fileHandleForReading] readDataToEndOfFile] encoding: NSASCIIStringEncoding] autorelease] componentsSeparatedByString: #" "] objectAtIndex: 3] substringToIndex: 8];
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict setObject: string forKey: #"myKey"];
[records addObject: dict];
[dict release];
[task release];
[grep release];
[pipe release];
How would I release the string and are there any other leaks? Also, if I remove everything from the array records with removeAllObjects, is everything released okay then too? The array should never be released and be available at all time, I'm just worrying about its objects.
Edit: The only leak pointed out had to do with the NSPipe and should be fixed in the code.
Thanks for any help!

Memory management in Objective-C has one fundamental rule:
You take ownership of an object if you create it using a method whose name begins with “alloc” or “new” or contains “copy” (for example, alloc, newObject, or mutableCopy), or if you send it a retain message. You are responsible for relinquishing ownership of objects you own using release or autorelease. Any other time you receive an object, you must not release it.
Thus every call to new in your code sample should be balanced with a call to release or autorelease. The NSArray, along with most other objects in the code, isn't created with either, so it doesn't need to be released. The [NSString alloc] is autoreleased, so it's taken care of. Collections manage their own items, retaining and releasing them as necessary: when an item is inserted, it's retained; when it's removed, it's released. Dictionary keys are copied rather than retained.
Where you've got an unbalanced new (and hence leak) is the first NSPipe you created. Release it before creating the pipe for grep's standard output. Perhaps you simply left it out of the sample, but you're also not setting any arguments for the grep task.

substringToIndex: returns an autoreleased string, so there's no need to release it.
The only memory leak I see is where you set up your 'pipe' var a second time (for the task's standard output) without first releasing its current value (the NSPipe instance used for standard error & input).
Mutable collections like NSMutableArray will retain the objects they contain (as do all mutable/nonmutable collections) then release them when they're removed (or when the collection itself is deallocated).

Related

Mac OS X: Execute scripts with hooks from an application

Im building an cocoa app that monitors something™ and I am planning to have some hooks for users. So I want to enable the user to put a script (Bash, Ruby, Python you name it) with a specified name (let's say after_event) into the Application Support directory and that script gets executed after a certain event in my code. Ideally I could pass some variables to the script so the script knows what happened.
Any ideas on this?
So problem one is: How do I get the path of the Application Support "the SDK way"? problem two is: How do I execute script with variables like THAT_APPEND="foo"?
Thanks,
Philip
Because sharing is caring here is the method that executes the scripts:
-(void) runScript:(NSString*)scriptName withVariables:(NSDictionary *)variables
{
NSString *appSupportPath = [NSFileManager defaultManager] applicationSupportDirectory];
NSArray *arguments;
NSString* newpath = [NSString stringWithFormat:#"%#/%#",appSupportPath, scriptName];
if ([[NSFileManager defaultManager]fileExistsAtPath:newpath]){
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: newpath];
NSLog(#"Executing hook: %#",newpath);
arguments = [NSArray arrayWithObjects:newpath, nil];
[task setArguments: arguments];
[task setEnvironment:variables];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *string;
string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog (#"script returned:\n%#", string);
}
}
}
UPDATE: I updated the code to be more generic. Now NSTask will tell the kernel to execute the script directly so your user can not online use Bash scripts but also python, perl, php whatever she likes. The only thing she needs to use is a Shebang in that file.
The NSFileManager Category can be found here.
Look for NSTask documentation. There's an environment member you can manipulate. Also adding command line parameters in a form -name = value should be trivial.

NSTask Does Not Terminate

I'm trying to use NSTask to run the UNIX 'apropos' command. Here's my code:
NSTask *apropos = [[NSTask alloc] init];
NSPipe *pipe = [[NSPipe alloc] init];
[apropos setLaunchPath:#"/usr/bin/apropos"];
[apropos setArguments:[NSArray arrayWithObjects:#"filename", #"match", nil]];
[apropos setStandardOutput:pipe];
[apropos launch];
[apropos waitUntilExit];
The problem is that this never returns. I also tried using Apple's example code (TaskWrapper) and it returns the output (in three segments) but it never calls the processFinished handler.
Furthermore, the appendOutput: handler receives duplicates. So, for example, if apropos returns this:
1
2
3
4
5
I might receive something like this:
1
2
3
1
2
3
4
5
(grouped into 3 append messages).
I note that Apropos displays the output in a format where it's possible to scroll up and down in the command line instead of just directly outputting the data straight to standard output; how do I read this reliably through NSTask and NSPipe?
I’ve just tested this program and it works fine: the program terminates and /tmp/apropos.txt contains the output of apropos.
#import <Foundation/Foundation.h>
int main()
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSTask *apropos = [[[NSTask alloc] init] autorelease];
NSPipe *pipe = [[[NSPipe alloc] init] autorelease];
NSFileHandle *readHandle = [pipe fileHandleForReading];
[apropos setLaunchPath:#"/usr/bin/apropos"];
[apropos setArguments:[NSArray arrayWithObjects:#"filename", #"match", nil]];
[apropos setStandardOutput:pipe];
[apropos launch];
[apropos waitUntilExit];
NSString *output = [[[NSString alloc]
initWithData:[readHandle readDataToEndOfFile]
encoding:NSUTF8StringEncoding] autorelease];
[output writeToFile:#"/tmp/apropos.txt" atomically:YES
encoding:NSUTF8StringEncoding error:NULL];
[pool drain];
return 0;
}
Are you by any chance using NSLog() to inspect the output? If so, you might need to set a pipe for stdin as explained in this answer of mine to an NSTask related question. It seems that NSLog() sending data to stderr affects NSTask.
With your original code, I would imagine it's because you're not reading the output of the command. The pipes only have a limited buffer size, and if you don't read the output of the task, it can end up hung waiting for the buffer to empty out. I don't know anything about the sample code you tried so I can't help there. As for the last question, apropos only uses the pager when it's connected to a terminal. You're not emulating a terminal, so you don't have to worry. You can prove this by running apropos whatever | cat in the terminal and verifying that the pager is not invoked.

How to gradually retrieve data from NSTask?

I am working on a GUI (Cocoa) for a command-line tool to make it more accessible to people. Despite it being a GUI, I would like to display the output to an NSTextView. The problem is that the output is large and the analysis the tool carries out can take hours/days.
Normally, when working with NSTask and NSPipe, the output is displayed only after the task is completely finished (which can take a long time). What I want to do is split the output up and display it gradually (updating every minute for example).
So far I have placed the processing of the data in a separate thread:
[NSThread detachNewThreadSelector:#selector(processData:) toTarget:self withObject:raxmlHandle];
- (void)processData:(id)sender {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *startString = [results string];
NSString *newString = [[NSString alloc] initWithData:[raxmlHandle readDataToEndOfFile] encoding:NSASCIIStringEncoding];
[results setString:[startString stringByAppendingString:newString]];
[startString release];
[newString release];
[pool release];
}
All this is still a bit of voodoo to me and I am not exactly sure how to deal with this challenge.
Do you have any suggestions or recommendations?
Thanks!
You need to use a notification provided by NSFileHandle.
First, add yourself as an observer to the NSFileHandleReadCompletionNotification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(outputReceived:)
name:NSFileHandleReadCompletionNotification
object:nil];
Then, prepare a task, call readInBackgrounAndNotify of the file handle of the pipe, and launch the task.
NSTask*task=[[NSTask alloc] init];
[task setLaunchPath:...];
NSPipe*pipe=[NSPipe pipe];
[task setStandardOutput:pipe];
[task setStandardError:pipe];
// this causes the notification to be fired when the data is available
[[pipe fileHandleForReading] readInBackgroundAndNotify];
[task launch];
Now, to actually receive the data, you need to define a method
-(void)outputReceived:(NSNotification*)notification{
NSFileHandle*fh=[notification object];
// it might be good to check that this file handle is the one you want to read
...
NSData*d=[[aNotification userInfo] objectForKey:#"NSFileHandleNotificationDataItem"];
... do something with data ...
}
You might want to read Notification Programming Topics to understand what is going on.

Saving System Profiler info in a .spx file using NSTask

In Cocoa, I am trying to implement a button, which when the user clicks on will capture the System profiler report and paste it on the Desktop.
Code
NSTask *taskDebug;
NSPipe *pipeDebug;
taskDebug = [[NSTask alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:selfselector:#selector(taskFinished:) name:NSTaskDidTerminateNotification object:taskDebug];
[profilerButton setTitle:#"Please Wait"];
[profilerButton setEnabled:NO];
[taskDebug setLaunchPath: #"/usr/sbin/system_profiler"];
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",#">", #"
~/Desktop/Profiler.spx",nil];
[taskDebug setArguments:args];
[taskDebug launch];
But this does not save the file to the Desktop. Having
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",nil]
works and it drops the whole sys profiler output in the Console Window.
Any tips on why this does not work or how to better implement this ? I am trying to refrain from using a shell script or APpleScript to get the system profiler. If nothing work's that would be my final option.
Thanks in advance.
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",#">", #"~/Desktop/Profiler.spx",nil];
That won't work because you aren't going through the shell, and > is a shell operator. (Also, ~ isn't special except when you expand it using stringByExpandingTildeInPath.)
Create an NSFileHandle for writing to that Profiler.spx file, making sure to use the full absolute path, not the tilde-abbreviated path. Then, set that NSFileHandle as the task's standard output. This is essentially what the shell does when you use a > operator in it.
This got it done ( thanks to Peter and Costique)
[taskDebug setLaunchPath: #"/usr/sbin/system_profiler"];
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"- detailLevel",#"full",nil];
[taskDebug setArguments:args];
[[NSFileManager defaultManager] createFileAtPath: [pathToFile stringByExpandingTildeInPath] contents: nil attributes: nil];
outFile = [ NSFileHandle fileHandleForWritingAtPath:[pathToFile stringByExpandingTildeInPath]];
[taskDebug setStandardOutput:outFile];
[taskDebug launch];
Create an NSPipe, send [taskDebug setStandardOutput: myPipe] and read from the pipe's file handle.

NSarray release

If i declare an NSArray with alloc & retain in single sentence then should i release the NSArray object twice (i.e. [arrayObject release] 2 times) ?
If you are creating an NSArray with an alloc and a retain on the same line then you are probably doing something wrong.
Objects are alloced with a retain count of +1, so there is no need to call retain on it as well.
To answer your question directly; yes, you do have to release it twice. Once because you created the object and once because you retained it. But I would question why you need to retain it an extra time in the first place.
You don't need to retain it. You already retain--or take ownership of--an object when you alloc/init. Revisit the Memory Management Programming Guide for Cocoa.
No, you have to release the object for each alloc and each retain. (And you can't alloc an object more than 1 time anyway.)
If you do
NSArray* arrayObject;
arrayObject = [[NSArray alloc] init];
arrayObject = [[NSArray alloc] init];
...
then it just wrong code. The latter assignment will cover the old one, which causes a leak. Either use 2 objects, and release each of them once:
NSArray* arrayObject1, arrayObject2;
arrayObject1 = [[NSArray alloc] init];
arrayObject2 = [[NSArray alloc] init];
...
[arrayObject1 release];
[arrayObject2 release];
or release the object before another init.
NSArray* arrayObject;
arrayObject = [[NSArray alloc] init];
...
[arrayObject release];
arrayObject = [[NSArray alloc] init];
...
[arrayObject release];

Resources