3

I'm investigating a problem on Android where an IOException is getting thrown because of a failure to close a file:

java.io.IOException: close failed: EIO (I/O error)
    at libcore.io.IoUtils.close(IoUtils.java:41)
    at java.io.FileInputStream.close(FileInputStream.java:121)
    at com.adamrosenfield.wordswithcrosses.io.JPZIO.convertJPZPuzzle(JPZIO.java:191)
    at com.adamrosenfield.wordswithcrosses.net.AbstractJPZDownloader.download(AbstractJPZDownloader.java:56)
    at com.adamrosenfield.wordswithcrosses.net.AbstractJPZDownloader.download(AbstractJPZDownloader.java:41)
    at com.adamrosenfield.wordswithcrosses.net.AbstractDownloader.download(AbstractDownloader.java:112)
    at com.adamrosenfield.wordswithcrosses.net.AbstractDownloader.download(AbstractDownloader.java:108)
    at com.adamrosenfield.wordswithcrosses.net.Downloaders.download(Downloaders.java:257)
    at com.adamrosenfield.wordswithcrosses.BrowseActivity.internalDownload(BrowseActivity.java:702)
    at com.adamrosenfield.wordswithcrosses.BrowseActivity.access$6(BrowseActivity.java:696)
    at com.adamrosenfield.wordswithcrosses.BrowseActivity$7.run(BrowseActivity.java:691)
    at java.lang.Thread.run(Thread.java:856)
Caused by: libcore.io.ErrnoException: close failed: EIO (I/O error)
    at libcore.io.Posix.close(Native Method)
    at libcore.io.BlockGuardOs.close(BlockGuardOs.java:75)
    at libcore.io.IoUtils.close(IoUtils.java:38)
    ... 11 more

The relevant code:

public static void convertJPZPuzzle(File jpzFile, File destFile,
        PuzzleMetadataSetter metadataSetter) throws IOException {
    FileInputStream fis = new FileInputStream(jpzFile);
    try {
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(destFile));
        try {
            if (!convertJPZPuzzle(fis, dos, metadataSetter)) {
                throw new IOException("Failed to convert JPZ file: " + jpzFile);
            }
        } finally {
            dos.close();
        }
    } finally {
        fis.close();
    }
}

The full source is on GitHub.

The exception is being thrown from the line fis.close(). From what I can tell from reading the Android sources, it looks like FileInputStream.close() just calls down into close(2) on the underlying file descriptor in native code.

The manual pages don't seem to specify what can cause an EIO error, they just say things like "An I/O error occurred." or "If an I/O error occurred while reading from or writing to the file system during close()". The Mac OS X man pages say it can occur when "A previously-uncommitted write(2) encountered an input/output error." on those systems.

What exactly can cause close(2) to fail with the error EIO for a file descriptor which was only opened for reading, as in this case? Clearly it's not an uncommitted write(2). In the case of this particular file, it was downloaded using Android's DownloadManager service, which means there might be lingering threads and/or processes trying to access it simultaneously, but I can hardly see how that would affect trying to close it. Also, the file is just about to be deleted after this code runs (here), but unless Android has an undocumented time machine in it, future code ought not to have an affect here.

I'm interested specifically in the answer on Android and/or Linux, but a more general answer for other OSes would also be appreciated.

Adam Rosenfield
  • 390,455
  • 97
  • 512
  • 589
  • I couldn't get very far because I don't know `c` or `posix` systems. Related question and answer [here](http://stackoverflow.com/questions/7297001/if-close2-fails-with-eio-will-the-file-descriptor-still-be-deleted). Source code for `libcore.io` [here](https://android.googlesource.com/platform/libcore/+/9b510df35b57946d843ffc34cf23fdcfc84c5220/luni/src/main/java/libcore/io) and the `native` implementation of `close` (for the fd) [here](https://android.googlesource.com/platform/libcore/+/9b510df35b57946d843ffc34cf23fdcfc84c5220/luni/src/main/native/libcore_io_Posix.cpp). – Sotirios Delimanolis Sep 09 '13 at 23:55
  • In the last link, you'd have to find the implementation of `close()` on line 429. – Sotirios Delimanolis Sep 09 '13 at 23:55
  • @SotiriosDelimanolis: Yes, I saw that question—that discusses the state of the file descriptor after the `EIO` error; I'm interested in what *causes* the `EIO`, not what to do after the error occurs. The implementation of `close()` is a system call inside the Linux kernel. – Adam Rosenfield Sep 10 '13 at 00:00
  • It seems associated with a bad inode, but spelunking the kernel code is taking a little longer than I thought... – jxh Sep 10 '13 at 00:08
  • Same here. I don't see any way `close` can return with `EIO` except for exotic filesystems. What filesystem type is the file in question on? – R.. GitHub STOP HELPING ICE Sep 10 '13 at 00:10
  • @R..: I don't know for sure, since this error occurred on a different user's device, but it was on an SD card. My Android device's SD card is formattes as `vfat` (according to `mount(1)`), so I suspect that's what the user's SD card was likely formatted as. – Adam Rosenfield Sep 10 '13 at 00:24

2 Answers2

3

I'm guessing the EIO comes from bad_file_flush in fs/bad_inode.c. It seems when the kernel has any failure accessing an inode, it transforms the open file description to a pseudo-open-file of with bad_inode_ops as its file ops. I can't find the code that does this for FAT-based filesystems, but perhaps there's some generic code.

As for the reason, it's probably something like attaching a USB cable and mounting the filesystem from an attached computer, removing the SD card, etc.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • Since I unfortunately do not have access to my user's device, I won't really be able to know for sure what was going on, but until then, this seems like the most likely explanation. After poking around the kernel sources for a while, it seems that `sys_close()` can only return 0, `-EINTR`, `-EBADF`, or the return value from the file system's `flush()` implementation, and `vfat` doesn't implement `flush()`. – Adam Rosenfield Sep 10 '13 at 04:34
-1

In general, you should always anticipate IOExceptions when closing streams. The code is very straightforward, but see here for the cleanest example Java can afford:

https://stackoverflow.com/a/156520/1489860

However, in your specific case, I imagine an exception is being thrown because it appears you are changing the value of the InputStream in the unzipOrPassthrough(InputStream) method and then later trying to close it:

        if (entry == null) {
        is = new ByteArrayInputStream(baos.toByteArray());

When you later call close on the FileInputStream class, it probably freaks out because it is now a ByteArrayInputStream and no longer a FileInputStream.

Community
  • 1
  • 1
Jeffrey Mixon
  • 12,846
  • 4
  • 32
  • 55
  • 1
    It doesn't even make sense. If it is now a `ByteArrayInputStream` *object,* he *can't* 'later call close on the `FileInputStream` class'. He is closing the `ByteArrayInputStream`. Period. The part about 'now a `ByteArrayInputStream` and no longer a `FileInputStream` is nonsense. The reference is of type `InputStream.` References to objects can't remember what type of object they *were* pointing to. – user207421 Sep 10 '13 at 00:20
  • `unzipOrPassthrough()` reads from the passed in input stream, but it doesn't close it. It does reassign the local parameter `is`, but that has no effect on the original stream passed in. – Adam Rosenfield Sep 10 '13 at 00:31