driskell-listen 3.0.6.10

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -0
  3. data/CONTRIBUTING.md +38 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +293 -0
  6. data/bin/driskell-listen +12 -0
  7. data/lib/driskell-listen.rb +61 -0
  8. data/lib/driskell-listen/adapter.rb +44 -0
  9. data/lib/driskell-listen/adapter/base.rb +147 -0
  10. data/lib/driskell-listen/adapter/bsd.rb +106 -0
  11. data/lib/driskell-listen/adapter/config.rb +26 -0
  12. data/lib/driskell-listen/adapter/darwin.rb +71 -0
  13. data/lib/driskell-listen/adapter/linux.rb +96 -0
  14. data/lib/driskell-listen/adapter/polling.rb +37 -0
  15. data/lib/driskell-listen/adapter/simulated_darwin.rb +65 -0
  16. data/lib/driskell-listen/adapter/windows.rb +99 -0
  17. data/lib/driskell-listen/backend.rb +45 -0
  18. data/lib/driskell-listen/change.rb +80 -0
  19. data/lib/driskell-listen/cli.rb +65 -0
  20. data/lib/driskell-listen/directory.rb +83 -0
  21. data/lib/driskell-listen/event/config.rb +59 -0
  22. data/lib/driskell-listen/event/loop.rb +117 -0
  23. data/lib/driskell-listen/event/processor.rb +122 -0
  24. data/lib/driskell-listen/event/queue.rb +56 -0
  25. data/lib/driskell-listen/file.rb +80 -0
  26. data/lib/driskell-listen/fsm.rb +131 -0
  27. data/lib/driskell-listen/internals/thread_pool.rb +21 -0
  28. data/lib/driskell-listen/listener.rb +132 -0
  29. data/lib/driskell-listen/listener/config.rb +45 -0
  30. data/lib/driskell-listen/logger.rb +32 -0
  31. data/lib/driskell-listen/options.rb +23 -0
  32. data/lib/driskell-listen/queue_optimizer.rb +132 -0
  33. data/lib/driskell-listen/record.rb +104 -0
  34. data/lib/driskell-listen/record/entry.rb +56 -0
  35. data/lib/driskell-listen/silencer.rb +97 -0
  36. data/lib/driskell-listen/silencer/controller.rb +48 -0
  37. data/lib/driskell-listen/version.rb +5 -0
  38. metadata +126 -0
@@ -0,0 +1,37 @@
1
+ module Driskell::Listen
2
+ module Adapter
3
+ # Polling Adapter that works cross-platform and
4
+ # has no dependencies. This is the adapter that
5
+ # uses the most CPU processing power and has higher
6
+ # file IO than the other implementations.
7
+ #
8
+ class Polling < Base
9
+ OS_REGEXP = // # match every OS
10
+
11
+ DEFAULTS = { latency: 1.0, wait_for_delay: 0.05 }
12
+
13
+ private
14
+
15
+ def _configure(_, &callback)
16
+ @polling_callbacks ||= []
17
+ @polling_callbacks << callback
18
+ end
19
+
20
+ def _run
21
+ loop do
22
+ start = Time.now.to_f
23
+ @polling_callbacks.each do |callback|
24
+ callback.call(nil)
25
+ nap_time = options.latency - (Time.now.to_f - start)
26
+ # TODO: warn if nap_time is negative (polling too slow)
27
+ sleep(nap_time) if nap_time > 0
28
+ end
29
+ end
30
+ end
31
+
32
+ def _process_event(dir, _)
33
+ _queue_change(:tree, dir, '.')
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ module Driskell::Listen
2
+ module Adapter
3
+ class SimulatedDarwin < Linux
4
+ def self.usable?
5
+ os = RbConfig::CONFIG['target_os']
6
+ return false unless const_get('OS_REGEXP') =~ os
7
+ /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT']
8
+ end
9
+
10
+ class FakeEvent
11
+ attr_reader :dir
12
+
13
+ def initialize(watched_dir, event)
14
+ # NOTE: avoid using event.absolute_name since new API
15
+ # will need to have a custom recursion implemented
16
+ # to properly match events to configured directories
17
+ @real_path = full_path(event).relative_path_from(watched_dir)
18
+ @dir = "#{Pathname(watched_dir) + dir_for_event(event, @real_path)}/"
19
+ end
20
+
21
+ def real_path
22
+ @real_path.to_s
23
+ end
24
+
25
+ private
26
+
27
+ def dir?(event)
28
+ event.flags.include?(:isdir)
29
+ end
30
+
31
+ def moved?(event)
32
+ (event.flags & [:moved_to, :moved_from])
33
+ end
34
+
35
+ def dir_for_event(event, rel_path)
36
+ (moved?(event) || dir?(event)) ? rel_path.dirname : rel_path
37
+ end
38
+
39
+ def full_path(event)
40
+ Pathname.new(event.watcher.path) + event.name
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def _process_event(watched_dir, event)
47
+ ev = FakeEvent.new(watched_dir, event)
48
+
49
+ _log(
50
+ :debug,
51
+ "fake_fsevent: #{ev.dir}(#{ev.real_path}=#{event.flags.inspect})")
52
+
53
+ _darwin.send(:_process_event, watched_dir, [ev.dir])
54
+ end
55
+
56
+ def _darwin
57
+ @darwin ||= Class.new(Darwin) do
58
+ def _configure(*_args)
59
+ # Skip FSEvent setup
60
+ end
61
+ end.new(mq: @mq)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,99 @@
1
+ module Driskell::Listen
2
+ module Adapter
3
+ # Adapter implementation for Windows `wdm`.
4
+ #
5
+ class Windows < Base
6
+ OS_REGEXP = /mswin|mingw|cygwin/i
7
+
8
+ BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
9
+ Please add the following to your Gemfile to avoid polling for changes:
10
+ gem 'wdm', '>= 0.1.0' if Gem.win_platform?
11
+ EOS
12
+
13
+ def self.usable?
14
+ return false unless super
15
+ require 'wdm'
16
+ true
17
+ rescue LoadError
18
+ _log :debug, format('wdm - load failed: %s:%s', $ERROR_INFO,
19
+ $ERROR_POSITION * "\n")
20
+
21
+ Kernel.warn BUNDLER_DECLARE_GEM
22
+ false
23
+ end
24
+
25
+ private
26
+
27
+ def _configure(dir, &callback)
28
+ require 'wdm'
29
+ _log :debug, 'wdm - starting...'
30
+ @worker ||= WDM::Monitor.new
31
+ @worker.watch_recursively(dir.to_s, :files) do |change|
32
+ callback.call([:file, change])
33
+ end
34
+
35
+ @worker.watch_recursively(dir.to_s, :directories) do |change|
36
+ callback.call([:dir, change])
37
+ end
38
+
39
+ events = [:attributes, :last_write]
40
+ @worker.watch_recursively(dir.to_s, *events) do |change|
41
+ callback.call([:attr, change])
42
+ end
43
+ end
44
+
45
+ def _run
46
+ @worker.run!
47
+ end
48
+
49
+ def _process_event(dir, event)
50
+ _log :debug, "wdm - callback: #{event.inspect}"
51
+
52
+ type, change = event
53
+
54
+ full_path = Pathname(change.path)
55
+
56
+ rel_path = full_path.relative_path_from(dir).to_s
57
+
58
+ options = { change: _change(change.type) }
59
+
60
+ case type
61
+ when :file
62
+ _queue_change(:file, dir, rel_path, options)
63
+ when :attr
64
+ unless full_path.directory?
65
+ _queue_change(:file, dir, rel_path, options)
66
+ end
67
+ when :dir
68
+ if change.type == :removed
69
+ # TODO: check if watched dir?
70
+ _queue_change(:dir, dir, Pathname(rel_path).dirname.to_s, {})
71
+ elsif change.type == :added
72
+ _queue_change(:dir, dir, rel_path, {})
73
+ else
74
+ # do nothing - changed directory means either:
75
+ # - removed subdirs (handled above)
76
+ # - added subdirs (handled above)
77
+ # - removed files (handled by _file_callback)
78
+ # - added files (handled by _file_callback)
79
+ # so what's left?
80
+ end
81
+ end
82
+ rescue
83
+ details = event.inspect
84
+ _log :error, format('wdm - callback (%): %s:%s', details, $ERROR_INFO,
85
+ $ERROR_POSITION * "\n")
86
+ raise
87
+ end
88
+
89
+ def _change(type)
90
+ { modified: [:modified, :attrib], # TODO: is attrib really passed?
91
+ added: [:added, :renamed_new_file],
92
+ removed: [:removed, :renamed_old_file] }.each do |change, types|
93
+ return change if types.include?(type)
94
+ end
95
+ nil
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,45 @@
1
+ require 'driskell-listen/adapter'
2
+ require 'driskell-listen/adapter/base'
3
+ require 'driskell-listen/adapter/config'
4
+
5
+ # This class just aggregates configuration object to avoid Listener specs
6
+ # from exploding with huge test setup blocks
7
+ module Driskell::Listen
8
+ class Backend
9
+ def initialize(directories, queue, silencer, config)
10
+ adapter_select_opts = config.adapter_select_options
11
+
12
+ adapter_class = Adapter.select(adapter_select_opts)
13
+
14
+ # Use default from adapter if possible
15
+ @min_delay_between_events = config.min_delay_between_events
16
+ @min_delay_between_events ||= adapter_class::DEFAULTS[:wait_for_delay]
17
+ @min_delay_between_events ||= 0.1
18
+
19
+ adapter_opts = config.adapter_instance_options(adapter_class)
20
+
21
+ aconfig = Adapter::Config.new(directories, queue, silencer, adapter_opts)
22
+ @adapter = adapter_class.new(aconfig)
23
+ end
24
+
25
+ def start
26
+ adapter.start
27
+ end
28
+
29
+ def stop
30
+ adapter.stop
31
+ end
32
+
33
+ def preempt_change(dir, rel_path, data)
34
+ adapter.preempt_change dir, rel_path, data
35
+ end
36
+
37
+ def min_delay_between_events
38
+ @min_delay_between_events
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :adapter
44
+ end
45
+ end
@@ -0,0 +1,80 @@
1
+ require 'driskell-listen/file'
2
+ require 'driskell-listen/directory'
3
+
4
+ module Driskell::Listen
5
+ # TODO: rename to Snapshot
6
+ class Change
7
+ # TODO: test this class for coverage
8
+ class Config
9
+ def initialize(queue, silencer)
10
+ @queue = queue
11
+ @silencer = silencer
12
+ end
13
+
14
+ def silenced?(path, type)
15
+ @silencer.silenced?(Pathname(path), type)
16
+ end
17
+
18
+ def queue(*args)
19
+ @queue << args
20
+ end
21
+ end
22
+
23
+ attr_reader :record
24
+
25
+ def initialize(config, record)
26
+ @config = config
27
+ @record = record
28
+ end
29
+
30
+ # Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
31
+ def invalidate(type, rel_path, options)
32
+ watched_dir = Pathname.new(record.root)
33
+
34
+ change = options[:change]
35
+ cookie = options[:cookie]
36
+
37
+ if !cookie && config.silenced?(rel_path, type)
38
+ Driskell::Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
39
+ return
40
+ end
41
+
42
+ path = watched_dir + rel_path
43
+
44
+ Driskell::Listen::Logger.debug do
45
+ log_details = options[:silence] && 'recording' || change || 'unknown'
46
+ "#{log_details}: #{type}:#{path} (#{options.inspect})"
47
+ end
48
+
49
+ if change
50
+ options = cookie ? { cookie: cookie } : {}
51
+ config.queue(type, change, watched_dir, rel_path, options)
52
+ else
53
+ if type == :tree
54
+ # Invalid the entire directory tree
55
+ Directory.scan(self, rel_path, **options, recurse: true)
56
+ elsif type == :dir
57
+ # Invalid directory contents, but do not recurse
58
+ Directory.scan(self, rel_path, **options, recurse: false)
59
+ else
60
+ change = File.change(record, rel_path)
61
+ return if !change || options[:silence]
62
+ config.queue(:file, change, watched_dir, rel_path)
63
+ end
64
+ end
65
+ rescue RuntimeError => ex
66
+ msg = format(
67
+ '%s#%s crashed %s:%s',
68
+ self.class,
69
+ __method__,
70
+ exinspect,
71
+ ex.backtrace * "\n")
72
+ Driskell::Listen::Logger.error(msg)
73
+ raise
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :config
79
+ end
80
+ end
@@ -0,0 +1,65 @@
1
+ require 'thor'
2
+ require 'driskell-listen'
3
+ require 'logger'
4
+
5
+ module Driskell::Listen
6
+ class CLI < Thor
7
+ default_task :start
8
+
9
+ desc 'start', 'Starts Listen'
10
+
11
+ class_option :verbose,
12
+ type: :boolean,
13
+ default: false,
14
+ aliases: '-v',
15
+ banner: 'Verbose'
16
+
17
+ class_option :directory,
18
+ type: :array,
19
+ default: '.',
20
+ aliases: '-d',
21
+ banner: 'The directory to listen to'
22
+
23
+ class_option :relative,
24
+ type: :boolean,
25
+ default: false,
26
+ aliases: '-r',
27
+ banner: 'Convert paths relative to current directory'
28
+
29
+ def start
30
+ Driskell::Listen::Forwarder.new(options).start
31
+ end
32
+ end
33
+
34
+ class Forwarder
35
+ attr_reader :logger
36
+ def initialize(options)
37
+ @options = options
38
+ @logger = ::Logger.new(STDOUT)
39
+ @logger.level = ::Logger::INFO
40
+ @logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
41
+ end
42
+
43
+ def start
44
+ logger.info 'Starting listen...'
45
+ directory = @options[:directory]
46
+ relative = @options[:relative]
47
+ callback = proc do |modified, added, removed|
48
+ if @options[:verbose]
49
+ logger.info "+ #{added}" unless added.empty?
50
+ logger.info "- #{removed}" unless removed.empty?
51
+ logger.info "> #{modified}" unless modified.empty?
52
+ end
53
+ end
54
+
55
+ listener = Driskell::Listen.to(
56
+ directory,
57
+ relative: relative,
58
+ &callback)
59
+
60
+ listener.start
61
+
62
+ sleep 0.5 while listener.processing?
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ require 'set'
2
+
3
+ module Driskell::Listen
4
+ # TODO: refactor (turn it into a normal object, cache the stat, etc)
5
+ class Directory
6
+ def self.scan(snapshot, rel_path, options)
7
+ record = snapshot.record
8
+
9
+ previous = record.dir_entries(rel_path)
10
+
11
+ dir = Pathname.new(record.root)
12
+ path = dir + rel_path
13
+ current = Set.new(path.children)
14
+
15
+ Driskell::Listen::Logger.debug do
16
+ format('%s: %s(%s): %s -> %s',
17
+ (options[:silence] ? 'Recording' : 'Scanning'),
18
+ rel_path, options.inspect, previous.inspect, current.inspect)
19
+ end
20
+
21
+ record.update_dir(rel_path)
22
+
23
+ current.each do |full_path|
24
+ # Find old type so we can ensure we invalidate directory contents
25
+ # if we were previously a file, and vice versa
26
+ if previous.key?(full_path.basename)
27
+ old = previous.delete(full_path.basename)
28
+ old_type = old.key?(:mtime) ? :dir : :file
29
+ else
30
+ old_type = nil
31
+ end
32
+
33
+ item_rel_path = full_path.relative_path_from(dir).to_s
34
+ if detect_type(full_path) == :dir
35
+ if old_type == :file
36
+ snapshot.invalidate(:file, item_rel_path, options)
37
+ end
38
+
39
+ # Only invalidate subdirectories if we're recursing or it is new
40
+ if options[:recurse] || old_type.nil?
41
+ snapshot.invalidate(:tree, item_rel_path, options)
42
+ end
43
+ else
44
+ if old_type == :dir
45
+ snapshot.invalidate(:tree, item_rel_path, options)
46
+ end
47
+
48
+ snapshot.invalidate(:file, item_rel_path, options)
49
+ end
50
+ end
51
+
52
+ process_previous(snapshot, Pathname.new(rel_path), previous, options)
53
+ rescue Errno::ENOENT, Errno::EHOSTDOWN
54
+ record.unset_path(rel_path)
55
+ process_previous(snapshot, Pathname.new(rel_path), previous, options)
56
+ rescue Errno::ENOTDIR
57
+ record.unset_path(rel_path)
58
+ process_previous(snapshot, path, previous, options)
59
+ snapshot.invalidate(:file, rel_path, options)
60
+ rescue
61
+ Driskell::Listen::Logger.warn do
62
+ format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
63
+ end
64
+ raise
65
+ end
66
+
67
+ def self.process_previous(snapshot, path, previous, options)
68
+ previous.each do |entry, data|
69
+ type = data.key?(:mtime) ? :file : :tree
70
+ rel_path_s = (path + entry).to_s
71
+ snapshot.invalidate(type, rel_path_s, options)
72
+ end
73
+ end
74
+
75
+ def self.detect_type(full_path)
76
+ stat = ::File.lstat(full_path.to_s)
77
+ stat.directory? ? :dir : :file
78
+ rescue Errno::ENOENT
79
+ # report as dir for scanning
80
+ :dir
81
+ end
82
+ end
83
+ end