Last active
February 7, 2021 03:14
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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