Relaunching a cocoa app - objective-c

I have an application that checks its command line parameters and stores values in persistent stores. One of those is a password that I don't want sticking around for people to see with 'ps' and friends. The approach I'm currently looking at is to, after I've stored the values I need, relaunch the process without the command line parameters. My naive approach is this, where args[0] is the path to the application:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:[args objectAtIndex:0]];
[task launch];
[task release];
[NSApp terminate:nil];
The child is run. However, when my app is terminated the child doesn't seem to orphan but gets stuck. Am I just way off on this one?
More info: So it seems that when I call [NSApp terminate:nil] the NSTask that was launched gets stuck, but if I just exit() then it works fine. However, I'm concerned that things that are open (keychain, plist, etc.) will be in a bad state if I do that.
And note that lots of example code out there is about some watchdog-like process that restarts a separate process when needed. I'm trying to restart the current process that's already running from within that same process.

There are plenty of examples on the web, but this one (also below) looks like it has all the code you need. There are more detailed explanations out there, as well.
// gcc -Wall -arch i386 -arch ppc -mmacosx-version-min=10.4 -Os -framework AppKit -o relaunch relaunch.m
#import <AppKit/AppKit.h>
#interface TerminationListener : NSObject
{
const char *executablePath;
pid_t parentProcessId;
}
- (void) relaunch;
#end
#implementation TerminationListener
- (id) initWithExecutablePath:(const char *)execPath parentProcessId:(pid_t)ppid
{
self = [super init];
if (self != nil) {
executablePath = execPath;
parentProcessId = ppid;
// This adds the input source required by the run loop
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:#selector(applicationDidTerminate:) name:NSWorkspaceDidTerminateApplicationNotification object:nil];
if (getppid() == 1) {
// ppid is launchd (1) => parent terminated already
[self relaunch];
}
}
return self;
}
- (void) applicationDidTerminate:(NSNotification *)notification
{
if (parentProcessId == [[[notification userInfo] valueForKey:#"NSApplicationProcessIdentifier"] intValue]) {
// parent just terminated
[self relaunch];
}
}
- (void) relaunch
{
[[NSWorkspace sharedWorkspace] launchApplication:[NSString stringWithUTF8String:executablePath]];
exit(0);
}
#end
int main (int argc, const char * argv[])
{
if (argc != 3) return EXIT_FAILURE;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[[[TerminationListener alloc] initWithExecutablePath:argv[1] parentProcessId:atoi(argv[2])] autorelease];
[[NSApplication sharedApplication] run];
[pool release];
return EXIT_SUCCESS;
}

I know its a bit late to answer but this answer may help others. Here is a cool trick that can help you.
By using the terminal command, just open your application as a new instance and terminate the current instance.
This is how it is done:
....
NSString* path = [[NSBundle mainBundle] bundlePath];
NSString* cmd = [NSString stringWithFormat:#"open -n %#", path];
[self runCommand:cmd];
exit(0);
}
/// temrinal function
-(NSString*)runCommand:(NSString*)commandToRun;
{
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments = [NSArray arrayWithObjects:
#"-c" ,
[NSString stringWithFormat:#"%#", commandToRun],
nil];
NSLog(#"run command: %#",commandToRun);
[task setArguments: arguments];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *output;
output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
return output;
}

Create an external process that launches yours when it terminates. Then terminate. Launching Cocoa programs with NSTask doesn't work quite right.

For anyone who still wants to use NSTask to relaunch,I found one possible way: Please DO NOT set NSPipe of the NSTask,because the NSTask will terminate the app itself,once the app terminated,the NSTask that started might get stuck there.
At least for me,after I removed the NSPipe settings,my app relaunch successfully.
The following is what I do:
1. Write a command line tool which has 3 parameters: app bundle id,app full path,please note that in this command line you need to terminate the app and wait for a while to make sure it is really terminated before launch the new one,I keep checking app.isTerminated and sleep(1) if it's not terminated.
Launch the Command line tool in app using NSTask,and set the parameters accorddingly,Don't use NSPipe,simply create NSTask and launch
The app relaunches now

Related

How do I run a binary from a Cocoa app?

I'm making a simple launcher for a binary so I can have it appear in my launchpad.
My initial thought was that running system("./myProgram"); would be sufficient, but it doesn't appear to actually do anything as the terminal instance it runs doesn't stay open after running the command, immediately shutting down whatever other tasks the program did.
So my question is, is there a way for me to do this that keeps it open indefinitely?
Edit: I want my launcher to close immediately after launching the program, so it would be less than ideal to rely on something that requires it to stay open
Edit: the following all work, but only when run from xcode, when running it stand-alone it doesn't launch the program at all
system("open /Applications/Utilities/Terminal.app myProgram");
system("open myProgram");
system("/bin/sh -c ./myProgram&");
system("./myProgram&");
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/bash"];
[task setArguments: #[#"-c", #"./myProgram"]];
[task launch];
NSTask does not give any errors, and it doesn't throw any exceptions either when the app runs
Literally every other aspect of the program works, it just won't launch, and it won't say why
Based on all the "feedback" here's what I got so far. And it still doesn't work unless I provide an absolute path (which is no good in case I want to move it later)
//
// AppDelegate.m
// DFLauncher
//
// Created by Electric Coffee on 11/02/15.
// Copyright (c) 2015 Electric Coffee. All rights reserved.
//
#import "AppDelegate.h"
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#end
NSString *CURRENT_DIR;
NSString *FILE_PATH;
NSString *INIT_PATH = #"/data/init/init.txt";
NSString *VOLUME_ON = #"[SOUND:YES]";
NSString *VOLUME_OFF = #"[SOUND:NO]";
BOOL contains(NSString *a, NSString *b) {
return [a rangeOfString: b].location != NSNotFound;
}
NSData *replaceString(NSString *fileContents, NSString *from, NSString *to) {
return [[fileContents stringByReplacingOccurrencesOfString: from withString: to]
dataUsingEncoding: NSUTF8StringEncoding];
}
#implementation AppDelegate
- (void)applicationDidFinishLaunching: (NSNotification *)aNotification {
CURRENT_DIR = [[NSFileManager new] currentDirectoryPath]; //[[NSBundle mainBundle] bundlePath];
//NSLog(#"%#", CURRENT_DIR);
FILE_PATH = [CURRENT_DIR stringByAppendingString: INIT_PATH];
_fileContents = [NSString stringWithContentsOfFile: FILE_PATH
encoding: NSUTF8StringEncoding
error: NULL];
if (contains(_fileContents, VOLUME_OFF))
[_toggleMute setState: YES];
if (contains(_fileContents, VOLUME_ON))
[_toggleMute setState: NO];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
}
- (IBAction)playButtonClick: (id)sender {
//system("open /Applications/Utilities/Terminal.app df"); // doesn't quite work
//system("open /Applications/df_osx/df");
//system("/bin/sh -c /Applications/df_osx/df&");
//system("/Applications/df_osx/df&");
NSString *gamePath = [CURRENT_DIR stringByAppendingString: #"/df&"];
NSTask *task = [NSTask new];
[task setLaunchPath: #"/bin/bash"];
[task setArguments: #[#"-c", gamePath]];
NSError *error = task.standardError;
[task launch];
[NSAlert alertWithError: error];
//[NSApp terminate: self];
}
- (IBAction)folderButtonClick: (id)sender {
system("open .");
}
- (IBAction)quitButtonClick: (id)sender {
[NSApp terminate: self];
}
- (IBAction)mute: (id)sender {
NSData *result;
NSFileManager *fm = [NSFileManager defaultManager];
if ([sender state] == NSOffState)
result = replaceString(_fileContents, VOLUME_OFF, VOLUME_ON);
else
result = replaceString(_fileContents, VOLUME_ON, VOLUME_OFF);
[fm createFileAtPath: FILE_PATH contents: result attributes: nil];
}
#end
Hacky solution that works (but isn't elegant at all)
I had to replace
FILE_PATH = [CURRENT_DIR stringByAppendingString: INIT_PATH];
With
CURRENT_DIR = [[[[NSBundle mainBundle] bundlePath] stringByDeletingPathExtension] stringByDeletingLastPathComponent];
To get the correct path for the file, but it works now.

Launching NSTask from popover

I'm having some trouble getting this NSTask to launch from an NSPopover view. The popover I created has several things in it. First the user selects a path using NSOpenPanel, then the user enters a username so that I can construct a path to transfer data to. Finally, there's a button that is supposed to launch the NSTask that initiates the data transfer. I had to add some extra code to make the text field work for user input. See the following link for what I followed to get the text field to work: NSStatusItem with NSPopover and NSTextField
Any reason why this wouldn't work? This code had been working when it was contained in a NSWindow.
- (IBAction)beginTransfer:(id)sender {
//Construct a string of the user selected source path.
NSString *userAccountPath = [_sourcePathTextField stringValue];
//Check that I got the expected result
NSLog (#"%#", userAccountPath);
//Create an array with the pieces I need to construct a destination account path
NSArray *strings = [NSArray arrayWithObjects:#"/Users/", [userAccountTextField stringValue], nil];
//Construct a string destination account location from the pieces
NSString *userAccountDestinationPath = [strings componentsJoinedByString:#""];
//Make sure the construction is correct
NSLog (#"%#", userAccountDestinationPath);
//Setup NSTask
NSTask *transferFiles;
transferFiles = [[NSTask alloc] init];
//Arguements for rsync
NSArray *arguements;
arguements = [NSArray arrayWithObjects:#"--paH", #"--info=progress2", #"--human-readable", #"--exclude", #"/Library/Keychains", userAccountPath, userAccountDestinationPath, nil];
//Setup output pipe
NSPipe *transferOutput = [NSPipe pipe];
[transferFiles setStandardOutput:transferOutput];
//Launch rsync with arguements from above
[transferFiles setArguments:arguements];
[transferFiles setLaunchPath:#"/usr/local/bin/rsync"];
[transferFiles launch];
NSFileHandle *fh = [transferOutput fileHandleForReading];
[fh waitForDataInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receivedData:) name:#"NSFileHandleDataAvailableNotification" object:nil];
[transferFiles waitUntilExit];
[transferFiles release];
[fh release];
}

Handling error when using NSTask

As a learning project Im writing a simple gui for Apache stresstesting command line tool "ab". It requiers a full URL, including a filename such as index.html or simular, as one of its parameters. If a filename is not specified "ab" echos "Invalid url" and shows a lists of available flags.
I would like to catch this "error" and have tried using NSTasks standarderror output. Can´t really get it to work. Would this even classify as an error that would pipe to a standard error?
Besides validating the URL input before launching the NSTask, do you think I can prevent or rather catch this error?
My simple code:
- (void) stressTest:(NSString *)url withNumberOfRequests:(int)requests sendSimultaneously:(int)connections {
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *abPath = [[mainBundle bundlePath] stringByAppendingString:#"/Contents/Resources/ab"];
NSString* requestsStr = [NSString stringWithFormat:#"%i", requests];
NSString* connectionsStr = [NSString stringWithFormat:#"%i", connections];
// Init objects for tasks and pipe
NSTask *abCmd = [NSTask new];
NSPipe *outputPipe = [NSPipe pipe];
[abCmd setLaunchPath:abPath];
[abCmd setArguments:[NSArray arrayWithObjects:#"-n", requestsStr, #"-c", connectionsStr, url, nil]];
[abCmd setStandardOutput:outputPipe];
[abCmd launch];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readCompleted:) name:NSFileHandleReadToEndOfFileCompletionNotification object:[outputPipe fileHandleForReading]];
[[outputPipe fileHandleForReading] readToEndOfFileInBackgroundAndNotify];
}
- (void)readCompleted:(NSNotification *)notification {
NSString * tempString = [[NSString alloc] initWithData:[[notification userInfo] objectForKey:NSFileHandleNotificationDataItem] encoding:NSASCIIStringEncoding];
[resultTextOutlet setString:tempString];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:[notification object]];
}
ab writes its error messages, including usage information, to standard error. You're currently only reading from standard output. To access the error messages or usage information you'll need to allocate a second NSPipe, pass it to -[NSTask setStandardError:], and then read data from it.

Clang NSTask with streams

Never-mind all the "why?","useless?", and "don't bother" comments. I want to compile a program inside another program using clang. I can create the NSTask and set up the arguments and it will work if the file exists, (ie. no stream), and writes to a physical file. I haven't been able to get what I would really like which is to use streams for both input and output. I know that both clang and gcc allow for compiling stdin if you use the -xc and - options but am unable to implement that feature using pipes. I am also not sure how to redirect clang's output to a file handle or stream.
Here is the code I have that compiles it and generates the correct output in outfile
task = [[NSTask alloc] init];
NSPipe* outputPipe = [[NSPipe alloc] init];
[task setStandardOutput:outputPipe ];
[task setStandardError: [task standardOutput]];
NSPipe* inPipe = [NSPipe pipe];
[task setStandardInput:inPipe];
[task setLaunchPath:#"/usr/bin/clang"];
NSString* outfile= [NSString stringWithFormat:#"%#.out",[[filename lastPathComponent] stringByDeletingPathExtension]];
//[data writeToFile:#"file.c" atomically:YES];
[task setArguments:[NSArray arrayWithObjects:filename,#"-S",#"-o",outfile,nil]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(getData:)
name: NSFileHandleReadCompletionNotification
object: [[task standardOutput] fileHandleForReading]];
[[[task standardOutput] fileHandleForReading] readInBackgroundAndNotify];
[task launch];
I have tried using this for the input stream:
/* on pipe creation*/
dup2([[inPipe fileHandleForReading] fileDescriptor], STDIN_FILENO);
NSFileHandle* curInputHandle = [inPipe fileHandleForWriting];
/* tried before launch and after, no output just sits */
[curInputHandle writeData:[NSData dataWithContentsOfFile:filename]];
Sometimes, I assume when the pipe closes while the NSTask still in existance the output file is created and will run. This makes me think that clang is just waiting for stdin to close. Is there a way to close the pipe when the data has been read?
For output I have tried to use NSPipe's fileHandleForWriting as the parameter of -o, That gives an error of [NSConcretePipe fileSystemRepresentation] unrecognized selector. I have tried creating a file handle with the file descriptor of stdout to the same error. I don't know of any command line argument that redirects it. I've tried using | to redirect but haven't been able to get it to work. If there is any unix magic to redirect it I can dup stdout to anywhere I want.
So is there any way to close a pipe when all the data it is read? And Redirect clangs output? If there is any other way to accomplish the same thing easier or cleaner I am open to any implementation.
Any help on these two items would be so great.
It is not clear to me what your problem is or what you've tried. However, if you are going to read the output from a pipe on your main thread using notifications and wish to also write to a pipe one option is to write to the pipe in another thread. The code below, based on your code, does this using GCD. For simplicity in this example the binary is deposited in /tmp:
// send a simple program to clang using a GCD task
- (void)provideStdin:(NSFileHandle *)stdinHandle
{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
[stdinHandle writeData:[#"int main(int argc, char **argv)\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle writeData:[#"{\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle writeData:[#" write(1, \"hello\\n\", 6);\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle writeData:[#"}\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle closeFile]; // sent the code, close the file (pipe in this case)
});
}
// read the output from clang and dump to console
- (void) getData:(NSNotification *)notifcation
{
NSData *dataRead = [[notifcation userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *textRead = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding];
NSLog(#"read %3ld: %#", (long)[textRead length], textRead);
}
// invoke clang using an NSTask, reading output via notifications
// and providing input via an async GCD task
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSTask *task = [NSTask new];
NSPipe *outputPipe = [NSPipe new];
[task setStandardOutput:outputPipe];
[task setStandardError:outputPipe];
NSFileHandle *outputHandle = [outputPipe fileHandleForReading];
NSPipe* inPipe = [NSPipe pipe];
[task setStandardInput:inPipe];
[task setLaunchPath:#"/usr/bin/clang"];
[task setArguments:[NSArray arrayWithObjects:#"-o", #"/tmp/clang.out", #"-xc",#"-",nil]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(getData:)
name:NSFileHandleReadCompletionNotification
object:outputHandle];
[outputHandle readInBackgroundAndNotify];
[task launch];
[self provideStdin:[inPipe fileHandleForWriting]];
}

NSTask or AScript?

I am new to objetive-c as I came from Java (Android) and then a little of AppleScript.
So my app is making a git commit. BUT as you know the terminal has output that the user may want to see. So should I continue to use my NSTask for behind the scenes stuff, or should I just use AppleScript and let the user carry on from the terminal. Mainly my push.m looks like this :
#import "Push.h"
#implementation Push
#synthesize test;
#synthesize dirPath;
-(IBAction)chooseFolder:(id)sender{
dirPath = [self get];
NSArray *array = [dirPath componentsSeparatedByString:#"/"];
NSString *pub = [array lastObject];
[projectName setStringValue:pub];
BOOL fileyn = [self check:dirPath];
if(fileyn) {
} else {
}
}
-(IBAction)pushAction:(id)sender {
[self push];
[self push];
}
-(void)push{
if(dirPath == nil || dirPath == #"") {
[self chooseFolder:nil];
}
NSString *commitText = [commitMessage stringValue];
[commitMessage setStringValue:#""];
commitText = [NSString stringWithFormat:#"%#",commitText];
if (i == 1) {
} else {
[self runScript:dirPath:#"add" :#"*" :nil];
[self runScript:dirPath:#"commit" :#"-m" :commitText];
[self runScript:dirPath:#"push" :#"origin" :#"HEAD"];
}
}
-(void) runScript:(NSString *) path:(NSString* )cmd1:(NSString *) cmd2:(NSString *) cmd3{
NSTask *aTask = [[NSTask alloc] init];
NSPipe *pipe;
pipe = [NSPipe pipe];
[aTask setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
NSArray* args = [NSArray arrayWithObjects:cmd1,cmd2,cmd3, nil];
[aTask setArguments:args];
[aTask setCurrentDirectoryPath:path];
[aTask setLaunchPath:#"/usr/local/git/bin/git"];
[aTask setArguments:args];
[aTask launch];
[finished setStringValue:#"finished"];
}
-(IBAction)back:(id)sender{
test = [[NSWindowController alloc] initWithWindowNibName:#"POP"];
[test showWindow:self];
[window close];
}
-(BOOL)check:(NSString *) pow{
BOOL isFile = [[NSFileManager defaultManager] fileExistsAtPath:pow isDirectory:NO];
return isFile;
}
-(NSString *)get {
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
if ([panel runModal] != NSFileHandlingPanelOKButton) return nil;
return [[panel directoryURL] path];
}
#end
So what should I do? Any thing to improve? Thanks in advance!
This sort of application design is really up to you; there's no right answer here. It depends on what you're trying to accomplish, and what you'd like to allow the user to do. If you simply want to show users the output of whatever commands you're running, you can simply get the output out of the NSTask (using the output pipe you set) and display it in a text view for the user to look through.
However, if you want to run interactive git commands, that gets a little bit more complicated. I'm going on a limb here since your question wasn't very specific about what 'let the user carry on from the terminal' means, so if this isn't what you meant, then let me know. In terms of good user interface and user experience, unless you have no other choice, it's almost never a good idea to force someone to go to a different app to keep using your own. If an app wants to display results to a user, it's best to do it in-app (with a custom view, or a web view, for instance), not to drive the user elsewhere. If you want the commands to be interactive, its far better design to come up with an interface for handling that in your app versus running an AppleScript and directing users to the terminal to see and do things.
Anyway, at a quick glance, your code seems like it should work (except for the aforementioned problems in the comments, which you should fix) — and since you hadn't mentioned any problems with it, I assume it does. This isn't a programming problem; it's a design one, and one you'll have to consider the answers to it yourself (and, that being said, I'm voting to close this question since it doesn't really fit into the StackOverflow guidelines — the answer is a matter of opinion, not of facts).