I want to run some lines of applescript code from within my application. The standard way is to use the NSAppleScript class. However, because that code might take a few minutes to complete, I have to use a separate thread or the interface will stop. The big problem is, as it says here, the NSAppleScript class can ONLY be run on the main thread.
So, if I run the code on a separate thread, my app crashes; If I run it on the main thread, it stops. Any ideas?
Also, I considered using NSTask and the osascript command, but I saw somewhere (can't find the link) that osascript doesn't support user input such as dialog boxes and stuff. I'm not sure if that's true, but if it is then osascript is not a solution.
So I ended up writing a helper for this, launched via NSTask. Here is the code, in case anyone's interested:
For the launcher:
NSArray* args = [NSArray arrayWithObject: <<AS code here>>];
task = [NSTask new];
[task setLaunchPath: [[NSBundle mainBundle] pathForResource: #"ASHelper" ofType: #""]];
[task setArguments: args];
[task launch];
And the helper:
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSString* source = [NSString stringWithCString: argv[1] length: strlen(argv[1])];
NSAppleScript* as = [[NSAppleScript alloc] initWithSource: source];
[as executeAndReturnError: nil];
}
return 0;
}
Related
I have virtually no programing experience beyond shell scripting, but I'm in a situation where I need to call a binary with launchd and because of security changes in Catilina, it looks as though it cannot be a AppleScript .app, Automator .app, or a Platypus .app. All three of those call some sort of GUI element and none will work as at startup.
I've cobbled together a snippet of code that compiles and works to call the script I need. I've bundled it a .app, signed it, and it works. But I'd very much like to call the script relative to the binary.
How can I call the equivalent of
[task setArguments:#[ #"../Resources/script" ]];
instead of
[task setArguments:#[ #"/Full/Path/To/script" ]];
Below is the entirety of the main.m
#import <Foundation/Foundation.h>
enter code here
int main(int argc, const char * argv[]) {
#autoreleasepool {
// insert code here...
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/zsh"];
[task setArguments:#[ #"/Full/Path/To/script" ]];
[task launch];
}
return 0;
}
As an aside, I know there are much better ways to do this, but my goal os not to have an award winning app, but to simply bridge a specific problem. Many thanks.
You get the current directory with NSFileManager
NSString* path = [[NSFileManager defaultManager] currentDirectoryPath];
The first argument in argv is the path to the binary
Either
NSString *path = [NSString stringWithUTF8String:argv[0]];
or
NSString *path = [[NSProcessInfo processInfo] arguments][0];
You should use NSBundle for this. In particular, you should use:
[[NSBundle mainBundle] pathForResource:#"script" ofType:nil]
as in:
[task setArguments:#[ [[NSBundle mainBundle] pathForResource:#"script" ofType:nil] ]];
I am having endless problems checking to see if the screen saver is running. If I use an NSTask with ps, it crashes or hangs on a lot of users. If I use notifications it seems to be spotty for others.
Any ideas as to why this NSTask is flakey? (Yes, I know it's messy for now as I debug)
-(BOOL)checkScreenSaverRunning
{
MYLog(#"Building task to check screen saver running");
BOOL foundSaver=FALSE;
NSTask *task;
int i;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/ps"];
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"-ax", nil];
[task setArguments: arguments];
NSPipe *stdpipe;
stdpipe = [NSPipe pipe];
[task setStandardOutput: stdpipe];
NSFileHandle *stdfile;
stdfile = [stdpipe fileHandleForReading];
MYLog(#"Launching task to check screen saver running");
[task launch];
while ([task isRunning]){
NSData *stddata;
stddata = [stdfile readDataToEndOfFile];
if([stddata length]>0){
NSString *stdstring = [[NSString alloc] initWithData:stddata
encoding:NSUTF8StringEncoding];
NSArray *stdReturnValues=[stdstring componentsSeparatedByString:#"\n"];
for(i=0;i<[stdReturnValues count];i++){
if([[stdReturnValues objectAtIndex:i]
rangeOfString:#"ScreenSaverEngine"].location != NSNotFound){
foundSaver=TRUE;
MYLog(#"Found screensaver in running processes");
}
}
[stdstring release];
stdstring=nil;
}
}
MYLog(#"Task ended");
[task release];
if(foundSaver)screenSaverIsActive=TRUE;
else screenSaverIsActive=FALSE;
return(foundSaver);
}
What is your higher-level purpose for wanting to know if the screen saver is running? There may be a better way to accomplish that.
If you're trying to diagnose a crash or a hang, show the crash or hang report.
Anyway, if you're going to spawn a subprocess for this, you should probably use killall -0 ScreenSaverEngine instead of ps. killall will find a process by name for you. Using the signal 0 (-0) means "just test for process existence, don't actually signal it". Do [task setStandardError:[NSFileHandle fileHandleWithNullDevice]] to make sure its output goes nowhere. You determine if the process existed by examining the success or failure status of the task after it terminates.
I mounted the hard drive by using the GUI in Mac.
However, I want to mount the hard drive by using the terminal commands.
How can I execute a terminal command mount_smbfs from my Objective-C Cocoa application?
NSTask* task = [[NSTask alloc] init];
[task setLaunchPath:#"/sbin/mount_smbfs"];
[task setArguments:[NSArray arrayWithObjects:#"//user:50000#smb://192.168.2.1/Share",#"Volumes/C$/upload", nil]];
[task launch];
Here is my edited with my code Could you please help me?
You can wrap the call to mount_smbfs in NSTask to execute it from your Obj-C program:
NSTask* task = [NSTask new];
[task setLaunchPath:#"/sbin/mount_smbfs"];
[task setArguments:[NSArray arrayWithObjects:#"//myUser:myPassword#SERVER/share", #"mountPath", nil]];
In setArguments you provide an array with at least 2 elements: the path to the share, and the mount point.
Also check man mount_smbfs for more argument options.
It's simpler to do this with AppleScript:
- (BOOL) mount {
NSAppleScript *script = [[NSAppleScript alloc] initWithSource:
#"tell application \"Finder\"\n"
" mount volume \"smb://server.domain/SomeMountPoint\"\n"
"end tell"];
if (!script) {
NSLog(#"Error creating AppleScript object");
return NO;
}
NSDictionary *errorMessage = nil;
NSAppleEventDescriptor *result = [script executeAndReturnError:&errorMessage];
return (BOOL)result;
}
There are some limitations:
You have to use NSAppleScript on the main thread.
Your application won't respond to any events while Finder tries to mount the volume.
If mounting fails, you don't have any control over how Finder presents the error message.
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
I'm running into an issue with relaunching my application on 10.5. In my Info.plist I have LSMinimumSystemVersionByArchitecture set so that the application will run in 64-bit for x86_64 and 32-bit on i386, ppc, and ppc64.
I have a preference in the app that allows the user to switch between a Dock icon & NSStatusItem, and it prompts the user to relaunch the app once they change the setting using using the following code:
id fullPath = [[NSBundle mainBundle] executablePath];
NSArray *arg = [NSArray arrayWithObjects:nil];
[NSTask launchedTaskWithLaunchPath:fullPath arguments:arg];
[NSApp terminate:self];
When this executes on 10.5, however it relaunches the application in 64-bit which is not a desired result for me. From what I gather reading the docs its because the LS* keys are not read when the app is launched via command line.
Is there a way around this? I tried doing something like below, which worked on 10.6, but on 10.5 it was chirping at me that the "launch path not accessible". ([NSApp isOnSnowLeopardOrBetter] is a category that checks the AppKit version number).
id path = [[NSBundle mainBundle] executablePath];
NSString *fullPath = nil;
if (![NSApp isOnSnowLeopardOrBetter])
fullPath = [NSString stringWithFormat:#"/usr/bin/arch -i386 -ppc %#", path];
else
fullPath = path;
NSArray *arg = [NSArray arrayWithObjects:nil];
[NSTask launchedTaskWithLaunchPath:fullPath arguments:arg];
[NSApp terminate:self];
You should instead use the methods of NSWorkspace, which does take into account Info.plist keys. For example, use -(BOOL)launchApplication:(NSString*).
It's because you use spaces inside fullpath, use arguments within an array [NSArray arrayWithObjects:#"/usr/bin/arch",#"-i386",#"-ppc",path,nil].
try this code for relaunch your app:
//terminate your app in some of your method:
[[NSApplication sharedApplication]terminate:nil];
- (void)applicationWillTerminate:(NSNotification *)notification {
if (i need to restart my app) {
NSTask *task = [NSTask new];
[task setLaunchPath:#"/usr/bin/open"];
[task setArguments:[NSArray arrayWithObjects:#"/Applications/MyApp.app", nil]];
[task launch];
}
}