listen 3.0.2 → 3.8.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.
@@ -1,23 +1,25 @@
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! **
11
14
 
12
15
  Directory: %s
13
16
 
14
- is already begin watched through: %s
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
1
4
  require 'listen/record/entry'
2
5
  require 'listen/record/symlink_detector'
3
6
 
@@ -7,14 +10,17 @@ module Listen
7
10
  # TODO: deprecate
8
11
 
9
12
  attr_reader :root
10
- def initialize(directory)
11
- @tree = _auto_hash
13
+
14
+ def initialize(directory, silencer)
15
+ reset_tree
12
16
  @root = directory.to_s
17
+ @silencer = silencer
13
18
  end
14
19
 
15
20
  def add_dir(rel_path)
16
- return if [nil, '', '.'].include? rel_path
17
- @tree[rel_path] ||= {}
21
+ if !empty_dirname?(rel_path.to_s)
22
+ @tree[rel_path.to_s]
23
+ end
18
24
  end
19
25
 
20
26
  def update_file(rel_path, data)
@@ -29,77 +35,75 @@ module Listen
29
35
 
30
36
  def file_data(rel_path)
31
37
  dirname, basename = Pathname(rel_path).split.map(&:to_s)
32
- if [nil, '', '.'].include? dirname
33
- tree[basename] ||= {}
34
- tree[basename].dup
38
+ if empty_dirname?(dirname)
39
+ @tree[basename].dup
35
40
  else
36
- tree[dirname] ||= {}
37
- tree[dirname][basename] ||= {}
38
- tree[dirname][basename].dup
41
+ @tree[dirname][basename] ||= {}
42
+ @tree[dirname][basename].dup
39
43
  end
40
44
  end
41
45
 
42
46
  def dir_entries(rel_path)
43
- subtree =
44
- if [nil, '', '.'].include? rel_path.to_s
45
- tree
46
- else
47
- tree[rel_path.to_s] ||= _auto_hash
48
- tree[rel_path.to_s]
49
- 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
50
53
 
51
- result = {}
52
- subtree.each do |key, values|
53
- # only get data for file entries
54
- 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
55
59
  end
56
- result
57
60
  end
58
61
 
59
62
  def build
60
- @tree = _auto_hash
63
+ reset_tree
61
64
  # TODO: test with a file name given
62
65
  # TODO: test other permissions
63
66
  # TODO: test with mixed encoding
64
67
  symlink_detector = SymlinkDetector.new
65
- remaining = Queue.new
68
+ remaining = ::Queue.new
66
69
  remaining << Entry.new(root, nil, nil)
67
70
  _fast_build_dir(remaining, symlink_detector) until remaining.empty?
68
71
  end
69
72
 
70
73
  private
71
74
 
72
- def _auto_hash
73
- Hash.new { |h, k| h[k] = Hash.new }
75
+ def empty_dirname?(dirname)
76
+ dirname == '.' || dirname == ''
74
77
  end
75
78
 
76
- def tree
77
- @tree
79
+ def reset_tree
80
+ @tree = Hash.new { |h, k| h[k] = {} }
78
81
  end
79
82
 
80
83
  def _fast_update_file(dirname, basename, data)
81
- if [nil, '', '.'].include? dirname
82
- tree[basename] = (tree[basename] || {}).merge(data)
84
+ if empty_dirname?(dirname.to_s)
85
+ @tree[basename] = @tree[basename].merge(data)
83
86
  else
84
- tree[dirname] ||= {}
85
- tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
87
+ @tree[dirname][basename] = (@tree[dirname][basename] || {}).merge(data)
86
88
  end
87
89
  end
88
90
 
89
91
  def _fast_unset_path(dirname, basename)
90
92
  # this may need to be reworked to properly remove
91
93
  # entries from a tree, without adding non-existing dirs to the record
92
- if [nil, '', '.'].include? dirname
93
- return unless tree.key?(basename)
94
- tree.delete(basename)
95
- else
96
- return unless tree.key?(dirname)
97
- 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)
98
100
  end
99
101
  end
100
102
 
101
103
  def _fast_build_dir(remaining, symlink_detector)
102
104
  entry = remaining.pop
105
+ return if @silencer.silenced?(entry.record_dir_key, :dir)
106
+
103
107
  children = entry.children # NOTE: children() implicitly tests if dir
104
108
  symlink_detector.verify_unwatched!(entry)
105
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.2'
4
+ VERSION = '3.8.0'
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,57 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listen
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.8.0
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: 2015-07-08 00:00:00.000000000 Z
11
+ date: 2023-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb-fsevent
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.10'
17
20
  - - ">="
18
21
  - !ruby/object:Gem::Version
19
- version: 0.9.3
22
+ version: 0.10.3
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.10'
24
30
  - - ">="
25
31
  - !ruby/object:Gem::Version
26
- version: 0.9.3
32
+ version: 0.10.3
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rb-inotify
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - ">="
37
+ - - "~>"
32
38
  - !ruby/object:Gem::Version
33
39
  version: '0.9'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 0.9.10
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
37
46
  requirements:
38
- - - ">="
47
+ - - "~>"
39
48
  - !ruby/object:Gem::Version
40
49
  version: '0.9'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: 1.3.5
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
50
  - - ">="
53
51
  - !ruby/object:Gem::Version
54
- version: 1.3.5
52
+ version: 0.9.10
55
53
  description: The Listen gem listens to file modifications and notifies you about the
56
54
  changes. Works everywhere!
57
55
  email: thibaud@thibaud.gg
@@ -78,16 +76,17 @@ files:
78
76
  - lib/listen/change.rb
79
77
  - lib/listen/cli.rb
80
78
  - lib/listen/directory.rb
79
+ - lib/listen/error.rb
81
80
  - lib/listen/event/config.rb
82
81
  - lib/listen/event/loop.rb
83
82
  - lib/listen/event/processor.rb
84
83
  - lib/listen/event/queue.rb
85
84
  - lib/listen/file.rb
86
85
  - lib/listen/fsm.rb
87
- - lib/listen/internals/thread_pool.rb
88
86
  - lib/listen/listener.rb
89
87
  - lib/listen/listener/config.rb
90
88
  - lib/listen/logger.rb
89
+ - lib/listen/monotonic_time.rb
91
90
  - lib/listen/options.rb
92
91
  - lib/listen/queue_optimizer.rb
93
92
  - lib/listen/record.rb
@@ -95,11 +94,18 @@ files:
95
94
  - lib/listen/record/symlink_detector.rb
96
95
  - lib/listen/silencer.rb
97
96
  - lib/listen/silencer/controller.rb
97
+ - lib/listen/thread.rb
98
98
  - lib/listen/version.rb
99
99
  homepage: https://github.com/guard/listen
100
100
  licenses:
101
101
  - MIT
102
- 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.8.0
107
+ homepage_uri: https://github.com/guard/listen
108
+ source_code_uri: https://github.com/guard/listen/tree/v3.8.0
103
109
  post_install_message:
104
110
  rdoc_options: []
105
111
  require_paths:
@@ -108,17 +114,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
114
  requirements:
109
115
  - - ">="
110
116
  - !ruby/object:Gem::Version
111
- version: 1.9.3
117
+ version: 2.4.0
112
118
  required_rubygems_version: !ruby/object:Gem::Requirement
113
119
  requirements:
114
120
  - - ">="
115
121
  - !ruby/object:Gem::Version
116
122
  version: '0'
117
123
  requirements: []
118
- rubyforge_project:
119
- rubygems_version: 2.4.5
124
+ rubygems_version: 3.0.1
120
125
  signing_key:
121
126
  specification_version: 4
122
127
  summary: Listen to file modifications
123
128
  test_files: []
124
- has_rdoc:
@@ -1,21 +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
- killed << @threads.pop.kill until @threads.empty?
17
- killed.pop.join until killed.empty?
18
- end
19
- end
20
- end
21
- end