Using NSPipe with NSTask to simulate command line input - macos

I'm writing a very simple GUI tool for a command line app. It only has 2 buttons. Connect and Quit.
In applicationDidFinishLaunching I run the following
NSPipe *pipe = [[NSPipe alloc] init];
writer = [pipe fileHandleForWriting];
NSTask *runTask = [[[NSTask alloc] init] autorelease];
NSString *exefile = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent:#"vpngui"];
[runTask setLaunchPath: exefile];
NSString *exeDir = [[NSBundle mainBundle] resourcePath];
NSArray *pargs;
pargs = [NSArray arrayWithObjects: exeDir, nil];
[runTask setArguments: pargs];
[runTask setStandardInput:pipe];
[runTask launch];
Then when the Connect button is clicked the following line of code is run
writer writeData:[#"Connect" dataUsingEncoding:NSUTF8StringEncoding]];
and for the Quit button
writer writeData:[#"Quit" dataUsingEncoding:NSUTF8StringEncoding]];
Somehow the command line app never gets the Connect and Quit commands

The data is probably buffered.
Try forcing to flush the file handle by calling the synchronizeFile: method:
synchronizeFile
Causes all in-memory data and attributes of the file represented by the receiver to be written to permanent storage.

I meet the same problem and
closeFile: works for me.
closeFile: to indicate that you have finished writing.
But next time you want to write you'd have to create a new fileHandle instance because the old one has been closed.

Related

Xcode 8 extension executing NSTask

My goal is to create an extension that executes clang-format. My code looks something like this:
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler
{
NSError *error = nil;
NSURL *executableURL = [[self class] executableURL];
if (!executableURL)
{
NSString *errorDescription = [NSString stringWithFormat:#"Failed to find clang-format. Ensure it is installed at any of these locations\n%#", [[self class] clangFormatUrls]];
completionHandler([NSError errorWithDomain:SourceEditorCommandErrorDomain
code:1
userInfo:#{NSLocalizedDescriptionKey: errorDescription}]);
return;
}
NSMutableArray *args = [NSMutableArray array];
[args addObject:#"-style=LLVM"];
[args addObject:#"someFile.m"];
NSPipe *outputPipe = [NSPipe pipe];
NSPipe *errorPipe = [NSPipe pipe];
NSTask *task = [[NSTask alloc] init];
task.launchPath = executableURL.path;
task.arguments = args;
task.standardOutput = outputPipe;
task.standardError = errorPipe;
#try
{
[task launch];
}
#catch (NSException *exception)
{
completionHandler([NSError errorWithDomain:SourceEditorCommandErrorDomain
code:2
userInfo:#{NSLocalizedDescriptionKey: [NSString stringWithFormat:#"Failed to run clang-format: %#", exception.reason]}]);
return;
}
[task waitUntilExit];
NSString *output = [[NSString alloc] initWithData:[[outputPipe fileHandleForReading] readDataToEndOfFile]
encoding:NSUTF8StringEncoding];
NSString *errorOutput = [[NSString alloc] initWithData:[[errorPipe fileHandleForReading] readDataToEndOfFile]
encoding:NSUTF8StringEncoding];
[[outputPipe fileHandleForReading] closeFile];
[[errorPipe fileHandleForReading] closeFile];
int status = [task terminationStatus];
if (status == 0)
{
NSLog(#"Success: %#", output);
}
else
{
error = [NSError errorWithDomain:SourceEditorCommandErrorDomain
code:3
userInfo:#{NSLocalizedDescriptionKey: errorOutput}];
}
completionHandler(error);
}
The reason I need that try-catch block is because an exception is thrown when I try to run this code. The exception reason is:
Error: launch path not accessible
The path for my clang-format is /usr/local/bin/clang-format. What I discovered is that it doesn't like me trying to access an application in /usr/local/bin, but /bin is ok (e.g. If I try to execute /bin/ls there is no problem).
Another solution I tried was to run /bin/bash by setting the launch path and arguments like this:
task.launchPath = [[[NSProcessInfo processInfo] environment] objectForKey:#"SHELL"];
task.arguments = #[#"-l", #"-c", #"/usr/local/bin/clang-format -style=LLVM someFile.m"];
This successfully launches the task, but it fails with the following error output:
/bin/bash: /etc/profile: Operation not permitted
/bin/bash: /usr/local/bin/clang-format: Operation not permitted
The first error message is due to trying to call the -l parameter in bash, which tries to log in as the user.
Any idea how I can enable access to those other folders? Is there some kind of sandbox environment setting I need to enable?
I guess that because of the sandboxing this is not possible.
You could bundle the clang-format executable and use it from there.
Personally, I think you are going at it all wrong. Extensions are supposed to be quick (if you watch the video on Xcode extensions he repeats multiple times to get in and get out). And they are severely limited.
However, there is another - the container app may be able to do this processing for your extension without all the hacks. The downside is that you have to pass the buffer to and from the extension.
It’s not easy, but it can be done. Easy peasy way to get your container to run. First, modify the container app’s Info.plist (not the extension Info.plist) so that it has a URL type.
In your extension you can “wake up” the container app by running the following:
let customurl = NSURL.init(string: “yoururlschemehere://")
NSWorkspace.shared().open(customurl as! URL)
As for communication between the two, Apple has a plethora of methods. Me, I’m old-school, so I’m using DistributedNotificationCenter - for the moment.
Although I haven’t tried it, I do not see why the container app should have an issue chatting with clang (I’m using the container app for settings).

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.

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.

Sending an E-Mail with attachments in Cocoa

I have an NSTextView with text & images in it, which is supposed to send both in an e-mail.I know that the message.framework is deprecated,so I came up with the idea to send it via NSTask, since mail is integrated.I came up with the code below, however in the log I get this:
*** -[NSCFDictionary setObject:forKey:]: attempt to insert
nil value (key:
_NSTaskInputFileHandle)
This is the code I am using:
NSError *error;
if([textView writeRTFDToFile:#"/Library/Application Support/log.rtfd" atomically:NO])
{
NSArray *args = [NSArray arrayWithObjects:#"-s", [subject stringValue], [sendto stringValue], nil];
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath:#"/usr/bin/mailx"];
[task setArguments:args];
[task setStandardInput:[NSFileHandle fileHandleForReadingAtPath:#"/Library/Application Support/log.rtfd"]];
[task launch];
[task waitUntilExit];
Can someone tell me what I am doing wrong?
You can also try the Scripting Bridge. See Apple's SBSendEmail example.

Resources