-
-
Notifications
You must be signed in to change notification settings - Fork 5
macOS: migrate to libfuse3 with FSKit support #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/macos-libfuse3
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,20 +2,29 @@ | |
|
|
||
| import org.cryptomator.jfuse.api.DirFiller; | ||
| import org.cryptomator.jfuse.api.Stat; | ||
| import org.cryptomator.jfuse.mac.extr.fuse3.fuse_darwin_attr; | ||
| import org.cryptomator.jfuse.mac.extr.fuse3.fuse_fill_dir_t; | ||
| import org.cryptomator.jfuse.mac.extr.fuse3.stat; | ||
|
|
||
| import java.lang.foreign.Arena; | ||
| import java.lang.foreign.MemorySegment; | ||
| import java.util.function.Consumer; | ||
|
|
||
| record DirFillerImpl(MemorySegment buf, MemorySegment callback, Arena arena) implements DirFiller { | ||
| record DirFillerImpl(MemorySegment buf, MemorySegment callback, Arena arena, boolean darwinExtensions) implements DirFiller { | ||
|
|
||
| @Override | ||
| public int fill(String name, Consumer<Stat> statFiller, long offset, int flags) { | ||
| var statSegment = stat.allocate(arena); | ||
| statFiller.accept(new StatImpl(statSegment)); | ||
| return fuse_fill_dir_t.invoke(callback, buf, arena.allocateFrom(name), statSegment, offset, flags); | ||
| MemorySegment segment; | ||
| Stat statWrapper; | ||
| if (darwinExtensions) { | ||
| segment = fuse_darwin_attr.allocate(arena); | ||
| statWrapper = new StatImpl(segment); | ||
| } else { | ||
| segment = stat.allocate(arena); | ||
| statWrapper = new StatCompatImpl(segment); | ||
| } | ||
|
Comment on lines
+19
to
+25
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we really need the compat version? since this impl is macOS-specific, can't we always use |
||
| statFiller.accept(statWrapper); | ||
| return fuse_fill_dir_t.invoke(callback, buf, arena.allocateFrom(name), segment, offset, flags); | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,9 @@ class FuseFunctions { | |
| private FuseFunctions() { | ||
| var lookup = SymbolLookup.loaderLookup(); | ||
| var linker = Linker.nativeLinker(); | ||
| this.fuse_parse_cmdline = lookup.find("fuse_parse_cmdline") | ||
| // Prefer fuse_parse_cmdline_312 (macFUSE 5.2.0+), fall back to fuse_parse_cmdline | ||
| this.fuse_parse_cmdline = lookup.find("fuse_parse_cmdline_312") | ||
| .or(() -> lookup.find("fuse_parse_cmdline")) | ||
|
Comment on lines
-36
to
+38
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. normally the unversioned method is an alias for the newest versioned method. We usually use versioned methods for the reverse case: When we want to explicitly use an older version. Are you sure this is correct? |
||
| .map(symbol -> linker.downcallHandle(symbol, PARSE_CMDLINE_DESCRIPTOR)) | ||
| .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol fuse_parse_cmdline")); | ||
| this.fuse_set_feature_flag = lookup.find("fuse_set_feature_flag") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| import org.cryptomator.jfuse.api.Fuse; | ||
| import org.cryptomator.jfuse.api.FuseConnInfo; | ||
| import org.cryptomator.jfuse.api.Stat; | ||
| import org.cryptomator.jfuse.api.FuseMount; | ||
| import org.cryptomator.jfuse.api.FuseMountFailedException; | ||
| import org.cryptomator.jfuse.api.FuseOperations; | ||
|
|
@@ -17,27 +18,44 @@ | |
| import java.lang.foreign.Arena; | ||
| import java.lang.foreign.MemorySegment; | ||
| import java.lang.foreign.ValueLayout; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| final class FuseImpl extends Fuse { | ||
|
|
||
| private volatile boolean darwinExtensions; | ||
|
|
||
| public FuseImpl(FuseOperations fuseOperations) { | ||
| super(fuseOperations, fuse_operations::allocate); | ||
| } | ||
|
|
||
| @Override | ||
| protected FuseMount mount(List<String> args) throws FuseMountFailedException { | ||
| var fuseArgs = parseArgs(args); | ||
| // macFUSE 5.2.0 crashes in fuse_darwin_mount if volname is not set (strdup of NULL) | ||
| var effectiveArgs = ensureVolname(args); | ||
| var fuseArgs = parseArgs(effectiveArgs); | ||
| var fuse = createFuseFS(fuseArgs); | ||
| if (fuse_h.fuse_mount(fuse, fuseArgs.mountPoint()) != 0) { | ||
| throw new FuseMountFailedException("fuse_mount failed"); | ||
| } | ||
| // Defer fuse_mount to loop() — macFUSE FSKit requires mount+loop on the same thread | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not an option, leaks resource. why should mount + loop be on different threads in the old implemenation? there are no different threads before |
||
| return new FuseMountImpl(fuse, fuseArgs); | ||
| } | ||
|
|
||
| private static List<String> ensureVolname(List<String> args) { | ||
| for (var arg : args) { | ||
| if (arg.contains("volname=")) { | ||
| return args; | ||
| } | ||
| } | ||
| var result = new ArrayList<>(args); | ||
| var progName = args.isEmpty() ? "jfuse" : args.getFirst(); | ||
| result.add("-ovolname=" + progName); | ||
| return result; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| MemorySegment createFuseFS(FuseArgs fuseArgs) throws FuseMountFailedException { | ||
| var fuse = FuseNewHelper.getInstance().fuse_new(fuseArgs.args(), fuseOperationsStruct, fuseOperationsStruct.byteSize(), MemorySegment.NULL); | ||
| var helper = FuseNewHelper.getInstance(); | ||
| this.darwinExtensions = helper.darwinExtensions(); | ||
| var fuse = helper.fuse_new(fuseArgs.args(), fuseOperationsStruct, fuseOperationsStruct.byteSize(), MemorySegment.NULL); | ||
| if (MemorySegment.NULL.equals(fuse)) { | ||
| throw new FuseMountFailedException("fuse_new failed"); | ||
| } | ||
|
|
@@ -46,7 +64,6 @@ MemorySegment createFuseFS(FuseArgs fuseArgs) throws FuseMountFailedException { | |
|
|
||
| @VisibleForTesting | ||
| FuseArgs parseArgs(List<String> cmdLineArgs) throws IllegalArgumentException { | ||
| System.out.println("DEBUG: cmdLineArgs = " + cmdLineArgs); // Add this | ||
| var args = fuse_args.allocate(fuseArena); | ||
| var argc = cmdLineArgs.size(); | ||
| var argv = fuseArena.allocate(ValueLayout.ADDRESS, argc + 1L); | ||
|
|
@@ -112,9 +129,14 @@ MemorySegment init(MemorySegment conn, MemorySegment cfg) { | |
| var connInfo = new FuseConnInfoImpl(conn); | ||
| if (fuse_h.fuse_version() >= 317) { | ||
| connInfo = new FuseConnInfoImpl317(conn); | ||
| connInfo.setFeatureFlag(FuseConnInfo.FUSE_CAP_READDIRPLUS); | ||
| } else { | ||
| connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS); | ||
| } | ||
| // only request READDIRPLUS if the kernel advertises support | ||
| if ((connInfo.capable() & FuseConnInfo.FUSE_CAP_READDIRPLUS) != 0) { | ||
| if (fuse_h.fuse_version() >= 317) { | ||
| connInfo.setFeatureFlag(FuseConnInfo.FUSE_CAP_READDIRPLUS); | ||
| } else { | ||
| connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS); | ||
| } | ||
| } | ||
| var config = new FuseConfigImpl(cfg); | ||
| fuseOperations.init(connInfo, config); | ||
|
|
@@ -157,14 +179,18 @@ int fsyncdir(MemorySegment path, int datasync, MemorySegment fi) { | |
| return fuseOperations.fsyncdir(MemoryUtils.toUtf8StringOrNull(path), datasync, new FileInfoImpl(fi)); | ||
| } | ||
|
|
||
| private Stat wrapStat(MemorySegment segment) { | ||
| return darwinExtensions ? new StatImpl(segment) : new StatCompatImpl(segment); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| int getattr(MemorySegment path, MemorySegment stat, MemorySegment fi) { | ||
| return fuseOperations.getattr(path.getString(0), new StatImpl(stat), FileInfoImpl.ofNullable(fi)); | ||
| return fuseOperations.getattr(path.getString(0), wrapStat(stat), FileInfoImpl.ofNullable(fi)); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| int fgetattr(MemorySegment path, MemorySegment stat, MemorySegment fi) { | ||
| return fuseOperations.getattr(path.getString(0), new StatImpl(stat), new FileInfoImpl(fi)); | ||
| return fuseOperations.getattr(path.getString(0), wrapStat(stat), new FileInfoImpl(fi)); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
|
|
@@ -209,7 +235,7 @@ private int read(MemorySegment path, MemorySegment buf, long size, long offset, | |
|
|
||
| private int readdir(MemorySegment path, MemorySegment buf, MemorySegment filler, long offset, MemorySegment fi, int flags) { | ||
| try (var arena = Arena.ofConfined()) { | ||
| return fuseOperations.readdir(path.getString(0), new DirFillerImpl(buf, filler, arena), offset, new FileInfoImpl(fi), flags); | ||
| return fuseOperations.readdir(path.getString(0), new DirFillerImpl(buf, filler, arena, darwinExtensions), offset, new FileInfoImpl(fi), flags); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -260,7 +286,6 @@ private int unlink(MemorySegment path) { | |
| int utimens(MemorySegment path, MemorySegment times, MemorySegment fi) { | ||
| try (var arena = Arena.ofConfined()) { | ||
| if (MemorySegment.NULL.equals(times)) { | ||
| // set both times to current time | ||
| var segment = timespec.allocate(arena); | ||
| timespec.tv_sec(segment, 0); | ||
| timespec.tv_nsec(segment, stat_h.UTIME_NOW()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package org.cryptomator.jfuse.mac; | ||
|
|
||
| import org.cryptomator.jfuse.api.FuseMount; | ||
| import org.cryptomator.jfuse.api.FuseMountFailedException; | ||
| import org.cryptomator.jfuse.mac.extr.fuse3.fuse_h; | ||
| import org.cryptomator.jfuse.mac.extr.fuse3.fuse_loop_config_v1; | ||
|
|
||
|
|
@@ -14,20 +15,20 @@ record FuseMountImpl(MemorySegment fuse, FuseArgs fuseArgs) implements FuseMount | |
|
|
||
| @Override | ||
| public int loop() { | ||
| // depends on fuse version: https://github.com/libfuse/libfuse/blob/fuse-3.12.0/include/fuse.h#L1011-L1050 | ||
| // macFUSE FSKit backend requires fuse_mount and fuse_loop on the same thread | ||
| if (fuse_h.fuse_mount(fuse, fuseArgs.mountPoint()) != 0) { | ||
| throw new RuntimeException(new FuseMountFailedException("fuse_mount failed")); | ||
| } | ||
|
Comment on lines
+19
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| if (!fuseArgs.multithreaded() || fuse_h.fuse_version() < FUSE_3_2) { | ||
| // FUSE 3.1: to keep things simple, we just don't support fuse_loop_mt | ||
| return fuse_h.fuse_loop(fuse); | ||
| } else if (fuse_h.fuse_version() < FUSE_3_12) { | ||
| // FUSE 3.2 | ||
| try (var arena = Arena.ofConfined()) { | ||
| var loopCfg = fuse_loop_config_v1.allocate(arena); | ||
| fuse_loop_config_v1.clone_fd(loopCfg, fuseArgs.cloneFd()); | ||
| fuse_loop_config_v1.max_idle_threads(loopCfg, fuseArgs.maxIdleThreads()); | ||
| return fuse_h.fuse_loop_mt(fuse, loopCfg); | ||
| } | ||
| } else { | ||
| // FUSE 3.12 | ||
| var loopCfg = fuse_h.fuse_loop_cfg_create(); | ||
| try { | ||
| fuse_h.fuse_loop_cfg_set_clone_fd(loopCfg, fuseArgs.cloneFd()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,10 @@ | |
|
|
||
| import org.cryptomator.jfuse.mac.extr.fuse3.fuse_h; | ||
|
|
||
| import java.lang.foreign.Arena; | ||
| import java.lang.foreign.FunctionDescriptor; | ||
| import java.lang.foreign.Linker; | ||
| import java.lang.foreign.MemoryLayout; | ||
| import java.lang.foreign.MemorySegment; | ||
| import java.lang.foreign.SymbolLookup; | ||
| import java.lang.foreign.ValueLayout; | ||
|
|
@@ -16,38 +18,79 @@ | |
| * This class is necessary due to changes in libfuse 3.17.1 and onwards: The function {@code fuse_new} is _not_ available anymore as unversioned symbol. | ||
| * One can call the function either by also specifying the version or call the unversioned symbol {@code fuse_new_31}. | ||
| * <p> | ||
| * Versioned symbols require a custom SymbolLookup (see PR #117), but libraries loaded via {@link System#loadLibrary(String)} cannot be accessed. | ||
| * Hence, this would require to _always_ set the libPath, loosing compatiblity to more exotic linux OSs with custom lib locations. | ||
| * To circumvent this issue, we call {@code fuse_version} to decide which (unversioned) symbol name we have to use. | ||
| * On macFUSE, we call {@code _fuse_new_31} (the 5-arg internal variant) directly, passing a properly | ||
| * initialized {@code libfuse_version} struct with {@code darwin_extensions_enabled=0}. | ||
| * This is required for FSKit backend support — the 4-arg wrapper zeros the version struct, | ||
| * which has the same effect, but calling the internal symbol directly makes the intent explicit | ||
| * and matches what fuse_main_real_versioned does. | ||
|
Comment on lines
+21
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation mismatch: Javadoc says Line 22 states 📝 Proposed fix * On macFUSE, we call {`@code` _fuse_new_31} (the 5-arg internal variant) directly, passing a properly
- * initialized {`@code` libfuse_version} struct with {`@code` darwin_extensions_enabled=0}.
+ * initialized {`@code` libfuse_version} struct with {`@code` darwin_extensions_enabled=1}.
* This is required for FSKit backend support — the 4-arg wrapper zeros the version struct,🤖 Prompt for AI Agents |
||
| */ | ||
| public class FuseNewHelper { | ||
|
|
||
| private static final AtomicReference<FuseNewHelper> INSTANCE = new AtomicReference<>(null); | ||
| private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup()); | ||
| private static final FunctionDescriptor DESC = FunctionDescriptor.of( | ||
|
|
||
| // 5-arg _fuse_new_31: (fuse_args*, fuse_operations*, size_t, libfuse_version*, void*) -> fuse* | ||
| private static final FunctionDescriptor DESC_5ARG = FunctionDescriptor.of( | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_LONG, | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_POINTER | ||
| ); | ||
|
|
||
| // 4-arg fallback: (fuse_args*, fuse_operations*, size_t, void*) -> fuse* | ||
| private static final FunctionDescriptor DESC_4ARG = FunctionDescriptor.of( | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_POINTER, | ||
| fuse_h.C_LONG, | ||
| fuse_h.C_POINTER | ||
| ); | ||
|
|
||
| // libfuse_version struct: {uint32 major, uint32 minor, uint32 hotfix, uint32 flags} | ||
| private static final MemoryLayout LIBFUSE_VERSION_LAYOUT = MemoryLayout.structLayout( | ||
| ValueLayout.JAVA_INT.withName("major"), | ||
| ValueLayout.JAVA_INT.withName("minor"), | ||
| ValueLayout.JAVA_INT.withName("hotfix"), | ||
| ValueLayout.JAVA_INT.withName("flags") | ||
| ); | ||
|
|
||
| private final MethodHandle fuse_new; | ||
| private final boolean fiveArg; | ||
| private final boolean darwinExtensions; | ||
|
|
||
| private FuseNewHelper(String symbolName) { | ||
| private FuseNewHelper(String symbolName, boolean fiveArg) { | ||
| this.fiveArg = fiveArg; | ||
| this.darwinExtensions = fiveArg; | ||
| this.fuse_new = Linker.nativeLinker().downcallHandle( | ||
| findOrThrow(symbolName), | ||
| DESC); | ||
| fiveArg ? DESC_5ARG : DESC_4ARG); | ||
| } | ||
|
|
||
| public MemorySegment fuse_new(MemorySegment args, MemorySegment op, long op_size, MemorySegment private_data) { | ||
| try { | ||
| return (MemorySegment) fuse_new.invokeExact(args, op, op_size, private_data); | ||
| if (fiveArg) { | ||
| try (var arena = Arena.ofConfined()) { | ||
| var version = arena.allocate(LIBFUSE_VERSION_LAYOUT); | ||
| version.set(ValueLayout.JAVA_INT, 0, 3); // major | ||
| version.set(ValueLayout.JAVA_INT, 4, 18); // minor | ||
| version.set(ValueLayout.JAVA_INT, 8, 2); // hotfix | ||
| version.set(ValueLayout.JAVA_INT, 12, 1); // darwin_extensions_enabled=1 | ||
| return (MemorySegment) fuse_new.invokeExact(args, op, op_size, version, private_data); | ||
| } | ||
| } else { | ||
| return (MemorySegment) fuse_new.invokeExact(args, op, op_size, private_data); | ||
| } | ||
| } catch (Throwable ex) { | ||
| throw new AssertionError("should not reach here", ex); | ||
| } | ||
| } | ||
|
|
||
| public boolean darwinExtensions() { | ||
| return darwinExtensions; | ||
| } | ||
|
|
||
| public synchronized static FuseNewHelper getInstance() { | ||
| if (INSTANCE.get() == null) { | ||
| INSTANCE.set(createInstance()); | ||
|
|
@@ -57,9 +100,14 @@ public synchronized static FuseNewHelper getInstance() { | |
|
|
||
| private static FuseNewHelper createInstance() throws IllegalStateException { | ||
| if (getLibVersion() < 317) { | ||
| return new FuseNewHelper("fuse_new"); | ||
| return new FuseNewHelper("fuse_new", false); | ||
| } else { | ||
| return new FuseNewHelper("fuse_new_31"); | ||
| // Prefer _fuse_new_31 (5-arg internal) if available, fall back to fuse_new_31 (4-arg wrapper) | ||
| if (SYMBOL_LOOKUP.find("_fuse_new_31").isPresent()) { | ||
| return new FuseNewHelper("_fuse_new_31", true); | ||
|
Comment on lines
+105
to
+107
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this sounds like internal API. Is this the official, documented way to enable |
||
| } else { | ||
| return new FuseNewHelper("fuse_new_31", false); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undo the system property change. This lib already exposes a way to pass fuse mount flags, which is sufficient to specify the backend.