Sandboxed Mac app exhausting security scoped URL resources - objective-c

I am developing a Mac application that prompts the user for files using the NSOpenPanel. The application is sandboxed (testing on OSX 10.9.4). I noticed that if I open a large amount of files (~3000), the open panel starts to emit errors to the log. This also happens if I try to open less amount of files in chucks for several times.
After the errors start to appear the first time, every time the NSOpenPanel is used again to open files, no matter for how many files, these errors will be generated again (until the application is closed).
The error message looks like this:
TestPanel[98508:303] __41+[NSSavePanel _consumeSandboxExtensions:]_block_invoke: sandbox_consume_fs_extension failed
One line for each file I try to open.
I managed to reproduce this behavior with a simple app: A sandboxed application with a single button invoking the following code:
NSOpenPanel* panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:YES];
[panel setCanChooseDirectories:NO];
[panel setCanChooseFiles:YES];
[panel beginSheetModalForWindow:[self window] completionHandler:^(NSInteger result) {
NSLog(#"%lu", [panel.URLs count]);
}];
The errors appear before the code reaches the completion handler.
It seems that I can still get the URLs from the panel in the completion handler but it really pollutes the system log.
EDIT:
Seems that this problem is not directly related to the NSOpenPanel/NSSavePanel panels. A very similar thing happens when using drap/drop with files. Something like this:
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
...
NSPasteboard *pboard = [sender draggingPasteboard];
if ([[pboard types] containsObject:NSURLPboardType]) {
NSArray *urls = [pboard readObjectsForClasses:#[[NSURL class]] options:nil];
}
...
}
This will generate the following log messages when dragging a large amount of files (the "magic" number seems to be somewhere around 2900):
Consume sandbox extension for itemIdentifier (2937) from pasteboard failed!
As with the NSOpenPanel, after the first occurrence of this, every single file dropped will generate the same error in the log.
EDIT 2:
#mahal tertin's reply pointed me to the right direction. The problem is indeed with the number of files and the fact that security scoped URL resources are limited.
However, there seems to be no reasonable solution found. The problem is that when the user clicks "OK" on the NSOpenPanel (or drops the files on a drag&drop aware control), behind the scenes the OS already attempts to create these security scoped URLs and implicitly calls startAccessingSecurityScopedResource for you. So if the user attempts to open more files than the limit, the resources are exhausted and the only option is to close and restart the application.
Calling stopAccessingSecurityScopedResource on the returned URLs seem to free the resources however this solution was discouraged by Apple's representative on the official developers forums (link is behind login).
It seems that the app is at the mercy of the user not to open too many files. And that is not even at once, since there is no approved way to release these resources. You can warn the user in documentation or even with an in-app alert but there is no way to prevent them from messing up the app and forcing a restart.
So if the app runs long enough and the user keeps opening files, the app will eventually become unusable.
Still looking for a reasonable solution for this.

After searching high and low and asking in various places, I am going to close this question and conclude there is no answer or solution to this. I am posting the known information on this for future reference.
All the solutions suggested are just workarounds that may minimize the problem and try to guide the user toward not trying to open too many files. But there nothing that can be done to actually solve this.
Here are the known facts about this issue:
No matter what you do, the user can attempt to open too many files in the NSOpenPanel dialog and exhaust the security scoped URL resources
Once these resources are exhausted, it is not possible to open any more files for reading/writing. The application needs to be closed and reopened
Even if the user doesn't attempt to open too many files at once, the application may still exhaust these resources if it runs long enough and the user opens enough files over time since startAccessingSecurityScopedResource is called automatically for files opened with NSOpenPanel (or the drag/drop mechanism) and nothing ever closes these resources
Calling stopAccessingSecurityScopedResource on all URL retrieved by the open panel will free these resources but this practice is discouraged by Apple, saying it might not be compatible with future solutions
When you receive the list of URLs from NSOpenPanel (or drag/drop), there is no way to tell if all URLs were successfully accessed or if there are URLs that are over the limit and therefore invalid.
Apple is aware of this and may fix it in the future. It is still not fixed in 10.10 and of course, that will not help current applications running on current/previous OSX version.
It seems Apple has really dropped the ball on this one, the Sandbox implementation seems very sloppy and short sighted.

The behavior you experience is because the security scoped resources are limited:
NSURL - (BOOL)startAccessingSecurityScopedResource tells
If sufficient kernel resources are leaked, your app loses its ability
to add file-system locations to its sandbox...
The current limit is roughly what you experienced. See:
What are the current kernel resource limits on security-scoped bookmarks?
To prevent it:
only start accessing those SSBs you need at a given time and subsequently stop accessing them
start access not files but enclosing folders: ask the user not to choose files but a full folder. This will grant you access to the whole tree beneath that directory
on draggingEntered: show a NSOpenPanel with the enclosing directory(ies) to grant access

Related

Prevent NSDocument saving in temporary dictionary

I have an app with subclass of NSDocument that has overridden method writeToURL:(NSURL *) ofType:(NSString *) error:(NSError **) which saves data at given NSURL location, but also can save additional file (with appended .my2ext) with debug information. Previously it worked well (I created the app several years ago), but now I see that instead of user selected location the method gets some temporary directory:
file:///var/folders/yv/gwf3_hjs0ps7sb3psh3d0w3m0000gn/T/TemporaryItems/(A%20Document%20Being%20Saved%20By%20MyApp%202)/myfilename.myext
Then, as I understand, the framework relocates the main file (at given url), but the additional file gets lost. So, can I somehow obtain the user selected path to save directly into it? Or prevent using temp directories at all?
I've already turned off the SandBox mode, but this didn't help. I also know that I can use "File Package" approach, but my app is created for a few people only, so, there is not interest in good production approach, only in simplicity.
I tried to google any possible solution, but found nothing helpful or just related. Even the documentation says nothing about using temporary directories! So, I decided to override different NSDocument methods. After several experiments I almost lost hope, but then I found that the method
saveToURL: ofType: forSaveOperation: delegate: didSaveSelector: contextInfo: provides real, user selected location. And this finally solved the problem.

What exactly should I pass to -[NSApp activateIgnoringOtherApps:] to get my application to start "naturally" in comparison to most other OS X apps?

When I learned how to start NSApplications on my own, the code I used (based on here and here) did
[NSApp activateIgnoringOtherApps:YES];
which forces the app to the front at startup.
I'd like to know what most other apps do. I want to be able to run programs both directly from the binary and from an app bundle, and I'm not using Xcode to build this (raw building). So I'd rather this act naturally, so to speak.
The docs do say Finder issues NO, but... why Finder? Isn't this a method that's run from within the process, not outside? (I'm not in control of the choice.) And what about the Dock and other possible entry points?
I even went so far as to disassemble 10.8's NSApplicationMain() to see what it did, but as far as I can tell from the 32-bit version, unless this "light launch" thing issues this selector, this selector is never called.
Is there an answer to this question? Thanks... and sorry if this is confusing; I tried to word it as clearly as possible.
Apps normally do not call -activateIgnoringOtherApps: at all. And, generally speaking, shouldn't. Certainly, it wouldn't be in NSApplicationMain(), which is too early and fairly distantly related to actual app start-up.
Apps are normally launched by Launch Services (which is what is used by the Finder, the Dock, and /usr/bin/open, as well as any other app that might open yours or a document which yours handles). Roughly what happens is that Launch Services deactivates the app which called it to open something else and then, in the launched app, Cocoa's internals do something like (but not necessarily identical to) [NSApp activateIgnoringOtherApps:NO]. In this way, the launched app only activates if nothing else was activated in the interval between those two events. If that interval is long (because something was slow) and the user switched to something else in the meantime, you don't want to steal focus from whatever they switched to.
You should only call [NSApp activateIgnoringOtherApps:YES] in response to a user request to activate your app in a context which won't include the automatic deactivation of the current app by Launch Services. For example, if you have a command-line program which transforms itself into a GUI app (using -[NSApplication setActivationPolicy:] or the deprecated TransformProcessType()), then the user running that tool means they want it active. But Terminal is active and won't be deactivated spontaneously just by virtue of having run your program. So, the program has to steal focus.
If your program is a bundled app, then running it from the command line should be done with /usr/bin/open rather than directly executing the executable inside the bundle. Then, you don't need to call -activateIgnoringOtherApps: at all and the question of what value to pass is moot.

Cocoa (OSX) drag & drop promised files - when are they ready for use?

I am trying to DnD promised files from Mac OSX applications to my own application. I am using the following:
NSArray *filenames = [info namesOfPromisedFilesDroppedAtDestination:[NSURL fileURLWithPath:path]];
(info is of NSDraggingInfo)
And indeed I get a list of filenames, which I can copy into my application.
However, I see the files aren't always complete - meaning, they are not guaranteed to be fully copied to the file system by the time I process them.
I am now looking for the right solution - how should I tell when to start processing the files? Is there some other method that returns the file names and their status?
Any help is highly appreciated!
Thanks,
Nili
Promised files don’t even start writing until you accept the file, so it’s normal that you’re seeing them only half-written.
As far as I know there’s no way to know when a file’s finished writing. Are the apps you are accepting these files from not also advertising a suitable ‘file contents’ or ‘file’ or other plain, non-promise pasteboard type? Mostly apps advertise they’ll create “promise” types are just so the Finder can drop a file into a folder the user selects without the app having to write the whole (potentially huge) file ahead of time—it’s not supposed to be for applications talking to other applications.

App Sandbox: document-scoped bookmark not resolving; not returning any error

I'm sandboxing my app, and trying to allow for import/export of multiple files, using an XML file to refer to them. To allow my app (or another sandboxed app) access to the files listed in the XML, I'm also including a serialized security-scoped bookmark. I'm serializing it as described in this answer, and my unit tests (which are not sandboxed) write and read the XML data without issue. When my app resolves the bookmark, the NSURL returned is nil, as is the NSError reference. Since I don't believe that should be the case, why is it happening? I can work around it by prompting the user to select a file/directory with an NSOpenPanel, but I'd still like to get the bookmarks to work as they should.
Reproduced in a test project
To reproduce at home, create a new Cocoa app in Xcode, and use the following Gist for the files in the project: https://gist.github.com/2582589 (updated with a proper next-view loop)
Then, follow Apple's instructions to code-sign the project. You reproduce the problem (which I submitted to Apple as rdar://11369377) by clicking the buttons in sequence. You pick any file on disk (outside the app's container), then an XML to export to, and then the same XML to import.
Hopefully you guys will be able to help me figure out what I'm doing wrong. Either I'm doing something wrong and the framework is erroneously keeping to itself, or I'm doing it right and it's totally broken. I try not to blame the framework, so which is it? Or is there another possibility?
Sample Code
Exporting the XML to docURL:
// After the user picks an XML (docURL) destination with NSSavePanel
[targetURL startAccessingSecurityScopedResource];
NSData *bookmark = [targetURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:docURL
error:&error];
[targetURL stopAccessingSecurityScopedResource];
Importing the XML from docURL:
// After the user selected the XML (docURL) from an NSOpenPanel
NSURL *result = [NSURL URLByResolvingBookmarkData:bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope
relativeToURL:docURL
bookmarkDataIsStale:nil
error:&error];
I tried surrounding this call with[docURL ..AccessingSecurityScopedResource], which didn't make a difference (as expected, since the docURL is already within scope after having been selected in the Open Panel
Also, I specify the following in my app.entitlements file:
com.apple.security.files.user-selected.read-write
com.apple.security.files.bookmarks.app-scope
com.apple.security.files.bookmarks.collection-scope
As mentioned above, the second step (resolving the bookmark) completes, but leaves both error and result nil. As I've been implementing sandboxing, most of the mistakes I've made have resulted in an NSError being returned, which helped me to resolve the bug. But now there's no error, and no URL is resolved.
Miscellaneous troubleshooting steps
I tried placing the XML file into my app's sandbox, which didn't make a difference, so access to the XML file is not the problem
The app uses ARC, but so do the unit tests, which succeed. I tried using an alloc/init instead of the autoreleased class method, too (just in case)
I pasted the URL resolution code immediately after creating the bookmark, and it runs fine, producing a security-scoped URL
I did a po on the originally created bookmark (before serialization), and then on the bookmark after deserialization, and they match 100%. Serialization is not the problem
I replaced the resolution call with CFURLCreateByResolvingBookmarkData(..), with no change. If it is a bug, it's present in the Core Foundation API as well as the Cocoa layer
Specifying a value for bookmarkDataIsStale: has no effect
If I specify 0 for options:, then I do get back a valid NSURL, but it has no security scope, and therefore subsequent calls to read the file do still fail
In other words, the deserialized bookmark does appear to be valid. If the bookmark data were corrupted, I doubt NSURL would be able to do anything with it
NSURL.h didn't contain any useful comments to point out something I'm doing wrong
Is anyone else using security-scoped document bookmarks in a sandboxed application with success? If so, what are you doing differently than I am?
OS Version Request
Can someone with access to the Mountain Lion beta verify whether or not my sample project shows the same (lack of an) error? If it's a bug that has been fixed after Lion, I won't worry about it. I'm not in the developer program yet, and so don't have access. I'm not sure if answering that question would violate the NDA, but I hope not.
In your Gist code, change the following line in AppDelegate.m (line 61):
[xmlTextFileData writeToURL:savePanel.URL atomically:YES];
to
[xmlTextFileData writeToURL:savePanel.URL atomically:NO];
Your code will then work.
The reason for this is likely the same reason for which it is necessary to have an existing (but empty) file that will contain the document-scoped bookmarks before calling [anURL bookmarkDataWithOptions]: While creating the NSData instance, the ScopedBookmarkAgent adds something (like a tag, probably an extended file attribute) to that file.
If you write data (i.e. the bookmark URLs) to that file atomically, in fact they're written not directly to the file but first to a temporary file that is renamed if the write operation was successful. It seems that the tag that has been added to the (empty, but existing) file that will contain the bookmarks is getting lost during this process of writing to a temporary file and then renaming it (and thereby likely deleting the original, empty file).
By the way: It shouldn't be necessary to create app-scoped bookmarks before passing the respective URLs to the xml file containing the document-scoped bookmarks.
Addition: com.apple.security.files.bookmarks.collection-scope has been renamed to com.apple.security.files.bookmarks.document-scope in 10.7.4.

Is a file available to be opened?

Short version: I think I'm asking for a file too soon, but it's pretending like it's ready. Am I missing something?
Slightly longer version: I am writing files to disk. Before I do so, I have the user add some meta data, including the new file name. Once the user is done, the screen goes away and the program writes the file to disk. The user can then look at a list of files. That list is generated by reading the contents of a folder. The new file is in the list of files, but when I try to extract info from the file to display (e.g. file size) the program crashes. As best as I can tell, the crash occurs because, while the file is there in name, it's not available to be read. (By the way, these are small files - a few hundred k.)
First, is it possible that a file shows up in the directory but isn't all there yet?
a
And second, if so, how do I check to see if the file is ready to be read?
Thanks much.
UPDATE:
Thanks. I'll try to add more info. I'm recording an audio file with AVAudioRecorder. The init line is:
soundrecording = [[AVAudioRecorder alloc] initWithURL:url settings:recordSettings error:&error];
The program goes through it's UI updates and metering and all that. When the audio is stopped, I call:
[soundrecording stop];
and when everything else is updated and ready to move on, I call:
[soundrecording release];
soundrecording=NULL;
As far as I understand, this should take care of releasing the file, yes?
Thanks again.
The first thing I would do is confirm that you're right about the file not being ready yet. To do that, sleep your program for a second or two after writing and before reading. A few hundred KB should not take longer than that to be ready.
If it still fails, my guess is that you haven't closed the file handle that you used to write it. It may be unready for reading because the file system thinks you might keep writing.
Usually, the way to check to see if a file is ready is to attempt to open it. If that succeeds, you can read it. Or if it fails with an error, you can handle the error gracefully:
In a command-line utility, you might print the error and quit, and the user could try again.
If it's a background program that should not quit, like a server, you could log the error. You might also try again automatically after a delay. If it's a big deal kind of error, you might want to have the program email you about it.
In an GUI window app, you probably want to show an error dialog or panel, and then give the user an opportunity to retry.
Now that you have added sample code, I can say some more.
First, the class reference seems to say that the stop method will close the file. However it also seems to suggest that there is an underlying audio session going on, and possibly some conversion. I think I recall that the iPhone's Voice Notes app, which probably uses this API, has to do some work to compress a long recording after it's completed.
So I support your hunch. I think that your file may not be closed yet, but on another thread that is processing the recorded data into a proper format to save.
You probably want to set a NSTimer to attempt to open the file every second or so, so that your user interface can perk up when it's done. You probably want to show a "Please wait" sort of message in the meantime, or otherwise let the user know it's working.