From d8ac22a13f092731bf29a0399f31f3074764c95d Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 20 Dec 2019 01:39:16 +0000 Subject: watchable: use fiddle for inotify support We have String#unpack at our disposal for working with "struct inotify_event", so use it instead of depending on an extension which requires a compiler and development headers to install. --- lib/dtas/watchable.rb | 115 ++++++++++++++++++++------------------- lib/dtas/watchable/fiddle_ino.rb | 78 ++++++++++++++++++++++++++ lib/dtas/watchable/inotify.rb | 13 +++++ 3 files changed, 149 insertions(+), 57 deletions(-) create mode 100644 lib/dtas/watchable/fiddle_ino.rb create mode 100644 lib/dtas/watchable/inotify.rb diff --git a/lib/dtas/watchable.rb b/lib/dtas/watchable.rb index d0f37af..2502b7c 100644 --- a/lib/dtas/watchable.rb +++ b/lib/dtas/watchable.rb @@ -1,71 +1,72 @@ # Copyright (C) 2013-2019 all contributors # License: GPL-3.0+ # frozen_string_literal: true +require_relative '../dtas' +require_relative 'nonblock' begin -require 'sleepy_penguin' + module DTAS::Watchable # :nodoc: + module InotifyCommon # :nodoc: + FLAGS = 8 | 128 # IN_CLOSE_WRITE | IN_MOVED_TO -# used to restart DTAS::Source::SplitFX processing in dtas-player -# if the YAML file is edited -module DTAS::Watchable # :nodoc: - class InotifyReadableIter < SleepyPenguin::Inotify # :nodoc: - def self.new - super(:CLOEXEC) - end - - FLAGS = CLOSE_WRITE | MOVED_TO - - def readable_iter - or_call = false - while event = take(true) # drain the buffer - w = @watches[event.wd] or next - if (event.mask & FLAGS) != 0 && w[event.name] - or_call = true + def readable_iter + or_call = false + while event = take(true) # drain the buffer + w = @watches[event.wd] or next + if (event.mask & FLAGS) != 0 && w[event.name] + or_call = true + end + end + if or_call + @on_readable.call + :delete + else + :wait_readable end end - if or_call - @on_readable.call - :delete - else - :wait_readable - end - end - # we must watch the directory, since - def watch_files(paths, blk) - @watches = {} # wd -> { basename -> true } - @on_readable = blk - @dir2wd = {} - Array(paths).each do |path| - watchdir, watchbase = File.split(File.expand_path(path)) - begin - wd = @dir2wd[watchdir] ||= add_watch(watchdir, FLAGS) - m = @watches[wd] ||= {} - m[watchbase] = true - rescue SystemCallError => e - warn "#{watchdir.dump}: #{e.message} (#{e.class})" + # we must watch the directory, since + def watch_files(paths, blk) + @watches = {} # wd -> { basename -> true } + @on_readable = blk + @dir2wd = {} + Array(paths).each do |path| + watchdir, watchbase = File.split(File.expand_path(path)) + begin + wd = @dir2wd[watchdir] ||= add_watch(watchdir, FLAGS) + m = @watches[wd] ||= {} + m[watchbase] = true + rescue SystemCallError => e + warn "#{watchdir.dump}: #{e.message} (#{e.class})" + end end end - end - end + end # module InotifyCommon - def watch_begin(blk) - @ino = InotifyReadableIter.new - @ino.watch_files(@watch_extra << @infile, blk) - @ino - end + begin + require_relative 'watchable/inotify' + rescue LoadError + # TODO: support kevent + require_relative 'watchable/fiddle_ino' + end - def watch_extra(paths) - @ino.watch_extra(paths) - end + def watch_begin(blk) + @ino = DTAS::Watchable::InotifyReadableIter.new + @ino.watch_files(@watch_extra << @infile, blk) + @ino + end - # Closing the inotify descriptor (instead of using inotify_rm_watch) - # is cleaner because it avoids EINVAL on race conditions in case - # a directory is deleted: https://lkml.org/lkml/2007/7/9/3 - def watch_end(srv) - srv.wait_ctl(@ino, :delete) - @ino = @ino.close - end -end + def watch_extra(paths) + @ino.watch_extra(paths) + end -rescue LoadError -end + # Closing the inotify descriptor (instead of using inotify_rm_watch) + # is cleaner because it avoids EINVAL on race conditions in case + # a directory is deleted: https://lkml.org/lkml/2007/7/9/3 + def watch_end(srv) + srv.wait_ctl(@ino, :delete) + @ino = @ino.close + end + end # module DTAS::Watchable +rescue LoadError, StandardError => e + warn "#{e.message} (#{e.class})" +end # begin diff --git a/lib/dtas/watchable/fiddle_ino.rb b/lib/dtas/watchable/fiddle_ino.rb new file mode 100644 index 0000000..3b9d668 --- /dev/null +++ b/lib/dtas/watchable/fiddle_ino.rb @@ -0,0 +1,78 @@ +# Copyright (C) 2013-2019 all contributors +# License: GPL-3.0+ +# frozen_string_literal: true +require 'fiddle' + +# used to restart DTAS::Source::SplitFX processing in dtas-player +# if the YAML file is edited +class DTAS::Watchable::InotifyReadableIter # :nodoc: + include DTAS::Watchable::InotifyCommon + + Inotify_init = Fiddle::Function.new(DTAS.libc['inotify_init1'], + [ Fiddle::TYPE_INT ], + Fiddle::TYPE_INT) + + Inotify_add_watch = Fiddle::Function.new(DTAS.libc['inotify_add_watch'], + [ Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT ], + Fiddle::TYPE_INT) + + # IO.select compatibility + attr_reader :to_io #:nodoc: + + def initialize # :nodoc: + fd = Inotify_init.call(02000000 | 04000) # CLOEXEC | NONBLOCK + raise "inotify_init failed: #{Fiddle.last_error}" if fd < 0 + @to_io = DTAS::Nonblock.for_fd(fd) + @buf = ''.b + @q = [] + end + + # struct inotify_event { + # int wd; /* Watch descriptor */ + # uint32_t mask; /* Mask describing event */ + # uint32_t cookie; /* Unique cookie associating related + # events (for rename(2)) */ + # uint32_t len; /* Size of name field */ + # char name[]; /* Optional null-terminated name */ + InotifyEvent = Struct.new(:wd, :mask, :cookie, :len, :name) # :nodoc: + + def take(nonblock) # :nodoc: + event = @q.pop and return event + case rv = @to_io.read_nonblock(16384, @buf, exception: false) + when :wait_readable, nil + return + else + until rv.empty? + hdr = rv.slice!(0,16) + name = nil + wd, mask, cookie, len = res = hdr.unpack('iIII') + wd && mask && cookie && len or + raise "bogus inotify_event #{res.inspect} hdr=#{hdr.inspect}" + if len > 0 + name = rv.slice!(0, len) + name.size == len or raise "short name #{name.inspect} != #{len}" + name.sub!(/\0+\z/, '') or + raise "missing: `\\0', inotify_event.name=#{name.inspect}" + name = DTAS.dedupe_str(name) + end + ie = InotifyEvent.new(wd, mask, cookie, len, name) + if event + @q << ie + else + event = ie + end + end # /until rv.empty? + return event + end while true + end + + def add_watch(watchdir, flags) + wd = Inotify_add_watch.call(@to_io.fileno, watchdir, flags) + raise "inotify_add_watch failed: #{Fiddle.last_error}" if wd < 0 + wd + end + + def close + @to_io = @to_io.close if @to_io + end +end diff --git a/lib/dtas/watchable/inotify.rb b/lib/dtas/watchable/inotify.rb new file mode 100644 index 0000000..eface0e --- /dev/null +++ b/lib/dtas/watchable/inotify.rb @@ -0,0 +1,13 @@ +# Copyright (C) 2013-2019 all contributors +# License: GPL-3.0+ +# frozen_string_literal: true +require 'sleepy_penguin' + +# used to restart DTAS::Source::SplitFX processing in dtas-player +# if the YAML file is edited +class DTAS::Watchable::InotifyReadableIter < SleepyPenguin::Inotify # :nodoc: + include DTAS::Watchable::InotifyCommon + def self.new + super(:CLOEXEC) + end +end -- cgit v1.2.3-24-ge0c7