I have an app which uses ubiquitous containers and files in them to share data between devices. It's a bit unusual in that it indexes files in directories the user grants access to, which may or may not exist on a second device - those files are identified by SHA-1 hash. So a second device scanning before iCloud data has fully sync'd can create duplicate references which lead to an unpleasant user experience.
To solve this, I store a small binary index in the root of the ubiquitous file container of the shared data, containing all of the known hashes, and as the user proceeds through the onboarding process, a background thread is attempting to "prime" the ubiquitous container by calling FileManager.default.startDownloadingUbiquitousItemAt()
for each expected folder and file in a sane order.
This likely creates a situation not anticipated by the iOS/iCloud integration's design, as it means my app has a sort of precognition of files it should not yet know about.
In the common case, it works, but there is a corner case where iCloud sync has just begun, and very, very little metadata is available (the common case, however, in an emulator), in which two issues come up:
-
I/O may hang indefinitely, trying to read a file as it is arriving. This one I can work around by running the I/O in a thread created with the POSIX
pthread_create
and usingpthread_cancel
to kill it after a timeout. -
Attempts to call
FileManager.default.startDownloadingUbiquitousItemAt()
fails with an errorError Domain=NSCocoaErrorDomain Code=257 "The file couldn’t be opened because you don’t have permission to view it."
. The permissions aspect of it is nonsense, but I can believe there's no applicable "sort of exists, sort of doesn't" error code to use and someone punted. The problem is that this same error will be thrown on any attempt to access that file for the life of the application - a restart is required to make it usable.
Clearly, the error or the hallucinated permission failure is cached somewhere in the bowels of iOS's FileManager
. I was hoping startAccessingSecurityScopedResource()
would allow me to bypass such a cache, as it does with URL.resourceValues()
returning stale file sizes and last modified times. But it does not.
Is there some way to clear this state without popping up a UI with an Exit button (not exactly the desired iOS user experience)?
I'm not sure what's going on here, but I do have a few comments and questions:
First off, are you using coordinated I/O for all of your actual "access"? The basic "point" of coordinate I/O is to prevent apps from making I/O calls before the system is actually ready for them to be made, which should make this sort of thing unnecessary:
- I/O may hang indefinitely, trying to read a file as it is arriving. This one I can work around by running the I/O in a thread created with the POSIX pthread_create and using pthread_cancel to kill it after a timeout.
As a general note, I'm very skeptical of any usage of pthread cancellation. Our system's are basically built on two parallel system/IPC architectures ("BSD" an "mach") which have never really been properly integrated. An API like pthread_cancel really only deals with the BSD architecture, which means the consequences to the mach side are... undefined. At a minimum it probably creates mach port leaks and at worst... well, the problem with mach is that the "the worst" is VERY difficult to reliably define.
If you're "living" in the BSD layer, then one option here would be use setiopolicy_np to control automatic materialization as described in TN3150: Getting ready for dataless files | Apple Developer Documentation. However, I have a few warnings about this approach:
-
I think this works fine on iOS (indeed, I believe it's what's causing the hang above), but haven't ever actually tested it in any great detail.
-
I don't know how higher level APIs like NSFileManager will handle this. In most cases I think they'll do what you'd "expect" (basically, return/fail instead of blocking), but I also wouldn't be surprised if their are edge cases that are "weird".
-
This architecture was primarily created to support the "replicated file provider" architecture, not the older (original) "file provider" extension point. On iOS, it's possible that some apps are still using that extension point and I don't know how that will behave with this API. This doesn't matter if you're only interacting with iCloud Drive, but it could be an issue if you try to use this for "all" file access.
FileManager.default.startDownloadingUbiquitousItemAt() for each expected folder and file in a sane order. This likely creates a situation not anticipated by the iOS/iCloud integration's design, as it means my app has a sort of precognition of files it should not yet know about.
How "aggressively" are you doing this? I don't think the "precognition" itself is an issue, as the main reason startDownloadingUbiquitousItemAt exists at all is the idea that an app can know "more" about the files it's going to need than the system does. However, "hammering" a large number of request into the system at once is probably counter productive, as you'll just end up piling up requests that it can't actually complete. Putting that in concrete terms, if call "startDownloadingUbiquitousItemAt" on a "large" number of items, all the system can really do is:
-
Start a network transfer for every item and download "all" of them slowly.
-
Do it's own queuing and download all of them is some sequence that's no longer visible or controllable by you.
I'll tell you now that it's going to do #2, but that's not helpful if the point here was to better prioritize work. I'd also be careful here about calling startDownloadingUbiquitousItemAt on directories, as that implies the download of files as well.
Shifting to this error:
- Attempts to call FileManager.default.startDownloadingUbiquitousItemAt() fails with an error Error Domain=NSCocoaErrorDomain Code=257 "The file couldn’t be opened because you don’t have permission to view it.".
Is that the full contents of the error object? These error are often nested as the code moves between components and I want to make sure we don't have any other info.
The permissions aspect of it is nonsense, but I can believe there's no applicable "sort of exists, sort of doesn't" error code to use and someone punted.
Yes, with the additional qualification that a framework like Foundation can only return it's "own" errors (this is why the nesting behavior exists) and Foundation has basically reach the point where adding more error codes is more likely to create problems than solve them.
The problem is that this same error will be thrown on any attempt to access that file for the life of the application - a restart is required to make it usable.
Where did you "get" the URL you're passing in? Are these just built up by appending to the base path or are you iterating directories and extending from there?
Clearly, the error or the hallucinated permission failure is cached somewhere in the bowels of iOS's FileManager.
No, or at least not the way you're thinking. All startDownloadingUbiquitousItemAt really does is call out to the fileproviderd and say "download this", then return whatever error it got back from fileproviderd. I don't think the error is really a "cache" issue, but it's possible that something about the URL you're passing over is pointing the file provider system at the wrong thing.
In terms of next steps, I think it depends on how the issue here are "entangled", but there are basically two paths:
-
I think some combination of setiopolicy_np and/or coordinated I/O should prevent your I/O from ever blocking. In theory, that might also prevent the second issue from occurring, but that's less clear to me.
-
If you want to investigate the second error directly, then the next step is to dig into the system log to see what's actually going on, as well as filing a bug.
Expanding on #2, here is what I would suggest:
-
Add logging to your app that specifically "brackets" (before and after) every call to startDownloadingUbiquitousItemAt and that shows the error when it fails. This step is critical because you need to be able to easily identify the section of the system log that's actually related to the error you're getting.
-
Install the File Provider and/or iCloud Drive profiles on your device, power the device off, then go do "something else" for as long as possible (this creates a clear time gap, so it's easier to see where the system started up).
-
Reproduce the problem, noting the time of the failure.
-
Collect the sysdiagnose, using the instructions in the link above. Do NOT reboot the device until you've collected all diagnostic data.
Once you've got that data, please file a bug that describes the issue you're having, the logging you added, failure time, and then upload the sysdiagnose. Once the bug is filed, post that number back here so I can take a look.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware