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).
Related
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.
I have a C function that prints to stdout using fprintf, and I'm attempting to display the contents of stdout in a UIAlertView. My code is as follows:
NSFileHandle *stdoutFileHandle = [NSFileHandle fileHandleWithStandardOutput];
NSData *stdoutData = [stdoutFileHandle availableData];
NSString *stdoutString = [[NSString alloc] initWithData:stdoutData encoding:NSASCIIStringEncoding];
UIAlertView *stdoutAlert = [[UIAlertView alloc] initWithTitle:#"STDOUT OUTPUT" message:stdoutString delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[stdoutAlert show];
I'm getting the following error when I run my code.
Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '[NSConcreteFileHandle availableData]: Bad file descriptor'
I get an equivalent error when I replace [stdoutFileHandle availableData] with [stdoutFileHandle readDataToEndOfFile].
The problem is you are reading from a output stream. To make this work you need to trick stdout to write it's contents to an input stream instead of to the console.
I know the old C way to do this, but you're not gonna like it. This uses pipe() and dup2().
int pipefd[2];
pipe(pipefd);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
At this point anything written to stdout can be read by pipefd[0]. At that point you can use -initWithFileDescriptor: to read from pipefd[0].
NSFileHandle *stdoutReader = [[NSFileHandle alloc] initWithFileDescriptor:pipefd[0]];
Note, you will want to do lots of error checking. Hope that helps.
I followed the selected answer post in this question:
What is the best way to redirect stdout to NSTextView in Cocoa?
It felt a bit more familiar to follow for me. I created an NSPipe, and NSFileHandler object for the pipe and read handler and I used notifications. I put the open method below in the viewDidAppear and viewDidDisappear method because of my needs but you can put it wherever it's appropriate
// Piping stdout info from here WILL NOT PIPE NSLOG:
// https://stackoverflow.com/questions/2406204/what-is-the-best-way-to-redirect-stdout-to-nstextview-in-cocoa
- (void) openConsolePipe {
_pipe = [NSPipe pipe];
_pipeReadHandle = [_pipe fileHandleForReading] ;
dup2([[_pipe fileHandleForWriting] fileDescriptor], fileno(stdout)) ;
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(handleNotification:) name: NSFileHandleReadCompletionNotification object: _pipeReadHandle] ;
[_pipeReadHandle readInBackgroundAndNotify] ;
}
- (void) closeConsolePipe {
if (_pipe != nil) {
[[_pipe fileHandleForWriting] closeFile];
// documentation suggests don't need to close reading file handle b/c auto but this suggests otherwise:
// https://stackoverflow.com/questions/13747232/using-nstask-and-nspipe-causes-100-cpu-usage
// [[_pipe fileHandleForReading] closeFile];
}
}
- (void) handleNotification:(NSNotification *)notification {
[_pipeReadHandle readInBackgroundAndNotify] ;
NSString *str = [[NSString alloc] initWithData: [[notification userInfo] objectForKey: NSFileHandleNotificationDataItem] encoding: NSUTF8StringEncoding];
// do what you want with the str here.
[_consoleView setText:[_consoleView.text stringByAppendingString:str]];
[_consoleView scrollRangeToVisible:NSMakeRange([_consoleView.text length], 0)];
}
I hope this ends up helping someone else googling this...
I have the following code (below) in my app, which simply tries to use NSTask to touch one file with the directory time/date stamp. It works just fine in an app which only accesses one directory all the time, however, it doesn't with another that frequently changes directories to access some of it's data. When I check the currentfile and currentpath they both show the correct paths. I've expired every possibility I can think of; any help would be great appreciated — thank you.
- (void)someMethod:(NSString *)currentfile {
NSFileManager *filemanager = [[NSFileManager alloc] init];
if ([filemanager changeCurrentDirectoryPath: #"/"] == NO)
NSLog (#"Cannot change directory.\n");
NSString *currentpath = [filemanager currentDirectoryPath];
NSLog (#"Current directory is %#", currentpath);
[filemanager release];
NSArray*arguments = [NSArray arrayWithObjects:#"-r",currentpath,currentfile,nil];
[self touchFiles:arguments];
return;
}
- (void)touchFiles:(NSArray *)arguments {
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/touch"];
[task setArguments:arguments];
[task launch];
[task release];
return;
}
You can change a file's modification time without using NSTask. Use -[NSURL setResourceValue:forKey:error:] with the key NSURLContentModificationDateKey.
As to why your use of NSTask and touch is failing, perhaps you don't have permissions to modify the file's modification time. Check the console log to see if any error was reported from touch or redirect the task's standard error output to someplace else and check that.
ECHOAppDelegate.m:
- (void)charlieInputTextHandler:(NSString *)theMessage {
if (jarvisSecondTimeCheck1 == TRUE) {
NSRunAlertPanel(#"ECHO", theMessage, #"", #"", #"");
NSData *sendData1 = [theMessage dataUsingEncoding:NSUTF8StringEncoding];
[[inputPipe1 fileHandleForWriting] writeData:sendData1];
NSData *sendReturn1 = [#"\r" dataUsingEncoding:NSUTF8StringEncoding];
[[inputPipe1 fileHandleForWriting] writeData:sendReturn1];
[ContentsTextField1 insertText:theMessage];
[ContentsTextField1 insertText:#"\r"];
} else {
NSRunAlertPanel(#"ECHO", #"The task is not running; therefore, you cannot send DATA to JARVIS.", #"", #"", #"");
}
}
ChatController.m:
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
ECHOAppDelegate *echo = [[ECHOAppDelegate alloc] init];
[echo charlieInputTextHandler:[message stringValue]];
if(![jid isEqual:[message from]]) return;
if([message isChatMessageWithBody])
{
NSString *messageStr = [[message elementForName:#"body"] stringValue];
NSString *paragraph = [NSString stringWithFormat:#"%#\n\n", messageStr];
NSMutableParagraphStyle *mps = [[[NSMutableParagraphStyle alloc] init] autorelease];
[mps setAlignment:NSLeftTextAlignment];
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:2];
[attributes setObject:mps forKey:NSParagraphStyleAttributeName];
[attributes setObject:[NSColor colorWithCalibratedRed:250 green:250 blue:250 alpha:1] forKey:NSBackgroundColorAttributeName];
NSAttributedString *as = [[NSAttributedString alloc] initWithString:paragraph attributes:attributes];
[as autorelease];
[[messageView textStorage] appendAttributedString:as];
}
}
Ok, for some reason jarvisSecondTimeCheck1 (a bool global variable) returns FALSE even though that I know 900% that it's true because I clarified that in applicationDidFinishLaunching.
And the other part of the code:
NSData *sendData1 = [theMessage dataUsingEncoding:NSUTF8StringEncoding];
[[inputPipe1 fileHandleForWriting] writeData:sendData1];
NSData *sendReturn1 = [#"\r" dataUsingEncoding:NSUTF8StringEncoding];
[[inputPipe1 fileHandleForWriting] writeData:sendReturn1];
[ContentsTextField1 insertText:theMessage];
[ContentsTextField1 insertText:#"\r"];
Does not work either. But again, I know this works. Is it because I'm triggering charlieInputTextHandler from another delegate?
Thanks!
You're missing a hell of a lot of relevant code. At a rough guess, I'd say this is a big clue as to what's going wrong:
ECHOAppDelegate *echo = [[ECHOAppDelegate alloc] init];
You shouldn't ever need to instantiate your app delegate more than once. I'd expect something like the following instead:
ECHOAppDelegate *echo = (ECHOAppDelegate *)[UIApplication sharedApplication].delegate;
I'm assuming you're setting jarvisSecondTimeCheck1 on your original instance and expecting it to be set on any other instance you instantiate. This isn't how objects work. I strongly recommend reading the iOS Application Programming Guide section on the app delegate and Learning Objective-C: A Primer.
Sounds like one of your pointers is nil. Sending a message to a nil object will give you nil, which could explain why you're not seeing the results you expect. So check all your pointers and IBOutlet variables to ensure they are set correctly. And check any assumptions too!
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