/* HBQueue.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 "HBQueue.h"
#import "HBRemoteCore.h"
#import "HBQueueWorker.h"
#import "HBPreferencesKeys.h"
#import "NSArray+HBAdditions.h"
#import
static void *HBQueueContext = &HBQueueContext;
NSString * const HBQueueDidChangeStateNotification = @"HBQueueDidChangeStateNotification";
NSString * const HBQueueDidAddItemNotification = @"HBQueueDidAddItemNotification";
NSString * const HBQueueDidRemoveItemNotification = @"HBQueueDidRemoveItemNotification";
NSString * const HBQueueDidChangeItemNotification = @"HBQueueDidChangeItemNotification";
NSString * const HBQueueItemNotificationIndexesKey = @"HBQueueItemNotificationIndexesKey";
NSString * const HBQueueDidMoveItemNotification = @"HBQueueDidMoveItemNotification";
NSString * const HBQueueItemNotificationSourceIndexesKey = @"HBQueueItemNotificationSourceIndexesKey";
NSString * const HBQueueItemNotificationTargetIndexesKey = @"HBQueueItemNotificationTargetIndexesKey";
NSString * const HBQueueLowSpaceAlertNotification = @"HBQueueLowSpaceAlertNotification";
NSString * const HBQueueDidStartNotification = @"HBQueueDidStartNotification";
NSString * const HBQueueDidCompleteNotification = @"HBQueueDidCompleteNotification";
NSString * const HBQueueDidStartItemNotification = @"HBQueueDidStartItemNotification";
NSString * const HBQueueDidCompleteItemNotification = @"HBQueueDidCompleteItemNotification";
NSString * const HBQueueItemNotificationItemKey = @"HBQueueItemNotificationItemKey";
@interface HBQueue ()
@property (nonatomic, readonly) NSURL *fileURL;
@property (nonatomic, readonly) NSMutableArray> *itemsInternal;
@property (nonatomic, readonly) NSArray *workers;
@property (nonatomic) IOPMAssertionID assertionID;
@property (nonatomic) NSUInteger pendingItemsCount;
@property (nonatomic) NSUInteger failedItemsCount;
@property (nonatomic) NSUInteger completedItemsCount;
@property (nonatomic) NSUInteger workingItemsCount;
@end
@implementation HBQueue
- (void)setUpWorkers
{
NSArray *xpcServiceNames = @[@"fr.handbrake.HandBrakeXPCService", @"fr.handbrake.HandBrakeXPCService2",
@"fr.handbrake.HandBrakeXPCService3", @"fr.handbrake.HandBrakeXPCService4"];
NSMutableArray *workers = [[NSMutableArray alloc] init];
for (NSString *xpcServiceName in xpcServiceNames)
{
HBQueueWorker *worker = [[HBQueueWorker alloc] initWithXPCServiceName:xpcServiceName];
[workers addObject:worker];
}
_workers = [workers copy];
}
- (void)setUpObservers
{
for (HBQueueWorker *worker in _workers)
{
[NSNotificationCenter.defaultCenter addObserverForName:HBQueueWorkerDidChangeStateNotification
object:worker
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification * _Nonnull note) {
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeStateNotification object:self];
}];
[NSNotificationCenter.defaultCenter addObserverForName:HBQueueWorkerDidStartItemNotification
object:worker
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification * _Nonnull note) {
[self updateStats];
HBQueueJobItem *item = note.userInfo[HBQueueWorkerItemNotificationItemKey];
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidStartItemNotification object:self userInfo:@{HBQueueItemNotificationItemKey: item}];
}];
[NSNotificationCenter.defaultCenter addObserverForName:HBQueueWorkerDidCompleteItemNotification
object:worker
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification * _Nonnull note) {
[self updateStats];
HBQueueJobItem *item = note.userInfo[HBQueueWorkerItemNotificationItemKey];
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteItemNotification object:self userInfo:@{HBQueueItemNotificationItemKey: item}];
[self completedItem:item];
}];
}
[NSUserDefaultsController.sharedUserDefaultsController addObserver:self forKeyPath:@"values.HBQueueWorkerCounts"
options:0 context:HBQueueContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == HBQueueContext)
{
if (self.isEncoding)
{
[self encodeNextQueueItem];
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (instancetype)initWithURL:(NSURL *)fileURL
{
self = [super init];
if (self)
{
_fileURL = fileURL;
_itemsInternal = [self load];
_undoManager = [[NSUndoManager alloc] init];
_assertionID = -1;
[self setEncodingJobsAsPending];
[self removeCompletedAndCancelledItems];
[self updateStats];
[self setUpWorkers];
[self setUpObservers];
}
return self;
}
#pragma mark - Load and save
- (NSMutableArray *)load
{
NSError *error;
NSData *queue = [NSData dataWithContentsOfURL:self.fileURL];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:queue];
unarchiver.requiresSecureCoding = YES;
NSSet *objectClasses = [NSSet setWithObjects:[NSMutableArray class], [HBQueueJobItem class], [HBQueueActionStopItem class], nil];
NSArray *loadedItems = [unarchiver decodeTopLevelObjectOfClasses:objectClasses
forKey:NSKeyedArchiveRootObjectKey
error:&error];
if (error)
{
[HBUtilities writeErrorToActivityLog:error];
}
[unarchiver finishDecoding];
return loadedItems ? [loadedItems mutableCopy] : [NSMutableArray array];
}
- (void)save
{
if (![NSKeyedArchiver archiveRootObject:self.itemsInternal toFile:self.fileURL.path])
{
[HBUtilities writeToActivityLog:"Failed to write the queue to disk"];
}
}
#pragma mark - Public methods
- (NSArray> *)items
{
return self.itemsInternal;
}
- (NSUInteger)workersCount
{
NSUInteger count = [NSUserDefaults.standardUserDefaults integerForKey:HBQueueWorkerCounts];
return count > 0 && count <= 4 ? count : 1;
}
- (void)addJob:(HBJob *)item
{
NSParameterAssert(item);
[self addJobs:@[item]];
}
- (void)addJobs:(NSArray *)jobs
{
NSParameterAssert(jobs);
NSMutableArray *itemsToAdd = [NSMutableArray array];
for (HBJob *job in jobs)
{
HBQueueJobItem *item = [[HBQueueJobItem alloc] initWithJob:job];
[itemsToAdd addObject:item];
}
if (itemsToAdd.count)
{
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.itemsInternal.count, itemsToAdd.count)];
[self addItems:itemsToAdd atIndexes:indexes];
if (self.isEncoding && self.countOfEncodings < self.workersCount)
{
[self encodeNextQueueItem];
}
}
}
- (BOOL)itemExistAtURL:(NSURL *)url
{
NSParameterAssert(url);
for (HBQueueJobItem *item in self.itemsInternal)
{
if ([item isKindOfClass:[HBQueueJobItem class]] &&
(item.state == HBQueueItemStateReady || item.state == HBQueueItemStateWorking)
&& [item.completeOutputURL isEqualTo:url])
{
return YES;
}
}
return NO;
}
- (void)addItems:(NSArray> *)items atIndexes:(NSIndexSet *)indexes
{
NSParameterAssert(items);
NSParameterAssert(indexes);
// Forward
NSUInteger currentIndex = indexes.firstIndex;
NSUInteger currentObjectIndex = 0;
while (currentIndex != NSNotFound)
{
[self.itemsInternal insertObject:items[currentObjectIndex] atIndex:currentIndex];
currentIndex = [indexes indexGreaterThanIndex:currentIndex];
currentObjectIndex++;
}
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidAddItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}];
NSUndoManager *undo = self.undoManager;
[[undo prepareWithInvocationTarget:self] removeItemsAtIndexes:indexes];
if (!undo.isUndoing)
{
if (items.count == 1)
{
[undo setActionName:NSLocalizedString(@"Add Job To Queue", @"Queue undo action name")];
}
else
{
[undo setActionName:NSLocalizedString(@"Add Jobs To Queue", @"Queue undo action name")];
}
}
[self updateStats];
[self save];
}
- (void)prepareItemForEditingAtIndex:(NSUInteger)index
{
HBQueueJobItem *item = self.itemsInternal[index];
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:index];
if (item.state == HBQueueItemStateWorking)
{
[self cancelItemsAtIndexes:indexes];
}
item.state = HBQueueItemStateRescanning;
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeItemNotification
object:self
userInfo:@{HBQueueItemNotificationIndexesKey: [NSIndexSet indexSetWithIndex:index]}];
[self updateStats];
}
- (void)removeItemsAtIndexes:(NSIndexSet *)indexes
{
NSParameterAssert(indexes);
if (indexes.count == 0)
{
return;
}
NSArray> *removeItems = [self.itemsInternal objectsAtIndexes:indexes];
if (self.itemsInternal.count > indexes.lastIndex)
{
[self.itemsInternal removeObjectsAtIndexes:indexes];
}
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidRemoveItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}];
NSUndoManager *undo = self.undoManager;
[[undo prepareWithInvocationTarget:self] addItems:removeItems atIndexes:indexes];
if (!undo.isUndoing)
{
if (indexes.count == 1)
{
[undo setActionName:NSLocalizedString(@"Remove Job From Queue", @"Queue undo action name")];
}
else
{
[undo setActionName:NSLocalizedString(@"Remove Jobs From Queue", @"Queue undo action name")];
}
}
[self updateStats];
[self save];
}
- (void)moveItems:(NSArray> *)items toIndex:(NSUInteger)index
{
NSMutableArray *source = [NSMutableArray array];
NSMutableArray *dest = [NSMutableArray array];
for (id object in items.reverseObjectEnumerator)
{
NSUInteger sourceIndex = [self.itemsInternal indexOfObject:object];
[self.itemsInternal removeObjectAtIndex:sourceIndex];
if (sourceIndex < index)
{
index--;
}
[self.itemsInternal insertObject:object atIndex:index];
[source addObject:@(index)];
[dest addObject:@(sourceIndex)];
}
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidMoveItemNotification
object:self
userInfo:@{HBQueueItemNotificationSourceIndexesKey: dest,
HBQueueItemNotificationTargetIndexesKey: source}];
NSUndoManager *undo = self.undoManager;
[[undo prepareWithInvocationTarget:self] moveQueueItemsAtIndexes:source toIndexes:dest];
if (!undo.isUndoing)
{
if (items.count == 1)
{
[undo setActionName:NSLocalizedString(@"Move Job in Queue", @"Queue undo action name")];
}
else
{
[undo setActionName:NSLocalizedString(@"Move Jobs in Queue", @"Queue undo action name")];
}
}
[self save];
}
- (void)moveQueueItemsAtIndexes:(NSArray *)source toIndexes:(NSArray *)dest
{
NSMutableArray *newSource = [NSMutableArray array];
NSMutableArray *newDest = [NSMutableArray array];
for (NSInteger idx = source.count - 1; idx >= 0; idx--)
{
NSUInteger sourceIndex = [source[idx] integerValue];
NSUInteger destIndex = [dest[idx] integerValue];
[newSource addObject:@(destIndex)];
[newDest addObject:@(sourceIndex)];
id obj = [self.itemsInternal objectAtIndex:sourceIndex];
[self.itemsInternal removeObjectAtIndex:sourceIndex];
[self.itemsInternal insertObject:obj atIndex:destIndex];
}
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidMoveItemNotification
object:self
userInfo:@{HBQueueItemNotificationSourceIndexesKey: newDest,
HBQueueItemNotificationTargetIndexesKey: newSource}];
NSUndoManager *undo = self.undoManager;
[[undo prepareWithInvocationTarget:self] moveQueueItemsAtIndexes:newSource toIndexes:newDest];
if (!undo.isUndoing)
{
if (source.count == 1)
{
[undo setActionName:NSLocalizedString(@"Move Job in Queue", @"Queue undo action name")];
}
else
{
[undo setActionName:NSLocalizedString(@"Move Jobs in Queue", @"Queue undo action name")];
}
}
[self save];
}
/**
* This method will clear the queue of any encodes that are not still pending
* this includes both successfully completed encodes as well as canceled encodes
*/
- (void)removeCompletedAndCancelledItems
{
NSIndexSet *indexes = [self.itemsInternal HB_indexesOfObjectsUsingBlock:^BOOL(id item) {
return (item.state == HBQueueItemStateCompleted || item.state == HBQueueItemStateCanceled);
}];
[self removeItemsAtIndexes:indexes];
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidRemoveItemNotification object:self userInfo:@{@"indexes": indexes}];
[self save];
}
/**
* This method will clear the queue of all encodes. effectively creating an empty queue
*/
- (void)removeAllItems
{
[self removeItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.itemsInternal.count)]];
[self save];
}
- (void)removeNotWorkingItems
{
NSIndexSet *indexes = [self.itemsInternal HB_indexesOfObjectsUsingBlock:^BOOL(id item) {
return (item.state != HBQueueItemStateWorking && item.state != HBQueueItemStateRescanning);
}];
[self removeItemsAtIndexes:indexes];
}
- (void)removeCompletedItems
{
NSIndexSet *indexes = [self.itemsInternal HB_indexesOfObjectsUsingBlock:^BOOL(id item) {
return (item.state == HBQueueItemStateCompleted);
}];
[self removeItemsAtIndexes:indexes];
}
- (void)resetItemsAtIndexes:(NSIndexSet *)indexes
{
NSMutableIndexSet *updatedIndexes = [NSMutableIndexSet indexSet];
NSUInteger currentIndex = indexes.firstIndex;
while (currentIndex != NSNotFound) {
id item = self.itemsInternal[currentIndex];
if (item.state == HBQueueItemStateCanceled || item.state == HBQueueItemStateCompleted ||
item.state == HBQueueItemStateFailed || item.state == HBQueueItemStateRescanning)
{
item.state = HBQueueItemStateReady;
[updatedIndexes addIndex:currentIndex];
}
currentIndex = [indexes indexGreaterThanIndex:currentIndex];
}
[self updateStats];
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}];
[self save];
}
- (void)resetAllItems
{
NSIndexSet *indexes = [self.itemsInternal HB_indexesOfObjectsUsingBlock:^BOOL(id item) {
return (item.state != HBQueueItemStateWorking && item.state != HBQueueItemStateRescanning);
}];
[self resetItemsAtIndexes:indexes];
}
- (void)resetFailedItems
{
NSIndexSet *indexes = [self.itemsInternal HB_indexesOfObjectsUsingBlock:^BOOL(id item) {
return (item.state == HBQueueItemStateFailed);
}];
[self resetItemsAtIndexes:indexes];
}
/**
* This method will set any item marked as encoding back to pending
* currently used right after a queue reload
*/
- (void)setEncodingJobsAsPending
{
NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
NSUInteger idx = 0;
for (id item in self.itemsInternal)
{
// We want to keep any queue item that is pending or was previously being encoded
if (item.state == HBQueueItemStateWorking || item.state == HBQueueItemStateRescanning)
{
item.state = HBQueueItemStateReady;
[indexes addIndex:idx];
}
idx++;
}
[self updateStats];
[self save];
}
- (BOOL)canEncode
{
return self.pendingItemsCount > 0 && ![self isEncoding];
}
- (BOOL)isEncoding
{
BOOL isEncoding = NO;
for (HBQueueWorker *worker in self.workers)
{
isEncoding |= worker.isEncoding;
}
return isEncoding;
}
- (NSUInteger)countOfEncodings
{
NSUInteger count = 0;
for (HBQueueWorker *worker in self.workers)
{
count += worker.isEncoding ? 1 : 0;
}
return count;
}
- (BOOL)canPause
{
BOOL canPause = NO;
for (HBQueueWorker *worker in self.workers)
{
if (worker.isEncoding)
{
canPause |= worker.canPause;
}
}
return canPause;
}
- (void)pause
{
for (HBQueueWorker *worker in self.workers)
{
if (worker.canPause)
{
[worker pause];
}
}
[self allowSleep];
}
- (BOOL)canResume
{
BOOL canResume = NO;
for (HBQueueWorker *worker in self.workers)
{
if (worker.isEncoding)
{
canResume |= worker.canResume;
}
}
return canResume;
}
- (void)resume
{
for (HBQueueWorker *worker in self.workers)
{
if (worker.canResume)
{
[worker resume];
}
}
[self preventSleep];
}
#pragma mark - Sleep
- (void)preventSleep
{
if (_assertionID != -1)
{
// nothing to do
return;
}
CFStringRef reasonForActivity= CFSTR("HandBrake is currently scanning and/or encoding");
IOReturn success = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleSystemSleep,
kIOPMAssertionLevelOn, reasonForActivity, &_assertionID);
if (success != kIOReturnSuccess)
{
[HBUtilities writeToActivityLog:"HBRemoteCore: failed to prevent system sleep"];
}
}
- (void)allowSleep
{
if (_assertionID == -1)
{
// nothing to do
return;
}
IOReturn success = IOPMAssertionRelease(_assertionID);
if (success == kIOReturnSuccess)
{
_assertionID = -1;
}
}
#pragma mark - Private queue editing methods
- (void)updateStats
{
NSUInteger pendingCount = 0, failedCount = 0, completedCount = 0, workingCount = 0;
for (HBQueueJobItem *item in self.itemsInternal)
{
if ([item isKindOfClass:[HBQueueJobItem class]])
{
if (item.state == HBQueueItemStateReady)
{
pendingCount++;
}
else if (item.state == HBQueueItemStateCompleted)
{
completedCount++;
}
else if (item.state == HBQueueItemStateFailed)
{
failedCount++;
}
else if (item.state == HBQueueItemStateWorking)
{
workingCount++;
}
}
}
self.pendingItemsCount = pendingCount;
self.failedItemsCount = failedCount;
self.completedItemsCount = completedCount;
self.workingItemsCount = workingCount;
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeStateNotification object:self];
}
- (BOOL)isDiskSpaceLowAtURL:(NSURL *)url
{
if ([NSUserDefaults.standardUserDefaults boolForKey:HBQueuePauseIfLowSpace])
{
NSURL *volumeURL = nil;
NSDictionary *attrs = [url resourceValuesForKeys:@[NSURLIsVolumeKey, NSURLVolumeURLKey] error:NULL];
long long minCapacity = [[NSUserDefaults.standardUserDefaults stringForKey:HBQueueMinFreeSpace] longLongValue] * 1000000000;
volumeURL = [attrs[NSURLIsVolumeKey] boolValue] ? url : attrs[NSURLVolumeURLKey];
if (volumeURL)
{
[volumeURL removeCachedResourceValueForKey:NSURLVolumeAvailableCapacityKey];
attrs = [volumeURL resourceValuesForKeys:@[NSURLVolumeAvailableCapacityKey] error:NULL];
if (attrs[NSURLVolumeAvailableCapacityKey])
{
if ([attrs[NSURLVolumeAvailableCapacityKey] longLongValue] < minCapacity)
{
return YES;
}
}
}
}
return NO;
}
/**
* Used to get the next pending queue item and return it if found
*/
- (id)nextPendingQueueItem
{
for (id item in self.itemsInternal)
{
if (item.state == HBQueueItemStateReady)
{
return item;
}
}
return nil;
}
- (HBQueueWorker *)firstAvailableWorker
{
for (HBQueueWorker *worker in self.workers)
{
if (worker.isEncoding == NO)
{
return worker;
}
}
return nil;
}
- (void)addStopAction
{
id nextItem = self.nextPendingQueueItem;
NSUInteger index = nextItem ? [self.itemsInternal indexOfObject:nextItem] : self.itemsInternal.count;
if ([nextItem isKindOfClass:[HBQueueActionStopItem class]] == NO)
{
HBQueueActionStopItem *stopItem = [[HBQueueActionStopItem alloc] init];
[self addItems:@[stopItem] atIndexes:[NSIndexSet indexSetWithIndex:index]];
}
}
- (void)start
{
if (self.canEncode)
{
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidStartNotification object:self];
[self preventSleep];
[self encodeNextQueueItem];
}
}
/**
* Starts the queue
*/
- (void)encodeNextQueueItem
{
id nextItem = self.nextPendingQueueItem;
// since we have completed an encode, we go to the next
if ([nextItem isKindOfClass:[HBQueueActionStopItem class]])
{
if (self.isEncoding == NO)
{
[HBUtilities writeToActivityLog:"Queue manually stopped"];
NSUInteger index = [self.itemsInternal indexOfObject:nextItem];
[self removeItemsAtIndexes:[NSIndexSet indexSetWithIndex:index]];
[self allowSleep];
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteNotification object:self];
}
}
else
{
// Check to see if there are any more pending items in the queue
HBQueueJobItem *nextJobItem = self.nextPendingQueueItem;
HBQueueWorker *worker = self.firstAvailableWorker;
if (nextJobItem && [self isDiskSpaceLowAtURL:nextJobItem.outputURL])
{
[HBUtilities writeToActivityLog:"Queue Stopped, low space on destination disk"];
[self allowSleep];
if (self.isEncoding == NO)
{
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteNotification object:self];
}
else
{
[self pause];
}
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueLowSpaceAlertNotification object:self];
}
// If we still have more pending items in our queue, lets go to the next one
else if (nextJobItem && worker && self.countOfEncodings < self.workersCount)
{
[worker encodeItem:nextJobItem];
// Erase undo manager history
[self.undoManager removeAllActions];
if (self.firstAvailableWorker)
{
[self encodeNextQueueItem];
}
}
else if (self.isEncoding == NO)
{
[HBUtilities writeToActivityLog:"Queue Done, there are no more pending encodes"];
[self allowSleep];
[NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteNotification object:self];
}
}
[self save];
}
- (void)completedItem:(id)item
{
NSParameterAssert(item);
[self save];
[self encodeNextQueueItem];
}
/**
* Cancels the current job
*/
- (void)doCancelAll
{
for (HBQueueWorker *worker in self.workers)
{
if (worker.isEncoding)
{
[worker cancel];
}
}
}
/**
* Cancels the current job and starts processing the next in queue.
*/
- (void)cancelCurrentAndContinue
{
[self doCancelAll];
}
/**
* Cancels the current job and stops libhb from processing the remaining encodes.
*/
- (void)cancelCurrentAndStop
{
if (self.isEncoding)
{
[self addStopAction];
[self doCancelAll];
}
}
/**
* Finishes the current job and stops libhb from processing the remaining encodes.
*/
- (void)finishCurrentAndStop
{
if (self.isEncoding)
{
[self addStopAction];
}
}
- (void)cancelItemsAtIndexes:(NSIndexSet *)indexes
{
NSArray> *items = [self.items objectsAtIndexes:indexes];
for (HBQueueJobItem *item in items) {
for (HBQueueWorker *worker in self.workers) {
if (worker.item == item) {
[worker cancel];
}
}
}
}
- (nullable HBQueueWorker *)workerForItem:(HBQueueJobItem *)item
{
for (HBQueueWorker *worker in self.workers)
{
if (worker.item == item)
{
return worker;
}
}
return nil;
}
@end