FileManager contentsOfDirectory fails with error 256

There is a macFUSE bug that can result in an Input/output error when listing a directory. In most cases this doesn't cause any major issues, just a "ls: <mount>: Input/output error" message when running ls.

The problem is that the contentsOfDirectory(at:includingPropertiesForKeys:options:)  now does not work, however, the contentsOfDirectory(atPath:) method does.

There appears to be a difference in how these methods handle errors, although the documentation doesn't seem to suggest this.

The following code returns an array of strings without an error:

Code Block
FileManager.default.contentsOfDirectory(atPath: "/Users/<user>")


The following code results in an error:

Code Block
FileManager.default.contentsOfDirectory(at: URL.init(fileURLWithPath:"/Users/<user>"), includingPropertiesForKeys: nil)

REPL:
Code Block
$E3: NSError = domain: "NSCocoaErrorDomain" - code: 256 {
_userInfo = 3 key/value pairs {
[0] = {
key = "NSURL"
value = "file:///Users/<user>/<mount>"
}
[1] = {
key = "NSFilePath"
value = "/Users/<user>/<mount>"
}
[2] = {
key = "NSUnderlyingError"
value =
}
}
}

Xcode:
Code Block
Error Domain=NSCocoaErrorDomain Code=256 "The file “<mount>” couldn’t be opened." UserInfo={NSURL=file:///Users/<user>/<mount>, NSFilePath=/Users/<user>/<mount>, NSUnderlyingError=0x600000289740 {Error Domain=NSPOSIXErrorDomain Code=5 "Input/output error”}}


Passing [] as the includingPropertiesForKeys value gives the same result.

It's also interesting that in the REPL the NSUnderlyingError value is missing.

Replies

I tested the following (Xcode 12.4):

Code Block
do {
let content = try FileManager.default.contentsOfDirectory(atPath: "/Users/me")
print("content", content)
} catch {
print("error", error)
}
do {
let content = try FileManager.default.contentsOfDirectory(at: URL.init(fileURLWithPath:"/Users/me"), includingPropertiesForKeys: nil)
print("content", content)
} catch {
print("error", error)
}
do {
let content = try FileManager.default.contentsOfDirectory(at: URL.init(fileURLWithPath:"/Users/me"), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
print("content", content)
} catch {
print("error", error)
}


It does provide in all cases the complete directory (including hidden files except in last case).
But does not work in playground.
You need to trigger the Input/output error first. This can be achieved with ifuse and connecting an iOS device:
Code Block
brew install ifuse
mkdir iphone
ifuse iphone

Now disconnecting the iOS device will result in the "Input/output error".

This is the output when running the above code in Xcode 12.4:
Code Block
content <directory contents>
error Error Domain=NSCocoaErrorDomain Code=256 "The file “<mount>” couldn’t be opened." UserInfo={NSURL=file:///Users/<user>/<mount>, NSFilePath=/Users/<user>/<mount>, NSUnderlyingError=0x600002b69d10 {Error Domain=NSPOSIXErrorDomain Code=5 "Input/output error"}}
error Error Domain=NSCocoaErrorDomain Code=256 "The file “<mount>” couldn’t be opened." UserInfo={NSURL=file:///Users/<user>/<mount>, NSFilePath=/Users/<user>/<mount>, NSUnderlyingError=0x600002b6e0a0 {Error Domain=NSPOSIXErrorDomain Code=5 "Input/output error"}}

I’m not sure what your specific complaint is here:
  • Are you concerned that contentsOfDirectory(at:…) and contentsOfDirectory(atPath:…) have different error handling?

  • Or that your VFS plug-in is causing problems for seemingly-unrelated code?

If it’s the former, that’s not something I’d spend time worrying around. As a general rule the URL routines are more modern and thus better at returning errors.

If it’s the latter, this is something for your VFS plug-in vendor to resolve. Posix error 5 is EIO, and that error is most definitely coming from the VFS layer. FileManager is just an innocent bystander here.

Share and Enjoy

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

I assumed the behavior of the methods would be comparable as the documentation seems to be almost the same.

It looks as though they are fundamentally different though, contentsOfDirectory(atPath:…) performs a shallow read and contentsOfDirectory(at:…) seems to perform a stat call on all the entries. Based on the following code comment I thought it might be possible to avoid the stat call but it seems to happen in all cases:
Code Block
If you wish to only receive the URLs and no other attributes, then pass '0' for 'options' and an empty NSArray ('[NSArray array]') for 'keys'. If you wish to have the property caches of the vended URLs pre-populated with a default set of attributes, then pass '0' for 'options' and 'nil' for 'keys'.


I assumed the behavior of the methods would be comparable as the
documentation seems to be almost the same.

contentsOfDirectory(at:…) seems to perform a stat call on all
the entries.

Would that it were that simple (-:

In the standard case both methods use getattrlistbulk, which is a key performance win. I ran some tests (see below) and it seems that calling the URL method with the empty array and no options results in exactly the same getattrlistbulk call as the path method. I suspect what’s going on here is that some ancillary call, outside of the main directory iteration loop, is triggering the failure.

If you want to explore this further create a tiny test tool that looks like this:

Code Block
let u = URL(fileURLWithPath: "/Users/quinn/Test")
_ = try! FileManager.default.contentsOfDirectory(atPath: u.path)
mkdir("/Users/quinn/Test1", 0o755)
_ = try! FileManager.default.contentsOfDirectory(atPath: u.path)
mkdir("/Users/quinn/Test2", 0o755)
_ = try! FileManager.default.contentsOfDirectory(at: u, includingPropertiesForKeys: nil, options: [])
mkdir("/Users/quinn/Test3", 0o755)


IMPORTANT The three mkdir calls are included as markers, and the first call to contentsOfDirectory(atPath:…) is there purely to prime the pump. Without it there’s a bunch of one-time initialisation overhead that occurs between the first and second marker, which muddies this whole story.

The next calls occur between the markers, which allows you to see what’s going on with fs_usage. For example, in my case I see this:

Code Block
… mkdir [ 17] /Users/quinn/Test1 …
… stat64 /Users/quinn/Test …
… getattrlist /Users/quinn/Test …
… getattrlist /Users/quinn/Test …
… getattrlist /Users/quinn/Test …
… open F=3 (R__________X) /Users/quinn/Test …
… fstat64 F=3 …
… getattrlistbulk …
… getattrlistbulk …
… close F=3 …
… mkdir [ 17] /Users/quinn/Test2 …
… getattrlist /Users/quinn/Test …
… getattrlist /Users/quinn/Test …
… open F=3 (R__________X) /Users/quinn/Test …
… fstat64 F=3 …
… getattrlistbulk …
… getattrlistbulk …
… close F=3 …
… mkdir [ 17] /Users/quinn/Test3 …


So, by looking between the mkdir markers you can see exactly what BSD file system calls are made by each FileManager call. You should also be able to locate the source of the error; these are shown in square brackets, for example, the [ 17] indicates that mkdir failed with EEXIST.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
I modified the contentsOfDirectory(at:…) arguments as the code comment suggests to "only receive the URLs and no other attributes":

Code Block swift
let u = URL(fileURLWithPath: "/Users/<user>")
_ = try! FileManager.default.contentsOfDirectory(atPath: u.path)
mkdir("/Users/<user>/Test1", 0o755)
_ = try! FileManager.default.contentsOfDirectory(atPath: u.path)
mkdir("/Users/<user>/Test2", 0o755)
_ = try! FileManager.default.contentsOfDirectory(at: u, includingPropertiesForKeys: [], options: FileManager.DirectoryEnumerationOptions(rawValue: 0))
mkdir("/Users/<user>/Test3", 0o755)


The output of fs_usage when contentsOfDirectory(at:…) throws a fatal error:
Code Block
mkdir [ 1] /Users/<user>/Test1
stat64 /Users/<user>
getattrlist /Users/<user>
getattrlist /Users/<user>
getattrlist /Users/<user>
open F=4 (R__________X) /Users/<user>
fstat64 F=4
getattrlistbulk
getattrlist [ 5] /Users/<user>/<mount>
getattrlistbulk
getattrlistbulk
close F=4
mkdir [ 1] /Users/<user>/Test2
getattrlist /Users/<user>
getattrlist /Users/<user>
open F=4 (R__________X) /Users/<user>
fstat64 F=4
getattrlistbulk
getattrlist [ 5] /Users/<user>/<mount>
getattrlistbulk
close F=4