Running FileManager.default.enumerator on different tasks

I'm writing a function where a directory and its descendants are scanned to produce a list of files. The function is part of a class, FileScanner. Here's a simplified version of the function:

func scanFolder(_ folderURL: URL) async {
						
	// Set up configuration for directory search, varies based on need to search sub directories
	let resourceKeysArray: [URLResourceKey] = [.nameKey, .isDirectoryKey, .fileResourceTypeKey, .creationDateKey, .contentModificationDateKey, .contentTypeKey]
	let resourceKeysSet = Set<URLResourceKey>(resourceKeysArray)
	let options = FileManager.DirectoryEnumerationOptions(arrayLiteral: [.skipsHiddenFiles, .skipsPackageDescendants])
			
			
	if let enumerator = FileManager.default.enumerator(at: folderURL, includingPropertiesForKeys: resourceKeysArray, options: options) {
				
        await folderScanStatus.markRunning()

		while
			await !folderScanStatus.cancelled,
			let fileURL = enumerator.nextObject() as? URL
		{
            print("\(id) found \(fileURL.path)") // Logging for debug purposes
			foundFiles.append(fileURL)
		}
	}
}	

The code is async, as it has to call some async functions (I've left a couple in for illustration.)

The user can have multiple scans in process simultaneously. For each one a new FileScanner is created, and de-inited once the scan has completed - the results are copied across elsewhere. scanFolder is called from within a Task:

    Task { await scanFolder(someURL) }

I am finding that if two of these processes run at once, they can actually interfere with each other - i.e. one can scan the directory of the other.

I added some logging to highlight the issue. The log line shows the ID of the scanner, and the file it has found. In this run there were two scanners:

  • EDF43558-608E-47A4-81E5-97B9707B1D0F, scanning /Volumes/Back-up A/
  • 982EC712-D79E-4785-A1BA-3B53F85967F0, scanning /Users/TEST/

And here's some extracts from the log showing them working as expected:

982EC712-D79E-4785-A1BA-3B53F85967F0 found /Users/TEST/Files/R_24_04-04.txt
EDF43558-608E-47A4-81E5-97B9707B1D0F found /Volumes/Back-up A/180704f01.txt

And here's an example showing Scanner 982EC712-D79E-4785-A1BA-3B53F85967F0 finding a file that the other one also picked up:

982EC712-D79E-4785-A1BA-3B53F85967F0 found /Volumes/Back-up A/19839f92.txt
:
EDF43558-608E-47A4-81E5-97B9707B1D0F found /Volumes/Back-up A/19839f92.txt

Any ideas why this is happening? I was under the impression FileManager was thread safe?

Any ideas why this is happening? I was under the impression FileManager was thread safe?

Generally, FileManager is considered to be thread safe, but you should better not expect that is declaring FileManager solves all the issues about concurrency.

In fact, there's a description:

FileManager

Threading Considerations

The methods of the shared FileManager object can be called from multiple threads safely. However, if you use a delegate to receive notifications about the status of move, copy, remove, and link operations, you should create a unique instance of the file manager object, assign your delegate to that object, and use that file manager to initiate your operations.

I guess there may be other cases you should create a unique instance of the file manager object in addition to delegate cases.

Can you try using a unique instance of FileManager instead of FileManager.default and tell us what happens?

I was under the impression FileManager was thread safe?

That’s correct.

I suspect that there’s something specific to Swift concurrency involved here. If you run the same test from multiple threads, created using Thread, do you still see the problem?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

OK - my bad! I set-up a separate project, away from my main code base that test running multiple FileManager enumerations in parallel (using Tasks) and it all worked...couldn't provoke it no matter how hard I prodded it. And then my eyes flicked back to my main code and a small dim light bulb in my head went on....four hours later I've tracked down a somewhat subtle bug in my code.

thank you for your responses!

Running FileManager.default.enumerator on different tasks
 
 
Q