filewatch 0.6.7 → 0.6.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,7 +5,47 @@ end
5
5
 
6
6
  module FileWatch
7
7
  class Watch
8
- attr_accessor :logger
8
+ class WatchedFile
9
+ def self.new_initial(path, inode)
10
+ new(path, inode, true)
11
+ end
12
+
13
+ def self.new_ongoing(path, inode)
14
+ new(path, inode, false)
15
+ end
16
+
17
+ attr_reader :size, :inode
18
+ attr_writer :create_sent, :initial, :timeout_sent
19
+
20
+ attr_reader :path
21
+
22
+ def initialize(path, inode, initial)
23
+ @path = path
24
+ @size, @create_sent, @timeout_sent = 0, false, false
25
+ @inode, @initial = inode, initial
26
+ end
27
+
28
+ def update(stat, inode = nil)
29
+ @size = stat.size
30
+ @inode = inode if inode
31
+ end
32
+
33
+ def create_sent?
34
+ @create_sent
35
+ end
36
+
37
+ def initial?
38
+ @initial
39
+ end
40
+
41
+ def timeout_sent?
42
+ @timeout_sent
43
+ end
44
+
45
+ def to_s() inspect; end
46
+ end
47
+
48
+ attr_accessor :logger, :ignore_after
9
49
 
10
50
  public
11
51
  def initialize(opts={})
@@ -18,20 +58,18 @@ module FileWatch
18
58
  end
19
59
  @watching = []
20
60
  @exclude = []
21
- @files = Hash.new { |h, k| h[k] = Hash.new }
61
+ @files = Hash.new { |h, k| h[k] = WatchedFile.new(k, false, false) }
22
62
  @unwatched = Hash.new
23
63
  # we need to be threadsafe about the mutation
24
64
  # of the above 2 ivars because the public
25
65
  # methods each, discover, watch and unwatch
26
66
  # can be called from different threads.
27
67
  @lock = Mutex.new
68
+ # we need to be threadsafe about the quit mutation
69
+ @quit = false
70
+ @quit_lock = Mutex.new
28
71
  end # def initialize
29
72
 
30
- public
31
- def logger=(logger)
32
- @logger = logger
33
- end
34
-
35
73
  public
36
74
  def exclude(path)
37
75
  path.to_a.each { |p| @exclude << p }
@@ -42,7 +80,9 @@ module FileWatch
42
80
  synchronized do
43
81
  if !@watching.member?(path)
44
82
  @watching << path
45
- _discover_file(path, true)
83
+ _discover_file(path) do |filepath, stat|
84
+ WatchedFile.new_initial(filepath, inode(filepath, stat))
85
+ end
46
86
  end
47
87
  end
48
88
  return true
@@ -86,18 +126,18 @@ module FileWatch
86
126
  def each(&block)
87
127
  synchronized do
88
128
  # Send any creates.
89
- @files.keys.each do |path|
90
- if ! @files[path][:create_sent]
91
- if @files[path][:initial]
129
+ @files.each do |path, watched_file|
130
+ if !watched_file.create_sent?
131
+ if watched_file.initial?
92
132
  yield(:create_initial, path)
93
133
  else
94
134
  yield(:create, path)
95
135
  end
96
- @files[path][:create_sent] = true
136
+ watched_file.create_sent = true
97
137
  end
98
138
  end
99
139
 
100
- @files.keys.each do |path|
140
+ @files.each do |path, watched_file|
101
141
  begin
102
142
  stat = File::Stat.new(path)
103
143
  rescue Errno::ENOENT
@@ -108,23 +148,33 @@ module FileWatch
108
148
  next
109
149
  end
110
150
 
151
+ if expired?(stat, watched_file)
152
+ if !watched_file.timeout_sent?
153
+ @logger.debug? && @logger.debug("#{path}: file expired")
154
+ yield(:timeout, path)
155
+ watched_file.timeout_sent = true
156
+ end
157
+ next
158
+ end
159
+
111
160
  inode = inode(path,stat)
112
- if inode != @files[path][:inode]
113
- @logger.debug? && @logger.debug("#{path}: old inode was #{@files[path][:inode].inspect}, new is #{inode.inspect}")
161
+ old_size = watched_file.size
162
+
163
+ if inode != watched_file.inode
164
+ @logger.debug? && @logger.debug("#{path}: old inode was #{watched_file.inode.inspect}, new is #{inode.inspect}")
114
165
  yield(:delete, path)
115
166
  yield(:create, path)
116
- elsif stat.size < @files[path][:size]
117
- @logger.debug? && @logger.debug("#{path}: file rolled, new size is #{stat.size}, old size #{@files[path][:size]}")
167
+ elsif stat.size < old_size
168
+ @logger.debug? && @logger.debug("#{path}: file rolled, new size is #{stat.size}, old size #{old_size}")
118
169
  yield(:delete, path)
119
170
  yield(:create, path)
120
- elsif stat.size > @files[path][:size]
121
- @logger.debug? && @logger.debug("#{path}: file grew, old size #{@files[path][:size]}, new size #{stat.size}")
171
+ elsif stat.size > old_size
172
+ @logger.debug? && @logger.debug("#{path}: file grew, old size #{old_size}, new size #{stat.size}")
122
173
  yield(:modify, path)
123
174
  end
124
175
 
125
- @files[path][:size] = stat.size
126
- @files[path][:inode] = inode
127
- end # @files.keys.each
176
+ watched_file.update(stat, inode)
177
+ end
128
178
  end
129
179
  end # def each
130
180
 
@@ -132,7 +182,9 @@ module FileWatch
132
182
  def discover
133
183
  synchronized do
134
184
  @watching.each do |path|
135
- _discover_file(path)
185
+ _discover_file(path) do |filepath, stat|
186
+ WatchedFile.new_ongoing(filepath, inode(filepath, stat))
187
+ end
136
188
  end
137
189
  end
138
190
  end
@@ -140,8 +192,8 @@ module FileWatch
140
192
  public
141
193
  def subscribe(stat_interval = 1, discover_interval = 5, &block)
142
194
  glob = 0
143
- @quit = false
144
- while !@quit
195
+ reset_quit
196
+ while !quit?
145
197
  each(&block)
146
198
 
147
199
  glob += 1
@@ -155,7 +207,24 @@ module FileWatch
155
207
  end # def subscribe
156
208
 
157
209
  private
158
- def _discover_file(path, initial=false)
210
+ def expired?(stat, watched_file)
211
+ file_expired?(stat) && watched_file.size == stat.size
212
+ end
213
+
214
+ def discover_expired?(stat)
215
+ file_expired?(stat)
216
+ end
217
+
218
+ def file_expired?(stat)
219
+ return false unless expiry_enabled?
220
+ # (Time.now - stat.mtime) <- in jruby, this does int and float
221
+ # conversions before the subtraction and returns a float.
222
+ # so use all ints instead
223
+ (Time.now.to_i - stat.mtime.to_i) > @ignore_after
224
+ end
225
+
226
+ private
227
+ def _discover_file(path)
159
228
  _globbed_files(path).each do |file|
160
229
  next if @files.member?(file)
161
230
  next if @unwatched.member?(file)
@@ -175,15 +244,24 @@ module FileWatch
175
244
  next if skip
176
245
 
177
246
  stat = File::Stat.new(file)
178
- @files[file] = {
179
- :size => 0,
180
- :inode => inode(file,stat),
181
- :create_sent => false,
182
- :initial => initial
183
- }
247
+ # let the caller build the object in its context
248
+ watched_file = yield(file, stat)
249
+
250
+ if discover_expired?(stat)
251
+ msg = "_discover_file: #{file}: skipping because it was last modified more than #{@ignore_after} seconds ago"
252
+ @logger.debug? && @logger.debug(msg)
253
+ watched_file.update(stat)
254
+ end
255
+
256
+ @files[file] = watched_file
184
257
  end
185
258
  end # def _discover_file
186
259
 
260
+ private
261
+ def expiry_enabled?
262
+ !@ignore_after.nil?
263
+ end
264
+
187
265
  private
188
266
  def _globbed_files(path)
189
267
  globbed_dirs = Dir.glob(path)
@@ -201,9 +279,19 @@ module FileWatch
201
279
  @lock.synchronize { block.call }
202
280
  end
203
281
 
282
+ private
283
+ def quit?
284
+ @quit_lock.synchronize { @quit }
285
+ end
286
+
287
+ private
288
+ def reset_quit
289
+ @quit_lock.synchronize { @quit = false }
290
+ end
291
+
204
292
  public
205
293
  def quit
206
- @quit = true
294
+ @quit_lock.synchronize { @quit = true }
207
295
  end # def quit
208
296
  end # class Watch
209
297
  end # module FileWatch
@@ -0,0 +1,79 @@
1
+ require 'filewatch/tail_base'
2
+
3
+ module FileWatch
4
+ class YieldingTail
5
+ include TailBase
6
+
7
+ public
8
+ def subscribe(&block)
9
+ # subscribe(stat_interval = 1, discover_interval = 5, &block)
10
+ @watch.subscribe(@opts[:stat_interval],
11
+ @opts[:discover_interval]) do |event, path|
12
+ case event
13
+ when :create, :create_initial
14
+ if @files.member?(path)
15
+ @logger.debug? && @logger.debug("#{event} for #{path}: already exists in @files")
16
+ next
17
+ end
18
+ if _open_file(path, event)
19
+ yield_read_file(path, &block)
20
+ end
21
+ when :modify
22
+ if !@files.member?(path)
23
+ @logger.debug? && @logger.debug(":modify for #{path}, does not exist in @files")
24
+ if _open_file(path, event)
25
+ yield_read_file(path, &block)
26
+ end
27
+ else
28
+ yield_read_file(path, &block)
29
+ end
30
+ when :delete
31
+ @logger.debug? && @logger.debug(":delete for: #{path} - closed and deleted from @files")
32
+ if @files[path]
33
+ yield_read_file(path, &block)
34
+ @files[path].close
35
+ end
36
+ @files.delete(path)
37
+ @statcache.delete(path)
38
+ when :timeout
39
+ @logger.debug? && @logger.debug(":timeout for: #{path} - closed and deleted from @files")
40
+ if (deleted = @files.delete(path))
41
+ deleted.close
42
+ end
43
+ @statcache.delete(path)
44
+ else
45
+ @logger.warn("unknown event type #{event} for #{path}")
46
+ end
47
+ end # @watch.subscribe
48
+ end # def subscribe
49
+
50
+ private
51
+ def yield_read_file(path, &block)
52
+ @buffers[path] ||= FileWatch::BufferedTokenizer.new(@opts[:delimiter])
53
+ delimiter_byte_size = @opts[:delimiter].bytesize
54
+ changed = false
55
+ loop do
56
+ begin
57
+ data = @files[path].sysread(32768)
58
+ changed = true
59
+ @buffers[path].extract(data).each do |line|
60
+ yield(path, line)
61
+ @sincedb[@statcache[path]] += (line.bytesize + delimiter_byte_size)
62
+ end
63
+ rescue Errno::EWOULDBLOCK, Errno::EINTR, EOFError
64
+ break
65
+ end
66
+ end
67
+
68
+ if changed
69
+ now = Time.now.to_i
70
+ delta = now - @sincedb_last_write
71
+ if delta >= @opts[:sincedb_write_interval]
72
+ @logger.debug? && @logger.debug("writing sincedb (delta since last write = #{delta})")
73
+ _sincedb_write
74
+ @sincedb_last_write = now
75
+ end
76
+ end
77
+ end
78
+ end # module YieldingTail
79
+ end # module FileWatch
metadata CHANGED
@@ -1,32 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filewatch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.7
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Sissel
8
8
  - Pete Fritchman
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-12-02 00:00:00.000000000 Z
12
+ date: 2015-12-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: stud
16
15
  requirement: !ruby/object:Gem::Requirement
17
16
  requirements:
18
- - - ">="
17
+ - - '>='
19
18
  - !ruby/object:Gem::Version
20
19
  version: '0'
21
- type: :development
20
+ name: stud
22
21
  prerelease: false
22
+ type: :development
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - ">="
25
+ - - '>='
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
- description: Watch files and directories in ruby. Also supports tailing and glob file
29
- patterns.
28
+ description: Watch files and directories in ruby. Also supports tailing and glob file patterns.
30
29
  email:
31
30
  - jls@semicomplete.com
32
31
  - petef@databits.net
@@ -39,13 +38,12 @@ files:
39
38
  - lib/JRubyFileExtension.jar
40
39
  - lib/filewatch/buftok.rb
41
40
  - lib/filewatch/helper.rb
41
+ - lib/filewatch/observing_tail.rb
42
42
  - lib/filewatch/tail.rb
43
+ - lib/filewatch/tail_base.rb
43
44
  - lib/filewatch/watch.rb
44
45
  - lib/filewatch/winhelper.rb
45
- - spec/buftok_spec.rb
46
- - spec/tail_spec.rb
47
- - spec/watch_spec.rb
48
- - spec/winhelper_spec.rb
46
+ - lib/filewatch/yielding_tail.rb
49
47
  - test/filewatch/tail.rb
50
48
  - test/globtail/Makefile
51
49
  - test/globtail/framework.sh
@@ -72,25 +70,25 @@ files:
72
70
  homepage: https://github.com/jordansissel/ruby-filewatch
73
71
  licenses: []
74
72
  metadata: {}
75
- post_install_message:
73
+ post_install_message:
76
74
  rdoc_options: []
77
75
  require_paths:
78
76
  - lib
79
77
  - lib
80
78
  required_ruby_version: !ruby/object:Gem::Requirement
81
79
  requirements:
82
- - - ">="
80
+ - - '>='
83
81
  - !ruby/object:Gem::Version
84
82
  version: '0'
85
83
  required_rubygems_version: !ruby/object:Gem::Requirement
86
84
  requirements:
87
- - - ">="
85
+ - - '>='
88
86
  - !ruby/object:Gem::Version
89
87
  version: '0'
90
88
  requirements: []
91
- rubyforge_project:
92
- rubygems_version: 2.4.6
93
- signing_key:
89
+ rubyforge_project:
90
+ rubygems_version: 2.4.8
91
+ signing_key:
94
92
  specification_version: 4
95
93
  summary: filewatch - file watching for ruby
96
94
  test_files: []
data/spec/buftok_spec.rb DELETED
@@ -1,18 +0,0 @@
1
- require 'filewatch/buftok'
2
-
3
- describe FileWatch::BufferedTokenizer do
4
-
5
- context "when using the default delimiter" do
6
- it "splits the lines correctly" do
7
- expect(subject.extract("hello\nworld\n")).to eq ["hello", "world"]
8
- end
9
- end
10
-
11
- context "when passing a custom delimiter" do
12
- subject { FileWatch::BufferedTokenizer.new("\r\n") }
13
-
14
- it "splits the lines correctly" do
15
- expect(subject.extract("hello\r\nworld\r\n")).to eq ["hello", "world"]
16
- end
17
- end
18
- end
data/spec/tail_spec.rb DELETED
@@ -1,232 +0,0 @@
1
- require 'filewatch/tail'
2
- require 'stud/temporary'
3
- require "rbconfig"
4
-
5
- describe FileWatch::Tail do
6
- before(:all) do
7
- @thread_abort = Thread.abort_on_exception
8
- Thread.abort_on_exception = true
9
- end
10
-
11
- after(:all) do
12
- Thread.abort_on_exception = @thread_abort
13
- end
14
-
15
- let(:file_path) { f = Stud::Temporary.pathname }
16
- let(:sincedb_path) { Stud::Temporary.pathname }
17
-
18
- before :each do
19
- Thread.new(subject) { sleep 0.5; subject.quit } # force the subscribe loop to exit
20
- end
21
-
22
- context "when watching a new file" do
23
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning, :stat_interval => 0) }
24
-
25
- before :each do
26
- subject.tail(file_path)
27
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
28
- end
29
-
30
- it "reads new lines off the file" do
31
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"])
32
- end
33
- end
34
-
35
- context "when watching a file" do
36
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning, :stat_interval => 0) }
37
-
38
- before :each do
39
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
40
- subject.tail(file_path)
41
- end
42
-
43
- it "reads new lines off the file" do
44
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"])
45
- end
46
-
47
- end
48
-
49
- context "when watching a CRLF file" do
50
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path,
51
- :start_new_files_at => :beginning,
52
- :delimiter => "\r\n") }
53
-
54
- before :each do
55
- File.open(file_path, "wb") { |file| file.write("line1\r\nline2\r\n") }
56
- subject.tail(file_path)
57
- end
58
-
59
- it "reads new lines off the file" do
60
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"])
61
- end
62
- end
63
-
64
- context "when a file is deleted" do
65
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning) }
66
-
67
- before :each do
68
- File.open(file_path, "w") { |file| file.write("line1\nline2\n") }
69
- subject.tail(file_path)
70
- File.unlink file_path
71
- end
72
-
73
- it "should not raise exception" do
74
- Thread.new(subject) { sleep 0.1; subject.quit } # force the subscribe loop to exit
75
- expect { subject.subscribe {|p,l| } }.to_not raise_exception
76
- end
77
- end
78
-
79
- describe "sincedb" do
80
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning, :stat_interval => 0) }
81
-
82
- before :each do
83
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
84
- subject.tail(file_path)
85
- end
86
-
87
- context "when reading a new file" do
88
- it "updates sincedb after subscribe" do
89
- subject.subscribe {|_,_| }
90
- stat = File::Stat.new(file_path)
91
- sincedb_id = subject.sincedb_record_uid(file_path,stat).join(' ')
92
- expect(File.read(sincedb_path)).to eq("#{sincedb_id} #{stat.size}\n")
93
- end
94
- end
95
-
96
- context "when restarting tail" do
97
- before :each do
98
- subject.subscribe {|_,_| }
99
- sleep 0.6 # wait for tail.quit
100
- subject.tail(file_path) # re-tail file
101
- File.open(file_path, "ab") { |file| file.write("line3\nline4\n") }
102
- Thread.new(subject) { sleep 0.5; subject.quit }
103
- end
104
-
105
- it "picks off from where it stopped" do
106
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line3"], [file_path, "line4"])
107
- end
108
-
109
- it "updates on tail.quit" do
110
- subject.subscribe {|_,_| }
111
- stat = File::Stat.new(file_path)
112
- sincedb_id = subject.sincedb_record_uid(file_path,stat).join(' ')
113
- expect(File.read(sincedb_path)).to eq("#{sincedb_id} #{stat.size}\n")
114
- end
115
- end
116
- end
117
-
118
- context "ingesting files bigger than 32k" do
119
- let(:lineA) { "a" * 12000 }
120
- let(:lineB) { "b" * 25000 }
121
- let(:lineC) { "c" * 8000 }
122
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning) }
123
-
124
- before :each do
125
- IO.write(file_path, "#{lineA}\n#{lineB}\n#{lineC}\n")
126
- end
127
-
128
- context "when restarting after stopping at the first line" do
129
-
130
- let(:new_subject) { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning) }
131
-
132
- before :each do
133
- subject.tail(file_path)
134
- subject.subscribe {|f, l| break if @test; @test = 1}
135
- subject.sincedb_write
136
- subject.quit
137
- Thread.new(new_subject) { sleep 0.5; new_subject.quit } # force the subscribe loop to exit
138
- end
139
-
140
- it "should store in sincedb the position up until the first string" do
141
- device, dev_major, dev_minor, pos = *IO.read(sincedb_path).split(" ").map {|n| n.to_i }
142
- expect(pos).to eq(12001) # string.bytesize + "\n".bytesize
143
- end
144
-
145
- it "should read the second and third lines entirely" do
146
- new_subject.tail(file_path) # re-tail file
147
- expect { |b| new_subject.subscribe(&b) }.to yield_successive_args([file_path, lineB], [file_path, lineC])
148
- end
149
- end
150
- end
151
-
152
- context "when watching a directory" do
153
-
154
- let(:directory) { Stud::Temporary.directory }
155
- let(:file_path) { File.join(directory, "1.log") }
156
-
157
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning, :stat_interval => 0) }
158
-
159
- before :each do
160
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
161
- subject.tail(File.join(directory, "*"))
162
- end
163
-
164
- after :each do
165
- FileUtils.rm_rf(directory)
166
- end
167
-
168
- it "reads new lines from the beginning" do
169
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"])
170
- end
171
-
172
- context "when a file is renamed" do
173
-
174
- before :each do
175
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"])
176
- File.rename(file_path, file_path + ".bak")
177
- end
178
-
179
- it "should not re-read the file" do
180
- Thread.new(subject) { |s| sleep 1; s.quit }
181
- expect { |b| subject.subscribe(&b) }.not_to yield_control
182
- end
183
- end
184
-
185
- let(:new_file_path) { File.join(directory, "2.log") }
186
-
187
- context "when a new file is later added to the directory" do
188
- # Note tests in this context rely on FileWatch::Watch reading
189
- # file 1.log first then 2.log and that depends on how Dir.glob is implemented
190
- # in different rubies on different operating systems
191
- before do
192
- File.open(new_file_path, "wb") { |file| file.write("line2.1\nline2.2\n") }
193
- end
194
-
195
- it "reads new lines from the beginning for all files" do
196
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"],
197
- [new_file_path, "line2.1"], [new_file_path, "line2.2"])
198
- end
199
-
200
- context "and when the sincedb path is not given" do
201
- subject { FileWatch::Tail.new(:start_new_files_at => :beginning, :stat_interval => 0) }
202
-
203
- it "reads new lines from the beginning for all files" do
204
- expect { |b| subject.subscribe(&b) }.to yield_successive_args([file_path, "line1"], [file_path, "line2"],
205
- [new_file_path, "line2.1"], [new_file_path, "line2.2"])
206
- end
207
- end
208
- end
209
- end
210
-
211
- if RbConfig::CONFIG['host_os'] !~ /mswin|mingw|cygwin/
212
- context "when quiting" do
213
- subject { FileWatch::Tail.new(:sincedb_path => sincedb_path, :start_new_files_at => :beginning, :stat_interval => 0) }
214
-
215
- before :each do
216
- subject.tail(file_path)
217
- File.open(file_path, "wb") { |file| file.write("line1\nline2\n") }
218
- end
219
-
220
- it "closes the file handles" do
221
- buffer = []
222
- subject.subscribe do |path, line|
223
- buffer.push([path, line])
224
- end
225
- subject.sincedb_write
226
- subject.quit
227
- lsof = `lsof -p #{Process.pid} | grep #{file_path}`
228
- expect(lsof).to be_empty
229
- end
230
- end
231
- end
232
- end