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