lib/ffi/libfuse/fuse_common.rb in ffi-libfuse-0.0.1.rctest12 vs lib/ffi/libfuse/fuse_common.rb in ffi-libfuse-0.1.0.rc20220550

- old
+ new

@@ -11,11 +11,11 @@ typedef :pointer, :session attach_function :fuse_get_session, [:fuse], :session attach_function :fuse_set_signal_handlers, [:session], :int attach_function :fuse_remove_signal_handlers, [:session], :void - attach_function :fuse_loop, [:fuse], :int, blocking: false + attach_function :fuse_loop, [:fuse], :int attach_function :fuse_clean_cache, [:fuse], :int attach_function :fuse_exit, [:fuse], :void attach_function :fuse_destroy, [:fuse], :void attach_function :fuse_daemonize, [:int], :int @@ -40,20 +40,15 @@ # @param [Hash<Symbol,Object>] options passed to {run_native} or {run_ruby} # @return [Integer] an exit code for the fuse process (0 for success) def run(native: false, **options) return false unless mounted? - if native - run_native(**options) - else - run_ruby(**options) - end + native ? run_native(**options) : run_ruby(**options) rescue Errno => e -e.errno - rescue StandardError => e - warn e - warn e.backtrace.join("\n") + rescue StandardError, ScriptError => e + warn "#{e}\n#{e.backtrace.join("\n")}" -1 ensure teardown end @@ -73,19 +68,18 @@ # # Cons: # # * clone_fd is ignored # * filesystem interrupts probably can't work - def run_ruby(foreground: true, single_thread: true, traps: {}, **options) + def run_ruby(foreground: true, single_thread: true, traps: {}, remember: false, **options) Ackbar.trap(default_traps.merge(traps)) do |signals| daemonize unless foreground - if single_thread - fuse_loop(signals: signals, **options) - else - fuse_loop_mt(signals: signals, **options) - end + # Monitor for signals (and cache cleaning if required) + signals.monitor { fuse_cache_timeout(remember) } + + single_thread ? fuse_loop(**options) : fuse_loop_mt(**options) 0 end end # Running fuse loop natively @@ -98,47 +92,32 @@ # Cons # # * multi-threading will create a new ruby thread for every callback # * cannot daemonize multi-threaded (hangs) TODO: Why - pthread_lock?, GVL? # * cannot pass signals to the filesystem + # * connot use fuse_context (because the ruby thread is not the native thread) # # @api private # @param [Boolean] foreground # @param [Boolean] single_thread - # def run_native(foreground: true, single_thread: true, **options) - if !single_thread && !foreground - warn 'Cannot run native multi-thread fuse_loop when daemonized. Using single_thread mode' - single_thread = true - end + raise 'Cannot run deamonized native multi-thread fuse_loop' if !single_thread && !foreground clear_default_traps (se = session) && Libfuse.fuse_set_signal_handlers(se) Libfuse.fuse_daemonize(foreground ? 1 : 0) - - if single_thread - Libfuse.fuse_loop(@fuse) - else - native_fuse_loop_mt(**options) - end + single_thread ? Libfuse.fuse_loop(@fuse) : native_fuse_loop_mt(**options) ensure (se = session) && Libfuse.fuse_remove_signal_handlers(se) end - # Tell the processing loop to stop and force unmount the filesystem which is unfortunately required to make - # the processing threads, which are mostly blocked on io reads from /dev/fuse fd, to exit. - def exit - Libfuse.fuse_exit(@fuse) if @fuse - # Force threads blocked reading on #io to finish - unmount - end - # Ruby implementation of fuse default traps # @see Ackbar def default_traps - @default_traps ||= { INT: -> { exit }, HUP: -> { exit }, TERM: -> { exit }, PIPE: 'IGNORE' } + exproc = ->(signame) { exit(signame) } + @default_traps ||= { INT: exproc, HUP: exproc, TERM: exproc, TSTP: exproc, PIPE: 'IGNORE' } end # @api private # Ruby implementation of fuse_daemonize which does not work under MRI probably due to the way ruby needs to # understand native threads. @@ -183,55 +162,66 @@ delay end # @api private # Ruby implementation of single threaded fuse loop - def fuse_loop(signals:, remember: false, **_options) - loop do - break unless mounted? - - timeout = remember ? fuse_clean_cache : nil - - ready, _ignore_writable, errors = ::IO.select([io, signals.pr], [], [io], timeout) - - next unless ready || errors - - raise 'FUSE error' unless errors.empty? - - break if ready.include?(io) && !process - - break if ready.include?(signals.pr) && !signals.next - rescue Errno::EBADF - raise if mounted? # This will occur on exit - end + def fuse_loop(**_options) + fuse_process until fuse_exited? end # @api private # Ruby implementation of multi threaded fuse loop # # We cannot simulate the clone_fd behaviour of fuse_loop_mt as the required fd is not exposed in the low level # fuse api. # # @see ThreadPool ThreadPool for the mechanism that controls creation and termination of worker threads - def fuse_loop_mt(signals:, max_idle_threads: 10, max_threads: nil, remember: false, **_options) - # Monitor for signals (and cache cleaning if required) + def fuse_loop_mt(max_idle_threads: 10, max_threads: nil, **_options) + ThreadPool.new(name: 'FuseThread', max_idle: max_idle_threads.to_i, max_active: max_threads&.to_i) do + raise StopIteration if fuse_exited? - signals.monitor { remember ? fuse_clean_cache : nil } - ThreadPool.new(name: 'FuseThread', max_idle: max_idle_threads.to_i, max_active: max_threads) { process }.join + fuse_process + end.join end # @!visibility private def teardown return unless @fuse - unmount - Libfuse.fuse_destroy(@fuse) - ObjectSpace.undefine_finalizer(self) + self.exit&.join + + Libfuse.fuse_destroy(@fuse) if @fuse @fuse = nil + ensure + ObjectSpace.undefine_finalizer(self) end - # @!visibility private + # Starts a thread to unmount the filesystem and stop the processing loop. + # generally expected to be called from a signal handler + # @return [Thread] the unmount thread + def exit(_signame = nil) + return unless @fuse + + # Unmount/exit in a separate thread so the main fuse thread can keep running. + @exit ||= Thread.new do + unmount + + # without this sleep before exit, MacOS does not complete unmounting + sleep 0.2 if mac_fuse? + + Libfuse.fuse_exit(@fuse) + + true + end + end + + private + + def fuse_cache_timeout(remember) + remember ? fuse_clean_cache : nil + end + def session return nil unless @fuse @session ||= Libfuse.fuse_get_session(@fuse) @session.null? ? nil : @session @@ -241,9 +231,13 @@ def clear_default_traps %i[INT HUP TERM PIPE].each do |sig| prev = Signal.trap(sig, 'SYSTEM_DEFAULT') Signal.trap(sig, prev) unless prev == 'DEFAULT' end + end + + def mac_fuse? + FFI::Platform::IS_MAC end end end end