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,137 @@
1
+ require 'listen/options'
2
+ require 'listen/record'
3
+ require 'listen/change'
4
+
5
+ module Listen
6
+ module Adapter
7
+ class Base
8
+ attr_reader :options
9
+
10
+ # TODO: only used by tests
11
+ DEFAULTS = {}
12
+
13
+ attr_reader :config
14
+
15
+ def initialize(config)
16
+ @started = false
17
+ @config = config
18
+
19
+ @configured = nil
20
+
21
+ fail 'No directories to watch!' if config.directories.empty?
22
+
23
+ defaults = self.class.const_get('DEFAULTS')
24
+ @options = Listen::Options.new(config.adapter_options, defaults)
25
+ rescue
26
+ _log_exception 'adapter config failed: %s:%s called from: %s', caller
27
+ raise
28
+ end
29
+
30
+ # TODO: it's a separate method as a temporary workaround for tests
31
+ def configure
32
+ if @configured
33
+ _log(:warn, 'Adapter already configured!')
34
+ return
35
+ end
36
+
37
+ @configured = true
38
+
39
+ @callbacks ||= {}
40
+ config.directories.each do |dir|
41
+ callback = @callbacks[dir] || lambda do |event|
42
+ _process_event(dir, event)
43
+ end
44
+ @callbacks[dir] = callback
45
+ _configure(dir, &callback)
46
+ end
47
+
48
+ @snapshots ||= {}
49
+ # TODO: separate config per directory (some day maybe)
50
+ change_config = Change::Config.new(config.queue, config.silencer)
51
+ config.directories.each do |dir|
52
+ record = Record.new(dir)
53
+ snapshot = Change.new(change_config, record)
54
+ @snapshots[dir] = snapshot
55
+ end
56
+ end
57
+
58
+ def started?
59
+ @started
60
+ end
61
+
62
+ def start
63
+ configure
64
+
65
+ if started?
66
+ _log(:warn, 'Adapter already started!')
67
+ return
68
+ end
69
+
70
+ @started = true
71
+
72
+ calling_stack = caller.dup
73
+ Listen::Internals::ThreadPool.add do
74
+ begin
75
+ @snapshots.values.each do |snapshot|
76
+ _timed('Record.build()') { snapshot.record.build }
77
+ end
78
+ _run
79
+ rescue
80
+ msg = 'run() in thread failed: %s:\n'\
81
+ ' %s\n\ncalled from:\n %s'
82
+ _log_exception(msg, calling_stack)
83
+ raise # for unit tests mostly
84
+ end
85
+ end
86
+ end
87
+
88
+ def stop
89
+ _stop
90
+ end
91
+
92
+ def self.usable?
93
+ const_get('OS_REGEXP') =~ RbConfig::CONFIG['target_os']
94
+ end
95
+
96
+ private
97
+
98
+ def _stop
99
+ end
100
+
101
+ def _timed(title)
102
+ start = Time.now.to_f
103
+ yield
104
+ diff = Time.now.to_f - start
105
+ Listen::Logger.info format('%s: %.05f seconds', title, diff)
106
+ rescue
107
+ Listen::Logger.warn "#{title} crashed: #{$ERROR_INFO.inspect}"
108
+ raise
109
+ end
110
+
111
+ # TODO: allow backend adapters to pass specific invalidation objects
112
+ # e.g. Darwin -> DirRescan, INotify -> MoveScan, etc.
113
+ def _queue_change(type, dir, rel_path, options)
114
+ @snapshots[dir].invalidate(type, rel_path, options)
115
+ end
116
+
117
+ def _log(*args, &block)
118
+ self.class.send(:_log, *args, &block)
119
+ end
120
+
121
+ def _log_exception(msg, caller_stack)
122
+ formatted = format(
123
+ msg,
124
+ $ERROR_INFO,
125
+ $ERROR_POSITION * "\n",
126
+ caller_stack * "\n"
127
+ )
128
+
129
+ _log(:error, formatted)
130
+ end
131
+
132
+ def self._log(*args, &block)
133
+ Listen::Logger.send(*args, &block)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,106 @@
1
+ # Listener implementation for BSD's `kqueue`.
2
+ # @see http://www.freebsd.org/cgi/man.cgi?query=kqueue
3
+ # @see https://github.com/mat813/rb-kqueue/blob/master/lib/rb-kqueue/queue.rb
4
+ #
5
+ module Listen
6
+ module Adapter
7
+ class BSD < Base
8
+ OS_REGEXP = /bsd|dragonfly/i
9
+
10
+ DEFAULTS = {
11
+ events: [
12
+ :delete,
13
+ :write,
14
+ :extend,
15
+ :attrib,
16
+ :rename
17
+ # :link, :revoke
18
+ ]
19
+ }
20
+
21
+ BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
22
+ Please add the following to your Gemfile to avoid polling for changes:
23
+ require 'rbconfig'
24
+ if RbConfig::CONFIG['target_os'] =~ /#{OS_REGEXP}/
25
+ gem 'rb-kqueue', '>= 0.2'
26
+ end
27
+ EOS
28
+
29
+ def self.usable?
30
+ return false unless super
31
+ require 'rb-kqueue'
32
+ require 'find'
33
+ true
34
+ rescue LoadError
35
+ Kernel.warn BUNDLER_DECLARE_GEM
36
+ false
37
+ end
38
+
39
+ private
40
+
41
+ def _configure(directory, &_callback)
42
+ @worker ||= KQueue::Queue.new
43
+ @callback = _callback
44
+ # use Record to make a snapshot of dir, so we
45
+ # can detect new files
46
+ _find(directory.to_s) { |path| _watch_file(path, @worker) }
47
+ end
48
+
49
+ def _run
50
+ @worker.run
51
+ end
52
+
53
+ def _process_event(dir, event)
54
+ full_path = _event_path(event)
55
+ if full_path.directory?
56
+ # Force dir content tracking to kick in, or we won't have
57
+ # names of added files
58
+ _queue_change(:dir, dir, '.', recursive: true)
59
+ elsif full_path.exist?
60
+ path = full_path.relative_path_from(dir)
61
+ _queue_change(:file, dir, path.to_s, change: _change(event.flags))
62
+ end
63
+
64
+ # If it is a directory, and it has a write flag, it means a
65
+ # file has been added so find out which and deal with it.
66
+ # No need to check for removed files, kqueue will forget them
67
+ # when the vfs does.
68
+ _watch_for_new_file(event) if full_path.directory?
69
+ end
70
+
71
+ def _change(event_flags)
72
+ { modified: [:attrib, :extend],
73
+ added: [:write],
74
+ removed: [:rename, :delete]
75
+ }.each do |change, flags|
76
+ return change unless (flags & event_flags).empty?
77
+ end
78
+ nil
79
+ end
80
+
81
+ def _event_path(event)
82
+ Pathname.new(event.watcher.path)
83
+ end
84
+
85
+ def _watch_for_new_file(event)
86
+ queue = event.watcher.queue
87
+ _find(_event_path(event).to_s) do |file_path|
88
+ unless queue.watchers.detect { |_, v| v.path == file_path.to_s }
89
+ _watch_file(file_path, queue)
90
+ end
91
+ end
92
+ end
93
+
94
+ def _watch_file(path, queue)
95
+ queue.watch_file(path, *options.events, &@callback)
96
+ rescue Errno::ENOENT => e
97
+ _log :warn, "kqueue: watch file failed: #{e.message}"
98
+ end
99
+
100
+ # Quick rubocop workaround
101
+ def _find(*paths, &block)
102
+ Find.send(:find, *paths, &block)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,26 @@
1
+ require 'pathname'
2
+
3
+ module Listen
4
+ module Adapter
5
+ class Config
6
+ attr_reader :directories
7
+ attr_reader :silencer
8
+ attr_reader :queue
9
+ attr_reader :adapter_options
10
+
11
+ def initialize(directories, queue, silencer, adapter_options)
12
+ # Default to current directory if no directories are supplied
13
+ directories = [Dir.pwd] if directories.to_a.empty?
14
+
15
+ # TODO: fix (flatten, array, compact?)
16
+ @directories = directories.map do |directory|
17
+ Pathname.new(directory.to_s).realpath
18
+ end
19
+
20
+ @silencer = silencer
21
+ @queue = queue
22
+ @adapter_options = adapter_options
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,71 @@
1
+ require 'thread'
2
+ require 'listen/internals/thread_pool'
3
+
4
+ module Listen
5
+ module Adapter
6
+ # Adapter implementation for Mac OS X `FSEvents`.
7
+ #
8
+ class Darwin < Base
9
+ OS_REGEXP = /darwin(1.+)?$/i
10
+
11
+ # The default delay between checking for changes.
12
+ DEFAULTS = { latency: 0.1 }
13
+
14
+ private
15
+
16
+ # NOTE: each directory gets a DIFFERENT callback!
17
+ def _configure(dir, &callback)
18
+ require 'rb-fsevent'
19
+
20
+ opts = { latency: options.latency }
21
+
22
+ @workers ||= ::Queue.new
23
+ @workers << FSEvent.new.tap do |worker|
24
+ _log :debug, "fsevent: watching: #{dir.to_s.inspect}"
25
+ worker.watch(dir.to_s, opts, &callback)
26
+ end
27
+ end
28
+
29
+ def _run
30
+ first = @workers.pop
31
+
32
+ # NOTE: _run is called within a thread, so run every other
33
+ # worker in it's own thread
34
+ _run_workers_in_background(_to_array(@workers))
35
+ _run_worker(first)
36
+ end
37
+
38
+ def _process_event(dir, event)
39
+ _log :debug, "fsevent: processing event: #{event.inspect}"
40
+ event.each do |path|
41
+ new_path = Pathname.new(path.sub(/\/$/, ''))
42
+ _log :debug, "fsevent: #{new_path}"
43
+ # TODO: does this preserve symlinks?
44
+ rel_path = new_path.relative_path_from(dir).to_s
45
+ _queue_change(:dir, dir, rel_path, recursive: true)
46
+ end
47
+ end
48
+
49
+ def _run_worker(worker)
50
+ _log :debug, "fsevent: running worker: #{worker.inspect}"
51
+ worker.run
52
+ rescue
53
+ _log_exception 'fsevent: running worker failed: %s:%s called from: %s', caller
54
+ end
55
+
56
+ def _run_workers_in_background(workers)
57
+ workers.each do |worker|
58
+ # NOTE: while passing local variables to the block below is not
59
+ # thread safe, using 'worker' from the enumerator above is ok
60
+ Listen::Internals::ThreadPool.add { _run_worker(worker) }
61
+ end
62
+ end
63
+
64
+ def _to_array(queue)
65
+ workers = []
66
+ workers << queue.pop until queue.empty?
67
+ workers
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,106 @@
1
+ module Listen
2
+ module Adapter
3
+ # @see https://github.com/nex3/rb-inotify
4
+ class Linux < Base
5
+ OS_REGEXP = /linux/i
6
+
7
+ DEFAULTS = {
8
+ events: [
9
+ :recursive,
10
+ :attrib,
11
+ :create,
12
+ :delete,
13
+ :move,
14
+ :close_write
15
+ ],
16
+ wait_for_delay: 0.1
17
+ }
18
+
19
+ private
20
+
21
+ WIKI_URL = 'https://github.com/guard/listen'\
22
+ '/wiki/Increasing-the-amount-of-inotify-watchers'
23
+
24
+ INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '')
25
+ FATAL: Listen error: unable to monitor directories for changes.
26
+ Visit #{WIKI_URL} for info on how to fix this.
27
+ EOS
28
+
29
+ def _configure(directory, &callback)
30
+ require 'rb-inotify'
31
+ @worker ||= ::INotify::Notifier.new
32
+ @worker.watch(directory.to_s, *options.events, &callback)
33
+ rescue Errno::ENOSPC
34
+ abort(INOTIFY_LIMIT_MESSAGE)
35
+ end
36
+
37
+ def _run
38
+ @worker.run
39
+ end
40
+
41
+ def _process_event(dir, event)
42
+ # NOTE: avoid using event.absolute_name since new API
43
+ # will need to have a custom recursion implemented
44
+ # to properly match events to configured directories
45
+ path = Pathname.new(event.watcher.path) + event.name
46
+ rel_path = path.relative_path_from(dir).to_s
47
+
48
+ _log(:debug) { "inotify: #{rel_path} (#{event.flags.inspect})" }
49
+
50
+ if /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT']
51
+ if (event.flags & [:moved_to, :moved_from]) || _dir_event?(event)
52
+ rel_path = path.dirname.relative_path_from(dir).to_s
53
+ _queue_change(:dir, dir, rel_path, {})
54
+ else
55
+ _queue_change(:dir, dir, rel_path, {})
56
+ end
57
+ return
58
+ end
59
+
60
+ return if _skip_event?(event)
61
+
62
+ cookie_params = event.cookie.zero? ? {} : { cookie: event.cookie }
63
+
64
+ # Note: don't pass options to force rescanning the directory, so we can
65
+ # detect moving/deleting a whole tree
66
+ if _dir_event?(event)
67
+ _queue_change(:dir, dir, rel_path, cookie_params)
68
+ return
69
+ end
70
+
71
+ params = cookie_params.merge(change: _change(event.flags))
72
+
73
+ _queue_change(:file, dir, rel_path, params)
74
+ end
75
+
76
+ def _skip_event?(event)
77
+ # Event on root directory
78
+ return true if event.name == ''
79
+ # INotify reports changes to files inside directories as events
80
+ # on the directories themselves too.
81
+ #
82
+ # @see http://linux.die.net/man/7/inotify
83
+ _dir_event?(event) && (event.flags & [:close, :modify]).any?
84
+ end
85
+
86
+ def _change(event_flags)
87
+ { modified: [:attrib, :close_write],
88
+ moved_to: [:moved_to],
89
+ moved_from: [:moved_from],
90
+ added: [:create],
91
+ removed: [:delete] }.each do |change, flags|
92
+ return change unless (flags & event_flags).empty?
93
+ end
94
+ nil
95
+ end
96
+
97
+ def _dir_event?(event)
98
+ event.flags.include?(:isdir)
99
+ end
100
+
101
+ def _stop
102
+ @worker.close
103
+ end
104
+ end
105
+ end
106
+ end