listen 2.10.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ require 'listen/adapter'
2
+ require 'listen/adapter/base'
3
+ require 'listen/adapter/config'
4
+
5
+ # This class just aggregates configuration object to avoid Listener specs
6
+ # from exploding with huge test setup blocks
7
+ module Listen
8
+ class Backend
9
+ def initialize(directories, queue, silencer, config)
10
+ adapter_select_opts = config.adapter_select_options
11
+
12
+ adapter_class = Adapter.select(adapter_select_opts)
13
+
14
+ # Use default from adapter if possible
15
+ @min_delay_between_events = config.min_delay_between_events
16
+ @min_delay_between_events ||= adapter_class::DEFAULTS[:wait_for_delay]
17
+ @min_delay_between_events ||= 0.1
18
+
19
+ adapter_opts = config.adapter_instance_options(adapter_class)
20
+
21
+ aconfig = Adapter::Config.new(directories, queue, silencer, adapter_opts)
22
+ @adapter = adapter_class.new(aconfig)
23
+ end
24
+
25
+ def start
26
+ adapter.start
27
+ end
28
+
29
+ def stop
30
+ # TODO: does nothing
31
+ end
32
+
33
+ def min_delay_between_events
34
+ @min_delay_between_events
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :adapter
40
+ end
41
+ end
data/lib/listen/change.rb CHANGED
@@ -2,58 +2,77 @@ require 'listen/file'
2
2
  require 'listen/directory'
3
3
 
4
4
  module Listen
5
+ # TODO: rename to Snapshot
5
6
  class Change
6
- include Celluloid
7
+ # TODO: test this class for coverage
8
+ class Config
9
+ def initialize(queue, silencer)
10
+ @queue = queue
11
+ @silencer = silencer
12
+ end
13
+
14
+ def silenced?(path, type)
15
+ @silencer.silenced?(Pathname(path), type)
16
+ end
17
+
18
+ def queue(*args)
19
+ @queue << args
20
+ end
21
+ end
7
22
 
8
- attr_accessor :listener
23
+ attr_reader :record
9
24
 
10
- def initialize(listener)
11
- @listener = listener
25
+ def initialize(config, record)
26
+ @config = config
27
+ @record = record
12
28
  end
13
29
 
14
- def change(type, watched_dir, rel_path, options = {})
30
+ # Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
31
+ def invalidate(type, rel_path, options)
32
+ watched_dir = Pathname.new(record.root)
33
+
15
34
  change = options[:change]
16
35
  cookie = options[:cookie]
17
36
 
18
- if !cookie && listener.silencer.silenced?(Pathname(rel_path), type)
19
- _log :debug, "(silenced): #{rel_path.inspect}"
37
+ if !cookie && config.silenced?(rel_path, type)
38
+ Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
20
39
  return
21
40
  end
22
41
 
23
42
  path = watched_dir + rel_path
24
43
 
25
- log_details = options[:silence] && 'recording' || change || 'unknown'
26
- _log :debug, "#{log_details}: #{type}:#{path} (#{options.inspect})"
44
+ Listen::Logger.debug do
45
+ log_details = options[:silence] && 'recording' || change || 'unknown'
46
+ "#{log_details}: #{type}:#{path} (#{options.inspect})"
47
+ end
27
48
 
28
49
  if change
29
- # TODO: move this to Listener to avoid Celluloid overhead
30
- # from caller
31
50
  options = cookie ? { cookie: cookie } : {}
32
- listener.queue(type, change, watched_dir, rel_path, options)
51
+ config.queue(type, change, watched_dir, rel_path, options)
33
52
  else
34
- return unless (record = listener.sync(:record))
35
-
36
53
  if type == :dir
37
- return unless (change_queue = listener.async(:change_pool))
38
- Directory.scan(change_queue, record, watched_dir, rel_path, options)
54
+ # NOTE: POSSIBLE RECURSION
55
+ # TODO: fix - use a queue instead
56
+ Directory.scan(self, rel_path, options)
39
57
  else
40
- change = File.change(record, watched_dir, rel_path)
58
+ change = File.change(record, rel_path)
41
59
  return if !change || options[:silence]
42
- listener.queue(:file, change, watched_dir, rel_path)
60
+ config.queue(:file, change, watched_dir, rel_path)
43
61
  end
44
62
  end
45
- rescue Celluloid::Task::TerminatedError
46
- _log :debug, "Change#change was terminated: #{$ERROR_INFO.inspect}"
47
- rescue RuntimeError
48
- _log :error, format('Change#change crashed %s:%s', $ERROR_INFO.inspect,
49
- $ERROR_POSITION * "\n")
63
+ rescue RuntimeError => ex
64
+ msg = format(
65
+ '%s#%s crashed %s:%s',
66
+ self.class,
67
+ __method__,
68
+ exinspect,
69
+ ex.backtrace * "\n")
70
+ Listen::Logger.error(msg)
50
71
  raise
51
72
  end
52
73
 
53
74
  private
54
75
 
55
- def _log(type, message)
56
- Celluloid::Logger.send(type, message)
57
- end
76
+ attr_reader :config
58
77
  end
59
78
  end
data/lib/listen/cli.rb CHANGED
@@ -14,12 +14,6 @@ module Listen
14
14
  aliases: '-v',
15
15
  banner: 'Verbose'
16
16
 
17
- class_option :forward,
18
- type: :string,
19
- default: '127.0.0.1:4000',
20
- aliases: '-f',
21
- banner: 'The address to forward filesystem events'
22
-
23
17
  class_option :directory,
24
18
  type: :array,
25
19
  default: '.',
@@ -41,14 +35,13 @@ module Listen
41
35
  attr_reader :logger
42
36
  def initialize(options)
43
37
  @options = options
44
- @logger = Logger.new(STDOUT)
45
- @logger.level = Logger::INFO
38
+ @logger = ::Logger.new(STDOUT)
39
+ @logger.level = ::Logger::INFO
46
40
  @logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
47
41
  end
48
42
 
49
43
  def start
50
44
  logger.info 'Starting listen...'
51
- address = @options[:forward]
52
45
  directory = @options[:directory]
53
46
  relative = @options[:relative]
54
47
  callback = proc do |modified, added, removed|
@@ -61,13 +54,12 @@ module Listen
61
54
 
62
55
  listener = Listen.to(
63
56
  directory,
64
- forward_to: address,
65
57
  relative: relative,
66
58
  &callback)
67
59
 
68
60
  listener.start
69
61
 
70
- sleep 0.5 while listener.listen?
62
+ sleep 0.5 while listener.processing?
71
63
  end
72
64
  end
73
65
  end
@@ -1,78 +1,80 @@
1
1
  require 'set'
2
2
 
3
3
  module Listen
4
+ # TODO: refactor (turn it into a normal object, cache the stat, etc)
4
5
  class Directory
5
- def self.scan(queue, sync_record, dir, rel_path, options)
6
- return unless (record = sync_record.async)
6
+ def self.scan(snapshot, rel_path, options)
7
+ record = snapshot.record
8
+ dir = Pathname.new(record.root)
9
+ previous = record.dir_entries(rel_path)
7
10
 
8
- previous = sync_record.dir_entries(dir, rel_path)
9
-
10
- record.add_dir(dir, rel_path)
11
+ record.add_dir(rel_path)
11
12
 
12
13
  # TODO: use children(with_directory: false)
13
14
  path = dir + rel_path
14
15
  current = Set.new(path.children)
15
16
 
16
- _log(:debug) do
17
+ Listen::Logger.debug do
17
18
  format('%s: %s(%s): %s -> %s',
18
19
  (options[:silence] ? 'Recording' : 'Scanning'),
19
20
  rel_path, options.inspect, previous.inspect, current.inspect)
20
21
  end
21
22
 
22
23
  current.each do |full_path|
23
- type = full_path.directory? ? :dir : :file
24
+ type = detect_type(full_path)
24
25
  item_rel_path = full_path.relative_path_from(dir).to_s
25
- _change(queue, type, dir, item_rel_path, options)
26
+ _change(snapshot, type, item_rel_path, options)
26
27
  end
27
28
 
28
29
  # TODO: this is not tested properly
29
30
  previous = previous.reject { |entry, _| current.include? path + entry }
30
31
 
31
- _async_changes(dir, rel_path, queue, previous, options)
32
+ _async_changes(snapshot, Pathname.new(rel_path), previous, options)
32
33
 
33
34
  rescue Errno::ENOENT, Errno::EHOSTDOWN
34
- record.unset_path(dir, rel_path)
35
- _async_changes(dir, rel_path, queue, previous, options)
35
+ record.unset_path(rel_path)
36
+ _async_changes(snapshot, Pathname.new(rel_path), previous, options)
36
37
 
37
38
  rescue Errno::ENOTDIR
38
39
  # TODO: path not tested
39
- record.unset_path(dir, rel_path)
40
- _async_changes(dir, path, queue, previous, options)
41
- _change(queue, :file, dir, rel_path, options)
40
+ record.unset_path(rel_path)
41
+ _async_changes(snapshot, path, previous, options)
42
+ _change(snapshot, :file, rel_path, options)
42
43
  rescue
43
- _log(:warn) do
44
+ Listen::Logger.warn do
44
45
  format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
45
46
  end
46
47
  raise
47
48
  end
48
49
 
49
- def self._async_changes(dir, path, queue, previous, options)
50
+ def self._async_changes(snapshot, path, previous, options)
51
+ fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children)
50
52
  previous.each do |entry, data|
51
53
  # TODO: this is a hack with insufficient testing
52
54
  type = data.key?(:mtime) ? :file : :dir
53
- _change(queue, type, dir, (Pathname(path) + entry).to_s, options)
55
+ rel_path_s = (path + entry).to_s
56
+ _change(snapshot, type, rel_path_s, options)
54
57
  end
55
58
  end
56
59
 
57
- def self._change(queue, type, dir, path, options)
58
- return queue.change(type, dir, path, options) if type == :dir
60
+ def self._change(snapshot, type, path, options)
61
+ return snapshot.invalidate(type, path, options) if type == :dir
59
62
 
60
63
  # Minor param cleanup for tests
61
64
  # TODO: use a dedicated Event class
62
65
  opts = options.dup
63
66
  opts.delete(:recursive)
64
- if opts.empty?
65
- queue.change(type, dir, path)
66
- else
67
- queue.change(type, dir, path, opts)
68
- end
67
+ snapshot.invalidate(type, path, opts)
69
68
  end
70
69
 
71
- def self._log(type, &block)
72
- return unless Celluloid.logger
73
- Celluloid.logger.send(type) do
74
- block.call
75
- end
70
+ def self.detect_type(full_path)
71
+ # TODO: should probably check record first
72
+ stat = ::File.lstat(full_path.to_s)
73
+ stat.directory? ? :dir : :file
74
+ rescue Errno::ENOENT
75
+ # TODO: ok, it should really check the record here
76
+ # report as dir for scanning
77
+ :dir
76
78
  end
77
79
  end
78
80
  end
@@ -0,0 +1,59 @@
1
+ module Listen
2
+ module Event
3
+ class Config
4
+ def initialize(
5
+ listener,
6
+ event_queue,
7
+ queue_optimizer,
8
+ wait_for_delay,
9
+ &block)
10
+
11
+ @listener = listener
12
+ @event_queue = event_queue
13
+ @queue_optimizer = queue_optimizer
14
+ @min_delay_between_events = wait_for_delay
15
+ @block = block
16
+ end
17
+
18
+ def sleep(*args)
19
+ Kernel.sleep(*args)
20
+ end
21
+
22
+ def call(*args)
23
+ @block.call(*args) if @block
24
+ end
25
+
26
+ def timestamp
27
+ Time.now.to_f
28
+ end
29
+
30
+ def event_queue
31
+ @event_queue
32
+ end
33
+
34
+ def callable?
35
+ @block
36
+ end
37
+
38
+ def optimize_changes(changes)
39
+ @queue_optimizer.smoosh_changes(changes)
40
+ end
41
+
42
+ def min_delay_between_events
43
+ @min_delay_between_events
44
+ end
45
+
46
+ def stopped?
47
+ listener.state == :stopped
48
+ end
49
+
50
+ def paused?
51
+ listener.state == :paused
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :listener
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,113 @@
1
+ require 'listen/event/processor'
2
+
3
+ module Listen
4
+ module Event
5
+ class Loop
6
+ class Error < RuntimeError
7
+ class NotStarted < Error
8
+ end
9
+ end
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @wait_thread = nil
14
+ @state = :paused
15
+ @reasons = Thread::Queue.new
16
+ end
17
+
18
+ def wakeup_on_event
19
+ return if stopped?
20
+ return unless processing?
21
+ return unless wait_thread.alive?
22
+ _wakeup(:event)
23
+ end
24
+
25
+ def paused?
26
+ wait_thread && state == :paused
27
+ end
28
+
29
+ def processing?
30
+ return false if stopped?
31
+ return false if paused?
32
+ state == :processing
33
+ end
34
+
35
+ def setup
36
+ # TODO: use a Fiber instead?
37
+ q = Thread::Queue.new
38
+ @wait_thread = Internals::ThreadPool.add do
39
+ _wait_for_changes(q, config)
40
+ end
41
+
42
+ Listen::Logger.debug('Waiting for processing to start...')
43
+ Timeout.timeout(5) { q.pop }
44
+ end
45
+
46
+ def resume
47
+ fail Error::NotStarted if stopped?
48
+ return unless wait_thread
49
+ _wakeup(:resume)
50
+ end
51
+
52
+ def pause
53
+ fail NotImplementedError
54
+ end
55
+
56
+ def teardown
57
+ return unless wait_thread
58
+ if wait_thread.alive?
59
+ _wakeup(:teardown)
60
+ wait_thread.join
61
+ end
62
+ @wait_thread = nil
63
+ end
64
+
65
+ def stopped?
66
+ !wait_thread
67
+ end
68
+
69
+ private
70
+
71
+ attr_reader :config
72
+ attr_reader :wait_thread
73
+
74
+ attr_accessor :state
75
+
76
+ def _wait_for_changes(ready_queue, config)
77
+ processor = Event::Processor.new(config, @reasons)
78
+
79
+ _wait_until_resumed(ready_queue)
80
+ processor.loop_for(config.min_delay_between_events)
81
+ rescue StandardError => ex
82
+ _nice_error(ex)
83
+ end
84
+
85
+ def _sleep(*args)
86
+ Kernel.sleep(*args)
87
+ end
88
+
89
+ def _wait_until_resumed(ready_queue)
90
+ self.state = :paused
91
+ ready_queue << :ready
92
+ sleep
93
+ self.state = :processing
94
+ end
95
+
96
+ def _nice_error(ex)
97
+ indent = "\n -- "
98
+ msg = format(
99
+ 'exception while processing events: %s Backtrace:%s%s',
100
+ ex,
101
+ indent,
102
+ ex.backtrace * indent
103
+ )
104
+ Listen::Logger.error(msg)
105
+ end
106
+
107
+ def _wakeup(reason)
108
+ @reasons << reason
109
+ wait_thread.wakeup
110
+ end
111
+ end
112
+ end
113
+ end