diff --git a/jfuse-api/pom.xml b/jfuse-api/pom.xml index 3dc7fbc0..6e4f7489 100644 --- a/jfuse-api/pom.xml +++ b/jfuse-api/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse-api diff --git a/jfuse-api/src/main/java/org/cryptomator/jfuse/api/Fuse.java b/jfuse-api/src/main/java/org/cryptomator/jfuse/api/Fuse.java index 975af65f..61f6ced4 100644 --- a/jfuse-api/src/main/java/org/cryptomator/jfuse/api/Fuse.java +++ b/jfuse-api/src/main/java/org/cryptomator/jfuse/api/Fuse.java @@ -16,8 +16,10 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -105,7 +107,11 @@ public static FuseBuilder builder() { */ @Blocking @MustBeInvokedByOverriders - public void mount(String progName, Path mountPoint, String... flags) throws FuseMountFailedException, IllegalArgumentException { + public synchronized void mount(String progName, Path mountPoint, String... flags) throws FuseMountFailedException, IllegalArgumentException { + if (!fuseScope.isAlive()) { + throw new IllegalStateException("Already closed"); //TODO: throw specialized exception + } + FuseMount lock = new UnmountedFuseMount(); if (!mount.compareAndSet(UNMOUNTED, lock)) { throw new IllegalStateException("Already mounted"); @@ -119,19 +125,24 @@ public void mount(String progName, Path mountPoint, String... flags) throws Fuse try { var fuseMount = this.mount(args); - executor.submit(() -> fuseLoop(fuseMount)); // TODO keep reference of future and report result - waitForMountingToComplete(mountPoint); + Future fuseLoop = executor.submit(() -> fuseLoop(fuseMount)); + waitForMountingToComplete(mountPoint, fuseLoop); + if (fuseLoop.isDone()) { + throw new FuseMountFailedException("fuse_loop() returned prematurely with non-zero exit code " + fuseLoop.get()); + } mount.compareAndSet(lock, fuseMount); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new FuseMountFailedException("Interrupted while waiting for mounting to finish"); + } catch (ExecutionException e) { + throw new FuseMountFailedException("Exception when starting fuse_loop. Message: " + e.getCause().getMessage()); } finally { mount.compareAndSet(lock, UNMOUNTED); // if value is still `lock`, mount has failed. } } @VisibleForTesting - void waitForMountingToComplete(Path mountPoint) throws InterruptedException { + void waitForMountingToComplete(Path mountPoint, Future fuseLoop) throws InterruptedException { var probe = Files.getFileAttributeView(mountPoint.resolve(MOUNT_PROBE.substring(1)), BasicFileAttributeView.class); do { try { @@ -139,7 +150,7 @@ void waitForMountingToComplete(Path mountPoint) throws InterruptedException { } catch (IOException e) { // noop } - } while (!mountProbeSucceeded.await(200, TimeUnit.MILLISECONDS)); + } while (!fuseLoop.isDone() && !mountProbeSucceeded.await(200, TimeUnit.MILLISECONDS)); } @Blocking diff --git a/jfuse-api/src/test/java/org/cryptomator/jfuse/api/FuseTest.java b/jfuse-api/src/test/java/org/cryptomator/jfuse/api/FuseTest.java index 2fb864d8..fa1ea532 100644 --- a/jfuse-api/src/test/java/org/cryptomator/jfuse/api/FuseTest.java +++ b/jfuse-api/src/test/java/org/cryptomator/jfuse/api/FuseTest.java @@ -13,16 +13,18 @@ import java.nio.file.spi.FileSystemProvider; import java.time.Duration; import java.util.List; +import java.util.concurrent.Future; public class FuseTest { - private FuseOperations fuseOps = Mockito.mock(FuseOperations.class); - private Fuse fuse = Mockito.spy(new FuseStub(fuseOps)); + private final FuseOperations fuseOps = Mockito.mock(FuseOperations.class); + private final FuseMount fuseMount = Mockito.spy(new FuseMountStub()); + private final Fuse fuse = Mockito.spy(new FuseStub(fuseMount, fuseOps)); + private final Path mountPoint = Mockito.mock(Path.class, "/mnt"); @Test @DisplayName("waitForMountingToComplete() waits for getattr(\"/jfuse_mount_probe\")") public void testWaitForMountingToComplete() throws IOException { - Path mountPoint = Mockito.mock(Path.class, "/mnt"); Path probePath = Mockito.mock(Path.class, "/mnt/jfuse_mount_probe"); FileSystem fs = Mockito.mock(FileSystem.class); FileSystemProvider fsProv = Mockito.mock(FileSystemProvider.class); @@ -39,16 +41,64 @@ public void testWaitForMountingToComplete() throws IOException { fuse.fuseOperations.getattr("/jfuse_mount_probe", Mockito.mock(Stat.class), Mockito.mock(FileInfo.class)); throw new NoSuchFileException("/mnt/jfuse_mount_probe still not found"); }).when(attrView).readAttributes(); + Future fuseLoop = Mockito.mock(Future.class); + Mockito.doReturn(false).when(fuseLoop).isDone(); - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> fuse.waitForMountingToComplete(mountPoint)); + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> fuse.waitForMountingToComplete(mountPoint, fuseLoop)); + Mockito.verify(fuseLoop, Mockito.atLeastOnce()).isDone(); + } - Mockito.verify(fuseOps).getattr(Mockito.eq("/jfuse_mount_probe"), Mockito.any(), Mockito.any()); + @Test + @DisplayName("waitForMountingToComplete() waits returns immediately if fuse_loop fails") + public void testPrematurelyFuseLoopReturn() throws IOException { + Path probePath = Mockito.mock(Path.class, "/mnt/jfuse_mount_probe"); + FileSystem fs = Mockito.mock(FileSystem.class); + FileSystemProvider fsProv = Mockito.mock(FileSystemProvider.class); + BasicFileAttributeView attrView = Mockito.mock(BasicFileAttributeView.class); + Mockito.doReturn(probePath).when(mountPoint).resolve("jfuse_mount_probe"); + Mockito.doReturn(fs).when(probePath).getFileSystem(); + Mockito.doReturn(fsProv).when(fs).provider(); + Mockito.doReturn(attrView).when(fsProv).getFileAttributeView(probePath, BasicFileAttributeView.class); + Future fuseLoop = Mockito.mock(Future.class); + Mockito.doReturn(true).when(fuseLoop).isDone(); + + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> fuse.waitForMountingToComplete(mountPoint, fuseLoop)); + Mockito.verify(fuseLoop, Mockito.atLeastOnce()).isDone(); + } + + @Test + @DisplayName("Already closed fuseMount throws IllegalStateException on mount") + public void testMountThrowsIllegalStateIfClosed() { + Assertions.assertDoesNotThrow(fuse::close); + Assertions.assertThrows(IllegalStateException.class, () -> fuse.mount("test3000", mountPoint)); + } + + @Test + @DisplayName("Already mounted fuseMount throws IllegalStateException on mount") + public void testMountThrowsIllegalStateIfAlreadyMounted() throws InterruptedException { + Mockito.doNothing().when(fuse).waitForMountingToComplete(Mockito.eq(mountPoint), Mockito.any()); + Assertions.assertDoesNotThrow(() -> fuse.mount("test3000", mountPoint)); + Assertions.assertThrows(IllegalStateException.class, () -> fuse.mount("test3000", mountPoint)); + } + + @Test + @DisplayName("If fuse_loop instantly returns with non-zero result, throw FuseMountFailedException") + public void testMountThrowsFuseMountFailedIfLoopReturnsNonZero() throws InterruptedException { + Mockito.doAnswer(invocation -> { + Thread.sleep(1000); + return null; + }).when(fuse).waitForMountingToComplete(Mockito.eq(mountPoint), Mockito.any()); + Mockito.doReturn(1).when(fuseMount).loop(); + Assertions.assertThrows(FuseMountFailedException.class, () -> fuse.mount("test3000", mountPoint)); } private static class FuseStub extends Fuse { - protected FuseStub(FuseOperations fuseOperations) { + FuseMount fuseMount; + + protected FuseStub(FuseMount mountStub, FuseOperations fuseOperations) { super(fuseOperations, allocator -> allocator.allocate(0L)); + this.fuseMount = mountStub; } @Override @@ -58,23 +108,23 @@ protected void bind(FuseOperations.Operation operation) { @Override protected FuseMount mount(List args) { - return new FuseMount() { - - @Override - public int loop() { - return 0; - } - - @Override - public void unmount() { - // no-op - } - - @Override - public void destroy() { - // no-op - } - }; + return fuseMount; + } + } + + private record FuseMountStub() implements FuseMount { + + @Override + public int loop() { + return 0; + } + + @Override + public void unmount() { + } + + @Override + public void destroy() { } } diff --git a/jfuse-examples/pom.xml b/jfuse-examples/pom.xml index be015173..15c100df 100644 --- a/jfuse-examples/pom.xml +++ b/jfuse-examples/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse-examples diff --git a/jfuse-linux-aarch64/pom.xml b/jfuse-linux-aarch64/pom.xml index 0aa75f6e..e35adde6 100644 --- a/jfuse-linux-aarch64/pom.xml +++ b/jfuse-linux-aarch64/pom.xml @@ -5,7 +5,7 @@ jfuse-parent org.cryptomator - 0.4.1 + 0.4.2 4.0.0 jfuse-linux-aarch64 diff --git a/jfuse-linux-amd64/pom.xml b/jfuse-linux-amd64/pom.xml index b124c530..9259d906 100644 --- a/jfuse-linux-amd64/pom.xml +++ b/jfuse-linux-amd64/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse-linux-amd64 diff --git a/jfuse-mac/pom.xml b/jfuse-mac/pom.xml index 77453e67..56c2c0a0 100644 --- a/jfuse-mac/pom.xml +++ b/jfuse-mac/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse-mac diff --git a/jfuse-tests/pom.xml b/jfuse-tests/pom.xml index 683a628a..6465b8bd 100644 --- a/jfuse-tests/pom.xml +++ b/jfuse-tests/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse-tests diff --git a/jfuse-win/pom.xml b/jfuse-win/pom.xml index d8707f30..2fc80a1a 100644 --- a/jfuse-win/pom.xml +++ b/jfuse-win/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse-win diff --git a/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java b/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java index 166860c9..b0504cea 100644 --- a/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java +++ b/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java @@ -30,7 +30,7 @@ public FuseImpl(FuseOperations fuseOperations) { } @Override - public void mount(String progName, Path mountPoint, String... flags) throws FuseMountFailedException { + public synchronized void mount(String progName, Path mountPoint, String... flags) throws FuseMountFailedException { var adjustedMP = mountPoint; if (mountPoint.equals(mountPoint.getRoot()) && mountPoint.isAbsolute()) { //winfsp accepts only drive letters written in drive relative notation diff --git a/jfuse/pom.xml b/jfuse/pom.xml index f72c304a..b826d6ef 100644 --- a/jfuse/pom.xml +++ b/jfuse/pom.xml @@ -5,7 +5,7 @@ org.cryptomator jfuse-parent - 0.4.1 + 0.4.2 4.0.0 jfuse diff --git a/pom.xml b/pom.xml index 3e8a0160..51300262 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.cryptomator jfuse-parent pom - 0.4.1 + 0.4.2 jFUSE Java bindings for FUSE using foreign functions & memory API https://github.com/cryptomator/jfuse