ffi-libfuse 0.0.1.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/README.md +100 -0
- data/lib/ffi/accessors.rb +145 -0
- data/lib/ffi/devt.rb +30 -0
- data/lib/ffi/flock.rb +47 -0
- data/lib/ffi/gnu_extensions.rb +115 -0
- data/lib/ffi/libfuse/ackbar.rb +112 -0
- data/lib/ffi/libfuse/adapter/context.rb +37 -0
- data/lib/ffi/libfuse/adapter/debug.rb +89 -0
- data/lib/ffi/libfuse/adapter/fuse2_compat.rb +91 -0
- data/lib/ffi/libfuse/adapter/fuse3_support.rb +87 -0
- data/lib/ffi/libfuse/adapter/interrupt.rb +37 -0
- data/lib/ffi/libfuse/adapter/pathname.rb +23 -0
- data/lib/ffi/libfuse/adapter/ruby.rb +334 -0
- data/lib/ffi/libfuse/adapter/safe.rb +58 -0
- data/lib/ffi/libfuse/adapter/thread_local_context.rb +36 -0
- data/lib/ffi/libfuse/adapter.rb +79 -0
- data/lib/ffi/libfuse/callbacks.rb +61 -0
- data/lib/ffi/libfuse/fuse2.rb +159 -0
- data/lib/ffi/libfuse/fuse3.rb +162 -0
- data/lib/ffi/libfuse/fuse_args.rb +166 -0
- data/lib/ffi/libfuse/fuse_buffer.rb +155 -0
- data/lib/ffi/libfuse/fuse_callbacks.rb +48 -0
- data/lib/ffi/libfuse/fuse_cmdline_opts.rb +44 -0
- data/lib/ffi/libfuse/fuse_common.rb +249 -0
- data/lib/ffi/libfuse/fuse_config.rb +205 -0
- data/lib/ffi/libfuse/fuse_conn_info.rb +211 -0
- data/lib/ffi/libfuse/fuse_context.rb +79 -0
- data/lib/ffi/libfuse/fuse_file_info.rb +100 -0
- data/lib/ffi/libfuse/fuse_loop_config.rb +43 -0
- data/lib/ffi/libfuse/fuse_operations.rb +870 -0
- data/lib/ffi/libfuse/fuse_opt.rb +54 -0
- data/lib/ffi/libfuse/fuse_poll_handle.rb +59 -0
- data/lib/ffi/libfuse/fuse_version.rb +43 -0
- data/lib/ffi/libfuse/job_pool.rb +53 -0
- data/lib/ffi/libfuse/main.rb +200 -0
- data/lib/ffi/libfuse/test/operations.rb +56 -0
- data/lib/ffi/libfuse/test.rb +3 -0
- data/lib/ffi/libfuse/thread_pool.rb +147 -0
- data/lib/ffi/libfuse/version.rb +8 -0
- data/lib/ffi/libfuse.rb +24 -0
- data/lib/ffi/ruby_object.rb +95 -0
- data/lib/ffi/stat/constants.rb +29 -0
- data/lib/ffi/stat/native.rb +50 -0
- data/lib/ffi/stat/time_spec.rb +137 -0
- data/lib/ffi/stat.rb +96 -0
- data/lib/ffi/stat_vfs.rb +81 -0
- data/lib/ffi/struct_array.rb +39 -0
- data/lib/ffi/struct_wrapper.rb +100 -0
- data/sample/memory_fs.rb +189 -0
- data/sample/no_fs.rb +69 -0
- metadata +165 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'fuse_version'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
# Its a trap!
|
8
|
+
#
|
9
|
+
# Implements the self-pipe trick for handling signals in multi-threaded ruby programs
|
10
|
+
class Ackbar
|
11
|
+
# Read side of the self-pipe
|
12
|
+
# @return [IO] for use with IO.select only
|
13
|
+
# @see monitor
|
14
|
+
# @see next
|
15
|
+
attr_reader :pr
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Run a block with the given traps, restoring the original traps on completion
|
19
|
+
# @param [Hash] traps map of signal to handler as per Signal.trap
|
20
|
+
# @yieldparam [Ackbar] signals
|
21
|
+
# @return [Object] the result of the block
|
22
|
+
def trap(traps, force: false)
|
23
|
+
signals = new(traps, force: force)
|
24
|
+
yield signals
|
25
|
+
ensure
|
26
|
+
signals&.restore
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param [Hash<Symbol|String|Integer,String|Proc>] traps
|
31
|
+
# Map of signal or signo to signal handler as per Signal.trap
|
32
|
+
# @param [Boolean] force
|
33
|
+
# If not set traps that are not currently set to 'DEFAULT' will be ignored
|
34
|
+
def initialize(traps, force: false, signal: Signal)
|
35
|
+
@signal = signal
|
36
|
+
@traps = traps.transform_keys { |k| signame(k) }
|
37
|
+
@pr, @pw = ::IO.pipe
|
38
|
+
@monitor = nil
|
39
|
+
@restore = @traps.map { |(sig, handler)| [sig, trap(sig, handler, force: force)] }.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
# Handle the next available signal on the pipe (without blocking)
|
43
|
+
# @return [Boolean] false if pipe is closed, true if pipe would block
|
44
|
+
# @return [Array<String,Object>] signal name, signal result when a signal has been processed
|
45
|
+
def next
|
46
|
+
signame = signal.signame(@pr.read_nonblock(1).unpack1('c'))
|
47
|
+
t = @traps[signame]
|
48
|
+
[signame, t.arity.zero? ? t.call : t.call(signame)]
|
49
|
+
rescue EOFError
|
50
|
+
# the signal pipe writer is closed - we are exiting.
|
51
|
+
@pr.close
|
52
|
+
false
|
53
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
54
|
+
# oh well...
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
# Restore traps as they were at #new and close the write side of the pipe
|
59
|
+
def restore
|
60
|
+
@restore.each_pair { |signame, handler| signal.trap(signame, handler) }
|
61
|
+
@restore = nil
|
62
|
+
@pw.close
|
63
|
+
@monitor&.join
|
64
|
+
end
|
65
|
+
alias close restore
|
66
|
+
|
67
|
+
# Start a thread to monitor for signals optionally yields between signals
|
68
|
+
# @param [String] name The name of the monitor thread
|
69
|
+
# @yieldreturn [Integer] timeout in seconds or nil to wait until signal or {restore}
|
70
|
+
# @return [Thread] the monitor thread
|
71
|
+
def monitor(name: 'SignalMonitor')
|
72
|
+
@monitor ||= Thread.new do
|
73
|
+
Thread.current.name = name
|
74
|
+
loop do
|
75
|
+
timeout = block_given? ? yield : nil
|
76
|
+
|
77
|
+
ready, _ignore_writable, _errors = ::IO.select([@pr], [], [], timeout)
|
78
|
+
|
79
|
+
break if ready&.include?(@pr) && !self.next
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @!visibility private
|
85
|
+
# normalize signal identifiers to things that are keys in Signal.list
|
86
|
+
def signame(sig)
|
87
|
+
sig.is_a?(Integer) ? signal.signame(sig) : sig.to_s.upcase.sub(/^SIG/, '')
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
attr_reader :signal
|
93
|
+
|
94
|
+
def trap(signame, handler, force: false)
|
95
|
+
prev =
|
96
|
+
case handler
|
97
|
+
when String, Symbol
|
98
|
+
signal.trap(signame, handler.to_s)
|
99
|
+
else
|
100
|
+
signal.trap(signame) { |signo| send_signal(signo) }
|
101
|
+
end
|
102
|
+
|
103
|
+
signal.trap(signame, prev) unless force || prev == 'DEFAULT'
|
104
|
+
prev
|
105
|
+
end
|
106
|
+
|
107
|
+
def send_signal(signo)
|
108
|
+
@pw&.write([signo].pack('c')) unless @pw&.closed?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../fuse_context'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
module Adapter
|
8
|
+
# Wrapper module to inject {FuseContext} as first arg to each callback method (except :destroy)
|
9
|
+
#
|
10
|
+
# {ThreadLocalContext} may be a less intrusive means to make the context available to callbacks
|
11
|
+
module Context
|
12
|
+
# @!visibility private
|
13
|
+
def fuse_wrappers(*wrappers)
|
14
|
+
wrappers.unshift(
|
15
|
+
{
|
16
|
+
wrapper: proc { |_fm, *args, **_, &b| self.class.context_callback(*args, &b) },
|
17
|
+
excludes: %i[destroy]
|
18
|
+
}
|
19
|
+
)
|
20
|
+
return wrappers unless defined?(super)
|
21
|
+
|
22
|
+
super(*wrappers)
|
23
|
+
end
|
24
|
+
|
25
|
+
module_function
|
26
|
+
|
27
|
+
# @yieldparam [FuseContext] ctx
|
28
|
+
# @yieldparam [Array] *args
|
29
|
+
def context_callback(*args)
|
30
|
+
ctx = FuseContext.get
|
31
|
+
ctx = nil if ctx.null?
|
32
|
+
yield ctx, *args
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module Libfuse
|
5
|
+
module Adapter
|
6
|
+
# Debug callbacks
|
7
|
+
#
|
8
|
+
# When included in a filesystem class, and if debugging is enabled via {Main#fuse_debug}, then installs a wrapper
|
9
|
+
# via #{FuseCallbacks#fuse_wrappers} to log callbacks via #warn
|
10
|
+
module Debug
|
11
|
+
DEFAULT_FORMAT = "%<p>s %<n>s %<t>s %<m>s(%<a>s)\n\t=> %<r>s"
|
12
|
+
|
13
|
+
# @return [Boolean] true if debug is enabled
|
14
|
+
def debug?
|
15
|
+
@debug
|
16
|
+
end
|
17
|
+
|
18
|
+
# Configure debug output
|
19
|
+
# @abstract
|
20
|
+
# @return [Hash<Symbol,String>] options to pass to {debug_callback}
|
21
|
+
def debug_config
|
22
|
+
{}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
def fuse_wrappers(*wrappers)
|
27
|
+
conf = { prefix: self.class.name }.merge!(debug_config)
|
28
|
+
# validate config for bad formats, strftime etc
|
29
|
+
Debug.debug_format(:test_debug, [], :result, **conf)
|
30
|
+
wrappers << proc { |fm, *args, &b| Debug.debug_callback(fm, *args, **conf, &b) } if debug?
|
31
|
+
return wrappers unless defined?(super)
|
32
|
+
|
33
|
+
super(*wrappers)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @!visibility private
|
37
|
+
def fuse_debug(enabled)
|
38
|
+
super if defined?(super)
|
39
|
+
@debug = enabled
|
40
|
+
end
|
41
|
+
|
42
|
+
module_function
|
43
|
+
|
44
|
+
# Debug fuse method, args and result
|
45
|
+
# @param [Symbol] fuse_method the callback name
|
46
|
+
# @param [Array] args callback arguments
|
47
|
+
# @param [Hash<Symbol,String>] options see {debug} for defaults
|
48
|
+
# @option options [String] prefix
|
49
|
+
# @option options [String] strftime a date time format
|
50
|
+
# @option options [String] format format string with fields
|
51
|
+
#
|
52
|
+
# * %<n>: Time.now - use strftime option to control time format
|
53
|
+
# * %<t>: Thread name
|
54
|
+
# * %<m>: Fuse method
|
55
|
+
# * %<a>: Comma separate list of arguments
|
56
|
+
# * %<r>: Result of the method call (or any error raised)
|
57
|
+
# * %<p>: The value of prefix option
|
58
|
+
def debug_callback(fuse_method, *args, **options)
|
59
|
+
debug(fuse_method, args, yield(*args), **options)
|
60
|
+
rescue SystemCallError => e
|
61
|
+
# expected behaviour
|
62
|
+
debug(fuse_method, args, "#{e.class.name}(errno=#{e.errno}): #{e.message}", **options)
|
63
|
+
raise
|
64
|
+
rescue StandardError, ScriptError => e
|
65
|
+
# unexpected, debug with backtrace
|
66
|
+
debug(fuse_method, args, (["#{e.class.name}: #{e.message}"] + e.backtrace).join("\n\t"), **options)
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
|
70
|
+
# @!visibility private
|
71
|
+
def debug(fuse_method, args, result, **options)
|
72
|
+
warn debug_format(fuse_method, args, result, **options)
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
# @!visibility private
|
77
|
+
def debug_format(fuse_method, args, result, prefix: 'DEBUG', strftime: '%FT%T%:z', format: DEFAULT_FORMAT)
|
78
|
+
format(format,
|
79
|
+
p: prefix,
|
80
|
+
n: Time.now.strftime(strftime),
|
81
|
+
t: Thread.current.name || Thread.current,
|
82
|
+
m: fuse_method,
|
83
|
+
a: args.map(&:to_s).join(','),
|
84
|
+
r: result)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module Libfuse
|
5
|
+
module Adapter
|
6
|
+
# Wrapper module to assist filesystem written for Fuse3 to be compatible with Fuse2
|
7
|
+
module Fuse2Compat
|
8
|
+
# @!visibility private
|
9
|
+
# Wrapper shim for fuse methods to ensure compatibility with Fuse2
|
10
|
+
module Fuse2Prepend
|
11
|
+
include Adapter
|
12
|
+
|
13
|
+
def getattr(path, stat, fuse_file_info = nil)
|
14
|
+
super(path, stat, fuse_file_info)
|
15
|
+
end
|
16
|
+
|
17
|
+
def truncate(path, size, fuse_file_info = nil)
|
18
|
+
super(path, size, fuse_file_info)
|
19
|
+
end
|
20
|
+
|
21
|
+
def init(fuse_conn_info, fuse_config = nil)
|
22
|
+
super(fuse_conn_info, fuse_config)
|
23
|
+
end
|
24
|
+
|
25
|
+
def chown(path, uid, gid, fuse_file_info = nil)
|
26
|
+
super(path, uid, gid, fuse_file_info)
|
27
|
+
end
|
28
|
+
|
29
|
+
def chmod(path, mode, fuse_file_info = nil)
|
30
|
+
super(path, mode, fuse_file_info)
|
31
|
+
end
|
32
|
+
|
33
|
+
def utimens(path, atime, mtime, fuse_file_info = nil)
|
34
|
+
super(path, atime, mtime, fuse_file_info)
|
35
|
+
end
|
36
|
+
|
37
|
+
def readdir(path, buffer, filler, offset, fuse_file_info, fuse_readdir_flag = 0)
|
38
|
+
filler = proc { |buf, name, stat, off = 0, _fuse_fill_dir_flag = 0| filler.call(buf, name, stat, off) }
|
39
|
+
super(path, buffer, filler, offset, fuse_file_info, fuse_readdir_flag)
|
40
|
+
end
|
41
|
+
|
42
|
+
def fgetattr(*args)
|
43
|
+
getattr(*args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def ftruncate(*args)
|
47
|
+
truncate(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fuse_respond_to?(fuse_method)
|
51
|
+
fuse_method = fuse_method[1..].to_sym if %i[fgetattr ftruncate].include?(fuse_method)
|
52
|
+
super(fuse_method)
|
53
|
+
end
|
54
|
+
|
55
|
+
def fuse_flags
|
56
|
+
res = defined?(super) ? super : []
|
57
|
+
if respond_to?(:init_fuse_config)
|
58
|
+
fuse_config = FuseConfig.new
|
59
|
+
init_fuse_config(fuse_config, :fuse2)
|
60
|
+
res << :nullpath_ok if fuse_config.nullpath_ok?
|
61
|
+
end
|
62
|
+
|
63
|
+
res
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @!visibility private
|
68
|
+
module Fuse3Prepend
|
69
|
+
def init(*args)
|
70
|
+
init_fuse_config(args.detect { |a| a.is_a?(FuseConfig) }) if respond_to?(:init_fuse_config)
|
71
|
+
super if defined?(super)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @!visibility private
|
76
|
+
def self.included(mod)
|
77
|
+
prepend_module = Libfuse.FUSE_MAJOR_VERSION == 2 ? Fuse2Prepend : Fuse3Prepend
|
78
|
+
mod.prepend(prepend_module)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @!method init_fuse_config(fuse_config,compat)
|
82
|
+
# @abstract
|
83
|
+
# Define this method to configure the fuse config object so that under Fuse2 the config options
|
84
|
+
# can be converted to appropriate flags.
|
85
|
+
#
|
86
|
+
# @param [FuseConfig] fuse_config the fuse config object
|
87
|
+
# @param [Symbol] compat either :fuse2 or :fuse3
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module Libfuse
|
5
|
+
module Adapter
|
6
|
+
# Wrapper module to assist filesystem written for Fuse2 to be compatible with Fuse3
|
7
|
+
#
|
8
|
+
# @note there are some deprecated Fuse2 features that have no forwards compatibility in Fuse3
|
9
|
+
#
|
10
|
+
# - flag nopath became fuse_config.nullpath_ok but it is not compatible since chmod/chown etc will now
|
11
|
+
# receive a null path but the filesystem cannot receive the new fuse_file_info arg
|
12
|
+
#
|
13
|
+
module Fuse3Support
|
14
|
+
# The actual module methods that are prepended
|
15
|
+
# @!visibility private
|
16
|
+
module Prepend
|
17
|
+
include Adapter
|
18
|
+
|
19
|
+
# Inner adapters received Fuse2 compatible args
|
20
|
+
def fuse3_compat?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def getattr(*args)
|
25
|
+
fi = args.pop
|
26
|
+
return fgetattr(*args, fi) if fi && respond_to?(:fgetattr)
|
27
|
+
|
28
|
+
super(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def truncate(*args)
|
32
|
+
fi = args.pop
|
33
|
+
return ftruncate(*args, fi) if fi && respond_to?(:ftruncate)
|
34
|
+
|
35
|
+
super(*args)
|
36
|
+
end
|
37
|
+
|
38
|
+
def chown(*args)
|
39
|
+
args.pop
|
40
|
+
super(*args)
|
41
|
+
end
|
42
|
+
|
43
|
+
def chmod(*args)
|
44
|
+
args.pop
|
45
|
+
super(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO: Fuse3 deprecated flag utime_omit_ok - which meant that UTIME_OMIT and UTIME_NOW are passed through
|
49
|
+
# instead of ????
|
50
|
+
# Strictly if the flag is not set this compat shim should convert utime now values to Time.now
|
51
|
+
# but there is no way to handle OMIT
|
52
|
+
def utimens(*args)
|
53
|
+
args.pop
|
54
|
+
super(*args)
|
55
|
+
end
|
56
|
+
|
57
|
+
def init(*args)
|
58
|
+
args.pop
|
59
|
+
|
60
|
+
# TODO: populate FuseConfig with output from fuse_flags/FuseConnInfo where appropriate
|
61
|
+
super(*args)
|
62
|
+
end
|
63
|
+
|
64
|
+
def readdir(*args, &block)
|
65
|
+
# swallow the flag arg unknown to fuse 2
|
66
|
+
args.pop
|
67
|
+
|
68
|
+
args = args.map do |a|
|
69
|
+
next a unless a.is_a?(FFI::Function)
|
70
|
+
|
71
|
+
# wrap the filler proc for the extra flags argument
|
72
|
+
proc { |buf, name, stat, off| a.call(buf, name, stat, off, 0) }
|
73
|
+
end
|
74
|
+
|
75
|
+
super(*args, &block)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# @!visibility private
|
80
|
+
def self.included(mod)
|
81
|
+
# We prepend our shim module so caller doesn't have to call super
|
82
|
+
mod.prepend(Prepend) if FUSE_MAJOR_VERSION > 2
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../fuse_context'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
module Adapter
|
8
|
+
# Wrapper module to handle interrupts
|
9
|
+
#
|
10
|
+
# Include this module if you want all requests to check for interruption before processing
|
11
|
+
#
|
12
|
+
# To handle interrupts only for specific callbacks just call {Libfuse.raise_interrupt} or
|
13
|
+
# {Libfuse.fuse_interrupted?} during callback processing rather than including this adapter
|
14
|
+
module Interrupt
|
15
|
+
# @!visibility private
|
16
|
+
def fuse_wrappers(*wrappers)
|
17
|
+
wrappers << {
|
18
|
+
wrapper: proc { |_fuse_method, *args, &b| Interrupt.interrupt_callback(*args, &b) },
|
19
|
+
excludes: %i[init destroy]
|
20
|
+
}
|
21
|
+
return wrappers unless defined?(super)
|
22
|
+
|
23
|
+
super(*wrappers)
|
24
|
+
end
|
25
|
+
|
26
|
+
module_function
|
27
|
+
|
28
|
+
# @raise [Errno::EINTR] if the fuse request is marked as interrupted
|
29
|
+
def interrupt_callback(*args)
|
30
|
+
return -Errno::EINTR::Errno if Libfuse.fuse_interrupted?
|
31
|
+
|
32
|
+
yield(*args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|