bundlePath returns empty when running as a Launch Deamon on MacOSX 10.5 - macos

I've got a Cocoa command-line app which is built to target 10.5 SDK. In the app, I have
NSString *appPath = [[NSBundle mainBundle] bundlePath];
NSLog(#"%#", appPath);
On Mac OSX 10.5, when I run the app from the command line, I get the expected output of the path. However, if I set the app up to run as a LaunchDeamon, it only outputs a '/'
It works as expected on 10.6 and on 10.7 both as a Deamon and as an app. Anyone know why the difference would be? Is there a better way to get the application path that would work on 10.5+?
UPDATE
For me, the solution in the accepted answer did not work. However, the comment about adding the "WorkingDirectory" key to the LaunchDeamon's plist file worked out. Apparently this is needed for Mac OS X 10.5, but not 10.6+.

Thanks for answering my clarifying question.
NSBundle depends on a existing bundle, with it's associated Info.plists and bundle ID's (e.g. com.apple.textedit.app), etc.
While a single binary is not a bundle, I'm guessing that Apple engineering fixed up [[NSBundle mainBundle] bundlePath] to do "the right thing" in 10.6 & 10.7. But you still need a solution for 10.5.
Maybe the UNIX library function char * getcwd(char *buf, size_t size) would get you to where you need to be.
For a proper solution, I'd recommend doing a run-time conditional check with code that looks something like this:
+ (NSString *) getAppPath
{
NSString * appPath = NULL;
SInt32 minorVersionNum;
OSErr err;
err = Gestalt(gestaltSystemVersionMinor,&minorVersionNum);
// do this only if we're running on anything *older* than 10.6
if((noErr == err) && (minorVersionNumber < 6))
{
// hopefully the below define is enough space for any returned path
#define kMaxPathLength 512
size_t bufferLength = kMaxPathLength;
char bufferToHoldPath[kMaxPathLength];
// initialize the buffer & guarantee null-terminated strings
bzero(&bufferToHoldPath,bufferLength);
if( getcwd(&bufferToHoldPath, bufferLength) != NULL)
{
appPath = [NSString stringWithUTF8String: bufferToHoldPath];
}
}
// this code runs for 10.6 and *newer*, and attempts it on
// 10.5 only if the above getcwd call failed
if(NULL == appPath)
{
appPath = [[NSBundle mainBundle] bundlePath];
}
return(appPath);
}
I did not test this code out so YMMV.

Related

Determine if a file is inside any macOS Trash folder

There's a similar question for iOS, but I found that the proprosed solutions do not work on macOS in all cases.
On a Mac, there are many possible Trash folders:
/.Trashes
~/.Trash
~/Library/Mobile Documents/com~apple~CloudDocs/.Trash – this one is from iCloud
/Users/xxx/.Trash – any other user's trash
/Volumes/xxx/.Trashes
This code should work but doesn't for the case of the iCloud trash:
NSURL *theURL = ...;
NSURLRelationship relationship = NSURLRelationshipOther;
NSError *error = nil;
[NSFileManager.defaultManager
getRelationship: &relationship
ofDirectory: NSTrashDirectory
inDomain: 0
toItemAtURL: theURL
error: &error];
BOOL insideTrash = !error && (relationship == NSURLRelationshipContains);
If the URL points to any iCloud folder (including the Trash folder shown above), I get this error:
Error Domain=NSCocoaErrorDomain Code=3328
"The requested operation couldn’t be completed because the feature is not supported."
Curiously, even the header file of "NSFileManager" in the 10.15 SDK suggests to use this same code:
/* trashItemAtURL:resultingItemURL:error: [...]
To easily discover if an item is in the Trash, you may use
[fileManager getRelationship:&result ofDirectory:NSTrashDirectory
inDomain:0 toItemAtURL:url error:&error]
&& result == NSURLRelationshipContains.
*/
There also seems to be an issue with trashItemAtURL: on iCloud-synched folders.
So, how do I solve this? If the Finder can detect the iCloud trash, I should be, too.
(Note: The app I use for testing this is not even sandboxed)
More findings: Fails with dead symlinks, too
The officially suggested method of using getRelationship: also fails with an error if the url points to a symlink whose target doesn't exist.
So, basically, this function is quite broken (verified in 10.13.6, 10.15.7 and 11.0.1).
Here's code to demonstrate the bug, which I've filed with Apple under FB8890518:
#import <Foundation/Foundation.h>
static void testSymlink (NSString* symlinkName, NSString* symlinkTarget)
{
NSString *path = [[NSString stringWithFormat:#"~/.Trash/%#", symlinkName] stringByExpandingTildeInPath];
NSURL *url = [NSURL fileURLWithPath:path];
symlink (symlinkTarget.UTF8String, path.UTF8String);
NSLog(#"created symlink at <%#> pointing to <%#>", url.path, symlinkTarget);
NSURLRelationship relationship = -1;
NSError *error = nil;
[NSFileManager.defaultManager getRelationship:&relationship ofDirectory:NSTrashDirectory inDomain:0 toItemAtURL:url error:&error];
NSString *rel = #"undetermined";
if (relationship == 0) rel = #"NSURLRelationshipContains";
if (relationship == 1) rel = #"NSURLRelationshipSame";
if (relationship == 2) rel = #"NSURLRelationshipOther";
NSLog(#"result:\n relationship: %#\n error: %#", rel, error);
}
int main(int argc, const char * argv[])
{
#autoreleasepool {
testSymlink (#"validSymlink", #"/System");
testSymlink (#"brokenSymlink", #"/nonexisting_file");
}
return 0;
}
With the realization that [NSFileManager getRelationship:] even fails for broken symlinks, I conclude that this is a bug in macOS that's been existing undetected for years.
I came up with the following work-around:
Use the getRelationship: operation, then check the returned error first:
If there's no error, then check if relationship == NSURLRelationshipContains, and use that as my result.
Else, in case of any error, check whether the path contains "/.Trash/" or "/.Trashes/" - if so, assume the item is inside the Trash folder.
NSURL *theURL = ...;
NSURLRelationship relationship = NSURLRelationshipOther;
NSError *error = nil;
[NSFileManager.defaultManager
getRelationship: &relationship
ofDirectory: NSTrashDirectory
inDomain: 0
toItemAtURL: theURL
error: &error];
BOOL insideTrash = !error && (relationship == NSURLRelationshipContains)
|| error && (
[theURL.path containsString:#"/.Trash/"]
|| [theURL.path containsString:#"/.Trashes/"]
)
);

Permission to access /Library/Application Support from Mac App bundle

I have an app bundle (built with Unity 3d if that is relevant) that I am able to create a .pkg installer using productbuild and distribute on the App Store without issue. However, the app downloads and caches a fair amount of media and has some optional configuration files that need to be shared between all users on the machine. According to Apple's documentation the configuration files should probably go in the /Library/Application Support directory and the media in /Library/Caches. I've made a modified version of that app that uses those directories instead of the ones that the sandboxed app can access, but it doesn't have permission to the /Library directory unless I run the app as root, which isn't a realistic option.
I've searched google for several hours, but I can't seem to find anything about creating such an installer. I did read this answer which has a screenshot of an installer that has the option to install for all users, but either I'm missing some option to enable that or that screenshot is just outdated because I can't seem to create a .pkg that gives me that option.
So I guess my question boils down to this: how do I package my app so it can be installed for all users, and have permission to read and write to /Library/Application Support/{app name}, or is there another preferred way to share configuration files and/or media between multiple users on the same machine?
For anyone else who has a similar problem, the correct answer is that you can't do this using productbuild, but you can using pkgbuild.
My productbuild build step for the app store looks like this:
productbuild --component "{mystoreapp.app/}" /Applications --sign "{signing identity}" "{mystorepkg.pkg}"
The corresponding packing command for pkgbuild looks like this:
pkgbuild --component "{mymodifiedapp.app/}" --sign "{signing identity}" --ownership preserve --scripts "{path/to/my/scripts}" --identifier {com.yourcompany.yourapp} --version "{versionNumber}" --install-location /Applications "{mymodifiedpkg.pkg}"
Note that signing is optional here as it will be distributed outside the store. where {path/to/my/scripts} has a file called postinstall in it that looks like this:
#this function creates a directory if it doesn't exist
create_directory() {
if [[ ! -d "$1" ]]
then
if [[ ! -L "$1" ]]
then
echo "directory $1 doesn't exist. Creating now"
mkdir "$1"
echo "directory $1 created"
else
echo "directory $1 exists"
fi
fi
}
#this function gives all users read and write (and execute) access to the directory
fix_dir_permissions() {
chmod 777 "$1"
}
baseDirName="/Library/Application Support/{your app name bere}"
subDirs[0]="$baseDirName/{sub dir here}"
#create base directory
create_directory "$baseDirName"
#create all subdirectories and give permissions
for subDir in "${subDirs[#]}"
do
create_directory "$subDir"
fix_dir_permissions "$subDir"
done
exit 0
This script will run after the install is over, and will create your application support directory as well as any subdirectories you need and change the permissions on them so all users have access to them.
I solved the same problem by creating a little help app that is called from the main program at any time access to "Application Support" is needed. The helper program is spawned from the main program using AuthorizationExecuteWithPrivileges. It only calls functions to create a directory and set permissions. So all is done from within the main application, not from a installer package. AuthorizationExecuteWithPrivileges (yes its offically deprecated) then shows a macos system window that requires to enter a password etc.
For this part quite a nice implementation exists here:
https://github.com/sveinbjornt/STPrivilegedTask/
It is important to:
wait until the called program is finished
then wait again until the folder exists (which may need some msecs)
It took me quite a while to get this done, so i'll share it here. Although the application is C++ the code here must use obective-c.
See my post here for signing and notarizing this:
How to notarize an app bundle containing helpers embedded in it?
The helper program:
#import <Foundation/Foundation.h>
#include <iostream>
#include <fstream>
int main(int argc, const char * argv[]) {
#autoreleasepool {
NSString *supportDirectory = [NSString stringWithFormat:#"%s", argv[1] ];
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:supportDirectory withIntermediateDirectories:YES attributes:nil error:&error];
if (!success)
return -1;
BOOL success2 = [[NSFileManager defaultManager] setAttributes:#{ NSFilePosixPermissions : #0777 } ofItemAtPath:supportDirectory error:&error];
}
return 0;
}
Caller Process:
void macCheckCreateFolderAdmin(std::string path)
{
NSString *supportDirectory = [NSString stringWithFormat:#"%s", path.c_str() ];
NSError *error = nil;
BOOL success;
if ([[NSFileManager defaultManager] fileExistsAtPath: supportDirectory])
return;
char *args[] =
{
const_cast<char *>(path.c_str()),
NULL
};
AuthorizationRef authorizationRef;
OSStatus status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authorizationRef);
NSString *nsExecPath = NSProcessInfo.processInfo.arguments[0];
const char *applFolder = [nsExecPath UTF8String];
std::string s = std::string(applFolder);
std::size_t found = s.rfind("/");
if( found != std::string::npos)
s = s.substr(0, found + 1);
s += "myHelperProgram";
FILE *outputFile = nullptr;
OSStatus status2 = AuthorizationExecuteWithPrivileges(authorizationRef, s.c_str(), kAuthorizationFlagDefaults, args, &outputFile);
if(status2 != 0)
return;
int processIdentifier = fcntl(fileno(outputFile), F_GETOWN, 0);
pid_t pid = 0, count = 0;
while(count < 30 && (pid = waitpid(processIdentifier, &status, WNOHANG)) == 0)
{
count ++;
usleep(200 * 1000);
}
}
}

Using xattr to set the Mac OSX quarantine property

There is a lot of information on StackOverflow and elsewhere about how to clear the Mac quarantine property.
In my case I would like to set it.
This is in order to test that my app is properly signed so that the user will hot get the "Untrusted Developer" warning after downloading it.
My app is particularly large (we distribute from a large file download site, not the store) and it is not convenient to have to upload and download to test this.
I have had some battles with code signing the past week so this testing is important to me.
Once a file has the quarantine property I see how I can alter it to have the values:
0002 = downloaded but never opened (this is the one that causes the warning)
0022 = app aborted by user from the warning dialogue (you hit 'cancel' in the dialogue)
0062 = app opened (at least) once (you hit 'open' in the dialogue)
But I don't know how to give it the property in the first place.
The code for this isn't hard, but you need FSRef's to do it, which are deprecated. That said, it still works on 10.9. You have to link with CoreServices.
int main(int argc, const char * argv[]) {
#autoreleasepool {
if (argc != 2) {
printf("quarantine <path>\n");
exit(1);
}
NSString *path = #(argv[1]);
OSStatus result;
FSRef pathRef;
result = FSPathMakeRef((UInt8*)[path UTF8String], &pathRef, 0);
if (result != noErr) {
NSLog(#"Error making ref (%d): %s", result, GetMacOSStatusCommentString(result));
exit(result);
}
NSDictionary *quarantineProperties = #{(__bridge id)kLSQuarantineTypeKey: (__bridge id)kLSQuarantineTypeOtherDownload};
result = LSSetItemAttribute(&pathRef,
kLSRolesAll,
kLSItemQuarantineProperties,
(__bridge CFTypeRef)quarantineProperties);
if (result != noErr) {
NSLog(#"Error setting attribute (%d): %s", result, GetMacOSStatusCommentString(result));
}
exit(result);
}
return 0;
}
Another approach is to just copy the quarantine information from one file to another. You can serialize xattr information like this:
xattr -p com.apple.quarantine file > file.xattr
You can then apply those attributes to another file like this:
xattr -w com.apple.quarantine "`cat file.xattr`" file
(That should work, but I haven't tested it with quarantining in particular. I use a similar technique to save code signatures and reapply them.)

How does OS X's defaults command get access to prefs of sandboxed apps?

I am writing a preferences editor tool (see http://www.tempel.org/PrefsEditor). It is effectively a GUI version of the defaults command.
I have trouble reading (let alone writing) preferences of random sandboxed applications, though.
For instance, when I try to get the keys of the Maps app, I get NULL returned:
CFArrayRef prefs = CFPreferencesCopyKeyList (CFSTR("com.apple.Maps"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
However, the defaults command is able to read those prefs:
defaults read com.apple.Maps
I like to know how the defaults command accomplishes this, trying to do the same in my tool.
try that:
CFPropertyListRef prop = CFPreferencesCopyValue(CFSTR("ElementsVersion"),
CFSTR("/Users/karsten/Library/Containers/com.apple.Maps/Data/Library/Preferences/com.apple.Maps"),
CFSTR("kCFPreferencesCurrentUser"),
CFSTR("kCFPreferencesAnyHost"));
seems you need the path to the file, not just the bundle-id
Karsten’s answer is correct but for the sake of completeness, the defaults command uses the undocumented _CFPreferencesCopyApplicationMap() function to retrieve the full URL of the preferences.
#import <CoreFoundation/CoreFoundation.h>
extern CFDictionaryRef _CFPreferencesCopyApplicationMap(CFStringRef userName, CFStringRef hostName);
int main(int argc, char *argv[])
{
#autoreleasepool
{
CFDictionaryRef applicationMap = _CFPreferencesCopyApplicationMap(kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
CFArrayRef urls = CFDictionaryGetValue(applicationMap, CFSTR("com.apple.mail"));
CFShow(urls);
CFRelease(applicationMap);
}
}
I've added to 0xced's excellent answer so that the code can be packaged into a command-line tool that accepts the bundle ID as an argument. Forgive me if this is obvious to experienced Mac programmers, but as someone who has never used CoreFoundation I found this to be non-trivial.
#import <CoreFoundation/CoreFoundation.h>
extern CFDictionaryRef _CFPreferencesCopyApplicationMap(CFStringRef userName, CFStringRef hostName);
int main(int argc, char *argv[]) {
#autoreleasepool {
if (argc < 2) {
// Print usage string & exit.
fprintf(stderr, "usage: GetPrefDomains bundle_id\n");
exit(1);
}
// Get the bundle ID from the first command-line argument.
CFStringRef bundleID = CFStringCreateWithCString(NULL, argv[1], kCFStringEncodingUTF8);
// Get the list of preference domain urls.
CFDictionaryRef applicationMap = _CFPreferencesCopyApplicationMap(kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
CFArrayRef urls = CFDictionaryGetValue(applicationMap, bundleID);
// If no urls exist (invalid bundle ID), exit.
if (!urls) {
fprintf(stderr, "No preference domains found.\n");
exit(0);
}
// Print the paths to the preference domains.
CFIndex urlsCount = CFArrayGetCount(urls);
for (int i = 0; i < urlsCount; i++) {
CFURLRef url = CFArrayGetValueAtIndex(urls, i);
CFStringRef path = CFURLCopyPath(url);
printf("%s\n", CFStringGetCStringPtr(path, kCFStringEncodingUTF8));
}
// Clean up.
CFRelease(bundleID);
CFRelease(applicationMap);
}
}
Save the code as GetPrefDomains.m, compile, and invoke as:
GetPrefDomains com.apple.mail
This was useful to me because surprisingly the defaults command is case-sensitive and misbehaves silently with certain Apple applications that are under the filesystem protections in SIP on Mojave 10.14 or later (Safari & Mail, most notably). Add in the fact that Apple's capitalization rules are not consistent (com.apple.mail vs. com.apple.Notes), sandboxed preference paths, and the fact that that the filesystem is not case-sensitive and you quickly run into some very frustrating edge cases.

How do you wait for an application to close in OS X?

I am using the code below to check if an application is running and close it. Can someone provide an example of how to request an application calose and wait for it to close before proceeding?
+ (BOOL)isApplicationRunningWithName:(NSString *)applicationName {
BOOL isAppActive = NO;
NSDictionary *aDictionary;
NSArray *selectedApps = [[NSWorkspace sharedWorkspace] runningApplications];
for (aDictionary in selectedApps) {
if ([[aDictionary valueForKey:#"NSApplicationName"] isEqualToString: applicationName]) {
isAppActive = YES;
break;
}
}
return isAppActive;
}
+ (void)stopApplication:(NSString *)pathToApplication {
NSString *appPath = [[NSWorkspace sharedWorkspace] fullPathForApplication:pathToApplication];
NSString *identifier = [[NSBundle bundleWithPath:appPath] bundleIdentifier];
NSArray *selectedApps = [NSRunningApplication runningApplicationsWithBundleIdentifier:identifier];
// quit all
[selectedApps makeObjectsPerformSelector:#selector(terminate)];
}
You can use Key-Value Observing to observe the terminated property of each running application. This way, you'll get notified when each application terminates, without having to poll.
One way would be to periodically call isApplicationRunningWithName on a timer, and wait until that function returns NO.
The commandline timelimit will let you send a close signal to an app, wait x seconds, then kill it (or send any other signal you like, kill is -9) if hasn't obeyed the "warning" signal.
(Note: I haven't tried compiling it on Mac, but I believe it's fairly POSIX-compliant code and not Linux-specific as it runs on BSD and others.)

Resources