I experience an odd behaviour regarding saving an NSPersistentDocument. I can create a new document which is autosaved without an issue. But when I save it write(to: ofTyp
Good news and bad news
I have succeeded to reproduce the exact same error with Xcode 9.2 running on MacOS 10.13.2.
NSBinaryStoreSecureDecodingClasses
option.NSPersistentStoreCoordinator .migratePersistentStore()
aka -[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]:
migratePersistentStore
, NSBinaryStoreSecureDecodingClasses
option is not set for both writing to and reading from a temporary file.Somewhat strange for me. Why the migration is executed upon saving, instead of upon opening a file? The lightweight migration has been already done at the step 3?
EDIT:
It seems that the name of migratePersistentStore
does not mean migration from old version to new version. It seems to mean migrate from an old persistent store with an old URL aka filename and/or file format to a new persistent store with a new URL and/or file format. So it is called when Save As... or Saving a new document which is already saved in an Autosave directory.
Another, simpler workaround:
The following code has a bug. Please refer to the bug fixed version cited below.
extension NSPersistentStoreCoordinator {
@objc func x_migratePersistentStore(_ store: NSPersistentStore, to URL: URL, options: [AnyHashable : Any]? = nil, withType storeType: String) throws -> NSPersistentStore {
var opt: [AnyHashable : Any] = options ?? [:]
if #available(OSX 10.13, *) {
opt[NSBinaryStoreSecureDecodingClasses] = NSSet(array: [ NSColor.self ])
}
return try x_migratePersistentStore(store, to: URL, options: opt, withType: storeType)
}
}
class Document: NSPersistentDocument {
override init() {
super.init()
let s1 = #selector(NSPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
let s2 = #selector(NSPersistentStoreCoordinator.x_migratePersistentStore(_:to:options:withType:))
let m1 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s1)!
let m2 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s2)!
method_exchangeImplementations(m1, m2)
}
override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
// keep yours
}
}
Added:
This workaround is going to be a practical solution until they enhance NSPersistentDocument.write(to...)
to take into account of options or they implement other means to handle options. Those options will be given to NSPersistentStoreCoordinator.migratePersistentStore(...)
and then be used to instantiate NSPersistentStore
.
Bug Fixed Version:
There was a bug in the preceding workaround. Opening document files, creating new files, making changes, waiting for thirty second to the document being auto-saved, closing them, and/or save-as-ing them randomly would cause the initial error. Reported by Wizard of Kneup.
Here is a fixed version using singleton to make sure swizzling is applied only once.
extension NSPersistentStoreCoordinator {
@objc func x_migratePersistentStore(_ store: NSPersistentStore, to URL: URL, options: [AnyHashable : Any]? = nil, withType storeType: String) throws -> NSPersistentStore {
var opt: [AnyHashable : Any] = options ?? [:]
if #available(OSX 10.13, *) {
opt[NSBinaryStoreSecureDecodingClasses] = NSSet(array: [ NSColor.self ])
}
return try x_migratePersistentStore(store, to: URL, options: opt, withType: storeType)
}
class MigratePersistentStoreInitializer {
init() {
let s1 = #selector(NSPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
let s2 = #selector(NSPersistentStoreCoordinator.x_migratePersistentStore(_:to:options:withType:))
let m1 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s1)!
let m2 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s2)!
method_exchangeImplementations(m1, m2)
}
static let singlton = MigratePersistentStoreInitializer() // Lazy Stored Property
}
}
class Document: NSPersistentDocument {
override init() {
super.init()
let _ = NSPersistentStoreCoordinator.MigratePersistentStoreInitializer.singlton
}
override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
// keep yours
}
}
References:
Could you try this nasty workaround?
class CustomPersistentStoreCoordinator : NSPersistentStoreCoordinator {
static var m1 : Method? = nil
static var m2 : Method? = nil
override func migratePersistentStore(_ store: NSPersistentStore, to URL: URL, options: [AnyHashable : Any]? = nil, withType storeType: String) throws -> NSPersistentStore {
var opt: [AnyHashable : Any] = options ?? [:]
if #available(OSX 10.13, *) {
opt[NSBinaryStoreSecureDecodingClasses] = NSSet(array: [ NSColor.self ])
}
let m1 = CustomPersistentStoreCoordinator.m1!
let m2 = CustomPersistentStoreCoordinator.m2!
method_exchangeImplementations(m2, m1)
let x = try self.migratePersistentStore(store, to: URL, options: opt, withType: storeType)
method_exchangeImplementations(m1, m2)
return x
}
}
class Document: NSPersistentDocument {
override init() {
super.init()
let s1 = #selector(NSPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
let s2 = #selector(CustomPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
let m1 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s1)!
let m2 = class_getInstanceMethod(CustomPersistentStoreCoordinator.self, s2)!
CustomPersistentStoreCoordinator.m1 = m1
CustomPersistentStoreCoordinator.m2 = m2
method_exchangeImplementations(m1, m2)
}
override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
// keep yours
}
}
Thank you @Wizard of Kneup!
That clearly shows the application certainly read a document file and then encounter the error.
Let's start investigation.
(1) What file does the app attempt to read?
(lldb) breakpoint set -n '-[NSBinaryObjectStoreFile readFromFile:error:]'
Breakpoint 3: # locations.
Run the app again to reproduce the problem. Once the breakpoint hits, type the following commands to the lldb prompt:
po $rdi
p (SEL)$rsi
po $rdx
po $rcx
po $r8
po $r9
The filename would be shown. Disable the breakpoint. Use the number # which was returned at breakpoint set before. e.g. 3
(lldb) breakpoint disable 3
(2) What object does the app try to decode?
(lldb) breakpoint set -n '-[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:]'
Breakpoint 4: # locations.
Same as above. Use a set of po
commands to get some information. It seems an array of something.
(lldb) breakpoint disable 4
(3) What class does the app reject to decode?
(lldb) breakpoint set -n '-[NSCoder _validateAllowedClass:forKey:allowingInvocations:]'
I wrote the breakpoints one by one and disable each time. But, no need to do that. Alternatively, set all breakpoints at a time and do hit-and-investigate-then-continue.
The questions are why the app reads the file. That might be for migration.
-[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]
But why override func configurePersistentStoreCoordinator()
is not called? I have no knowledge on that now.
I hope you would have some hints for getting closer to a solution.
Added:
To set a breakpoint, sometime we need to stop at the very beginning of execution of application.
For instance, set a breakpoint at init()
of AppDelegate
to get a chance of manually setting breakpoints.
class AppDelegate: NSObject, NSApplicationDelegate {
override init() {
=> super.init()
}
}
Appended:
I had misunderstood. The correction:
The func configurePersistentStoreCoordinator
seems to be called when a class NSPersistentStore
is instantiated with a desired URL, i.e. filename. So the timing is not related to the action of reading from nor writing to a document file. Instead, it is the first time when the internal document is about to be connected to the URL.
func configurePersistentStoreCoordinator
is called.The fact we had seen that func configurePersistentStoreCoordinator
was not called upon SaveAs seems to be normal, if the document has been loaded from an existing file in advance. The options for the NSPersistentStore
we provided in that func are already there, I think.
Tips for setting a breakpoint