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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/CONTRIBUTING.md +38 -0
- data/LICENSE.txt +22 -0
- data/README.md +297 -0
- data/bin/listen +12 -0
- data/lib/listen.rb +55 -0
- data/lib/listen/adapter.rb +43 -0
- data/lib/listen/adapter/base.rb +137 -0
- data/lib/listen/adapter/bsd.rb +106 -0
- data/lib/listen/adapter/config.rb +26 -0
- data/lib/listen/adapter/darwin.rb +71 -0
- data/lib/listen/adapter/linux.rb +106 -0
- data/lib/listen/adapter/polling.rb +37 -0
- data/lib/listen/adapter/windows.rb +99 -0
- data/lib/listen/backend.rb +41 -0
- data/lib/listen/change.rb +78 -0
- data/lib/listen/cli.rb +65 -0
- data/lib/listen/directory.rb +76 -0
- data/lib/listen/event/config.rb +59 -0
- data/lib/listen/event/loop.rb +117 -0
- data/lib/listen/event/processor.rb +122 -0
- data/lib/listen/event/queue.rb +56 -0
- data/lib/listen/file.rb +80 -0
- data/lib/listen/fsm.rb +131 -0
- data/lib/listen/internals/thread_pool.rb +21 -0
- data/lib/listen/listener.rb +132 -0
- data/lib/listen/listener/config.rb +45 -0
- data/lib/listen/logger.rb +32 -0
- data/lib/listen/options.rb +23 -0
- data/lib/listen/queue_optimizer.rb +132 -0
- data/lib/listen/record.rb +120 -0
- data/lib/listen/record/entry.rb +51 -0
- data/lib/listen/record/symlink_detector.rb +39 -0
- data/lib/listen/silencer.rb +97 -0
- data/lib/listen/silencer/controller.rb +48 -0
- data/lib/listen/version.rb +3 -0
- metadata +124 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
require 'listen/event/processor'
|
5
|
+
|
6
|
+
module Listen
|
7
|
+
module Event
|
8
|
+
class Loop
|
9
|
+
class Error < RuntimeError
|
10
|
+
class NotStarted < Error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
@wait_thread = nil
|
17
|
+
@state = :paused
|
18
|
+
@reasons = ::Queue.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def wakeup_on_event
|
22
|
+
return if stopped?
|
23
|
+
return unless processing?
|
24
|
+
return unless wait_thread.alive?
|
25
|
+
_wakeup(:event)
|
26
|
+
end
|
27
|
+
|
28
|
+
def paused?
|
29
|
+
wait_thread && state == :paused
|
30
|
+
end
|
31
|
+
|
32
|
+
def processing?
|
33
|
+
return false if stopped?
|
34
|
+
return false if paused?
|
35
|
+
state == :processing
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup
|
39
|
+
# TODO: use a Fiber instead?
|
40
|
+
q = ::Queue.new
|
41
|
+
@wait_thread = Internals::ThreadPool.add do
|
42
|
+
_wait_for_changes(q, config)
|
43
|
+
end
|
44
|
+
|
45
|
+
Listen::Logger.debug('Waiting for processing to start...')
|
46
|
+
Timeout.timeout(5) { q.pop }
|
47
|
+
end
|
48
|
+
|
49
|
+
def resume
|
50
|
+
fail Error::NotStarted if stopped?
|
51
|
+
return unless wait_thread
|
52
|
+
_wakeup(:resume)
|
53
|
+
end
|
54
|
+
|
55
|
+
def pause
|
56
|
+
# TODO: works?
|
57
|
+
# fail NotImplementedError
|
58
|
+
end
|
59
|
+
|
60
|
+
def teardown
|
61
|
+
return unless wait_thread
|
62
|
+
if wait_thread.alive?
|
63
|
+
_wakeup(:teardown)
|
64
|
+
wait_thread.join
|
65
|
+
end
|
66
|
+
@wait_thread = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def stopped?
|
70
|
+
!wait_thread
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
attr_reader :config
|
76
|
+
attr_reader :wait_thread
|
77
|
+
|
78
|
+
attr_accessor :state
|
79
|
+
|
80
|
+
def _wait_for_changes(ready_queue, config)
|
81
|
+
processor = Event::Processor.new(config, @reasons)
|
82
|
+
|
83
|
+
_wait_until_resumed(ready_queue)
|
84
|
+
processor.loop_for(config.min_delay_between_events)
|
85
|
+
rescue StandardError => ex
|
86
|
+
_nice_error(ex)
|
87
|
+
end
|
88
|
+
|
89
|
+
def _sleep(*args)
|
90
|
+
Kernel.sleep(*args)
|
91
|
+
end
|
92
|
+
|
93
|
+
def _wait_until_resumed(ready_queue)
|
94
|
+
self.state = :paused
|
95
|
+
ready_queue << :ready
|
96
|
+
sleep
|
97
|
+
self.state = :processing
|
98
|
+
end
|
99
|
+
|
100
|
+
def _nice_error(ex)
|
101
|
+
indent = "\n -- "
|
102
|
+
msg = format(
|
103
|
+
'exception while processing events: %s Backtrace:%s%s',
|
104
|
+
ex,
|
105
|
+
indent,
|
106
|
+
ex.backtrace * indent
|
107
|
+
)
|
108
|
+
Listen::Logger.error(msg)
|
109
|
+
end
|
110
|
+
|
111
|
+
def _wakeup(reason)
|
112
|
+
@reasons << reason
|
113
|
+
wait_thread.wakeup
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Listen
|
2
|
+
module Event
|
3
|
+
class Processor
|
4
|
+
def initialize(config, reasons)
|
5
|
+
@config = config
|
6
|
+
@reasons = reasons
|
7
|
+
_reset_no_unprocessed_events
|
8
|
+
end
|
9
|
+
|
10
|
+
# TODO: implement this properly instead of checking the state at arbitrary
|
11
|
+
# points in time
|
12
|
+
def loop_for(latency)
|
13
|
+
@latency = latency
|
14
|
+
|
15
|
+
loop do
|
16
|
+
_wait_until_events
|
17
|
+
_wait_until_events_calm_down
|
18
|
+
_wait_until_no_longer_paused
|
19
|
+
_process_changes
|
20
|
+
end
|
21
|
+
rescue Stopped
|
22
|
+
Listen::Logger.debug('Processing stopped')
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
class Stopped < RuntimeError
|
28
|
+
end
|
29
|
+
|
30
|
+
def _wait_until_events_calm_down
|
31
|
+
loop do
|
32
|
+
now = _timestamp
|
33
|
+
|
34
|
+
# Assure there's at least latency between callbacks to allow
|
35
|
+
# for accumulating changes
|
36
|
+
diff = _deadline - now
|
37
|
+
break if diff <= 0
|
38
|
+
|
39
|
+
# give events a bit of time to accumulate so they can be
|
40
|
+
# compressed/optimized
|
41
|
+
_sleep(:waiting_until_latency, diff)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def _wait_until_no_longer_paused
|
46
|
+
# TODO: may not be a good idea?
|
47
|
+
_sleep(:waiting_for_unpause) while config.paused?
|
48
|
+
end
|
49
|
+
|
50
|
+
def _check_stopped
|
51
|
+
return unless config.stopped?
|
52
|
+
|
53
|
+
_flush_wakeup_reasons
|
54
|
+
raise Stopped
|
55
|
+
end
|
56
|
+
|
57
|
+
def _sleep(_local_reason, *args)
|
58
|
+
_check_stopped
|
59
|
+
sleep_duration = config.sleep(*args)
|
60
|
+
_check_stopped
|
61
|
+
|
62
|
+
_flush_wakeup_reasons do |reason|
|
63
|
+
next unless reason == :event
|
64
|
+
_remember_time_of_first_unprocessed_event unless config.paused?
|
65
|
+
end
|
66
|
+
|
67
|
+
sleep_duration
|
68
|
+
end
|
69
|
+
|
70
|
+
def _remember_time_of_first_unprocessed_event
|
71
|
+
@first_unprocessed_event_time ||= _timestamp
|
72
|
+
end
|
73
|
+
|
74
|
+
def _reset_no_unprocessed_events
|
75
|
+
@first_unprocessed_event_time = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def _deadline
|
79
|
+
@first_unprocessed_event_time + @latency
|
80
|
+
end
|
81
|
+
|
82
|
+
def _wait_until_events
|
83
|
+
# TODO: long sleep may not be a good idea?
|
84
|
+
_sleep(:waiting_for_events) while config.event_queue.empty?
|
85
|
+
@first_unprocessed_event_time ||= _timestamp
|
86
|
+
end
|
87
|
+
|
88
|
+
def _flush_wakeup_reasons
|
89
|
+
reasons = @reasons
|
90
|
+
until reasons.empty?
|
91
|
+
reason = reasons.pop
|
92
|
+
yield reason if block_given?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def _timestamp
|
97
|
+
config.timestamp
|
98
|
+
end
|
99
|
+
|
100
|
+
# for easier testing without sleep loop
|
101
|
+
def _process_changes
|
102
|
+
_reset_no_unprocessed_events
|
103
|
+
|
104
|
+
changes = []
|
105
|
+
changes << config.event_queue.pop until config.event_queue.empty?
|
106
|
+
|
107
|
+
callable = config.callable?
|
108
|
+
return unless callable
|
109
|
+
|
110
|
+
hash = config.optimize_changes(changes)
|
111
|
+
result = [hash[:modified], hash[:added], hash[:removed]]
|
112
|
+
return if result.all?(&:empty?)
|
113
|
+
|
114
|
+
block_start = _timestamp
|
115
|
+
config.call(*result)
|
116
|
+
Listen::Logger.debug "Callback took #{_timestamp - block_start} sec"
|
117
|
+
end
|
118
|
+
|
119
|
+
attr_reader :config
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
module Event
|
5
|
+
class Queue
|
6
|
+
class Config
|
7
|
+
def initialize(relative)
|
8
|
+
@relative = relative
|
9
|
+
end
|
10
|
+
|
11
|
+
def relative?
|
12
|
+
@relative
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(config, &block)
|
17
|
+
@event_queue = ::Queue.new
|
18
|
+
@block = block
|
19
|
+
@config = config
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(args)
|
23
|
+
type, change, dir, path, options = *args
|
24
|
+
fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type
|
25
|
+
fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
|
26
|
+
fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
|
27
|
+
|
28
|
+
dir = _safe_relative_from_cwd(dir)
|
29
|
+
event_queue.public_send(:<<, [type, change, dir, path, options])
|
30
|
+
|
31
|
+
block.call(args) if block
|
32
|
+
end
|
33
|
+
|
34
|
+
def empty?
|
35
|
+
event_queue.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def pop
|
39
|
+
event_queue.pop
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :event_queue
|
45
|
+
attr_reader :block
|
46
|
+
attr_reader :config
|
47
|
+
|
48
|
+
def _safe_relative_from_cwd(dir)
|
49
|
+
return dir unless config.relative?
|
50
|
+
dir.relative_path_from(Pathname.pwd)
|
51
|
+
rescue ArgumentError
|
52
|
+
dir
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/listen/file.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
class File
|
5
|
+
def self.change(record, rel_path)
|
6
|
+
path = Pathname.new(record.root) + rel_path
|
7
|
+
lstat = path.lstat
|
8
|
+
|
9
|
+
data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
|
10
|
+
|
11
|
+
record_data = record.file_data(rel_path)
|
12
|
+
|
13
|
+
if record_data.empty?
|
14
|
+
record.update_file(rel_path, data)
|
15
|
+
return :added
|
16
|
+
end
|
17
|
+
|
18
|
+
if data[:mode] != record_data[:mode]
|
19
|
+
record.update_file(rel_path, data)
|
20
|
+
return :modified
|
21
|
+
end
|
22
|
+
|
23
|
+
if data[:mtime] != record_data[:mtime]
|
24
|
+
record.update_file(rel_path, data)
|
25
|
+
return :modified
|
26
|
+
end
|
27
|
+
|
28
|
+
return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING']
|
29
|
+
return unless self.inaccurate_mac_time?(lstat)
|
30
|
+
|
31
|
+
# Check if change happened within 1 second (maybe it's even
|
32
|
+
# too much, e.g. 0.3-0.5 could be sufficient).
|
33
|
+
#
|
34
|
+
# With rb-fsevent, there's a (configurable) latency between
|
35
|
+
# when file was changed and when the event was triggered.
|
36
|
+
#
|
37
|
+
# If a file is saved at ???14.998, by the time the event is
|
38
|
+
# actually received by Listen, the time could already be e.g.
|
39
|
+
# ???15.7.
|
40
|
+
#
|
41
|
+
# And since Darwin adapter uses directory scanning, the file
|
42
|
+
# mtime may be the same (e.g. file was changed at ???14.001,
|
43
|
+
# then at ???14.998, but the fstat time would be ???14.0 in
|
44
|
+
# both cases).
|
45
|
+
#
|
46
|
+
# If change happend at ???14.999997, the mtime is 14.0, so for
|
47
|
+
# an mtime=???14.0 we assume it could even be almost ???15.0
|
48
|
+
#
|
49
|
+
# So if Time.now.to_f is ???15.999998 and stat reports mtime
|
50
|
+
# at ???14.0, then event was due to that file'd change when:
|
51
|
+
#
|
52
|
+
# ???15.999997 - ???14.999998 < 1.0s
|
53
|
+
#
|
54
|
+
# So the "2" is "1 + 1" (1s to cover rb-fsevent latency +
|
55
|
+
# 1s maximum difference between real mtime and that recorded
|
56
|
+
# in the file system)
|
57
|
+
#
|
58
|
+
return if data[:mtime].to_i + 2 <= Time.now.to_f
|
59
|
+
|
60
|
+
md5 = Digest::MD5.file(path).digest
|
61
|
+
record.update_file(rel_path, data.merge(md5: md5))
|
62
|
+
:modified if record_data[:md5] && md5 != record_data[:md5]
|
63
|
+
rescue SystemCallError
|
64
|
+
record.unset_path(rel_path)
|
65
|
+
:removed
|
66
|
+
rescue
|
67
|
+
Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
|
68
|
+
raise
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.inaccurate_mac_time?(stat)
|
72
|
+
# 'mac' means Modified/Accessed/Created
|
73
|
+
|
74
|
+
# Since precision depends on mounted FS (e.g. you can have a FAT partiion
|
75
|
+
# mounted on Linux), check for fields with a remainder to detect this
|
76
|
+
|
77
|
+
[stat.mtime, stat.ctime, stat.atime].map(&:usec).all?(&:zero?)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/listen/fsm.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Code copied from https://github.com/celluloid/celluloid-fsm
|
2
|
+
module Listen
|
3
|
+
module FSM
|
4
|
+
DEFAULT_STATE = :default # Default state name unless one is explicitly set
|
5
|
+
|
6
|
+
# Included hook to extend class methods
|
7
|
+
def self.included(klass)
|
8
|
+
klass.send :extend, ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# Obtain or set the default state
|
13
|
+
# Passing a state name sets the default state
|
14
|
+
def default_state(new_default = nil)
|
15
|
+
if new_default
|
16
|
+
@default_state = new_default.to_sym
|
17
|
+
else
|
18
|
+
defined?(@default_state) ? @default_state : DEFAULT_STATE
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Obtain the valid states for this FSM
|
23
|
+
def states
|
24
|
+
@states ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Declare an FSM state and optionally provide a callback block to fire
|
28
|
+
# Options:
|
29
|
+
# * to: a state or array of states this state can transition to
|
30
|
+
def state(*args, &block)
|
31
|
+
if args.last.is_a? Hash
|
32
|
+
# Stringify keys :/
|
33
|
+
options = args.pop.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
34
|
+
else
|
35
|
+
options = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
args.each do |name|
|
39
|
+
name = name.to_sym
|
40
|
+
default_state name if options['default']
|
41
|
+
states[name] = State.new(name, options['to'], &block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Be kind and call super if you must redefine initialize
|
47
|
+
def initialize
|
48
|
+
@state = self.class.default_state
|
49
|
+
end
|
50
|
+
|
51
|
+
# Obtain the current state of the FSM
|
52
|
+
attr_reader :state
|
53
|
+
|
54
|
+
def transition(state_name)
|
55
|
+
new_state = validate_and_sanitize_new_state(state_name)
|
56
|
+
return unless new_state
|
57
|
+
transition_with_callbacks!(new_state)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Immediate state transition with no checks, or callbacks. "Dangerous!"
|
61
|
+
def transition!(state_name)
|
62
|
+
@state = state_name
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def validate_and_sanitize_new_state(state_name)
|
68
|
+
state_name = state_name.to_sym
|
69
|
+
|
70
|
+
return if current_state_name == state_name
|
71
|
+
|
72
|
+
if current_state && !current_state.valid_transition?(state_name)
|
73
|
+
valid = current_state.transitions.map(&:to_s).join(', ')
|
74
|
+
msg = "#{self.class} can't change state from '#{@state}'"\
|
75
|
+
" to '#{state_name}', only to: #{valid}"
|
76
|
+
fail ArgumentError, msg
|
77
|
+
end
|
78
|
+
|
79
|
+
new_state = states[state_name]
|
80
|
+
|
81
|
+
unless new_state
|
82
|
+
return if state_name == default_state
|
83
|
+
fail ArgumentError, "invalid state for #{self.class}: #{state_name}"
|
84
|
+
end
|
85
|
+
|
86
|
+
new_state
|
87
|
+
end
|
88
|
+
|
89
|
+
def transition_with_callbacks!(state_name)
|
90
|
+
transition! state_name.name
|
91
|
+
state_name.call(self)
|
92
|
+
end
|
93
|
+
|
94
|
+
def states
|
95
|
+
self.class.states
|
96
|
+
end
|
97
|
+
|
98
|
+
def default_state
|
99
|
+
self.class.default_state
|
100
|
+
end
|
101
|
+
|
102
|
+
def current_state
|
103
|
+
states[@state]
|
104
|
+
end
|
105
|
+
|
106
|
+
def current_state_name
|
107
|
+
current_state && current_state.name || ''
|
108
|
+
end
|
109
|
+
|
110
|
+
class State
|
111
|
+
attr_reader :name, :transitions
|
112
|
+
|
113
|
+
def initialize(name, transitions = nil, &block)
|
114
|
+
@name, @block = name, block
|
115
|
+
@transitions = nil
|
116
|
+
@transitions = Array(transitions).map(&:to_sym) if transitions
|
117
|
+
end
|
118
|
+
|
119
|
+
def call(obj)
|
120
|
+
obj.instance_eval(&@block) if @block
|
121
|
+
end
|
122
|
+
|
123
|
+
def valid_transition?(new_state)
|
124
|
+
# All transitions are allowed unless expressly
|
125
|
+
return true unless @transitions
|
126
|
+
|
127
|
+
@transitions.include? new_state.to_sym
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|