Skip to content

Commit

Permalink
fix: Fixes to Fuse2Compat
Browse files Browse the repository at this point in the history
let fuse handle fallback for fgetattr and ftruncate

fix bug in utimens (touch)

avoid module prepend if not using Fuse2

BREAKING_CHANGE: Main#fuse_options now ignores return value. Raise Error instead of returning false/nil.  FuseArgs#parse! also raises Error if parsing fails.
  • Loading branch information
lwoggardner committed Jan 12, 2024
1 parent 7aede2a commit 9f39408
Show file tree
Hide file tree
Showing 11 changed files with 58 additions and 56 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ class HelloFS
end

# Start the file system
FFI::Libfuse.fuse_main(operations: HelloFS.new) if __FILE__ == $0
exit(FFI::Libfuse.fuse_main(operations: HelloFS.new)) if __FILE__ == $0

exit(1)

```
<!-- SAMPLE END: sample/hello_fs.rb -->
Expand Down
2 changes: 1 addition & 1 deletion lib/ffi/libfuse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
module FFI
# Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
module Libfuse
# Filesystems can raise this error to indicate errors from filesystem users
# Filesystems can raise this error to indicate misconfiguration issues etc...
class Error < StandardError; end

# Opinionated default args for {.main}.
Expand Down
21 changes: 7 additions & 14 deletions lib/ffi/libfuse/adapter/fuse2_compat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,20 @@ def chmod(path, mode, fuse_file_info = nil)
super(path, mode, fuse_file_info)
end

def utimens(path, atime, mtime, fuse_file_info = nil)
super(path, atime, mtime, fuse_file_info)
def utimens(path, times, fuse_file_info = nil)
super(path, times, fuse_file_info)
end

def readdir(path, buffer, filler, offset, fuse_file_info, fuse_readdir_flag = 0)
f3_fill = proc { |buf, name, stat, off = 0, _fuse_fill_dir_flag = 0| filler.call(buf, name, stat, off) }
super(path, buffer, f3_fill, offset, fuse_file_info, fuse_readdir_flag)
end

def fgetattr(path, stat, ffi)
stat.clear # For some reason (at least on OSX) the stat is not clear when this is called.
getattr(path, stat, ffi)
0
end

def ftruncate(*args)
truncate(*args)
end

def fuse_respond_to?(fuse_method)
fuse_method = fuse_method[1..].to_sym if %i[fgetattr ftruncate].include?(fuse_method)
# getdir is never supported here anyway
# fgetattr and ftruncate already fallback to the respective basic method
return false if %i[getdir fgetattr ftruncate].include?(fuse_method)

super(fuse_method)
end

Expand Down Expand Up @@ -100,7 +93,7 @@ def init(*args)

# @!visibility private
def self.included(mod)
mod.prepend(Prepend)
mod.prepend(Prepend) if FUSE_MAJOR_VERSION < 3
end

# @!method init_fuse_config(fuse_config,compat)
Expand Down
4 changes: 2 additions & 2 deletions lib/ffi/libfuse/filesystem/virtual_dir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ def initialize(accounting: Accounting.new)
# @!group FUSE Callbacks

# For the root path provides this directory's stat information, otherwise passes on to the next filesystem
def getattr(path, stat_buf = nil, _ffi = nil)
def getattr(path, stat_buf = nil, ffi = nil)
if root?(path)
stat_buf&.directory(nlink: entries.size + 2, **virtual_stat)
return self
end

path_method(__method__, path, stat_buf, notsup: Errno::ENOSYS)
path_method(__method__, path, stat_buf, ffi, notsup: Errno::ENOSYS)
end

# Safely passes on file open to next filesystem
Expand Down
4 changes: 0 additions & 4 deletions lib/ffi/libfuse/filesystem/virtual_fs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,6 @@ def build(files, base_path = Pathname.new('/'))
# * :copy_file_range can raise ENOTSUP to trigger glibc to fallback to inefficient copy
def fuse_respond_to?(method)
case method
when :getdir, :fgetattr
# TODO: Find out if fgetattr works on linux, something wrong with stat values on OSX.
# https://github.com/osxfuse/osxfuse/issues/887
false
when :read_buf, :write_buf
!no_buf
else
Expand Down
5 changes: 3 additions & 2 deletions lib/ffi/libfuse/fuse2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ class << self
def parse_cmdline(args, handler: nil)
# This also handles -h to print help information on stderr
# Parse mountpoint, -f , -s from args
# @return [Array<(String,Boolean,Boolean)>|nil]
# @return [Array<(String,Boolean,Boolean)>]
# mountpoint, multi_thread, foreground options from args if available
# nil if no mountpoint, or options is requesting help or version information
mountpoint_ptr = FFI::MemoryPointer.new(:pointer, 1)
multi_thread_ptr = FFI::MemoryPointer.new(:int, 1)
foreground_ptr = FFI::MemoryPointer.new(:int, 1)

return nil unless Libfuse.fuse_parse_cmdline2(args, mountpoint_ptr, multi_thread_ptr, foreground_ptr).zero?
res = Libfuse.fuse_parse_cmdline2(args, mountpoint_ptr, multi_thread_ptr, foreground_ptr)
raise Error unless res.zero?

# noinspection RubyResolve
mp_data_ptr = mountpoint_ptr.get_pointer(0)
Expand Down
2 changes: 1 addition & 1 deletion lib/ffi/libfuse/fuse3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class Fuse3 < FuseCommon
class << self
def parse_cmdline(args, handler: nil)
cmdline_opts = FuseCmdlineOpts.new
return nil unless Libfuse.fuse_parse_cmdline3(args, cmdline_opts).zero?
raise Error unless Libfuse.fuse_parse_cmdline3(args, cmdline_opts).zero?

handler&.fuse_debug(cmdline_opts.debug) if handler.respond_to?(:fuse_debug)

Expand Down
7 changes: 5 additions & 2 deletions lib/ffi/libfuse/fuse_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ def insert(pos, arg)
# - :error an error, alternatively raise {Error}
# - :keep retain the current argument for further processing
# - :handled,:discard remove the current argument from further processing
# @return [nil|self] nil on error otherwise self
# @raise Error if an error is raised during parsing
# @return [self]
def parse!(opts, data = nil, ignore: %i[non_option unmatched], &block)
ignore ||= []

Expand All @@ -140,7 +141,9 @@ def parse!(opts, data = nil, ignore: %i[non_option unmatched], &block)
end

fop = fuse_opt_proc(symbols, bool_opts, param_opts, ignore, &block)
Libfuse.fuse_opt_parse(self, data, int_opts, fop).zero? ? self : nil
raise Error unless Libfuse.fuse_opt_parse(self, data, int_opts, fop).zero?

self
end

private
Expand Down
12 changes: 8 additions & 4 deletions lib/ffi/libfuse/fuse_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ def run(native: false, **options)
teardown
end

# @api private
# @param [Boolean] foreground
# @param [Boolean] single_thread
# @param [Hash<String,Proc>] traps see {Ackbar.trap}
#
# @param [Hash<String,Proc>] traps as per Signal.trap
# these are merged over default signal handlers for INT, HUP, TERM that unmount and exit filesystem
# @param [Integer] remember fuse cache timeout
# @api private
# Implement fuse loop in ruby
#
# Pros:
Expand Down Expand Up @@ -112,7 +113,9 @@ def run_native(foreground: true, single_thread: true, **options)
end

# Ruby implementation of fuse default traps
# @see Ackbar
#
# * INT, HUP, TERM to unmount and exit filesystem
# * PIPE is ignored
def default_traps
exproc = ->(signame) { exit(signame) }
@default_traps ||= { INT: exproc, HUP: exproc, TERM: exproc, TSTP: exproc, PIPE: 'IGNORE' }
Expand Down Expand Up @@ -190,6 +193,7 @@ def safe_fuse_process
fuse_process || (sleep(0.1) && false)
end

# @!visibility private
def teardown
return unless @fuse

Expand Down
51 changes: 27 additions & 24 deletions lib/ffi/libfuse/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ def default_args(*extra_args)

# Main function of FUSE
#
# This function:
#
# - parses command line options - see {fuse_parse_cmdline}
# exiting immediately if help or version options were processed
# - calls {#fuse_debug}, {#fuse_options}, {#fuse_configure} if implemented by operations
# - installs signal handlers for INT, HUP, TERM to unmount and exit filesystem
# - installs custom signal handlers if operations implements {fuse_traps}
# - creates a fuse handle mounted with registered operations - see {fuse_create}
# - calls either the single-threaded (option -s) or the multi-threaded event loop - see {FuseCommon#run}
# - calls {#fuse_configure} if implemented by operations
# - creates a fuse handle see {fuse_create}
# - returns 0 if help or version options were processed (ie after all messages have been printed by libfuse)
# - returns 2 if fuse handle is not successfully mounted
# - calls {#fuse_traps} if implemented by operations
# - calls run on the fuse handle with options from previous steps- see {FuseCommon#run}
#
# @param [Array<String>] argv mount.fuse arguments
# expects progname, mountpoint, options....
Expand All @@ -46,23 +44,23 @@ def default_args(*extra_args)
# @return [Integer] suitable for process exit code
def fuse_main(*argv, operations:, args: argv.any? ? argv : default_args, private_data: nil)
run_args = fuse_parse_cmdline(args: args, handler: operations)
return 2 unless run_args

fuse_args = run_args.delete(:args)
mountpoint = run_args.delete(:mountpoint)

return 3 unless fuse_configure(operations: operations, **run_args)
show_only = run_args[:show_help] || run_args[:show_version]

return 3 if !show_only && !fuse_configure(operations)

warn "FuseCreate: mountpoint: #{mountpoint}, args: [#{fuse_args.argv.join(' ')}]" if run_args[:debug]
warn "FuseRun: #{run_args}" if run_args[:debug]

fuse = fuse_create(mountpoint, args: fuse_args, operations: operations, private_data: private_data)

return 0 if run_args[:show_help] || run_args[:show_version]
return 0 if show_only
return 2 if !fuse || !mountpoint

return unless fuse

run_args[:traps] = operations.fuse_traps if operations.respond_to?(:fuse_traps)
fuse.run(**run_args)
end
alias main fuse_main
Expand All @@ -72,7 +70,6 @@ def fuse_main(*argv, operations:, args: argv.any? ? argv : default_args, private
# - parses standard command line options (-d -s -h -V)
# will call {fuse_debug}, {fuse_version}, {fuse_help} if implemented by handler
# - calls {fuse_options} for custom option processing if implemented by handler
# - records signal handlers if operations implements {fuse_traps}
# - parses standard fuse mount options
#
# @param [Array<String>] argv mount.fuse arguments
Expand All @@ -88,24 +85,25 @@ def fuse_main(*argv, operations:, args: argv.any? ? argv : default_args, private
# * show_version [Boolean]: -v or --version
# * debug [Boolean]: -d
# * others are options to pass to {FuseCommon#run}
# @return [nil] if args are not parsed successfully
def fuse_parse_cmdline(*argv, args: argv.any? ? argv : default_args, handler: nil)
args = fuse_init_args(args)

# Parse args and print cmdline help
run_args = Fuse.parse_cmdline(args, handler: handler)
return nil unless run_args

return nil if handler.respond_to?(:fuse_options) && !handler.fuse_options(args)
handler.fuse_options(args) if handler.respond_to?(:fuse_options)

run_args[:traps] = handler.fuse_traps if handler.respond_to?(:fuse_traps)

return nil unless parse_run_options(args, run_args)
parse_run_options(args, run_args)

run_args[:args] = args
run_args
rescue Error
nil
end

# @return [FuseCommon|nil] the mounted filesystem or nil if not mounted
# @return [FuseCommon] the mounted filesystem handle
# @return [nil] if not mounted (eg due to --help or --version, or an error)
def fuse_create(mountpoint, *argv, operations:, args: nil, private_data: nil)
args = fuse_init_args(args || argv)

Expand All @@ -116,8 +114,8 @@ def fuse_create(mountpoint, *argv, operations:, args: nil, private_data: nil)
end

# @!visibility private
def fuse_configure(operations:, show_help: false, show_version: false, **_)
return true unless operations.respond_to?(:fuse_configure) && !show_help && !show_version
def fuse_configure(operations)
return true unless operations.respond_to?(:fuse_configure)

# Provide sensible values for FuseContext in case this is referenced during configure
FFI::Libfuse::FuseContext.overrides do
Expand Down Expand Up @@ -166,15 +164,16 @@ def parse_run_options(args, run_args)
# @abstract
# Called to allow filesystem to handle custom options and observe standard mount options #
# @param [FuseArgs] args
# @return [Boolean] true if args parsed successfully
# @raise [Error] if there is an error parsing the options
# @return [void]
# @see FuseArgs#parse!
# @example
# OPTIONS = { 'config=' => :config, '-c ' => :config }
# def fuse_options(args)
# args.parse!(OPTIONS) do |key:, value:, out:, **opts|
#
# # raise errors for invalid config
# raise FFI::Libfuse::FuseArgs::Error, "Invalid config" unless valid_config?(key,value)
# raise FFI::Libfuse::Error, "Invalid config" unless valid_config?(key,value)
#
# # Configure the file system
# @config = value if key == :config
Expand All @@ -189,6 +188,8 @@ def parse_run_options(args, run_args)

# @!method fuse_traps
# @abstract
# Passed to {FuseCommon#run} to allow filesystem to handle custom signal traps. These traps
# are merged over those from {FuseCommon#default_traps}
# @return [Hash<String|Symbol|Integer,String|Proc>]
# map of signal name or number to signal handler as per Signal.trap
# @example
Expand All @@ -198,6 +199,7 @@ def parse_run_options(args, run_args)

# @!method fuse_version
# @abstract
# Called as part of generating output for the -V option
# @return [String] a custom version string to output with -V option

# @!method fuse_help
Expand All @@ -214,6 +216,7 @@ def parse_run_options(args, run_args)
# @!method fuse_configure
# @abstract
# Called immediately before the filesystem is mounted, after options have been parsed
# (eg to validate required options)
#
# @raise [Error] to prevent the mount from proceeding
# @return [void]
Expand Down
2 changes: 1 addition & 1 deletion sample/hello_fs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ def read(_path, *_args)
end

# Start the file system
FFI::Libfuse.fuse_main(operations: HelloFS.new) if __FILE__ == $0
exit(FFI::Libfuse.fuse_main(operations: HelloFS.new)) if __FILE__ == $0

0 comments on commit 9f39408

Please sign in to comment.