/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package libcore.java.nio.channels; import junit.framework.TestCase; import android.system.OsConstants; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InterruptedIOException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import libcore.io.Libcore; import static libcore.io.IoUtils.closeQuietly; /** * A test for file interrupt behavior. Because forcing a real file to block on read or write is * difficult this test uses Unix FIFO / Named Pipes. FIFOs appear to Java as files but the test * has more control over the available data. Reader will block until the other end writes, and * writers can also be made to block. * * <p>Using FIFOs has a few drawbacks: * <ol> * <li>FIFOs are not supported from Java or the command-line on Android, so this test includes * native code to create the FIFO. * <li>FIFOs will not open() until there is both a reader and a writer of the FIFO; each test must * always attach both ends or experience a blocked test. * <li>FIFOs are not supported on some file systems. e.g. VFAT, so the test has to be particular * about the temporary directory it uses to hold the FIFO. * <li>Writes to FIFOs are buffered by the OS which makes blocking behavior more difficult to * induce. See {@link ChannelWriter} and {@link StreamWriter}. * </ol> */ public class FileIOInterruptTest extends TestCase { private static File VOGAR_DEVICE_TEMP_DIR = new File("/data/data/file_io_interrupt_test"); private File fifoFile; @Override public void setUp() throws Exception { super.setUp(); // This test relies on a FIFO file. The file system must support FIFOs, so we check the path. String tmpDirName = System.getProperty("java.io.tmpdir"); File tmpDir; if (tmpDirName.startsWith("/sdcard")) { // Vogar execution on device runs in /sdcard. Unfortunately the file system used does not // support FIFOs so the test must use one that is more likely to work. if (!VOGAR_DEVICE_TEMP_DIR.exists()) { assertTrue(VOGAR_DEVICE_TEMP_DIR.mkdir()); } VOGAR_DEVICE_TEMP_DIR.deleteOnExit(); tmpDir = VOGAR_DEVICE_TEMP_DIR; } else { tmpDir = new File(tmpDirName); } fifoFile = new File(tmpDir, "fifo_file.tmp"); if (fifoFile.exists()) { fifoFile.delete(); } fifoFile.deleteOnExit(); // Create the fifo. This will throw an exception if the file system does not support it. Libcore.os.mkfifo(fifoFile.getAbsolutePath(), OsConstants.S_IRWXU); } @Override public void tearDown() throws Exception { super.tearDown(); fifoFile.delete(); VOGAR_DEVICE_TEMP_DIR.delete(); // Clear the interrupted state, if set. Thread.interrupted(); } public void testStreamRead_exceptionWhenAlreadyClosed() throws Exception { FifoWriter fifoWriter = new FifoWriter(fifoFile); fifoWriter.start(); FileInputStream fis = new FileInputStream(fifoFile); fis.close(); byte[] buffer = new byte[10]; try { fis.read(buffer); fail(); } catch (IOException expected) { assertSame(IOException.class, expected.getClass()); } fifoWriter.tidyUp(); } // This test fails on the RI: close() does not wake up a blocking FileInputStream.read() call. public void testStreamRead_exceptionOnCloseWhenBlocked() throws Exception { FifoWriter fifoWriter = new FifoWriter(fifoFile); fifoWriter.start(); FileInputStream fis = new FileInputStream(fifoFile); StreamReader streamReader = new StreamReader(fis); Thread streamReaderThread = createAndStartThread("StreamReader", streamReader); // Delay until we can be fairly sure the reader thread is blocking. streamReader.waitForThreadToBlock(); // Now close the OutputStream to see what happens. fis.close(); // Test for expected behavior in the reader thread. waitToDie(streamReaderThread); assertSame(InterruptedIOException.class, streamReader.ioe.getClass()); assertFalse(streamReader.wasInterrupted); // Tidy up the writer thread. fifoWriter.tidyUp(); } public void testStreamWrite_exceptionWhenAlreadyClosed() throws Exception { FifoReader fifoReader = new FifoReader(fifoFile); fifoReader.start(); FileOutputStream fos = new FileOutputStream(fifoFile); byte[] buffer = new byte[10]; fos.close(); try { fos.write(buffer); fail(); } catch (IOException expected) { assertSame(IOException.class, expected.getClass()); } fifoReader.tidyUp(); } // This test fails on the RI: close() does not wake up a blocking FileInputStream.write() call. public void testStreamWrite_exceptionOnCloseWhenBlocked() throws Exception { FifoReader fifoReader = new FifoReader(fifoFile); fifoReader.start(); FileOutputStream fos = new FileOutputStream(fifoFile); StreamWriter streamWriter = new StreamWriter(fos); Thread streamWriterThread = createAndStartThread("StreamWriter", streamWriter); // Delay until we can be fairly sure the writer thread is blocking. streamWriter.waitForThreadToBlock(); // Now close the OutputStream to see what happens. fos.close(); // Test for expected behavior in the writer thread. waitToDie(streamWriterThread); assertSame(InterruptedIOException.class, streamWriter.ioe.getClass()); assertFalse(streamWriter.wasInterrupted); // Tidy up the reader thread. fifoReader.tidyUp(); } public void testChannelRead_exceptionWhenAlreadyClosed() throws Exception { testChannelRead_exceptionWhenAlreadyClosed(ChannelReader.Method.READ); } public void testChannelReadV_exceptionWhenAlreadyClosed() throws Exception { testChannelRead_exceptionWhenAlreadyClosed(ChannelReader.Method.READV); } private void testChannelRead_exceptionWhenAlreadyClosed(ChannelReader.Method method) throws Exception { FifoWriter fifoWriter = new FifoWriter(fifoFile); fifoWriter.start(); FileInputStream fis = new FileInputStream(fifoFile); FileChannel fileInputChannel = fis.getChannel(); fileInputChannel.close(); ByteBuffer buffer = ByteBuffer.allocateDirect(10); try { if (method == ChannelReader.Method.READ) { fileInputChannel.read(buffer); } else { ByteBuffer buffer2 = ByteBuffer.allocateDirect(10); fileInputChannel.read(new ByteBuffer[] { buffer, buffer2}); } fail(); } catch (IOException expected) { assertSame(ClosedChannelException.class, expected.getClass()); } fifoWriter.tidyUp(); } public void testChannelRead_exceptionWhenAlreadyInterrupted() throws Exception { testChannelRead_exceptionWhenAlreadyInterrupted(ChannelReader.Method.READ); } public void testChannelReadV_exceptionWhenAlreadyInterrupted() throws Exception { testChannelRead_exceptionWhenAlreadyInterrupted(ChannelReader.Method.READV); } private void testChannelRead_exceptionWhenAlreadyInterrupted(ChannelReader.Method method) throws Exception { FifoWriter fifoWriter = new FifoWriter(fifoFile); fifoWriter.start(); FileInputStream fis = new FileInputStream(fifoFile); FileChannel fileInputChannel = fis.getChannel(); Thread.currentThread().interrupt(); ByteBuffer buffer = ByteBuffer.allocateDirect(10); try { if (method == ChannelReader.Method.READ) { fileInputChannel.read(buffer); } else { ByteBuffer buffer2 = ByteBuffer.allocateDirect(10); fileInputChannel.read(new ByteBuffer[] { buffer, buffer2}); } fail(); } catch (IOException expected) { assertSame(ClosedByInterruptException.class, expected.getClass()); } // Check but also clear the interrupted status, so we can wait for the FifoWriter thread in // tidyUp(). assertTrue(Thread.interrupted()); fifoWriter.tidyUp(); } public void testChannelRead_exceptionOnCloseWhenBlocked() throws Exception { testChannelRead_exceptionOnCloseWhenBlocked(ChannelReader.Method.READ); } public void testChannelReadV_exceptionOnCloseWhenBlocked() throws Exception { testChannelRead_exceptionOnCloseWhenBlocked(ChannelReader.Method.READV); } private void testChannelRead_exceptionOnCloseWhenBlocked(ChannelReader.Method method) throws Exception { FifoWriter fifoWriter = new FifoWriter(fifoFile); fifoWriter.start(); FileInputStream fis = new FileInputStream(fifoFile); FileChannel fileInputChannel = fis.getChannel(); ChannelReader channelReader = new ChannelReader(fileInputChannel, method); Thread channelReaderThread = createAndStartThread("ChannelReader", channelReader); // Delay until we can be fairly sure the reader thread is blocking. channelReader.waitForThreadToBlock(); // Now close the FileChannel to see what happens. fileInputChannel.close(); // Test for expected behavior in the reader thread. waitToDie(channelReaderThread); assertSame(AsynchronousCloseException.class, channelReader.ioe.getClass()); assertFalse(channelReader.wasInterrupted); // Tidy up the writer thread. fifoWriter.tidyUp(); } public void testChannelRead_exceptionOnInterrupt() throws Exception { testChannelRead_exceptionOnInterrupt(ChannelReader.Method.READ); } public void testChannelReadV_exceptionOnInterrupt() throws Exception { testChannelRead_exceptionOnInterrupt(ChannelReader.Method.READV); } private void testChannelRead_exceptionOnInterrupt(ChannelReader.Method method) throws Exception { FifoWriter fifoWriter = new FifoWriter(fifoFile); fifoWriter.start(); FileChannel fileChannel = new FileInputStream(fifoFile).getChannel(); ChannelReader channelReader = new ChannelReader(fileChannel, method); Thread channelReaderThread = createAndStartThread("ChannelReader", channelReader); // Delay until we can be fairly sure the reader thread is blocking. channelReader.waitForThreadToBlock(); // Now interrupt the reader thread to see what happens. channelReaderThread.interrupt(); // Test for expected behavior in the reader thread. waitToDie(channelReaderThread); assertSame(ClosedByInterruptException.class, channelReader.ioe.getClass()); assertTrue(channelReader.wasInterrupted); // Tidy up the writer thread. fifoWriter.tidyUp(); } public void testChannelWrite_exceptionWhenAlreadyClosed() throws Exception { testChannelWrite_exceptionWhenAlreadyClosed(ChannelWriter.Method.WRITE); } public void testChannelWriteV_exceptionWhenAlreadyClosed() throws Exception { testChannelWrite_exceptionWhenAlreadyClosed(ChannelWriter.Method.WRITEV); } private void testChannelWrite_exceptionWhenAlreadyClosed(ChannelWriter.Method method) throws Exception { FifoReader fifoReader = new FifoReader(fifoFile); fifoReader.start(); FileChannel fileOutputChannel = new FileOutputStream(fifoFile).getChannel(); fileOutputChannel.close(); ByteBuffer buffer = ByteBuffer.allocateDirect(10); try { if (method == ChannelWriter.Method.WRITE) { fileOutputChannel.write(buffer); } else { ByteBuffer buffer2 = ByteBuffer.allocateDirect(10); fileOutputChannel.write(new ByteBuffer[] { buffer, buffer2 }); } fail(); } catch (IOException expected) { assertSame(ClosedChannelException.class, expected.getClass()); } fifoReader.tidyUp(); } public void testChannelWrite_exceptionWhenAlreadyInterrupted() throws Exception { testChannelWrite_exceptionWhenAlreadyInterrupted(ChannelWriter.Method.WRITE); } public void testChannelWriteV_exceptionWhenAlreadyInterrupted() throws Exception { testChannelWrite_exceptionWhenAlreadyInterrupted(ChannelWriter.Method.WRITEV); } private void testChannelWrite_exceptionWhenAlreadyInterrupted(ChannelWriter.Method method) throws Exception { FifoReader fifoReader = new FifoReader(fifoFile); fifoReader.start(); FileOutputStream fos = new FileOutputStream(fifoFile); FileChannel fileInputChannel = fos.getChannel(); Thread.currentThread().interrupt(); ByteBuffer buffer = ByteBuffer.allocateDirect(10); try { if (method == ChannelWriter.Method.WRITE) { fileInputChannel.write(buffer); } else { ByteBuffer buffer2 = ByteBuffer.allocateDirect(10); fileInputChannel.write(new ByteBuffer[] { buffer, buffer2 }); } fail(); } catch (IOException expected) { assertSame(ClosedByInterruptException.class, expected.getClass()); } // Check but also clear the interrupted status, so we can wait for the FifoReader thread in // tidyUp(). assertTrue(Thread.interrupted()); fifoReader.tidyUp(); } public void testChannelWrite_exceptionOnCloseWhenBlocked() throws Exception { testChannelWrite_exceptionOnCloseWhenBlocked(ChannelWriter.Method.WRITE); } public void testChannelWriteV_exceptionOnCloseWhenBlocked() throws Exception { testChannelWrite_exceptionOnCloseWhenBlocked(ChannelWriter.Method.WRITEV); } private void testChannelWrite_exceptionOnCloseWhenBlocked(ChannelWriter.Method method) throws Exception { FifoReader fifoReader = new FifoReader(fifoFile); fifoReader.start(); FileChannel fileOutputChannel = new FileOutputStream(fifoFile).getChannel(); ChannelWriter channelWriter = new ChannelWriter(fileOutputChannel, method); Thread channelWriterThread = createAndStartThread("ChannelWriter", channelWriter); // Delay until we can be fairly sure the writer thread is blocking. channelWriter.waitForThreadToBlock(); // Now close the channel to see what happens. fileOutputChannel.close(); // Test for expected behavior in the writer thread. waitToDie(channelWriterThread); // The RI throws ChannelClosedException. AsynchronousCloseException is more correct according to // the docs. assertSame(AsynchronousCloseException.class, channelWriter.ioe.getClass()); assertFalse(channelWriter.wasInterrupted); // Tidy up the writer thread. fifoReader.tidyUp(); } public void testChannelWrite_exceptionOnInterrupt() throws Exception { testChannelWrite_exceptionOnInterrupt(ChannelWriter.Method.WRITE); } public void testChannelWriteV_exceptionOnInterrupt() throws Exception { testChannelWrite_exceptionOnInterrupt(ChannelWriter.Method.WRITEV); } private void testChannelWrite_exceptionOnInterrupt(ChannelWriter.Method method) throws Exception { FifoReader fifoReader = new FifoReader(fifoFile); fifoReader.start(); FileChannel fileChannel = new FileOutputStream(fifoFile).getChannel(); ChannelWriter channelWriter = new ChannelWriter(fileChannel, method); Thread channelWriterThread = createAndStartThread("ChannelWriter", channelWriter); // Delay until we can be fairly sure the writer thread is blocking. channelWriter.waitForThreadToBlock(); // Now interrupt the writer thread to see what happens. channelWriterThread.interrupt(); // Test for expected behavior in the writer thread. waitToDie(channelWriterThread); assertSame(ClosedByInterruptException.class, channelWriter.ioe.getClass()); assertTrue(channelWriter.wasInterrupted); // Tidy up the reader thread. fifoReader.tidyUp(); } private static class StreamReader implements Runnable { private final FileInputStream inputStream; volatile boolean started; volatile IOException ioe; volatile boolean wasInterrupted; StreamReader(FileInputStream inputStream) { this.inputStream = inputStream; } @Override public void run() { byte[] buffer = new byte[10]; try { started = true; int bytesRead = inputStream.read(buffer); fail("This isn't supposed to happen: read() returned: " + bytesRead); } catch (IOException e) { this.ioe = e; } wasInterrupted = Thread.interrupted(); } public void waitForThreadToBlock() { for (int i = 0; i < 10 && !started; i++) { delay(100); } assertTrue(started); // Just give it some more time to start blocking. delay(100); } } private static class StreamWriter implements Runnable { private final FileOutputStream outputStream; volatile int bytesWritten; volatile IOException ioe; volatile boolean wasInterrupted; StreamWriter(FileOutputStream outputStream) { this.outputStream = outputStream; } @Override public void run() { // Writes to FIFOs are buffered. We try to fill the buffer and induce blocking (the // buffer is typically 64k). byte[] buffer = new byte[10000]; while (true) { try { outputStream.write(buffer); bytesWritten += buffer.length; } catch (IOException e) { this.ioe = e; break; } wasInterrupted = Thread.interrupted(); } } public void waitForThreadToBlock() { int lastCount = bytesWritten; for (int i = 0; i < 10; i++) { delay(500); int newBytesWritten = bytesWritten; if (newBytesWritten > 0 && lastCount == newBytesWritten) { // The thread is probably blocking. return; } lastCount = bytesWritten; } fail("Writer never started blocking. Bytes written: " + bytesWritten); } } private static class ChannelReader implements Runnable { enum Method { READ, READV, } private final FileChannel channel; private final Method method; volatile boolean started; volatile IOException ioe; volatile boolean wasInterrupted; ChannelReader(FileChannel channel, Method method) { this.channel = channel; this.method = method; } @Override public void run() { ByteBuffer buffer = ByteBuffer.allocateDirect(10); try { started = true; if (method == Method.READ) { channel.read(buffer); } else { ByteBuffer buffer2 = ByteBuffer.allocateDirect(10); channel.read(new ByteBuffer[] { buffer, buffer2 }); } fail("All tests should block until an exception"); } catch (IOException e) { this.ioe = e; } wasInterrupted = Thread.interrupted(); } public void waitForThreadToBlock() { for (int i = 0; i < 10 && !started; i++) { delay(100); } assertTrue(started); // Just give it some more time to start blocking. delay(100); } } private static class ChannelWriter implements Runnable { enum Method { WRITE, WRITEV, } private final FileChannel channel; private final Method method; volatile int bytesWritten; volatile IOException ioe; volatile boolean wasInterrupted; ChannelWriter(FileChannel channel, Method method) { this.channel = channel; this.method = method; } @Override public void run() { ByteBuffer buffer1 = ByteBuffer.allocateDirect(10000); ByteBuffer buffer2 = ByteBuffer.allocateDirect(10000); // Writes to FIFOs are buffered. We try to fill the buffer and induce blocking (the // buffer is typically 64k). while (true) { // Make the buffers look non-empty. buffer1.position(0).limit(buffer1.capacity()); buffer2.position(0).limit(buffer2.capacity()); try { if (method == Method.WRITE) { bytesWritten += channel.write(buffer1); } else { bytesWritten += channel.write(new ByteBuffer[]{ buffer1, buffer2 }); } } catch (IOException e) { this.ioe = e; break; } } wasInterrupted = Thread.interrupted(); } public void waitForThreadToBlock() { int lastCount = bytesWritten; for (int i = 0; i < 10; i++) { delay(500); int newBytesWritten = bytesWritten; if (newBytesWritten > 0 && lastCount == newBytesWritten) { // The thread is probably blocking. return; } lastCount = bytesWritten; } fail("Writer never started blocking. Bytes written: " + bytesWritten); } } /** * Opens a FIFO for writing. Exists to unblock the other end of the FIFO. */ private static class FifoWriter extends Thread { private final File file; private FileOutputStream fos; public FifoWriter(File file) { super("FifoWriter"); this.file = file; } @Override public void run() { try { fos = new FileOutputStream(file); } catch (IOException ignored) { } } public void tidyUp() { FileIOInterruptTest.waitToDie(this); closeQuietly(fos); } } /** * Opens a FIFO for reading. Exists to unblock the other end of the FIFO. */ private static class FifoReader extends Thread { private final File file; private FileInputStream fis; public FifoReader(File file) { super("FifoReader"); this.file = file; } @Override public void run() { try { fis = new FileInputStream(file); } catch (IOException ignored) { } } public void tidyUp() { FileIOInterruptTest.waitToDie(this); closeQuietly(fis); } } private static Thread createAndStartThread(String name, Runnable runnable) { Thread t = new Thread(runnable, name); t.setDaemon(true); t.start(); return t; } private static void waitToDie(Thread thread) { // Protect against this thread already being interrupted, which would prevent the test waiting // for the requested time. assertFalse(Thread.currentThread().isInterrupted()); try { thread.join(5000); } catch (InterruptedException ignored) { } if (thread.isAlive()) { fail("Thread \"" + thread.getName() + "\" did not exit."); } } private static void delay(int millis) { // Protect against this thread being interrupted, which would prevent us waiting. assertFalse(Thread.currentThread().isInterrupted()); try { Thread.sleep(millis); } catch (InterruptedException ignored) { } } }