How to open a document using an application launched via NSTask? - objective-c

I've grown tired of the built-in open Mac OS X command, mostly because it runs programs with your actual user ID instead of the effective user ID; this results in the fact sudo open Foo opens Foo with its associated application with your account instead of the root account, and it annoys me. So I decided to make some kind of replacement.
So far I've been successful: I can open any program under the open -a or open -b fashion, and support optionally waiting. I'm using NSTask for that purpose.
However, I'd like to be able to open documents too. As far as I can see, you need to use NSWorkspace for that, but using NSWorkspace to launch programs results in them being launched with your account's credentials instead of your command line program's credentials. Which is precisely what the default open tool does, and precisely what I don't want.
So, how can I have a program request that another program opens a document without using NSWorkspace? From the NSTask object, I can have the process ID, but that's about it.

Hopefully this will do the trick:
- (void)openFile:(NSString *)filePath withTask:(NSTask *)task {
int pid = [task processIdentifier];
NSAppleEventDescriptor *target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeKernelProcessID bytes:&pid length:sizeof(pid)];
const char *urlUTF8 = [[[NSURL fileURLWithPath:filePath] absoluteString] UTF8String];
NSAppleEventDescriptor *urlDescriptor = [NSAppleEventDescriptor descriptorWithDescriptorType:typeFileURL bytes:urlUTF8 length:strlen(urlUTF8)];
NSAppleEventDescriptor *event = [NSAppleEventDescriptor appleEventWithEventClass:kEventParamAppleEvent eventID:kAEOpen targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
[event setParamDescriptor:urlDescriptor forKeyword:keyDirectObject];
OSStatus err = AESendMessage([event aeDesc], NULL, kAENoReply | kAENeverInteract, kAEDefaultTimeout);
if (err != noErr) {
// Error handling goes here
}
// Activate the application
event = [NSAppleEventDescriptor appleEventWithEventClass:kAEMiscStandards eventID:kAEActivate targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
err = AESendMessage([event aeDesc], NULL, kAENoReply | kAENeverInteract, kAEDefaultTimeout);
}
You may have to launch the application
using an NSTask and then send it the
appropriate open Apple Event.
Actually, can you launch using an
NSTask and then open the file via
NSWorkspace once you know it's
running? Or does that launch a new
instance of the application under your
user?
Original reply:
Does
open -a SomeApplication SomeFile
work?

Related

Asking a user for elevated privileges and elevating the application without an Apple developer certificate

Apparently, as of 10.7, AuthorizationExecuteWithPrivileges is deprecated. The general gist of the information I've gathered on this seems to suggest using ServiceManagement.framework's SMJobBless() function to have a helper application deployed.
My understanding of it though, is that this will need a developer certificate to be purchased from Apple to code sign both my application and the helper process - or this will not work. Is this correct?
I originally used AuthorizationExecuteWithPrivileges to ask a user for elevated privileges, since they are needed to access another running process. Without that, my app can't work as the unofficial plugin it's intended to. Is the code signing way really the only way to go from here? I'm trying to avoid purchasing a developer certificate due to the sheer cost of it.
Has anyone found any alternative ways to relaunch an application with elevated privileges, with user permission of course?
#CarlosP's answer with code to escape the path & arguments:
- (BOOL)runProcessAsAdministrator:(NSString*)scriptPath
withArguments:(NSArray*)arguments
output:(NSString**)output
errorDescription:(NSString**)errorDescription {
//Check path.
if (![scriptPath hasPrefix:#"/"]) {
#throw [NSException exceptionWithName:
NSInvalidArgumentException reason:#"Absolute path required." userInfo:nil];
}
//Define script.
static NSAppleScript* appleScript = nil;
if (!appleScript) {
appleScript = [[NSAppleScript alloc] initWithSource:
#"on run commandWithArguments\n"
" activate\n"
" repeat with currentArgument in commandWithArguments\n"
" set contents of currentArgument to quoted form of currentArgument\n"
" end repeat\n"
" set AppleScript's text item delimiters to space\n"
" return do shell script (commandWithArguments as text) with administrator privileges\n"
"end run"];
}
//Set command.
NSAppleEventDescriptor* commandWithArguments = [NSAppleEventDescriptor listDescriptor];
[commandWithArguments insertDescriptor:
[NSAppleEventDescriptor descriptorWithString:scriptPath] atIndex:0];
//Set arguments.
for (NSString* currentArgument in arguments) {
[commandWithArguments insertDescriptor:
[NSAppleEventDescriptor descriptorWithString:currentArgument] atIndex:0];
}
//Create target & event.
ProcessSerialNumber processSerial = {0, kCurrentProcess};
NSAppleEventDescriptor* scriptTarget =
[NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber bytes:&processSerial length:sizeof(ProcessSerialNumber)];
NSAppleEventDescriptor* scriptEvent =
[NSAppleEventDescriptor appleEventWithEventClass:kCoreEventClass
eventID:kAEOpenApplication
targetDescriptor:scriptTarget
returnID:kAutoGenerateReturnID
transactionID:kAnyTransactionID];
[scriptEvent setParamDescriptor:commandWithArguments forKeyword:keyDirectObject];
//Run script.
NSDictionary* errorInfo = [NSDictionary dictionary];
NSAppleEventDescriptor* eventResult = [appleScript executeAppleEvent:scriptEvent error:&errorInfo];
//Success?
if (!eventResult) {
if (errorDescription)
*errorDescription = [errorInfo objectForKey:NSAppleScriptErrorMessage];
return NO;
} else {
if (output)
*output = [eventResult stringValue];
return YES;
}
}
Update
In Yosemite, do shell script just calls a version of AuthorizationExecuteWithPrivileges embedded in StandardAdditions.osax.
It's conceivable that the with administrator privileges option for do shell script will go away when AuthorizationExecuteWithPrivileges does.
Personally, I would just continue calling AuthorizationExecuteWithPrivileges directly.
do shell script does have the advantage of reaping the process automatically. That requires a little extra work with AuthorizationExecuteWithPrivileges.
Is the code signing way really the only way to go from here?
To my knowledge, there is no secure alternative to AuthorizationExecuteWithPrivileges.
It still works fine under Yosemite. Haven't tried out El Capitan yet.
You can try to fail gracefully if the call goes away in the future.
I'm trying to avoid purchasing a developer certificate due to the sheer cost of it.
Well, if it helps, the code signing certificate will be valid for several years.
I'm pretty sure I've let my developer account lapse without any issues.
So it's basically $99 every five years.

Spawning Quicktime in a Sandboxed Mac App

I had asked a previous question about an issue I was having with AVPlayerView. After toying with it a bit, I actually like my mac app looks and runs a lot better when, instead of opening a new window with an AVPlayerView, it launches QuickTime, and tells it to open my http url of a video. I have come up with a few ways to do this, all of which work without sandboxing and none of which work with. I am currently using an NSTask to essentially
open -a "Quicktime Player" "http://example.com/video.m4v"
Again, this works, but only when my app is not sandboxed. Is there any way to do this in a sandboxed app?
Thanks in advance for any input or suggestions.
Sandbox doesn't allow to work with NSTask by default. Use the appropriate entitlement.
To open quicktime and start playing a movie you're probably better off using NSWorkspace like this:
[[NSWorkspace sharedWorkspace] openURLs:#[url]
withAppBundleIdentifier:#"com.apple.QuickTimePlayerX"
options:NSWorkspaceLaunchAsync
additionalEventParamDescriptor:NULL
launchIdentifiers:nil];
Alternatively go straight down to Launch Services where you even have more control of what and how things are launched:
NSURL *appToOpenWith = // get the URL of Quicktime using NSWorkspace URLForApplicationWithBundleIdentifier;
LSLaunchURLSpec inLaunchSpec;
inLaunchSpec.appURL = (__bridge CFURLRef) appToOpenWith;
inLaunchSpec.itemURLs = (__bridge CFArrayRef) ([NSArray arrayWithObject:theNSURLPointingToYourM4V ]);
inLaunchSpec.passThruParams = NULL;
inLaunchSpec.launchFlags = kLSLaunchDefaults; // could be done async... are we here in the main thread?
inLaunchSpec.asyncRefCon = NULL;
CFURLRef outLaunchedURL;
OSStatus diditOpen = LSOpenFromURLSpec (&inLaunchSpec, &outLaunchedURL);
if (noErr != diditOpen) {
NSLog(#"couldn't open selected items with error: %i", diditOpen);
} else {
NSLog(#"opened with: %#", [(__bridge NSURL*)outLaunchedURL description]);
}

currentDirectoryPath and NSTask

OK, let's say I'm creating a (Bash) Terminal emulator - I'm not actually, but it's pretty close in terms of description.
I've managed to get (almost) everything working, however I'm facing one simple issue: maintaining the current directory.
I mean... let's say the user runs pwd and we execute this via NSTask and /usr/bin/env bash. This outputs the current app's directory. That's fine.
Now, let's say the user enters cd ... The path is changing right? (OK, even for that particular session, but it is changing, nope?)
So, I though of storing the task's currentDirectoryPath when the Task is terminated and then re-set it when starting another Bash-related task.
However, it keeps getting the very same path (the one the app bundle is in).
What am I missing?
Any ideas on how to get this working?
The code
- (NSString*)exec:(NSArray *)args environment:(NSDictionary*)env action:(void (^)(NSString*))action completed:(void (^)(NSString*))completed
{
_task = [NSTask new];
_output = [NSPipe new];
_error = [NSPipe new];
_input = [NSPipe new];
NSFileHandle* outputF = [_output fileHandleForReading];
NSFileHandle* errorF = [_error fileHandleForReading];
__block NSString* fullOutput = #"";
NSMutableDictionary* envs = [NSMutableDictionary dictionary];
envs[#"PATH"] = [[APP environment] envPath];
[_task setLaunchPath:#"/usr/bin/env"];
if (env)
{
for (NSString* key in env)
{
envs[key] = env[key];
}
}
[_task setEnvironment:envs];
[_task setArguments:args];
[_task setStandardOutput:_output];
[_task setStandardError:_error];
[_task setStandardInput:_input];
void (^outputter)(NSFileHandle*) = ^(NSFileHandle *file){
NSData *data = [file availableData];
NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
action(str);
fullOutput = [fullOutput stringByAppendingString:str];
};
[outputF setReadabilityHandler:outputter];
[errorF setReadabilityHandler:outputter];
[_task setTerminationHandler:^(NSTask* task){
completed(fullOutput);
dispatch_async(dispatch_get_main_queue(), ^{
[[APP environment] setPwd:[task currentDirectoryPath]];
});
[task.standardOutput fileHandleForReading].readabilityHandler = nil;
[task.standardError fileHandleForReading].readabilityHandler = nil;
[task.standardInput fileHandleForWriting].writeabilityHandler = nil;
[task terminate];
task = nil;
}];
if (![[[APP environment] pwd] isEqualToString:#""])
[_task setCurrentDirectoryPath:[[APP environment] pwd]];
[_task launch];
return #"";
}
As a general matter, it is difficult to impossible to modify another process's environment and other properties from the outside. Similarly, it is not generally possible to query those from the outside. Debuggers and ps can do it by using special privileges.
The parent process which created the process in question has the opportunity to set the initial environment and properties at the point where it spawns the subprocess.
The cd command is necessarily a shell built-in command precisely because it has to modify the shell process's state. That change does not directly affect any other existing process's state. It will be inherited by subprocesses that are subsequently created.
The currentDirectoryPath property of NSTask is only meaningful at the point where the task is launched. It is the current directory that the new process will inherit. It does not track the subprocess's current directory, because it can't. Querying it only returns the value that the NSTask object was configured to use (or the default value which is the current directory of the process which created the NSTask object).
If you're trying to write something like a terminal emulator, you will need to create a long-running interactive shell subprocess with communication pipes between the parent and the shell. Don't run individual commands in separate processes. Instead, write the commands to the interactive shell over the pipe and read the subsequent output. It probably doesn't make sense to try to interpret that output since it can be general in form and not easily parsable. Just display it directly to the user.
Alternatively, you will have to interpret some commands locally in the parent process. This will be analogous to shell built-ins. So, you would have to recognize a cd command and, instead of launching an NSTask to execute it, you would just modify the state of the parent process in such a way that the new current directory will be used for subsequent tasks. That is, you could track the new current directory in a variable and set currentDirectoryPath for all subsequent NSTask objects before launching them or you could modify the parent process's current directory using -[NSFileManager changeCurrentDirectoryPath:] and that will automatically be inherited by future subprocesses.
I am fairly certain that running cd in NSTask does not change the value of task.currentDirectoryPath. Have you tried setting a break point in your dispatch call to see if that value is actually being set correctly?
Edit:
From your termination handler try doing [[APP environment] setPwd:[[[NSProcessInfo processInfo]environment]objectForKey:#"PATH"]];

Global events, the Mac App Store, and the sandbox

I'm working on an app where using global key-down events will be a requirement for its operation. Additionally, I plan on distributing this strictly via the App Store. (It's a Mac app, not iOS.) I've gotten an example of listening for the global events working via addGlobalMonitorForEventsMatchingMask, but with caveats.
Note: I am making the choice to use the modern API's and not rely on the earlier Carbon hotkey methods. In the event that they are deprecated eventually, I don't want to have to figure this problem out later.
The principle issue is that the app has to be trusted in order for global events to be detected. Otherwise, accessibility has to be enabled for all apps. When I enable accessibility, events are detected successfully. This requirement is documented here, https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/MonitoringEvents/MonitoringEvents.html.
I would prefer that for my users, they will not have to enable accessibility. From other research I've done, you can get an application to be trusted by calling AXMakeProcessTrusted, then restarting the application.
In the code that I'm using, I do not get an authentication prompt. The app will restart, but is still not trusted (likely because I don't get an authentication prompt). Here's my code for this part:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
if (!AXAPIEnabled() && !AXIsProcessTrusted()) {
NSString *appPath = [[NSBundle mainBundle] bundlePath];
AXError error = AXMakeProcessTrusted( (CFStringRef)CFBridgingRetain(appPath) );
[self restartApp];
}
}
- (void)restartApp{
NSTask *task = [[NSTask alloc] init];
NSMutableArray *args = [NSMutableArray array];
[args addObject:#"-c"];
[args addObject:[NSString stringWithFormat:#"sleep %d; open \"%#\"", 3, [[NSBundle mainBundle] bundlePath]]];
[task setLaunchPath:#"/bin/sh"];
[task setArguments:args];
[task launch];
[NSApp terminate:nil];
}
Further, I've looked at the documentation for Authorization Service Tasks here https://developer.apple.com/library/archive/documentation/Security/Conceptual/authorization_concepts/03authtasks/authtasks.html#//apple_ref/doc/uid/TP30000995-CH206-BCIGAIAG.
The first thing that worries me that pops out is this info box, "Important The authorization services API is not supported within an app sandbox because it allows privilege escalation."
If this API is required to get the authentication prompt before restarting the app, it seems that I may not be able to get global events without the accessibility feature enabled.
In summary, my specific questions are:
Is there an error in my sample code about how to get the
authentication prompt to appear?
In order to get the authentication prompt to appear, am I required
to use the Authorization Services API?
Is it possible, or not possible, to have a sandboxed app that has
access to global events?
First of all, there is no way you can automatically allow an app to use accessibility API which would work in a sandbox environment and thus in app store. The recommended way is to simply guide users so they can easily enable it themselves. The new API call AXIsProcessTrustedWithOptions is exactly for that:
NSDictionary *options = #{(id) kAXTrustedCheckOptionPrompt : #YES};
AXIsProcessTrustedWithOptions((CFDictionaryRef) options);
Now, to your first and second question (just for the sake of completeness - again it won't work in sandbox):
The idea behind AXMakeProcessTrusted was that you actually create a new auxiliary application that you run as root from the main application. This utility then calls AXMakeProcessTrusted passing in the executable of the main application. Finally you have to restart the main app. The API call has been deprecated in OSX 10.9.
To spawn a new process as a root you have to use launchd using SMJobSubmit. This will prompt a user with an authentication prompt saying that an application is trying to install a helper tool and whether it should be allowed. Concretely:
+ (BOOL)makeTrustedWithError:(NSError **)error {
NSString *label = FMTStr(#"%#.%#", kShiftItAppBundleId, #"mktrusted");
NSString *command = [[NSBundle mainBundle] pathForAuxiliaryExecutable:#"mktrusted"];
AuthorizationItem authItem = {kSMRightModifySystemDaemons, 0, NULL, 0};
AuthorizationRights authRights = {1, &authItem};
AuthorizationFlags flags = kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights;
AuthorizationRef auth;
if (AuthorizationCreate(&authRights, kAuthorizationEmptyEnvironment, flags, &auth) == errAuthorizationSuccess) {
// this is actually important - if from any reason the job was not removed, it won't relaunch
// to check for the running jobs use: sudo launchctl list
// the sudo is important since this job runs under root
SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef) label, auth, false, NULL);
// this is actually the launchd plist for a new process
// https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html#//apple_ref/doc/man/5/launchd.plist
NSDictionary *plist = #{
#"Label" : label,
#"RunAtLoad" : #YES,
#"ProgramArguments" : #[command],
#"Debug" : #YES
};
BOOL ret;
if (SMJobSubmit(kSMDomainSystemLaunchd, (CFDictionaryRef) plist, auth, (CFErrorRef *) error)) {
FMTLogDebug(#"Executed %#", command);
ret = YES;
} else {
FMTLogError(#"Failed to execute %# as priviledged process: %#", command, *error);
ret = NO;
}
// From whatever reason this did not work very well
// seems like it removed the job before it was executed
// SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef) label, auth, false, NULL);
AuthorizationFree(auth, 0);
return ret;
} else {
FMTLogError(#"Unable to create authorization object");
return NO;
}
}
As for the restarting, this is usually done also using an external utility to which waits for a main application to finish and starts it again (by using PID). If you use sparkle framework you can reuse the existing one:
+ (void) relaunch {
NSString *relaunch = [[NSBundle bundleForClass:[SUUpdater class]] pathForResource:#"relaunch" ofType:#""];
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *pid = FMTStr(#"%d", [[NSProcessInfo processInfo] processIdentifier]);
[NSTask launchedTaskWithLaunchPath:relaunch arguments:#[path, pid]];
[NSApp terminate:self];
}
Another option is to hack the /Library/Application Support/com.apple.TCC/TCC.db sqlite database add the permissions manually using an auxiliary helper:
NSString *sqlite = #"/usr/bin/sqlite3";
NSString *sql = FMTStr(#"INSERT or REPLACE INTO access values ('kTCCServiceAccessibility', '%#', 1, 1, 1, NULL);", MY_BUNDLE_ID);
NSArray *args = #[#"/Library/Application Support/com.apple.TCC/TCC.db", sql];
NSTask *task = [NSTask launchedTaskWithLaunchPath:sqlite arguments:args];
[task waitUntilExit];
This however will disqualify the app from being app store. More over it is really just a hack and the db / schema can change any time. Some applications (e.g. Divvy.app used to do this) used this hack within the application installer post install script. This way prevents the dialog telling that an app is requesting to install an auxiliary tool.
Basically, MAS restrictions will require you to the route of having tge user turning on AX for all.
I found a potential solution on GitHub.
https://github.com/K8TIY/CW-Station
It has an auxiliary application which would be run at root to request access for the main application. It is a little outdated and is using some functions which have been deprecated so I am working on modernizing it. It looks like a good starting point.

Is there a way to programmatically connect to a remote server from Cocoa?

Is there an Coca/obj-C API call to mimic the "Connect to Server" action in Finder? It's possible with Automater, so it seems like Finder has a hook somewhere.
Turns out there's an old Carbon function (can't find a Cocoa equivalent) called FSMountServerVolumeSync which does what I was looking for. You can supply and smb:// URL and login credentials.
File Manager Reference
OSStatus FSMountServerVolumeSync (
CFURLRef url,
CFURLRef mountDir,
CFStringRef user,
CFStringRef password,
FSVolumeRefNum *mountedVolumeRefNum,
OptionBits flags
);
An easy way is to just run some applescript code. I'll show you 2 choices. This first one is the standard way to show that Finder window from applescript.
NSString* cmd = #"choose URL";
The resulting window is bare-bones though, so you can actually open the Finder's window with this command...
NSString* cmd = #"tell application \"Finder\" to activate\ndelay 0.2\ntell application \"System Events\" to keystroke \"k\" using command down";
After choosing either of the "cmd" strings, you can execute that applescript code with this...
NSAppleScript* theScript = [[NSAppleScript alloc] initWithSource:cmd];
[theScript executeAndReturnError:nil];
[theScript release];
This might not be the best way, but can't you just use mount?