Cocoa App that deletes itself - macos

I'm writing a Mac OSX application using Cocoa that is designed to stop working after a specified date, to avoid the user simply changing their system clock and then re-running the app I would like the program to close and delete itself if it is loaded after the expiry date. Is this possible?
I am distributing the application directly not through the app store. Also, checking the date using the internet isn't really an option because the app needs to be useable offline.
Thanks,
Matthew

It's possible, but not reliably so. To delete your app, just get your main bundle's URL and tell NSFileManager to delete it. But your app bundle may not be writable — and thus not deletable either — and the user may have any number of backups even if you do manage to delete it. I would not write anything that depends on this being possible unless I had tight control over the systems the program will run on. (I mean, I probably wouldn't write something that does this anyway, because it's a little crazy. But if I were going to write something like this, it would have to be something that only runs on my own systems.)

You could perform some sanity checks in the system to get an idea whether the user manually set the clock back to the past.
Note that I still don't think the plan of (maliciously) deleting user files is a great idea in general and the following approach in particular will certainly break under Sandboxing..
..but out of curiosity: Here's a snippet that will check all files in /var/log and return whether some of them have been modified in the future (= the system is quite likely running "in the past")
- (bool)isFakeSystemTime
{
int futureFileCount = 0;
// let's check against 1 day from now in the future to be safe
NSTimeInterval secondsPerDay = 24 * 60 * 60;
NSDate *tomorrow = [[[NSDate alloc] initWithTimeIntervalSinceNow:secondsPerDay] autorelease];
NSString *directoryPath = #"/var/log";
NSArray *filesInDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:directoryPath error:nil];
for (NSString* fileName in filesInDirectory)
{
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[directoryPath stringByAppendingPathComponent:fileName] error:nil];
NSDate *date = [attributes valueForKey:#"NSFileModificationDate"];
if (!date)
continue;
if ([date compare:tomorrow] == NSOrderedDescending)
{
NSLog(#"File '%#' modified >=1 day in the future", fileName);
futureFileCount++;
}
}
// again, some heuristic to be (more) on the safe side
return futureFileCount > 5;
}

Related

Know if MainApp is launched by HelperApp

I am really stuck here with a simple task, but still cannot get it working.
I've managed to implement 'LaunchAtLogin' feature with the HelperApp as described in this article.
But my application should react differently on launch-by-helper-app and launch-by-user actions. So my task now is to have some kind of flag indicating that MainApp was launched by HelperApp.
I know there are many similar questions, but still none of the solutions works for me. Sandbox seems to cut all the parameters I am trying to send to the MainApp.
Here what I have tried:
- [NSWorkspace - launchApplicationAtURL: options: configuration: error:]
- [NSWorkspace - openURLs: withAppBundleIdentifier: options: additionalEventParamDescriptor: launchIdentifiers:]
- LSOpenApplication()
Until recently I thought I can rely on -psn argument sent by Finder when user manually launches the application. But this argument is sent on 10.8 even when MainApp is launched by HelperApp.
In the article mentioned above, author suggests using [NSWorkspace - launchApplicationAtURL: options: configuration: error:]. Parameters are sent, but nothing arrives to the MainApp.
Has anyone succeeded to accomplish this (or similar) task?
Need help!
Thanks in advance!
After hell of searching and experimenting I am ready to answer my own question, so others can save their time and efforts.
My conclusion is that for now there is no way HelperApp can launch MainApp with some arguments under the Sandbox. At least I have not found any way to do that.
Launch MainApp like this:
[[NSWorkspace sharedWorkspace] launchApplication:newPath];
In MainApp add the following:
Application_IsLaunchedByHelperApp = YES;
ProcessSerialNumber currPSN;
OSStatus err = GetCurrentProcess(&currPSN);
if (!err)
{
// Get information about our process
NSDictionary * currDict = [(NSDictionary *)ProcessInformationCopyDictionary(&currPSN,
kProcessDictionaryIncludeAllInformationMask) autorelease];
// Get the PSN of the app that launched us. Its not really the parent app, in the unix sense.
long long temp = [[currDict objectForKey:#"ParentPSN"] longLongValue];
long long hi = (temp >> 32) & 0x00000000FFFFFFFFLL;
long long lo = (temp >> 0) & 0x00000000FFFFFFFFLL;
ProcessSerialNumber parentPSN = {(UInt32)hi, (UInt32)lo};
// Get info on the launching process
NSDictionary * parentDict = [(NSDictionary*)ProcessInformationCopyDictionary(&parentPSN,
kProcessDictionaryIncludeAllInformationMask) autorelease];
// analyze
// parent app info is not null ?
if (parentDict && parentDict.count > 0)
{
NSString * launchedByAppBundleId = [parentDict objectForKey:#"CFBundleIdentifier"];
if (![launchedByAppBundleId isEqualToString:HELPER_APP_BUNDLE_ID])
{
Application_IsLaunchedByHelperApp = NO;
}
}
}
That's it. Application_IsLaunchedByHelperApp now has correct value.
The solution is not mine. I've found it on the web (cocoabuilder, I guess). Good luck to everyone! And thanks for your attention to my questions.
UPDATE
Looks like there are cases when launched at login app shows launchedByAppBundleId = #"com.apple.loginwindow". So the last part of code will look like this:
//
// analyze
//
// parent app info is not null ?
if (parentDict && parentDict.count > 0)
{
NSString * launchedByAppBundleId = [parentDict objectForKey:#"CFBundleIdentifier"];
if (![launchedByAppBundleId isEqualToString:HELPER_APP_BUNDLE_ID] &&
![launchedByAppBundleId isEqualToString:#"com.apple.loginwindow"])
{
Application_IsLaunchedByHelperApp = NO;
}
}
The place to seek an answer is the Apple Developer Forums - folk there have dealt with all sorts of issues around helper apps and the sandbox. Searching for application groups and custom URL schemes going back, say, 1-2 years may turn up a solution that meets your needs. If you are still stuck post a question on those forums, someone will probably know what you need.
If you are not an Apple Developer so have no access to those forums, or do not intend to distribute via the Mac App Store, then just turn off the sandbox - in its current state this isn't technology you choose to use... HTH.

How to properly save a QTMovie after editing using QTKit?

I am making minor edits to a QTMovie in an application using NSDocument architecture (such as adding a track, as shown below). After the edit, I want to save to the original file. However, I keep getting a 'file is busy' error. I assume this is due to some oversight I made in the handling of the files, or a failure in how I am using NSDocument. Any tips would be helpful! Here is (some of) the relevant code:
// open file that has the track I want to add to my movie
QTMovie* tempMovie = [QTMovie movieWithURL:outputFileURL error:nil];
// Use old API to add the track
AddMovieSelection([movie quickTimeMovie], [tempMovie quickTimeMovie]);
// get the filename from the NSDocument
NSString *filename = [[self fileURL] path];
NSLog(#"writing to filename: %#", filename);
// FLATTEN the movie file so I don't get external references
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:1];
[attributes setObject:[NSNumber numberWithBool:YES] forKey:QTMovieFlatten];
// attempt to write
NSError *error;
// this is where I get the "file is busy"
if (![movie writeToFile:filename withAttributes:attributes error:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
NSRunAlertPanel(#"Error", [error localizedDescription], nil, nil, nil);
}
Do I have to first release the movie in my NSDocument? What is the "proper" way to do that? Keep in mind, I am not necessarily finished with this document, I am not closing it. I have just finished this operation, and I want the file on disk to reflect my changes. I would love to use [movie updateMovieFile], but that call doesn't seem to flatten the movie. I don't want any external references in my file.
I am not too familiar with the QuickTime C API, so I honestly can't tell you anything about what is going wrong there. Absolute guesswork: Maybe a call to EndMediaEdits is missing?
Though that shouldn't be required by AddMovieSelection, you said "[...] such as adding a track [...]". So maybe there is something else going on, like AddMediaSample or something similar?
That said, if you don't need to target anything below 10.5 and all you need to do is add some segment from another movie, you can achieve that without dropping down to the C API:
Have a look at
-[QTMovie insertSegmentOfTrack:fromRange:scaledToRange:]
and
-[QTMovie insertSegmentOfMovie:fromRange:scaledToRange:], if you want to have the inserted segment "overlayed" (temporally speaking).
-[QTMovie insertSegmentOfMovie:timeRange:atTime:] and -[QTMovie insertSegmentOfTrack:timeRange:atTime:], if you want { movieA.firstPart, movieB, movieA.secondPart }.
Do I have to first release the movie in my NSDocument?
You mean in order to write it to disk? No: That should even result in a crash.
The role of release is to handle memory-management. It doesn't have much to do with the busy-state of a file.
Turns out I just wasn't using the NSDocument architecture properly. When I changed it to use Save/SaveAs properly, this problem went away.

Cocoa: Correct way to get list of possible PlugIns directories for an app?

In a Cocoa app generally we can install a plugin bundle in one of a number of places. If for example the app is called "MyApp" you'd be able to install the plugin at:
/Applications/MyApp.app/Contents/PlugIns
~/Library/Application Support/MyApp/PlugIns
/Library/Application Support/MyApp/PlugIns
/Network/Library/Application Support/MyApp/PlugIns
I'm building an NSArray of paths to search in the correct order but I'm pretty sure I'm doing this wrong since it feels like I'm doing too much work for something Apple seem to provide a lot of functions for.
NSArray *systemSearchPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSAllDomainsMask, YES);
NSMutableArray *searchPaths = [NSMutableArray array];
NSFileManager *fileManager = [NSFileManager defaultManager];
for (NSString *systemPath in systemSearchPaths) {
NSString *systemPluginsPath = [systemPath stringByAppendingPathComponent:#"PlugIns"];
// FIXME: Remove debug code
NSLog(#"Considering plugin path %#", systemPluginsPath);
if ([fileManager fileExistsAtPath:systemPluginsPath]) {
[searchPaths addObject:systemPluginsPath];
}
}
[searchPaths addObject:[[NSBundle mainBundle] builtInPlugInsPath]];
This results in the Array returned by NSSearchPathForDirectoriesInDomains, with the builtInPlugInsPath value appended to the end.
However, it actually searches directories like "~/Library/Application Support/PlugIns" (missing the "MyApp") folder. Before I start hacking the code to inject the name of my application (which is subject to change at any time), am I doing this wrong?
Is there a way to just tell Cocoa "give me all search paths for 'PlugIns'" directories for this application"?
Nope. You're doing it right.
You can get the name of your application at run time by asking the main bundle for its info dictionary and looking for kCFBundleNameKey therein. When you rename your application, change the bundle name in your Info.plist.
(Definitely do not use your application's filename, as that's much more fragile.)
Be aware that users might not like it if the plug-ins they installed stop working because you renamed your application.
Note that your code above will not catch the PlugIns folder inside the application bundle. For that, ask your main bundle for its built-in plug-ins path or URL.

How do I share a Core Data store between processes using NSDistributedNotifications?

Background
I've already posted a question about the basics of sharing a Core Data store between processes.
I'm trying to implement the recommendations given and I'm running into problems.
My Goal
I have two processes - the Helper App and the UI. They both share a single data store. I want the UI to update it's NSManagedObjectContext when the Helper App has saved new data to the store.
Current Program Flow
The Helper App Process writes data to the Store.
In the Helper App, I listen for NSManagedObjectContextDidSaveNotification notifications.
When the context is saved, I encode the inserted, deleted and updated objects using their URI representations and NSArchiver.
I send an NSNotification to the NSDistributedNotificationCenter with this encoded dictionary as the userInfo.
The UI Process is listening for the save notification. When it receives the notification, it unarchives the userInfo using NSUnarchiver.
It looks up all the updated/inserted/deleted objects from the URIs given and replaces them with NSManagedObjects.
It constructs an NSNotification with the updated/inserted/deleted objects.
I call mergeChangesFromContextDidSaveNotification: on the Managed Object Context of the UI Process, passing in the NSNotification I constructed in the previous step.
The Problem
Inserted objects are faulted into the UI Managed Object Context fine and they appear in the UI. The problem comes with updated objects. They just don't update.
What I've tried
The most obvious thing to try would
be to pass the save Notification
from the Helper App process to the
UI process. Easy, right? Well, no.
Distributed Notifications won't
allow me to do that as the userInfo
dictionary is not in the right
format. That's why I'm doing all the
NSArchiving stuff.
I've tried calling
refreshObject:mergeChanges:YES on
the NSManagedObjects to be updated,
but this doesn't seem to have any
effect.
I've tried performing the
mergeChangesFromContextDidSaveNotification:
selector on the main thread and the
current thread. Neither seems to
affect the result.
I've tried using
mergeChangesFromContextDidSaveNotification:
before between threads, which of
course is much simpler and it worked
perfectly. But I need this same
functionality between processes.
Alternatives?
Am I missing something here? I'm consistently getting the feeling I'm making this much more complex than it needs to be, but after reading the documentation several times and spending a few solid days on this, I can't see any other way of refreshing the MOC of the UI.
Is there a more elegant way of doing this? Or am I just making a silly mistake somewhere in my code?
The Code
I've tried to make it as readable as possible, but it's still a mess. Sorry.
Helper App Code
-(void)workerThreadObjectContextDidSave:(NSNotification *)saveNotification {
NSMutableDictionary *savedObjectsEncodedURIs = [NSMutableDictionary dictionary];
NSArray *savedObjectKeys = [[saveNotification userInfo] allKeys];
for(NSString *thisSavedObjectKey in savedObjectKeys) {
// This is the set of updated/inserted/deleted NSManagedObjects.
NSSet *thisSavedObjectSet = [[saveNotification userInfo] objectForKey:thisSavedObjectKey];
NSMutableSet *thisSavedObjectSetEncoded = [NSMutableSet set];
for(id thisSavedObject in [thisSavedObjectSet allObjects]) {
// Construct a set of URIs that will be encoded as NSData
NSURL *thisSavedObjectURI = [[(NSManagedObject *)thisSavedObject objectID] URIRepresentation];
[thisSavedObjectSetEncoded addObject:thisSavedObjectURI];
}
// Archive the set of URIs.
[savedObjectsEncodedURIs setObject:[NSArchiver archivedDataWithRootObject:thisSavedObjectSetEncoded] forKey:thisSavedObjectKey];
}
if ([[savedObjectsEncodedURIs allValues] count] > 0) {
// Tell UI process there are new objects that need merging into it's MOC
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:#"com.synapticmishap.lapsus.save" object:#"HelperApp" userInfo:(NSDictionary *)savedObjectsEncodedURIs];
}
}
UI Code
-(void)mergeSavesIntoMOC:(NSNotification *)notification {
NSDictionary *objectsToRefresh = [notification userInfo];
NSMutableDictionary *notificationUserInfo = [NSMutableDictionary dictionary];
NSArray *savedObjectKeys = [[notification userInfo] allKeys];
for(NSString *thisSavedObjectKey in savedObjectKeys) {
// Iterate through all the URIs in the decoded set. For each URI, get the NSManagedObject and add it to a set.
NSSet *thisSavedObjectSetDecoded = [NSUnarchiver unarchiveObjectWithData:[[notification userInfo] objectForKey:thisSavedObjectKey]];
NSMutableSet *savedManagedObjectSet = [NSMutableSet set];
for(NSURL *thisSavedObjectURI in thisSavedObjectSetDecoded) {
NSManagedObject *thisSavedManagedObject = [managedObjectContext objectWithID:[persistentStoreCoordinator managedObjectIDForURIRepresentation:thisSavedObjectURI]];
[savedManagedObjectSet addObject:thisSavedManagedObject];
// If the object is to be updated, refresh the object and merge in changes.
// This doesn't work!
if ([thisSavedObjectKey isEqualToString:NSUpdatedObjectsKey]) {
[managedObjectContext refreshObject:thisSavedManagedObject mergeChanges:YES];
[managedObjectContext save:nil];
}
}
[notificationUserInfo setObject:savedManagedObjectSet forKey:thisSavedObjectKey];
}
// Build a notification suitable for merging changes into MOC.
NSNotification *saveNotification = [NSNotification notificationWithName:#"" object:nil userInfo:(NSDictionary *)notificationUserInfo];
[managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:YES];
}
I used the method in
http://www.mlsite.net/blog/?p=518
then every object is correctly faulted but the faults are fetch in cache so still no update
I had to do
[moc stalenessInterval = 0];
And it finally worked, with relationship.
You're looking for - (void)refreshObject:(NSManagedObject *)object mergeChanges:(BOOL)flag I believe.
This will refresh the object with the info in the persistent store, merging changes if you want.
I'd go with Mike's suggestion and just watch the store file for changes.
Though it may not be the most efficient, I've had success using - [NSManagedObjectContext reset] from a second process when there's a change to a store. In my case case, the code is fairly linear — all I do is run a fetch request for some data after resetting. I don't know how this will work with bindings and a complicated UI, but you may be able to post a notification to manually update things if it's not handled automatically.
I had this exact same issue with an iPhone app that I've been working on. In my case, the solution involved setting the Context's stalenessInterval to something suitably infinitesimal (e.g., 0.5 seconds).
This works, except for sandboxes apps. You can't send a notification with a user info dict. Instead consider some other IPC like XPC or DO.
On a side note, using NSDustributedNotificationCenter is not always 100% if the system is busy.
Setting stalenessInterval of managed object context works. My case involves multiple threads instead of process though.
Starting with iOS 9, you should now use mergeChangesFromRemoteContextSave:intoContexts:. See this for an explanation: https://www.innoq.com/en/blog/ios-writing-core-data-in-today-extension/

Mounting a folder as a device in Finder using Cocoa

Is there a way to mount a folder on the hard disk as a device in Finder. The intend here is to provide the user with an easy way to get to a folder that my application uses to store data. I don't want my user to go searching for data in Application Data. I would rather allow them to make this data available as a mounted volume or device in Finder. I would also like this volume or device to be read/write, so that if the user makes any changes to the data files, the changes will get reflected in the original folder.
Is there a way to do this in cocoa, carbon or applescript.
Try looking into FUSE. You can have all sorts of psuedo filesystems with that.
But I'd caution a little against what you are trying to do. It may make more sense to just have a button that opens the folder in your application, rather than create a new device. I personally would find it hard to continue to use an application that does such a thing. It doesn't really fit with the rest of the available applications.
You could also use an alias to point to your Application Data directory.
You can use sparse disk image to create "fake" drive.
But why not make data directory configurable in your application? Or use subdirectory in ~/Documents/?
Alias/symlink on desktop will be the easiest solution:
ln -s '~/Application Data/Yourapp' '~/Desktop/Yourapp Data'
Can I suggest rethinking this entirely? A symlink or alias would work, but, if possible, a better idea would be to register for the filetypes people will be moving into that folder, and then respond to opening them by moving or copying them to the correct folder. I'm thinking of something like the Dashboard interface, where if you double-click a downloaded .wdgt file, it asks if you want to 'install' the widget and then, if you do, copies it into ~/Library/Widgets. Obviously, if you're dealing with common types like images, folders, or generic text files, this might be impractical.
For implementation, you'd just add the document types to your Info.plist, and handle them in you App Delegate's -application:openFile: method.
I would also urge caution with this, seems potentially somewhat confusing to most users. That said, have you considered simply creating a softlink to the directory in question?
I Do it using NSWorkspace. In my case I make an initial check with the function -(BOOL)isMountedPath;
The code for mounting is:
NSURL *path=[NSURL URLWithString:#"smb://server.resource/KEYS_DB"];
if(NO==[self isMountedPath:[path absoluteString]])
{
NSWorkspace *ws=[NSWorkspace sharedWorkspace];
[ws openURL:path];
}
The code to check if a path is mounted is:
-(BOOL)isMountedPath:(NSString *)share
{
NSArray * keys = #[NSURLVolumeURLForRemountingKey];
NSArray * mountPaths = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:keys options:0];
NSError * error;
NSURL * remount;
for (NSURL * mountPath in mountPaths) {
[mountPath getResourceValue:&remount forKey:NSURLVolumeURLForRemountingKey error:&error];
if(remount){
if ([[[NSURL URLWithString:share] host] isEqualToString:[remount host]] && [[[NSURL URLWithString:share] path] isEqualToString:[remount path]])
{
printf("Already mounted at %s\n", [[mountPath path] UTF8String]);
return YES;
}
}
}
return NO;
}
Other possible useful method is:
-(NSString *)mountedPath:(NSString *)share
{
NSArray * keys = #[NSURLVolumeURLForRemountingKey];
NSArray * mountPaths = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:keys options:0];
NSError * error;
NSURL * remount;
for (NSURL * mountPath in mountPaths) {
[mountPath getResourceValue:&remount forKey:NSURLVolumeURLForRemountingKey error:&error];
if(remount){
if ([[[NSURL URLWithString:share] host] isEqualToString:[remount host]] && [[[NSURL URLWithString:share] path] isEqualToString:[remount path]])
{
printf("Already mounted at %s\n", [[mountPath path] UTF8String]);
return [mountPath path];
}
}
}
return nil;
}

Resources