ffi-libfuse 0.1.0.rc20220550 → 0.3.3

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