Add protocol context to protocol kit.
This commit is contained in:
parent
169c455d11
commit
122ef91e57
3
Podfile
3
Podfile
|
@ -9,8 +9,9 @@ def shared_pods
|
|||
pod 'SQLCipher', :git => 'https://github.com/sqlcipher/sqlcipher.git', :commit => 'd5c2bec'
|
||||
# pod 'YapDatabase/SQLCipher', path: '../YapDatabase'
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/WhisperSystems/YapDatabase.git', branch: 'release/unencryptedHeaders'
|
||||
pod 'AxolotlKit', path: '../SignalProtocolKit'
|
||||
pod 'SignalServiceKit', path: '.'
|
||||
pod 'AxolotlKit', git: 'https://github.com/WhisperSystems/SignalProtocolKit.git', branch: 'mkirk/framework-friendly'
|
||||
# pod 'AxolotlKit', git: 'https://github.com/WhisperSystems/SignalProtocolKit.git', branch: 'mkirk/framework-friendly'
|
||||
#pod 'AxolotlKit', path: '../SignalProtocolKit'
|
||||
pod 'HKDFKit', git: 'https://github.com/WhisperSystems/HKDFKit.git', branch: 'mkirk/framework-friendly'
|
||||
#pod 'HKDFKit', path: '../HKDFKit'
|
||||
|
|
10
Podfile.lock
10
Podfile.lock
|
@ -129,7 +129,7 @@ PODS:
|
|||
DEPENDENCIES:
|
||||
- AFNetworking
|
||||
- ATAppUpdater
|
||||
- AxolotlKit (from `https://github.com/WhisperSystems/SignalProtocolKit.git`, branch `mkirk/framework-friendly`)
|
||||
- AxolotlKit (from `../SignalProtocolKit`)
|
||||
- Curve25519Kit (from `https://github.com/WhisperSystems/Curve25519Kit`, branch `mkirk/framework-friendly`)
|
||||
- GRKOpenSSLFramework (from `https://github.com/WhisperSystems/GRKOpenSSLFramework`)
|
||||
- HKDFKit (from `https://github.com/WhisperSystems/HKDFKit.git`, branch `mkirk/framework-friendly`)
|
||||
|
@ -146,8 +146,7 @@ DEPENDENCIES:
|
|||
|
||||
EXTERNAL SOURCES:
|
||||
AxolotlKit:
|
||||
:branch: mkirk/framework-friendly
|
||||
:git: https://github.com/WhisperSystems/SignalProtocolKit.git
|
||||
:path: ../SignalProtocolKit
|
||||
Curve25519Kit:
|
||||
:branch: mkirk/framework-friendly
|
||||
:git: https://github.com/WhisperSystems/Curve25519Kit
|
||||
|
@ -171,9 +170,6 @@ EXTERNAL SOURCES:
|
|||
:git: https://github.com/WhisperSystems/YapDatabase.git
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
AxolotlKit:
|
||||
:commit: 6dd55895b523e887c633bd31b9eedbfb515b8a5d
|
||||
:git: https://github.com/WhisperSystems/SignalProtocolKit.git
|
||||
Curve25519Kit:
|
||||
:commit: 03a19c80aafc10a3464f0c086b1eb38239c507ac
|
||||
:git: https://github.com/WhisperSystems/Curve25519Kit
|
||||
|
@ -221,6 +217,6 @@ SPEC CHECKSUMS:
|
|||
YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de
|
||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||
|
||||
PODFILE CHECKSUM: 0d804514eb2db34b9874b653e543255d8c2f5751
|
||||
PODFILE CHECKSUM: d1c081f5e8cda394caa2bfbb157d628f33352cff
|
||||
|
||||
COCOAPODS: 1.3.1
|
||||
|
|
|
@ -3152,11 +3152,7 @@
|
|||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
"GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
"SSK_BUILDING_FOR_TESTS=1",
|
||||
);
|
||||
"GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1";
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
|
|
@ -55,7 +55,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
actionBlock:^{
|
||||
dispatch_async([OWSDispatch sessionStoreQueue], ^{
|
||||
[[TSStorageManager sharedManager]
|
||||
deleteAllSessionsForContact:thread.contactIdentifier];
|
||||
deleteAllSessionsForContact:thread.contactIdentifier
|
||||
protocolContext:protocolContext];
|
||||
});
|
||||
}],
|
||||
[OWSTableItem itemWithTitle:@"Archive all sessions"
|
||||
|
|
|
@ -392,6 +392,7 @@ if __name__ == "__main__":
|
|||
|
||||
parser = argparse.ArgumentParser(description='Precommit script.')
|
||||
parser.add_argument('--all', action='store_true', help='process all files in or below current dir')
|
||||
parser.add_argument('--path', help='used to specify a path to process.')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.all:
|
||||
|
@ -399,6 +400,11 @@ if __name__ == "__main__":
|
|||
for filename in filenames:
|
||||
file_path = os.path.abspath(os.path.join(rootdir, filename))
|
||||
process_if_appropriate(file_path)
|
||||
elif args.path:
|
||||
for rootdir, dirnames, filenames in os.walk(args.path):
|
||||
for filename in filenames:
|
||||
file_path = os.path.abspath(os.path.join(rootdir, filename))
|
||||
process_if_appropriate(file_path)
|
||||
else:
|
||||
filepaths = []
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSRecordTranscriptJob.h"
|
||||
|
@ -69,7 +69,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if (transcript.isEndSessionMessage) {
|
||||
DDLogInfo(@"%@ EndSession was sent to recipient: %@.", self.logTag, transcript.recipientId);
|
||||
dispatch_async([OWSDispatch sessionStoreQueue], ^{
|
||||
[self.storageManager deleteAllSessionsForContact:transcript.recipientId];
|
||||
[self.storageManager deleteAllSessionsForContact:transcript.recipientId protocolContext:protocolContext];
|
||||
});
|
||||
[[[TSInfoMessage alloc] initWithTimestamp:transcript.timestamp
|
||||
inThread:thread
|
||||
|
|
|
@ -718,9 +718,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
inThread:thread
|
||||
messageType:TSInfoMessageTypeSessionDidEnd] saveWithTransaction:transaction];
|
||||
|
||||
dispatch_async([OWSDispatch sessionStoreQueue], ^{
|
||||
[self.storageManager deleteAllSessionsForContact:envelope.source];
|
||||
});
|
||||
[self.storageManager deleteAllSessionsForContact:envelope.source protocolContext:transaction];
|
||||
}
|
||||
|
||||
- (void)handleExpirationTimerUpdateMessageWithEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
|
||||
|
|
|
@ -1107,7 +1107,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
if (extraDevices && extraDevices.count > 0) {
|
||||
DDLogInfo(@"%@ removing extra devices: %@", self.logTag, extraDevices);
|
||||
for (NSNumber *extraDeviceId in extraDevices) {
|
||||
[self.storageManager deleteSessionForContact:recipient.uniqueId deviceId:extraDeviceId.intValue];
|
||||
[self.storageManager deleteSessionForContact:recipient.uniqueId
|
||||
deviceId:extraDeviceId.intValue
|
||||
protocolContext:protocolContext];
|
||||
}
|
||||
|
||||
[recipient removeDevices:[NSSet setWithArray:extraDevices]];
|
||||
|
@ -1292,7 +1294,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
OWSAssert(deviceNumber);
|
||||
OWSAssert(storage);
|
||||
|
||||
if (![storage containsSession:identifier deviceId:[deviceNumber intValue]]) {
|
||||
if (![storage containsSession:identifier deviceId:[deviceNumber intValue] protocolContext:protocolContext]) {
|
||||
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block PreKeyBundle *_Nullable bundle;
|
||||
__block NSException *_Nullable exception;
|
||||
|
@ -1427,7 +1429,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
dispatch_async([OWSDispatch sessionStoreQueue], ^{
|
||||
for (NSUInteger i = 0; i < [devices count]; i++) {
|
||||
int deviceNumber = [devices[i] intValue];
|
||||
[[TSStorageManager sharedManager] deleteSessionForContact:identifier deviceId:deviceNumber];
|
||||
[[TSStorageManager sharedManager] deleteSessionForContact:identifier
|
||||
deviceId:deviceNumber
|
||||
protocolContext:protocolContext];
|
||||
}
|
||||
completionHandler();
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#import "TSStorageManager.h"
|
||||
#import <AxolotlKit/SessionStore.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TSStorageManager (SessionStore) <SessionStore>
|
||||
|
||||
- (void)archiveAllSessionsForContact:(NSString *)contactIdentifier;
|
||||
|
@ -19,3 +21,5 @@
|
|||
- (void)printAllSessions;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -8,18 +8,11 @@
|
|||
#import <AxolotlKit/SessionRecord.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const TSStorageManagerSessionStoreCollection = @"TSStorageManagerSessionStoreCollection";
|
||||
NSString *const kSessionStoreDBConnectionKey = @"kSessionStoreDBConnectionKey";
|
||||
|
||||
void AssertIsOnSessionStoreQueue()
|
||||
{
|
||||
#ifdef DEBUG
|
||||
if (@available(iOS 10.0, *)) {
|
||||
dispatch_assert_queue([OWSDispatch sessionStoreQueue]);
|
||||
} // else, skip assert as it's a development convenience.
|
||||
#endif
|
||||
}
|
||||
|
||||
@implementation TSStorageManager (SessionStore)
|
||||
|
||||
/**
|
||||
|
@ -27,36 +20,39 @@ void AssertIsOnSessionStoreQueue()
|
|||
* Note that it's still technically possible to access this collection from a different collection,
|
||||
* but that should be considered a bug.
|
||||
*/
|
||||
+ (YapDatabaseConnection *)sessionDBConnection
|
||||
+ (YapDatabaseConnection *)protocolStoreDBConnection
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static YapDatabaseConnection *sessionDBConnection;
|
||||
static YapDatabaseConnection *protocolStoreDBConnection;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sessionDBConnection = [TSStorageManager sharedManager].newDatabaseConnection;
|
||||
sessionDBConnection.objectCacheEnabled = NO;
|
||||
protocolStoreDBConnection = [TSStorageManager sharedManager].newDatabaseConnection;
|
||||
protocolStoreDBConnection.objectCacheEnabled = NO;
|
||||
#if DEBUG
|
||||
sessionDBConnection.permittedTransactions = YDB_AnySyncTransaction;
|
||||
protocolStoreDBConnection.permittedTransactions = YDB_AnySyncTransaction;
|
||||
#endif
|
||||
});
|
||||
|
||||
return sessionDBConnection;
|
||||
return protocolStoreDBConnection;
|
||||
}
|
||||
|
||||
- (YapDatabaseConnection *)sessionDBConnection
|
||||
// TODO: Audit usage of this connection.
|
||||
- (YapDatabaseConnection *)protocolStoreDBConnection
|
||||
{
|
||||
return [[self class] sessionDBConnection];
|
||||
return [[self class] protocolStoreDBConnection];
|
||||
}
|
||||
|
||||
#pragma mark - SessionStore
|
||||
|
||||
- (SessionRecord *)loadSession:(NSString *)contactIdentifier deviceId:(int)deviceId
|
||||
- (SessionRecord *)loadSession:(NSString *)contactIdentifier deviceId:(int)deviceId protocolContext:(id)protocolContext
|
||||
{
|
||||
AssertIsOnSessionStoreQueue();
|
||||
OWSAssert(contactIdentifier.length > 0);
|
||||
OWSAssert(deviceId >= 0);
|
||||
OWSAssert([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
|
||||
__block NSDictionary *dictionary;
|
||||
[self.sessionDBConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
dictionary = [transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
NSDictionary *_Nullable dictionary =
|
||||
[transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
|
||||
SessionRecord *record;
|
||||
|
||||
|
@ -71,24 +67,33 @@ void AssertIsOnSessionStoreQueue()
|
|||
return record;
|
||||
}
|
||||
|
||||
- (NSArray *)subDevicesSessions:(NSString *)contactIdentifier
|
||||
- (NSArray *)subDevicesSessions:(NSString *)contactIdentifier protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
OWSAssert(contactIdentifier.length > 0);
|
||||
OWSAssert([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
|
||||
// Deprecated. We aren't currently using this anywhere, but it's "required" by the SessionStore protocol.
|
||||
// If we are going to start using it I'd want to re-verify it works as intended.
|
||||
OWSFail(@"%@ subDevicesSessions is deprecated", self.logTag);
|
||||
AssertIsOnSessionStoreQueue();
|
||||
|
||||
__block NSDictionary *dictionary;
|
||||
[self.sessionDBConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
dictionary = [transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
NSDictionary *_Nullable dictionary =
|
||||
[transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
|
||||
return dictionary ? dictionary.allKeys : @[];
|
||||
}
|
||||
|
||||
- (void)storeSession:(NSString *)contactIdentifier deviceId:(int)deviceId session:(SessionRecord *)session
|
||||
- (void)storeSession:(NSString *)contactIdentifier
|
||||
deviceId:(int)deviceId
|
||||
session:(SessionRecord *)session
|
||||
protocolContext:protocolContext
|
||||
{
|
||||
AssertIsOnSessionStoreQueue();
|
||||
OWSAssert(contactIdentifier.length > 0);
|
||||
OWSAssert(deviceId >= 0);
|
||||
OWSAssert([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
// We need to ensure subsequent usage of this SessionRecord does not consider this session as "fresh". Normally this
|
||||
// is achieved by marking things as "not fresh" at the point of deserialization - when we fetch a SessionRecord from
|
||||
|
@ -98,68 +103,65 @@ void AssertIsOnSessionStoreQueue()
|
|||
// NOTE: this may no longer be necessary now that we have a non-caching session db connection.
|
||||
[session markAsUnFresh];
|
||||
|
||||
__block NSDictionary *immutableDictionary;
|
||||
[self.sessionDBConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
immutableDictionary =
|
||||
[transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
NSDictionary *immutableDictionary =
|
||||
[transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
|
||||
NSMutableDictionary *dictionary = [immutableDictionary mutableCopy];
|
||||
|
||||
if (!dictionary) {
|
||||
dictionary = [NSMutableDictionary dictionary];
|
||||
}
|
||||
NSMutableDictionary *dictionary
|
||||
= (immutableDictionary ? [immutableDictionary mutableCopy] : [NSMutableDictionary new]);
|
||||
|
||||
[dictionary setObject:session forKey:@(deviceId)];
|
||||
|
||||
[self.sessionDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[transaction setObject:[dictionary copy]
|
||||
forKey:contactIdentifier
|
||||
inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
[transaction setObject:[dictionary copy]
|
||||
forKey:contactIdentifier
|
||||
inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}
|
||||
|
||||
- (BOOL)containsSession:(NSString *)contactIdentifier deviceId:(int)deviceId
|
||||
- (BOOL)containsSession:(NSString *)contactIdentifier deviceId:(int)deviceId protocolContext:(id)protocolContext
|
||||
{
|
||||
AssertIsOnSessionStoreQueue();
|
||||
OWSAssert(contactIdentifier.length > 0);
|
||||
OWSAssert(deviceId >= 0);
|
||||
OWSAssert([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
|
||||
return [self loadSession:contactIdentifier deviceId:deviceId].sessionState.hasSenderChain;
|
||||
return [self loadSession:contactIdentifier deviceId:deviceId protocolContext:protocolContext]
|
||||
.sessionState.hasSenderChain;
|
||||
}
|
||||
|
||||
- (void)deleteSessionForContact:(NSString *)contactIdentifier deviceId:(int)deviceId
|
||||
- (void)deleteSessionForContact:(NSString *)contactIdentifier
|
||||
deviceId:(int)deviceId
|
||||
protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
AssertIsOnSessionStoreQueue();
|
||||
OWSAssert(contactIdentifier.length > 0);
|
||||
OWSAssert(deviceId >= 0);
|
||||
OWSAssert([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
DDLogInfo(
|
||||
@"[TSStorageManager (SessionStore)] deleting session for contact: %@ device: %d", contactIdentifier, deviceId);
|
||||
|
||||
__block NSDictionary *immutableDictionary;
|
||||
[self.sessionDBConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
immutableDictionary =
|
||||
[transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
NSMutableDictionary *dictionary = [immutableDictionary mutableCopy];
|
||||
NSDictionary *immutableDictionary =
|
||||
[transaction objectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
|
||||
if (!dictionary) {
|
||||
dictionary = [NSMutableDictionary dictionary];
|
||||
}
|
||||
NSMutableDictionary *dictionary
|
||||
= (immutableDictionary ? [immutableDictionary mutableCopy] : [NSMutableDictionary new]);
|
||||
|
||||
[dictionary removeObjectForKey:@(deviceId)];
|
||||
|
||||
[self.sessionDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[transaction setObject:[dictionary copy]
|
||||
forKey:contactIdentifier
|
||||
inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
[transaction setObject:[dictionary copy]
|
||||
forKey:contactIdentifier
|
||||
inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}
|
||||
|
||||
- (void)deleteAllSessionsForContact:(NSString *)contactIdentifier
|
||||
- (void)deleteAllSessionsForContact:(NSString *)contactIdentifier protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
AssertIsOnSessionStoreQueue();
|
||||
OWSAssert(contactIdentifier.length > 0);
|
||||
OWSAssert([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
DDLogInfo(@"[TSStorageManager (SessionStore)] deleting all sessions for contact:%@", contactIdentifier);
|
||||
|
||||
[self.sessionDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[transaction removeObjectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}];
|
||||
[transaction removeObjectForKey:contactIdentifier inCollection:TSStorageManagerSessionStoreCollection];
|
||||
}
|
||||
|
||||
- (void)archiveAllSessionsForContact:(NSString *)contactIdentifier
|
||||
|
@ -272,3 +274,5 @@ void AssertIsOnSessionStoreQueue()
|
|||
#endif
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -11,12 +11,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
*/
|
||||
+ (dispatch_queue_t)attachmentsQueue;
|
||||
|
||||
/**
|
||||
* Signal protocol session state must be coordinated on a serial queue. This is sometimes used synchronously,
|
||||
* so never dispatching sync *from* this queue to avoid deadlock.
|
||||
*/
|
||||
+ (dispatch_queue_t)sessionStoreQueue;
|
||||
|
||||
/**
|
||||
* Serial message sending queue
|
||||
*/
|
||||
|
|
|
@ -18,16 +18,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return queue;
|
||||
}
|
||||
|
||||
+ (dispatch_queue_t)sessionStoreQueue
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static dispatch_queue_t queue;
|
||||
dispatch_once(&onceToken, ^{
|
||||
queue = dispatch_queue_create("org.whispersystems.signal.sessionStoreQueue", NULL);
|
||||
});
|
||||
return queue;
|
||||
}
|
||||
|
||||
+ (dispatch_queue_t)sendingQueue
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
|
|
Loading…
Reference in New Issue