Better locking behavior for playlist storage

This should fix up potential locking issues with maintaining a copy of
the results set while certain other background actions may happen, such
as the player updating play counts while playing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
main
Christopher Snowhill 2022-10-11 22:58:36 -07:00
parent e54d05a907
commit c9181f571f
9 changed files with 74 additions and 23 deletions

View File

@ -225,6 +225,9 @@ static AppController *kAppController = nil;
NSError *error = nil; NSError *error = nil;
[playlistController.persistentContainerLock lock]; [playlistController.persistentContainerLock lock];
NSArray *results = [playlistController.persistentContainer.viewContext executeFetchRequest:request error:&error]; NSArray *results = [playlistController.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[playlistController.persistentContainerLock unlock]; [playlistController.persistentContainerLock unlock];
if(results && [results count] == 1) { if(results && [results count] == 1) {

View File

@ -267,18 +267,20 @@ static void *playlistControllerContext = &playlistControllerContext;
PlayCount *pc = pe.playCountItem; PlayCount *pc = pe.playCountItem;
if(pc) { if(pc) {
[self.persistentContainerLock lock];
pc.count += 1; pc.count += 1;
pc.lastPlayed = [NSDate date]; pc.lastPlayed = [NSDate date];
[self.persistentContainerLock unlock];
} else { } else {
[self.persistentContainerLock lock]; [self.persistentContainerLock lock];
pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext]; pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext];
[self.persistentContainerLock unlock];
pc.count = 1; pc.count = 1;
pc.firstSeen = pc.lastPlayed = [NSDate date]; pc.firstSeen = pc.lastPlayed = [NSDate date];
pc.album = pe.album; pc.album = pe.album;
pc.artist = pe.artist; pc.artist = pe.artist;
pc.title = pe.title; pc.title = pe.title;
pc.filename = pe.filenameFragment; pc.filename = pe.filenameFragment;
[self.persistentContainerLock unlock];
} }
[self commitPersistentStore]; [self commitPersistentStore];
@ -290,13 +292,13 @@ static void *playlistControllerContext = &playlistControllerContext;
if(!pc) { if(!pc) {
[self.persistentContainerLock lock]; [self.persistentContainerLock lock];
pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext]; pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext];
[self.persistentContainerLock unlock];
pc.count = 0; pc.count = 0;
pc.firstSeen = [NSDate date]; pc.firstSeen = [NSDate date];
pc.album = pe.album; pc.album = pe.album;
pc.artist = pe.artist; pc.artist = pe.artist;
pc.title = pe.title; pc.title = pe.title;
pc.filename = pe.filenameFragment; pc.filename = pe.filenameFragment;
[self.persistentContainerLock unlock];
} }
} }
@ -307,16 +309,18 @@ static void *playlistControllerContext = &playlistControllerContext;
if(!pc) { if(!pc) {
[self.persistentContainerLock lock]; [self.persistentContainerLock lock];
pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext]; pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext];
[self.persistentContainerLock unlock];
pc.count = 0; pc.count = 0;
pc.firstSeen = [NSDate date]; pc.firstSeen = [NSDate date];
pc.album = pe.album; pc.album = pe.album;
pc.artist = pe.artist; pc.artist = pe.artist;
pc.title = pe.title; pc.title = pe.title;
pc.filename = pe.filenameFragment; pc.filename = pe.filenameFragment;
[self.persistentContainerLock unlock];
} }
[self.persistentContainerLock lock];
pc.rating = rating; pc.rating = rating;
[self.persistentContainerLock unlock];
[self commitPersistentStore]; [self commitPersistentStore];
} }
@ -1380,6 +1384,9 @@ static void *playlistControllerContext = &playlistControllerContext;
NSError *error = nil; NSError *error = nil;
[self.persistentContainerLock lock]; [self.persistentContainerLock lock];
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error]; NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[self.persistentContainerLock unlock]; [self.persistentContainerLock unlock];
if(results && [results count] > 0) { if(results && [results count] > 0) {
@ -1401,6 +1408,9 @@ static void *playlistControllerContext = &playlistControllerContext;
NSError *error = nil; NSError *error = nil;
[self.persistentContainerLock lock]; [self.persistentContainerLock lock];
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error]; NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[self.persistentContainerLock unlock]; [self.persistentContainerLock unlock];
if(results && [results count] > 0) { if(results && [results count] > 0) {
@ -1937,15 +1947,18 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
} }
- (BOOL)pathSuggesterEmpty { - (BOOL)pathSuggesterEmpty {
BOOL rval;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"]; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
NSError *error = nil; NSError *error = nil;
[self.persistentContainerLock lock]; [self.persistentContainerLock lock];
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error]; NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(!results || [results count] < 1) rval = YES;
else rval = NO;
[self.persistentContainerLock unlock]; [self.persistentContainerLock unlock];
if(!results || [results count] < 1) return YES; return rval;
else return NO;
} }
@end @end

View File

@ -366,9 +366,9 @@ extern NSMutableDictionary<NSString *, AlbumArtwork *> *kArtworkDictionary;
if(![kArtworkDictionary objectForKey:imageCacheTag]) { if(![kArtworkDictionary objectForKey:imageCacheTag]) {
[kPersistentContainerLock lock]; [kPersistentContainerLock lock];
AlbumArtwork *art = [NSEntityDescription insertNewObjectForEntityForName:@"AlbumArtwork" inManagedObjectContext:kPersistentContainer.viewContext]; AlbumArtwork *art = [NSEntityDescription insertNewObjectForEntityForName:@"AlbumArtwork" inManagedObjectContext:kPersistentContainer.viewContext];
[kPersistentContainerLock unlock];
art.artHash = imageCacheTag; art.artHash = imageCacheTag;
art.artData = albumArtInternal; art.artData = albumArtInternal;
[kPersistentContainerLock unlock];
[kArtworkDictionary setObject:art forKey:imageCacheTag]; [kArtworkDictionary setObject:art forKey:imageCacheTag];
} }
@ -591,7 +591,6 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) {
NSError *error = nil; NSError *error = nil;
[kPersistentContainerLock lock]; [kPersistentContainerLock lock];
NSArray *results = [kPersistentContainer.viewContext executeFetchRequest:request error:&error]; NSArray *results = [kPersistentContainer.viewContext executeFetchRequest:request error:&error];
[kPersistentContainerLock unlock];
if(!results || [results count] < 1) { if(!results || [results count] < 1) {
NSPredicate *filenamePredicate = [NSPredicate predicateWithFormat:@"filename == %@", self.filenameFragment]; NSPredicate *filenamePredicate = [NSPredicate predicateWithFormat:@"filename == %@", self.filenameFragment];
@ -599,10 +598,13 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) {
request = [NSFetchRequest fetchRequestWithEntityName:@"PlayCount"]; request = [NSFetchRequest fetchRequestWithEntityName:@"PlayCount"];
request.predicate = filenamePredicate; request.predicate = filenamePredicate;
[kPersistentContainerLock lock];
results = [kPersistentContainer.viewContext executeFetchRequest:request error:&error]; results = [kPersistentContainer.viewContext executeFetchRequest:request error:&error];
[kPersistentContainerLock unlock];
} }
if(results) {
results = [results copy];
}
[kPersistentContainerLock unlock];
if(!results || [results count] < 1) return nil; if(!results || [results count] < 1) return nil;

View File

@ -569,11 +569,11 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
[self->playlistController.persistentContainerLock lock]; [self->playlistController.persistentContainerLock lock];
pe = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:self->playlistController.persistentContainer.viewContext]; pe = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:self->playlistController.persistentContainer.viewContext];
[self->playlistController.persistentContainerLock unlock];
pe.url = url; pe.url = url;
pe.index = index + i; pe.index = index + i;
pe.rawTitle = [[url path] lastPathComponent]; pe.rawTitle = [[url path] lastPathComponent];
pe.queuePosition = -1; pe.queuePosition = -1;
[self->playlistController.persistentContainerLock unlock];
}); });
[entries addObject:pe]; [entries addObject:pe];
@ -593,10 +593,10 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
[self->playlistController.persistentContainerLock lock]; [self->playlistController.persistentContainerLock lock];
pe = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:self->playlistController.persistentContainer.viewContext]; pe = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:self->playlistController.persistentContainer.viewContext];
[self->playlistController.persistentContainerLock unlock];
[pe setValuesForKeysWithDictionary:entry]; [pe setValuesForKeysWithDictionary:entry];
pe.index = index + i; pe.index = index + i;
pe.queuePosition = -1; pe.queuePosition = -1;
[self->playlistController.persistentContainerLock unlock];
}); });
[entries addObject:pe]; [entries addObject:pe];
@ -797,6 +797,9 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
NSError *error; NSError *error;
[playlistController.persistentContainerLock lock]; [playlistController.persistentContainerLock lock];
NSArray *results = [moc executeFetchRequest:request error:&error]; NSArray *results = [moc executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[playlistController.persistentContainerLock unlock]; [playlistController.persistentContainerLock unlock];
if(results && [results count] > 0) { if(results && [results count] > 0) {
@ -926,8 +929,8 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
NSError *error = nil; NSError *error = nil;
[playlistController.persistentContainerLock lock]; [playlistController.persistentContainerLock lock];
NSArray *results = [moc executeFetchRequest:request error:&error]; NSArray *results = [moc executeFetchRequest:request error:&error];
[playlistController.persistentContainerLock unlock];
if(!results) { if(!results) {
[playlistController.persistentContainerLock unlock];
ALog(@"Error fetching AlbumArtwork objects: %@\n%@", [error localizedDescription], [error userInfo]); ALog(@"Error fetching AlbumArtwork objects: %@\n%@", [error localizedDescription], [error userInfo]);
abort(); abort();
} }
@ -935,6 +938,7 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
for(AlbumArtwork *art in results) { for(AlbumArtwork *art in results) {
[kArtworkDictionary setObject:art forKey:art.artHash]; [kArtworkDictionary setObject:art forKey:art.artHash];
} }
[playlistController.persistentContainerLock unlock];
request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"]; request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"];
@ -944,15 +948,20 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
[playlistController.persistentContainerLock lock]; [playlistController.persistentContainerLock lock];
results = [moc executeFetchRequest:request error:&error]; results = [moc executeFetchRequest:request error:&error];
[playlistController.persistentContainerLock unlock];
if(!results) { if(!results) {
[playlistController.persistentContainerLock unlock];
ALog(@"Error fetching PlaylistEntry objects: %@\n%@", [error localizedDescription], [error userInfo]); ALog(@"Error fetching PlaylistEntry objects: %@\n%@", [error localizedDescription], [error userInfo]);
abort(); abort();
} }
if([results count] == 0) return NO; if([results count] == 0) {
[playlistController.persistentContainerLock unlock];
return NO;
}
NSMutableArray *resultsCopy = [results mutableCopy]; NSMutableArray *resultsCopy = [results mutableCopy];
[playlistController.persistentContainerLock unlock];
NSMutableIndexSet *pruneSet = [[NSMutableIndexSet alloc] init]; NSMutableIndexSet *pruneSet = [[NSMutableIndexSet alloc] init];
NSUInteger index = 0; NSUInteger index = 0;
for(PlaylistEntry *pe in resultsCopy) { for(PlaylistEntry *pe in resultsCopy) {

View File

@ -69,6 +69,9 @@
NSError *error = nil; NSError *error = nil;
[lock lock]; [lock lock];
NSArray *results = [pc.viewContext executeFetchRequest:request error:&error]; NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[lock unlock]; [lock unlock];
if(!results || [results count] < 1) return; if(!results || [results count] < 1) return;

View File

@ -44,6 +44,9 @@
NSError *error = nil; NSError *error = nil;
[lock lock]; [lock lock];
NSArray *results = [pc.viewContext executeFetchRequest:request error:&error]; NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[lock unlock]; [lock unlock];
if(results && [results count] > 0) { if(results && [results count] > 0) {
@ -69,12 +72,10 @@
[lock lock]; [lock lock];
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext]; SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
[lock unlock];
if(token) { if(token) {
token.path = [url path]; token.path = [url path];
token.bookmark = bookmark; token.bookmark = bookmark;
[lock lock];
[pc.viewContext save:&err]; [pc.viewContext save:&err];
[lock unlock]; [lock unlock];
if(err) { if(err) {
@ -84,6 +85,8 @@
[NSClassFromString(@"SandboxBroker") cleanupFolderAccess]; [NSClassFromString(@"SandboxBroker") cleanupFolderAccess];
[self refresh]; [self refresh];
} }
} else {
[lock unlock];
} }
} }

View File

@ -40,7 +40,6 @@ extern NSPersistentContainer *kPersistentContainer;
+ (PlaylistEntry *)playlistEntryWithMetadataItem:(NSMetadataItem *)metadataItem { + (PlaylistEntry *)playlistEntryWithMetadataItem:(NSMetadataItem *)metadataItem {
[kPersistentContainerLock lock]; [kPersistentContainerLock lock];
PlaylistEntry *entry = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:kPersistentContainer.viewContext]; PlaylistEntry *entry = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:kPersistentContainer.viewContext];
[kPersistentContainerLock unlock];
entry.deLeted = YES; entry.deLeted = YES;
@ -85,6 +84,7 @@ extern NSPersistentContainer *kPersistentContainer;
entry.url = url; entry.url = url;
[entry setMetadata:dict]; [entry setMetadata:dict];
[kPersistentContainerLock unlock];
return entry; return entry;
} }

View File

@ -1446,12 +1446,12 @@ static SQLiteStore *g_sharedStore = nil;
- (PlaylistEntry *_Nonnull)getTrack:(int64_t)trackId { - (PlaylistEntry *_Nonnull)getTrack:(int64_t)trackId {
[kPersistentContainerLock lock]; [kPersistentContainerLock lock];
PlaylistEntry *entry = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:kPersistentContainer.viewContext]; PlaylistEntry *entry = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:kPersistentContainer.viewContext];
[kPersistentContainerLock unlock];
if(trackId < 0) { if(trackId < 0) {
entry.error = YES; entry.error = YES;
entry.errorMessage = NSLocalizedString(@"ErrorInvalidTrackId", @""); entry.errorMessage = NSLocalizedString(@"ErrorInvalidTrackId", @"");
entry.deLeted = YES; entry.deLeted = YES;
[kPersistentContainerLock unlock];
return entry; return entry;
} }
@ -1462,6 +1462,7 @@ static SQLiteStore *g_sharedStore = nil;
entry.error = YES; entry.error = YES;
entry.errorMessage = NSLocalizedString(@"ErrorSqliteProblem", @""); entry.errorMessage = NSLocalizedString(@"ErrorSqliteProblem", @"");
entry.deLeted = YES; entry.deLeted = YES;
[kPersistentContainerLock unlock];
return entry; return entry;
} }
@ -1472,6 +1473,7 @@ static SQLiteStore *g_sharedStore = nil;
entry.error = YES; entry.error = YES;
entry.errorMessage = NSLocalizedString(@"ErrorSqliteProblem", @""); entry.errorMessage = NSLocalizedString(@"ErrorSqliteProblem", @"");
entry.deLeted = YES; entry.deLeted = YES;
[kPersistentContainerLock unlock];
return entry; return entry;
} }
@ -1547,6 +1549,7 @@ static SQLiteStore *g_sharedStore = nil;
entry.errorMessage = NSLocalizedString(@"ErrorTrackMissing", @""); entry.errorMessage = NSLocalizedString(@"ErrorTrackMissing", @"");
entry.deLeted = YES; entry.deLeted = YES;
} }
[kPersistentContainerLock unlock];
sqlite3_reset(st); sqlite3_reset(st);
@ -1863,8 +1866,10 @@ static SQLiteStore *g_sharedStore = nil;
int64_t entryId = sqlite3_column_int64(st, select_playlist_out_entry_id); int64_t entryId = sqlite3_column_int64(st, select_playlist_out_entry_id);
entry = [self getTrack:trackId]; entry = [self getTrack:trackId];
if(!entry.deLeted && !entry.error) { if(!entry.deLeted && !entry.error) {
[kPersistentContainerLock lock];
entry.index = index; entry.index = index;
entry.entryId = entryId; entry.entryId = entryId;
[kPersistentContainerLock unlock];
} else { } else {
[kPersistentContainerLock lock]; [kPersistentContainerLock lock];
[kPersistentContainer.viewContext deleteObject:entry]; [kPersistentContainer.viewContext deleteObject:entry];

View File

@ -166,6 +166,9 @@ static SandboxBroker *kSharedSandboxBroker = nil;
NSError *error = nil; NSError *error = nil;
[lock lock]; [lock lock];
NSArray *results = [pc.viewContext executeFetchRequest:request error:&error]; NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[lock unlock]; [lock unlock];
if(results && [results count] > 0) { if(results && [results count] > 0) {
ret = [[SandboxEntry alloc] initWithToken:results[0]]; ret = [[SandboxEntry alloc] initWithToken:results[0]];
@ -183,6 +186,9 @@ static SandboxBroker *kSharedSandboxBroker = nil;
error = nil; error = nil;
[lock lock]; [lock lock];
results = [pc.viewContext executeFetchRequest:request error:&error]; results = [pc.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[lock unlock]; [lock unlock];
if(results && [results count] > 0) { if(results && [results count] > 0) {
@ -253,12 +259,14 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
[lock lock]; [lock lock];
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext]; SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
[lock unlock];
if(token) { if(token) {
token.path = [folderUrl path]; token.path = [folderUrl path];
token.bookmark = bookmark; token.bookmark = bookmark;
[lock unlock];
[SandboxBroker cleanupFolderAccess]; [SandboxBroker cleanupFolderAccess];
} else {
[lock unlock];
} }
}); });
} }
@ -301,13 +309,13 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
[lock lock]; [lock lock];
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext]; SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
[lock unlock];
if(token) { if(token) {
token.path = [url path]; token.path = [url path];
token.bookmark = bookmark; token.bookmark = bookmark;
token.folder = NO; token.folder = NO;
} }
[lock unlock];
}); });
} }
} }
@ -369,12 +377,14 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
[lock lock]; [lock lock];
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext]; SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
[lock unlock];
if(token) { if(token) {
token.path = [folderUrl path]; token.path = [folderUrl path];
token.bookmark = bookmark; token.bookmark = bookmark;
[lock unlock];
[SandboxBroker cleanupFolderAccess]; [SandboxBroker cleanupFolderAccess];
} else {
[lock unlock];
} }
} }
}); });
@ -391,14 +401,17 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
request.sortDescriptors = @[sortDescriptor]; request.sortDescriptors = @[sortDescriptor];
NSError *error = nil; NSError *error = nil;
NSMutableArray *resultsCopy = nil;
[lock lock]; [lock lock];
NSArray *results = [pc.viewContext executeFetchRequest:request error:&error]; NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
if(results) {
resultsCopy = [results mutableCopy];
}
[lock unlock]; [lock unlock];
BOOL isUpdated = NO; BOOL isUpdated = NO;
if(results && [results count]) { if(resultsCopy && [resultsCopy count]) {
NSMutableArray *resultsCopy = [results mutableCopy];
for(NSUInteger i = 0; i < [resultsCopy count] - 1; ++i) { for(NSUInteger i = 0; i < [resultsCopy count] - 1; ++i) {
SandboxToken *token = resultsCopy[i]; SandboxToken *token = resultsCopy[i];
NSURL *url = [NSURL fileURLWithPath:token.path]; NSURL *url = [NSURL fileURLWithPath:token.path];