Issue launching X11 app via NSTask - objective-c

My app uses NSTask to execute shell scripts, one of those scripts launches an X11 app (specifically meld).
I would have expected this to work:
#!/bin/bash
source ~/.profile # setup $PATH/etc
meld .
But it fails with:
gtk.icon_theme_get_default().append_search_path(meld.paths.icon_dir())
Traceback (most recent call last):
File "/usr/local/bin/meld", line 132, in <module>
gtk.icon_theme_get_default().append_search_path(meld.paths.icon_dir())
AttributeError: 'NoneType' object has no attribute 'append_search_path'
As a proof of concept I changed the script to this, which works perfectly:
#!/usr/bin/ruby
exec 'osascript -e \'tell app "Terminal" to do script "meld ' + Dir.pwd + '" in front window\''
Does anyone what is causing the problem? Here is my code for executing the shell script:
NSTask *task = [[NSTask alloc] init];
task.launchPath = self.scriptURL.path;
task.standardOutput = [NSPipe pipe];
task.currentDirectoryPath = workingDirectoryURL.path;
[task launch];

X11 apps learn the display server's address using the DISPLAY environment variable.
On OS X, the DISPLAY value is randomized for security reasons, so you can't hard-code it, as you noticed. Instead, there is a launchd agent that tells launchd to set DISPLAY when it starts a process.
Somewhere between launchd and meld, the value of DISPLAY is being dropped or overwritten. Your job is to find out where.
Make sure the launchd agent is running. Run launchctl list and look for org.macosforge.xquartz.startx. Since meld is working from Terminal.app this part is probably correct.
Make sure DISPLAY is set in your app. It should be listed in [NSProcessInfo processInfo].environment.
Make sure DISPLAY is set in the NSTask you launch. Try running /usr/bin/env with an NSTask, and make sure DISPLAY appears in its output.
Make sure DISPLAY is set inside the script you run. Try echo $DISPLAY before and after sourcing ~/.profile. (Sometimes the .profile itself overwrites DISPLAY to an incorrect value because that's sometimes the right thing to do on other operating systems.)
EDIT: this is the code I used to copy DISPLAY from my GUI app to the shell script:
NSTask *task = ...
task.environment = [NSProcessInfo processInfo].environment;
[task.launch];

Change your script to spew the environment variables and compare "working" to "not-working".
There are subtle differences between how shells are initialized across the different execution models. Almost assuredly, the environment is the source of your issues.

Take a look at man launchctl
launchctl submit ... -p /usr/local/bin/meld -- .
to have launchd launch the job; that will set certain environment variables for you that are part of the per-user session.

Related

Can't run Apple script from command-line application

I created a command-line application for Mac OS without the GUI. This application is located at /usr/local/bin. And in some cases I need to execute Apple Script within that application. To do this, I create an NSTask and trying to run the following command:
NSTask *createTask = [[NSTask alloc] init];
createTask.launchPath = #"/bin/bash";
NSString *showAlert = [NSString stringWithFormat:#"/usr/bin/osascript -e 'tell application \"Finder\" to display alert \"My text.\"'"];
NSArray *arguments = [NSArray arrayWithObjects:#"-c",showAlert, nil];
createTask.arguments = arguments;
[createTask launch];
After it runs, nothing happens, only in logs appear the message:
Apr 14 15:35:15 Mac-mini kernel[0]: my-app: guarded fd exception: fd 0 code 0x2 guard 0x7fff8b9e12a8
Apr 14 15:35:15 Mac-mini com.apple.xpc.launchd[1] (com.apple.ReportCrash.Root[26852]): Endpoint has been activated through legacy launch(3) APIs. Please switch to XPC or bootstrap_check_in(): com.apple.ReportCrash.DirectoryService
Apr 14 15:35:15 Mac-mini diagnosticd[16097]: error evaluating process info - pid: 26851, punique: 26851
Apr 14 15:35:16 Mac-mini sandboxd[16099] ([26851]): my-app(26851) deny file-read-data /
But if you run this command directly from terminal, it is executed correctly. Please tell me, what am I doing wrong?
I think the issue may be with your use of quotes. When I try and manually run the same command in the shell with your quoting style, it won't work. My examples below do work. Can you switch your single and double quotes? Encase your initial call with single quotes and then use the double quote around the osascript? Also, there is no need to use the tell application \"Finder\" to as display alert is not part of the Finder dictionary.
You have...
/usr/bin/osascript -e 'tell application \"Finder\" to display alert \"My text.\"'
Try changing it to...
/usr/bin/osascript -e "display alert \"My text.\""
Or an even simpler version...
osascript -e "display alert \"My text.\""

Different $PATH from Console and Cocoa?

OK, I must be missing something very simple but here's what:
If I echo $PATH in the terminal, I'm getting /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/drkameleon which is correct
Now if I run an NSTask and try executing a simple bash script (/usr/bin/env bash myscript.sh) to echo $PATH, it prints /Applications/Xcode.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin
(I've even tried with getenv, or print the entire [[NSProcessInfo processInfo] environment] dictionary, but the PATH variable is simply... wrong)
What's going on? How can I have access to the real $PATH as seen in the terminal?
When executing a command via NSTask, not your bash, zshell or whatever kind of shell you are using, is started. Hence the PATH (and other environment variables) are different from your environment variables when echoing them in the terminal.
Reason: NSTask uses fork() and exec() for command execution.
IMHO, there are two possible solutions for the problem.
1. You could set the wanted PATH via the setEnvironment:method of NSTask
Here is some untested example code, which should do the trick:
NSTask *task = //Configure your task
NSDictionary* env = [task environment];
NSString* currentPATH = env["PATH"];
NSString* yourPathExtension = #"/your/path";
env["PATH"] = (currentPATH != nil) ? [yourPath stringByAppendingFormat:#":%#", currentPATH] : yourPathExtension;
[task setEnvironment:env];
This adds :<old-PATH> after the current PATH from NSTask.
2. You could start /bin/bash within NSTask
task setLaunchPath:#"/bin/bash"];
NSArray *args = [NSArray arrayWithObjects:#"-l",
#"-c",
#"<your command here>",
nil];
[task setArguments: args];
This task will start /bin/bash with the PATH configured via ~/.bashrc etc. and execute the command within the bash.
Pro: Your command has all the usual environment variables of your bash
Con: You rely on the users PATH variable which can be quite different from yours which may lead to unexpected behavior.
Which solution is the best one for you depends on your use case. In your provided example, both approaches should work fine.
In general, according to the NSTask and NSProcessInfo documentation, the environment variables are equal to the variables of the process from which the application has been started. Hence you might solve your problem when starting your application from the bash.

Unable to find running application in Cocoa

I am getting the list of running applications in Cocoa with the following code:
for (NSRunningApplication *app in [[NSWorkspace sharedWorkspace] runningApplications]) {
MNSLog(#"%#",[app localizedName]);
}
However an application I started from a terminal session is not appearing in the list ('Terminal' is well appearing). The application was started from the same user which is executing the cocoa code.Is my launched application under Terminal ? And in such a case how can I find its name and arguments ?Running ps in another terminal session show my process properly.
Use an NSTask to execute the ps Shell command. You can check the ps man page to determine which arguments you want to pass it based on the information you want to get back. Use NSPipe and NSFileHandle to get the results from the task.
If you want to do some filtering you can pipe the ps output through grep before your app picks up the result.
For your first question, I think NSWorkspace can only see apps that use the window server so you will only see Terminal, not the executables that it is running internally.
You can use sysctl or ps command to get a list of all BSD processes. Have look at unable to detect application running with another user

Problems with NsTask to execute terminal command

Normally in the terminal I would execute this command to communicate with a serial USB device.
echo -e '\xFF\x01\x01' > /dev/cu.usbserial-A8003YzT
I'm trying to do this from within a cocoa app using NStask, but I'm getting no love for some reason.
Heres my code:
- (IBAction) doCommand:(id)sender{
{
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/echo"];
[task setArguments:
[NSArray arrayWithObjects:#"-e '\\xFF\\x01\\x01' > /dev/cu.usbserial-A8003YzT", nil]];
[task launch];
[task release];
}}
I know the code is essentially working, as I've executed other terminal commands with the same script.....not sure why I can't get the echo to fire....perhaps I'm missing somthing simple?
Thanks for any help
You're trying to send '\xFF\x01\x01' > /dev/cu.usbserial-A8003YzT as the first argument to echo, but that's not what happens when you run that command from a shell prompt. Only the first, quoted part is sent as an argument to echo. Bash interprets the > itself, captures the output from the echo command, and redirects it to the indicated file - in this case, a file that represents a usb device.
If you want to run an NSTask that will interpret a shell command with redirects, pipes, and such, you'll need to use /bin/sh as the launch path, and -c, your shell command as arguments.
Alternatively, you could skip the NSTask altogether, and simply open an NSFileHandle to the device file, then send the three-byte sequence you want to send it. Echo is handy for interactive debugging stuff like this in a terminal, but launching an external task in your app, just to write three bytes to a file, is pretty drastic overkill... :-)

NSTask and Git -- Permissions issues

In my Cocoa application I'm trying to use NSTask to run some basic Git commands. Whenever I run a command that requires permissions (SSH keys) to access a remote (e.g. git push, git pull), it fails with the following error:
Permission denied (publickey). The remote end hung up unexpectedly
Running the same commands from Terminal works just fine, so I'm thinking that this might be an issue with NSTask not setting an environment variable that would be used somewhere in the process of accessing the ssh keys. I tried manually setting the HOME and USER environment variables like this:
[task setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:NSHomeDirectory(), #"HOME", NSUserName(), #"USER", nil]];
But this has no effect. Is there any particular environment variable I have to set in NSTask for this to work properly?
EDIT: Thanks to Dustin's tip, I got a little bit further in figuring this out. I used the env command to list the environment variables for my current session and I found this:
SSH_AUTH_SOCK=/tmp/launch-DMQopt/Listeners
To test, I copied that path and set it as an environment variable of NSTask and ran the code again, and this time it worked! That said, I'm certain that SSH_AUTH_SOCK changes for each session so I can't just hardcode it. How do I dynamically generate/retrieve this variable?
You could try and follow the tutorial "Wrapping rsync or SSH in an NSTask" (from Ira), which does mention SSH_AUTH_SOCK variable:
Since writing this post I've realised that I omitted an important additional step in setting up the environment variables for the NSTask.
In order to make passwordless key-based authentication work it's necessary to grab the SSH_AUTH_SOCK variable from the user's environment and include this in the NSTask's environment.
So, when setting environment variables for example;
NSTask *task;
NSDictionary *environmentDict = [[NSProcessInfo processInfo] environment];
// Environment variables needed for password based authentication
NSMutableDictionary *env = [NSMutableDictionary dictionaryWithObjectsAndKeys:
#"NONE", #"DISPLAY", askPassPath, #"SSH_ASKPASS",
userName,#"AUTH_USERNAME",
hostName,#"AUTH_HOSTNAME",
nil];
// Environment variable needed for key based authentication
[env setObject:[environmentDict objectForKey:#"SSH_AUTH_SOCK"] forKey:#"SSH_AUTH_SOCK"];
// Setting the task's environment
[task setEnvironment:env];
However, the OP indragie comments:
I had tried this earlier but since it was being invoked with XCode, the SSH_AUTH_SOCK env var. wasn't being passed to it.
Opening the app from Finder corrects this issue.
With askPassPath being the path for the Askpass executable, which is included as part of the application’s main bundle. (In order to do this, find the executable under “Products” in xcode, and then drag it into “Copy Bundle Resources” on the main application’s target.)
// Get the path of the Askpass program, which is
// setup to be included as part of the main application bundle
NSString *askPassPath = [NSBundle pathForResource:#"Askpass"
ofType:#""
inDirectory:[[NSBundle mainBundle] bundlePath]];