In main function body, I need to call system(RELATIVE_PATH + /MyShellScript.sh). The script and other related stuff resides in main project bundle. How to get relative path? IMHO it should be possible to extract it from main parameter - "char *argv[]"...
What exactly are you trying to do? What is your shell script doing, and why are you calling it from main()? You should not normally need to modify the main() function in a Cocoa app.
Generally, you should avoid using a shell script unless absolutely necessary, but if you must then you should create an NSTask object and use that to execute your script. If you want to run it at launch, you should place your code to launch your script in the application delegate's applicationDidFinishLaunching: method.
You can get the path to resources inside your app's bundle using the various methods of NSBundle:
NSString* path = [[NSBundle mainBundle] pathForResource:#"MyShellScript" ofType:#"sh"];
int main (int argc, const char * argv[])
{
NSLog(#"%s", argv[0]);
return 0;
}
output
2012-01-24 21:33:22.015 CLIXMPLParser[13042:707] /Users/zrxq/Library/Developer/Xcode/DerivedData/CLIXMPLParser-dsibnvwgoqvjnvbhlxkyzcojqnwr/Build/Products/Debug/CLIXMPLParser
Related
I am working on a SIMBL Plugin that is loaded by a 3rd-party host application on macOS. It is almost entirely written in C++ and has only minimal objective-c components. (The UI is largely provided by API calls into the host app.) One of the requirements is that the plugin bundle can be loaded multiple times from different sub-directories. It is a Lua interpreter, and the goal is for each instance to host a different configuration of lua scripts that appear in separate menus on the host application. Third parties could bundle this plugin with a custom configuration for their script(s) and they would appear as separate items in the app's plugin menu.
This issue I have is this: I need to find out what directory my plugin is executing in. I could create a special class called MY_BUNDLE_ID_CLASS and use:
[NSBundle bundleForClass:[MY_BUNDLE_ID_CLASS class]];
Once I have the correct NSBundle, getting the file path is trivial.
The problem is that if multiple instances of my bundle are loaded (from different folders), Cocoa complains that the class MY_BUNDLE_ID_CLASS is defined in multiple locations and won't guarantee me which one was used. For other similar classes this would be fine for my plugin, because my unique class names are macros that equate to a mangled name that includes the version number, but in this case it isn't okay. It would potentially be multiple instances of the same version. Is there any other way to find out the folder my plugin code is executing from? It seems like a simple request, but I am coming up empty. I welcome suggestions.
Given an address in an executable, the dladdr function can be used to query the dynamic linker about the dynamically-linked image containing that address; i.e., given a reference to a symbol in your plugin, dladdr can give you the dynamic linking information about your plugin.
The runtime lookup can look as follows:
// Sample: BundleClass.m, the principal class for the plugin
#import "BundleClass.h"
#import <dlfcn.h>
// We'll be using a reference to this variable compiled into the plugin,
// but we can just as easily use a function pointer or similar -- anything
// that will be statically compiled into the plugin.
int someVariable = 0;
#implementation BundleClass
+ (void)load {
Dl_info info;
if (dladdr(&someVariable, &info) != 0) {
NSLog(#"Plugin loaded from %s", info.dli_fname);
} else {
// Handle lookup failure.
}
}
#end
Instead of &someSymbol, you can also use a reference to a function (e.g. &someFunctionDefinedInThePlugin), but you should be careful not to pass in a pointer that could be dynamically allocated — since that will likely either fail, or point you to the memory space of the host process.
On my machine, with a trivial macOS host app setup, the following loading code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:#"DynamicBundle" withExtension:#"bundle"];
if (!bundleURL) {
NSLog(#"Failed to find bundle!");
return;
}
NSLog(#"Bundle class before loading: %#", NSClassFromString(#"BundleClass"));
NSBundle *bundle = [NSBundle bundleWithURL:bundleURL];
NSError *error = nil;
if (![bundle loadAndReturnError:&error]) {
NSLog(#"Failed to load bundle: %#", error);
return;
}
NSLog(#"Bundle class after loading: %#", NSClassFromString(#"BundleClass"));
}
successfully produces
Bundle class before loading: (null)
Loaded plugin from /Volumes/ExtSSD/Developer/Xcode/DerivedData/HostApp/Build/Products/Debug/HostApp.app/Contents/Resources/DynamicBundle.bundle/Contents/MacOS/DynamicBundle
Bundle class after loading: BundleClass
which is indeed the path to the plugin on disk.
I make a little tool built on "Command Line Tool" project.
Everything works well except the Cursor in this tool.
Here is the launch code:
int main(int argc, const char * argv[]) {
NSApplication * app = [NSApplication sharedApplication];
MyDelegate * delegate = [[MyDelegate alloc] init];
app.delegate = delegate;
[app run];
return 0;
}
And in MyDelegate, I create window, view, etc. However I override the resetCursorRects function in the View, but nothing happened.
If the same code run in an Application project, everything will be OK.
All the thing I tried include:
Make a subclass from NSApplication and override run function to handle uesr event.
Make a NSTrackingArea in the view to update cursor.
Perform selector(run) on mainThread and wait until done.
However they didn't work at all.
Now all the reason I guessed is the function NSApplicationMain(argc, argv) is not equal with [app run].
What's the difference? Can any one help me?
PS: If I use NSApplicationMain(argc, argv), xcode will give me an error that I must have a bundle and Info.plist. I don't want create an application, because it's to fat.
A program which is not a bundled app is, by default, a background-only process. A bundled app is, by default, a foreground-capable process. (One can set the LSUIElement key in an app's Info.plist to change that.)
Only the active app can set the cursor. A background-only or UI element (a.k.a. accessory) process can't become the active app.
If you want your program to be able to be activated and control the cursor, you can use [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular] to make it a foreground-capable app process. Note that this will make it appear in the Dock and Command-Tab application switcher (but with a generic-executable icon).
Of course, it's just easier and simpler to make your program a full-fledged bundled app.
I've created an Xcode project using Swift and a privileged Helper tool using Objective-C. The helper tool works fine within a project which has also been created in Objective-C but it doesn't seem to work within a project created with Swift.
The service itself is being installed. I can see the helper binary within the /Library/PrivilegedHelperTools directory and it's permissions seem to be okay (as well as the user: root). Removing the helper by using launchctl results in re-installing the tool when my project runs (that works as expected) but I can't call any method of the helper tool.
There is neither any exception being thrown nor does any other error occur (at least there seem to be no error as the Console shows nothing as well).
Does anybody know whether this might be an issue with Swift? Because running the same helper tool within another project (written in Objective-C) works well.
I could figure out what the problem was. The helper tool has a main.m wich contains a main() method. I just forgot to fill it with code that creates an instance of my helper class and trigger its listener:
#import <Foundation/Foundation.h>
#import "Helper.h"
int main(int argc, const char * argv[])
{
#autoreleasepool
{
Helper *helper = [[Helper alloc] init];
[helper run];
}
return EXIT_FAILURE;
}
This code causes the Helper instance to run in an infinite loop waiting for incoming connections (from Helper.h):
- (void)run
{
[_listener resume];
[[NSRunLoop currentRunLoop] run];
}
_listener is an instance of NSXPCListener.
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.