listen 3.0.8 → 3.7.1

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.
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
4
+ require 'listen/error'
2
5
 
3
6
  module Listen
4
7
  # @private api
5
8
  class Record
6
9
  class SymlinkDetector
7
- WIKI = 'https://github.com/guard/listen/wiki/Duplicate-directory-errors'
10
+ README_URL = 'https://github.com/guard/listen/blob/master/README.md'
8
11
 
9
12
  SYMLINK_LOOP_ERROR = <<-EOS
10
13
  ** ERROR: directory is already being watched! **
@@ -13,11 +16,10 @@ module Listen
13
16
 
14
17
  is already being watched through: %s
15
18
 
16
- MORE INFO: #{WIKI}
19
+ MORE INFO: #{README_URL}
17
20
  EOS
18
21
 
19
- class Error < RuntimeError
20
- end
22
+ Error = ::Listen::Error # for backward compatibility
21
23
 
22
24
  def initialize
23
25
  @real_dirs = Set.new
@@ -25,14 +27,14 @@ module Listen
25
27
 
26
28
  def verify_unwatched!(entry)
27
29
  real_path = entry.real_path
28
- @real_dirs.add?(real_path) || _fail(entry.sys_path, real_path)
30
+ @real_dirs.add?(real_path) or _fail(entry.sys_path, real_path)
29
31
  end
30
32
 
31
33
  private
32
34
 
33
35
  def _fail(symlinked, real_path)
34
- STDERR.puts format(SYMLINK_LOOP_ERROR, symlinked, real_path)
35
- fail Error, 'Failed due to looped symlinks'
36
+ warn(format(SYMLINK_LOOP_ERROR, symlinked, real_path))
37
+ raise ::Listen::Error::SymlinkLoop, 'Failed due to looped symlinks'
36
38
  end
37
39
  end
38
40
  end
data/lib/listen/record.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
  require 'listen/record/entry'
3
5
  require 'listen/record/symlink_detector'
@@ -8,14 +10,17 @@ module Listen
8
10
  # TODO: deprecate
9
11
 
10
12
  attr_reader :root
11
- def initialize(directory)
12
- @tree = _auto_hash
13
+
14
+ def initialize(directory, silencer)
15
+ reset_tree
13
16
  @root = directory.to_s
17
+ @silencer = silencer
14
18
  end
15
19
 
16
20
  def add_dir(rel_path)
17
- return if [nil, '', '.'].include? rel_path
18
- @tree[rel_path] ||= {}
21
+ if !empty_dirname?(rel_path.to_s)
22
+ @tree[rel_path.to_s]
23
+ end
19
24
  end
20
25
 
21
26
  def update_file(rel_path, data)
@@ -30,35 +35,32 @@ module Listen
30
35
 
31
36
  def file_data(rel_path)
32
37
  dirname, basename = Pathname(rel_path).split.map(&:to_s)
33
- if [nil, '', '.'].include? dirname
34
- tree[basename] ||= {}
35
- tree[basename].dup
38
+ if empty_dirname?(dirname)
39
+ @tree[basename].dup
36
40
  else
37
- tree[dirname] ||= {}
38
- tree[dirname][basename] ||= {}
39
- tree[dirname][basename].dup
41
+ @tree[dirname][basename] ||= {}
42
+ @tree[dirname][basename].dup
40
43
  end
41
44
  end
42
45
 
43
46
  def dir_entries(rel_path)
44
- subtree =
45
- if [nil, '', '.'].include? rel_path.to_s
46
- tree
47
- else
48
- tree[rel_path.to_s] ||= _auto_hash
49
- tree[rel_path.to_s]
50
- end
47
+ rel_path_s = rel_path.to_s
48
+ subtree = if empty_dirname?(rel_path_s)
49
+ @tree
50
+ else
51
+ @tree[rel_path_s]
52
+ end
51
53
 
52
- result = {}
53
- subtree.each do |key, values|
54
- # only get data for file entries
55
- result[key] = values.key?(:mtime) ? values : {}
54
+ subtree.each_with_object({}) do |(key, values), result|
55
+ # only return data for file entries inside the dir (which will each be sub-hashes)
56
+ if values.is_a?(Hash)
57
+ result[key] = values.has_key?(:mtime) ? values : {}
58
+ end
56
59
  end
57
- result
58
60
  end
59
61
 
60
62
  def build
61
- @tree = _auto_hash
63
+ reset_tree
62
64
  # TODO: test with a file name given
63
65
  # TODO: test other permissions
64
66
  # TODO: test with mixed encoding
@@ -70,37 +72,38 @@ module Listen
70
72
 
71
73
  private
72
74
 
73
- def _auto_hash
74
- Hash.new { |h, k| h[k] = Hash.new }
75
+ def empty_dirname?(dirname)
76
+ dirname == '.' || dirname == ''
75
77
  end
76
78
 
77
- def tree
78
- @tree
79
+ def reset_tree
80
+ @tree = Hash.new { |h, k| h[k] = {} }
79
81
  end
80
82
 
81
83
  def _fast_update_file(dirname, basename, data)
82
- if [nil, '', '.'].include? dirname
83
- tree[basename] = (tree[basename] || {}).merge(data)
84
+ if empty_dirname?(dirname.to_s)
85
+ @tree[basename] = @tree[basename].merge(data)
84
86
  else
85
- tree[dirname] ||= {}
86
- tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
87
+ @tree[dirname][basename] = (@tree[dirname][basename] || {}).merge(data)
87
88
  end
88
89
  end
89
90
 
90
91
  def _fast_unset_path(dirname, basename)
91
92
  # this may need to be reworked to properly remove
92
93
  # entries from a tree, without adding non-existing dirs to the record
93
- if [nil, '', '.'].include? dirname
94
- return unless tree.key?(basename)
95
- tree.delete(basename)
96
- else
97
- return unless tree.key?(dirname)
98
- tree[dirname].delete(basename)
94
+ if empty_dirname?(dirname.to_s)
95
+ if @tree.key?(basename)
96
+ @tree.delete(basename)
97
+ end
98
+ elsif @tree.key?(dirname)
99
+ @tree[dirname].delete(basename)
99
100
  end
100
101
  end
101
102
 
102
103
  def _fast_build_dir(remaining, symlink_detector)
103
104
  entry = remaining.pop
105
+ return if @silencer.silenced?(entry.record_dir_key, :dir)
106
+
104
107
  children = entry.children # NOTE: children() implicitly tests if dir
105
108
  symlink_detector.verify_unwatched!(entry)
106
109
  children.each { |child| remaining << child }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Silencer
3
5
  class Controller
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Silencer
3
5
  # The default list of directories that get ignored.
4
- DEFAULT_IGNORED_DIRECTORIES = %r{^(?:
5
- \.git
6
+ DEFAULT_IGNORED_FILES = %r{\A(?:
7
+ \.git
6
8
  | \.svn
7
9
  | \.hg
8
10
  | \.rbx
@@ -11,11 +13,15 @@ module Listen
11
13
  | vendor/bundle
12
14
  | log
13
15
  | tmp
14
- |vendor/ruby
15
- )(/|$)}x
16
+ | vendor/ruby
17
+
18
+ # emacs temp files
19
+ | \#.+\#
20
+ | \.\#.+
21
+ )(/|\z)}x.freeze
16
22
 
17
23
  # The default list of files that get ignored.
18
- DEFAULT_IGNORED_EXTENSIONS = /(?:
24
+ DEFAULT_IGNORED_EXTENSIONS = %r{(?:
19
25
  # Kate's tmp\/swp files
20
26
  \..*\d+\.new
21
27
  | \.kate-swp
@@ -33,7 +39,7 @@ module Listen
33
39
  | ^4913
34
40
 
35
41
  # Sed temporary files - but without actual words, like 'sedatives'
36
- | (?:^
42
+ | (?:\A
37
43
  sed
38
44
 
39
45
  (?:
@@ -46,45 +52,48 @@ module Listen
46
52
  )
47
53
  )
48
54
 
55
+ # Mutagen sync temporary files
56
+ | \.mutagen-temporary.*
57
+
49
58
  # other files
50
59
  | \.DS_Store
51
60
  | \.tmp
52
61
  | ~
53
- )$/x
62
+ )\z}x.freeze
54
63
 
64
+ # TODO: deprecate these mutators; use attr_reader instead
55
65
  attr_accessor :only_patterns, :ignore_patterns
56
66
 
57
- def initialize
58
- configure({})
67
+ def initialize(**options)
68
+ configure(options)
59
69
  end
60
70
 
71
+ # TODO: deprecate this mutator
61
72
  def configure(options)
62
73
  @only_patterns = options[:only] ? Array(options[:only]) : nil
63
74
  @ignore_patterns = _init_ignores(options[:ignore], options[:ignore!])
64
75
  end
65
76
 
66
- # Note: relative_path is temporarily expected to be a relative Pathname to
67
- # make refactoring easier (ideally, it would take a string)
68
-
69
- # TODO: switch type and path places - and verify
70
77
  def silenced?(relative_path, type)
71
- path = relative_path.to_s
72
-
73
- if only_patterns && type == :file
74
- return true unless only_patterns.any? { |pattern| path =~ pattern }
75
- end
78
+ path = relative_path.to_s # in case it is a Pathname
76
79
 
77
- ignore_patterns.any? { |pattern| path =~ pattern }
80
+ _ignore?(path) || (only_patterns && type == :file && !_only?(path))
78
81
  end
79
82
 
80
83
  private
81
84
 
82
- attr_reader :options
85
+ def _ignore?(path)
86
+ ignore_patterns.any? { |pattern| path =~ pattern }
87
+ end
88
+
89
+ def _only?(path)
90
+ only_patterns.any? { |pattern| path =~ pattern }
91
+ end
83
92
 
84
93
  def _init_ignores(ignores, overrides)
85
94
  patterns = []
86
95
  unless overrides
87
- patterns << DEFAULT_IGNORED_DIRECTORIES
96
+ patterns << DEFAULT_IGNORED_FILES
88
97
  patterns << DEFAULT_IGNORED_EXTENSIONS
89
98
  end
90
99
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ require_relative 'logger'
6
+
7
+ module Listen
8
+ module Thread
9
+ class << self
10
+ # Creates a new thread with the given name.
11
+ # Any exceptions raised by the thread will be logged with the thread name and complete backtrace.
12
+ # rubocop:disable Style/MultilineBlockChain
13
+ def new(name, &block)
14
+ thread_name = "listen-#{name}"
15
+ caller_stack = caller
16
+
17
+ ::Thread.new do
18
+ rescue_and_log(thread_name, caller_stack: caller_stack, &block)
19
+ end.tap do |thread|
20
+ thread.name = thread_name
21
+ end
22
+ end
23
+ # rubocop:enable Style/MultilineBlockChain
24
+
25
+ def rescue_and_log(method_name, *args, caller_stack: nil)
26
+ yield(*args)
27
+ rescue => exception
28
+ _log_exception(exception, method_name, caller_stack: caller_stack)
29
+ end
30
+
31
+ private
32
+
33
+ def _log_exception(exception, thread_name, caller_stack: nil)
34
+ complete_backtrace = if caller_stack
35
+ [*exception.backtrace, "--- Thread.new ---", *caller_stack]
36
+ else
37
+ exception.backtrace
38
+ end
39
+ message = "Exception rescued in #{thread_name}:\n#{_exception_with_causes(exception)}\n#{complete_backtrace * "\n"}"
40
+ Listen.logger.error(message)
41
+ end
42
+
43
+ def _exception_with_causes(exception)
44
+ result = +"#{exception.class}: #{exception}"
45
+ if exception.cause
46
+ result << "\n"
47
+ result << "--- Caused by: ---\n"
48
+ result << _exception_with_causes(exception.cause)
49
+ end
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
- VERSION = '3.0.8'
4
+ VERSION = '3.7.1'
3
5
  end
data/lib/listen.rb CHANGED
@@ -1,23 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
4
+ require 'weakref'
2
5
  require 'listen/logger'
3
6
  require 'listen/listener'
4
7
 
5
- require 'listen/internals/thread_pool'
6
-
7
- # Always set up logging by default first time file is required
8
- #
9
- # NOTE: If you need to clear the logger completely, do so *after*
10
- # requiring this file. If you need to set a custom logger,
11
- # require the listen/logger file and set the logger before requiring
12
- # this file.
13
- Listen.setup_default_logger_if_unset
14
-
15
8
  # Won't print anything by default because of level - unless you've set
16
9
  # LISTEN_GEM_DEBUGGING or provided your own logger with a high enough level
17
- Listen::Logger.info "Listen loglevel set to: #{Listen.logger.level}"
18
- Listen::Logger.info "Listen version: #{Listen::VERSION}"
10
+ Listen.logger.info "Listen loglevel set to: #{Listen.logger.level}"
11
+ Listen.logger.info "Listen version: #{Listen::VERSION}"
19
12
 
20
13
  module Listen
14
+ @listeners = Queue.new
15
+
21
16
  class << self
22
17
  # Listens to file system modifications on a either single directory or
23
18
  # multiple directories.
@@ -32,24 +27,21 @@ module Listen
32
27
  # @return [Listen::Listener] the listener
33
28
  #
34
29
  def to(*args, &block)
35
- @listeners ||= []
36
30
  Listener.new(*args, &block).tap do |listener|
37
- @listeners << listener
31
+ @listeners.enq(WeakRef.new(listener))
38
32
  end
39
33
  end
40
34
 
41
35
  # This is used by the `listen` binary to handle Ctrl-C
42
36
  #
43
37
  def stop
44
- Internals::ThreadPool.stop
45
- @listeners ||= []
46
-
47
- # TODO: should use a mutex for this
48
- @listeners.each do |listener|
49
- # call stop to halt the main loop
50
- listener.stop
38
+ while (listener = @listeners.deq(true))
39
+ begin
40
+ listener.stop
41
+ rescue WeakRef::RefError
42
+ end
51
43
  end
52
- @listeners = nil
44
+ rescue ThreadError
53
45
  end
54
46
  end
55
47
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listen
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.8
4
+ version: 3.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thibaud Guillaume-Gentil
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-18 00:00:00.000000000 Z
11
+ date: 2022-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb-fsevent
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.9'
19
+ version: '0.10'
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 0.9.4
22
+ version: 0.10.3
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - "~>"
28
28
  - !ruby/object:Gem::Version
29
- version: '0.9'
29
+ version: '0.10'
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.9.4
32
+ version: 0.10.3
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rb-inotify
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: '0.9'
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: 0.9.7
42
+ version: 0.9.10
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,21 +49,7 @@ dependencies:
49
49
  version: '0.9'
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: 0.9.7
53
- - !ruby/object:Gem::Dependency
54
- name: bundler
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: 1.3.5
60
- type: :development
61
- prerelease: false
62
- version_requirements: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - ">="
65
- - !ruby/object:Gem::Version
66
- version: 1.3.5
52
+ version: 0.9.10
67
53
  description: The Listen gem listens to file modifications and notifies you about the
68
54
  changes. Works everywhere!
69
55
  email: thibaud@thibaud.gg
@@ -90,16 +76,17 @@ files:
90
76
  - lib/listen/change.rb
91
77
  - lib/listen/cli.rb
92
78
  - lib/listen/directory.rb
79
+ - lib/listen/error.rb
93
80
  - lib/listen/event/config.rb
94
81
  - lib/listen/event/loop.rb
95
82
  - lib/listen/event/processor.rb
96
83
  - lib/listen/event/queue.rb
97
84
  - lib/listen/file.rb
98
85
  - lib/listen/fsm.rb
99
- - lib/listen/internals/thread_pool.rb
100
86
  - lib/listen/listener.rb
101
87
  - lib/listen/listener/config.rb
102
88
  - lib/listen/logger.rb
89
+ - lib/listen/monotonic_time.rb
103
90
  - lib/listen/options.rb
104
91
  - lib/listen/queue_optimizer.rb
105
92
  - lib/listen/record.rb
@@ -107,11 +94,18 @@ files:
107
94
  - lib/listen/record/symlink_detector.rb
108
95
  - lib/listen/silencer.rb
109
96
  - lib/listen/silencer/controller.rb
97
+ - lib/listen/thread.rb
110
98
  - lib/listen/version.rb
111
99
  homepage: https://github.com/guard/listen
112
100
  licenses:
113
101
  - MIT
114
- metadata: {}
102
+ metadata:
103
+ allowed_push_host: https://rubygems.org
104
+ bug_tracker_uri: https://github.com/guard/listen/issues
105
+ changelog_uri: https://github.com/guard/listen/releases
106
+ documentation_uri: https://www.rubydoc.info/gems/listen/3.7.1
107
+ homepage_uri: https://github.com/guard/listen
108
+ source_code_uri: https://github.com/guard/listen/tree/v3.7.1
115
109
  post_install_message:
116
110
  rdoc_options: []
117
111
  require_paths:
@@ -120,17 +114,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
114
  requirements:
121
115
  - - ">="
122
116
  - !ruby/object:Gem::Version
123
- version: 1.9.3
117
+ version: 2.4.0
124
118
  required_rubygems_version: !ruby/object:Gem::Requirement
125
119
  requirements:
126
120
  - - ">="
127
121
  - !ruby/object:Gem::Version
128
122
  version: '0'
129
123
  requirements: []
130
- rubyforge_project:
131
- rubygems_version: 2.5.1
124
+ rubygems_version: 3.0.1
132
125
  signing_key:
133
126
  specification_version: 4
134
127
  summary: Listen to file modifications
135
128
  test_files: []
136
- has_rdoc:
@@ -1,29 +0,0 @@
1
- module Listen
2
- # @private api
3
- module Internals
4
- module ThreadPool
5
- def self.add(&block)
6
- Thread.new { block.call }.tap do |th|
7
- (@threads ||= Queue.new) << th
8
- end
9
- end
10
-
11
- def self.stop
12
- return unless @threads ||= nil
13
- return if @threads.empty? # return to avoid using possibly stubbed Queue
14
-
15
- killed = Queue.new
16
- # You can't kill a read on a descriptor in JRuby, so let's just
17
- # ignore running threads (listen rb-inotify waiting for disk activity
18
- # before closing) pray threads die faster than they are created...
19
- limit = RUBY_ENGINE == 'jruby' ? [1] : []
20
-
21
- killed << @threads.pop.kill until @threads.empty?
22
- until killed.empty?
23
- th = killed.pop
24
- th.join(*limit) unless th[:listen_blocking_read_thread]
25
- end
26
- end
27
- end
28
- end
29
- end