Skip to content

Instantly share code, notes, and snippets.

@tempelmann
Last active February 7, 2021 03:14
Show Gist options
  • Select an option

  • Save tempelmann/2ad5351bb19878f80e50cf5defd995de to your computer and use it in GitHub Desktop.

Select an option

Save tempelmann/2ad5351bb19878f80e50cf5defd995de to your computer and use it in GitHub Desktop.
Demonstrates crashing bug in [NSWorkspace recycleURLs:completionHandler:] when trashing over 500 files at once
//
// recycleURLs_crash_test
//
// Created by Thomas Tempelmann on 01Feb21.
//
// Purpose:
// Demonstrates that using [NSWorkspace recycleURLs:] on > 600 files often leads to a crash on macOS 10.12 and 10.13.
// In my testing (on 10.13.6), up to 500 items never crash, while it starts crashing sometimes with 650 items
// and nearly every time when it's over 800. I also get spurious errors on the first recycle interation when
// using 600 items. This could be a precursor to the crash, but not sure.
//
// How to use:
// 1. Create a test folder with at least 10000 files inside. You could use this script:
// #!/bin/bash
// #
// # Creates N files with random names in the current dir.
// # Prefixes all file names with "+" so that we can easily identify them.
// #
// n=$1
// if [[ $n -lt 1 ]]; then
// echo "You need to provide a number specifing how many files you want created in the current dir"
// exit 1
// fi
// cat /usr/share/dict/words >.filenames # all dict words, one per line
// sort -R <.filenames | head -n $n >.sortednames
// while read p; do
// touch "+$p"
// done <.sortednames
// rm .filenames .sortednames
// 2. Build this as a command line tool, then run this command by passing two args:
// Path to the folder containing at least a few thousand files.
// Number of files to delete with a single `recycleURLs:` invocation. Try 1000 to get the crash.
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 3) {
fprintf (stderr, "Pass a path to a folder containing files for trashing and a numeric limit (e.g. 1000)\n");
return 1;
}
const char *cpath = argv[1];
NSString *dirpath = [NSString stringWithUTF8String:cpath];
int limit = atoi(argv[2]);
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
__block NSInteger movedItems = 0;
while (1) { // repeat until all files are trashed
// read dir contents
NSError *error = nil;
NSArray<NSString*> *names = [NSFileManager.defaultManager contentsOfDirectoryAtPath:dirpath error:&error];
if (error) {
fprintf (stderr, "Reading of dir failed: %s\n", error.localizedDescription.UTF8String);
return 2;
}
NSMutableArray *urls = [NSMutableArray array];
[names enumerateObjectsUsingBlock:^(NSString * _Nonnull name, NSUInteger idx, BOOL * _Nonnull stop) {
NSURL *url = [NSURL fileURLWithPath:[dirpath stringByAppendingPathComponent:name]];
[urls addObject:url];
if (urls.count >= limit) *stop = YES;
}];
if (urls.count <= 1) {
printf ("Not enough files left to trash: %ld\n", urls.count);
break;
}
printf ("Trashing %ld items…\n", urls.count);
__block BOOL done = NO;
#if 0 // Use old FS… API, which do not crash but are just as fast
NSMutableArray *failedURLs = [NSMutableArray array];
NSMutableDictionary *trashedURLs = [NSMutableDictionary dictionary];
NSOperationQueue *trashQueue = [NSOperationQueue new];
trashQueue.maxConcurrentOperationCount = 20;
for (NSURL *url in urls) {
NSString *path = url.path;
[trashQueue addOperationWithBlock:^{
char *movedPath = nil;
OSStatus res = FSPathMoveObjectToTrashSync(path.UTF8String, &movedPath, 0);
@synchronized (trashQueue) {
if (res) {
[failedURLs addObject:url];
} else {
NSString *path = [NSString stringWithCString:movedPath encoding:NSUTF8StringEncoding];
trashedURLs[url] = path;
movedItems += 1;
}
if (movedPath) free (movedPath);
}
}];
}
// wait for it to finish
while ((failedURLs.count + trashedURLs.count) < urls.count) {
[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
if (failedURLs.count > 0) {
fprintf (stderr, "Trashing failed for %ld items\n", failedURLs.count);
}
#else
[NSWorkspace.sharedWorkspace recycleURLs:urls completionHandler:^(NSDictionary<NSURL *,NSURL *> * _Nonnull newURLs, NSError * _Nullable error) {
if (error) {
fprintf (stderr, "Error while trashing: %s\n", error.description.UTF8String);
}
movedItems += newURLs.count;
done = YES;
}];
// wait for it to finish
while (!done) {
[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
#endif
printf ("Trash operation finished\n");
}
CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
printf ("Moved %ld items in %.01lfs\n", movedItems, endTime-startTime);
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment