Class: Listen::Listener

Inherits:
Object
  • Object
show all
Defined in:
lib/listen/listener.rb

Direct Known Subclasses

TCP::Listener

Constant Summary collapse

RELATIVE_PATHS_WITH_MULTIPLE_DIRECTORIES_WARNING_MESSAGE =
"The relative_paths option doesn't work when listening to multiple diretories."

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) {|modified, added, removed| ... } ⇒ Listener

Initializes the directories listener.

Parameters:

  • directory (String)

    the directories to listen to

  • options (Hash)

    the listen options (see Listen::Listener::Options)

Yields:

  • (modified, added, removed)

    the changed files

Yield Parameters:

  • modified (Array<String>)

    the list of modified files

  • added (Array<String>)

    the list of added files

  • removed (Array<String>)

    the list of removed files



24
25
26
27
28
29
30
31
# File 'lib/listen/listener.rb', line 24

def initialize(*args, &block)
  @options     = _init_options(args.last.is_a?(Hash) ? args.pop : {})
  @directories = args.flatten.map { |path| Pathname.new(path).realpath }
  @changes     = []
  @block       = block
  @registry    = Celluloid::Registry.new
  _init_debug
end

Instance Attribute Details

#blockObject

Returns the value of attribute block.



9
10
11
# File 'lib/listen/listener.rb', line 9

def block
  @block
end

#changesObject

Returns the value of attribute changes.



9
10
11
# File 'lib/listen/listener.rb', line 9

def changes
  @changes
end

#directoriesObject

Returns the value of attribute directories.



9
10
11
# File 'lib/listen/listener.rb', line 9

def directories
  @directories
end

#optionsObject

Returns the value of attribute options.



9
10
11
# File 'lib/listen/listener.rb', line 9

def options
  @options
end

#pausedObject

Returns the value of attribute paused.



9
10
11
# File 'lib/listen/listener.rb', line 9

def paused
  @paused
end

#registryObject

Returns the value of attribute registry.



10
11
12
# File 'lib/listen/listener.rb', line 10

def registry
  @registry
end

#stoppingObject

Returns the value of attribute stopping.



9
10
11
# File 'lib/listen/listener.rb', line 9

def stopping
  @stopping
end

#supervisorObject

Returns the value of attribute supervisor.



10
11
12
# File 'lib/listen/listener.rb', line 10

def supervisor
  @supervisor
end

Instance Method Details

#_calculate_add_remove_difference(actions, path, default_if_exists) ⇒ Object (private)



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/listen/listener.rb', line 201

def _calculate_add_remove_difference(actions, path, default_if_exists)
  added = actions.count { |x| x == :added }
  removed = actions.count { |x| x == :removed }
  diff = added - removed

  if path.exist?
    if diff > 0
      :added
    elsif diff.zero? && added > 0
      :modified
    else
      default_if_exists
    end
  else
    diff < 0 ? :removed : nil
  end
end

#_detect_possible_editor_save(changes) ⇒ Object (private)



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/listen/listener.rb', line 235

def _detect_possible_editor_save(changes)
  return unless changes.size == 2

  from, to = changes.sort { |x,y| x.keys.first <=> y.keys.first }
  from, to = from[:moved_from], to[:moved_to]
  return unless from and to

  # Expect an ignored moved_from and non-ignored moved_to
  # to qualify as an "editor modify"
  _silenced?(from) && !_silenced?(to) ? to : nil
end

#_init_actorsObject (private)



127
128
129
130
131
132
133
134
135
# File 'lib/listen/listener.rb', line 127

def _init_actors
  @supervisor = Celluloid::SupervisionGroup.run!(registry)
  supervisor.add(Silencer, as: :silencer, args: self)
  supervisor.add(Record, as: :record, args: self)
  supervisor.pool(Change, as: :change_pool, args: self)

  adapter_class = Adapter.select(options)
  supervisor.add(adapter_class, as: :adapter, args: self)
end

#_init_debugObject (private)



119
120
121
122
123
124
125
# File 'lib/listen/listener.rb', line 119

def _init_debug
  if options[:debug] || ENV['LISTEN_GEM_DEBUGGING'] =~ /true|1/i
    Celluloid.logger.level = Logger::INFO
  else
    Celluloid.logger.level = Logger::FATAL
  end
end

#_init_options(options = {}) ⇒ Object (private)



111
112
113
114
115
116
117
# File 'lib/listen/listener.rb', line 111

def _init_options(options = {})
  { debug: false,
    latency: nil,
    wait_for_delay: 0.1,
    force_polling: false,
    polling_fallback_message: nil }.merge(options)
end

#_local_fs?Boolean (private)

Returns:

  • (Boolean)


175
176
177
# File 'lib/listen/listener.rb', line 175

def _local_fs?
  !registry[:adapter].is_a?(Adapter::TCP)
end

#_logical_action_for(path, actions) ⇒ Object (private)



193
194
195
196
197
198
199
# File 'lib/listen/listener.rb', line 193

def _logical_action_for(path, actions)
  actions << :added if actions.delete(:moved_to)
  actions << :removed if actions.delete(:moved_from)

  modified = actions.find { |x| x == :modified }
  _calculate_add_remove_difference(actions, path, modified)
end

#_pop_changesObject (private)



157
158
159
160
161
# File 'lib/listen/listener.rb', line 157

def _pop_changes
  popped = []
  popped << @changes.shift until @changes.empty?
  popped
end

remove extraneous rb-inotify events, keeping them only if it's a possible editor rename() call (e.g. Kate and Sublime)



221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/listen/listener.rb', line 221

def _reinterpret_related_changes(cookies)
  table = { moved_to: :added, moved_from: :removed }
  cookies.map do |cookie, changes|
    file = _detect_possible_editor_save(changes)
    if file
      [[:modified, file]]
    else
      changes.map(&:first).reject do |type, path|
        _silenced?(path)
      end.map { |type, path| [table.fetch(type, type), path] }
    end
  end.flatten(1)
end

#_silenced?(path) ⇒ Boolean (private)

Returns:

  • (Boolean)


247
248
249
250
# File 'lib/listen/listener.rb', line 247

def _silenced?(path)
  type = path.directory? ? 'Dir' : 'File'
  registry[:silencer].silenced?(path, type)
end

#_smoosh_changes(changes) ⇒ Object (private)



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/listen/listener.rb', line 163

def _smoosh_changes(changes)
  if _local_fs?
    cookies = changes.group_by { |x| x[:cookie] }
    _squash_changes(_reinterpret_related_changes(cookies))
  else
    smooshed = { modified: [], added: [], removed: [] }
    changes.each { |h| type = h.keys.first; smooshed[type] << h[type].to_s }
    smooshed.each { |_, v| v.uniq! }
    smooshed
  end
end

#_squash_changes(changes) ⇒ Object (private)



179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/listen/listener.rb', line 179

def _squash_changes(changes)
  actions = changes.group_by(&:last).map do |path, action_list|
    [_logical_action_for(path, action_list.map(&:first)), path.to_s]
  end
  Celluloid.logger.info "listen: raw changes: #{actions.inspect}"

  { modified: [], added: [], removed: [] }.tap do |squashed|
    actions.each do |type, path|
      squashed[type] << path unless type.nil?
    end
    Celluloid.logger.info "listen: final changes: #{squashed.inspect}"
  end
end

#_wait_for_changesObject (private)



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/listen/listener.rb', line 137

def _wait_for_changes
  loop do
    break if @stopping

    changes = []
    begin
      sleep options[:wait_for_delay] # wait for changes to accumulate
      new_changes = _pop_changes
      changes += new_changes
    end until new_changes.empty?
    unless changes.empty?
      hash = _smoosh_changes(changes)
      block.call(hash[:modified], hash[:added], hash[:removed])
    end
  end
rescue => ex
  Kernel.warn "[Listen warning]: Change block raised an exception: #{$!}"
  Kernel.warn "Backtrace:\n\t#{ex.backtrace.join("\n\t")}"
end

#ignore(regexps) ⇒ Object

Adds ignore patterns to the existing one (See DEFAULT_IGNORED_DIRECTORIES and DEFAULT_IGNORED_EXTENSIONS in Listen::Silencer)

Parameters:

  • new (Regexp, Array<Regexp>)

    ignoring patterns.



85
86
87
88
# File 'lib/listen/listener.rb', line 85

def ignore(regexps)
  @options[:ignore] = [options[:ignore], regexps]
  registry[:silencer] = Silencer.new(self)
end

#ignore!(regexps) ⇒ Object

Overwrites ignore patterns (See DEFAULT_IGNORED_DIRECTORIES and DEFAULT_IGNORED_EXTENSIONS in Listen::Silencer)

Parameters:

  • new (Regexp, Array<Regexp>)

    ignoring patterns.



94
95
96
97
98
# File 'lib/listen/listener.rb', line 94

def ignore!(regexps)
  @options.delete(:ignore)
  @options[:ignore!] = regexps
  registry[:silencer] = Silencer.new(self)
end

#listen?Boolean

Returns true if Listener is neither paused nor stopped

Returns:

  • (Boolean)


77
78
79
# File 'lib/listen/listener.rb', line 77

def listen?
  @paused == false && @stopping == false
end

#only(regexps) ⇒ Object

Sets only patterns, to listen only to specific regexps

Parameters:

  • new (Regexp, Array<Regexp>)

    ignoring patterns.



104
105
106
107
# File 'lib/listen/listener.rb', line 104

def only(regexps)
  @options[:only] = regexps
  registry[:silencer] = Silencer.new(self)
end

#pauseObject

Pauses listening callback (adapter still running)



54
55
56
# File 'lib/listen/listener.rb', line 54

def pause
  @paused = true
end

#paused?Boolean

Returns true if Listener is paused

Returns:

  • (Boolean)


69
70
71
# File 'lib/listen/listener.rb', line 69

def paused?
  @paused == true
end

#startObject

Starts the listener by initializing the adapter and building the directory record concurrently, then it starts the adapter to watch for changes. The current thread is not blocked after starting.



37
38
39
40
41
42
43
# File 'lib/listen/listener.rb', line 37

def start
  _init_actors
  unpause
  @stopping = false
  registry[:adapter].async.start
  Thread.new { _wait_for_changes }
end

#stopObject

Terminates all Listen actors and kill the adapter.



47
48
49
50
# File 'lib/listen/listener.rb', line 47

def stop
  @stopping = true
  supervisor.terminate
end

#unpauseObject

Unpauses listening callback



60
61
62
63
# File 'lib/listen/listener.rb', line 60

def unpause
  registry[:record].build
  @paused = false
end