WindowsFileSystem: open files with delete-sharing

Open files using `Files.new{Input,Output}Stream`
instead of `new File{Input,Output}Stream`.

The new method opens files with deletion sharing,
the old one does not.

This patch will hopefully fix the class of errors
where Bazel fails to delete a file that's already
open.

Change-Id: I1a841ea6ba644ac09dde4838007ba1ecab46defb

Closes #6092.

Change-Id: I1a841ea6ba644ac09dde4838007ba1ecab46defb
PiperOrigin-RevId: 211976021
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
index 771e073..16848c2 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
@@ -56,7 +56,7 @@
   }
 
   /** Returns either normal or profiled FileInputStream. */
-  private InputStream createFileInputStream(Path path) throws FileNotFoundException {
+  private InputStream createFileInputStream(Path path) throws IOException {
     final String name = path.toString();
     if (profiler.isActive()
         && (profiler.isProfiling(ProfilerTask.VFS_READ)
@@ -64,34 +64,41 @@
       long startTime = Profiler.nanoTimeMaybe();
       try {
         // Replace default FileInputStream instance with the custom one that does profiling.
-        return new ProfiledFileInputStream(name);
+        return new ProfiledFileInputStream(name, newFileInputStream(name));
       } finally {
         profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
       }
     } else {
       // Use normal FileInputStream instance if profiler is not enabled.
-      return new FileInputStream(path.toString());
+      return newFileInputStream(name);
     }
   }
 
+  protected InputStream newFileInputStream(String path) throws IOException {
+    return new FileInputStream(path);
+  }
+
+  protected OutputStream newFileOutputStream(String path, boolean append) throws IOException {
+    return new FileOutputStream(path, append);
+  }
+
   /**
    * Returns either normal or profiled FileOutputStream. Should be used by subclasses to create
    * default OutputStream instance.
    */
-  protected OutputStream createFileOutputStream(Path path, boolean append)
-      throws FileNotFoundException {
+  protected OutputStream createFileOutputStream(Path path, boolean append) throws IOException {
     final String name = path.toString();
     if (profiler.isActive()
         && (profiler.isProfiling(ProfilerTask.VFS_WRITE)
             || profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
       long startTime = Profiler.nanoTimeMaybe();
       try {
-        return new ProfiledFileOutputStream(name, append);
+        return new ProfiledFileOutputStream(name, newFileOutputStream(name, append));
       } finally {
         profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
       }
     } else {
-      return new FileOutputStream(name, append);
+      return newFileOutputStream(name, append);
     }
   }
 
@@ -110,12 +117,33 @@
     }
   }
 
-  private static final class ProfiledFileInputStream extends FileInputStream {
+  private static final class ProfiledFileInputStream extends InputStream {
     private final String name;
+    private final InputStream stm;
 
-    public ProfiledFileInputStream(String name) throws FileNotFoundException {
-      super(name);
+    public ProfiledFileInputStream(String name, InputStream stm) {
       this.name = name;
+      this.stm = stm;
+    }
+
+    @Override
+    public int available() throws IOException {
+      return stm.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+      stm.close();
+    }
+
+    @Override
+    public void mark(int readlimit) {
+      stm.mark(readlimit);
+    }
+
+    @Override
+    public boolean markSupported() {
+      return stm.markSupported();
     }
 
     @Override
@@ -124,7 +152,7 @@
       try {
         // Note that FileInputStream#read() does *not* call any of our overridden methods,
         // so there's no concern with double counting here.
-        return super.read();
+        return stm.read();
       } finally {
         profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
       }
@@ -139,19 +167,40 @@
     public int read(byte[] b, int off, int len) throws IOException {
       long startTime = Profiler.nanoTimeMaybe();
       try {
-        return super.read(b, off, len);
+        return stm.read(b, off, len);
       } finally {
         profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
       }
     }
+
+    @Override
+    public void reset() throws IOException {
+      stm.reset();
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+      return stm.skip(n);
+    }
   }
 
-  private static final class ProfiledFileOutputStream extends FileOutputStream {
+  private static final class ProfiledFileOutputStream extends OutputStream {
     private final String name;
+    private final OutputStream stm;
 
-    public ProfiledFileOutputStream(String name, boolean append) throws FileNotFoundException {
-      super(name, append);
+    public ProfiledFileOutputStream(String name, OutputStream stm) {
       this.name = name;
+      this.stm = stm;
+    }
+
+    @Override
+    public void close() throws IOException {
+      stm.close();
+    }
+
+    @Override
+    public void flush() throws IOException {
+      stm.flush();
     }
 
     @Override
@@ -163,7 +212,17 @@
     public void write(byte[] b, int off, int len) throws IOException {
       long startTime = Profiler.nanoTimeMaybe();
       try {
-        super.write(b, off, len);
+        stm.write(b, off, len);
+      } finally {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
+      }
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+      long startTime = Profiler.nanoTimeMaybe();
+      try {
+        stm.write(b);
       } finally {
         profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
       }
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
index d39173a..c1249cb 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
@@ -27,8 +27,12 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.DosFileAttributes;
 
 /** File system implementation for Windows. */
@@ -51,6 +55,22 @@
     return "ntfs";
   }
 
+  private static java.nio.file.Path getNioPath(String path) {
+    return Paths.get(path);
+  }
+
+  @Override
+  protected InputStream newFileInputStream(String path) throws IOException {
+    return Files.newInputStream(getNioPath(path));
+  }
+
+  @Override
+  protected OutputStream newFileOutputStream(String path, boolean append) throws IOException {
+    return append
+        ? Files.newOutputStream(getNioPath(path), StandardOpenOption.APPEND)
+        : Files.newOutputStream(getNioPath(path));
+  }
+
   @Override
   public boolean delete(Path path) throws IOException {
     long startTime = Profiler.nanoTimeMaybe();
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java
index cdcb22e..f34980c 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java
@@ -23,6 +23,8 @@
 import com.google.devtools.build.lib.testutil.ManualClock;
 import com.google.devtools.build.lib.testutil.TestUtils;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Paths;
@@ -136,4 +138,46 @@
     }
     return subDirs;
   }
+
+  @Test
+  public void testDeleteFileOpenForReading() throws Exception {
+    Path path = absolutize("foo.txt");
+    FileSystemUtils.writeIsoLatin1(path, "foo");
+    // Sanity check: attempt reading from the file.
+    try (InputStream is = path.getInputStream()) {
+      byte[] contents = new byte[3];
+      assertThat(is.read(contents)).isEqualTo(3);
+      assertThat(contents).isEqualTo(new byte[] {'f', 'o', 'o'});
+    }
+    // Actual test: attempt deleting the file while open, then reading from it.
+    try (InputStream is = path.getInputStream()) {
+      assertThat(path.delete()).isTrue();
+      assertThat(path.exists()).isFalse();
+
+      byte[] contents = new byte[3];
+      assertThat(is.read(contents)).isEqualTo(3);
+      assertThat(contents).isEqualTo(new byte[] {'f', 'o', 'o'});
+    }
+  }
+
+  @Test
+  public void testDeleteFileOpenForWriting() throws Exception {
+    Path path = absolutize("foo.txt");
+    // Sanity check: attempt writing to the file.
+    assertThat(path.exists()).isFalse();
+    try (OutputStream os = path.getOutputStream(/* append */ false)) {
+      os.write(new byte[] {'b', 'a', 'r'});
+    }
+    try (InputStream is = path.getInputStream()) {
+      byte[] contents = new byte[3];
+      assertThat(is.read(contents)).isEqualTo(3);
+      assertThat(contents).isEqualTo(new byte[] {'b', 'a', 'r'});
+    }
+    // Actual test: attempt deleting the file while open, then writing to it.
+    try (OutputStream os = path.getOutputStream(/* append */ false)) {
+      assertThat(path.delete()).isTrue();
+      assertThat(path.exists()).isFalse();
+      os.write(new byte[] {'b', 'a', 'r'});
+    }
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
index 89303fd..861e4af 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
@@ -31,6 +31,9 @@
 import com.google.devtools.build.lib.windows.util.WindowsTestUtil;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.AccessDeniedException;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -332,4 +335,58 @@
       assertThat(p.isFile()).isTrue();
     }
   }
+
+  @Test
+  public void testDeleteFileOpenForReading() throws Exception {
+    testUtil.scratchFile("foo.txt", "foo");
+    Path path = testUtil.createVfsPath(fs, "foo.txt");
+    // Sanity check: attempt reading from the file.
+    try (InputStream is = path.getInputStream()) {
+      byte[] contents = new byte[3];
+      assertThat(is.read(contents)).isEqualTo(3);
+      assertThat(contents).isEqualTo(new byte[] {'f', 'o', 'o'});
+    }
+    // Actual test: attempt deleting the file while open, then reading from it.
+    try (InputStream is = path.getInputStream()) {
+      assertThat(path.delete()).isTrue();
+      assertThat(path.exists()).isFalse();
+
+      byte[] contents = new byte[3];
+      assertThat(is.read(contents)).isEqualTo(3);
+      assertThat(contents).isEqualTo(new byte[] {'f', 'o', 'o'});
+    }
+  }
+
+  @Test
+  public void testDeleteFileOpenForWriting() throws Exception {
+    testUtil.scratchFile("bar.txt", "bar");
+    Path path = testUtil.createVfsPath(fs, "bar.txt");
+    // Sanity check: attempt writing to the file.
+    try (InputStream is = path.getInputStream()) {
+      byte[] contents = new byte[3];
+      assertThat(is.read(contents)).isEqualTo(3);
+      assertThat(contents).isEqualTo(new byte[] {'b', 'a', 'r'});
+    }
+    // Actual test: attempt deleting the file while open, then writing to it.
+    try (OutputStream os = path.getOutputStream(/* append */ false)) {
+      assertThat(path.delete()).isTrue();
+      assertThat(path.exists()).isFalse();
+      os.write(new byte[] {'b', 'a', 'r'});
+    }
+  }
+
+  @Test
+  public void testCannotOpenDirectoryAsFile() throws Exception {
+    testUtil.scratchFile("foo/bar.txt", "bar");
+    Path file = testUtil.createVfsPath(fs, "foo/bar.txt");
+    Path dir = file.getParentDirectory();
+    // Sanity check: we can open the file.
+    try (InputStream is = file.getInputStream()) {}
+    assertThat(dir.isDirectory()).isTrue();
+    try (InputStream is = dir.getInputStream()) {
+      fail("Expected failure");
+    } catch (IOException e) {
+      assertThat(e).isInstanceOf(AccessDeniedException.class);
+    }
+  }
 }