/* HBDistributedArray.m $ This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "HBDistributedArray.h" #import "HBUtilities.h" #include /** * HBProxyArrayObject wraps an object inside a proxy * to make it possible to keep a reference to an array * object even if the underlying has been swapped */ @interface HBProxyArrayObject : NSProxy - (instancetype)initWithObject:(id)object; @property (nonatomic, strong) id representedObject; @property (unsafe_unretained, nonatomic, readonly) NSString *uuid; @end @implementation HBProxyArrayObject - (instancetype)initWithObject:(id)object { _representedObject = object; return self; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [self.representedObject methodSignatureForSelector:selector]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.representedObject]; } - (NSString *)uuid { return [self.representedObject uuid]; } @end NSString *HBDistributedArrayChanged = @"HBDistributedArrayChanged"; NSString *HBDistributedArraWrittenToDisk = @"HBDistributedArraWrittenToDisk"; @interface HBDistributedArray () @property (nonatomic, readonly) NSMutableArray *array; @property (nonatomic, readonly) NSURL *fileURL; @property (nonatomic, readwrite) NSTimeInterval modifiedTime; @property (nonatomic, readonly) NSSet *objectClasses; @property (nonatomic, readonly) BOOL requiresSecureCoding; @property (nonatomic, readonly) sem_t *mutex; @property (nonatomic, readwrite) uint32_t mutexCount; @end @implementation HBDistributedArray - (instancetype)initWithURL:(NSURL *)fileURL class:(Class)objectClass { self = [super init]; if (self) { _fileURL = [fileURL copy]; _array = [[NSMutableArray alloc] init]; _objectClasses = [NSSet setWithObjects:[NSMutableArray class], objectClass, nil]; // Enable secure coding only on 10.9 and later if ([NSURL instancesRespondToSelector:@selector(fileSystemRepresentation)]) { _requiresSecureCoding = YES; } NSString *identifier = [[NSBundle mainBundle] bundleIdentifier]; NSArray *runningInstances = [NSRunningApplication runningApplicationsWithBundleIdentifier:identifier]; const char *name = [NSString stringWithFormat:@"%@/%@", identifier, _fileURL.lastPathComponent.stringByDeletingPathExtension].UTF8String; // Unlink the semaphore if we are the only // instance running, this fixes the case where // HB crashed while the sem is locked. if (runningInstances.count == 1) { sem_unlink(name); } // Use a named semaphore as a mutex for now // it can cause a deadlock if an instance // crashed while it has the lock on the semaphore. _mutex = sem_open(name, O_CREAT, 0777, 1); if (_mutex == SEM_FAILED) { [HBUtilities writeToActivityLog:"%s: %d", "Error in creating semaphore: ", errno]; } [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:HBDistributedArraWrittenToDisk object:nil]; if ([[NSFileManager defaultManager] fileExistsAtPath:_fileURL.path]) { // Load the array from disk [self lock]; [self reload]; [self unlock]; } } return self; } - (void)dealloc { [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; [self lock]; [self synchronize]; [self unlock]; sem_close(_mutex); } - (void)lock { if (self.mutexCount == 0) { sem_wait(self.mutex); } self.mutexCount++; } - (void)unlock { if (self.mutexCount == 1) { sem_post(self.mutex); } self.mutexCount--; } - (HBDistributedArrayContent)beginTransaction { [self lock]; // We got the lock, need to check if // someone else modified the file // while we were locked, because we // could have not received the notification yet NSDate *date = nil; [self.fileURL getResourceValue:&date forKey:NSURLAttributeModificationDateKey error:nil]; if (date.timeIntervalSinceReferenceDate > ceil(self.modifiedTime)) { // File was modified while we waited on the lock // reload it [self reload]; return HBDistributedArrayContentReload; } return HBDistributedArrayContentAcquired; } - (void)commit { // Save changes to disk // and unlock [self synchronize]; [self unlock]; } - (void)postNotification { [[NSNotificationCenter defaultCenter] postNotificationName:HBDistributedArrayChanged object:self]; } /** * Handle the distributed notification */ - (void)handleNotification:(NSNotification *)notification { if (!([notification.object integerValue] == getpid())) { [self lock]; [self reload]; [self unlock]; } } /** * Reload the array from disk */ - (void)reload { NSMutableArray *jobsArray = nil; @try { if (self.requiresSecureCoding) { NSData *queue = [NSData dataWithContentsOfURL:self.fileURL]; NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:queue]; unarchiver.requiresSecureCoding = YES; jobsArray = [unarchiver decodeObjectOfClasses:self.objectClasses forKey:NSKeyedArchiveRootObjectKey]; [unarchiver finishDecoding]; } else { jobsArray = [NSKeyedUnarchiver unarchiveObjectWithFile:self.fileURL.path]; } } @catch (NSException *exception) { jobsArray = nil; } // Swap the proxy objects representation with the new // one read from disk NSMutableArray *proxyArray = [NSMutableArray array]; for (id anObject in jobsArray) { NSString *uuid = [anObject uuid]; HBProxyArrayObject *proxy = nil; for (HBProxyArrayObject *temp in self.array) { if ([[temp uuid] isEqualToString:uuid]) { temp.representedObject = anObject; proxy = temp; break; } } if (proxy) { [proxyArray addObject:proxy]; } else { [proxyArray addObject:[self wrapObjectIfNeeded:anObject]]; } } [self setArray:proxyArray]; [self postNotification]; // Update the time, so we can avoid reloaded the file from disk later. self.modifiedTime = [NSDate timeIntervalSinceReferenceDate]; } /** * Writes the changes to disk */ - (void)synchronize { NSMutableArray *temp = [NSMutableArray array]; // Unwrap the array objects and save them to disk for (HBProxyArrayObject *proxy in self) { [temp addObject:proxy.representedObject]; } if (![NSKeyedArchiver archiveRootObject:temp toFile:self.fileURL.path]) { [HBUtilities writeToActivityLog:"Failed to write the queue to disk"]; } // Send a distributed notification. [[NSDistributedNotificationCenter defaultCenter] postNotificationName:HBDistributedArraWrittenToDisk object:[NSString stringWithFormat:@"%d", getpid()] userInfo:nil deliverImmediately:YES]; // Update the time, so we can avoid reloaded the file from disk later. self.modifiedTime = [NSDate timeIntervalSinceReferenceDate]; } /** * Wraps an object inside a HBObjectProxy instance * if it's not already wrapped. * * @param anObject the object to wrap * * @return a wrapped object */ - (id)wrapObjectIfNeeded:(id)anObject { if ([[anObject class] isEqual:[HBProxyArrayObject class]]) { return anObject; } else { return [[HBProxyArrayObject alloc] initWithObject:anObject]; } } #pragma mark - Methods needed to subclass NSMutableArray - (void)insertObject:(id)anObject atIndex:(NSUInteger)index { [self.array insertObject:[self wrapObjectIfNeeded:anObject] atIndex:index]; } - (void)removeObjectAtIndex:(NSUInteger)index { [self.array removeObjectAtIndex:index]; } - (void)addObject:(id)anObject { [self.array addObject:[self wrapObjectIfNeeded:anObject]]; } - (void)removeLastObject { [self.array removeLastObject]; } - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject { (self.array)[index] = [self wrapObjectIfNeeded:anObject]; } - (NSUInteger)count { return [self.array count]; } - (id)objectAtIndex:(NSUInteger)index { return (self.array)[index]; } @end