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,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
@@ -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
@@ -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