driskell-listen 3.0.6.10

Sign up to get free protection for your applications and to get access to all the features.
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,147 @@
1
+ require 'driskell-listen/options'
2
+ require 'driskell-listen/record'
3
+ require 'driskell-listen/change'
4
+
5
+ module Driskell::Listen
6
+ module Adapter
7
+ class Base
8
+ attr_reader :options
9
+
10
+ # TODO: only used by tests
11
+ DEFAULTS = {}
12
+
13
+ attr_reader :config
14
+
15
+ def initialize(config)
16
+ @started = false
17
+ @config = config
18
+
19
+ @configured = nil
20
+
21
+ fail 'No directories to watch!' if config.directories.empty?
22
+
23
+ defaults = self.class.const_get('DEFAULTS')
24
+ @options = Driskell::Listen::Options.new(config.adapter_options, defaults)
25
+ rescue
26
+ _log_exception 'adapter config failed: %s:%s called from: %s', caller
27
+ raise
28
+ end
29
+
30
+ # TODO: it's a separate method as a temporary workaround for tests
31
+ def configure
32
+ if @configured
33
+ _log(:warn, 'Adapter already configured!')
34
+ return
35
+ end
36
+
37
+ @configured = true
38
+
39
+ @callbacks ||= {}
40
+ config.directories.each do |dir|
41
+ callback = @callbacks[dir] || lambda do |event|
42
+ _process_event(dir, event)
43
+ end
44
+ @callbacks[dir] = callback
45
+ _configure(dir, &callback)
46
+ end
47
+
48
+ @snapshots ||= {}
49
+ # TODO: separate config per directory (some day maybe)
50
+ change_config = Change::Config.new(config.queue, config.silencer)
51
+ config.directories.each do |dir|
52
+ record = Record.new(dir)
53
+ snapshot = Change.new(change_config, record)
54
+ @snapshots[dir] = snapshot
55
+ end
56
+ end
57
+
58
+ def started?
59
+ @started
60
+ end
61
+
62
+ def start
63
+ configure
64
+
65
+ if started?
66
+ _log(:warn, 'Adapter already started!')
67
+ return
68
+ end
69
+
70
+ @started = true
71
+
72
+ calling_stack = caller.dup
73
+ Driskell::Listen::Internals::ThreadPool.add do
74
+ begin
75
+ @snapshots.values.each do |snapshot|
76
+ _timed('Record.build()') { snapshot.record.build }
77
+ end
78
+ _run
79
+ rescue
80
+ msg = 'run() in thread failed: %s:\n'\
81
+ ' %s\n\ncalled from:\n %s'
82
+ _log_exception(msg, calling_stack)
83
+ raise # for unit tests mostly
84
+ end
85
+ end
86
+ end
87
+
88
+ def stop
89
+ _stop
90
+ end
91
+
92
+ def self.usable?
93
+ const_get('OS_REGEXP') =~ RbConfig::CONFIG['target_os']
94
+ end
95
+
96
+ # Seeds
97
+ def preempt_change(dir, rel_path, data)
98
+ if @snapshots.nil? || !@snapshots.key?(dir)
99
+ _log(:warn, 'Cannot preempt change for directory not being watched')
100
+ return
101
+ end
102
+
103
+ @snapshots[dir].record.update_file(rel_path, data)
104
+ end
105
+
106
+ private
107
+
108
+ def _stop
109
+ end
110
+
111
+ def _timed(title)
112
+ start = Time.now.to_f
113
+ yield
114
+ diff = Time.now.to_f - start
115
+ Driskell::Listen::Logger.info format('%s: %.05f seconds', title, diff)
116
+ rescue
117
+ Driskell::Listen::Logger.warn "#{title} crashed: #{$ERROR_INFO.inspect}"
118
+ raise
119
+ end
120
+
121
+ # TODO: allow backend adapters to pass specific invalidation objects
122
+ # e.g. Darwin -> DirRescan, INotify -> MoveScan, etc.
123
+ def _queue_change(type, dir, rel_path, options = {})
124
+ @snapshots[dir].invalidate(type, rel_path, options)
125
+ end
126
+
127
+ def _log(*args, &block)
128
+ self.class.send(:_log, *args, &block)
129
+ end
130
+
131
+ def _log_exception(msg, caller_stack)
132
+ formatted = format(
133
+ msg,
134
+ $ERROR_INFO,
135
+ $ERROR_POSITION * "\n",
136
+ caller_stack * "\n"
137
+ )
138
+
139
+ _log(:error, formatted)
140
+ end
141
+
142
+ def self._log(*args, &block)
143
+ Driskell::Listen::Logger.send(*args, &block)
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,106 @@
1
+ # Listener implementation for BSD's `kqueue`.
2
+ # @see http://www.freebsd.org/cgi/man.cgi?query=kqueue
3
+ # @see https://github.com/mat813/rb-kqueue/blob/master/lib/rb-kqueue/queue.rb
4
+ #
5
+ module Driskell::Listen
6
+ module Adapter
7
+ class BSD < Base
8
+ OS_REGEXP = /bsd|dragonfly/i
9
+
10
+ DEFAULTS = {
11
+ events: [
12
+ :delete,
13
+ :write,
14
+ :extend,
15
+ :attrib,
16
+ :rename
17
+ # :link, :revoke
18
+ ]
19
+ }
20
+
21
+ BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
22
+ Please add the following to your Gemfile to avoid polling for changes:
23
+ require 'rbconfig'
24
+ if RbConfig::CONFIG['target_os'] =~ /#{OS_REGEXP}/
25
+ gem 'rb-kqueue', '>= 0.2'
26
+ end
27
+ EOS
28
+
29
+ def self.usable?
30
+ return false unless super
31
+ require 'rb-kqueue'
32
+ require 'find'
33
+ true
34
+ rescue LoadError
35
+ Kernel.warn BUNDLER_DECLARE_GEM
36
+ false
37
+ end
38
+
39
+ private
40
+
41
+ def _configure(directory, &_callback)
42
+ @worker ||= KQueue::Queue.new
43
+ @callback = _callback
44
+ # use Record to make a snapshot of dir, so we
45
+ # can detect new files
46
+ _find(directory.to_s) { |path| _watch_file(path, @worker) }
47
+ end
48
+
49
+ def _run
50
+ @worker.run
51
+ end
52
+
53
+ def _process_event(dir, event)
54
+ full_path = _event_path(event)
55
+ if full_path.directory?
56
+ # Force dir content tracking to kick in, or we won't have
57
+ # names of added files
58
+ _queue_change(:dir, dir, '.', recursive: true)
59
+ elsif full_path.exist?
60
+ path = full_path.relative_path_from(dir)
61
+ _queue_change(:file, dir, path.to_s, change: _change(event.flags))
62
+ end
63
+
64
+ # If it is a directory, and it has a write flag, it means a
65
+ # file has been added so find out which and deal with it.
66
+ # No need to check for removed files, kqueue will forget them
67
+ # when the vfs does.
68
+ _watch_for_new_file(event) if full_path.directory?
69
+ end
70
+
71
+ def _change(event_flags)
72
+ { modified: [:attrib, :extend],
73
+ added: [:write],
74
+ removed: [:rename, :delete]
75
+ }.each do |change, flags|
76
+ return change unless (flags & event_flags).empty?
77
+ end
78
+ nil
79
+ end
80
+
81
+ def _event_path(event)
82
+ Pathname.new(event.watcher.path)
83
+ end
84
+
85
+ def _watch_for_new_file(event)
86
+ queue = event.watcher.queue
87
+ _find(_event_path(event).to_s) do |file_path|
88
+ unless queue.watchers.detect { |_, v| v.path == file_path.to_s }
89
+ _watch_file(file_path, queue)
90
+ end
91
+ end
92
+ end
93
+
94
+ def _watch_file(path, queue)
95
+ queue.watch_file(path, *options.events, &@callback)
96
+ rescue Errno::ENOENT => e
97
+ _log :warn, "kqueue: watch file failed: #{e.message}"
98
+ end
99
+
100
+ # Quick rubocop workaround
101
+ def _find(*paths, &block)
102
+ Find.send(:find, *paths, &block)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,26 @@
1
+ require 'pathname'
2
+
3
+ module Driskell::Listen
4
+ module Adapter
5
+ class Config
6
+ attr_reader :directories
7
+ attr_reader :silencer
8
+ attr_reader :queue
9
+ attr_reader :adapter_options
10
+
11
+ def initialize(directories, queue, silencer, adapter_options)
12
+ # Default to current directory if no directories are supplied
13
+ directories = [Dir.pwd] if directories.to_a.empty?
14
+
15
+ # TODO: fix (flatten, array, compact?)
16
+ @directories = directories.map do |directory|
17
+ Pathname.new(directory.to_s).realpath
18
+ end
19
+
20
+ @silencer = silencer
21
+ @queue = queue
22
+ @adapter_options = adapter_options
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,71 @@
1
+ require 'thread'
2
+ require 'driskell-listen/internals/thread_pool'
3
+
4
+ module Driskell::Listen
5
+ module Adapter
6
+ # Adapter implementation for Mac OS X `FSEvents`.
7
+ #
8
+ class Darwin < Base
9
+ OS_REGEXP = /darwin(1.+)?$/i
10
+
11
+ # The default delay between checking for changes.
12
+ DEFAULTS = { latency: 0.1 }
13
+
14
+ private
15
+
16
+ # NOTE: each directory gets a DIFFERENT callback!
17
+ def _configure(dir, &callback)
18
+ require 'rb-fsevent'
19
+
20
+ opts = { latency: options.latency }
21
+
22
+ @workers ||= ::Queue.new
23
+ @workers << FSEvent.new.tap do |worker|
24
+ _log :debug, "fsevent: watching: #{dir.to_s.inspect}"
25
+ worker.watch(dir.to_s, opts, &callback)
26
+ end
27
+ end
28
+
29
+ def _run
30
+ first = @workers.pop
31
+
32
+ # NOTE: _run is called within a thread, so run every other
33
+ # worker in it's own thread
34
+ _run_workers_in_background(_to_array(@workers))
35
+ _run_worker(first)
36
+ end
37
+
38
+ def _process_event(dir, event)
39
+ _log :debug, "fsevent: processing event: #{event.inspect}"
40
+ event.each do |path|
41
+ new_path = Pathname.new(path.sub(/\/$/, ''))
42
+ _log :debug, "fsevent: #{new_path}"
43
+ # TODO: does this preserve symlinks?
44
+ rel_path = new_path.relative_path_from(dir).to_s
45
+ _queue_change(:dir, dir, rel_path)
46
+ end
47
+ end
48
+
49
+ def _run_worker(worker)
50
+ _log :debug, "fsevent: running worker: #{worker.inspect}"
51
+ worker.run
52
+ rescue
53
+ _log_exception 'fsevent: running worker failed: %s:%s called from: %s', caller
54
+ end
55
+
56
+ def _run_workers_in_background(workers)
57
+ workers.each do |worker|
58
+ # NOTE: while passing local variables to the block below is not
59
+ # thread safe, using 'worker' from the enumerator above is ok
60
+ Driskell::Listen::Internals::ThreadPool.add { _run_worker(worker) }
61
+ end
62
+ end
63
+
64
+ def _to_array(queue)
65
+ workers = []
66
+ workers << queue.pop until queue.empty?
67
+ workers
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,96 @@
1
+ module Driskell::Listen
2
+ module Adapter
3
+ # @see https://github.com/nex3/rb-inotify
4
+ class Linux < Base
5
+ OS_REGEXP = /linux/i
6
+
7
+ DEFAULTS = {
8
+ events: [
9
+ :recursive,
10
+ :attrib,
11
+ :create,
12
+ :delete,
13
+ :move,
14
+ :close_write
15
+ ],
16
+ wait_for_delay: 0.1
17
+ }
18
+
19
+ private
20
+
21
+ WIKI_URL = 'https://github.com/guard/listen'\
22
+ '/wiki/Increasing-the-amount-of-inotify-watchers'
23
+
24
+ INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '')
25
+ FATAL: Listen error: unable to monitor directories for changes.
26
+ Visit #{WIKI_URL} for info on how to fix this.
27
+ EOS
28
+
29
+ def _configure(directory, &callback)
30
+ require 'rb-inotify'
31
+ @worker ||= ::INotify::Notifier.new
32
+ @worker.watch(directory.to_s, *options.events, &callback)
33
+ rescue Errno::ENOSPC
34
+ abort(INOTIFY_LIMIT_MESSAGE)
35
+ end
36
+
37
+ def _run
38
+ @worker.run
39
+ end
40
+
41
+ def _process_event(dir, event)
42
+ # NOTE: avoid using event.absolute_name since new API
43
+ # will need to have a custom recursion implemented
44
+ # to properly match events to configured directories
45
+ path = Pathname.new(event.watcher.path) + event.name
46
+ rel_path = path.relative_path_from(dir).to_s
47
+
48
+ _log(:debug) { "inotify: #{rel_path} (#{event.flags.inspect})" }
49
+
50
+ return if _skip_event?(event)
51
+
52
+ cookie_params = event.cookie.zero? ? {} : { cookie: event.cookie }
53
+
54
+ # Note: don't pass options to force rescanning the directory, so we can
55
+ # detect moving/deleting a whole tree
56
+ if _dir_event?(event)
57
+ _queue_change(:dir, dir, rel_path, cookie_params)
58
+ return
59
+ end
60
+
61
+ params = cookie_params.merge(change: _change(event.flags))
62
+
63
+ _queue_change(:file, dir, rel_path, params)
64
+ end
65
+
66
+ def _skip_event?(event)
67
+ # Event on root directory
68
+ return true if event.name == ''
69
+ # INotify reports changes to files inside directories as events
70
+ # on the directories themselves too.
71
+ #
72
+ # @see http://linux.die.net/man/7/inotify
73
+ _dir_event?(event) && (event.flags & [:close, :modify]).any?
74
+ end
75
+
76
+ def _change(event_flags)
77
+ { modified: [:attrib, :close_write],
78
+ moved_to: [:moved_to],
79
+ moved_from: [:moved_from],
80
+ added: [:create],
81
+ removed: [:delete] }.each do |change, flags|
82
+ return change unless (flags & event_flags).empty?
83
+ end
84
+ nil
85
+ end
86
+
87
+ def _dir_event?(event)
88
+ event.flags.include?(:isdir)
89
+ end
90
+
91
+ def _stop
92
+ @worker.close
93
+ end
94
+ end
95
+ end
96
+ end