README.md in ffi-libfuse-0.0.1.rctest12 vs README.md in ffi-libfuse-0.1.0.rc20220550

- old
+ new

@@ -1,64 +1,163 @@ # FFI::Libfuse Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse) -## Writing a FUSE Filesystem +## Requirements -Create a class that implements the abstract methods of {FFI::Libfuse::Main} and {FFI::Libfuse::FuseOperations} + * Ruby 2.7 + * Linux: libfuse (Fuse2) or libfuse3 (Fuse3) + * MacOS: macFuse (https://osxfuse.github.io/) -Call {FFI::Libfuse.fuse_main} to start the filesystem +## Building a FUSE Filesystem +Install the gem + +```bash +gem install ffi-libfuse +``` + +Create a filesystem class + +* implement FUSE callbacks for filesystem operations satisfying {FFI::Libfuse::FuseOperations} +* recommend including {FFI::Libfuse::Adapter::Ruby} to add some ruby sugar and safety to the native FUSE Callbacks +* recommend including {FFI::Libfuse::Adapter::Fuse2Compat} for compatibility with Fuse2/macFuse +* implement {FFI::Libfuse::Main} configuration methods, eg to parse custom options with {FFI::Libfuse::FuseArgs#parse!} + (as altered by any included adapters from {FFI::Libfuse::Adapter}) +* Provide an entrypoint to start the filesystem using {FFI::Libfuse::Main.fuse_main} + +{FFI::Libfuse::Filesystem} contains additional classes and modules to help build and compose filesystems + +<!-- SAMPLE BEGIN: sample/hello_fs.rb --> +*sample/hello_fs.rb* + ```ruby +#!/usr/bin/env ruby +# frozen_string_literal: true + require 'ffi/libfuse' -class MyFS - # include helpers to abstract away from the native C callbacks to more idiomatic Ruby +# Hello World! +class HelloFS include FFI::Libfuse::Adapter::Ruby - - # ... FUSE callbacks .... quacks like a FFI:Libfuse::FuseOperations - def getattr(*args) - #... + include FFI::Libfuse::Adapter::Fuse2Compat + + # FUSE Configuration methods + + def fuse_options(args) + args.parse!({ 'subject=' => :subject }) do |key:, value:, **| + raise FFI::Libfuse::Error, 'subject option must be at least 2 characters' unless value.size >= 2 + + @subject = value if key == :subject + :handled + end end - - def readdir(*args) - #... + + def fuse_help + '-o subject=<subject> a target to say hello to' end - + + def fuse_configure + @subject ||= 'World!' + @content = "Hello #{@subject}\n" + end + + # FUSE callbacks + + def getattr(path, stat, *_args) + case path + when '/' + stat.directory(mode: 0o550) + when '/hello.txt' + stat.file(mode: 0o440, size: @content.size) + else + raise Errno::ENOENT + end + end + + def readdir(_path, *_args) + yield 'hello.txt' + end + + def read(_path, *_args) + @content + end end -FFI::Libfuse::fuse_main(operations: MyFS.new) if __FILE__ == $0 +# Start the file system +FFI::Libfuse.fuse_main(operations: HelloFS.new) if __FILE__ == $0 + ``` +<!-- SAMPLE END: sample/hello_fs.rb --> -# Fuse2/Fuse3 compatibility +Mount the filesystem +```bash +hello_fs.rb -h # show help +hello_fs.rb /mnt/hello # run deamonized, mounted at /mnt/hello +``` + +Do file things + +```bash +ls /mnt/hello +cat /mnt/hello/hello.txt +``` + +## Fuse2/Fuse3 compatibility + FFI::Libfuse will prefer Fuse3 over Fuse2 by default. See {FFI::Libfuse::LIBFUSE} -For writing filesystems with backwards/forwards compatibility between fuse version see -{FFI::Libfuse::Adapter::Fuse2Compat} and {FFI::Libfuse::Adapter::Fuse3Support} +New filesystems should write for Fuse3 API and include {FFI::Libfuse::Adapter::Fuse2Compat} for backwards compatibility +Alternatively filesystems written against Fuse2 API can include {FFI::Libfuse::Adapter::Fuse3Support} + ## MACFuse -[macFUSE](https://osxfuse.github.io/) (previously OSXFuse) supports a superset of the Fuse2 api so FFI::Libfuse is - intended to work in that environment. +[macFUSE](https://osxfuse.github.io/) (previously OSXFuse) supports a superset of the Fuse2 api -# Multi-threading +**TODO** Implement macFuse extensions -Most Ruby filesystems are unlikely to benefit from multi-threaded operation so -{FFI::Libfuse.fuse_main} as shown above injects the '-s' (single-thread) option by default. -Pass the original options in directly if multi-threaded operation is desired for your filesystem +# Under the hood +{FFI::Libfuse} provides raw access to the underlying libfuse but there some constraints imposed by Ruby. + +## Low-level functions re-implemented in Ruby + +The C functions fuse_main(), fuse_daemonize() and fuse_loop<_mt>() are re-implemented to provide + +* dynamic compatibility between Fuse2 and Fuse3 +* support for multi-threading under MRI +* signal handling in ruby filesystem (eg HUP to reload) + +The `-o native' option will use the native C functions but only exists to assist with testing that FFI::Libfuse has +similar behaviour to C libfuse. + +See {FFI::Libfuse::Main} and {FFI::Libfuse::FuseCommon} + +## Multi-threading + +{FFI::Libfuse.fuse_main} forces the `-s` (single-thread) option to be set since most Ruby filesystems are +unlikely to benefit from the overhead caused by multi-threaded operation obtaining/releasing the GVL around each +callback. + +{FFI::Libfuse::Main.fuse_main} does not pass any options by default and should be used in situations where +multi-threaded operations may be desirable. + ```ruby -FFI::Libfuse::fuse_main($0,*ARGV, operations: MyFS.new) if __FILE__ == $0 +FFI::Libfuse::Main.fuse_main(operations: MyFS.new) if __FILE__ == $0 ``` -The {FFI::Libfuse::ThreadPool} can be configured with `-o max_threads=<n>,max_idle_threads=<n>` options +The multi-thread loop uses {FFI::Libfuse::ThreadPool} to control thread usage and can be configured with options +`-o max_threads=<n>,max_idle_threads=<n>` -Callbacks that are about to block (and release the GVL for MRI) should call {FFI::Libfuse::ThreadPool.busy}. +Callbacks that are about to block (and release the GVL for MRI) should call {FFI::Libfuse::ThreadPool.busy} which will +spawn additional worker threads as required. -A typical scenario would be a filesystem where some callbacks are blocking on network IO. +Note that uncaught exceptions in callbacks will kill the worker thread and if all worker threads are dead the +file system will stop and unmount. In particular if the first callback raises an exception ```ruby def read(*args) # prep, validate args etc.. (MRI holding the GVL anyway) FFI::Libfuse::ThreadPool.busy @@ -68,32 +167,16 @@ **Note** Fuse itself has conditions under which filesystem callbacks will be serialized. In particular see [this discussion](http://fuse.996288.n3.nabble.com/GetAttr-calls-being-serialised-td11741.html) on the serialisation of `#getattr` and `#readdir` calls. -## Under the hood +**TODO** Build an example filesystem that makes use of multi-threading -FFI::Libfuse tries to provide raw access to the underlying libfuse but there some constraints imposed by Ruby. -The functions fuse_main(), fuse_daemonize() and fuse_loop<_mt>() are re-implemented in Ruby so we can provide - - * dynamic compatibility between Fuse2 and Fuse3 - * integrated support for multi-threading under MRI (see {FFI::Libfuse::ThreadPool}) - * signal handling in ruby filesystem (eg HUP to reload) - -Sending `-o native' will used the native C functions but this exists to assist with testing that FFI::Libfuse has -similar behaviour to libfuse itself. - -See {FFI::Libfuse::Main} and {FFI::Libfuse::FuseCommon} - ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/lwoggardner/ffi-libfuse. -### TODO - * Include a MetaFS, PathMapperFS etc (possibly a separate library) - * Build a filesystem that can make use of multi-threaded operations - * Test with macFUSE ## License The gem is available under the terms of the [MIT](https://opensource.org/licenses/MIT) License.