listen 2.10.1 → 3.0.0

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