ffi-libfuse 0.0.1.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/README.md +100 -0
  4. data/lib/ffi/accessors.rb +145 -0
  5. data/lib/ffi/devt.rb +30 -0
  6. data/lib/ffi/flock.rb +47 -0
  7. data/lib/ffi/gnu_extensions.rb +115 -0
  8. data/lib/ffi/libfuse/ackbar.rb +112 -0
  9. data/lib/ffi/libfuse/adapter/context.rb +37 -0
  10. data/lib/ffi/libfuse/adapter/debug.rb +89 -0
  11. data/lib/ffi/libfuse/adapter/fuse2_compat.rb +91 -0
  12. data/lib/ffi/libfuse/adapter/fuse3_support.rb +87 -0
  13. data/lib/ffi/libfuse/adapter/interrupt.rb +37 -0
  14. data/lib/ffi/libfuse/adapter/pathname.rb +23 -0
  15. data/lib/ffi/libfuse/adapter/ruby.rb +334 -0
  16. data/lib/ffi/libfuse/adapter/safe.rb +58 -0
  17. data/lib/ffi/libfuse/adapter/thread_local_context.rb +36 -0
  18. data/lib/ffi/libfuse/adapter.rb +79 -0
  19. data/lib/ffi/libfuse/callbacks.rb +61 -0
  20. data/lib/ffi/libfuse/fuse2.rb +159 -0
  21. data/lib/ffi/libfuse/fuse3.rb +162 -0
  22. data/lib/ffi/libfuse/fuse_args.rb +166 -0
  23. data/lib/ffi/libfuse/fuse_buffer.rb +155 -0
  24. data/lib/ffi/libfuse/fuse_callbacks.rb +48 -0
  25. data/lib/ffi/libfuse/fuse_cmdline_opts.rb +44 -0
  26. data/lib/ffi/libfuse/fuse_common.rb +249 -0
  27. data/lib/ffi/libfuse/fuse_config.rb +205 -0
  28. data/lib/ffi/libfuse/fuse_conn_info.rb +211 -0
  29. data/lib/ffi/libfuse/fuse_context.rb +79 -0
  30. data/lib/ffi/libfuse/fuse_file_info.rb +100 -0
  31. data/lib/ffi/libfuse/fuse_loop_config.rb +43 -0
  32. data/lib/ffi/libfuse/fuse_operations.rb +870 -0
  33. data/lib/ffi/libfuse/fuse_opt.rb +54 -0
  34. data/lib/ffi/libfuse/fuse_poll_handle.rb +59 -0
  35. data/lib/ffi/libfuse/fuse_version.rb +43 -0
  36. data/lib/ffi/libfuse/job_pool.rb +53 -0
  37. data/lib/ffi/libfuse/main.rb +200 -0
  38. data/lib/ffi/libfuse/test/operations.rb +56 -0
  39. data/lib/ffi/libfuse/test.rb +3 -0
  40. data/lib/ffi/libfuse/thread_pool.rb +147 -0
  41. data/lib/ffi/libfuse/version.rb +8 -0
  42. data/lib/ffi/libfuse.rb +24 -0
  43. data/lib/ffi/ruby_object.rb +95 -0
  44. data/lib/ffi/stat/constants.rb +29 -0
  45. data/lib/ffi/stat/native.rb +50 -0
  46. data/lib/ffi/stat/time_spec.rb +137 -0
  47. data/lib/ffi/stat.rb +96 -0
  48. data/lib/ffi/stat_vfs.rb +81 -0
  49. data/lib/ffi/struct_array.rb +39 -0
  50. data/lib/ffi/struct_wrapper.rb +100 -0
  51. data/sample/memory_fs.rb +189 -0
  52. data/sample/no_fs.rb +69 -0
  53. metadata +165 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fuse_version'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ # Its a trap!
8
+ #
9
+ # Implements the self-pipe trick for handling signals in multi-threaded ruby programs
10
+ class Ackbar
11
+ # Read side of the self-pipe
12
+ # @return [IO] for use with IO.select only
13
+ # @see monitor
14
+ # @see next
15
+ attr_reader :pr
16
+
17
+ class << self
18
+ # Run a block with the given traps, restoring the original traps on completion
19
+ # @param [Hash] traps map of signal to handler as per Signal.trap
20
+ # @yieldparam [Ackbar] signals
21
+ # @return [Object] the result of the block
22
+ def trap(traps, force: false)
23
+ signals = new(traps, force: force)
24
+ yield signals
25
+ ensure
26
+ signals&.restore
27
+ end
28
+ end
29
+
30
+ # @param [Hash<Symbol|String|Integer,String|Proc>] traps
31
+ # Map of signal or signo to signal handler as per Signal.trap
32
+ # @param [Boolean] force
33
+ # If not set traps that are not currently set to 'DEFAULT' will be ignored
34
+ def initialize(traps, force: false, signal: Signal)
35
+ @signal = signal
36
+ @traps = traps.transform_keys { |k| signame(k) }
37
+ @pr, @pw = ::IO.pipe
38
+ @monitor = nil
39
+ @restore = @traps.map { |(sig, handler)| [sig, trap(sig, handler, force: force)] }.to_h
40
+ end
41
+
42
+ # Handle the next available signal on the pipe (without blocking)
43
+ # @return [Boolean] false if pipe is closed, true if pipe would block
44
+ # @return [Array<String,Object>] signal name, signal result when a signal has been processed
45
+ def next
46
+ signame = signal.signame(@pr.read_nonblock(1).unpack1('c'))
47
+ t = @traps[signame]
48
+ [signame, t.arity.zero? ? t.call : t.call(signame)]
49
+ rescue EOFError
50
+ # the signal pipe writer is closed - we are exiting.
51
+ @pr.close
52
+ false
53
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
54
+ # oh well...
55
+ true
56
+ end
57
+
58
+ # Restore traps as they were at #new and close the write side of the pipe
59
+ def restore
60
+ @restore.each_pair { |signame, handler| signal.trap(signame, handler) }
61
+ @restore = nil
62
+ @pw.close
63
+ @monitor&.join
64
+ end
65
+ alias close restore
66
+
67
+ # Start a thread to monitor for signals optionally yields between signals
68
+ # @param [String] name The name of the monitor thread
69
+ # @yieldreturn [Integer] timeout in seconds or nil to wait until signal or {restore}
70
+ # @return [Thread] the monitor thread
71
+ def monitor(name: 'SignalMonitor')
72
+ @monitor ||= Thread.new do
73
+ Thread.current.name = name
74
+ loop do
75
+ timeout = block_given? ? yield : nil
76
+
77
+ ready, _ignore_writable, _errors = ::IO.select([@pr], [], [], timeout)
78
+
79
+ break if ready&.include?(@pr) && !self.next
80
+ end
81
+ end
82
+ end
83
+
84
+ # @!visibility private
85
+ # normalize signal identifiers to things that are keys in Signal.list
86
+ def signame(sig)
87
+ sig.is_a?(Integer) ? signal.signame(sig) : sig.to_s.upcase.sub(/^SIG/, '')
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :signal
93
+
94
+ def trap(signame, handler, force: false)
95
+ prev =
96
+ case handler
97
+ when String, Symbol
98
+ signal.trap(signame, handler.to_s)
99
+ else
100
+ signal.trap(signame) { |signo| send_signal(signo) }
101
+ end
102
+
103
+ signal.trap(signame, prev) unless force || prev == 'DEFAULT'
104
+ prev
105
+ end
106
+
107
+ def send_signal(signo)
108
+ @pw&.write([signo].pack('c')) unless @pw&.closed?
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../fuse_context'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Adapter
8
+ # Wrapper module to inject {FuseContext} as first arg to each callback method (except :destroy)
9
+ #
10
+ # {ThreadLocalContext} may be a less intrusive means to make the context available to callbacks
11
+ module Context
12
+ # @!visibility private
13
+ def fuse_wrappers(*wrappers)
14
+ wrappers.unshift(
15
+ {
16
+ wrapper: proc { |_fm, *args, **_, &b| self.class.context_callback(*args, &b) },
17
+ excludes: %i[destroy]
18
+ }
19
+ )
20
+ return wrappers unless defined?(super)
21
+
22
+ super(*wrappers)
23
+ end
24
+
25
+ module_function
26
+
27
+ # @yieldparam [FuseContext] ctx
28
+ # @yieldparam [Array] *args
29
+ def context_callback(*args)
30
+ ctx = FuseContext.get
31
+ ctx = nil if ctx.null?
32
+ yield ctx, *args
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ module Libfuse
5
+ module Adapter
6
+ # Debug callbacks
7
+ #
8
+ # When included in a filesystem class, and if debugging is enabled via {Main#fuse_debug}, then installs a wrapper
9
+ # via #{FuseCallbacks#fuse_wrappers} to log callbacks via #warn
10
+ module Debug
11
+ DEFAULT_FORMAT = "%<p>s %<n>s %<t>s %<m>s(%<a>s)\n\t=> %<r>s"
12
+
13
+ # @return [Boolean] true if debug is enabled
14
+ def debug?
15
+ @debug
16
+ end
17
+
18
+ # Configure debug output
19
+ # @abstract
20
+ # @return [Hash<Symbol,String>] options to pass to {debug_callback}
21
+ def debug_config
22
+ {}
23
+ end
24
+
25
+ # @!visibility private
26
+ def fuse_wrappers(*wrappers)
27
+ conf = { prefix: self.class.name }.merge!(debug_config)
28
+ # validate config for bad formats, strftime etc
29
+ Debug.debug_format(:test_debug, [], :result, **conf)
30
+ wrappers << proc { |fm, *args, &b| Debug.debug_callback(fm, *args, **conf, &b) } if debug?
31
+ return wrappers unless defined?(super)
32
+
33
+ super(*wrappers)
34
+ end
35
+
36
+ # @!visibility private
37
+ def fuse_debug(enabled)
38
+ super if defined?(super)
39
+ @debug = enabled
40
+ end
41
+
42
+ module_function
43
+
44
+ # Debug fuse method, args and result
45
+ # @param [Symbol] fuse_method the callback name
46
+ # @param [Array] args callback arguments
47
+ # @param [Hash<Symbol,String>] options see {debug} for defaults
48
+ # @option options [String] prefix
49
+ # @option options [String] strftime a date time format
50
+ # @option options [String] format format string with fields
51
+ #
52
+ # * %<n>: Time.now - use strftime option to control time format
53
+ # * %<t>: Thread name
54
+ # * %<m>: Fuse method
55
+ # * %<a>: Comma separate list of arguments
56
+ # * %<r>: Result of the method call (or any error raised)
57
+ # * %<p>: The value of prefix option
58
+ def debug_callback(fuse_method, *args, **options)
59
+ debug(fuse_method, args, yield(*args), **options)
60
+ rescue SystemCallError => e
61
+ # expected behaviour
62
+ debug(fuse_method, args, "#{e.class.name}(errno=#{e.errno}): #{e.message}", **options)
63
+ raise
64
+ rescue StandardError, ScriptError => e
65
+ # unexpected, debug with backtrace
66
+ debug(fuse_method, args, (["#{e.class.name}: #{e.message}"] + e.backtrace).join("\n\t"), **options)
67
+ raise
68
+ end
69
+
70
+ # @!visibility private
71
+ def debug(fuse_method, args, result, **options)
72
+ warn debug_format(fuse_method, args, result, **options)
73
+ result
74
+ end
75
+
76
+ # @!visibility private
77
+ def debug_format(fuse_method, args, result, prefix: 'DEBUG', strftime: '%FT%T%:z', format: DEFAULT_FORMAT)
78
+ format(format,
79
+ p: prefix,
80
+ n: Time.now.strftime(strftime),
81
+ t: Thread.current.name || Thread.current,
82
+ m: fuse_method,
83
+ a: args.map(&:to_s).join(','),
84
+ r: result)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ module Libfuse
5
+ module Adapter
6
+ # Wrapper module to assist filesystem written for Fuse3 to be compatible with Fuse2
7
+ module Fuse2Compat
8
+ # @!visibility private
9
+ # Wrapper shim for fuse methods to ensure compatibility with Fuse2
10
+ module Fuse2Prepend
11
+ include Adapter
12
+
13
+ def getattr(path, stat, fuse_file_info = nil)
14
+ super(path, stat, fuse_file_info)
15
+ end
16
+
17
+ def truncate(path, size, fuse_file_info = nil)
18
+ super(path, size, fuse_file_info)
19
+ end
20
+
21
+ def init(fuse_conn_info, fuse_config = nil)
22
+ super(fuse_conn_info, fuse_config)
23
+ end
24
+
25
+ def chown(path, uid, gid, fuse_file_info = nil)
26
+ super(path, uid, gid, fuse_file_info)
27
+ end
28
+
29
+ def chmod(path, mode, fuse_file_info = nil)
30
+ super(path, mode, fuse_file_info)
31
+ end
32
+
33
+ def utimens(path, atime, mtime, fuse_file_info = nil)
34
+ super(path, atime, mtime, fuse_file_info)
35
+ end
36
+
37
+ def readdir(path, buffer, filler, offset, fuse_file_info, fuse_readdir_flag = 0)
38
+ filler = proc { |buf, name, stat, off = 0, _fuse_fill_dir_flag = 0| filler.call(buf, name, stat, off) }
39
+ super(path, buffer, filler, offset, fuse_file_info, fuse_readdir_flag)
40
+ end
41
+
42
+ def fgetattr(*args)
43
+ getattr(*args)
44
+ end
45
+
46
+ def ftruncate(*args)
47
+ truncate(*args)
48
+ end
49
+
50
+ def fuse_respond_to?(fuse_method)
51
+ fuse_method = fuse_method[1..].to_sym if %i[fgetattr ftruncate].include?(fuse_method)
52
+ super(fuse_method)
53
+ end
54
+
55
+ def fuse_flags
56
+ res = defined?(super) ? super : []
57
+ if respond_to?(:init_fuse_config)
58
+ fuse_config = FuseConfig.new
59
+ init_fuse_config(fuse_config, :fuse2)
60
+ res << :nullpath_ok if fuse_config.nullpath_ok?
61
+ end
62
+
63
+ res
64
+ end
65
+ end
66
+
67
+ # @!visibility private
68
+ module Fuse3Prepend
69
+ def init(*args)
70
+ init_fuse_config(args.detect { |a| a.is_a?(FuseConfig) }) if respond_to?(:init_fuse_config)
71
+ super if defined?(super)
72
+ end
73
+ end
74
+
75
+ # @!visibility private
76
+ def self.included(mod)
77
+ prepend_module = Libfuse.FUSE_MAJOR_VERSION == 2 ? Fuse2Prepend : Fuse3Prepend
78
+ mod.prepend(prepend_module)
79
+ end
80
+
81
+ # @!method init_fuse_config(fuse_config,compat)
82
+ # @abstract
83
+ # Define this method to configure the fuse config object so that under Fuse2 the config options
84
+ # can be converted to appropriate flags.
85
+ #
86
+ # @param [FuseConfig] fuse_config the fuse config object
87
+ # @param [Symbol] compat either :fuse2 or :fuse3
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ module Libfuse
5
+ module Adapter
6
+ # Wrapper module to assist filesystem written for Fuse2 to be compatible with Fuse3
7
+ #
8
+ # @note there are some deprecated Fuse2 features that have no forwards compatibility in Fuse3
9
+ #
10
+ # - flag nopath became fuse_config.nullpath_ok but it is not compatible since chmod/chown etc will now
11
+ # receive a null path but the filesystem cannot receive the new fuse_file_info arg
12
+ #
13
+ module Fuse3Support
14
+ # The actual module methods that are prepended
15
+ # @!visibility private
16
+ module Prepend
17
+ include Adapter
18
+
19
+ # Inner adapters received Fuse2 compatible args
20
+ def fuse3_compat?
21
+ false
22
+ end
23
+
24
+ def getattr(*args)
25
+ fi = args.pop
26
+ return fgetattr(*args, fi) if fi && respond_to?(:fgetattr)
27
+
28
+ super(*args)
29
+ end
30
+
31
+ def truncate(*args)
32
+ fi = args.pop
33
+ return ftruncate(*args, fi) if fi && respond_to?(:ftruncate)
34
+
35
+ super(*args)
36
+ end
37
+
38
+ def chown(*args)
39
+ args.pop
40
+ super(*args)
41
+ end
42
+
43
+ def chmod(*args)
44
+ args.pop
45
+ super(*args)
46
+ end
47
+
48
+ # TODO: Fuse3 deprecated flag utime_omit_ok - which meant that UTIME_OMIT and UTIME_NOW are passed through
49
+ # instead of ????
50
+ # Strictly if the flag is not set this compat shim should convert utime now values to Time.now
51
+ # but there is no way to handle OMIT
52
+ def utimens(*args)
53
+ args.pop
54
+ super(*args)
55
+ end
56
+
57
+ def init(*args)
58
+ args.pop
59
+
60
+ # TODO: populate FuseConfig with output from fuse_flags/FuseConnInfo where appropriate
61
+ super(*args)
62
+ end
63
+
64
+ def readdir(*args, &block)
65
+ # swallow the flag arg unknown to fuse 2
66
+ args.pop
67
+
68
+ args = args.map do |a|
69
+ next a unless a.is_a?(FFI::Function)
70
+
71
+ # wrap the filler proc for the extra flags argument
72
+ proc { |buf, name, stat, off| a.call(buf, name, stat, off, 0) }
73
+ end
74
+
75
+ super(*args, &block)
76
+ end
77
+ end
78
+
79
+ # @!visibility private
80
+ def self.included(mod)
81
+ # We prepend our shim module so caller doesn't have to call super
82
+ mod.prepend(Prepend) if FUSE_MAJOR_VERSION > 2
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../fuse_context'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Adapter
8
+ # Wrapper module to handle interrupts
9
+ #
10
+ # Include this module if you want all requests to check for interruption before processing
11
+ #
12
+ # To handle interrupts only for specific callbacks just call {Libfuse.raise_interrupt} or
13
+ # {Libfuse.fuse_interrupted?} during callback processing rather than including this adapter
14
+ module Interrupt
15
+ # @!visibility private
16
+ def fuse_wrappers(*wrappers)
17
+ wrappers << {
18
+ wrapper: proc { |_fuse_method, *args, &b| Interrupt.interrupt_callback(*args, &b) },
19
+ excludes: %i[init destroy]
20
+ }
21
+ return wrappers unless defined?(super)
22
+
23
+ super(*wrappers)
24
+ end
25
+
26
+ module_function
27
+
28
+ # @raise [Errno::EINTR] if the fuse request is marked as interrupted
29
+ def interrupt_callback(*args)
30
+ return -Errno::EINTR::Errno if Libfuse.fuse_interrupted?
31
+
32
+ yield(*args)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end