Search code examples
macosasynchronouscopymacos-carboncore-foundation

FSCopyObjectAsync callback method not being called while on background thread


I am using FSCopyObjectAsync to copy files across volumes. I have used the code from the cimgf to get me going and it is working pretty well.

One of the last issues I am getting hung up on is that the copy status callback isn't occurring on a background thread. When I don't start the copy operation by dispatch_async(copyQueue, ^{, the callback gets called perfectly. When I move it to the background, it won't fire. Here is the code:

//Excerpt from existing method
// Create the semaphore, specifying the initial pool size
fd_sema = dispatch_semaphore_create(1);

dispatch_queue_t copyQueue = dispatch_queue_create("copy.theQueue", 0);
dispatch_group_t group = dispatch_group_create();

for(SearchPath * p in searchPaths) {
    dispatch_async(copyQueue, ^{
        
        NSString * newDestination = [NSString stringWithFormat:@"%@%@",destination,p.relativePath];
        NSString * source = [NSString stringWithFormat:@"%@%@",p.basePath,p.relativePath];
        NSError * error = nil;
        
        //Wait until semaphore is available  
        dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
        
        //Update progress window text
        [progressview.label setStringValue:[NSString stringWithFormat:@"Copying \"%@\" to \"%@\"",[source lastPathComponent],[destination lastPathComponent]]];
        
        if(p.isDirectory) {
            
            BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:newDestination withIntermediateDirectories:NO attributes:nil error:nil];
            
            if(result) {
                //Item was a directory
                dispatch_semaphore_signal(fd_sema);
            }
            
        }else{
            
            [self startCopy:source dest:[newDestination stringByDeletingLastPathComponent]];
            
        }
        
        if(error) {
            MTLogDebug(@"Error! : %ld", error.code);
        }
    }); //End async
} //End for loop

//End excerpt  

- (void)startCopy:(NSString *)source dest:(NSString *) destination
{
    // Get the current run loop and schedule our callback
    //TODO:Make this work while on a background thread
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    FSFileOperationRef fileOp = FSFileOperationCreate(kCFAllocatorDefault);

    OSStatus status = FSFileOperationScheduleWithRunLoop(fileOp, runLoop, kCFRunLoopDefaultMode);
    if( status )
    {
        NSLog(@"Failed to schedule operation with run loop: %@", status);
        return;
    }

    // Create a filesystem ref structure for the source and destination and
    // populate them with their respective paths from our NSTextFields.
    FSRef sourceRef;
    FSRef destinationRef;

    //FSPathMakeRef( (const UInt8 *)[source fileSystemRepresentation], &sourceRef, NULL );
    FSPathMakeRefWithOptions((const UInt8 *)[source fileSystemRepresentation],
                             kFSPathMakeRefDefaultOptions, 
                             &sourceRef, 
                             NULL);

    Boolean isDir = true;
    //FSPathMakeRef( (const UInt8 *)[destination fileSystemRepresentation], &destinationRef, &isDir );    
    FSPathMakeRefWithOptions((const UInt8 *)[destination fileSystemRepresentation],
                             kFSPathMakeRefDefaultOptions, 
                             &destinationRef, 
                             &isDir);

    // Start the async copy.
    status = FSCopyObjectAsync (fileOp,
                                &sourceRef,
                                &destinationRef, // Full path to destination dir
                                NULL, // Use the same filename as source
                                kFSFileOperationDefaultOptions,
                                statusCallback,
                                0.1,
                                NULL);
    NSLog(@"Stat: %d",status);
    CFRelease(fileOp);

    if(status) {
        NSLog(@"Failed to begin asynchronous object copy: %d", status);
    }
}

static void statusCallback (FSFileOperationRef fileOp,
                            const FSRef *currentItem,
                            FSFileOperationStage stage,
                            OSStatus error,
                            CFDictionaryRef statusDictionary,
                            void *info)
{
    if (statusDictionary) {
        
        NSNumber *bytesCompleted = (__bridge NSNumber *) CFDictionaryGetValue(statusDictionary, kFSOperationBytesCompleteKey);
        
        NSURL *url = (__bridge NSURL *)convertedURLRef;
        
        if([bytesCompleted intValue] > 0) {
            
            if(stage == kFSOperationStageRunning) {
                
                //Update progress indicator
                [progressview.indicator setDoubleValue:progressview.indicator.doubleValue + [newNumberValue floatValue]];           
            }
        }
    }
    
    if (stage == kFSOperationStageComplete) {
        dispatch_semaphore_signal(fd_sema);
    }
}

Any help or insight is appreciated!


Solution

  • The problem is that you're using this with dispatch_async, but FSCopyObjectAsync ties the copy operation callbacks to a specific runloop, and thus a specific thread.

    What you need to do is not use dispatch_async, but either:

    1. perform the copy operation on the main thread (which should probably be OK, in the same way that executing an NSURLConnection on the main thread is OK)
    2. spin off a secondary NSThread, schedule the operation on that thread, and then start the runloop running by calling [[NSRunLoop currentRunLoop] run] (or the appropriate variant).