#import #include #import #import @interface WMFCrossProcessCoreDataSynchronizer () { dispatch_semaphore_t _semaphore; int _token; } @property (nonatomic, copy) NSString *identifier; @property (nonatomic, copy) NSURL *containerURL; @end static uint64_t bundleHash() { static dispatch_once_t onceToken; static uint64_t bundleHash; dispatch_once(&onceToken, ^{ bundleHash = (uint64_t)[[[NSBundle mainBundle] bundleIdentifier] hash]; }); return bundleHash; } @implementation WMFCrossProcessCoreDataSynchronizer - (instancetype)initWithIdentifier:(NSString *)identifier storageDirectory:(NSURL *)directoryURL { self = [super init]; if (self) { _semaphore = dispatch_semaphore_create(1); self.containerURL = directoryURL; self.identifier = identifier; } return self; } - (void)dealloc { [self stop]; } - (void)startSynchronizingContexts:(NSArray *)contexts { if (!self.identifier) { DDLogError(@"missing channel name"); return; } const char *name = [self.identifier UTF8String]; for (NSManagedObjectContext *context in contexts) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextDidSave:) name:NSManagedObjectContextDidSaveNotification object:context]; } @weakify(self) notify_register_dispatch(name, &_token, dispatch_get_main_queue(), ^(int token) { @strongify(self) uint64_t state; notify_get_state(token, &state); BOOL isExternal = state != bundleHash(); if (isExternal) { [self readCrossProcessCoreDataNotificationWithState:state intoContexts:contexts]; } }); } - (void)stop { if (_token != 0) { notify_cancel(_token); } [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Writing Changes from this Process - (void)contextDidSave:(NSNotification *)note { [self writeCrossProcessCoreDataNotification:note]; } - (void)writeCrossProcessCoreDataNotification:(NSNotification *)note { dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); NSDictionary *userInfo = note.userInfo; if (!userInfo) { return; } uint64_t state = bundleHash(); NSDictionary *archiveableUserInfo = [self archivableNotificationUserInfoForUserInfo:userInfo]; NSError *archiveError = nil; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:archiveableUserInfo requiringSecureCoding:NO error:&archiveError]; if (archiveError) { DDLogError(@"Error archiving cross process changes: %@", archiveError); return; } NSURL *fileURL = [self archivedChangesFileURLWithState:state]; [data writeToURL:fileURL atomically:YES]; const char *name = [self.identifier UTF8String]; notify_set_state(_token, state); notify_post(name); dispatch_semaphore_signal(_semaphore); } #pragma mark - Reading changes from other processes - (void)readCrossProcessCoreDataNotificationWithState:(uint64_t)state intoContexts:(NSArray *)contexts { NSURL *fileURL = [self archivedChangesFileURLWithState:state]; NSError *unarchiveError = nil; NSDictionary *userInfo = [self unarchivedDictionaryFromFileURL:fileURL error:&unarchiveError]; if (unarchiveError) { DDLogError(@"Error unarchiving cross process core data notification: %@", unarchiveError); return; } [NSManagedObjectContext mergeChangesFromRemoteContextSave:userInfo intoContexts:contexts]; } #pragma mark - Notification Archive Utitlities - (NSDictionary *)unarchivedDictionaryFromFileURL:(NSURL *)fileURL error:(NSError **)error { NSData *data = [NSData dataWithContentsOfURL:fileURL]; NSSet *allowedClasses = [NSSet setWithArray:[NSSecureUnarchiveFromDataTransformer allowedTopLevelClasses]]; return [NSKeyedUnarchiver unarchivedObjectOfClasses:allowedClasses fromData:data error:error]; } - (NSURL *)archivedChangesFileURLWithState:(uint64_t)state { NSString *fileName = [NSString stringWithFormat:@"%llu.%@.changes", state, self.identifier]; return [self.containerURL URLByAppendingPathComponent:fileName isDirectory:NO]; } - (nullable id)archiveableNotificationValueForValue:(id)value { if ([value isKindOfClass:[NSManagedObject class]]) { return [[value objectID] URIRepresentation]; } else if ([value isKindOfClass:[NSManagedObjectID class]]) { return [value URIRepresentation]; } else if ([value isKindOfClass:[NSSet class]] || [value isKindOfClass:[NSArray class]]) { return [value wmf_map:^id(id obj) { return [self archiveableNotificationValueForValue:obj]; }]; } else if ([value conformsToProtocol:@protocol(NSCoding)]) { return value; } else { return nil; } } - (NSDictionary *)archivableNotificationUserInfoForUserInfo:(NSDictionary *)userInfo { NSMutableDictionary *archiveableUserInfo = [NSMutableDictionary dictionaryWithCapacity:userInfo.count]; NSArray *allKeys = userInfo.allKeys; for (NSString *key in allKeys) { id value = userInfo[key]; if ([value isKindOfClass:[NSDictionary class]]) { value = [self archivableNotificationUserInfoForUserInfo:value]; } else { value = [self archiveableNotificationValueForValue:value]; } if (value) { archiveableUserInfo[key] = value; } } return archiveableUserInfo; } @end