2

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: ofType: for: absoluteOriginalContentsURL:) is called but it turns to that configurePersistentStoreCoordinator(for: ofType: modelConfiguration: storeOptions:) isn't called. That is unfortunate as I need to configure the store. Reason is that I need to register NSColor as decodable options[NSBinaryStoreSecureDecodingClasses] = NSSet(object: NSColor.self).

First attempt was to call it myself, but that didn't lead me anywhere. Calling it myself didn't register the settings as expected.

Code in my NSPersistentDocument looks like this:

 override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
        Swift.print("VTDocment.configurePersistentStoreCoordinator for \(url.lastPathComponent)")
        var options = addOptions(to: storeOptions)
        try super.configurePersistentStoreCoordinator(for: url, ofType: fileType, modelConfiguration: configuration, storeOptions: options)
    }      

    func addOptions(to: [String : Any]?) -> [String : Any] {
        var options = to != nil ? to! : [String:Any]()
        if #available(OSX 10.13, *) {
            options[NSBinaryStoreSecureDecodingClasses] = NSSet(object: NSColor.self)
        }
        options[NSMigratePersistentStoresAutomaticallyOption] = true
        options[NSInferMappingModelAutomaticallyOption] = true
        return options
    } 

override func write(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, originalContentsURL absoluteOriginalContentsURL: URL?) throws {
    try self.configurePersistentStoreCoordinator(for: url, ofType: typeName, modelConfiguration: "Document")
    do {
        Swift.print("Now... VTDocment.write to \(url.lastPathComponent)")
        try super.write(to: url, ofType: typeName, for: saveOperation, originalContentsURL: absoluteOriginalContentsURL)
    } catch {
        Swift.print("VTDocment.write error: \(error)")
    }
}

Result is the following output (second configurePersistentStoreCoordinator output is starting after the save as through menu command):

VTDocment.configurePersistentStoreCoordinator for Unsaved Visual Thinking with IBIS Document 12.ibisVT
VTDocment.configurePersistentStoreCoordinator for test.ibisVT
Now... VTDocment.write to test.ibisVT
value for key 'NS.objects' was of unexpected class 'NSColor'. Allowed classes are '{(
    NSNumber,
    NSString,
    NSCalendarDate,
    NSOrderedSet,
    NSDecimalNumber,
    NSUUID,
    NSDate,
    NSSet,
    NSNull,
    NSURL,
    NSData,
    NSDictionaryMapNode,
    NSDictionary,
    NSArray
)}'.
(null)

105827995370488

2018-02-09 05:53:52.250312+0100 Visual Thinking with IBIS[42589:19295813] -[NSException initialize]: unrecognized selector sent to instance 0x60400025d7c0
value for key 'NS.objects' was of unexpected class 'NSColor'. Allowed classes are '{(
    NSNumber,
    NSString,
    NSCalendarDate,
    NSOrderedSet,
    NSDecimalNumber,
    NSUUID,
    NSDate,
    NSSet,
    NSNull,
    NSURL,
    NSData,
    NSDictionaryMapNode,
    NSDictionary,
    NSArray
)}'.

EDIT Thanks for the questions @Tora! Here is a related stack trace. It's not from the same session but leading to the same result.

    0   CoreFoundation                      0x00007fff40f9ffcb __exceptionPreprocess + 171
1   libobjc.A.dylib                     0x00007fff67c41c76 objc_exception_throw + 48
2   Foundation                          0x00007fff430ef08f -[NSCoder(Exceptions) __failWithExceptionName:errorCode:format:] + 0
3   Foundation                          0x00007fff430ef20c -[NSCoder(Exceptions) __failWithExceptionName:errorCode:format:] + 381
4   Foundation                          0x00007fff43017555 -[NSCoder _validateAllowedClass:forKey:allowingInvocations:] + 239
5   Foundation                          0x00007fff42fd948c _decodeObjectBinary + 1944
6   Foundation                          0x00007fff42fdaa6f -[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:] + 1945
7   Foundation                          0x00007fff42fde8c5 -[NSArray(NSArray) initWithCoder:] + 202
8   Foundation                          0x00007fff42fd9523 _decodeObjectBinary + 2095
9   Foundation                          0x00007fff42fd8bee _decodeObject + 417
10  Foundation                          0x00007fff42ff171b -[NSKeyedUnarchiver decodeObjectOfClasses:forKey:] + 409
11  CoreData                            0x00007fff40b77491 -[NSDictionaryMapNode initWithCoder:] + 97
12  Foundation                          0x00007fff42fd9523 _decodeObjectBinary + 2095
13  Foundation                          0x00007fff42fdaa6f -[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:] + 1945
14  Foundation                          0x00007fff42fd9de8 -[NSDictionary(NSDictionary) initWithCoder:] + 202
15  Foundation                          0x00007fff42fd9523 _decodeObjectBinary + 2095
16  Foundation                          0x00007fff42fd8bee _decodeObject + 417
17  Foundation                          0x00007fff42ff171b -[NSKeyedUnarchiver decodeObjectOfClasses:forKey:] + 409
18  CoreData                            0x00007fff40bcfcc2 -[NSBinaryObjectStoreFile readBinaryStoreFromData:originalPath:error:] + 994
19  CoreData                            0x00007fff40bd019a -[NSBinaryObjectStoreFile readFromFile:error:] + 474
20  CoreData                            0x00007fff40aed247 -[NSDictionaryStoreMap initWithStore:fromPath:] + 215
21  CoreData                            0x00007fff40b75bd3 -[NSBinaryObjectStore initWithPersistentStoreCoordinator:configurationName:URL:options:] + 739
22  CoreData                            0x00007fff40b56e35 __91-[NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:]_block_invoke + 1845
23  CoreData                            0x00007fff40b69132 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 210
24  CoreData                            0x00007fff40b52f75 _perform + 213
25  CoreData                            0x00007fff40a7d2cf -[NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:] + 559
26  CoreData                            0x00007fff40b596e4 __84-[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]_block_invoke + 1284
27  CoreData                            0x00007fff40b69132 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 210
28  libdispatch.dylib                   0x00007fff687f7d50 _dispatch_client_callout + 8
29  libdispatch.dylib                   0x00007fff6880b1d6 _dispatch_queue_barrier_sync_invoke_and_complete + 60
30  CoreData                            0x00007fff40b52f62 _perform + 194
31  CoreData                            0x00007fff40b590ef -[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:] + 351
32  AppKit                              0x00007fff3ebc8fd6 -[NSPersistentDocument writeToURL:ofType:forSaveOperation:originalContentsURL:error:] + 2367
33  Visual Thinking with IBIS           0x000000010003edb2 _T025Visual_Thinking_with_IBIS10VTDocumentC5writey10Foundation3URLV2to_SS6ofTypeSo10NSDocumentC013SaveOperationK0O3forAGSg016originalContentsH0tKF + 1570
34  Visual Thinking with IBIS           0x000000010003f22c _T025Visual_Thinking_with_IBIS10VTDocumentC5writey10Foundation3URLV2to_SS6ofTypeSo10NSDocumentC013SaveOperationK0O3forAGSg016originalContentsH0tKFTo + 284
35  AppKit                              0x00007fff3f0996aa -[NSDocument(NSDocumentSaving) _writeSafelyToURL:ofType:forSaveOperation:forceTemporaryDirectory:error:] + 870
36  AppKit                              0x00007fff3f09a394 -[NSDocument(NSDocumentSaving) _writeSafelyToURL:ofType:forSaveOperation:error:] + 28
37  AppKit                              0x00007fff3ebc9eb3 -[NSPersistentDocument writeSafelyToURL:ofType:forSaveOperation:error:] + 446
38  AppKit                              0x00007fff3f0a5294 __85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_2.1146 + 238
39  AppKit                              0x00007fff3f0a5197 __85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke.1143 + 454
40  AppKit                              0x00007fff3f0a33ff __85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_3.999 + 1962
41  AppKit                              0x00007fff3eae0eb4 -[NSDocument(NSDocumentSerializationAPIs) continueFileAccessUsingBlock:] + 238
42  AppKit                              0x00007fff3f0a04b9 __119-[NSDocument(NSDocumentSaving) _fileCoordinator:asynchronouslyCoordinateReadingContentsAndWritingItemAtURL:byAccessor:]_block_invoke_2 + 94
43  AppKit                              0x00007fff3ea5f8c0 __62-[NSDocumentController(NSInternal) _onMainThreadInvokeWorker:]_block_invoke.2153 + 157
44  CoreFoundation                      0x00007fff40f3858c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
45  CoreFoundation                      0x00007fff40f1b043 __CFRunLoopDoBlocks + 275
46  CoreFoundation                      0x00007fff40f1ae08 __CFRunLoopRun + 3128
47  CoreFoundation                      0x00007fff40f19f43 CFRunLoopRunSpecific + 483
48  HIToolbox                           0x00007fff40231e26 RunCurrentEventLoopInMode + 286
49  HIToolbox                           0x00007fff40231b96 ReceiveNextEventCommon + 613
50  HIToolbox                           0x00007fff40231914 _BlockUntilNextEventMatchingListInModeWithFilter + 64
51  AppKit                              0x00007fff3e4fcf5f _DPSNextEvent + 2085
52  AppKit                              0x00007fff3ec92b4c -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 3044
53  AppKit                              0x00007fff3e4f1d6d -[NSApplication run] + 764
54  AppKit                              0x00007fff3e4c0f1a NSApplicationMain + 804
55  Visual Thinking with IBIS           0x0000000100019bed main + 13
56  libdyld.dylib                       0x00007fff68831115 start + 1

Many thanks in advance.

Update Solution from Tora (via method swizzling) works fine. However, sometimes (not sure when) the same issue can result. Here is the updated stack trace. This is mysterious. :-)

0   CoreFoundation                      0x00007fff40f9ffcb __exceptionPreprocess + 171
1   libobjc.A.dylib                     0x00007fff67c41c76 objc_exception_throw + 48
2   Foundation                          0x00007fff430ef08f -[NSCoder(Exceptions) __failWithExceptionName:errorCode:format:] + 0
3   Foundation                          0x00007fff430ef20c -[NSCoder(Exceptions) __failWithExceptionName:errorCode:format:] + 381
4   Foundation                          0x00007fff43017555 -[NSCoder _validateAllowedClass:forKey:allowingInvocations:] + 239
5   Foundation                          0x00007fff42fd948c _decodeObjectBinary + 1944
6   Foundation                          0x00007fff42fdaa6f -[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:] + 1945
7   Foundation                          0x00007fff42fde8c5 -[NSArray(NSArray) initWithCoder:] + 202
8   Foundation                          0x00007fff42fd9523 _decodeObjectBinary + 2095
9   Foundation                          0x00007fff42fd8bee _decodeObject + 417
10  Foundation                          0x00007fff42ff171b -[NSKeyedUnarchiver decodeObjectOfClasses:forKey:] + 409
11  CoreData                            0x00007fff40b77491 -[NSDictionaryMapNode initWithCoder:] + 97
12  Foundation                          0x00007fff42fd9523 _decodeObjectBinary + 2095
13  Foundation                          0x00007fff42fdaa6f -[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:] + 1945
14  Foundation                          0x00007fff42fd9de8 -[NSDictionary(NSDictionary) initWithCoder:] + 202
15  Foundation                          0x00007fff42fd9523 _decodeObjectBinary + 2095
16  Foundation                          0x00007fff42fd8bee _decodeObject + 417
17  Foundation                          0x00007fff42ff171b -[NSKeyedUnarchiver decodeObjectOfClasses:forKey:] + 409
18  CoreData                            0x00007fff40bcfcc2 -[NSBinaryObjectStoreFile readBinaryStoreFromData:originalPath:error:] + 994
19  CoreData                            0x00007fff40bd019a -[NSBinaryObjectStoreFile readFromFile:error:] + 474
20  CoreData                            0x00007fff40aed247 -[NSDictionaryStoreMap initWithStore:fromPath:] + 215
21  CoreData                            0x00007fff40b75bd3 -[NSBinaryObjectStore initWithPersistentStoreCoordinator:configurationName:URL:options:] + 739
22  CoreData                            0x00007fff40b56e35 __91-[NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:]_block_invoke + 1845
23  CoreData                            0x00007fff40b69132 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 210
24  CoreData                            0x00007fff40b52f75 _perform + 213
25  CoreData                            0x00007fff40a7d2cf -[NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:] + 559
26  CoreData                            0x00007fff40b596e4 __84-[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]_block_invoke + 1284
27  CoreData                            0x00007fff40b69132 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 210
28  libdispatch.dylib                   0x00007fff687f7d50 _dispatch_client_callout + 8
29  libdispatch.dylib                   0x00007fff6880b1d6 _dispatch_queue_barrier_sync_invoke_and_complete + 60
30  CoreData                            0x00007fff40b52f62 _perform + 194
31  CoreData                            0x00007fff40b590ef -[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:] + 351
32  AppKit                              0x00007fff3ebc8fd6 -[NSPersistentDocument writeToURL:ofType:forSaveOperation:originalContentsURL:error:] + 2367
33  Visual Thinking with IBIS           0x00000001000404a2 _T025Visual_Thinking_with_IBIS10VTDocumentC5writey10Foundation3URLV2to_SS6ofTypeSo10NSDocumentC013SaveOperationK0O3forAGSg016originalContentsH0tKF + 1570
34  Visual Thinking with IBIS           0x000000010004091c _T025Visual_Thinking_with_IBIS10VTDocumentC5writey10Foundation3URLV2to_SS6ofTypeSo10NSDocumentC013SaveOperationK0O3forAGSg016originalContentsH0tKFTo + 284
35  AppKit                              0x00007fff3f0996aa -[NSDocument(NSDocumentSaving) _writeSafelyToURL:ofType:forSaveOperation:forceTemporaryDirectory:error:] + 870
36  AppKit                              0x00007fff3f09a394 -[NSDocument(NSDocumentSaving) _writeSafelyToURL:ofType:forSaveOperation:error:] + 28
37  AppKit                              0x00007fff3ebc9eb3 -[NSPersistentDocument writeSafelyToURL:ofType:forSaveOperation:error:] + 446
38  AppKit                              0x00007fff3f0a5294 __85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_2.1146 + 238
39  AppKit                              0x00007fff3f0a5197 __85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke.1143 + 454
40  AppKit                              0x00007fff3f0a33ff __85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_3.999 + 1962
41  AppKit                              0x00007fff3eae0eb4 -[NSDocument(NSDocumentSerializationAPIs) continueFileAccessUsingBlock:] + 238
42  AppKit                              0x00007fff3f0a04b9 __119-[NSDocument(NSDocumentSaving) _fileCoordinator:asynchronouslyCoordinateReadingContentsAndWritingItemAtURL:byAccessor:]_block_invoke_2 + 94
43  AppKit                              0x00007fff3ea5f8c0 __62-[NSDocumentController(NSInternal) _onMainThreadInvokeWorker:]_block_invoke.2153 + 157
44  CoreFoundation                      0x00007fff40f3858c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
45  CoreFoundation                      0x00007fff40f1b043 __CFRunLoopDoBlocks + 275
46  CoreFoundation                      0x00007fff40f1ae08 __CFRunLoopRun + 3128
47  CoreFoundation                      0x00007fff40f19f43 CFRunLoopRunSpecific + 483
48  HIToolbox                           0x00007fff40231e26 RunCurrentEventLoopInMode + 286
49  HIToolbox                           0x00007fff40231b96 ReceiveNextEventCommon + 613
50  HIToolbox                           0x00007fff40231914 _BlockUntilNextEventMatchingListInModeWithFilter + 64
51  AppKit                              0x00007fff3e4fcf5f _DPSNextEvent + 2085
52  AppKit                              0x00007fff3ec92b4c -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 3044
53  AppKit                              0x00007fff3e4f1d6d -[NSApplication run] + 764
54  AppKit                              0x00007fff3e4c0f1a NSApplicationMain + 804
55  Visual Thinking with IBIS           0x000000010001a3dd main + 13
56  libdyld.dylib                       0x00007fff68831115 start + 1
Wizard of Kneup
  • 1,863
  • 1
  • 18
  • 35
  • I found this in the Apple Documentation: **Important** NSPersistentDocument does not support some document behaviors: [..] `saveToOperation` operation type. Core Data does not support saving changes to a new document while maintaining the unsaved state in the current document. from: https://developer.apple.com/documentation/appkit/nspersistentdocument Does this explain this? – Wizard of Kneup Feb 10 '18 at 18:27
  • maybe I need to migrate myself? why would that not be part of framework? – Wizard of Kneup Feb 13 '18 at 05:46
  • Wizard of Kneup, I have just tried and found, as you mentioned, `configurePersistentStoreCoordinator` is not called when `saveDocumentAs:` implemented from the beginning in `NSDocument` is executed through the menu. But no error occurred in my case. Hmm. Then, `Open Recent` the saved file, the Coordinator is called. Note that `NSDocument` seems to hide a `Save As...` menu item and add `Duplicate`. I manually added `Save As For Test...` binding to `saveDocumentAs:` for reproduction. Tested with Xcode 9.2. – Tora Feb 16 '18 at 01:10
  • The intention of `configurePersistentStoreCoordinator`, in my understanding, is to protect application from malicious files trying to inject harmful class instances. So, it should be called when opening files, but there is no need to be called when saving document to a file. I know you already know that. The point might be why the error happens when `saveAs`. – Tora Feb 16 '18 at 01:10
  • The cue might be `-[NSException initialize]: unrecognized selector sent to instance 0x60400025d7c0`, instead of `value for key 'NS.objects' was of unexpected class 'NSColor'....` Can we see the stack trace when the exception occurred. like this https://stackoverflow.com/questions/28822289/swift-error-unrecognized-selector-sent-to-instance That can be obtained by a `(lldb) bt` command when you are on Xcode or CrashReporter https://developer.apple.com/library/content/technotes/tn2004/tn2123.html – Tora Feb 16 '18 at 01:28
  • If you are on Xcode, does the following attempt show anything helpful? Wait for the exception. Click on any Swift function, instead of Objective-C function in the Debug Navigator on the left side of Xcode so that lldb expects the language Swift, instead of either Objective-C or Assembler. `(lldb) p unsafeBitCast(0x000060c00016e580, to: NSObject.self).description` `(String) $R1 = ""` Replace the address 0x00... with yours. – Tora Feb 16 '18 at 01:48
  • Just in case, to catch the exception to investigate it, in the Breakpoint Navigator located in the left side of Xcode, please add an Exception Breakpoint by clicking on a plus icon at the left bottom corner of the window. https://stackoverflow.com/questions/17802662/exception-breakpoint-in-xcode – Tora Feb 16 '18 at 03:32
  • Hi @Tora, thanks for helping out. I added the stack trace. It doesn't tell me a lot. Maybe it does to you? – Wizard of Kneup Feb 16 '18 at 04:47
  • What version of MacOS do you use? A class `NSBinaryObjectStoreFile` shown in your stack trace seems to disappear on MacOS 10.13.2. Header files of CoreData in the directory `/System/Library/Frameworks/CoreData.framework/Headers` of this version does not have a header file for that class. A web search result, however, show that was there. http://developer.limneos.net/?ios=11.1.2&framework=CoreData.framework&header=NSBinaryObjectStoreFile.h That implies updating the OS might help to solve the problem? – Tora Feb 16 '18 at 05:52
  • This looks like a bug to me (macOS 10.13.3). `migratePersistentStore:toURL:options:withType:error:` adds a Persistent Store to read the autosaved file but doesn't copy the `NSBinaryStoreSecureDecodingClasses` option. – Willeke Feb 16 '18 at 11:58
  • Thank you Tora and Willeke. Yes, I run 10.13.3.And yes, I have filed it as a bug https://bugreport.apple.com/web/?problemID=37431453 already a few days ago. However, I realise that as a hobby programmer it's really tricky to fully understand complex frameworks like Core Data. The probability that the problem is between keyboard and screen is normally bigger than the probability of having found an Apple bug - as the first person on earth. :-) – Wizard of Kneup Feb 16 '18 at 14:37

4 Answers4

2

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:

Tora
  • 970
  • 1
  • 8
  • 15
  • I am well impressed! Thank you! – Wizard of Kneup Feb 17 '18 at 04:55
  • That's my pleasure! I also needed solution for my under-development application. Thank you for the chance to think of it. – Tora Feb 17 '18 at 12:44
  • interestingly enough, the error pops up again. I have 4 documents open and close them one after the other. (not sure that 4 is important here). When I close the last one it fails with the very same error message. Stack Trace shows 2 more entries which I would associate with your method swizzling. I added the stack trace to overall question. Can you confirm? – Wizard of Kneup Feb 20 '18 at 04:56
  • That's interesting! Following to your procedure, I have achieved the same error that you got and started to look into it. Sorry for the late response due to being offline for the last a few days. – Tora Feb 22 '18 at 14:09
  • I have found there was a bug. The code has been updated. Thank you for the report!!! – Tora Feb 22 '18 at 22:09
  • of course, that makes sense. Well done Tora. Sorry for not thinking of it myself. I only have short stretches of programming these days. Difficult to get anything done. – Wizard of Kneup Feb 23 '18 at 04:08
  • I now got this warning from XCode: `One or more models in this application are using transformable properties with transformer names that are either unset, or set to NSKeyedUnarchiveFromDataTransformerName. Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead. At some point, Core Data will default to using "NSSecureUnarchiveFromData" when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.` Did you already look into changes with macOS 10.15? Not yet my priority. – Wizard of Kneup Oct 15 '19 at 12:41
  • I think your "nasty workaround" is no longer needed under 10.15. Could you please confirm? – Wizard of Kneup Oct 21 '19 at 19:32
  • This is a beautiful solution. Bravo Tora. @Wizard of Kneup, this is still needed under 10.15 for accessing store options in case of a save as. Don't know if your options stil need to be set though. – vomi Apr 18 '20 at 12:31
1

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.

  • Opening an existing file -> the func configurePersistentStoreCoordinator is called.
  • File > New and then Save... -> the func 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

enter image description here

Tora
  • 970
  • 1
  • 8
  • 15
  • Additional debugging tips: set, disable and enable the breakpoints in the breakpoint navigator. You don't have to restart the app, just open and close a document. – Willeke Feb 16 '18 at 10:29
  • That's not what I mean. Go to the **Breakpoint navigator**, click on the + at the bottom, choose Symbolic Breakpoint…, paste the name of the method without quotes. – Willeke Feb 16 '18 at 13:55
1

Good news and bad news

I have succeeded to reproduce the exact same error with Xcode 9.2 running on MacOS 10.13.2.

  1. The version 1 of document file is still in a Autosave directory.
  2. Document.xcdatamodel is updated to version 2 (Document 2.xcdatamodel)
  3. When the application starts, the file of version 1 in the Autosave directory is read with NSBinaryStoreSecureDecodingClasses option.
  4. The app works well.
  5. File > Save... triggers NSPersistentStoreCoordinator .migratePersistentStore() aka -[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]:
  6. During the course of migratePersistentStore, NSBinaryStoreSecureDecodingClasses option is not set for both writing to and reading from a temporary file.
  7. The error occurs.

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.

Tora
  • 970
  • 1
  • 8
  • 15
  • You don't need step 2. Create a new document, do some mutations, put the app in the background -> autosave, save the document, error. – Willeke Feb 16 '18 at 14:43
  • Do I understand you correctly that a data migration might be the problem? Wouldn't a new document be created in the latest data model version? – Wizard of Kneup Feb 16 '18 at 14:44
  • Willeke and Wizard of Kneup, thanks! You both are right. I misunderstood. The error happens upon saving to another file. By the way, this is quite basic requirement. The solution should be already available here and there, but I don't know yet. – Tora Feb 16 '18 at 15:20
1

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
  }

}
Tora
  • 970
  • 1
  • 8
  • 15
  • Wow. This is indeed one nasty piece of code. But it really does the trick. However, this is slightly mind-bobling. So, instead of (somehow) changing the stack to use your own version of NSPersistentStoreCoordinator you change the class structure itself. Fine, so we call the new function. But then in the function, if you switch it back and you call self.migragePersistentStore, don't we call the same function again recursively? Respect. Well done! – Wizard of Kneup Feb 16 '18 at 19:44
  • I think I get it. Self is referring to the original store. Of course. So, you call the original function. Clever. I didn't even know one can do these things. You know your stuff, Tora – Wizard of Kneup Feb 16 '18 at 20:46
  • Yea, that is what I thought, too! Recursive call? But use of `self.migratePersistentStore` and `super.migratePersistentStore` did not differ each other. I will need to think Objective-C's underlaying implementation including calling method in subclass and superclass. – Tora Feb 17 '18 at 00:24
  • I have posted another workaround using `extension`. This version is much simpler to understand. – Tora Feb 17 '18 at 00:25