sass-listen 3.0.7

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.
@@ -0,0 +1,21 @@
1
+ module 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 'listen/version'
4
+
5
+ require 'listen/backend'
6
+
7
+ require 'listen/silencer'
8
+ require 'listen/silencer/controller'
9
+
10
+ require 'listen/queue_optimizer'
11
+
12
+ require 'listen/fsm'
13
+
14
+ require 'listen/event/loop'
15
+ require 'listen/event/queue'
16
+ require 'listen/event/config'
17
+
18
+ require 'listen/listener/config'
19
+
20
+ module Listen
21
+ class Listener
22
+ include 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 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, :stopped] do
67
+ backend.start
68
+ end
69
+
70
+ state :frontend_ready, to: [:processing_events, :stopped] 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 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 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
+ Listen.logger.public_send(meth, *args, &block) if Listen.logger
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module 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
+ 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 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
+ 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,120 @@
1
+ require 'thread'
2
+ require 'listen/record/entry'
3
+ require 'listen/record/symlink_detector'
4
+
5
+ module Listen
6
+ class Record
7
+ # TODO: one Record object per watched directory?
8
+ # TODO: deprecate
9
+
10
+ attr_reader :root
11
+ def initialize(directory)
12
+ @tree = _auto_hash
13
+ @root = directory.to_s
14
+ end
15
+
16
+ def add_dir(rel_path)
17
+ return if [nil, '', '.'].include? rel_path
18
+ @tree[rel_path] ||= {}
19
+ end
20
+
21
+ def update_file(rel_path, data)
22
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
23
+ _fast_update_file(dirname, basename, data)
24
+ end
25
+
26
+ def unset_path(rel_path)
27
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
28
+ _fast_unset_path(dirname, basename)
29
+ end
30
+
31
+ def file_data(rel_path)
32
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
33
+ if [nil, '', '.'].include? dirname
34
+ tree[basename] ||= {}
35
+ tree[basename].dup
36
+ else
37
+ tree[dirname] ||= {}
38
+ tree[dirname][basename] ||= {}
39
+ tree[dirname][basename].dup
40
+ end
41
+ end
42
+
43
+ def dir_entries(rel_path)
44
+ subtree =
45
+ if [nil, '', '.'].include? rel_path.to_s
46
+ tree
47
+ else
48
+ tree[rel_path.to_s] ||= _auto_hash
49
+ tree[rel_path.to_s]
50
+ end
51
+
52
+ result = {}
53
+ subtree.each do |key, values|
54
+ # only get data for file entries
55
+ result[key] = values.key?(:mtime) ? values : {}
56
+ end
57
+ result
58
+ end
59
+
60
+ def build
61
+ @tree = _auto_hash
62
+ # TODO: test with a file name given
63
+ # TODO: test other permissions
64
+ # TODO: test with mixed encoding
65
+ symlink_detector = SymlinkDetector.new
66
+ remaining = ::Queue.new
67
+ remaining << Entry.new(root, nil, nil)
68
+ _fast_build_dir(remaining, symlink_detector) until remaining.empty?
69
+ end
70
+
71
+ private
72
+
73
+ def _auto_hash
74
+ Hash.new { |h, k| h[k] = Hash.new }
75
+ end
76
+
77
+ def tree
78
+ @tree
79
+ end
80
+
81
+ def _fast_update_file(dirname, basename, data)
82
+ if [nil, '', '.'].include? dirname
83
+ tree[basename] = (tree[basename] || {}).merge(data)
84
+ else
85
+ tree[dirname] ||= {}
86
+ tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
87
+ end
88
+ end
89
+
90
+ def _fast_unset_path(dirname, basename)
91
+ # this may need to be reworked to properly remove
92
+ # entries from a tree, without adding non-existing dirs to the record
93
+ if [nil, '', '.'].include? dirname
94
+ return unless tree.key?(basename)
95
+ tree.delete(basename)
96
+ else
97
+ return unless tree.key?(dirname)
98
+ tree[dirname].delete(basename)
99
+ end
100
+ end
101
+
102
+ def _fast_build_dir(remaining, symlink_detector)
103
+ entry = remaining.pop
104
+ children = entry.children # NOTE: children() implicitly tests if dir
105
+ symlink_detector.verify_unwatched!(entry)
106
+ children.each { |child| remaining << child }
107
+ add_dir(entry.record_dir_key)
108
+ rescue Errno::ENOTDIR
109
+ _fast_try_file(entry)
110
+ rescue SystemCallError, SymlinkDetector::Error
111
+ _fast_unset_path(entry.relative, entry.name)
112
+ end
113
+
114
+ def _fast_try_file(entry)
115
+ _fast_update_file(entry.relative, entry.name, entry.meta)
116
+ rescue SystemCallError
117
+ _fast_unset_path(entry.relative, entry.name)
118
+ end
119
+ end
120
+ end