diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 8239992..1c84eb2 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -197,6 +197,10 @@ B52DD1C91BE1F94600949AFE /* NSManagedObjectContext+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */; }; B52DD1CA1BE1F94600949AFE /* NSManagedObjectModel+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */; }; B52DD1CB1BE1F94600949AFE /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F2D1AFF849C0064E85B /* WeakObject.swift */; }; + B52FD3AA1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; + B52FD3AB1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; + B52FD3AC1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; + B52FD3AD1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; B533C4DB1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */; }; B533C4DC1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */; }; B533C4DD1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */; }; @@ -619,6 +623,7 @@ B529C2031CA4A2DB007E7EBD /* CSSaveResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSaveResult.swift; sourceTree = ""; }; B52DD1741BE1F8CC00949AFE /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B52DD17D1BE1F8CC00949AFE /* CoreStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreStoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Logging.swift"; sourceTree = ""; }; B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+CoreStore.swift"; sourceTree = ""; }; B538BA701D15B3E30003A766 /* CoreStoreBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreStoreBridge.m; sourceTree = ""; }; B53FB9FD1CAB2D2F00F0D40A /* CSMigrationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSMigrationResult.swift; sourceTree = ""; }; @@ -1210,6 +1215,7 @@ B5FAD6AB1B51285300714891 /* MigrationManager.swift */, B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */, B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */, + B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */, B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */, B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */, B5E84F321AFF85470064E85B /* NSManagedObjectContext+Setup.swift */, @@ -1558,6 +1564,7 @@ B5ECDC051CA8138100C7F112 /* CSOrderBy.swift in Sources */, B5E1B5981CAA0C23007FD580 /* CSObjectObserver.swift in Sources */, B5519A5F1CA21954002BEF78 /* CSAsynchronousDataTransaction.swift in Sources */, + B52FD3AA1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */, B51FE5AB1CD4D00300E54258 /* CoreStore+CustomDebugStringConvertible.swift in Sources */, B54A6A551BA15F2A007870FD /* FetchedResultsControllerDelegate.swift in Sources */, B5A261211B64BFDB006EB6D3 /* MigrationType.swift in Sources */, @@ -1711,6 +1718,7 @@ B5ECDC071CA8138100C7F112 /* CSOrderBy.swift in Sources */, B5E1B59A1CAA0C23007FD580 /* CSObjectObserver.swift in Sources */, B5519A601CA21954002BEF78 /* CSAsynchronousDataTransaction.swift in Sources */, + B52FD3AB1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */, B51FE5AD1CD4D00300E54258 /* CoreStore+CustomDebugStringConvertible.swift in Sources */, B5FE4DAD1C85D44E00FA6A91 /* SQLiteStore.swift in Sources */, 82BA18C51C4BBD5300A0916E /* ListObserver.swift in Sources */, @@ -1864,6 +1872,7 @@ B5ECDC0F1CA8161B00C7F112 /* CSGroupBy.swift in Sources */, B5ECDC211CA81A2100C7F112 /* CSDataStack+Querying.swift in Sources */, B52DD1C21BE1F94600949AFE /* MigrationManager.swift in Sources */, + B52FD3AD1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */, B5ECDC2D1CA81CC700C7F112 /* CSDataStack+Transaction.swift in Sources */, B5D7A5BA1CA3BF8F005C752B /* CSInto.swift in Sources */, B5A5F26A1CAEC50F004AB9AF /* CSSelect.swift in Sources */, @@ -2017,6 +2026,7 @@ B5E1B59B1CAA0C23007FD580 /* CSObjectObserver.swift in Sources */, B5519A611CA21954002BEF78 /* CSAsynchronousDataTransaction.swift in Sources */, B5FE4DAE1C85D44E00FA6A91 /* SQLiteStore.swift in Sources */, + B52FD3AC1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */, B51FE5AE1CD4D00300E54258 /* CoreStore+CustomDebugStringConvertible.swift in Sources */, B563218C1BD65216006C9394 /* DataStack+Transaction.swift in Sources */, B53FBA0E1CAB5E6500F0D40A /* CSCoreStore+Migrating.swift in Sources */, diff --git a/CoreStoreTests/BaseTests/BaseTestCase.swift b/CoreStoreTests/BaseTests/BaseTestCase.swift index 4ce253e..76eeaff 100644 --- a/CoreStoreTests/BaseTests/BaseTestCase.swift +++ b/CoreStoreTests/BaseTests/BaseTestCase.swift @@ -153,6 +153,8 @@ class TestLogger: CoreStoreLogger { // MARK: CoreStoreLogger + var enableObjectConcurrencyDebugging: Bool = true + func log(level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) { switch level { diff --git a/Sources/Internal/NSManagedObject+Logging.swift b/Sources/Internal/NSManagedObject+Logging.swift new file mode 100644 index 0000000..a6e83a6 --- /dev/null +++ b/Sources/Internal/NSManagedObject+Logging.swift @@ -0,0 +1,240 @@ +// +// NSManagedObject+Logging.swift +// CoreStore +// +// Copyright © 2017 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import CoreData + + +// MARK: - NSManagedObject + +internal extension NSManagedObject { + + @nonobjc + internal static func cs_swizzleMethodsForLogging() { + + struct Static { + + static let isSwizzled = Static.swizzle() + + private static func swizzle() -> Bool { + + NSManagedObject.cs_swizzle( + original: #selector(NSManagedObject.willAccessValue(forKey:)), + proxy: #selector(NSManagedObject.cs_willAccessValue(forKey:)) + ) + NSManagedObject.cs_swizzle( + original: #selector(NSManagedObject.willChangeValue(forKey:)), + proxy: #selector(NSManagedObject.cs_willChangeValue(forKey:)) + ) + NSManagedObject.cs_swizzle( + original: #selector(NSManagedObject.willChangeValue(forKey:withSetMutation:using:)), + proxy: #selector(NSManagedObject.cs_willChangeValue(forKey:withSetMutation:using:)) + ) + return true + } + } + assert(Static.isSwizzled) + } + + @nonobjc + private static func cs_swizzle(original originalSelector: Selector, proxy swizzledSelector: Selector) { + + let originalMethod = class_getInstanceMethod(NSManagedObject.self, originalSelector) + let swizzledMethod = class_getInstanceMethod(NSManagedObject.self, swizzledSelector) + let didAddMethod = class_addMethod( + NSManagedObject.self, + originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod) + ) + if didAddMethod { + + class_replaceMethod( + NSManagedObject.self, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod) + ) + } + else { + + method_exchangeImplementations(originalMethod, swizzledMethod) + } + } + + private dynamic func cs_willAccessValue(forKey key: String?) { + + self.cs_willAccessValue(forKey: key) + + guard CoreStore.logger.enableObjectConcurrencyDebugging else { + + return + } + + guard let context = self.managedObjectContext else { + + CoreStore.log( + .warning, + message: "Attempted to access the \"\(key ?? "")\" key of an object of type \(cs_typeName(self)) after has been deleted from its \(cs_typeName(NSManagedObjectContext.self))." + ) + return + } + if context.isTransactionContext { + + guard let transaction = context.parentTransaction else { + + CoreStore.log( + .warning, + message: "Attempted to access the \"\(key ?? "")\" key of an object of type \(cs_typeName(self)) after has been deleted from its transaction." + ) + return + } + CoreStore.assert( + transaction.isRunningInAllowedQueue(), + "Attempted to access the \"\(key ?? "")\" key of an object of type \(cs_typeName(self)) outside its transaction's designated queue." + ) + return + } + if context.isDataStackContext { + + guard context.parentStack != nil else { + + CoreStore.log( + .warning, + message: "Attempted to access the \"\(key ?? "")\" key of an object of type \(cs_typeName(self)) after has been deleted from its \(cs_typeName(DataStack.self)).") + return + } + CoreStore.assert( + Thread.isMainThread, + "Attempted to access the \"\(key ?? "")\" key of an object of type \(cs_typeName(self)) outside the main thread." + ) + return + } + } + + private dynamic func cs_willChangeValue(forKey key: String?) { + + self.cs_willChangeValue(forKey: key) + + guard CoreStore.logger.enableObjectConcurrencyDebugging else { + + return + } + + guard let context = self.managedObjectContext else { + + CoreStore.log( + .warning, + message: "Attempted to change the \"\(key ?? "")\" of an object of type \(cs_typeName(self)) after has been deleted from its \(cs_typeName(NSManagedObjectContext.self))." + ) + return + } + if context.isTransactionContext { + + guard let transaction = context.parentTransaction else { + + CoreStore.log( + .warning, + message: "Attempted to change the \"\(key ?? "")\" of an object of type \(cs_typeName(self)) after has been deleted from its transaction." + ) + return + } + CoreStore.assert( + transaction.isRunningInAllowedQueue(), + "Attempted to change the \"\(key ?? "")\" of an object of type \(cs_typeName(self)) outside its transaction's designated queue." + ) + return + } + if context.isDataStackContext { + + guard context.parentStack != nil else { + + CoreStore.log( + .warning, + message: "Attempted to change the \"\(key ?? "")\" of an object of type \(cs_typeName(self)) after has been deleted from its \(cs_typeName(DataStack.self)).") + return + } + CoreStore.assert( + Thread.isMainThread, + "Attempted to change the \"\(key ?? "")\" of an object of type \(cs_typeName(self)) outside the main thread." + ) + return + } + } + + private dynamic func cs_willChangeValue(forKey inKey: String, withSetMutation inMutationKind: NSKeyValueSetMutationKind, using inObjects: Set) { + + self.cs_willChangeValue( + forKey: inKey, + withSetMutation: inMutationKind, + using: inObjects + ) + + guard CoreStore.logger.enableObjectConcurrencyDebugging else { + + return + } + + guard let context = self.managedObjectContext else { + + CoreStore.log( + .warning, + message: "Attempted to mutate the \"\(inKey)\" of an object of type \(cs_typeName(self)) after has been deleted from its \(cs_typeName(NSManagedObjectContext.self))." + ) + return + } + if context.isTransactionContext { + + guard let transaction = context.parentTransaction else { + + CoreStore.log( + .warning, + message: "Attempted to mutate the \"\(inKey)\" of an object of type \(cs_typeName(self)) after has been deleted from its transaction." + ) + return + } + CoreStore.assert( + transaction.isRunningInAllowedQueue(), + "Attempted to mutate the \"\(inKey)\" of an object of type \(cs_typeName(self)) outside its transaction's designated queue." + ) + return + } + if context.isDataStackContext { + + guard context.parentStack != nil else { + + CoreStore.log( + .warning, + message: "Attempted to mutate the \"\(inKey)\" of an object of type \(cs_typeName(self)) after has been deleted from its \(cs_typeName(DataStack.self)).") + return + } + CoreStore.assert( + Thread.isMainThread, + "Attempted to mutate the \"\(inKey)\" of an object of type \(cs_typeName(self)) outside the main thread." + ) + return + } + } +} diff --git a/Sources/Logging/CoreStoreLogger.swift b/Sources/Logging/CoreStoreLogger.swift index 57e1714..9e9e46c 100644 --- a/Sources/Logging/CoreStoreLogger.swift +++ b/Sources/Logging/CoreStoreLogger.swift @@ -47,6 +47,11 @@ public enum LogLevel { */ public protocol CoreStoreLogger { + /** + When `true`, all `NSManagedObject` attribute and relationship access will raise an assertion when executed on the wrong transaction/datastack queue. Defaults to `false` if not implemented. + */ + var enableObjectConcurrencyDebugging: Bool { get set } + /** Handles log messages sent by the `CoreStore` framework. @@ -94,6 +99,12 @@ public protocol CoreStoreLogger { extension CoreStoreLogger { + public var enableObjectConcurrencyDebugging: Bool { + + get { return false } + set {} + } + public func abort(_ message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) { Swift.fatalError(message, file: fileName, line: UInt(lineNumber)) diff --git a/Sources/Logging/DefaultLogger.swift b/Sources/Logging/DefaultLogger.swift index fcfc486..1f558ab 100644 --- a/Sources/Logging/DefaultLogger.swift +++ b/Sources/Logging/DefaultLogger.swift @@ -33,6 +33,11 @@ import Foundation */ public final class DefaultLogger: CoreStoreLogger { + /** + When `true`, all `NSManagedObject` attribute and relationship access will raise an assertion when executed on the wrong transaction/datastack queue. Defaults to `false`. + */ + public var enableObjectConcurrencyDebugging: Bool = false + /** Creates a `DefaultLogger`. */ diff --git a/Sources/Setup/DataStack.swift b/Sources/Setup/DataStack.swift index 8463c43..e4fc2f9 100644 --- a/Sources/Setup/DataStack.swift +++ b/Sources/Setup/DataStack.swift @@ -59,6 +59,8 @@ public final class DataStack: Equatable { */ public required init(model: NSManagedObjectModel, migrationChain: MigrationChain = nil) { + _ = DataStack.isGloballyInitialized + CoreStore.assert( migrationChain.valid, "Invalid migration chain passed to the \(cs_typeName(DataStack.self)). Check that the model versions' order is correct and that no repetitions or ambiguities exist." @@ -499,6 +501,12 @@ public final class DataStack: Equatable { // MARK: Private + private static let isGloballyInitialized: Bool = { + + NSManagedObject.cs_swizzleMethodsForLogging() + return true + }() + private var configurationStoreMapping = [String: NSPersistentStore]() private var entityConfigurationsMapping = [String: Set]()