sass-listen 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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