fanotify 1.0.0
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 +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +43 -0
- data/benchmark/events.rb +27 -0
- data/examples/audit_logger.rb +14 -0
- data/examples/lazy_fetch.rb +29 -0
- data/examples/malware_scan.rb +20 -0
- data/ext/fanotify/compat.h +235 -0
- data/ext/fanotify/constants.c +12 -0
- data/ext/fanotify/extconf.rb +10 -0
- data/ext/fanotify/fanotify.c +481 -0
- data/lib/fanotify/event.rb +215 -0
- data/lib/fanotify/file_handle.rb +12 -0
- data/lib/fanotify/mark.rb +6 -0
- data/lib/fanotify/notifier.rb +458 -0
- data/lib/fanotify/version.rb +5 -0
- data/lib/fanotify.rb +38 -0
- data/sig/fanotify.rbs +93 -0
- data/tools/constants.def +59 -0
- data/tools/dump_constants.c +12 -0
- data/tools/generate_constants.rb +18 -0
- data/tools/vm/Makefile +5 -0
- data/tools/vm/config-fragment +3 -0
- data/tools/vm/run.sh +14 -0
- metadata +70 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fanotify
|
|
4
|
+
CLASS_FLAGS = {
|
|
5
|
+
notif: FAN_CLASS_NOTIF,
|
|
6
|
+
content: FAN_CLASS_CONTENT,
|
|
7
|
+
pre_content: FAN_CLASS_PRE_CONTENT
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
REPORT_FLAGS = {
|
|
11
|
+
tid: FAN_REPORT_TID,
|
|
12
|
+
fid: FAN_REPORT_FID,
|
|
13
|
+
dir_fid: FAN_REPORT_DIR_FID,
|
|
14
|
+
name: FAN_REPORT_NAME,
|
|
15
|
+
dfid_name: FAN_REPORT_DFID_NAME,
|
|
16
|
+
target_fid: FAN_REPORT_TARGET_FID | FAN_REPORT_FID,
|
|
17
|
+
pidfd: FAN_REPORT_PIDFD
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
EVENT_FLAGS = {
|
|
21
|
+
rdonly: O_RDONLY,
|
|
22
|
+
wronly: O_WRONLY,
|
|
23
|
+
rdwr: O_RDWR,
|
|
24
|
+
largefile: O_LARGEFILE,
|
|
25
|
+
cloexec: O_CLOEXEC,
|
|
26
|
+
nonblock: O_NONBLOCK
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
EVENT_MASKS = {
|
|
30
|
+
access: FAN_ACCESS,
|
|
31
|
+
modify: FAN_MODIFY,
|
|
32
|
+
attrib: FAN_ATTRIB,
|
|
33
|
+
close_write: FAN_CLOSE_WRITE,
|
|
34
|
+
close_nowrite: FAN_CLOSE_NOWRITE,
|
|
35
|
+
open: FAN_OPEN,
|
|
36
|
+
moved_from: FAN_MOVED_FROM,
|
|
37
|
+
moved_to: FAN_MOVED_TO,
|
|
38
|
+
create: FAN_CREATE,
|
|
39
|
+
delete: FAN_DELETE,
|
|
40
|
+
delete_self: FAN_DELETE_SELF,
|
|
41
|
+
move_self: FAN_MOVE_SELF,
|
|
42
|
+
open_exec: FAN_OPEN_EXEC,
|
|
43
|
+
overflow: FAN_Q_OVERFLOW,
|
|
44
|
+
fs_error: FAN_FS_ERROR,
|
|
45
|
+
open_perm: FAN_OPEN_PERM,
|
|
46
|
+
access_perm: FAN_ACCESS_PERM,
|
|
47
|
+
open_exec_perm: FAN_OPEN_EXEC_PERM,
|
|
48
|
+
pre_access: FAN_PRE_ACCESS,
|
|
49
|
+
event_on_child: FAN_EVENT_ON_CHILD,
|
|
50
|
+
rename: FAN_RENAME,
|
|
51
|
+
ondir: FAN_ONDIR
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Owns a fanotify group and provides safe event iteration.
|
|
55
|
+
class Notifier
|
|
56
|
+
PERMISSION_EVENTS = %i[open_perm access_perm open_exec_perm pre_access].freeze
|
|
57
|
+
PERMISSION_MASK = PERMISSION_EVENTS.reduce(0) { |mask, event| mask | EVENT_MASKS.fetch(event) }
|
|
58
|
+
FID_EVENTS = %i[attrib create delete delete_self moved_from moved_to move_self rename].freeze
|
|
59
|
+
MARK_ACTIONS = {add: FAN_MARK_ADD, remove: FAN_MARK_REMOVE}.freeze
|
|
60
|
+
MARK_TARGETS = {inode: FAN_MARK_INODE, mount: FAN_MARK_MOUNT, filesystem: FAN_MARK_FILESYSTEM}.freeze
|
|
61
|
+
|
|
62
|
+
attr_reader :report
|
|
63
|
+
|
|
64
|
+
def self.open(**options)
|
|
65
|
+
notifier = new(**options)
|
|
66
|
+
return notifier unless block_given?
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
yield notifier
|
|
70
|
+
ensure
|
|
71
|
+
notifier.close
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize(**options)
|
|
76
|
+
klass = options.delete(:class) || :notif
|
|
77
|
+
@report = Array(options.delete(:report)).map(&:to_sym)
|
|
78
|
+
nonblock = options.key?(:nonblock) ? options.delete(:nonblock) : false
|
|
79
|
+
@nonblock = nonblock
|
|
80
|
+
unlimited_queue = options.key?(:unlimited_queue) ? options.delete(:unlimited_queue) : false
|
|
81
|
+
unlimited_marks = options.key?(:unlimited_marks) ? options.delete(:unlimited_marks) : false
|
|
82
|
+
audit = options.key?(:audit) ? options.delete(:audit) : false
|
|
83
|
+
event_flags = Array(options.delete(:event_flags) || %i[rdonly largefile cloexec]).map(&:to_sym)
|
|
84
|
+
@on_overflow = options.delete(:on_overflow)
|
|
85
|
+
@on_error = options.delete(:on_error)
|
|
86
|
+
@on_error = @on_error.nil? ? :allow : @on_error
|
|
87
|
+
@on_error = @on_error.to_sym if @on_error.respond_to?(:to_sym)
|
|
88
|
+
@response_timeout = options.key?(:response_timeout) ? options.delete(:response_timeout) : 5.0
|
|
89
|
+
raise ArgumentError, "unknown options: #{options.keys.join(', ')}" unless options.empty?
|
|
90
|
+
unless [nonblock, unlimited_queue, unlimited_marks, audit].all? { |value| value == true || value == false }
|
|
91
|
+
raise ArgumentError, "boolean options must be true or false"
|
|
92
|
+
end
|
|
93
|
+
unless @on_overflow.nil? || @on_overflow.respond_to?(:call)
|
|
94
|
+
raise ArgumentError, "on_overflow must be callable or nil"
|
|
95
|
+
end
|
|
96
|
+
raise ArgumentError, "on_error must be :allow or :deny" unless %i[allow deny].include?(@on_error)
|
|
97
|
+
unless @response_timeout.nil? || (@response_timeout.is_a?(Numeric) && @response_timeout.real? &&
|
|
98
|
+
@response_timeout.finite? && @response_timeout.positive?)
|
|
99
|
+
raise ArgumentError, "response_timeout must be positive or nil"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
@class = klass.to_sym
|
|
103
|
+
validate_report!
|
|
104
|
+
access_modes = event_flags & %i[rdonly wronly rdwr]
|
|
105
|
+
raise ArgumentError, "event_flags require exactly one access mode" unless access_modes.one?
|
|
106
|
+
flags = fetch(CLASS_FLAGS, @class, "class") | flags_for(@report, REPORT_FLAGS, "report")
|
|
107
|
+
flags |= FAN_NONBLOCK if nonblock
|
|
108
|
+
flags |= FAN_UNLIMITED_QUEUE if unlimited_queue
|
|
109
|
+
flags |= FAN_UNLIMITED_MARKS if unlimited_marks
|
|
110
|
+
flags |= FAN_ENABLE_AUDIT if audit
|
|
111
|
+
@handle = Native.open(flags, flags_for(event_flags, EVENT_FLAGS, "event flag"))
|
|
112
|
+
@pending = {}
|
|
113
|
+
@pending_mutex = Mutex.new
|
|
114
|
+
@response_mutex = Mutex.new
|
|
115
|
+
@readers = {}
|
|
116
|
+
@readers_condition = ConditionVariable.new
|
|
117
|
+
@closing = false
|
|
118
|
+
@exclude_self = false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mark(action, path, events:, on: :inode, follow: true, only_dir: false)
|
|
122
|
+
unless [follow, only_dir].all? { |value| value == true || value == false }
|
|
123
|
+
raise ArgumentError, "follow and only_dir must be true or false"
|
|
124
|
+
end
|
|
125
|
+
mark = Mark.new(action: action.to_sym, path: File.path(path), events: Array(events).map(&:to_sym),
|
|
126
|
+
on: on.to_sym, follow:, only_dir:)
|
|
127
|
+
validate_mark!(mark)
|
|
128
|
+
flags = fetch(MARK_ACTIONS, mark.action, "mark action") | fetch(MARK_TARGETS, mark.on, "mark target")
|
|
129
|
+
flags |= FAN_MARK_DONT_FOLLOW unless mark.follow
|
|
130
|
+
flags |= FAN_MARK_ONLYDIR if mark.only_dir
|
|
131
|
+
@handle.mark(flags, flags_for(mark.events, EVENT_MASKS, "event"), AT_FDCWD, mark.path)
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def flush(on = :inode)
|
|
136
|
+
@handle.mark(FAN_MARK_FLUSH | fetch(MARK_TARGETS, on.to_sym, "mark target"), 0, AT_FDCWD, nil)
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read_events(timeout: nil)
|
|
141
|
+
unless timeout.nil? || (timeout.is_a?(Numeric) && timeout.real? && timeout.finite? &&
|
|
142
|
+
timeout >= 0 && timeout * 1000 <= 2_147_483_647)
|
|
143
|
+
raise ArgumentError, "timeout must be a non-negative finite number or nil"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
buffer = read_buffer(timeout)
|
|
147
|
+
return [] unless buffer
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
events = Event.__send__(:parse_owned, buffer, notifier: self)
|
|
151
|
+
rescue Exception # Closing the group releases permission events that could not be parsed.
|
|
152
|
+
begin
|
|
153
|
+
close
|
|
154
|
+
rescue Exception
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
raise
|
|
158
|
+
end
|
|
159
|
+
begin
|
|
160
|
+
events.each do |event|
|
|
161
|
+
next unless event.overflow?
|
|
162
|
+
|
|
163
|
+
safely_warn "fanotify event queue overflowed; events were lost"
|
|
164
|
+
@on_overflow&.call(event)
|
|
165
|
+
end
|
|
166
|
+
rescue Exception # SystemExit and Interrupt must not strand permission events.
|
|
167
|
+
events.each { |event| finish_event(event, :allow, suppress_errors: true) }
|
|
168
|
+
raise
|
|
169
|
+
end
|
|
170
|
+
events
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def each_event
|
|
174
|
+
return enum_for(__method__) unless block_given?
|
|
175
|
+
|
|
176
|
+
loop do
|
|
177
|
+
events = read_events(timeout: @nonblock ? 0.1 : nil)
|
|
178
|
+
begin
|
|
179
|
+
events.each do |event|
|
|
180
|
+
if @exclude_self && event.pid == Process.pid
|
|
181
|
+
finish_event(event, :allow)
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
yield event
|
|
187
|
+
ensure
|
|
188
|
+
handling_error = !$!.nil?
|
|
189
|
+
if handling_error || !event.deferred?
|
|
190
|
+
finish_event(event, handling_error ? @on_error : :allow, suppress_errors: handling_error)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
ensure
|
|
195
|
+
events.each do |event|
|
|
196
|
+
finish_event(event, :allow, suppress_errors: true) unless event.closed? || event.deferred?
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def exclude_self!
|
|
203
|
+
@exclude_self = true
|
|
204
|
+
self
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def pending_events
|
|
208
|
+
@pending_mutex.synchronize do
|
|
209
|
+
@pending.values.filter_map { |reference, _deadline| dereference(reference) }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def to_io
|
|
214
|
+
@response_mutex.synchronize do
|
|
215
|
+
raise Error, "notifier is closed" if @closing || closed?
|
|
216
|
+
|
|
217
|
+
@io = @handle.to_io.dup unless @io && !@io.closed?
|
|
218
|
+
@io
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def fileno = @handle.fileno
|
|
223
|
+
def closed? = @handle.closed?
|
|
224
|
+
|
|
225
|
+
def close
|
|
226
|
+
close_token = Object.new
|
|
227
|
+
@response_mutex.synchronize do
|
|
228
|
+
return if @closing || closed?
|
|
229
|
+
|
|
230
|
+
@closing = close_token
|
|
231
|
+
@readers.each_key do |reader|
|
|
232
|
+
reader.raise(Error, "notifier is closed") unless reader == Thread.current
|
|
233
|
+
rescue ThreadError
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
@response_mutex.synchronize do
|
|
238
|
+
@readers_condition.wait(@response_mutex) while @readers.any? { |reader, _| reader != Thread.current }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if @watchdog && @watchdog != Thread.current
|
|
242
|
+
@watchdog.kill
|
|
243
|
+
@watchdog.join
|
|
244
|
+
@watchdog = nil
|
|
245
|
+
end
|
|
246
|
+
pending_events.each { |event| finish_event(event, :allow, suppress_errors: true) }
|
|
247
|
+
@pending_mutex.synchronize { @pending.keys }.each { |token| finalize_deferred(token) }
|
|
248
|
+
nil
|
|
249
|
+
ensure
|
|
250
|
+
if @closing.equal?(close_token)
|
|
251
|
+
active_error = $!
|
|
252
|
+
close_error = nil
|
|
253
|
+
@response_mutex.synchronize do
|
|
254
|
+
[@io, @handle].each do |resource|
|
|
255
|
+
resource&.close unless resource&.closed?
|
|
256
|
+
rescue StandardError => error
|
|
257
|
+
close_error ||= error
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
raise close_error if !active_error && close_error
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
private
|
|
265
|
+
|
|
266
|
+
def read_buffer(timeout)
|
|
267
|
+
reader = Thread.current
|
|
268
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
269
|
+
@response_mutex.synchronize do
|
|
270
|
+
raise Error, "notifier is closed" if @closing || closed?
|
|
271
|
+
|
|
272
|
+
@readers[reader] = true
|
|
273
|
+
end
|
|
274
|
+
begin
|
|
275
|
+
Thread.handle_interrupt(Exception => :on_blocking) { @handle.read(timeout) }
|
|
276
|
+
ensure
|
|
277
|
+
@response_mutex.synchronize do
|
|
278
|
+
@readers.delete(reader)
|
|
279
|
+
@readers_condition.broadcast
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def finish_event(event, decision, suppress_errors: false)
|
|
286
|
+
responded = false
|
|
287
|
+
begin
|
|
288
|
+
if event.permission? && !event.responded?
|
|
289
|
+
event.public_send("#{decision}!")
|
|
290
|
+
responded = true
|
|
291
|
+
end
|
|
292
|
+
ensure
|
|
293
|
+
event.close
|
|
294
|
+
end
|
|
295
|
+
responded
|
|
296
|
+
rescue StandardError => response_error
|
|
297
|
+
raise unless suppress_errors
|
|
298
|
+
|
|
299
|
+
safely_warn "fanotify failed to #{decision} event: #{response_error.message}"
|
|
300
|
+
false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def respond_to_event(event_fd, response)
|
|
304
|
+
@response_mutex.synchronize { @handle.respond(event_fd, response) }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def register_pending(event, event_fd, descriptors)
|
|
308
|
+
deadline = monotonic_time + @response_timeout if @response_timeout
|
|
309
|
+
reference = WeakRef.new(event)
|
|
310
|
+
token = Object.new
|
|
311
|
+
@response_mutex.synchronize do
|
|
312
|
+
raise Error, "notifier is closed" if @closing || closed?
|
|
313
|
+
|
|
314
|
+
@pending_mutex.synchronize do
|
|
315
|
+
@pending[token] = [reference, deadline, event_fd, descriptors]
|
|
316
|
+
end
|
|
317
|
+
start_watchdog
|
|
318
|
+
end
|
|
319
|
+
token
|
|
320
|
+
rescue Exception # Interrupt must not leave a permission event tracked but undeferred.
|
|
321
|
+
@pending_mutex.synchronize { @pending.delete(token) } if token
|
|
322
|
+
raise
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def unregister_pending(token)
|
|
326
|
+
@pending_mutex.synchronize { @pending.delete(token) } if token
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def finalize_deferred(token)
|
|
330
|
+
entry = @pending_mutex.synchronize { @pending.delete(token) }
|
|
331
|
+
return unless entry
|
|
332
|
+
|
|
333
|
+
_reference, _deadline, event_fd, descriptors = entry
|
|
334
|
+
@response_mutex.synchronize { @handle.respond(event_fd, FAN_ALLOW) unless closed? }
|
|
335
|
+
safely_warn "fanotify deferred event was garbage collected; allowing it"
|
|
336
|
+
rescue StandardError => error
|
|
337
|
+
safely_warn "fanotify failed to allow garbage-collected event: #{error.message}"
|
|
338
|
+
begin
|
|
339
|
+
close
|
|
340
|
+
rescue StandardError => close_error
|
|
341
|
+
safely_warn "fanotify failed to close after response error: #{close_error.message}"
|
|
342
|
+
end
|
|
343
|
+
ensure
|
|
344
|
+
descriptors&.each do |descriptor|
|
|
345
|
+
descriptor.close unless descriptor.closed?
|
|
346
|
+
rescue StandardError
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def start_watchdog
|
|
352
|
+
interval = @response_timeout ? [[@response_timeout / 10.0, 0.01].max, 1.0].min : 0.1
|
|
353
|
+
@pending_mutex.synchronize do
|
|
354
|
+
return if @watchdog&.alive?
|
|
355
|
+
|
|
356
|
+
@watchdog = Thread.new { watchdog_loop(interval) }
|
|
357
|
+
@watchdog.name = "fanotify-response-watchdog" if @watchdog.respond_to?(:name=)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def watchdog_loop(interval)
|
|
362
|
+
loop do
|
|
363
|
+
sleep interval
|
|
364
|
+
now = monotonic_time
|
|
365
|
+
expired, collected = @pending_mutex.synchronize do
|
|
366
|
+
live = []
|
|
367
|
+
dead = []
|
|
368
|
+
@pending.each do |token, (reference, deadline, _event_fd, _descriptors)|
|
|
369
|
+
if (event = dereference(reference))
|
|
370
|
+
live << event if deadline && deadline <= now
|
|
371
|
+
else
|
|
372
|
+
dead << token
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
[live, dead]
|
|
376
|
+
end
|
|
377
|
+
collected.each { |token| finalize_deferred(token) }
|
|
378
|
+
expired.each do |event|
|
|
379
|
+
if finish_event(event, :allow, suppress_errors: true)
|
|
380
|
+
safely_warn "fanotify response timeout expired; allowing event"
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
stop = @pending_mutex.synchronize do
|
|
384
|
+
@watchdog = nil if @pending.empty?
|
|
385
|
+
@pending.empty?
|
|
386
|
+
end
|
|
387
|
+
break if stop
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def monotonic_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
392
|
+
|
|
393
|
+
def dereference(reference)
|
|
394
|
+
reference.__getobj__
|
|
395
|
+
rescue WeakRef::RefError
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def safely_warn(message)
|
|
400
|
+
warn message
|
|
401
|
+
rescue Exception # Warning failures must not kill the fail-open watchdog.
|
|
402
|
+
nil
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def validate_report!
|
|
406
|
+
unknown = @report - REPORT_FLAGS.keys
|
|
407
|
+
raise ArgumentError, "unknown report flags: #{unknown.join(', ')}" unless unknown.empty?
|
|
408
|
+
flags = flags_for(@report, REPORT_FLAGS, "report")
|
|
409
|
+
if @class != :notif && (@report & %i[fid dir_fid name dfid_name target_fid]).any?
|
|
410
|
+
raise ArgumentError, "FID reporting requires :notif class"
|
|
411
|
+
end
|
|
412
|
+
if (@report & %i[tid pidfd]).length == 2
|
|
413
|
+
raise ArgumentError, ":tid and :pidfd cannot be combined"
|
|
414
|
+
end
|
|
415
|
+
if (flags & FAN_REPORT_NAME).positive? && (flags & FAN_REPORT_DIR_FID).zero?
|
|
416
|
+
raise ArgumentError, ":name requires :dir_fid"
|
|
417
|
+
end
|
|
418
|
+
return unless (flags & FAN_REPORT_TARGET_FID).positive? &&
|
|
419
|
+
(flags & FAN_REPORT_DFID_NAME) != FAN_REPORT_DFID_NAME
|
|
420
|
+
|
|
421
|
+
raise ArgumentError, ":target_fid requires :dfid_name"
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def validate_mark!(mark)
|
|
425
|
+
unknown = mark.events - EVENT_MASKS.keys
|
|
426
|
+
raise ArgumentError, "unknown events: #{unknown.join(', ')}" unless unknown.empty?
|
|
427
|
+
if @class == :notif && (mark.events & PERMISSION_EVENTS).any?
|
|
428
|
+
raise ArgumentError, "permission events require :content or :pre_content class"
|
|
429
|
+
end
|
|
430
|
+
if mark.events.include?(:pre_access) && @class != :pre_content
|
|
431
|
+
raise ArgumentError, "pre_access requires :pre_content class"
|
|
432
|
+
end
|
|
433
|
+
raise ArgumentError, "overflow is receive-only" if mark.events.include?(:overflow)
|
|
434
|
+
if mark.events.include?(:event_on_child) && mark.on != :inode
|
|
435
|
+
raise ArgumentError, "event_on_child requires an inode mark"
|
|
436
|
+
end
|
|
437
|
+
if mark.events.include?(:fs_error) && (mark.on != :filesystem || !fid_reporting?)
|
|
438
|
+
raise ArgumentError, "fs_error requires filesystem mark and FID reporting"
|
|
439
|
+
end
|
|
440
|
+
if mark.on == :mount && (mark.events & FID_EVENTS).any?
|
|
441
|
+
raise ArgumentError, "#{(mark.events & FID_EVENTS).join(', ')} cannot use a mount mark"
|
|
442
|
+
end
|
|
443
|
+
return if (mark.events & FID_EVENTS).empty? || (@report & %i[fid dir_fid dfid_name]).any?
|
|
444
|
+
|
|
445
|
+
raise ArgumentError, "#{(mark.events & FID_EVENTS).join(', ')} require FID reporting"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def flags_for(names, mapping, label)
|
|
449
|
+
names.reduce(0) { |flags, name| flags | fetch(mapping, name.to_sym, label) }
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def fid_reporting? = (flags_for(@report, REPORT_FLAGS, "report") & FAN_REPORT_FID).positive?
|
|
453
|
+
|
|
454
|
+
def fetch(mapping, name, label)
|
|
455
|
+
mapping.fetch(name) { raise ArgumentError, "unknown #{label}: #{name.inspect}" }
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
data/lib/fanotify.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fanotify/version"
|
|
4
|
+
require "weakref"
|
|
5
|
+
|
|
6
|
+
module Fanotify
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
class UnsupportedError < Error; end
|
|
9
|
+
class PermissionError < Error; end
|
|
10
|
+
SystemCallError = ::SystemCallError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require "fanotify/fanotify"
|
|
14
|
+
|
|
15
|
+
module Fanotify
|
|
16
|
+
Native.constants(false).grep(/^FAN|^O_|^AT_/).each { |name| const_set(name, Native.const_get(name)) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative "fanotify/file_handle"
|
|
20
|
+
require_relative "fanotify/event"
|
|
21
|
+
require_relative "fanotify/mark"
|
|
22
|
+
require_relative "fanotify/notifier"
|
|
23
|
+
|
|
24
|
+
module Fanotify
|
|
25
|
+
class << self
|
|
26
|
+
def supported? = Native.supported?
|
|
27
|
+
|
|
28
|
+
def watch(path, events: %i[create delete modify], **options, &block)
|
|
29
|
+
return enum_for(__method__, path, events:, **options) unless block
|
|
30
|
+
|
|
31
|
+
options = {class: :notif, report: %i[fid dfid_name]}.merge(options)
|
|
32
|
+
Notifier.open(**options) do |notifier|
|
|
33
|
+
notifier.mark(:add, path, events: events + [:event_on_child], only_dir: true)
|
|
34
|
+
notifier.each_event(&block)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/sig/fanotify.rbs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Fanotify
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class UnsupportedError < Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class PermissionError < Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
SystemCallError: singleton(::SystemCallError)
|
|
14
|
+
|
|
15
|
+
def self.supported?: () -> bool
|
|
16
|
+
def self.watch: (String | _ToPath path, ?events: Array[Symbol], **untyped options) { (Event) -> void } -> void
|
|
17
|
+
| (String | _ToPath path, ?events: Array[Symbol], **untyped options) -> Enumerator[Event, void]
|
|
18
|
+
|
|
19
|
+
class FileHandle
|
|
20
|
+
attr_reader fsid: String
|
|
21
|
+
attr_reader type: Integer
|
|
22
|
+
attr_reader bytes: String
|
|
23
|
+
|
|
24
|
+
def initialize: (fsid: String, type: Integer, bytes: String) -> void
|
|
25
|
+
def to_s: () -> String
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Mark
|
|
29
|
+
attr_reader action: Symbol
|
|
30
|
+
attr_reader path: String
|
|
31
|
+
attr_reader events: Array[Symbol]
|
|
32
|
+
attr_reader on: Symbol
|
|
33
|
+
attr_reader follow: bool
|
|
34
|
+
attr_reader only_dir: bool
|
|
35
|
+
|
|
36
|
+
def initialize: (action: Symbol, path: String, events: Array[Symbol], on: Symbol, follow: bool, only_dir: bool) -> void
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Event
|
|
40
|
+
attr_reader raw_mask: Integer
|
|
41
|
+
attr_reader pid: Integer?
|
|
42
|
+
attr_reader notifier: Notifier?
|
|
43
|
+
attr_reader fid: FileHandle?
|
|
44
|
+
attr_reader dfid: FileHandle?
|
|
45
|
+
attr_reader name: String?
|
|
46
|
+
attr_reader pidfd: Integer?
|
|
47
|
+
attr_reader pidfd_error: Integer?
|
|
48
|
+
attr_reader old_dfid: FileHandle?
|
|
49
|
+
attr_reader old_name: String?
|
|
50
|
+
attr_reader new_dfid: FileHandle?
|
|
51
|
+
attr_reader new_name: String?
|
|
52
|
+
attr_reader error: Integer?
|
|
53
|
+
attr_reader error_count: Integer?
|
|
54
|
+
attr_reader range_offset: Integer?
|
|
55
|
+
attr_reader range_count: Integer?
|
|
56
|
+
|
|
57
|
+
def self.parse: (String buffer) -> Array[Event]
|
|
58
|
+
def mask: () -> Array[Symbol]
|
|
59
|
+
def file: () -> IO?
|
|
60
|
+
def path: () -> String?
|
|
61
|
+
def process_path: () -> String?
|
|
62
|
+
def deleted?: () -> bool
|
|
63
|
+
def overflow?: () -> bool
|
|
64
|
+
def permission?: () -> bool
|
|
65
|
+
def responded?: () -> bool
|
|
66
|
+
def deferred?: () -> bool
|
|
67
|
+
def closed?: () -> bool
|
|
68
|
+
def allow!: (?audit: bool) -> self
|
|
69
|
+
def deny!: (?audit: bool) -> self
|
|
70
|
+
def defer!: () -> self
|
|
71
|
+
def close: () -> nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Notifier
|
|
75
|
+
attr_reader report: Array[Symbol]
|
|
76
|
+
|
|
77
|
+
def self.new: (**untyped options) -> Notifier
|
|
78
|
+
def self.open: (**untyped options) { (Notifier) -> untyped } -> untyped
|
|
79
|
+
| (**untyped options) -> Notifier
|
|
80
|
+
def initialize: (**untyped options) -> void
|
|
81
|
+
def mark: (Symbol action, String | _ToPath path, events: Array[Symbol], ?on: Symbol, ?follow: bool, ?only_dir: bool) -> self
|
|
82
|
+
def flush: (?Symbol on) -> self
|
|
83
|
+
def read_events: (?timeout: Numeric?) -> Array[Event]
|
|
84
|
+
def each_event: () { (Event) -> void } -> void
|
|
85
|
+
| () -> Enumerator[Event, void]
|
|
86
|
+
def exclude_self!: () -> self
|
|
87
|
+
def pending_events: () -> Array[Event]
|
|
88
|
+
def to_io: () -> IO
|
|
89
|
+
def fileno: () -> Integer
|
|
90
|
+
def closed?: () -> bool
|
|
91
|
+
def close: () -> nil
|
|
92
|
+
end
|
|
93
|
+
end
|
data/tools/constants.def
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
X(FAN_ACCESS)
|
|
2
|
+
X(FAN_MODIFY)
|
|
3
|
+
X(FAN_ATTRIB)
|
|
4
|
+
X(FAN_CLOSE_WRITE)
|
|
5
|
+
X(FAN_CLOSE_NOWRITE)
|
|
6
|
+
X(FAN_OPEN)
|
|
7
|
+
X(FAN_MOVED_FROM)
|
|
8
|
+
X(FAN_MOVED_TO)
|
|
9
|
+
X(FAN_CREATE)
|
|
10
|
+
X(FAN_DELETE)
|
|
11
|
+
X(FAN_DELETE_SELF)
|
|
12
|
+
X(FAN_MOVE_SELF)
|
|
13
|
+
X(FAN_OPEN_EXEC)
|
|
14
|
+
X(FAN_Q_OVERFLOW)
|
|
15
|
+
X(FAN_FS_ERROR)
|
|
16
|
+
X(FAN_OPEN_PERM)
|
|
17
|
+
X(FAN_ACCESS_PERM)
|
|
18
|
+
X(FAN_OPEN_EXEC_PERM)
|
|
19
|
+
X(FAN_PRE_ACCESS)
|
|
20
|
+
X(FAN_EVENT_ON_CHILD)
|
|
21
|
+
X(FAN_RENAME)
|
|
22
|
+
X(FAN_ONDIR)
|
|
23
|
+
X(FAN_CLOEXEC)
|
|
24
|
+
X(FAN_NONBLOCK)
|
|
25
|
+
X(FAN_CLASS_NOTIF)
|
|
26
|
+
X(FAN_CLASS_CONTENT)
|
|
27
|
+
X(FAN_CLASS_PRE_CONTENT)
|
|
28
|
+
X(FAN_UNLIMITED_QUEUE)
|
|
29
|
+
X(FAN_UNLIMITED_MARKS)
|
|
30
|
+
X(FAN_ENABLE_AUDIT)
|
|
31
|
+
X(FAN_REPORT_PIDFD)
|
|
32
|
+
X(FAN_REPORT_TID)
|
|
33
|
+
X(FAN_REPORT_FID)
|
|
34
|
+
X(FAN_REPORT_DIR_FID)
|
|
35
|
+
X(FAN_REPORT_NAME)
|
|
36
|
+
X(FAN_REPORT_DFID_NAME)
|
|
37
|
+
X(FAN_REPORT_TARGET_FID)
|
|
38
|
+
X(FAN_MARK_ADD)
|
|
39
|
+
X(FAN_MARK_REMOVE)
|
|
40
|
+
X(FAN_MARK_DONT_FOLLOW)
|
|
41
|
+
X(FAN_MARK_ONLYDIR)
|
|
42
|
+
X(FAN_MARK_MOUNT)
|
|
43
|
+
X(FAN_MARK_IGNORED_MASK)
|
|
44
|
+
X(FAN_MARK_IGNORED_SURV_MODIFY)
|
|
45
|
+
X(FAN_MARK_FLUSH)
|
|
46
|
+
X(FAN_MARK_FILESYSTEM)
|
|
47
|
+
X(FAN_MARK_EVICTABLE)
|
|
48
|
+
X(FAN_MARK_IGNORE)
|
|
49
|
+
X(FAN_MARK_INODE)
|
|
50
|
+
X(FAN_ALLOW)
|
|
51
|
+
X(FAN_DENY)
|
|
52
|
+
X(FAN_AUDIT)
|
|
53
|
+
X(FANOTIFY_METADATA_VERSION)
|
|
54
|
+
X(O_RDONLY)
|
|
55
|
+
X(O_WRONLY)
|
|
56
|
+
X(O_RDWR)
|
|
57
|
+
X(O_LARGEFILE)
|
|
58
|
+
X(O_CLOEXEC)
|
|
59
|
+
X(O_NONBLOCK)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include "../ext/fanotify/compat.h"
|
|
3
|
+
|
|
4
|
+
int main(void)
|
|
5
|
+
{
|
|
6
|
+
#define X(name) printf(#name "=%llu\n", (unsigned long long)(name));
|
|
7
|
+
#include "constants.def"
|
|
8
|
+
#undef X
|
|
9
|
+
printf("FAN_NOFD=%d\n", FAN_NOFD);
|
|
10
|
+
printf("AT_FDCWD=%d\n", AT_FDCWD);
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source = <<~C
|
|
4
|
+
/* Generated by tools/generate_constants.rb. */
|
|
5
|
+
#include "ruby.h"
|
|
6
|
+
#include "compat.h"
|
|
7
|
+
|
|
8
|
+
void fanotify_define_constants(VALUE module)
|
|
9
|
+
{
|
|
10
|
+
#define X(name) rb_define_const(module, #name, ULL2NUM((unsigned long long)(name)));
|
|
11
|
+
#include "../../tools/constants.def"
|
|
12
|
+
#undef X
|
|
13
|
+
rb_define_const(module, "FAN_NOFD", INT2NUM(FAN_NOFD));
|
|
14
|
+
rb_define_const(module, "AT_FDCWD", INT2NUM(AT_FDCWD));
|
|
15
|
+
}
|
|
16
|
+
C
|
|
17
|
+
|
|
18
|
+
File.write(File.expand_path("../ext/fanotify/constants.c", __dir__), source)
|