I'm trying to execute an apple script from my C++ application but I still no have success to make it stable.
My first approach was with Carbon API:
::OSACompileExecute(Component, &ScriptTextDesc, kOSANullScript, kOSAModeNull, &ResultID);
Where I created component every script execution and release it in the same scope. But I stuck with the problem: sometimes it crashes when trying to release Component. So it seems that some problem is occured in the script engine and it corrupts Component somehow. I didn't find any soultions so I switch to another but similar approach:
::OSACompileExecute(m_Component, &ScriptTextDesc, kOSANullScript, kOSAModeNull, &ResultID);
Where m_Component is the ComponentInstance which is created in ctor and lives during the application execution. So the crashes on releasing Component is gone due to the absence that release step while application is working. But there is another problem: when user tries to log out then pop-up is shown which says "MyApp prevents log out"(something like that) and MyApp closes. So when I close this pop-up I can log out. So it seems that the problem with Component releasing do something that tells System that it can't be closed. I didn't find any solution here so I decided to sitch to the 3rd approach:
Cocoa - NSAppleScript
The solution is very simple and small:
QString ExecuteAppleScript(const QString& strScriptText)
{
QString strResult;
NSAutoreleasePool* Pool = [[NSAutoreleasePool alloc] init];
NSString* ScriptText = [[NSString alloc] initWithBytes:strScriptText.toUtf8().data() length:strScriptText.size()
encoding:NSUTF8StringEncoding];
NSAppleScript* Script = [[NSAppleScript alloc] initWithSource:ScriptText];
if (Script)
{
NSDictionary* ErrorInfo = nil;
NSAppleEventDescriptor* Result = [Script executeAndReturnError:&ErrorInfo];
if (Result)
{
strResult = QString::fromUtf8([[Result stringValue] UTF8String]);
}
else if (ErrorInfo)
{
QString strErrorInfo = QString::fromUtf8([[ErrorInfo description] UTF8String]);
}
}
[ScriptText release];
[Pool release];
return strResult;
}
There are a bit of Qt classes here but they are not relevant. So it works but it crashes with different places inside the NSAppleScript guts, one of the logs:
Thread 5 Crashed:
0 com.apple.applescript 0x000000010ff0a4c9 ASCoerceToDesc(unsigned int, unsigned int, int, AEDesc*) + 84
1 ...ple.CoreServices.CarbonCore 0x00007fff86545e48 CallComponentFunction + 28
2 com.apple.applescript 0x000000010ff05cb1 AppleScriptComponent + 1728
3 com.apple.applescript 0x000000010ff1ebd0 AGenericCall::Delegate(ComponentInstanceRecord*) + 46
4 com.apple.applescript 0x000000010ff1e524 AGenericManager::HandleOSACall(ComponentParameters*) + 54
5 com.apple.applescript 0x000000010ff1e4b4 GenericComponent + 219
6 com.apple.openscripting 0x00007fff8716ae34 OSACoerceToDesc + 65
7 com.apple.Foundation 0x00007fff87c61a83 -[NSAppleScript(NSPrivate) _executeWithMode:andReturnError:] + 170
So for now I don't know what else I can do to overcome those AppleScript crashes...
Additional info:
AppleScript executes very often, at least 1 script per second.
Script is(just took it from the C++ code):
do shell script \"ioreg -c IOHIDSystem"
"| perl -ane 'if (/Idle/) {$idle=(pop #F)/1000000000; print $idle,\\"\\";last}'\"
AppleScipt is executed not from the Main thread. I heard about problems with NSAppleScript on MacOS X versions before 10.6.6 but I test it on 10.6.7. Anyway 1 and 2 approaches should not be bothered by threads.
Crashes are random. So it can work all the day or can crashe right after the start.
Could anyone help me with it?
Thank you in advance!
You mention that you're running NSApplescript from a different thread. If you look in the documentation for NSApplescript, right at the top it says this:
Important: You should access NSAppleScript only from the main thread.
So just know that it's not thread safe and that's probably the reason for your problems.
EDIT: I see a couple other issues in your code. First, you are not releasing "Script" which you have created using alloc/init. Next, and this is the main issue... your "ErrorInfo" is a dictionary with key/value pairs. You are not using the keys to get the values and that's where it looks like your crash is occuring. It's having trouble returning the "description" of the dictionary. Here's example code I use to create an NSString of that error dictionary, so do something like this.
NSString* errorInfo = [NSString stringWithFormat:#"Error:%# %#", [errorDict valueForKey:#"NSAppleScriptErrorNumber"], [errorDict valueForKey:#"NSAppleScriptErrorMessage"]];
Finally, are you using some method like "performSelectorOnMainThread" to ensure it's run on the main thread?
Related
I'd like to get properties of the currently active app. I understand that this should be possible with ScriptingBridge, however, this seems to require you generate an sdef file and import this in your project for the app you are trying to target. Since I want to target all apps, is there another way to do this?
Example of accessing system preferences:
SystemPreferencesApplication *systemPreferences =
[SBApplication
applicationWithBundleIdentifier:#"com.apple.systempreferences"];
If there's another way to access properties of any active app, please do share. (For example; window title)
Thanks.
I assume you want to run an applescript. The scripting bridge is good if you have a lot of applescript code to run. However if you only have a small amount then a simpler way is with NSApplescript.
For example if you wanted to run this applescript...
tell application "System Events"
set theProcesses to processes
repeat with aProcess in theProcesses
tell aProcess to get properties
end repeat
end tell
Then you can write it this way...
NSString* cmd = #"tell application \"System Events\"\nset theProcesses to processes\nrepeat with aProcess in theProcesses\ntell aProcess to get properties\nend repeat\nend tell";
NSAppleScript* theScript = [[NSAppleScript alloc] initWithSource:cmd];
NSDictionary* errorDict = nil;
NSAppleEventDescriptor* result = [theScript executeAndReturnError:&errorDict];
[theScript release];
if (errorDict) {
NSLog(#"Error:%# %#", [errorDict valueForKey:#"NSAppleScriptErrorNumber"], [errorDict valueForKey:#"NSAppleScriptErrorMessage"]);
return;
}
// do something with result
NSLog(#"result: %#", result);
You can get a list of every currently running Application with
NSWorkSpace.sharedWorkspace.runningApplications;
Each object in that array should be an NSRunningApplication, which you can query and manipulate freely.
I need to run two threads simultaneously, but I am not getting how to do so.
I start thread:
[NSThread detachNewThreadSelector:#selector(MyNewThread:) toTarget:[CMyClass class] withObject:nil];
-(void)MyNewThread:(id)param{
NSAutoreleasePool *Pool = [[NSAutoreleasePool alloc] init];
NSString *strSwitcher = #"myCommand";
const char * cstrSwitcher = [strSwitcher UTF8String];
system(cstrSwitcher);
[Pool release];
}
and some other system command I want to send on other thread. When I send one system command prompt changes.(e.g. myCommand> ).
Now when I start another thread then that command only works when previous thread was stopped.
Anyone can help me??
By taking into account the info in your comment on the OP, I assume you want to call system() from multiple thread simultaneously.
Unfortunately, that cannot work because when you call system(), your application waits for a signal that is sent as soon as the child process exits. Because signals don't know anything about the threads in your application, system() cannot be run from multiple threads simultaneously.
Thanks for JeremyP to point into the direction of NSTask in the comments!
The alternative is to use the NSTask.
NSTask uses fork() to create a child process and calls waitpid() in the parent and execve() (or one of its siblings) in the child process. Using the macros defined in <sys/wait.h>, the child's return value is retrieved after it finishes. This way, multiple child process can be launched without blocking other threads. You can either do all that yourself, or just use the simple NSTask class.
I would suggest not using Hungarian notation (Windows picked that up back in the Win32 days, but dropped it in .NET) as that just complicates things. rename 'Pool' to 'pool', and 'strSwitcher' to 'switcher'. Now, just call system([switcher UTF8String]); instead of that extra variable. On top of this, remove the NSAutoreleasePool, and use the new #autoreleasepool { } definition, enclosing your code in it. Here's how it looks now.
- (void)myNewThread:(id)param {
#autoreleasepool {
NSString *switcher = #"myCommand";
system([switcher UTF8String]);
}
}
And if you'd like to switch to NSTasks to be able to run multiple executables, here's how it goes: (I also changed the method a bit.)
- (void)executeTaskAtPath:(NSString *)path withArguments:(NSArray *)arguments {
#autoreleasepool {
NSTask *task = [launchedTaskWithLaunchPath:path arguments:arguments];
[task waitUntilExit]; // This blocks the thread.
}
}
To find out if the task was terminated successfully, register for a NSTaskDidTerminateNotification at [NSNotificationCenter defaultCenter].
I've noticed some weird behavior with NSBundle when using it in a
command-line program. If, in my program, I take an existing bundle and
make a copy of it and then try to use pathForResource to look up
something in the Resources folder, nil is always returned unless the
bundle I'm looking up existed before my program started. I created a
sample app that replicates the issue and the relevant code is:
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *exePath = [NSString stringWithCString:argv[0]
encoding:NSASCIIStringEncoding];
NSString *path = [exePath stringByDeletingLastPathComponent];
NSString *templatePath = [path stringByAppendingPathComponent:#"TestApp.app"];
// This call works because TestApp.app exists before this program is run
NSString *resourcePath = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:templatePath];
NSLog(#"NOCOPY: %#", resourcePath);
NSString *copyPath = [path stringByAppendingPathComponent:#"TestAppCopy.app"];
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:templatePath
toPath:copyPath
error:nil])
{
// This call will fail if TestAppCopy.app does not exist before
// this program is run
NSString *resourcePath2 = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:copyPath];
NSLog(#"COPY: %#", resourcePath2);
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
}
[pool release];
}
For the purpose of this test app, let's assume that TestApp.app
already exists in the same directory as my test app. If I run this,
the 2nd NSLog call will output: COPY: (null)
Now, if I comment out the final removeItemAtPath call in the if
statement so that when my program exits TestAppCopy.app still exists
and then re-run, the program will work as expected.
I've tried this in a normal Cocoa application and I can't reproduce
the behavior. It only happens in a shell tool target.
Can anyone think of a reason why this is failing?
BTW: I'm trying this on 10.6.4 and I haven't tried on any other
versions of Mac OS X.
I can confirm that it is a bug in CoreFoundation, not Foundation. The bug is due to CFBundle code relying on a directory contents cache containing stale data. The code apparently assumes that neither the bundle directories nor their immediate parent directories will change during application runtime.
The CoreFoundation call corresponding to +[NSBundle pathForResource:ofType:inDirectory:] is CFBundleCopyResourceURLInDirectory(), and it exhibits the same misbehavior. (This is unsurprising, as -pathForResource:ofType:inDirectory: itself uses this call.)
The problem ultimately lies with _CFBundleCopyDirectoryContentsAtPath(). This is called during bundle loading and during all resource lookup. It caches information about the directories it looks up in contentsCache.
Here's the problem: When it comes time to get the contents of TestAppCopy.app, the cached contents of the directory containing TestApp.app don't include TestAppCopy.app. Because the cache ostensibly has the contents of that directory, only the cached contents are searched for TestAppCopy.app. When TestAppCopy.app is not found, the function takes that as a definitive "this path does not exist" and doesn't bother trying to open the directory:
__CFSpinLock(&CFBundleResourceGlobalDataLock);
if (contentsCache) dirDirContents = (CFArrayRef)CFDictionaryGetValue(contentsCache, dirName);
if (dirDirContents) {
Boolean foundIt = false;
CFIndex dirDirIdx, dirDirLength = CFArrayGetCount(dirDirContents);
for (dirDirIdx = 0; !foundIt && dirDirIdx < dirDirLength; dirDirIdx++) if (kCFCompareEqualTo == CFStringCompare(name, CFArrayGetValueAtIndex(dirDirContents, dirDirIdx), kCFCompareCaseInsensitive)) foundIt = true;
if (!foundIt) tryToOpen = false;
}
__CFSpinUnlock(&CFBundleResourceGlobalDataLock);
So, the contents array remains empty, gets cached for this path, and lookup continues. We now have cached the (incorrectly empty) contents of TestAppCopy.app, and as lookup drills down into this directory, we keep hitting bad cached information. Language lookup takes a stab when it finds nothing and hopes there's an en.lproj hanging around, but we still won't find anything, because we're looking in a stale cache.
CoreFoundation includes SPI functions to flush the CFBundle caches. The only place public API calls into them in CoreFoundation is __CFBundleDeallocate(). This flushes all cached information about the bundle's directory itself, but not its parent directory: _CFBundleFlushContentsCacheForPath(), which actually removes the data from the cache, removes only keys matching an anchored, case-insensitive search for the bundle path.
It would seem the only public way a client of CoreFoundation could flush bad information about TestApp.app's parent directory would be to make the parent directory a bundle directory (so TestApp.app lived alongside Contents), create a CFBundle for the parent bundle directory, then release that CFBundle. But, it seems that if you made the mistake of trying to work with the TestAppCopy.app bundle prior to flushing it, the bad data about TestAppCopy.app would not be flushed.
That sounds like a bug in the Foundation. The one key difference between a command line tool like that one and a Cocoa application is the run loop. Try refactoring the above into something like:
#interface Foo:NSObject
#end
#implementation Foo
- (void) doIt { .... your code from main() here .... }
#end
... main(...) {
Foo *f = [Foo new];
[f performSelector: #selector(doIt) withObject: nil afterDelay: 0.1 ...];
[[NSRunLoop currentRunLoop] run];
return 0; // not reached, I'd bet.
}
And see if that "fixes" it. It might. It might not (there are couple of other significant differences, obviously). In any case, do please file a bug via http://bugreport.apple.com/ and add the bug # as a comment.
I'm writing an OS X Service with MacRuby. It upcases the selected text. It mostly works, but… well, here's all of it:
#!/usr/local/bin/macruby
# encoding: UTF-8
framework 'Foundation'
framework 'AppKit'
class KCUpcase
def upcase(pasteboard, userData: s_userdata, error: s_error)
incoming_string = pasteboard.stringForType "public.utf8-plain-text"
outgoing_string = incoming_string.upcase
pasteboard.clearContents
pasteboard.setString(outgoing_string, forType: "public.utf8-plain-text")
end
end
NSLog "Starting…"
NSRegisterServicesProvider(KCUpcase.new, "Upcase")
NSLog "Registered…"
NSRunLoop.currentRunLoop\
.acceptInputForMode(NSDefaultRunLoopMode,
beforeDate:NSDate.dateWithTimeIntervalSinceNow(10.0))
NSLog "Done."
It's just a Foundation tool, not part of an Application.
Now, see the NSRunLoop… line? That doesn't really work. The program exits imediately. I suppose the loop runs once and then exits. Anyhoo, the fact is that it's definititely not waiting 10s for input. So, here's what I did instead:
NSRunLoop.currentRunLoop.runUntilDate NSDate.dateWithTimeIntervalSinceNow(60.0)
And that works, but naturally the program sticks around for 60s, and it's a kludge. So I implemented the whole thing in Objective C (Including KCUpcase, which is not shown). And… it works. With manual memory management. Once I switch to GC (-fobjc-gc-only), it exits imediately same as the MacRuby version.
#import <Foundation/Foundation.h>
#import "KCUpcase.h"
int main (int argc, const char * argv[]) {
NSLog(#"Starting…");
NSRegisterServicesProvider([[KCUpcase alloc] init], #"KCUpcase");
NSLog(#"Registered…");
[[NSRunLoop currentRunLoop]
acceptInputForMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:10.0]];
NSLog(#"Done.");
return 0;
}
But, alas, the fix is easy: because this is a Foundation tool (not an NSApplication), it seems I have to start GC manually by calling objc_startCollectorThread. Here:
#import <objc/objc-auto.h>
// ...
NSLog(#"Starting…");
objc_startCollectorThread();
NSRegisterServicesProvider([[KCUpcase alloc] init], #"KCUpcase");
// ...
Ok, but what's up with MacRuby then? Let's throw it into the mix:
#import <MacRuby/MacRuby.h>
// ...
NSLog(#"Starting…");
objc_startCollectorThread(); // This magic stops working once we add MacRuby
[[MacRuby sharedRuntime] evaluateString: #"$stderr.puts 'hi from macruby'"];
NSRegisterServicesProvider([[KCUpcase alloc] init], #"KCUpcase");
// ...
And, again, it's not waiting in the loop. And, again, ussing the runUntilDate: kludge instead of acceptInputForMode:beforeDate: works:
NSLog(#"Starting…");
[[MacRuby sharedRuntime] evaluateString: #"$stderr.puts 'hi from macruby'"];
NSRegisterServicesProvider([[KCUpcase alloc] init], #"KCUpcase");
NSLog(#"Registered…");
// Hmmm…
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]];
NSLog(#"Done.");
return 0;
So, I suppose I'm missing something terribly obvious. Please enlighten me.
And by the way, the full MacRuby version of the project is available here (download) with a Rake task that'll build and install it in ~/Library/Services. Then you need to enable its checkbox in Services in the Keyboard Preference Pane (once).
(or git clone git://gist.github.com/537075.git)
Aside: Interestingly, I tried calling NSLog inside the MacRuby string, and it raised NoMethodError. What gives?
It's a bit weird but here is a workaround:
framework 'Foundation'
framework 'AppKit'
class KCUpcase
def upcase(pasteboard, userData: s_userdata, error: s_error)
incoming_string = pasteboard.stringForType "public.utf8-plain-text"
outgoing_string = incoming_string.upcase
pasteboard.clearContents
pasteboard.setString(outgoing_string, forType: "public.utf8-plain-text")
end
end
puts "Starting…"
NSRegisterServicesProvider(KCUpcase.new, "Upcase")
puts "Registered…"
later = NSDate.dateWithTimeIntervalSinceNow(5)
NSRunLoop.currentRunLoop.runUntilDate later
puts "Done"
Basically, you need to define the timestamp before you dispatch the runloop request otherwise the main loop exists before getting the instruction. As you spotted, this is not really a MacRuby bug but still, hopefully that helps.
acceptInputForMode:beforeDate: only runs the loop once. As soon as any input (other than a timer) is processed, it exits. runUntilDate: however continues running the loop until the date is reached.
Consider the following main() method which you would find most iPhone applications:
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release];
return retVal;
}
In every iPhone app that I've run in Simulator with these (including several sample projects provided by Apple), the thread never exits UIApplicationMain() and any remaining code in main() is never executed. Is this expected behavior?
I have verified that statements after UIApplicationMain() never run by stepping through the code with a debugger. When the user stops an application (by hitting the "Home" button, for example), the resulting stack trace shows that [UIApplication _terminateWithStatus:] is eventually called. This function calls your application delegate's applicationWillTerminate: method. Once that finishes, [UIApplication _terminateWithStatus:] seems to kill/exit the thread.
Can someone confirm that this is how main() is supposed to work, or at least confirm the same behavior on their machine?
The original question was: "Why doesn’t an iPhone app’s main() function ever get a chance to finish?"
Short Answer: Because UIApplicationMain() is coded such that it never returns.
After doing several tests in Simulator and on the device, and asking another developer to do the same tests, I have confirmed that UIApplicationMain never returns. When the user terminates an application normally by hitting the Home button, The program ultimately terminates inside an unpublished UIApplication method called _terminateWithStatus. This method calls exit(0).
This behavior matches that of the NSApplicationMain function (which is AppKit/Cocoa version of the UIApplicationMain function). The documentation for NSApplicationMain() clearly states that it will never return.
I have submitted a bug (6600198) to Apple requesting that the official documentation (and Xcode template for main.m) be corrected to state that UIApplicationMain() never returns. While this is not a functional problem, the current template and docs are misleading.
Thanks to everyone for all the input and brainstorming!
Try:
int main(int argc, char *argv[])
{
NSLog(#"Step 0");
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(#"Step 1");
int retVal = UIApplicationMain(argc, argv, nil, nil);
NSLog(#"Step 2");
[pool release];
NSLog(#"Step 3");
return retVal;
}
It may be that the release of the pool is preventing further logging in which case you'd get step 2 but not step 3.
If step 2 isn't being printed, then it's almost certainly something wrong with UIApplicationMain - there's a chance that it doesn't return so put NSLog statements (step 1.1, Step 1.2, ...) at various points within it and run to find the last message logged.
Keep drilling down (Step 1.7.1, 1.7.2, .... 1.7.6.3.2, ...) - eventually, you'll track down the exact line (however deep in the call hierarchy) when the log messages stop being logged and that line will be your culprit (either "turning off" logging or exiting without returning normally).
One further snippet I found on the web:
=====
When you use this line:
int retVal = UIApplicationMain(argc, argv, #"MyApp", #"MyApp");
The first MyApp is your main app delegate class. The second is the class to where SpringBoard sends touch notifications.
Also, if you are using the SDK, and have a main nib defined in the Info.plist, then you can leave the call as:
int retVal = UIApplicationMain(argc, argv, nil, nil);
as all that will be covered when you create your xibs.
=====
Now I don't know enough about iPhone development (specifically xibs) to know what that last bit even means (or if you've set it up correctly) but it sounds like another phase of compilation.
However, my first thought from reading that is that Springboard will call your delegate class when the buttons are pressed to ask you to do something (like shut down gracefully). If it can't ask you (i.e., no delegate), it's probably within its rights to shut you down as it sees fit, such as with [UIApplication _terminateWithStatus:].
In the Windows world, you would probably send off a quit message to the main window but, as I say, iPhone development may be different.
Still, it's an avenue to investigate. I'd be interested in seeing what calls were made to a delegate if you provided one. The code included with the snippet above had this:
#implementation MyApp
- (void) applicationDidFinishLaunching:(id)unused {
rect = [ UIHardware fullScreenApplicationContentRect ];
rect.origin.x = 0.0f;
rect.origin.y = 0.0f;
window = [ [ UIWindow alloc ] initWithContentRect: rect ];
[ window makeKeyAndVisible ];
view = [ [ MyAppView alloc ] initWithFrame: rect ];
[ window setContentView: view ];
}
- (void) dealloc {
[ window release ];
[ view release ];
[ super dealloc ];
}
So maybe a delegate with dealloc() is the secret to getting it to exit back to main(). Why don't you give that a shot? It may get you closer to your goal even if it doesn't solve the core problem.
After [pool release] there is nothing to log to?
trying using fprintf and see what happens
int main(int argc, char *argv[])
{
/*
...
same as above
...
*/
[pool release];
char file_name = "/tmp/log"
FILE *file = fopen(file_name, "w");
fprintf(file_name, "END\n");
}
and tell us what happens
I also thought the easiest way to check was to set a break point right at the return
in gdb do
b main.c:x
where x is the line number of the return statement
After calling the UIApplicationMain function your application launches (establishing a runloop, etc) and all work should then be done outside the context of main (if you need it to run in main, do it before that point). When quitting an application it is generally more efficient to allow the OS to do memory cleanup.
I have that not return experience too. And have set break points to verify exactly like Clint said.
wisequark has a good point.
great topic. makes me feel more comfortable that i am not the only one who has the question.