From a73306fecb1326ad28946c73114c1100a3710324 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 12 Apr 2017 19:22:18 +0900 Subject: [PATCH] check correct queue for managed object value access --- CoreStore.xcodeproj/project.pbxproj | 10 +++ CoreStoreTests/DynamicModelTests.swift | 7 +- .../Internal/NSManagedObject+Logging.swift | 21 ++++- ...reStore+CustomDebugStringConvertible.swift | 48 ++++++++++ .../Dynamic Schema/CoreStoreSchema.swift | 34 ++++--- Sources/Setup/Dynamic Models/Entity.swift | 26 +----- .../Setup/Dynamic Models/Relationship.swift | 24 +++++ .../Setup/Dynamic Models/SchemaHistory.swift | 4 + Sources/Setup/Dynamic Models/Value.swift | 16 ++++ .../Setup/Dynamic Models/VersionLock.swift | 89 +++++++++++++++++++ 10 files changed, 239 insertions(+), 40 deletions(-) create mode 100644 Sources/Setup/Dynamic Models/VersionLock.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 406c4ef..7df750f 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -430,6 +430,10 @@ B5A5F2681CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; B5A5F2691CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; B5A5F26A1CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; + B5A991EC1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */; }; + B5A991ED1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */; }; + B5A991EE1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */; }; + B5A991EF1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */; }; B5AEFAB51C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; B5AEFAB61C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; B5AEFAB71C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; @@ -752,6 +756,7 @@ B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudStore.swift; sourceTree = ""; }; B5A261201B64BFDB006EB6D3 /* MigrationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationType.swift; sourceTree = ""; }; B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSelect.swift; sourceTree = ""; }; + B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionLock.swift; sourceTree = ""; }; B5AD60CD1C90141E00F2B2E8 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreBridge.swift; sourceTree = ""; }; B5BDC91A1C202269008147CD /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cartfile; path = ../Cartfile; sourceTree = ""; }; @@ -1134,6 +1139,7 @@ B5D339E61E9493A500C880DE /* Entity.swift */, B5D33A001E96012400C880DE /* Relationship.swift */, B5D339E11E948C3600C880DE /* Value.swift */, + B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */, ); path = "Dynamic Models"; sourceTree = ""; @@ -1785,6 +1791,7 @@ B549F65E1E569C7400FBAB2D /* QueryableAttributeType.swift in Sources */, B5E84F211AFF84860064E85B /* CoreStore+Observing.swift in Sources */, B559CD431CAA8B6300E4D58B /* CSSetupResult.swift in Sources */, + B5A991EC1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5FE4DA71C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, B52F743D1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, B5ECDBEC1CA6BF2000C7F112 /* CSFrom.swift in Sources */, @@ -1956,6 +1963,7 @@ B549F65F1E569C7400FBAB2D /* QueryableAttributeType.swift in Sources */, B559CD451CAA8B6300E4D58B /* CSSetupResult.swift in Sources */, 82BA18B81C4BBD4200A0916E /* ClauseTypes.swift in Sources */, + B5A991ED1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5ECDBEE1CA6BF2000C7F112 /* CSFrom.swift in Sources */, B52F743E1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, 82BA18D61C4BBD7100A0916E /* NSManagedObjectContext+Transaction.swift in Sources */, @@ -2127,6 +2135,7 @@ B549F6611E569C7400FBAB2D /* QueryableAttributeType.swift in Sources */, B52DD19B1BE1F92800949AFE /* CoreStoreLogger.swift in Sources */, B52DD1991BE1F92800949AFE /* DefaultLogger.swift in Sources */, + B5A991EF1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5220E201D130813009BC71E /* CSObjectMonitor.swift in Sources */, B52F74401E9B8724005F3DAC /* DynamicSchema.swift in Sources */, B5220E171D1306DF009BC71E /* UnsafeDataTransaction+Observing.swift in Sources */, @@ -2298,6 +2307,7 @@ B549F6601E569C7400FBAB2D /* QueryableAttributeType.swift in Sources */, B559CD461CAA8B6300E4D58B /* CSSetupResult.swift in Sources */, B56321A61BD65216006C9394 /* MigrationType.swift in Sources */, + B5A991EE1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5ECDBEF1CA6BF2000C7F112 /* CSFrom.swift in Sources */, B52F743F1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, B56321B41BD6521C006C9394 /* NSManagedObjectContext+Transaction.swift in Sources */, diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 953deed..bb4f994 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -39,7 +39,7 @@ class Dog: Animal { let nickname = Value.Optional("nickname") let age = Value.Required("age", default: 1) - let friends = Relationship.ToManyUnordered("friends") + let friends = Relationship.ToManyOrdered("friends") let friends2 = Relationship.ToManyUnordered("friends2", inverse: { $0.friends }) } @@ -63,6 +63,11 @@ class DynamicModelTests: BaseTestDataTestCase { Entity("Animal"), Entity("Dog"), Entity("Person") + ], + versionLock: [ + "Animal": [0x2698c812ebbc3b97, 0x751e3fa3f04cf9, 0x51fd460d3babc82, 0x92b4ba735b5a3053], + "Dog": [0x5285f8e3aff69199, 0x62c3291b59f2ec7c, 0xbe5a571397a4117b, 0x97fb40f5b79ffbdc], + "Person": [0xae4060a59f990ef0, 0x8ac83a6e1411c130, 0xa29fea58e2e38ab6, 0x2071bb7e33d77887] ] ) ) diff --git a/Sources/Internal/NSManagedObject+Logging.swift b/Sources/Internal/NSManagedObject+Logging.swift index fe4a0fa..91dc4ba 100644 --- a/Sources/Internal/NSManagedObject+Logging.swift +++ b/Sources/Internal/NSManagedObject+Logging.swift @@ -31,12 +31,29 @@ import CoreData internal extension NSManagedObject { + @nonobjc + internal func isRunningInAllowedQueue() -> Bool? { + + guard let context = self.managedObjectContext else { + + return nil + } + if context.isTransactionContext { + + return context.parentTransaction?.isRunningInAllowedQueue() + } + if context.isDataStackContext { + + return Thread.isMainThread + } + return nil + } // TODO: test before release (rolled back) // @nonobjc // internal static func cs_swizzleMethodsForLogging() { -// +// // struct Static { -// +// // static let isSwizzled = Static.swizzle() // // private static func swizzle() -> Bool { diff --git a/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift b/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift index b2b053f..ecce091 100644 --- a/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift +++ b/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift @@ -997,6 +997,54 @@ extension Where: CustomDebugStringConvertible, CoreStoreDebugStringConvertible { } +// MARK: - VersionLock + +extension VersionLock: CustomStringConvertible, CustomDebugStringConvertible, CoreStoreDebugStringConvertible { + + // MARK: CustomStringConvertible + + public var description: String { + + var string = "[" + if self.hashesByEntityName.isEmpty { + + string.append(":]") + return string + } + for (index, keyValue) in self.hashesByEntityName.enumerated() { + + let data = keyValue.value + let count = data.count + let bytes = data.withUnsafeBytes { (pointer: UnsafePointer) in + + return (0 ..< (count / MemoryLayout.size)) + .map({ "\("0x\(String(pointer[$0], radix: 16, uppercase: false))")" }) + } + string.append("\(index == 0 ? "\n" : ",\n")\"\(keyValue.key)\": [\(bytes.joined(separator: ", "))]") + } + string.indent(1) + string.append("\n]") + return string + } + + + // MARK: CustomDebugStringConvertible + + public var debugDescription: String { + + return formattedDebugDescription(self) + } + + + // MARK: CoreStoreDebugStringConvertible + + public var coreStoreDumpString: String { + + return self.description + } +} + + // MARK: - XcodeDataModel extension XcodeDataModel: CustomDebugStringConvertible, CoreStoreDebugStringConvertible { diff --git a/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift b/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift index ebeaefa..6a8cc12 100644 --- a/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift +++ b/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift @@ -31,23 +31,16 @@ import Foundation public final class CoreStoreSchema: DynamicSchema { - public convenience init(modelVersion: String, _ entity: DynamicEntity, _ entities: DynamicEntity...) { + public convenience init(modelVersion: String, entities: [DynamicEntity], versionLock: VersionLock? = nil) { self.init( modelVersion: modelVersion, - entities: [entity] + entities + entitiesByConfiguration: [DataStack.defaultConfigurationName: entities], + versionLock: versionLock ) } - public convenience init(modelVersion: String, entities: [DynamicEntity]) { - - self.init( - modelVersion: modelVersion, - entitiesByConfiguration: [DataStack.defaultConfigurationName: entities] - ) - } - - public required init(modelVersion: String, entitiesByConfiguration: [String: [DynamicEntity]]) { + public required init(modelVersion: String, entitiesByConfiguration: [String: [DynamicEntity]], versionLock: VersionLock? = nil) { var actualEntitiesByConfiguration: [String: Set] = [:] for (configuration, entities) in entitiesByConfiguration { @@ -58,7 +51,7 @@ public final class CoreStoreSchema: DynamicSchema { actualEntitiesByConfiguration[DataStack.defaultConfigurationName] = allEntities CoreStore.assert( - autoreleasepool { + cs_lazy { let expectedCount = allEntities.count return Set(allEntities.map({ ObjectIdentifier($0.type) })).count == expectedCount @@ -70,6 +63,23 @@ public final class CoreStoreSchema: DynamicSchema { self.modelVersion = modelVersion self.entitiesByConfiguration = actualEntitiesByConfiguration self.allEntities = allEntities + + if let versionLock = versionLock { + + CoreStore.assert( + versionLock == VersionLock(entityVersionHashesByName: self.rawModel().entityVersionHashesByName), + "A \(cs_typeName(VersionLock.self)) was provided for the \(cs_typeName(CoreStoreSchema.self)) with version \"\(modelVersion)\", but the actual hashes do not match. This may result in unwanted migrations or unusable persistent stores.\nExpected lock values: \(versionLock)\nActual lock values: \(VersionLock(entityVersionHashesByName: self.rawModel().entityVersionHashesByName))" + ) + } + else { + + #if DEBUG + CoreStore.log( + .notice, + message: "These are hashes for the \(cs_typeName(CoreStoreSchema.self)) with version name \"\(modelVersion)\". Copy the dictionary below and pass it to the \(cs_typeName(CoreStoreSchema.self)) initializer's \"versionLock\" argument:\n\(VersionLock(entityVersionHashesByName: self.rawModel().entityVersionHashesByName))" + ) + #endif + } } diff --git a/Sources/Setup/Dynamic Models/Entity.swift b/Sources/Setup/Dynamic Models/Entity.swift index b73c099..91508fd 100644 --- a/Sources/Setup/Dynamic Models/Entity.swift +++ b/Sources/Setup/Dynamic Models/Entity.swift @@ -43,8 +43,7 @@ public struct Entity: DynamicEntity, Hashable { public init(_ entityName: String) { - self.type = O.self - self.entityName = entityName + self.init(O.self, entityName) } public init(_ type: O.Type, _ entityName: String) { @@ -54,29 +53,6 @@ public struct Entity: DynamicEntity, Hashable { } - // MARK: - VersionHash - - public struct VersionHash: ExpressibleByArrayLiteral { - - let hash: Data - - public init(_ hash: Data) { - - self.hash = hash - } - - - // MARK: ExpressibleByArrayLiteral - - public typealias Element = UInt8 - - public init(arrayLiteral elements: UInt8...) { - - self.hash = Data(bytes: elements) - } - } - - // MARK: DynamicEntity public let type: CoreStoreObject.Type diff --git a/Sources/Setup/Dynamic Models/Relationship.swift b/Sources/Setup/Dynamic Models/Relationship.swift index 8f2b146..7cba56f 100644 --- a/Sources/Setup/Dynamic Models/Relationship.swift +++ b/Sources/Setup/Dynamic Models/Relationship.swift @@ -84,6 +84,10 @@ public enum RelationshipContainer { get { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) return self.accessRawObject() .getValue( forKvcKey: self.keyPath, @@ -92,6 +96,10 @@ public enum RelationshipContainer { } set { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) self.accessRawObject() .setValue( newValue, @@ -175,6 +183,10 @@ public enum RelationshipContainer { get { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) return self.accessRawObject() .getValue( forKvcKey: self.keyPath, @@ -190,6 +202,10 @@ public enum RelationshipContainer { } set { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) self.accessRawObject() .setValue( newValue, @@ -279,6 +295,10 @@ public enum RelationshipContainer { get { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) return self.accessRawObject() .getValue( forKvcKey: self.keyPath, @@ -294,6 +314,10 @@ public enum RelationshipContainer { } set { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) self.accessRawObject() .setValue( newValue, diff --git a/Sources/Setup/Dynamic Models/SchemaHistory.swift b/Sources/Setup/Dynamic Models/SchemaHistory.swift index 70de026..c6dc33f 100644 --- a/Sources/Setup/Dynamic Models/SchemaHistory.swift +++ b/Sources/Setup/Dynamic Models/SchemaHistory.swift @@ -31,6 +31,9 @@ import Foundation public final class SchemaHistory: ExpressibleByArrayLiteral { + + // MARK: - + public let currentModelVersion: ModelVersion public let migrationChain: MigrationChain @@ -157,6 +160,7 @@ public final class SchemaHistory: ExpressibleByArrayLiteral { CoreStore.abort("Could not resolve the \(cs_typeName(SchemaHistory.self)) current model version because the \(cs_typeName(MigrationChain.self)) have ambiguous leaf versions: \(candidateVersions)") } } + self.schemaByVersion = schemaByVersion self.migrationChain = migrationChain self.currentModelVersion = currentModelVersion diff --git a/Sources/Setup/Dynamic Models/Value.swift b/Sources/Setup/Dynamic Models/Value.swift index 7e20a43..6c9b9d4 100644 --- a/Sources/Setup/Dynamic Models/Value.swift +++ b/Sources/Setup/Dynamic Models/Value.swift @@ -70,6 +70,10 @@ public enum ValueContainer { get { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) return self.accessRawObject() .getValue( forKvcKey: self.keyPath, @@ -78,6 +82,10 @@ public enum ValueContainer { } set { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) self.accessRawObject() .setValue( newValue, @@ -139,6 +147,10 @@ public enum ValueContainer { get { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) return self.accessRawObject() .getValue( forKvcKey: self.keyPath, @@ -147,6 +159,10 @@ public enum ValueContainer { } set { + CoreStore.assert( + self.accessRawObject().isRunningInAllowedQueue() == true, + "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." + ) self.accessRawObject() .setValue( newValue, diff --git a/Sources/Setup/Dynamic Models/VersionLock.swift b/Sources/Setup/Dynamic Models/VersionLock.swift new file mode 100644 index 0000000..89e9b28 --- /dev/null +++ b/Sources/Setup/Dynamic Models/VersionLock.swift @@ -0,0 +1,89 @@ +// +// VersionLock.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 + + +// MARK: - VersionLock + +public struct VersionLock: ExpressibleByDictionaryLiteral, Equatable { + + public typealias HashElement = UInt64 + + public let hashesByEntityName: [EntityName: Data] + + public init(_ intArrayByEntityName: [EntityName: [HashElement]]) { + + self.init(keyValues: intArrayByEntityName.map({ $0 })) + } + + + // MARK: ExpressibleByDictionaryLiteral + + public typealias Key = EntityName + public typealias Value = [HashElement] + + public init(dictionaryLiteral elements: (EntityName, [HashElement])...) { + + self.init(keyValues: elements) + } + + + // MARK: Equatable + + public static func == (lhs: VersionLock, rhs: VersionLock) -> Bool { + + return lhs.hashesByEntityName == rhs.hashesByEntityName + } + + + // MARK: Internal + + internal init(entityVersionHashesByName: [String: Data]) { + + self.hashesByEntityName = entityVersionHashesByName + } + + + // MARK: Private + + private init(keyValues: [(EntityName, [HashElement])]) { + + var hashesByEntityName: [EntityName: Data] = [:] + for (entityName, intArray) in keyValues { + + hashesByEntityName[entityName] = Data( + buffer: UnsafeBufferPointer( + start: intArray, + count: intArray.count + ) + ) + } + self.hashesByEntityName = hashesByEntityName + } +} + + +