How to launch Finder Sync Extension on launching the main app? - objective-c

In my Cocoa application, I have a finder sync extension.
On launching the application, my finder sync extension doesn't start automatically.
I need to go to System Preferences -> Extensions and enable it.
How do i make sure that on launch of my main application (.app) file the finder sync extension is launched and is enabled?

Checkout https://blog.codecentric.de/en/2018/09/finder-sync-extension/
There is a section Restarting FinderSyncExtension on app launch with instructions on how to restart FinderSyncExtension on app launch and thus make it more reliable:
+ (void) restart
{
NSString* bundleID = NSBundle.mainBundle.bundleIdentifier;
NSString* extBundleID = [NSString stringWithFormat:#"%#.FinderSyncExt", bundleID];
NSArray<NSRunningApplication*>* apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:extBundleID];
ASTEach(apps, ^(NSRunningApplication* app) {
NSString* killCommand = [NSString stringWithFormat:#"kill -s 9 %d", app.processIdentifier];
system(killCommand.UTF8String);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSString* runCommand = [NSString stringWithFormat:#"pluginkit -e use -i %#", extBundleID];
system(runCommand.UTF8String);
});
}

Related

How to view app logging when ui testing

When running UI tests on my iOS app on our Jenkins CI slave, the UI is incorrect and my tests fail. Locally, the same UI is as expected and the tests pass.
I haven't been able to figure this out and I cannot debug it as it is on a remote machine. So, I would like to add logging to see what decisions are being made by the app to figure out why the UI is displaying incorrectly in the remote Jenkins slave.
The test log output does not contain log output from the app (via NSLog), nor can I find any of the strings from the appNSLog calls in any .xcactivitylog file in the derived data output folders.
Where can I find my logging output from my app when it is being run by the UI tests? This is with Xcode 8.2 running a iOS 9.3 simulated device.
The following solution is a bit hacky but allows you to get the logs even when running on the device.
Logger.h
#define Debug(...) _Debug(__VA_ARGS__)
void _Debug(NSString *format, ...);
Logger.m
void _Debug(NSString *format, ...)
{
format = [NSString stringWithFormat:#"[Debug]: %#", format];
va_list args;
va_start(args, format);
NSLogv(format, args); // logs to the console
NSString *logMessage = [[NSString alloc] initWithFormat:format arguments:arguments];
UIApplication *application = [UIApplication sharedApplication];
UIWindow *window = [application keyWindow];
window.accessibilityValue = [NSString stringWithFormat:#"%#\n%#", window.accessibilityValue, logMessage];
va_end(args);
}
Then from the UI Test, I can access the logs like this
+ (NSString *)logs
{
XCUIElementQuery *windowQuery = [[[XCUIApplication alloc] init] windows];
return [windowQuery elementAtIndex:0].value;
}

How to resume multiple background downloads using AFURLSessionManager in iOS app

I have an iOS app that provides multiple book download at the same time, Its a reader app actually. Earlier I was using AFDownloadRequestOperation for download the book. When user launch the app after app crashes or after user force quit (kill) the app, It automatically resume the download from the offset where it left downloading and for this to achieve I was using the following method:
AFDownloadRequestOperation* operation = [[AFDownloadRequestOperation
alloc] initWithRequest:request targetPath:path shouldResume:YES];
Now I am using AFNetworking 2.0 API AFHTTPSessionManager for background download. I have created the background session in the following way:
NSString* strIdentifier = [NSString stringWithFormat:#"someUniqueIdentifier"];
NSURLSessionConfiguration *sessionConfig;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >=8.0f)
{
sessionConfig =[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:strIdentifier];
}
else
{
sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:strIdentifier];
}
self.backgroundSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:sessionConfig];
Then I have started the background download task as follows with delegate callbacks:
self.bookDownloadTask = [self.backgroundSessionManager downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil];
[self.bookDownloadTask resume];
So again considering the earlier situation -
When user launch the app after app crashes or after user force quit (kill) the app, It does automatically start (not resume) the download but not from the offset where it left downloading, it start downloading form starting of the book.
Is there any way to automatically resume the download from the offset where it left downloading using AFHTTPSessionManager ??
Any help would be appreciated. Thanks !!

Cocoa app crashes when trying to open a save dialog: Timeout occured while waiting for the window

I wrote an HTML5 app and I'm making a wrapper for it in Cocoa for the Mac. I'm in the process of writing a method that will take a url and let the user save it to their disk. Here is the method so far:
- (void) downloadFile: (NSString *) url {
NSSavePanel * savePanel = [NSSavePanel savePanel];
[savePanel setAllowedFileTypes: #[#"png"]];
[savePanel setNameFieldStringValue:#"test"];
[savePanel beginWithCompletionHandler:^(NSInteger result){
//NSArray * files = [[openDlg URLs] valueForKey: #"relativePath"];
//[resultListener chooseFilenames: files] ;
}];
[savePanel runModal];
}
However when I invoke the method (long after the window is made) my app crashes and I get this error in the console: 2013-07-21 18:23:05.067 Reditr[31458:d11f] RVS:__54-[NSRemoteSavePanel _runOrderingOperationWithContext:]_block_invoke_0319 : Timeout occured while waiting for the window
You don't want to use both -runModal and -beginWithCompletionHandler:. Use one or the other (preferably the latter).

Close main application if "helper" application closed

I have a wrapper application, which opens an other application, let's call it helper app. This app is inside the wrapper application Resource folder.
I would like to close the main application, after the helper app was closed. How can I achieve this?
I would like to also know, how can I quit the wrapper app, after the helper opened.
Are you developing for OSX (10.6 and later ) ?
In this case use NSRunningApplication to terminate the wrapper application.
#implementation AppDelegate // Helper Application delegate
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:#"com.yourApplication.Launcher"];
if ([apps count])
[(NSRunningApplication *)[apps objectAtIndex:0] terminate];
}
#end
And you can launch your Helper synchronously with NSWorkspace :
- (void)launchHelper // Wrapper launch method
{
NSURL *helperURL = [[NSBundle mainBundle] URLForResource:#"Helper" withExtension:#"app"];
if (helperURL)
{
NSError *err = nil;
[[NSWorkspace sharedWorkspace] launchApplicationAtURL:helprURL options:NSWorkspaceLaunchAllowingClassicStartup configuration:nil error:&err];
// here Helper 'main' have been called
}
}
short answer - you can't!
Every application work in sandbox mode. You can call other application but you lose control on your (of course you can use AppDelegate standard method to handle enter/back foreground/bacground actions but that's it).

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.