Pierre Bernard
Pierre Bernard

Reputation: 3198

NSPersistentDocument crash on “save as”

My Core Data document-based application crashes on "save as". The problem seems similar to the one described in the cocoa-dev thread titled "NSPersistentDocument objects "gutted" after Duplicate, Rename in 10.9"

The key differences being that:

The problem affects even the simplest NSPersistentDocument. It has been around at least since 2014. Thus I hope for others to have encountered the same problem and have a workaround you care to share.

My sample project uses a single entity with a single attribute. It has a table view to display all instances of the entity and a button to create a new one. I strayed from the default template only to disable autosavesInPlace.

The steps to reproduce the crash are:

  1. Build and run on Yosemite. The bug appears to have been fixed in El Capitan
  2. Create a new document
  3. Insert a new object
  4. Save the document
  5. Close the document
  6. Re-open the document
  7. Change the value of the attribute in the table
  8. Use "Save As" to save under a new name

On OS X Yosemite this always crashes with the following backtrace:

_propertyAtIndexForEntityDescription ()
snapshot_get_value_as_object ()
-[NSManagedObject(_NSInternalMethods) _validatePropertiesWithError:] ()
-[NSManagedObject(_NSInternalMethods) _validateForSave:] ()
-[NSManagedObject validateForUpdate:] ()
-[NSManagedObjectContext(_NSInternalAdditions) _validateObjects:forOperation:error:exhaustive:forSave:] ()
-[NSManagedObjectContext(_NSInternalAdditions) _validateChangesForSave:] ()
-[NSManagedObjectContext(_NSInternalChangeProcessing) _prepareForPushChanges:] ()
-[NSManagedObjectContext save:] ()
-[NSPersistentDocument writeToURL:ofType:forSaveOperation:originalContentsURL:error:] ()
-[NSDocument _writeSafelyToURL:ofType:forSaveOperation:forceTemporaryDirectory:error:] ()
-[NSDocument _writeSafelyToURL:ofType:forSaveOperation:error:] ()
-[NSDocument writeSafelyToURL:ofType:forSaveOperation:error:] ()
-[NSPersistentDocument writeSafelyToURL:ofType:forSaveOperation:error:] ()
__66-[NSDocument saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_22353 ()
__66-[NSDocument saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke2350 ()
__66-[NSDocument saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_22222 ()
__110-[NSFileCoordinator(NSPrivate) _coordinateReadingItemAtURL:options:writingItemAtURL:options:error:byAccessor:]_block_invoke428 ()
-[NSFileCoordinator(NSPrivate) _invokeAccessor:orDont:andRelinquishAccessClaim:] ()
-[NSFileCoordinator(NSPrivate) _coordinateReadingItemAtURL:options:writingItemAtURL:options:error:byAccessor:] ()
-[NSDocument _fileCoordinator:coordinateReadingContentsAndWritingItemAtURL:byAccessor:] ()
-[NSDocument _fileCoordinator:asynchronouslyCoordinateReadingContentsAndWritingItemAtURL:byAccessor:] ()
__66-[NSDocument saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke2221 ()
-[NSDocument _prepareToSaveToURL:forSaveOperation:completionHandler:] ()
__66-[NSDocument saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke ()
-[NSDocument continueFileAccessUsingBlock:] ()
-[NSDocument _performFileAccessOnMainThread:usingBlock:] ()
-[NSDocument performAsynchronousFileAccessUsingBlock:] ()
-[NSDocument saveToURL:ofType:forSaveOperation:completionHandler:] ()
__85-[NSDocument saveToURL:ofType:forSaveOperation:delegate:didSaveSelector:contextInfo:]_block_invoke_2 ()
-[NSDocument _commitEditingThenContinue:] ()
__62-[NSPersistentDocument _documentEditor:didCommit:withContext:]_block_invoke ()

Edit 1. Possible workaround:

I can patch around the crash by preventing the original managed object context from being saved during the “save as” operation. After “save as” I immediately close the existing document and re-open the document from the new location. It’s all very ugly and may break other NSPersistentDocument behavior.

Edit 2. Above workaround loses unsaved changes

Preventing the original managed object context from saving does avoid the crash. The end-result however is a copy of the document in its last saved state. Unsaved changes are lost.

Edit 3. Gutted snapshot

By the time the old managed object context tries to save changes to the new file, the object snapshot no longer knows its entity <_CDSnapshot_Entity_: 0x600001f3cfd0> (entity: (null); id: 0x40000b <x-coredata://83B64FD3-B5B9-44CB-976D-54C0326FDFF5/Entity/p1> ; data: (null)). I don’t see any instance variable backing -[_CDSnapshot entity]. I assume it should find that from the object ID.

Upvotes: 1

Views: 360

Answers (1)

Pierre Bernard
Pierre Bernard

Reputation: 3198

I have come up with a workaround that appears to work for my use case.

- (BOOL)writeToURL:(NSURL *)absoluteURL
            ofType:(NSString *)typeName
  forSaveOperation:(NSSaveOperationType)saveOperation
originalContentsURL:(NSURL *)absoluteOriginalContentsURL
             error:(NSError **)error
{
    if ((saveOperation == NSSaveAsOperation) && (absoluteOriginalContentsURL != nil)) {
        NSFileManager *fileManager = [NSFileManager defaultManager];

        if (![fileManager copyItemAtURL:absoluteOriginalContentsURL toURL:absoluteURL error:error]) {
            return NO;
        }

        NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
        NSPersistentStoreCoordinator *persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator;
        NSPersistentStore *store = [persistentStoreCoordinator persistentStoreForURL:absoluteOriginalContentsURL];

        [persistentStoreCoordinator setURL:absoluteURL forPersistentStore:store];

        if (![managedObjectContext save:error]) {
            return NO;
        }

        return YES;
    }

    return [super writeToURL:absoluteURL
                             ofType:typeName
                   forSaveOperation:saveOperation
                originalContentsURL:absoluteOriginalContentsURL
                              error:error];
}

On save as, I copy the old document to the new (temporary) location. Then I set the new URL on the persistent store and let the managed object context save pending changes to that new document.

NSPersistentDocument takes care of generating the temporary URL passed in as absoluteURL, moves the saved file to the new location and calls setFileURL: once saving is complete.

I have disabled journaling on the SQLite store backing the document. Thus I need only copy the one file at absoluteOriginalContentsURL.

Upvotes: 1

Related Questions