ffi-libfuse 0.1.0.rc20220550 → 0.3.4

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: d59c87631dfe38e9dc49b36f829acdd704165162deff3bad6479f3394f348657
4
+ data.tar.gz: 01ab562dd537f1f11f7e3c454e49c5df3ef1ce5cfd6cb8b1aa6b5e6a7f89a4c0
5
5
  SHA512:
6
- metadata.gz: bfc6ff5b7a9e706be919eaf35fbdfe7b3c2575c68e03008f2381c723ab6896f9826047427e78e8915b90985c0ce26e04e83e639817dd5b7f40bbe1adc69e8aa8
7
- data.tar.gz: ac2c8aa63ee627f73cd2d8a06071273bbbc8af64f9ae21862ff6f9bc235c7a0aa031c4fe601aea2b7e3ad4f93ef8ffd248fba35fbcf7cbabf422c37727995f72
6
+ metadata.gz: ae437c153a84155c86a9e2d71d4dd1c6234879fae7553676efacd311e26cae3cf653dd4439b2ec4ab584cd01698636915fc6c9cefd14a2f045c2b19e4077e60b
7
+ data.tar.gz: af5fab46502ff889710facc930ee59d7bb27e129da3c70f0d5089c1fa85e29bbfe3ec91b01f75db907da5f8be220755edec6b4b3976421ce0454cc43830387da
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,34 @@
1
+ # Changelog
2
+
3
+ ## [0.3.4](https://github.com/lwoggardner/ffi-libfuse/compare/v0.3.3...v0.3.4) (2023-01-08)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **github:** allow downstream gems to use gem_version etc ([73f3b92](https://github.com/lwoggardner/ffi-libfuse/commit/73f3b92f5e8a1f86a9f6053b71470d7c113e6d19))
9
+
10
+ ## [0.3.3](https://github.com/lwoggardner/ffi-libfuse/compare/v0.1.0...v0.3.3) (2023-01-07)
11
+
12
+ ### Miscellaneous Chores
13
+
14
+ * **github:** release 0.3.3 ([b54a56f](https://github.com/lwoggardner/ffi-libfuse/commit/b54a56f3f93f15c7684aa2cb2c2dd38c9d033e7f))
15
+
16
+ Using github actions
17
+
18
+ ## 0.1.0 (2023-01-07)
19
+
20
+ ### ⚠ BREAKING CHANGES
21
+
22
+ * Support downstream RFuse/RFuseFS
23
+ * Changed option parsing.
24
+
25
+ {FFI::Libfuse::Main#fuse_options} takes a FuseArgs parameter and fuse_opt_proc is not used
26
+
27
+ ### Features
28
+
29
+ * FFI::Libfuse::Filesystem - base filesystems ([5b19005](https://github.com/lwoggardner/ffi-libfuse/commit/5b19005c4b1ff2237b85c4854f481ea6e3625c62))
30
+
31
+ ### Code Refactoring
32
+
33
+ * Support downstream RFuse/RFuseFS ([e6b3fb5](https://github.com/lwoggardner/ffi-libfuse/commit/e6b3fb552b8881dbf28f014617b7412f2542aaa3))
34
+ * 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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
5
+ module Libfuse
6
+ # @!visibility private
7
+ class GemHelper
8
+ SEMVER_TAG_REGEX = /^v\d+\.\d+\.\d+/.freeze
9
+
10
+ # branches the format is refs/heads/<branch_name>,
11
+ # tags it is refs/tags/<tag_name>.
12
+ # for pull requests it is refs/pull/<pr_number>/merge,
13
+ GIT_REF_TYPES = { 'heads' => :branch, 'tags' => :tag, 'pull' => :pull }.freeze
14
+
15
+ class << self
16
+ # set when install'd.
17
+ attr_accessor :instance
18
+
19
+ def install_tasks(main_branch:, version:)
20
+ require 'bundler/gem_tasks'
21
+ include Rake::DSL if defined? Rake::DSL
22
+ new(main_branch: main_branch, version: version).install
23
+ end
24
+
25
+ def git_ref(env: ENV)
26
+ ref = env.fetch('GIT_REF') do
27
+ # get branch ref, or detached head ref to tag
28
+ `git symbolic-ref HEAD 2>/dev/null || git name-rev HEAD | awk '{ gsub(/[\\^~@].*$/,"",$2); printf("refs/%s\\n",$2)}'`.strip # rubocop:disable Layout/LineLength
29
+ rescue StandardError
30
+ nil
31
+ end
32
+
33
+ return [ref, nil] unless ref&.start_with?('refs/')
34
+
35
+ _refs, ref_type, ref_name = ref.split('/', 3)
36
+ [ref_name, GIT_REF_TYPES[ref_type]]
37
+ end
38
+
39
+ def gem_version(main_branch:, version:, env: ENV)
40
+ ref_name, ref_type = git_ref(env: env)
41
+
42
+ version =
43
+ case ref_type
44
+ when :branch
45
+ ref_name == main_branch ? [version] : [version, ref_name]
46
+ when :tag
47
+ SEMVER_TAG_REGEX.match?(ref_name) ? [ref_name[1..]] : [version, ref_name]
48
+ when :pull
49
+ pr_number, merge, _rest = ref_name.split('/')
50
+ # GITHUB_BASE_REF The name of the base ref or target branch of the pull request in a workflow run
51
+ base_ref = env.fetch('GIT_BASE_REF', 'undefined')
52
+ [version, base_ref, "#{merge}#{pr_number}"]
53
+ else
54
+ [version, 'pre', ref_name]
55
+ end.select { |p| p && !p.empty? }.join('.').tr('//_-', '')
56
+
57
+ [version, ref_name, ref_type]
58
+ end
59
+ end
60
+
61
+ attr_reader :version, :main_branch, :git_ref_name, :git_ref_type, :gem_version, :gem_version_tag
62
+
63
+ def initialize(version:, main_branch:)
64
+ @version = version
65
+ @main_branch = main_branch
66
+ @gem_version, @git_ref_name, @git_ref_type =
67
+ self.class.gem_version(main_branch: main_branch, version: version)
68
+ @gem_version_tag = "v#{@gem_version}"
69
+ end
70
+
71
+ def install
72
+ task 'release:guard_clean' => %i[release_guard_tag]
73
+
74
+ desc 'Version info'
75
+ task :version do
76
+ v, gv = [version, gem_version].map { |ver| Gem::Version.new(ver) }
77
+ msg = "VERSION='#{v}' GEM_VERSION='#{gv}'"
78
+ raise "Mismatched versions - #{msg}" unless gv.release == v
79
+
80
+ puts msg
81
+ end
82
+
83
+ task release_guard_tag: [:version] do
84
+ # If we're on a tag then tag must be tag for this version
85
+ if git_ref_type == :tag && git_ref_name != gem_version_tag
86
+ raise "Checkout is tag '#{git_ref_name}' but does not match the gem version '#{gem_version}'"
87
+ end
88
+
89
+ # BASH expression - test tag does not exist OR exists and points at HEAD
90
+ cmd = '[ -z "$(git tag -l ${V_TAG})" ] || git tag --points-at HEAD | grep "^${V_TAG}$" > /dev/null'
91
+ unless system({ 'V_TAG' => gem_version_tag }, cmd)
92
+ raise "Tag #{gem_version_tag} exists but does not point at HEAD"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'gem_helper'
5
+
6
+ module FFI
7
+ # Ruby FFI Binding for [libfuse](https://github.com/libfuse/libfuse)
8
+ module Libfuse
9
+ # @visibility private
10
+ MAIN_BRANCH = 'main'
11
+ # @!visibility private
12
+ GEM_VERSION, = GemHelper.gem_version(main_branch: MAIN_BRANCH, version: VERSION)
13
+ end
14
+ 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.4'
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.4
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-08 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,11 @@ 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_helper.rb
182
+ - lib/ffi/libfuse/gem_version.rb
181
183
  - lib/ffi/libfuse/job_pool.rb
182
184
  - lib/ffi/libfuse/main.rb
183
- - lib/ffi/libfuse/test.rb
184
- - lib/ffi/libfuse/test/operations.rb
185
+ - lib/ffi/libfuse/test_helper.rb
185
186
  - lib/ffi/libfuse/thread_pool.rb
186
187
  - lib/ffi/libfuse/version.rb
187
188
  - lib/ffi/ruby_object.rb
@@ -200,7 +201,7 @@ homepage:
200
201
  licenses:
201
202
  - MIT
202
203
  metadata:
203
- source_code_uri: http://github.com/lwoggardner/ffi-libfuse
204
+ source_code_uri: https://github.com/lwoggardner/ffi-libfuse
204
205
  rubygems_mfa_required: 'true'
205
206
  post_install_message:
206
207
  rdoc_options: []
@@ -213,11 +214,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
214
  version: 2.7.0
214
215
  required_rubygems_version: !ruby/object:Gem::Requirement
215
216
  requirements:
216
- - - ">"
217
+ - - ">="
217
218
  - !ruby/object:Gem::Version
218
- version: 1.3.1
219
+ version: '0'
219
220
  requirements: []
220
- rubygems_version: 3.1.2
221
+ rubygems_version: 3.1.6
221
222
  signing_key:
222
223
  specification_version: 4
223
224
  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'