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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fuse_version'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ # @!visibility private
8
+ #
9
+ # Option description
10
+ #
11
+ # @see Args#parse!
12
+ class FuseOpt < FFI::Struct
13
+ layout template: :pointer, # Matching template and optional parameter formatting
14
+ offset: :ulong, # Unused in FFI::Libfuse (harder to prepare structs and offsets than just just call block)
15
+ value: :int # Value to set the variable to, or to be passed as 'key' to the processing function.
16
+
17
+ # @!method initialize(address=nil)
18
+
19
+ def fill(template, value)
20
+ str_ptr = FFI::MemoryPointer.from_string(template)
21
+ self[:template] = str_ptr
22
+ self[:offset] = (2**(8 * FFI::Type::INT.size)) - 1 # -(1U) in a LONG!!
23
+ self[:value] = value.to_i
24
+ self
25
+ end
26
+
27
+ def null
28
+ # NULL opt to terminate the list
29
+ self[:template] = FFI::Pointer::NULL
30
+ self[:offset] = 0
31
+ self[:value] = 0
32
+ end
33
+
34
+ # @!visibility private
35
+ # DataConverter for Hash<String,Integer> to Opt[] required by fuse_parse_opt
36
+ module OptList
37
+ extend FFI::DataConverter
38
+ native_type FFI::Type::POINTER
39
+
40
+ class << self
41
+ def to_native(opts, _ctx)
42
+ raise ArgumentError, "Opts #{opts} must be a Hash" unless opts.respond_to?(:each_pair)
43
+
44
+ native = FFI::MemoryPointer.new(:char, FuseOpt.size * (opts.size + 1), false)
45
+ opts.map.with_index { |(template, key), i| FuseOpt.new(native + (i * FuseOpt.size)).fill(template, key) }
46
+ FuseOpt.new(native + (opts.size * FuseOpt.size)).null
47
+
48
+ native
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
5
+ module Libfuse
6
+ attach_function :fuse_notify_poll, [:pointer], :int
7
+ attach_function :fuse_pollhandle_destroy, [:pointer], :void
8
+
9
+ class << self
10
+ # @!visibility private
11
+
12
+ # @!method fuse_notify_poll(ph)
13
+ # @!method fuse_pollhandle_destroy(ph)
14
+ end
15
+
16
+ # struct fuse_poll_handle
17
+ # @todo build a filsystem that uses poll and implement an appropriate ruby interface
18
+ # @see https://libfuse.github.io/doxygen/poll_8c.html
19
+ class FusePollHandle
20
+ extend FFI::DataConverter
21
+ native_type :pointer
22
+
23
+ class << self
24
+ # @!visibility private
25
+ def from_native(ptr, _ctx)
26
+ # TODO: we may need a weakref cache on ptr.address so that we don't create different ruby ph for the same
27
+ # address, and call destroy on the first one that goes out of scope.
28
+ new(ptr)
29
+ end
30
+
31
+ # @!visibility private
32
+ def to_native(value, _ctx)
33
+ value.ph
34
+ end
35
+
36
+ # @!visibility private
37
+ def finalizer(pollhandle)
38
+ proc { Libfuse.fuse_pollhandle_destroy(pollhandle) }
39
+ end
40
+ end
41
+
42
+ # @!visibility private
43
+ attr_reader :ph
44
+
45
+ # @!visibility private
46
+ def initialize(pollhandle)
47
+ @ph = pollhandle
48
+ ObjectSpace.define_finalizer(self, self.class.finalizer(pollhandle))
49
+ end
50
+
51
+ # @see FuseOperations#poll
52
+ def notify_poll
53
+ Libfuse.fuse_notify_poll(ph)
54
+ end
55
+ alias notify notify_poll
56
+ alias fuse_notify_poll notify_poll
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require_relative '../../ffi/accessors'
5
+ require_relative 'version'
6
+
7
+ module FFI
8
+ # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
9
+ module Libfuse
10
+ extend FFI::Library
11
+
12
+ # The fuse library to load from 'LIBFUSE' environment variable if set, otherwise prefer Fuse3 over Fuse2
13
+ LIBFUSE = ENV['LIBFUSE'] || %w[libfuse3.so.3 libfuse.so.2]
14
+ ffi_lib(LIBFUSE)
15
+
16
+ # @!scope class
17
+ # @!method fuse_version()
18
+ # @return [Integer] the fuse version
19
+ # See {FUSE_VERSION} which captures this result in a constant
20
+
21
+ attach_function :fuse_version, [], :int
22
+
23
+ # prior to 3.10 this is Major * 10 + Minor, after 3.10 and later is Major * 100 + Minor
24
+ # @return [Integer] the version of libfuse
25
+ FUSE_VERSION = fuse_version
26
+
27
+ fv_split = FUSE_VERSION >= 300 ? 100 : 10 # since 3.10
28
+
29
+ # @return [Integer] the FUSE major version
30
+ FUSE_MAJOR_VERSION = FUSE_VERSION / fv_split
31
+
32
+ # @return [Integer] the FUSE minor version
33
+ FUSE_MINOR_VERSION = FUSE_VERSION % fv_split
34
+
35
+ if FUSE_MAJOR_VERSION == 2 && FFI::Platform::IS_GNU
36
+ require_relative '../gnu_extensions'
37
+
38
+ extend(GNUExtensions)
39
+ # libfuse2 has busted symbols
40
+ ffi_lib_versions(%w[FUSE_2.9.1 FUSE_2.9 FUSE_2.8 FUSE_2.7 FUSE_2.6 FUSE_2.5 FUSE_2.4 FUSE_2.3 FUSE_2.2])
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'thread_pool'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ # A JobPool is a ThreadPool whose worker threads are consuming from a Queue
8
+ class JobPool
9
+ # Create a Job Pool
10
+ # @param [Hash<Symbol,Object>] options
11
+ # @see ThreadPool.new
12
+ # @param [Proc] worker the unit of work that will be yielded the scheduled jobs
13
+ def initialize(**options, &worker)
14
+ @jq = Queue.new
15
+ @tp = ThreadPool.new(**options) { (args = @jq.pop) && worker.call(*args) }
16
+ end
17
+
18
+ # Schedule a job
19
+ # @param [Array<Object>] args
20
+ # @return [self]
21
+ def schedule(*args)
22
+ @jq.push(args)
23
+ self
24
+ end
25
+ alias << schedule
26
+ alias push schedule
27
+
28
+ # Close the JobPool
29
+ # @return [self]
30
+ def close
31
+ @jq.close
32
+ self
33
+ end
34
+
35
+ # Join the JobPool
36
+ # @return [self]
37
+ def join(&block)
38
+ @tp.join(&block)
39
+ self
40
+ end
41
+
42
+ # @see ThreadPool#list
43
+ def list
44
+ @tp.list
45
+ end
46
+
47
+ # @see ThreadPool#group
48
+ def group
49
+ @tp.group
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fuse_common'
4
+ require_relative 'fuse_args'
5
+ require_relative 'fuse_operations'
6
+
7
+ module FFI
8
+ module Libfuse
9
+ # Controls the main run loop for a FUSE filesystem
10
+ module Main
11
+ class << self
12
+ # Main function of FUSE
13
+ #
14
+ # This function:
15
+ #
16
+ # - parses command line options - see {fuse_parse_cmdline}
17
+ # and exits immediately if help or version options were processed
18
+ # - installs signal handlers for INT, HUP, TERM to unmount and exit filesystem
19
+ # - installs custom signal handlers if operations implements {fuse_traps}
20
+ # - creates a fuse handle mounted with registered operations - see {fuse_create}
21
+ # - calls either the single-threaded (option -s) or the multi-threaded event loop - see {FuseCommon#run}
22
+ #
23
+ # @param [Array<String>] argv mount.fuse arguments
24
+ # expects progname, mountpoint, options....
25
+ # @param [FuseArgs] args
26
+ # alternatively constructed args
27
+ # @param [Object|FuseOperations] operations
28
+ # something that responds to the fuse callbacks and optionally our abstract configuration methods
29
+ # @param [Object] private_data
30
+ # any data to be made available to the {FuseOperations#init} callback
31
+ #
32
+ # @return [Integer] suitable for process exit code
33
+ def fuse_main(*argv, operations:, args: argv, private_data: nil)
34
+ run_args = fuse_parse_cmdline(args: args, handler: operations)
35
+ return 2 unless run_args
36
+
37
+ fuse_args = run_args.delete(:args)
38
+ mountpoint = run_args.delete(:mountpoint)
39
+
40
+ fuse = fuse_create(mountpoint, args: fuse_args, operations: operations, private_data: private_data)
41
+
42
+ return 0 if run_args[:show_help] || run_args[:show_version]
43
+ return 2 if !fuse || !mountpoint
44
+
45
+ return unless fuse
46
+
47
+ warn run_args.to_s if run_args[:debug]
48
+
49
+ fuse.run(**run_args)
50
+ end
51
+
52
+ # Parse command line arguments
53
+ #
54
+ # - parses standard command line options (-d -s -h -V)
55
+ # will call {fuse_debug}, {fuse_version}, {fuse_help} if implemented by handler
56
+ # - parses custom options if handler implements {fuse_options} and {fuse_opt_proc}
57
+ # - records signal handlers if operations implements {fuse_traps}
58
+ # - parses standard fuse mount options
59
+ #
60
+ # @param [Array<String>] argv mount.fuse arguments
61
+ # expects progname, [fsname,] mountpoint, options.... from mount.fuse3
62
+ # @param [FuseArgs] args
63
+ # alternatively constructed args
64
+ # @param [Object] handler
65
+ # something that responds to our abstract configuration methods
66
+ # @param [Object] private_data passed to handler.fuse_opt_proc
67
+ #
68
+ # @return [Hash<Symbol,Object>]
69
+ # * fsname [String]: the fsspec from /etc/fstab
70
+ # * mountpoint [String]: the mountpoint argument
71
+ # * args [FuseArgs]: remaining fuse_args to pass to {fuse_create}
72
+ # * show_help [Boolean]: -h or --help
73
+ # * show_version [Boolean]: -v or --version
74
+ # * debug [Boolean]: -d
75
+ # * others are options to pass to {FuseCommon#run}
76
+ def fuse_parse_cmdline(*argv, args: argv, handler: nil, private_data: nil)
77
+ args = fuse_init_args(args)
78
+
79
+ # Parse args and print cmdline help
80
+ run_args = Fuse.parse_cmdline(args, handler: handler)
81
+
82
+ # process custom options
83
+ if %i[fuse_options fuse_opt_proc].all? { |m| handler.respond_to?(m) }
84
+ parse_ok = args.parse!(handler.fuse_options, private_data) do |*p_args|
85
+ handler.fuse_opt_proc(*p_args)
86
+ end
87
+ return unless parse_ok
88
+ end
89
+
90
+ run_args[:traps] = handler.fuse_traps if handler.respond_to?(:fuse_traps)
91
+
92
+ args.parse!(RUN_OPTIONS, run_args) { |*opt_args| hash_opt_proc(*opt_args, discard: %i[native max_threads]) }
93
+
94
+ run_args[:args] = args
95
+ run_args
96
+ end
97
+
98
+ # @return [FuseCommon|nil] the mounted filesystem or nil if not mounted
99
+ def fuse_create(mountpoint, *argv, operations:, args: nil, private_data: nil)
100
+ args = fuse_init_args(args || argv.unshift(mountpoint))
101
+
102
+ operations = FuseOperations.new(delegate: operations) unless operations.is_a?(FuseOperations)
103
+
104
+ fuse = Fuse.new(mountpoint, args, operations, private_data)
105
+ fuse if fuse.mounted?
106
+ end
107
+
108
+ # Helper fuse_opt_proc function to capture options into a hash
109
+ #
110
+ # See {FuseArgs.parse!}
111
+ def hash_opt_proc(hash, arg, key, _out, discard: [])
112
+ return :keep if %i[unmatched non_option].include?(key)
113
+
114
+ hash[key] = arg =~ /=/ ? arg.split('=', 2).last : true
115
+ discard.include?(key) ? :discard : :keep
116
+ end
117
+
118
+ # @!visibility private
119
+
120
+ # Version text
121
+ def version
122
+ "#{name}: #{VERSION}"
123
+ end
124
+
125
+ def fuse_init_args(args)
126
+ if args.is_a?(Array)
127
+ args = args.map(&:to_s) # handle mountpoint as Pathname etc..
128
+
129
+ # https://github.com/libfuse/libfuse/issues/621 handle "source" field sent from /etc/fstab via mount.fuse3
130
+ # if arg[1] and arg[2] are both non option fields then replace arg1 with -ofsname=<arg1>
131
+ unless args.size <= 2 || args[1]&.start_with?('-') || args[2]&.start_with?('-')
132
+ args[1] = "-ofsname=#{args[1]}"
133
+ end
134
+ args = FuseArgs.create(*args)
135
+ end
136
+
137
+ return args if args.is_a?(FuseArgs)
138
+
139
+ raise ArgumentError "fuse main args: must be Array<String> or #{FuseArgs.class.name}"
140
+ end
141
+ end
142
+
143
+ # @!group Abstract Configuration
144
+
145
+ # @!method fuse_options
146
+ # @abstract
147
+ # @return [Hash] custom option schema
148
+ # @see FuseArgs#parse!
149
+
150
+ # @!method fuse_opt_proc(data,arg,key,out)
151
+ # @abstract
152
+ # Process custom options
153
+ # @see FuseArgs#parse!
154
+
155
+ # @!method fuse_traps
156
+ # @abstract
157
+ # @return [Hash] map of signal name or number to signal handler as per Signal.trap
158
+
159
+ # @!method fuse_version
160
+ # @abstract
161
+ # @return [String] a custom version string to output with -V option
162
+
163
+ # @!method fuse_help
164
+ # @abstract
165
+ # @return [String] help text to explain custom options to show with -h option
166
+
167
+ # @!method fuse_debug(enabled)
168
+ # @abstract
169
+ # Indicate to the filesystem whether debugging option is in use.
170
+ # @param [Boolean] enabled if -d option is in use
171
+ # @return [void]
172
+
173
+ # @!endgroup
174
+
175
+ # @!visibility private
176
+
177
+ # Standard help options
178
+ STANDARD_OPTIONS = {
179
+ '-h' => :show_help, '--help' => :show_help,
180
+ '-d' => :debug, 'debug' => :debug,
181
+ '-V' => :show_version, '--version' => :show_version
182
+ }.freeze
183
+
184
+ # Custom options that control how the fuse loop runs
185
+ RUN_OPTIONS = STANDARD_OPTIONS.merge(
186
+ {
187
+ 'native' => :native, # Use native libfuse functions for the process loop, primarily for testing
188
+ 'max_threads=' => :max_active,
189
+ 'remember=' => :remember
190
+ }
191
+ ).freeze
192
+
193
+ # Help text
194
+ HELP = <<~END_HELP
195
+ #{name} options:
196
+ -o max_threads maximum number of worker threads
197
+ END_HELP
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../fuse_operations'
4
+
5
+ module FFI
6
+ module Libfuse
7
+ module Test
8
+ # A FuseOperations that holds callback procs in a Hash rather than FFI objects and allows for direct invocation of
9
+ # callback methods
10
+ # @!parse FuseOperations
11
+ class Operations
12
+ include FuseCallbacks
13
+
14
+ def initialize(delegate:, fuse_wrappers: [])
15
+ @callbacks = {}
16
+ initialize_callbacks(delegate: delegate, wrappers: fuse_wrappers)
17
+ end
18
+
19
+ # @!visibility private
20
+ def [](member)
21
+ @callbacks[member]
22
+ end
23
+
24
+ # @!visibility private
25
+ def []=(member, value)
26
+ @callbacks[member] = value
27
+ end
28
+
29
+ # @!visibility private
30
+ def members
31
+ FuseOperations.members
32
+ end
33
+
34
+ private
35
+
36
+ # Allow the fuse operations to be called directly - useful for testing
37
+ # @todo some fancy wrapper to convert tests using Fuse2 signatures when Fuse3 is the loaded library
38
+ # and vice-versa
39
+ def method_missing(method, *args)
40
+ callback = callback?(method) && self[method]
41
+ return super unless callback
42
+
43
+ callback.call(*args)
44
+ end
45
+
46
+ def respond_to_missing?(method, _private = false)
47
+ self[method] && callback?(method)
48
+ end
49
+
50
+ def callback?(method)
51
+ callback_members.include?(method)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end