Communication between Main Application and loadable bundle - objective-c

OK, it's rather self-explanatory.
Let's say we've got our cocoa application.
Let's all assume that you've already got some "plugins", packaged as independent loadable bundles.
This is how bundles are currently being loaded (given that the plugin's "principal class" is actually an NSWindowController subclass :
// Load the bundle
NSString* bundlePath = [[NSBundle mainBundle] pathForResource:#"BundlePlugin"
ofType:#"bundle"]
NSBundle* b = [NSBundle bundleWithPath:bundlePath];
NSError* err = nil;
[b loadAndReturnError:&err];
if (!err)
{
// if everything goes fine, initialise the main controller
Class mainWindowControllerClass = [b principalClass];
id mainWindowController = [[mainWindowControllerClass alloc] initWithWindowNibName:#"PluginWindow"];
}
Now, here's the catch :
How do I "publish" some of my main app's objects to the plugin?
How do I make my plugin "know" about my main app's classes?
Is it even possible to actually establish some sort of communication between them, so that e.g. the plugin can interact with my main app? And if so, how?
SIDENOTES :
Could using Protocols eliminate the "unknown selector" errors and the need for extensive use of performSelector:withObject:?
Obviously I can set and get value to-and-from my newly created mainWindowController as long as they're defined as properties. But is this the most Cocoa-friendly way?

Related

Cocoa get current running path of SIMBL Plugin

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.

iOS 11 - Core Data - UIColor no longers works as transformable attribute

I store colours in my binary Core Data store using a transformable attribute, specifying the class of the attribute as UIColor like so:
#import "CoreDataEntity+CoreDataClass.h"
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#interface CoreDataEntity (CoreDataProperties)
+ (NSFetchRequest<CoreDataEntity *> *)fetchRequest;
#property (nullable, nonatomic, retain) UIColor *transformable;
#property (nullable, nonatomic, copy) NSString *string;
#end
NS_ASSUME_NONNULL_END
In the iOS 11 Beta this has stopped working with an error like this :
NSUnderlyingException=value for key 'NS.objects' was of unexpected class 'UIColor'. Allowed classes are '{(\n NSDecimalNumber,\n NSData,\n NSUUID,\n NSNumber,\n NSDate,\n NSArray,\n NSOrderedSet,\n NSDictionaryMapNode,\n NSString,\n NSSet,\n NSDictionary,\n NSURL,\n NSNull\n)}'.}";
NSUnderlyingException = "Can't read binary data from file";
}
I managed to replicate the specific problem in an XCode project on GitHub (Must be run with the XCode Beta twice to get the error).
In the demo project the store type is controlled by NSPersistentStoreDescription and setting it to NSBinaryStoreType, which I do in the AppDelegate in the exanple project, and I add objects in application didFinishLaunchingWithOptions, otherwise it's the standard template from an iOS11 app with core data. Plus a small datamodel and classes.
If you run the project twice, the first time it creates the datastore and everything is fine. The second time, the datastore tries to open and crashes the app. This problem only seems to be related to binary datastores from what I can tell, if I use an SQL backed datastore it works. However, my app is in the wild and uses binary.
I've reported it to Apple as a bug and sought help on the developer forums, but Apple has not acknowledged the bug and no help was coming.
I'm getting a bit worried as the iOS11 release date draws nearer and I have no solution, my app just won't work in iOS11.
I've tried changing the property to NSData and seeing if it was possible to just unarchive the data, but it seems it's still stored internally as a UIColor somehow and the database just won't open.
Can anyone see a workaround? I have the app in the wild, and possibly pushing out an update to convert the datastores before iOS11 could work for some, but that isn't going to guarantee all users get the fix and they could lose their data.
EDIT 1:
Radar number : 33895450
EDIT 2:
It just occured to me that this applies to any transformable attribute in core data, the values supported in the error message are just the default property types.
EDIT 3:
Just out of curiosity I filled out all the fields for the transformable attribute (it was never required before).
I added "NSKeyedUnarchiveFromData" to value transformer name of the core data entity, it should be the default, but you never know. No effect. It must be using the value transformer anyway to know that it's a UIColor.
I filled in the custom class field to be UIColor, no effect.
Edit 5 : I noticed earlier that UIColor now supports NSSecureCoding, should security somehow be the issue somehow overlooked in the other store typed.
Edit : Now that iOS is released, i’ve used one of my TSIs to further escalate this. Do i get them back if i have to use one to get them to fix their software?
Edit : Apple got back to me on my TSI, they said it’s under investigation, there is no workaround, and to wait on the bug. They refunded my TSI because they couldn’t help.
Edit 8: Same problem on macOS High Sierra, with NSColor instead of UIColor.
Apple still have not given me any feedback on my actual bug report.
Well Apple got back to me, there are new persistentStore options!
The text I got from apple:
/* Allows developers to provide an additional set of classes (which
must implement NSSecureCoding) that should be used while decoding a
binary store. Using this option is preferable to using
NSBinaryStoreInsecureDecodingCompatibilityOption.
*/ COREDATA_EXTERN NSString * const NSBinaryStoreSecureDecodingClasses
API_AVAILABLE(macosx(10.13),ios(11.0),tvos(11.0),watchos(4.0));
/* Indicate that the binary store should be decoded insecurely. This
may be necessary if a store has metadata or transformable properties
containing non-standard classes. If possible, developers should use
the NSBinaryStoreSecureDecodingClasses option to specify the contained
classes, allowing the binary store to to be securely decoded.
Applications linked before the availability date will default to using
this option.
*/ COREDATA_EXTERN NSString * const NSBinaryStoreInsecureDecodingCompatibilityOption
API_AVAILABLE(macosx(10.13),ios(11.0),tvos(11.0),watchos(4.0));
It's not immediately clear, but basically you have to supply an NSSet of classes you use as transformable attributes that comply with NSSecureCoding as an option when opening your persistent store.
An example for mine using the UIColor :
NSError *localError;
NSDictionary *options;
if (#available(iOS 11.0, *)) {
options = #{
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES,
NSBinaryStoreSecureDecodingClasses : [NSSet setWithObjects:[UIColor class], nil]
};
} else {
// Fallback on earlier versions
options = #{
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES,
};
}
NSPersistentStore *newStore = [self.psc addPersistentStoreWithType:NSBinaryStoreType configuration:#"iOS" URL:psURL options:options error:&localError];
EDIT: Adding a solution for the newer way to open core data persistent stores using NSPersistentStoreDescription. This code is based on the current core data template.
- (NSPersistentContainer *)persistentContainer {
// The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
#synchronized (self) {
if (_persistentContainer == nil) {
NSURL *defaultURL = [NSPersistentContainer defaultDirectoryURL];
defaultURL = [defaultURL URLByAppendingPathComponent:#"CoreDataTransformableAttribBug.binary"];
_persistentContainer = [[NSPersistentContainer alloc] initWithName:#"CoreDataTransformableAttribBug"];
NSPersistentStoreDescription *desc = [NSPersistentStoreDescription persistentStoreDescriptionWithURL:defaultURL];
desc.type = NSBinaryStoreType;
if (#available(iOS 11.0, *)) {
[desc setOption:[NSSet setWithObjects:[UIColor class], nil] forKey:NSBinaryStoreSecureDecodingClasses];
}
_persistentContainer.persistentStoreDescriptions = #[desc];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
NSLog(#"Unresolved error %#, %#", error, error.userInfo);
abort();
} else {
NSLog(#"Description = %#", storeDescription);
}
}];
}
}
return _persistentContainer;
}
I also updated my gitHub project with the fix in a branch
George did all the hard work. I only applied it to Swift. Here is my solution. I put it into my NSPersistentDocument descendant.
override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
var options = storeOptions != nil ? storeOptions! : [String:Any]()
if #available(OSX 10.13, *) {
options[NSBinaryStoreSecureDecodingClasses] = NSSet(object: NSColor.self)
}
options[NSMigratePersistentStoresAutomaticallyOption] = true
options[NSInferMappingModelAutomaticallyOption] = true
try super.configurePersistentStoreCoordinator(for: url, ofType: fileType, modelConfiguration: configuration, storeOptions: options)
}
Now I can read my files again. Thanks George!

How do I load a nib from a secondary process?

I have a process spawned with NSTask that needs to display a window. I started out just writing all the UI code by hand, but this has become a pain.
So, I created a new class with a xib, MyWindowController. I want to load up an instance of this controller in secondary process and have all the IBOutlets and whatnot work properly.
Here's what I've got so far:
// Get the bundle for the main application (not the subprocess).The executable lives in Contents/Helpers, so look two dirs up from its path for the main app bundle root.
NSArray *executablePathComponents = [[[NSBundle mainBundle] executableURL] pathComponents];
NSIndexSet *indexOfEveryComponentExceptLastTwo = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [executablePathComponents count] - 2)];
NSBundle *myBundle = [NSBundle bundleWithURL:[NSURL fileURLWithPathComponents:[executablePathComponents objectsAtIndexes:indexOfEveryComponentExceptLastTwo]]];
// Load the controller nib.
NSNib *windowControllerNib = [[NSNib alloc] initWithNibNamed:#"MyWindowController" bundle:myBundle];
MyWindowController *windowController = [[MyWindowController alloc] init];
NSArray *topLevelObjects = nil;
[windowControllerNib instantiateNibWithOwner:windowController topLevelObjects:topLevelObjects];
This gives me an instance of the window controller and it displays the window from the nib on screen, so this appears to work. BUT, instantiateNibWithOwner:topLevelObjects is deprecated in favor of instantiateNibWithOwner:topLevelObjects.
Using the non-deprecated method results in an exception: "-[NSNib instantiateWithOwner:topLevelObjects:]: unrecognized selector sent to instance 0x10291ab1"
At the very least I'd like to not use the deprecated method. But maybe there is a better way to approach this whole thing?

Drag Files come across Sandbox(__CFPasteboardIssueSandboxExtensionForPath)

I processed drag operation from browser view to custom view.It work well in snow lepoard,but not in Mountain Lion with sandbox.
in browser view:
NSMutableArray* urls = [[[NSMutableArray alloc] init] autorelease];
..............put some NSUrl to urls array....................
[pasteboard writeObjects:[NSArray arrayWithArray:urls]];
in my receive custom view:
NSArray* pasteboardItems = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSString class]] options:nil];
NSArray* pasteboardItems2 = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:nil];
NSArray* pasteboardItems3 = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSImage class]] options:nil];
NSLog(#"%#",pasteboardItems);
NSLog(#"%#",pasteboardItems2);
NSLog(#"%#",pasteboardItems3);
my log is:
2012-08-09 18:33:43.886 iCollage[6885:303] __CFPasteboardIssueSandboxExtensionForPath: error for [/Users/xxxx/Library/Containers/xxxxxxxxxxxx/Data/Downloads/1343902069.jpg]
2012-08-09 18:33:44.546 iCollage[6885:303] ( "file://localhost/Users/xxx/Library/Containers/xxxxxxxx/Data/Downloads/1343902069.jpg")
2012-08-09 18:33:44.547 iCollage[6885:303] ( "file://localhost/Users/xxxxx/Library/Containers/xxxxxx/Data/Downloads/1343902069.jpg")
2012-08-09 18:33:44.547 iCollage[6885:303] ()
my question is:
1.how to fix this error __CFPasteboardIssueSandboxExtensionForPath;I refer the docs and found nothing about that.I am ensuer that i have the permission to access the file!google says, may be "startAccessingSecurityScopedResource" will help me, then i try and failed
2.why pasteboardItems2 have value?i write to pasteboard only url but not string.It disgusted me that I can get the url both from NSString type and NSUrl type! (I try drag a file from iFinder, the url will only exist in pasteboardItems but not pasteboardItems2).Anybody know why? I think the first problem will auto fixed when some one help me fix this problem.
I believe Apple answer question 1:
Important: Although you can support dragging file paths, in general,
you should avoid doing so unless you are certain that the destination
app will never be run in an app sandbox. If you use an NSString, OS X
has no way to know whether that string should be interpreted as a
path; thus, OS X does not expand the destination app’s sandbox to
allow access to the file or directory at that location. Instead, use
an NSURL, a bookmark, or a filename pasteboard type.
WRT to question 2, it looks like you have pasted URLs so reading NSURL objects would seem to be correct. However I think you should implement the dragging using the following code (also from the link above):
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
int numberOfFiles = [files count];
// Perform operation using the list of files
}
return YES;
}
You need to generate security-scoped URL bookmark data on the sender side, and turn that data back into a URL on the receiver side. There's some other stuff you have to do after that when you want to actually access the URL; the documentation elaborates.
The receiving application, when running in a sandbox, will not be able to handle bare paths. This is a core part of being sandboxed; you are not allowed to use bare paths or their corresponding URLs to access files that aren't in your sandbox container and haven't been explicitly handed to you by the user.
Your pasteboardItems read object of NSString type, but you dragged a file(with jpg extension), you should register for NSString type in your init method:
[self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeString]];
You need to have Document Types defined in your application so that the sandboxing mechanism knows your application should be opening files with those extensions. You can do this by clicking the project on the left in Xcode, and in the Info tab, under Document Types add a new document type for each extension.
You just need to fill in the name and extensions field.
Also if you want to persist your permission to access the files dragged onto your application, you can use this class to wrap up all that logic. https://github.com/leighmcculloch/AppSandboxFileAccess

Objective-C: Load "Library" using reflection

I'm tasked with creating an application using XCode for OSX. This application needs to be able to load and run separate "modules" which will be determined dynamically (i.e., one user may have purchased modules 1 & 2, while user 2 would have purchased modules 3 and 6 -- only purchased modules should "run").
In C#, I would create a "library" project (that compiles to just a DLL). When the user purchases a module, I'd supply the appropriate DLL files and then my app would look for and load/run the DLL using reflection.
What would be the equivalent to this in XCode? Can I create a "library" and then load it using reflection? Keeping in mind that the app can't have prior knowledge of the module since in some cases, the user wouldn't even own the module files. I see various options such as "Cocoa Framework" and "Cocoa Library" as well as "C/C++ Library." What does each do and would any work to do what I need?
You can create a bundle. Xcode has templates for this (it is called "Loadable Bundle" and the icon is a Lego brick). You typically load a bundle using NSBundle's load method.
An example of loading it would be:
- (BOOL)loadPluginAtURL:(NSURL *)URL {
NSBundle *pluginBundle = [NSBundle bundleWithURL:URL];
if (![pluginBundle load]) { // is false if pluginBundle == nil automatically.
return NO;
}
id plugin = nil;
#try { // Use #try-#catch in case the principle class doesn't respond to +alloc or -init. Otherwise the host application would crash and that kinda sucks.
plugin = [[pluginBundle principalClass] alloc] init]; // Set the principle class in the bundle's info plist.
} #catch (id e) {
[bundle unload];
return NO;
}
if (plugin) {
[self.loadedPlugins addObject:plugin];// Define this as an NSMutableSet object.
return YES;
}
[pluginBundle unload];
return NO;
}
You can eventually provide a framework that the plugins can use, which can include protocols and classes. You may, for example check if the principle class of the bundle is a subclass of a specific class in your framework, so you don't send any messages the plugin doesn't respond to.