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,21 @@
1
+ module Driskell::Listen
2
+ # @private api
3
+ module Internals
4
+ module ThreadPool
5
+ def self.add(&block)
6
+ Thread.new { block.call }.tap do |th|
7
+ (@threads ||= Queue.new) << th
8
+ end
9
+ end
10
+
11
+ def self.stop
12
+ return unless @threads ||= nil
13
+ return if @threads.empty? # return to avoid using possibly stubbed Queue
14
+
15
+ killed = Queue.new
16
+ killed << @threads.pop.kill until @threads.empty?
17
+ killed.pop.join until killed.empty?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,132 @@
1
+ require 'English'
2
+
3
+ require 'driskell-listen/version'
4
+
5
+ require 'driskell-listen/backend'
6
+
7
+ require 'driskell-listen/silencer'
8
+ require 'driskell-listen/silencer/controller'
9
+
10
+ require 'driskell-listen/queue_optimizer'
11
+
12
+ require 'driskell-listen/fsm'
13
+
14
+ require 'driskell-listen/event/loop'
15
+ require 'driskell-listen/event/queue'
16
+ require 'driskell-listen/event/config'
17
+
18
+ require 'driskell-listen/listener/config'
19
+
20
+ module Driskell::Listen
21
+ class Listener
22
+ include Driskell::Listen::FSM
23
+
24
+ # Initializes the directories listener.
25
+ #
26
+ # @param [String] directory the directories to listen to
27
+ # @param [Hash] options the listen options (see Driskell::Listen::Listener::Options)
28
+ #
29
+ # @yield [modified, added, removed] the changed files
30
+ # @yieldparam [Array<String>] modified the list of modified files
31
+ # @yieldparam [Array<String>] added the list of added files
32
+ # @yieldparam [Array<String>] removed the list of removed files
33
+ #
34
+ def initialize(*dirs, &block)
35
+ options = dirs.last.is_a?(Hash) ? dirs.pop : {}
36
+
37
+ @config = Config.new(options)
38
+
39
+ eq_config = Event::Queue::Config.new(@config.relative?)
40
+ queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event }
41
+
42
+ silencer = Silencer.new
43
+ rules = @config.silencer_rules
44
+ @silencer_controller = Silencer::Controller.new(silencer, rules)
45
+
46
+ @backend = Backend.new(dirs, queue, silencer, @config)
47
+
48
+ optimizer_config = QueueOptimizer::Config.new(@backend, silencer)
49
+
50
+ pconfig = Event::Config.new(
51
+ self,
52
+ queue,
53
+ QueueOptimizer.new(optimizer_config),
54
+ @backend.min_delay_between_events,
55
+ &block)
56
+
57
+ @processor = Event::Loop.new(pconfig)
58
+
59
+ super() # FSM
60
+ end
61
+
62
+ default_state :initializing
63
+
64
+ state :initializing, to: :backend_started
65
+
66
+ state :backend_started, to: [:frontend_ready] do
67
+ backend.start
68
+ end
69
+
70
+ state :frontend_ready, to: [:processing_events] do
71
+ processor.setup
72
+ end
73
+
74
+ state :processing_events, to: [:paused, :stopped] do
75
+ processor.resume
76
+ end
77
+
78
+ state :paused, to: [:processing_events, :stopped] do
79
+ processor.pause
80
+ end
81
+
82
+ state :stopped, to: [:backend_started] do
83
+ backend.stop # should be before processor.teardown to halt events ASAP
84
+ processor.teardown
85
+ end
86
+
87
+ # Starts processing events and starts adapters
88
+ # or resumes invoking callbacks if paused
89
+ def start
90
+ transition :backend_started if state == :initializing
91
+ transition :frontend_ready if state == :backend_started
92
+ transition :processing_events if state == :frontend_ready
93
+ transition :processing_events if state == :paused
94
+ end
95
+
96
+ # Stops both listening for events and processing them
97
+ def stop
98
+ transition :stopped
99
+ end
100
+
101
+ # Stops invoking callbacks (messages pile up)
102
+ def pause
103
+ transition :paused
104
+ end
105
+
106
+ # processing means callbacks are called
107
+ def processing?
108
+ state == :processing_events
109
+ end
110
+
111
+ def paused?
112
+ state == :paused
113
+ end
114
+
115
+ def ignore(regexps)
116
+ @silencer_controller.append_ignores(regexps)
117
+ end
118
+
119
+ def ignore!(regexps)
120
+ @silencer_controller.replace_with_bang_ignores(regexps)
121
+ end
122
+
123
+ def only(regexps)
124
+ @silencer_controller.replace_with_only(regexps)
125
+ end
126
+
127
+ private
128
+
129
+ attr_reader :processor
130
+ attr_reader :backend
131
+ end
132
+ end
@@ -0,0 +1,45 @@
1
+ module Driskell::Listen
2
+ class Listener
3
+ class Config
4
+ DEFAULTS = {
5
+ # Listener options
6
+ debug: false, # TODO: is this broken?
7
+ wait_for_delay: nil, # NOTE: should be provided by adapter if possible
8
+ relative: false,
9
+
10
+ # Backend selecting options
11
+ force_polling: false,
12
+ polling_fallback_message: nil
13
+ }
14
+
15
+ def initialize(opts)
16
+ @options = DEFAULTS.merge(opts)
17
+ @relative = @options[:relative]
18
+ @min_delay_between_events = @options[:wait_for_delay]
19
+ @silencer_rules = @options # silencer will extract what it needs
20
+ end
21
+
22
+ def relative?
23
+ @relative
24
+ end
25
+
26
+ def min_delay_between_events
27
+ @min_delay_between_events
28
+ end
29
+
30
+ def silencer_rules
31
+ @silencer_rules
32
+ end
33
+
34
+ def adapter_instance_options(klass)
35
+ valid_keys = klass.const_get('DEFAULTS').keys
36
+ Hash[@options.select { |key, _| valid_keys.include?(key) }]
37
+ end
38
+
39
+ def adapter_select_options
40
+ valid_keys = %w(force_polling polling_fallback_message).map(&:to_sym)
41
+ Hash[@options.select { |key, _| valid_keys.include?(key) }]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module Driskell::Listen
2
+ def self.logger
3
+ @logger ||= nil
4
+ end
5
+
6
+ def self.logger=(logger)
7
+ @logger = logger
8
+ end
9
+
10
+ def self.setup_default_logger_if_unset
11
+ self.logger ||= ::Logger.new(STDERR).tap do |logger|
12
+ debugging = ENV['LISTEN_GEM_DEBUGGING']
13
+ logger.level =
14
+ case debugging.to_s
15
+ when /2/
16
+ ::Logger::DEBUG
17
+ when /true|yes|1/i
18
+ ::Logger::INFO
19
+ else
20
+ ::Logger::ERROR
21
+ end
22
+ end
23
+ end
24
+
25
+ class Logger
26
+ [:fatal, :error, :warn, :info, :debug].each do |meth|
27
+ define_singleton_method(meth) do |*args, &block|
28
+ Driskell::Listen.logger.public_send(meth, *args, &block) if Driskell::Listen.logger
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module Driskell::Listen
2
+ class Options
3
+ def initialize(opts, defaults)
4
+ @options = {}
5
+ given_options = opts.dup
6
+ defaults.keys.each do |key|
7
+ @options[key] = given_options.delete(key) || defaults[key]
8
+ end
9
+
10
+ return if given_options.empty?
11
+
12
+ msg = "Unknown options: #{given_options.inspect}"
13
+ Driskell::Listen::Logger.warn msg
14
+ fail msg
15
+ end
16
+
17
+ def method_missing(name, *_)
18
+ return @options[name] if @options.key?(name)
19
+ msg = "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})"
20
+ fail NameError, msg
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,132 @@
1
+ module Driskell::Listen
2
+ class QueueOptimizer
3
+ class Config
4
+ def initialize(adapter_class, silencer)
5
+ @adapter_class = adapter_class
6
+ @silencer = silencer
7
+ end
8
+
9
+ def exist?(path)
10
+ Pathname(path).exist?
11
+ end
12
+
13
+ def silenced?(path, type)
14
+ @silencer.silenced?(path, type)
15
+ end
16
+
17
+ def debug(*args, &block)
18
+ Driskell::Listen.logger.debug(*args, &block)
19
+ end
20
+ end
21
+
22
+ def smoosh_changes(changes)
23
+ # TODO: adapter could be nil at this point (shutdown)
24
+ cookies = changes.group_by do |_, _, _, _, options|
25
+ (options || {})[:cookie]
26
+ end
27
+ _squash_changes(_reinterpret_related_changes(cookies))
28
+ end
29
+
30
+ def initialize(config)
31
+ @config = config
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :config
37
+
38
+ # groups changes into the expected structure expected by
39
+ # clients
40
+ def _squash_changes(changes)
41
+ # We combine here for backward compatibility
42
+ # Newer clients should receive dir and path separately
43
+ changes = changes.map { |change, dir, path| [change, dir + path] }
44
+
45
+ actions = changes.group_by(&:last).map do |path, action_list|
46
+ [_logical_action_for(path, action_list.map(&:first)), path.to_s]
47
+ end
48
+
49
+ config.debug("listen: raw changes: #{actions.inspect}")
50
+
51
+ { modified: [], added: [], removed: [] }.tap do |squashed|
52
+ actions.each do |type, path|
53
+ squashed[type] << path unless type.nil?
54
+ end
55
+ config.debug("listen: final changes: #{squashed.inspect}")
56
+ end
57
+ end
58
+
59
+ def _logical_action_for(path, actions)
60
+ actions << :added if actions.delete(:moved_to)
61
+ actions << :removed if actions.delete(:moved_from)
62
+
63
+ modified = actions.detect { |x| x == :modified }
64
+ _calculate_add_remove_difference(actions, path, modified)
65
+ end
66
+
67
+ def _calculate_add_remove_difference(actions, path, default_if_exists)
68
+ added = actions.count { |x| x == :added }
69
+ removed = actions.count { |x| x == :removed }
70
+ diff = added - removed
71
+
72
+ # TODO: avoid checking if path exists and instead assume the events are
73
+ # in order (if last is :removed, it doesn't exist, etc.)
74
+ if config.exist?(path)
75
+ if diff > 0
76
+ :added
77
+ elsif diff.zero? && added > 0
78
+ :modified
79
+ else
80
+ default_if_exists
81
+ end
82
+ else
83
+ diff < 0 ? :removed : nil
84
+ end
85
+ end
86
+
87
+ # remove extraneous rb-inotify events, keeping them only if it's a possible
88
+ # editor rename() call (e.g. Kate and Sublime)
89
+ def _reinterpret_related_changes(cookies)
90
+ table = { moved_to: :added, moved_from: :removed }
91
+ cookies.map do |_, changes|
92
+ data = _detect_possible_editor_save(changes)
93
+ if data
94
+ to_dir, to_file = data
95
+ [[:modified, to_dir, to_file]]
96
+ else
97
+ not_silenced = changes.reject do |type, _, _, path, _|
98
+ config.silenced?(Pathname(path), type)
99
+ end
100
+ not_silenced.map do |_, change, dir, path, _|
101
+ [table.fetch(change, change), dir, path]
102
+ end
103
+ end
104
+ end.flatten(1)
105
+ end
106
+
107
+ def _detect_possible_editor_save(changes)
108
+ return unless changes.size == 2
109
+
110
+ from_type = from_change = from = nil
111
+ to_type = to_change = to_dir = to = nil
112
+
113
+ changes.each do |data|
114
+ case data[1]
115
+ when :moved_from
116
+ from_type, from_change, _, from, _ = data
117
+ when :moved_to
118
+ to_type, to_change, to_dir, to, _ = data
119
+ else
120
+ return nil
121
+ end
122
+ end
123
+
124
+ return unless from && to
125
+
126
+ # Expect an ignored moved_from and non-ignored moved_to
127
+ # to qualify as an "editor modify"
128
+ return unless config.silenced?(Pathname(from), from_type)
129
+ config.silenced?(Pathname(to), to_type) ? nil : [to_dir, to]
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,104 @@
1
+ require 'thread'
2
+ require 'driskell-listen/record/entry'
3
+
4
+ module Driskell::Listen
5
+ class Record
6
+ attr_reader :root
7
+
8
+ def initialize(directory)
9
+ @tree = _auto_hash
10
+ @tree['.'] = _auto_hash
11
+ @root = directory.to_s
12
+ end
13
+
14
+ def update_dir(rel_path)
15
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
16
+ _fast_update_dir(rel_path, dirname, basename)
17
+ end
18
+
19
+ def update_file(rel_path, data)
20
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
21
+ _fast_update_file(dirname, basename, data)
22
+ end
23
+
24
+ def unset_path(rel_path)
25
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
26
+ _fast_unset_path(rel_path, dirname, basename)
27
+ end
28
+
29
+ def file_data(rel_path)
30
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
31
+ tree[dirname] ||= {}
32
+ tree[dirname][basename] ||= {}
33
+ tree[dirname][basename].dup
34
+ end
35
+
36
+ def dir_entries(rel_path)
37
+ rel_path = rel_path.to_s
38
+ # Do not store anything for the directory when querying
39
+ tree.has_key?(rel_path) ? tree[rel_path] : {}
40
+ end
41
+
42
+ def build
43
+ @tree = _auto_hash
44
+ # TODO: test with a file name given
45
+ # TODO: test other permissions
46
+ # TODO: test with mixed encoding
47
+ remaining = ::Queue.new
48
+ remaining << Entry.new(root, nil, nil)
49
+ _fast_build_dir(remaining) until remaining.empty?
50
+ end
51
+
52
+ private
53
+
54
+ def _auto_hash
55
+ Hash.new { |h, k| h[k] = Hash.new }
56
+ end
57
+
58
+ def tree
59
+ @tree
60
+ end
61
+
62
+ def _fast_update_dir(record_as_key, dirname, basename)
63
+ tree[record_as_key] ||= {}
64
+ tree[dirname] ||= {}
65
+ exists = tree[dirname].has_key?(basename)
66
+ tree[dirname].merge!(basename => {}) if basename != '.'
67
+ exists
68
+ end
69
+
70
+ def _fast_update_file(dirname, basename, data)
71
+ tree[dirname] ||= {}
72
+ exists = tree[dirname].has_key?(basename)
73
+ tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
74
+ exists
75
+ end
76
+
77
+ def _fast_unset_path(rel_path, dirname, basename)
78
+ # this may need to be reworked to properly remove
79
+ # entries from a tree, without adding non-existing dirs to the record
80
+ return unless tree.key?(dirname)
81
+ tree[dirname].delete basename
82
+ tree.delete rel_path
83
+ end
84
+
85
+ def _fast_build_dir(remaining)
86
+ entry = remaining.pop
87
+ fail Errno::ENOTDIR if ::File.symlink?(entry.sys_path)
88
+ children = entry.children
89
+ children.each { |child| remaining << child }
90
+ return if entry.name.nil?
91
+ _fast_update_dir(entry.record_dir_key, entry.relative, entry.name)
92
+ rescue Errno::ENOTDIR
93
+ _fast_try_file(entry)
94
+ rescue SystemCallError
95
+ _fast_unset_path(entry.relative, entry.name)
96
+ end
97
+
98
+ def _fast_try_file(entry)
99
+ _fast_update_file(entry.relative, entry.name, entry.meta)
100
+ rescue SystemCallError
101
+ _fast_unset_path(entry.record_dir_key, entry.relative, entry.name)
102
+ end
103
+ end
104
+ end