Skip to content

Commit 4bf6826

Browse files
authored
Merge pull request #1412 from OneSignal/fix/user_module_user_defaults_crashes
Fix crashes when encoding user models
2 parents 80ebbcc + 98908ee commit 4bf6826

File tree

9 files changed

+104
-117
lines changed

9 files changed

+104
-117
lines changed

iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,12 @@ - (IBAction)removeAliasButton:(UIButton *)sender {
121121
}
122122

123123
- (IBAction)sendTagButton:(id)sender {
124-
if (self.tagKey.text && self.tagKey.text.length
125-
&& self.tagValue.text && self.tagValue.text.length) {
124+
if (self.tagValue.text && self.tagValue.text.length) {
126125
NSLog(@"Sending tag with key: %@ value: %@", self.tagKey.text, self.tagValue.text);
127126
[OneSignal.User addTagWithKey:self.tagKey.text value:self.tagValue.text];
127+
} else {
128+
NSLog(@"Removing tag with key: %@", self.tagKey.text);
129+
[OneSignal.User removeTag:self.tagKey.text];
128130
}
129131
}
130132

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
03E56DD328405F4A006AA1DA /* OneSignalAppDelegateOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E56DD228405F4A006AA1DA /* OneSignalAppDelegateOverrider.m */; };
5454
16664C4C25DDB195003B8A14 /* NSTimeZoneOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = 16664C4B25DDB195003B8A14 /* NSTimeZoneOverrider.m */; };
5555
37E6B2BB19D9CAF300D0C601 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37E6B2BA19D9CAF300D0C601 /* UIKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
56-
3C05904B2B86BC9E00450D48 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C05904A2B86BC9E00450D48 /* UnfairLock.swift */; };
5756
3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0EF49D28A1DBCB00E5434B /* OSUserInternalImpl.swift */; };
5857
3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */ = {isa = PBXBuildFile; fileRef = 3C115164289A259500565C41 /* OneSignalOSCore.docc */; };
5958
3C115171289A259500565C41 /* OneSignalOSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C115163289A259500565C41 /* OneSignalOSCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -945,7 +944,6 @@
945944
1AF75EAD1E8567FD0097B315 /* NSString+OneSignal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+OneSignal.m"; sourceTree = "<group>"; };
946945
37747F9319147D6500558FAD /* libOneSignal.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libOneSignal.a; sourceTree = BUILT_PRODUCTS_DIR; };
947946
37E6B2BA19D9CAF300D0C601 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
948-
3C05904A2B86BC9E00450D48 /* UnfairLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfairLock.swift; sourceTree = "<group>"; };
949947
3C0EF49D28A1DBCB00E5434B /* OSUserInternalImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSUserInternalImpl.swift; sourceTree = "<group>"; };
950948
3C115161289A259500565C41 /* OneSignalOSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalOSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
951949
3C115163289A259500565C41 /* OneSignalOSCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalOSCore.h; sourceTree = "<group>"; };
@@ -1643,14 +1641,6 @@
16431641
name = Frameworks;
16441642
sourceTree = "<group>";
16451643
};
1646-
3C0590492B86BC5300450D48 /* Utils */ = {
1647-
isa = PBXGroup;
1648-
children = (
1649-
3C05904A2B86BC9E00450D48 /* UnfairLock.swift */,
1650-
);
1651-
path = Utils;
1652-
sourceTree = "<group>";
1653-
};
16541644
3C115162289A259500565C41 /* OneSignalOSCore */ = {
16551645
isa = PBXGroup;
16561646
children = (
@@ -1664,7 +1654,6 @@
16641654
isa = PBXGroup;
16651655
children = (
16661656
3C115163289A259500565C41 /* OneSignalOSCore.h */,
1667-
3C0590492B86BC5300450D48 /* Utils */,
16681657
3C115188289ADEA300565C41 /* OSModelStore.swift */,
16691658
3C115186289ADE7700565C41 /* OSModelStoreListener.swift */,
16701659
3C115184289ADE4F00565C41 /* OSModel.swift */,
@@ -3344,7 +3333,6 @@
33443333
3C115187289ADE7700565C41 /* OSModelStoreListener.swift in Sources */,
33453334
3CE5F9E3289D88DC004A156E /* OSModelStoreChangedHandler.swift in Sources */,
33463335
3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */,
3347-
3C05904B2B86BC9E00450D48 /* UnfairLock.swift in Sources */,
33483336
3C11518D289AF5E800565C41 /* OSModelChangedHandler.swift in Sources */,
33493337
3C8E6DF928A6D89E0031E48A /* OSOperationExecutor.swift in Sources */,
33503338
);

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Utils/UnfairLock.swift

Lines changed: 0 additions & 47 deletions
This file was deleted.

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
124124
addRequestQueue.append(request)
125125

126126
case OS_REMOVE_ALIAS_DELTA:
127-
if let label = aliases.first?.key {
127+
for (label, _) in aliases {
128128
let request = OSRequestRemoveAlias(labelToRemove: label, identityModel: model)
129129
removeRequestQueue.append(request)
130130
}

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,6 @@ extension OSUserExecutor {
520520
return
521521
}
522522

523-
if let identityObject = parseIdentityObjectResponse(response) {
524-
OneSignalUserManagerImpl.sharedInstance.user.identityModel.hydrate(identityObject)
525-
}
526-
527523
if let propertiesObject = parsePropertiesObjectResponse(response) {
528524
OneSignalUserManagerImpl.sharedInstance.user.propertiesModel.hydrate(propertiesObject)
529525
}

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,16 @@ import OneSignalOSCore
3131

3232
class OSIdentityModel: OSModel {
3333
var onesignalId: String? {
34-
aliasesLock.locked {
35-
return aliases[OS_ONESIGNAL_ID]
36-
}
34+
return internalGetAlias(OS_ONESIGNAL_ID)
3735
}
3836

3937
var externalId: String? {
40-
aliasesLock.locked {
41-
return aliases[OS_EXTERNAL_ID]
42-
}
38+
return internalGetAlias(OS_EXTERNAL_ID)
4339
}
4440

41+
// All access to aliases should go through helper methods with locking
4542
var aliases: [String: String] = [:]
46-
private let aliasesLock = UnfairLock()
43+
private let aliasesLock = NSRecursiveLock()
4744

4845
// TODO: We need to make this token secure
4946
public var jwtBearerToken: String?
@@ -57,8 +54,10 @@ class OSIdentityModel: OSModel {
5754
}
5855

5956
override func encode(with coder: NSCoder) {
60-
super.encode(with: coder)
61-
coder.encode(aliases, forKey: "aliases")
57+
aliasesLock.withLock {
58+
super.encode(with: coder)
59+
coder.encode(aliases, forKey: "aliases")
60+
}
6261
}
6362

6463
required init?(coder: NSCoder) {
@@ -70,55 +69,57 @@ class OSIdentityModel: OSModel {
7069
self.aliases = aliases
7170
}
7271

72+
/** Threadsafe getter for an alias */
73+
private func internalGetAlias(_ label: String) -> String? {
74+
aliasesLock.withLock {
75+
return self.aliases[label]
76+
}
77+
}
78+
79+
/** Threadsafe setter or removal for aliases */
80+
private func internalAddAliases(_ aliases: [String: String]) {
81+
aliasesLock.withLock {
82+
for (label, id) in aliases {
83+
// Remove the alias if the ID field is ""
84+
self.aliases[label] = id.isEmpty ? nil : id
85+
}
86+
}
87+
self.set(property: "aliases", newValue: aliases)
88+
}
89+
7390
/**
7491
Called to clear the model's data in preparation for hydration via a fetch user call.
7592
*/
7693
func clearData() {
77-
aliasesLock.locked {
94+
aliasesLock.withLock {
7895
self.aliases = [:]
7996
}
8097
}
8198

8299
// MARK: - Alias Methods
83100

84101
func addAliases(_ aliases: [String: String]) {
85-
aliasesLock.locked {
86-
for (label, id) in aliases {
87-
self.aliases[label] = id
88-
}
89-
self.set(property: "aliases", newValue: aliases)
90-
}
102+
internalAddAliases(aliases)
91103
}
92104

93105
func removeAliases(_ labels: [String]) {
94-
aliasesLock.locked {
95-
for label in labels {
96-
self.aliases.removeValue(forKey: label)
97-
self.set(property: "aliases", newValue: [label: ""])
98-
}
106+
let aliasesToRemoveAsDict = labels.reduce(into: [String: String]()) { result, label in
107+
result[label] = ""
99108
}
109+
internalAddAliases(aliasesToRemoveAsDict)
100110
}
101111

102112
public override func hydrateModel(_ response: [String: Any]) {
103-
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityModel hydrateModel()")
104-
var newOnesignalId: String?
105-
var newExternalId: String?
106-
107-
aliasesLock.locked {
108-
for property in response {
109-
switch property.key {
110-
case "external_id":
111-
newExternalId = property.value as? String
112-
aliases[OS_EXTERNAL_ID] = newExternalId
113-
case "onesignal_id":
114-
newOnesignalId = property.value as? String
115-
aliases[OS_ONESIGNAL_ID] = newOnesignalId
116-
default:
117-
aliases[property.key] = property.value as? String
118-
}
119-
self.set(property: "aliases", newValue: aliases)
120-
}
113+
guard let remoteAliases = response as? [String: String] else {
114+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityModel.hydrateModel failed to parse response \(response) as Strings")
115+
return
121116
}
117+
118+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityModel hydrateModel with aliases: \(remoteAliases)")
119+
let newOnesignalId = remoteAliases[OS_ONESIGNAL_ID]
120+
let newExternalId = remoteAliases[OS_EXTERNAL_ID]
121+
122+
internalAddAliases(remoteAliases)
122123
fireUserStateChanged(newOnesignalId: newOnesignalId, newExternalId: newExternalId)
123124
}
124125

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class OSPropertiesModel: OSModel {
8282
var timezoneId = TimeZone.current.identifier
8383

8484
var tags: [String: String] = [:]
85-
private let tagsLock = UnfairLock()
85+
private let tagsLock = NSRecursiveLock()
8686

8787
// MARK: - Initialization
8888

@@ -102,10 +102,12 @@ class OSPropertiesModel: OSModel {
102102
}
103103

104104
override func encode(with coder: NSCoder) {
105-
super.encode(with: coder)
106-
coder.encode(language, forKey: "language")
107-
coder.encode(tags, forKey: "tags")
108-
// ... and more
105+
tagsLock.withLock {
106+
super.encode(with: coder)
107+
coder.encode(language, forKey: "language")
108+
coder.encode(tags, forKey: "tags")
109+
// ... and more
110+
}
109111
}
110112

111113
required init?(coder: NSCoder) {
@@ -125,31 +127,31 @@ class OSPropertiesModel: OSModel {
125127
*/
126128
func clearData() {
127129
// TODO: What about language, lat, long?
128-
tagsLock.locked {
130+
tagsLock.withLock {
129131
self.tags = [:]
130132
}
131133
}
132134

133135
// MARK: - Tag Methods
134136

135137
func addTags(_ tags: [String: String]) {
136-
tagsLock.locked {
138+
tagsLock.withLock {
137139
for (key, value) in tags {
138140
self.tags[key] = value
139141
}
140-
self.set(property: "tags", newValue: tags)
141142
}
143+
self.set(property: "tags", newValue: tags)
142144
}
143145

144146
func removeTags(_ tags: [String]) {
145-
tagsLock.locked {
146-
var tagsToSend: [String: String] = [:]
147+
var tagsToSend: [String: String] = [:]
148+
tagsLock.withLock {
147149
for tag in tags {
148150
self.tags.removeValue(forKey: tag)
149151
tagsToSend[tag] = ""
150152
}
151-
self.set(property: "tags", newValue: tagsToSend)
152153
}
154+
self.set(property: "tags", newValue: tagsToSend)
153155
}
154156

155157
public override func hydrateModel(_ response: [String: Any]) {
@@ -158,7 +160,7 @@ class OSPropertiesModel: OSModel {
158160
case "language":
159161
self.language = property.value as? String
160162
case "tags":
161-
tagsLock.locked {
163+
tagsLock.withLock {
162164
self.tags = property.value as? [String: String] ?? [:]
163165
}
164166
default:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# in tests, we may want to force cast and throw any errors
22
disabled_rules:
33
- force_cast
4+
- variable_name

iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,48 @@ final class OneSignalUserTests: XCTestCase {
9393
// 1. OpRepo: `deltaQueue.remove(at: index)` index out of bounds
9494
// 2. OSPropertyOperationExecutor: `deltaQueue.append(delta)` EXC_BAD_ACCESS
9595
}
96+
97+
/**
98+
This test reproduced a crash when the property model is being encoded.
99+
*/
100+
func testEncodingPropertiesModel_withConcurrency_doesNotCrash() throws {
101+
/* Setup */
102+
let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer())
103+
104+
/* When */
105+
DispatchQueue.concurrentPerform(iterations: 5_000) { i in
106+
// 1. Add tags
107+
for num in 0...9 {
108+
propertiesModel.addTags(["\(i)tag\(num)": "value"])
109+
}
110+
111+
// 2. Encode the model
112+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: "PropertyModel", withValue: propertiesModel)
113+
114+
// 3. Clear the tags
115+
propertiesModel.clearData()
116+
}
117+
}
118+
119+
/**
120+
This test reproduced a crash when the identity model is being encoded.
121+
*/
122+
func testEncodingIdentityModel_withConcurrency_doesNotCrash() throws {
123+
/* Setup */
124+
let identityModel = OSIdentityModel(aliases: nil, changeNotifier: OSEventProducer())
125+
126+
/* When */
127+
DispatchQueue.concurrentPerform(iterations: 5_000) { i in
128+
// 1. Add aliases
129+
for num in 0...9 {
130+
identityModel.addAliases(["\(i)alias\(num)": "value"])
131+
}
132+
133+
// 2. Encode the model
134+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: "IdentityModel", withValue: identityModel)
135+
136+
// 2. Clear the aliases
137+
identityModel.clearData()
138+
}
139+
}
96140
}

0 commit comments

Comments
 (0)