ffi-libfuse 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
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