ffi-libfuse 0.1.0.rc20220550 → 0.3.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d71e73d4261d20d3e8318fa01517342da232c0167f4febd73249fb99bb314e15
4
- data.tar.gz: 5d478d73897b3d590b1c23ddc656f416574865a53db88bb078ddc949c310bc81
3
+ metadata.gz: 688ea85b8e5ee83a4baae3055c14026e65e929c2e22a8d9a2d0e983584e8bd7b
4
+ data.tar.gz: 552a053bea357d44ccf6fa2fcc12d311116d66320e113d7de7acb929a8ac539e
5
5
  SHA512:
6
- metadata.gz: bfc6ff5b7a9e706be919eaf35fbdfe7b3c2575c68e03008f2381c723ab6896f9826047427e78e8915b90985c0ce26e04e83e639817dd5b7f40bbe1adc69e8aa8
7
- data.tar.gz: ac2c8aa63ee627f73cd2d8a06071273bbbc8af64f9ae21862ff6f9bc235c7a0aa031c4fe601aea2b7e3ad4f93ef8ffd248fba35fbcf7cbabf422c37727995f72
6
+ metadata.gz: ca1a1f2b2b9573c523583f0ee9d8fa05fecbbaf4ba36fbe73b4db5090626b8fc984091880677b4654caacda25d6e073b0a679b57539f6faecfd4c62f5bc45257
7
+ data.tar.gz: f8cb9a7d82ac0be86249066b9b69d2cfe03be3e6eb028881b16e715766b2a0f9dbba46ba2478310be75677e2b8c188f0b26235bbfa491dc42d9dff0ca904840e
data/.yardopts CHANGED
@@ -1,3 +1,3 @@
1
1
  --markup=markdown
2
2
  -
3
- CHANGES.md
3
+ CHANGELOG.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ ## [0.3.3](https://github.com/lwoggardner/ffi-libfuse/compare/v0.0.1...v0.3.3) (2023-01-07)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * Support downstream RFuse/RFuseFS
9
+
10
+ ### Features
11
+
12
+ * FFI::Libfuse::Filesystem - base filesystems ([5b19005](https://github.com/lwoggardner/ffi-libfuse/commit/5b19005c4b1ff2237b85c4854f481ea6e3625c62))
13
+
14
+
15
+ ### Code Refactoring
16
+
17
+ * Support downstream RFuse/RFuseFS ([e6b3fb5](https://github.com/lwoggardner/ffi-libfuse/commit/e6b3fb552b8881dbf28f014617b7412f2542aaa3))
18
+
19
+
20
+ ### Miscellaneous Chores
21
+
22
+ * **github:** release 0.3.3 ([b54a56f](https://github.com/lwoggardner/ffi-libfuse/commit/b54a56f3f93f15c7684aa2cb2c2dd38c9d033e7f))
23
+
24
+ ## [0.3.2](https://github.com/lwoggardner/ffi-libfuse/compare/v0.3.0...v0.3.2) (2023-01-07)
25
+
26
+
27
+ ### Miscellaneous Chores
28
+
29
+ * fix release-please action ([fd55030](https://github.com/lwoggardner/ffi-libfuse/commit/fd550301248ebd9616da457ef8c2d88a7e55f819))
30
+
31
+ ## [0.3.0](https://github.com/lwoggardner/ffi-libfuse/compare/v0.2.1...v0.3.0) (2023-01-07)
32
+
33
+
34
+ ### Miscellaneous Chores
35
+
36
+ * fix release-please action ([57d43e9](https://github.com/lwoggardner/ffi-libfuse/commit/57d43e9cac552b1b36469092a6278058893cadc4))
37
+
38
+ ## [0.2.1](https://github.com/lwoggardner/ffi-libfuse/compare/v0.1.0...v0.2.1) (2023-01-07)
39
+
40
+ ### Miscellaneous Chores
41
+
42
+ * release 0.2.1 ([bb0ef37](https://github.com/lwoggardner/ffi-libfuse/commit/bb0ef37c05a41c6b51a14e5cae292b2d7b75ef1c))
43
+
44
+ ## [0.1.0](https://github.com/lwoggardner/ffi-libfuse/compare/v0.0.1...v0.1.0) (2023-01-07)
45
+
46
+ ### ⚠ BREAKING CHANGES
47
+
48
+ * Support downstream RFuse/RFuseFS
49
+ * Changed option parsing.
50
+
51
+ {FFI::Libfuse::Main#fuse_options} takes a FuseArgs parameter and fuse_opt_proc is not used
52
+
53
+ ### Features
54
+
55
+ * FFI::Libfuse::Filesystem - base filesystems ([5b19005](https://github.com/lwoggardner/ffi-libfuse/commit/5b19005c4b1ff2237b85c4854f481ea6e3625c62))
56
+
57
+ ### Code Refactoring
58
+
59
+ * Support downstream RFuse/RFuseFS ([e6b3fb5](https://github.com/lwoggardner/ffi-libfuse/commit/e6b3fb552b8881dbf28f014617b7412f2542aaa3))
60
+ * Test on OSX with macFuse
data/lib/ffi/flock.rb CHANGED
@@ -12,18 +12,20 @@ module FFI
12
12
  SeekWhenceShort = enum :short, seek_whence
13
13
  SeekWhence = enum :int, seek_whence
14
14
 
15
- LockType = enum :short, %i[f_rdlck f_wrlck f_unlck]
16
- LockCmd = enum :int, [:f_getlk, 5, :f_setlk, 6, :f_setlkw, 7]
15
+ LockType = enum :short, %i[rdlck wrlck unlck]
16
+ LockCmd = enum :int, [:getlk, 5, :setlk, 6, :setlkw, 7]
17
17
  end
18
18
 
19
19
  include(Accessors)
20
20
 
21
- layout(type: Enums::LockType, whence: Enums::SeekWhenceShort, start: :off_t, len: :off_t, pid: :pid_t)
21
+ layout(l_type: Enums::LockType, l_whence: Enums::SeekWhenceShort, l_start: :off_t, l_len: :off_t, l_pid: :pid_t)
22
22
 
23
- ffi_attr_reader :type, :whence, :start, :len, :pid
23
+ l_members = members.grep(/^l_/).map { |m| m[2..].to_sym }
24
+
25
+ ffi_attr_reader(*l_members, format: 'l_%s')
24
26
 
25
27
  # @!attribute [r] type
26
- # @return [Symbol] lock type, :f_rdlck, :f_wrlck, :f_unlck
28
+ # @return [Symbol] lock type, :rdlck, :wrlck, :unlck
27
29
 
28
30
  # @!attribute [r] whence
29
31
  # @return [Symbol] specifies what the offset is relative to, one of :seek_set, :seek_cur or :seek_end
@@ -22,14 +22,14 @@ module FFI
22
22
 
23
23
  def getattr(*args)
24
24
  fi = args.pop
25
- return fgetattr(*args, fi) if fi && respond_to?(:fgetattr)
25
+ return fgetattr(*args, fi) if fi && fuse_super_respond_to?(:fgetattr)
26
26
 
27
27
  super(*args)
28
28
  end
29
29
 
30
30
  def truncate(*args)
31
31
  fi = args.pop
32
- return ftruncate(*args, fi) if fi && respond_to?(:ftruncate)
32
+ return ftruncate(*args, fi) if fi && fuse_super_respond_to?(:ftruncate)
33
33
 
34
34
  super(*args)
35
35
  end
@@ -50,7 +50,7 @@ module FFI
50
50
  # but there is no way to handle OMIT
51
51
  def utimens(*args)
52
52
  args.pop
53
- super(*args)
53
+ super(*args) if defined?(super)
54
54
  end
55
55
 
56
56
  def init(*args)
@@ -73,6 +73,10 @@ module FFI
73
73
 
74
74
  super(*args, &block)
75
75
  end
76
+
77
+ def fuse_respond_to(fuse_callback)
78
+ super || (%i[truncate getattr].include?(fuse_callback) && fuse_super_respond_to?("f#{fuse_callback}"))
79
+ end
76
80
  end
77
81
 
78
82
  # @!visibility private
@@ -27,7 +27,7 @@ module FFI
27
27
 
28
28
  # @raise [Errno::EINTR] if the fuse request is marked as interrupted
29
29
  def interrupt_callback(*args)
30
- return -Errno::EINTR::Errno if Libfuse.fuse_interrupted?
30
+ Libfuse.raise_interrupt
31
31
 
32
32
  yield(*args)
33
33
  end
@@ -10,7 +10,7 @@ module FFI
10
10
  # @!visibility private
11
11
  def fuse_wrappers(*wrappers)
12
12
  wrappers << {
13
- wrapper: proc { |fm, *args, **_, &b| Safe.safe_callback(fm, *args, &b) },
13
+ wrapper: proc { |fm, *args, **_, &b| Safe.safe_callback(fm, *args, default_errno: default_errno, &b) },
14
14
  excludes: %i[init destroy]
15
15
  }
16
16
  return wrappers unless defined?(super)
@@ -18,8 +18,10 @@ module FFI
18
18
  super(*wrappers)
19
19
  end
20
20
 
21
- # Callbacks that are expected to return meaningful positive integers
22
- MEANINGFUL_RETURN = %i[read write write_buf lseek copy_file_range getxattr listxattr].freeze
21
+ # @return [Integer] the default errno. ENOTRECOVERABLE unless overridden
22
+ def default_errno
23
+ defined?(super) ? super : Errno::ENOTRECOVERABLE::Errno
24
+ end
23
25
 
24
26
  module_function
25
27
 
@@ -27,30 +29,29 @@ module FFI
27
29
  #
28
30
  # @yieldreturn [SystemCallError] expected callback errors rescued to return equivalent -ve errno value
29
31
  # @yieldreturn [StandardError,ScriptError] unexpected callback errors are rescued
30
- # to return -Errno::ENOTRECOVERABLE after emitting backtrace to #warn
32
+ # to return -ve {default_errno} after emitting backtrace to #warn
31
33
  #
32
34
  # @yieldreturn [Integer]
33
35
  #
34
36
  # * -ve values returned directly
35
- # * +ve values returned directly for fuse_methods in {MEANINGFUL_RETURN} list
37
+ # * +ve values returned directly for fuse_methods in {FuseOperations.MEANINGFUL_RETURN} list
36
38
  # * otherwise returns 0
37
39
  #
38
40
  # @yieldreturn [Object] always returns 0 if no exception is raised
39
41
  #
40
- def safe_callback(fuse_method, *args)
42
+ def safe_callback(fuse_method, *args, default_errno: Errno::ENOTRECOVERABLE::Errno)
41
43
  result = yield(*args)
42
44
 
43
- return 0 unless result.is_a?(Integer)
44
- return 0 unless result.negative? || MEANINGFUL_RETURN.include?(fuse_method)
45
+ return result.to_i if FuseOperations.meaningful_return?(fuse_method)
45
46
 
46
- result
47
+ 0
47
48
  rescue SystemCallError => e
48
49
  -e.errno
49
50
  rescue StandardError, ScriptError => e
50
51
  # rubocop:disable Layout/LineLength
51
52
  warn ["FFI::Libfuse error in #{fuse_method}", *e.backtrace.reverse, "#{e.class.name}:#{e.message}"].join("\n\t")
52
53
  # rubocop:enable Layout/LineLength
53
- -Errno::ENOTRECOVERABLE::Errno
54
+ -default_errno.abs
54
55
  end
55
56
  end
56
57
  end
@@ -19,7 +19,7 @@ module FFI
19
19
  # @param [Integer] mode permissions for any dirs that need to be created
20
20
  # @yieldparam [String] the path component being created
21
21
  # @yieldreturn [FuseOperations] optionally a filesystem to mount at path, if the path did not previously exist
22
- def mkdir_p(path, mode = (0o0777 & ~FuseContext.get.umask), &mount_fs)
22
+ def mkdir_p(path, mode = (~FuseContext.get.umask & 0o0777), &mount_fs)
23
23
  return if root?(path) # nothing to make
24
24
 
25
25
  path.to_s.split('/')[1..].inject('') do |base_path, sub_dir|
@@ -71,7 +71,7 @@ module FFI
71
71
  # @return [Boolean] File exists at path and has zero size
72
72
  def empty_file?(path)
73
73
  s = stat(path)
74
- (s&.file? && s.size.zero?) || false
74
+ (s&.file? && s.size.zero?) || false # rubocop:disable Style/ZeroLengthPredicate
75
75
  end
76
76
 
77
77
  # Check if a directory is empty
@@ -182,6 +182,14 @@ module FFI
182
182
  end
183
183
 
184
184
  attr_reader :no_buf
185
+
186
+ # This class does not implement any fuse methods, ensure they are passed to method missing.
187
+ # eg Kernel.open
188
+ FFI::Libfuse::FuseOperations.fuse_callbacks.each do |c|
189
+ undef_method(c)
190
+ rescue StandardError
191
+ nil
192
+ end
185
193
  end
186
194
  end
187
195
  end
@@ -115,9 +115,7 @@ module FFI
115
115
  @fuse = nil if @fuse&.null?
116
116
  end
117
117
  ensure
118
- # if we unmount/destroy in the finalizer then the private_data object cannot be used in destory
119
- # as it's weakref will have been GC'd
120
- ObjectSpace.define_finalizer(self, self.class.finalize_fuse(@fuse, @mountpoint, @ch))
118
+ define_finalizer
121
119
  end
122
120
 
123
121
  # [IO] /dev/fuse file descriptor for use with IO.select
@@ -150,6 +148,16 @@ module FFI
150
148
  c = @ch
151
149
  @ch = nil
152
150
  Libfuse.fuse_unmount2(mountpoint, c)
151
+ ensure
152
+ # Can't unmount twice
153
+ define_finalizer
154
+ end
155
+
156
+ def define_finalizer
157
+ # if we unmount/destroy in the finalizer then the private_data object cannot be used in destory
158
+ # as it's weakref will have been GC'd
159
+ ObjectSpace.undefine_finalizer(self)
160
+ ObjectSpace.define_finalizer(self, self.class.finalize_fuse(@fuse, @mountpoint, @ch))
153
161
  end
154
162
  end
155
163
 
@@ -87,10 +87,10 @@ module FFI
87
87
  $stdout.puts "\n#{handler.fuse_help}" if handler.respond_to?(:fuse_help)
88
88
  end
89
89
 
90
- def finalize_fuse(fuse)
90
+ def finalize_fuse(fuse, mounted)
91
91
  proc do
92
92
  if fuse
93
- Libfuse.fuse_unmount3(fuse)
93
+ Libfuse.fuse_unmount3(fuse) if mounted
94
94
  Libfuse.fuse_destroy(fuse)
95
95
  end
96
96
  end
@@ -101,7 +101,7 @@ module FFI
101
101
 
102
102
  # Have we requested an unmount (note not actually checking if OS sees the fs as mounted)
103
103
  def mounted?
104
- session && !fuse_exited?
104
+ session && !fuse_exited? && @mounted
105
105
  end
106
106
 
107
107
  def initialize(mountpoint, args, operations, private_data)
@@ -119,9 +119,7 @@ module FFI
119
119
 
120
120
  @mounted = @fuse && Libfuse.fuse_mount3(@fuse, @mountpoint).zero?
121
121
  ensure
122
- # if we unmount/destroy in the finalizer then the private_data object cannot be used in destroy
123
- # as it's weakref will have been GC'd
124
- ObjectSpace.define_finalizer(self, self.class.finalize_fuse(@fuse))
122
+ define_finalizer
125
123
  end
126
124
 
127
125
  def fuse_exited?
@@ -152,7 +150,19 @@ module FFI
152
150
  end
153
151
 
154
152
  def unmount
155
- Libfuse.fuse_unmount3(@fuse) if @mounted && @fuse && !@fuse.null?
153
+ return unless @mounted && @fuse && !@fuse.null?
154
+
155
+ Libfuse.fuse_unmount3(@fuse)
156
+ @mounted = false
157
+ ensure
158
+ define_finalizer
159
+ end
160
+
161
+ def define_finalizer
162
+ # if we unmount/destroy in the finalizer then the private_data object cannot be used in destroy
163
+ # as it's weakref will have been GC'd
164
+ ObjectSpace.undefine_finalizer(self)
165
+ ObjectSpace.define_finalizer(self, self.class.finalize_fuse(@fuse, @mounted))
156
166
  end
157
167
  end
158
168
 
@@ -14,11 +14,13 @@ module FFI
14
14
  # Create a fuse_args struct from command line options
15
15
  # @param [Array<String>] argv command line args
16
16
  #
17
- # first arg is expected to be program name
17
+ # first arg is expected to be program name and is ignored by fuse_opt_parse
18
+ # it is handled specially only in fuse_parse_cmdline (ie no subtype is given)
18
19
  # @return [FuseArgs]
19
20
  # @example
20
21
  # FFI::Libfuse::FuseArgs.create($0,*ARGV)
21
22
  def self.create(*argv)
23
+ argv.unshift('ffi-libfuse') if argv.empty? || argv[0].start_with?('-')
22
24
  new.fill(*argv)
23
25
  end
24
26
 
@@ -162,7 +164,7 @@ module FFI
162
164
  [opt, arg[opt.rstrip.length..].lstrip]
163
165
  else
164
166
  warn "FuseOptProc error - Cannot match option for #{arg}"
165
- next -1
167
+ next FUSE_OPT_PROC_RETURN.fetch(:error)
166
168
  end
167
169
 
168
170
  safe_opt_proc(key: key, value: value, match: match, data: data, out: out, &block)
@@ -100,7 +100,7 @@ module FFI
100
100
  # @param [Boolean] foreground
101
101
  # @param [Boolean] single_thread
102
102
  def run_native(foreground: true, single_thread: true, **options)
103
- raise 'Cannot run deamonized native multi-thread fuse_loop' if !single_thread && !foreground
103
+ raise 'Cannot run daemonized native multi-thread fuse_loop' if !single_thread && !foreground
104
104
 
105
105
  clear_default_traps
106
106
  (se = session) && Libfuse.fuse_set_signal_handlers(se)
@@ -165,7 +165,7 @@ module FFI
165
165
  # @api private
166
166
  # Ruby implementation of single threaded fuse loop
167
167
  def fuse_loop(**_options)
168
- fuse_process until fuse_exited?
168
+ safe_fuse_process until fuse_exited?
169
169
  end
170
170
 
171
171
  # @api private
@@ -179,11 +179,17 @@ module FFI
179
179
  ThreadPool.new(name: 'FuseThread', max_idle: max_idle_threads.to_i, max_active: max_threads&.to_i) do
180
180
  raise StopIteration if fuse_exited?
181
181
 
182
- fuse_process
182
+ safe_fuse_process
183
183
  end.join
184
184
  end
185
185
 
186
186
  # @!visibility private
187
+
188
+ def safe_fuse_process
189
+ # sometimes we get null on unmount, and exit needs a chance to finish to avoid hangs
190
+ fuse_process || (sleep(0.1) && false)
191
+ end
192
+
187
193
  def teardown
188
194
  return unless @fuse
189
195
 
@@ -209,7 +215,6 @@ module FFI
209
215
  sleep 0.2 if mac_fuse?
210
216
 
211
217
  Libfuse.fuse_exit(@fuse)
212
-
213
218
  true
214
219
  end
215
220
  end
@@ -33,7 +33,10 @@ module FFI
33
33
  bitmask :lock_op, [:lock_sh, 0, :lock_ex, 2, :lock_nb, 4, :lock_un, 8]
34
34
  bitmask :falloc_mode, %i[keep_size punch_hole no_hide_stale collapse_range zero_range insert_range unshare_range]
35
35
  bitmask :flags_mask, %i[nullpath_ok nopath utime_omit_ok] if FUSE_MAJOR_VERSION < 3
36
- enum :xattr, [:xattr_create, 1, :xattr_replace]
36
+
37
+ # @!visibility private
38
+ XAttr = enum :xattr, [:xattr_create, 1, :xattr_replace]
39
+
37
40
  callback :fill_dir_t, fill_dir_t_args, :int
38
41
 
39
42
  # The file system operations as specified in libfuse.
@@ -47,6 +50,14 @@ module FFI
47
50
  class FuseOperations < FFI::Struct
48
51
  include FuseCallbacks
49
52
 
53
+ # Callbacks that are expected to return meaningful positive integers
54
+ MEANINGFUL_RETURN = %i[read write write_buf lseek copy_file_range getxattr listxattr].freeze
55
+
56
+ # @return [Boolean] true if fuse_callback expects a meaningful integer return
57
+ def self.meaningful_return?(fuse_callback)
58
+ MEANINGFUL_RETURN.include?(fuse_callback)
59
+ end
60
+
50
61
  # Container to dynamically build up the operations layout which is dependent on the loaded libfuse version
51
62
  op = {}
52
63
 
@@ -213,6 +224,7 @@ module FFI
213
224
  op[:truncate] = [:off_t]
214
225
  op[:truncate] << FuseFileInfo.by_ref if FUSE_MAJOR_VERSION >= 3
215
226
 
227
+ # Not directly implemented see utimens
216
228
  # int (*utime) (const char *, struct utimbuf *);
217
229
  op[:utime] = [:pointer] if FUSE_MAJOR_VERSION < 3
218
230
 
@@ -568,7 +580,7 @@ module FFI
568
580
  # @param [String] path
569
581
  # @param [FuseFileInfo] fuse_file_info
570
582
  # For checking lock ownership, the 'fuse_file_info->owner' argument must be used.
571
- # @param [Symbol] cmd either :f_getlck, :f_setlck or :f_setlkw.
583
+ # @param [Symbol] cmd either :getlck, :setlck or :setlkw.
572
584
  # @param [Flock] flock
573
585
  # For the meaning of fields in 'struct flock' see the man page for fcntl(2). The whence field will always
574
586
  # be set to :seek_set.
@@ -19,7 +19,7 @@ module FFI
19
19
  def fill(template, value)
20
20
  str_ptr = FFI::MemoryPointer.from_string(template)
21
21
  self[:template] = str_ptr
22
- self[:offset] = (2**(8 * FFI::Type::INT.size)) - 1 # -(1U) in a LONG!!
22
+ self[:offset] = (2**(FFI::Type::INT.size * 8)) - 1 # -(1U) in a LONG!!
23
23
  self[:value] = value.to_i
24
24
  self
25
25
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+
5
+ module FFI
6
+ # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
7
+ module Libfuse
8
+ # @!visibility private
9
+
10
+ SEMVER_TAG_REGEX = /^v\d+\.\d+\.\d+/.freeze
11
+
12
+ # branches the format is refs/heads/<branch_name>,
13
+ # tags it is refs/tags/<tag_name>.
14
+ # for pull requests it is refs/pull/<pr_number>/merge,
15
+ GIT_REF_TYPES = { 'heads' => :branch, 'tags' => :tag, 'pull' => :pull }.freeze
16
+
17
+ def self.git_ref(env: ENV)
18
+ ref = env.fetch('GIT_REF') do
19
+ # get branch ref, or detached head ref to tag
20
+ `git symbolic-ref HEAD 2>/dev/null || git name-rev HEAD | awk '{ gsub(/[\\^~@].*$/,"",$2); printf("refs/%s\\n",$2)}'`.strip # rubocop:disable Layout/LineLength
21
+ rescue StandardError
22
+ nil
23
+ end
24
+
25
+ return [ref, nil] unless ref&.start_with?('refs/')
26
+
27
+ _refs, ref_type, ref_name = ref.split('/', 3)
28
+ [ref_name, GIT_REF_TYPES[ref_type]]
29
+ end
30
+
31
+ def self.gem_version(main_branch: 'main', version: VERSION, env: ENV)
32
+ ref_name, ref_type = git_ref(env: env)
33
+
34
+ version =
35
+ case ref_type
36
+ when :branch
37
+ ref_name == main_branch ? [version] : [version, ref_name]
38
+ when :tag
39
+ SEMVER_TAG_REGEX.match?(ref_name) ? [ref_name[1..]] : [version, ref_name]
40
+ when :pull
41
+ pr_number, merge, _rest = ref_name.split('/')
42
+ # GITHUB_BASE_REF The name of the base ref or target branch of the pull request in a workflow run
43
+ base_ref = env.fetch('GIT_BASE_REF', 'undefined')
44
+ [version, base_ref, "#{merge}#{pr_number}"]
45
+ else
46
+ [version, 'pre', ref_name]
47
+ end.select { |p| p && !p.empty? }.join('.').tr('//_-', '')
48
+
49
+ [version, ref_name, ref_type]
50
+ end
51
+
52
+ GEM_VERSION, GIT_REF_NAME, GIT_REF_TYPE = gem_version
53
+ end
54
+ end
@@ -9,6 +9,19 @@ module FFI
9
9
  # Controls the main run loop for a FUSE filesystem
10
10
  module Main
11
11
  class << self
12
+ # Builds default argument list for #{fuse_main} regardless of being called directly or from mount.fuse3
13
+ #
14
+ # @param [Array<String>] extra_args additional arguments to add to $0 and *ARGV
15
+ # @return [Array<String>]
16
+ # @see https://github.com/libfuse/libfuse/issues/621
17
+ def default_args(*extra_args)
18
+ args = ARGV.dup
19
+
20
+ # If called from mount.fuse3 we already have a 'source' argument which should go at args[0]
21
+ args.unshift($0) unless args.size >= 2 && args[0..1].all? { |a| !a.start_with?('-') }
22
+ args.concat(extra_args)
23
+ end
24
+
12
25
  # Main function of FUSE
13
26
  #
14
27
  # This function:
@@ -31,7 +44,7 @@ module FFI
31
44
  # any data to be made available to the {FuseOperations#init} callback
32
45
  #
33
46
  # @return [Integer] suitable for process exit code
34
- def fuse_main(*argv, operations:, args: argv.any? ? argv : [$0, *ARGV], private_data: nil)
47
+ def fuse_main(*argv, operations:, args: argv.any? ? argv : default_args, private_data: nil)
35
48
  run_args = fuse_parse_cmdline(args: args, handler: operations)
36
49
  return 2 unless run_args
37
50
 
@@ -63,7 +76,7 @@ module FFI
63
76
  # - parses standard fuse mount options
64
77
  #
65
78
  # @param [Array<String>] argv mount.fuse arguments
66
- # expects progname, [fsname,] mountpoint, options.... from mount.fuse3
79
+ # expects progname, mountpoint, options....
67
80
  # @param [FuseArgs] args
68
81
  # alternatively constructed args
69
82
  # @param [Object] handler
@@ -75,7 +88,7 @@ module FFI
75
88
  # * show_version [Boolean]: -v or --version
76
89
  # * debug [Boolean]: -d
77
90
  # * others are options to pass to {FuseCommon#run}
78
- def fuse_parse_cmdline(*argv, args: argv, handler: nil)
91
+ def fuse_parse_cmdline(*argv, args: argv.any? ? argv : default_args, handler: nil)
79
92
  args = fuse_init_args(args)
80
93
 
81
94
  # Parse args and print cmdline help
@@ -94,16 +107,15 @@ module FFI
94
107
 
95
108
  # @return [FuseCommon|nil] the mounted filesystem or nil if not mounted
96
109
  def fuse_create(mountpoint, *argv, operations:, args: nil, private_data: nil)
97
- args = fuse_init_args(args || argv.unshift(mountpoint))
110
+ args = fuse_init_args(args || argv)
98
111
 
99
112
  operations = FuseOperations.new(delegate: operations) unless operations.is_a?(FuseOperations)
100
113
 
101
- fuse = Fuse.new(mountpoint, args, operations, private_data)
114
+ fuse = Fuse.new(mountpoint.to_s, args, operations, private_data)
102
115
  fuse if fuse.mounted?
103
116
  end
104
117
 
105
118
  # @!visibility private
106
-
107
119
  def fuse_configure(operations:, show_help: false, show_version: false, **_)
108
120
  return true unless operations.respond_to?(:fuse_configure) && !show_help && !show_version
109
121
 
@@ -120,20 +132,15 @@ module FFI
120
132
  end
121
133
  end
122
134
 
123
- # Version text
135
+ # @!visibility private
124
136
  def version
125
137
  "#{name}: #{VERSION}"
126
138
  end
127
139
 
140
+ private
141
+
128
142
  def fuse_init_args(args)
129
143
  if args.is_a?(Array)
130
- args = args.map(&:to_s) # handle mountpoint as Pathname etc..
131
-
132
- # https://github.com/libfuse/libfuse/issues/621 handle "source" field sent from /etc/fstab via mount.fuse3
133
- # if arg[1] and arg[2] are both non option fields then replace arg1 with -ofsname=<arg1>
134
- unless args.size <= 2 || args[1]&.start_with?('-') || args[2]&.start_with?('-')
135
- args[1] = "-ofsname=#{args[1]}"
136
- end
137
144
  warn "FuseArgs: #{args.join(' ')}" if args.include?('-d')
138
145
  args = FuseArgs.create(*args)
139
146
  end
@@ -143,8 +150,6 @@ module FFI
143
150
  raise ArgumentError "fuse main args: must be Array<String> or #{FuseArgs.class.name}"
144
151
  end
145
152
 
146
- private
147
-
148
153
  def parse_run_options(args, run_args)
149
154
  args.parse!(RUN_OPTIONS) do |key:, value:, **|
150
155
  run_args[key] = value
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../libfuse'
4
+ require 'open3'
5
+ require 'sys-filesystem'
6
+
7
+ # utilities for running tests with fuse filesystems
8
+ module FFI
9
+ module Libfuse
10
+ # Can be included test classes to assist with running/debugging filesystems
11
+ module TestHelper
12
+ # rubocop:disable Metrics/AbcSize
13
+ # rubocop:disable Metrics/MethodLength
14
+
15
+ # Runs the fuse loop on a pre configured fuse filesystem
16
+ # @param [FuseOperations] operations
17
+ # @param [Array<String>] args to pass to {FFI::Libfuse::Main.fuse_create}
18
+ # @param [Hash] options to pass to {FFI::Libfuse::FuseCommon.run}
19
+ # @yield [mnt]
20
+ # caller can execute and test file operations using mnt and ruby File/Dir etc
21
+ # the block is run in a forked process and is successful unless an exception is raised
22
+ # @yieldparam [String] mnt the temporary direct used as the mount point
23
+ # @raise [Error] if unexpected state is found during operations
24
+ # @return [void]
25
+ def with_fuse(operations, *args, **options)
26
+ raise ArgumentError, 'Needs block' unless block_given?
27
+
28
+ # ignore MacOS special files
29
+ args << '-onoappledouble,noapplexattr' if mac_fuse?
30
+ safe_fuse do |mnt|
31
+ # Start the fork before loading fuse (for MacOS)
32
+ fpid = Process.fork do
33
+ sleep 2.5 # Give fuse a chance to start
34
+ yield mnt
35
+ end
36
+
37
+ fuse = FFI::Libfuse::Main.fuse_create(mnt, *args, operations: operations)
38
+ raise FFI::Libfuse::Error, 'No fuse object returned from fuse_create' unless fuse
39
+
40
+ # Rake owns INT
41
+ fuse.default_traps.delete(:TERM)
42
+ fuse.default_traps.delete(:INT)
43
+
44
+ raise FFI::Libfuse::Error, 'fuse object is not mounted?' unless fuse.mounted?
45
+
46
+ t = Thread.new { fuse.run(foreground: true, **options) }
47
+
48
+ # TODO: Work out why waitpid2 hangs on mac unless the process has already finished
49
+ sleep 10 if mac_fuse?
50
+
51
+ _pid, block_status = Process.waitpid2(fpid)
52
+ block_exit = block_status.exitstatus
53
+ fuse.exit('fuse_helper')&.join
54
+ run_result = t.value
55
+
56
+ raise FFI::Libfuse::Error, 'fuse is still mounted after fuse.exit' if fuse.mounted?
57
+ raise FFI::Libfuse::Error, "forked file operations failed with #{block_exit}" unless block_exit.zero?
58
+ raise FFI::Libfuse::Error, "fuse run failed #{run_result}" unless run_result.zero?
59
+
60
+ if !mac_fuse? && mounted?(mnt)
61
+ raise FFI::Libfuse::Error, "OS reports fuse is still mounted at #{mnt} after fuse.exit"
62
+ end
63
+
64
+ true
65
+ end
66
+ end
67
+
68
+ # Runs a filesystem in a separate process
69
+ # @param [String] filesystem path to filesystem executable
70
+ # @param [Array<String>] args to pass the filesystem
71
+ # @param [Hash<String,String>] env environment to run the filesystem under
72
+ # @yield [mnt]
73
+ # caller can execute and test file operations using mnt and ruby File/Dir etc
74
+ # @yieldparam [String] mnt the temporary direct used as the mount point
75
+ # @raise [Error] if unexpected state is found during operations
76
+ # @return [Array] stdout, stderr, exit code as captured by Open3.capture3
77
+ # @note if the filesystem is configured to daemonize then no output will be captured
78
+ def run_filesystem(filesystem, *args, env: {})
79
+ fsname = File.basename(filesystem)
80
+ safe_fuse do |mnt|
81
+ t = Thread.new do
82
+ if defined?(Bundler)
83
+ Bundler.with_unbundled_env do
84
+ Open3.capture3(env, 'bundle', 'exec', filesystem.to_s, mnt, "-ofsname=#{fsname}", *args, binmode: true)
85
+ end
86
+ else
87
+ Open3.capture3(env, filesystem.to_s, mnt, "-ofsname=#{fsname}", *args, binmode: true)
88
+ end
89
+ end
90
+ sleep 1
91
+
92
+ begin
93
+ if block_given?
94
+ raise Error, "#{fsname} not mounted at #{mnt}" unless mounted?(mnt, fsname)
95
+
96
+ yield mnt
97
+ end
98
+ # rubocop:disable Lint/RescueException
99
+ # Minitest::Assertion and other test assertion classes are not derived from StandardError
100
+ rescue Exception => _err
101
+ # rubocop:enable Lint/RescueException
102
+ unmount(mnt) if mounted?(mnt)
103
+ o, e, _s = t.value
104
+ warn "Errors\n#{e}" unless e.empty?
105
+ warn "Output\n#{o}" unless o.empty?
106
+ raise
107
+ end
108
+
109
+ unmount(mnt) if mounted?(mnt)
110
+ o, e, s = t.value
111
+ [o, e, s.exitstatus]
112
+ end
113
+ end
114
+ # rubocop:enable Metrics/AbcSize
115
+ # rubocop:enable Metrics/MethodLength
116
+
117
+ def mounted?(mnt, _filesystem = '.*')
118
+ type, prefix = mac_fuse? ? %w[macfuse /private] : %w[fuse]
119
+ mounts = Sys::Filesystem.mounts.select { |m| m.mount_type == type }
120
+ mounts.detect { |m| m.mount_point == "#{prefix}#{mnt}" }
121
+ end
122
+
123
+ def unmount(mnt)
124
+ if mac_fuse?
125
+ system("diskutil unmount force #{mnt} >/dev/null 2>&1")
126
+ else
127
+ system("fusermount -zu #{mnt} >/dev/null 2>&1")
128
+ end
129
+ end
130
+
131
+ def safe_fuse
132
+ Dir.mktmpdir('ffi-libfuse-spec') do |mountpoint|
133
+ yield mountpoint
134
+ ensure
135
+ # Attempt to force unmount.
136
+ unmount(mountpoint) if mounted?(mountpoint)
137
+ end
138
+ end
139
+
140
+ def mac_fuse?
141
+ FFI::Platform::IS_MAC
142
+ end
143
+ end
144
+ end
145
+ end
@@ -3,6 +3,6 @@
3
3
  module FFI
4
4
  # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
5
5
  module Libfuse
6
- VERSION = '0.1.0'
6
+ VERSION = '0.3.3'
7
7
  end
8
8
  end
data/lib/ffi/libfuse.rb CHANGED
@@ -19,12 +19,12 @@ module FFI
19
19
  # Filesystems that want full control (eg to take advantage of multi-threaded operations) should call
20
20
  # {Main.fuse_main} instead
21
21
  # @note These may change between major versions
22
- DEFAULT_ARGS = [$0, '-s', '-odefault_permissions', *ARGV].freeze
22
+ DEFAULT_ARGS = %w[-s -odefault_permissions].freeze
23
23
 
24
24
  class << self
25
25
  # Filesystem entry point
26
26
  # @see Main.fuse_main
27
- def fuse_main(*argv, operations:, args: argv.any? ? argv : DEFAULT_ARGS, private_data: nil)
27
+ def fuse_main(*argv, operations:, args: argv.any? ? argv : Main.default_args(*DEFAULT_ARGS), private_data: nil)
28
28
  Main.fuse_main(args: args, operations: operations, private_data: private_data) || -1
29
29
  end
30
30
  alias main fuse_main
@@ -32,6 +32,7 @@ module FFI
32
32
  return nil if object_id.zero?
33
33
 
34
34
  _ptr, obj = RubyObject.cache[object_id]
35
+ obj = obj.__getobj__ if obj.is_a?(WeakRef)
35
36
  obj
36
37
  end
37
38
  end
@@ -66,6 +67,8 @@ module FFI
66
67
  raise TypeError, "No RubyObject stored at #{ptr.address}" unless cache.key?(ptr.address.object_id)
67
68
 
68
69
  _ptr, obj = cache[ptr.get(:long, 0)]
70
+ # unwrap as the object gets used
71
+ obj = obj.__getobj__ if obj.is_a?(WeakRef)
69
72
  obj
70
73
  end
71
74
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffi-libfuse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.rc20220550
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grant Gardner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-29 00:00:00.000000000 Z
11
+ date: 2023-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -130,7 +130,7 @@ extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
132
  - ".yardopts"
133
- - CHANGES.md
133
+ - CHANGELOG.md
134
134
  - LICENSE
135
135
  - README.md
136
136
  - lib/ffi/accessors.rb
@@ -178,10 +178,10 @@ files:
178
178
  - lib/ffi/libfuse/fuse_opt.rb
179
179
  - lib/ffi/libfuse/fuse_poll_handle.rb
180
180
  - lib/ffi/libfuse/fuse_version.rb
181
+ - lib/ffi/libfuse/gem_version.rb
181
182
  - lib/ffi/libfuse/job_pool.rb
182
183
  - lib/ffi/libfuse/main.rb
183
- - lib/ffi/libfuse/test.rb
184
- - lib/ffi/libfuse/test/operations.rb
184
+ - lib/ffi/libfuse/test_helper.rb
185
185
  - lib/ffi/libfuse/thread_pool.rb
186
186
  - lib/ffi/libfuse/version.rb
187
187
  - lib/ffi/ruby_object.rb
@@ -200,7 +200,7 @@ homepage:
200
200
  licenses:
201
201
  - MIT
202
202
  metadata:
203
- source_code_uri: http://github.com/lwoggardner/ffi-libfuse
203
+ source_code_uri: https://github.com/lwoggardner/ffi-libfuse
204
204
  rubygems_mfa_required: 'true'
205
205
  post_install_message:
206
206
  rdoc_options: []
@@ -213,11 +213,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
213
  version: 2.7.0
214
214
  required_rubygems_version: !ruby/object:Gem::Requirement
215
215
  requirements:
216
- - - ">"
216
+ - - ">="
217
217
  - !ruby/object:Gem::Version
218
- version: 1.3.1
218
+ version: '0'
219
219
  requirements: []
220
- rubygems_version: 3.1.2
220
+ rubygems_version: 3.1.6
221
221
  signing_key:
222
222
  specification_version: 4
223
223
  summary: FFI Bindings for Libfuse
data/CHANGES.md DELETED
@@ -1,14 +0,0 @@
1
- 0.1.0 / 2022-04
2
- ------------------
3
-
4
- #### BREAKING changes
5
- * Changed option parsing.
6
-
7
- {FFI::Libfuse::Main#fuse_options} now takes a FuseArgs parameter and fuse_opt_proc is not used
8
-
9
- #### New Features
10
- * Implemented helper filesystems in {FFI::Libfuse::Filesystem}
11
-
12
- #### Fixes
13
- * Test on OSX with macFuse
14
- * Lots
@@ -1,56 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'test/operations'