Skip to content

Commit

Permalink
Convert Listen into its own executable to run in separate process
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexB52 committed Feb 15, 2025
1 parent 38a7129 commit 3668fef
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 62 deletions.
4 changes: 0 additions & 4 deletions lib/retest.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require 'listen'

require 'string/similarity'
require 'observer'

Expand All @@ -17,8 +15,6 @@
require "retest/sounds"
require "retest/watcher"

Listen.adapter_warn_behavior = :log

module Retest
class Error < StandardError; end
class FileNotFound < StandardError; end
Expand Down
1 change: 1 addition & 0 deletions lib/retest/runner.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'forwardable'
require_relative "runner/cached_test_file"

module Retest
Expand Down
77 changes: 19 additions & 58 deletions lib/retest/watcher.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'pathname'

module Retest
module Watcher
def self.for(watcher)
Expand All @@ -24,25 +26,19 @@ def self.installed?
true
end

def self.extensions_regex(extensions)
Regexp.new("\\.(?:#{extensions.join("|")})$")
end

def self.watch(dir:, extensions:, polling: false)
command = "./lib/scripts/listen --exts #{extensions.join(',')} -w #{dir} --polling #{polling}"

watch_rd, watch_wr = IO.pipe
# Process needs its own process group otherwise the process gets killed on INT signal
# We need the process to still run when trying to stop the current test run
# Maybe there is another way to prevent killing these but for now a new process groups works
# Process group created with: Process.setsid
watch_rd, watch_wr = IO.pipe
pid = fork do
Process.setsid
Listen.to(dir, only: extensions_regex(extensions), relative: true, polling: polling) do |modified, added, removed|
if modified.any?
watch_wr.write "modify:#{modified.first}"
elsif added.any?
watch_wr.write "create:#{added.first}"
elsif removed.any?
watch_wr.write "remove:#{removed.first}"
end
end.start
sleep
end
# Process group created with: pgroup: true
pid = Process.spawn(command, out: watch_wr, pgroup: true)

at_exit do
Process.kill("TERM", pid) if pid
Expand All @@ -59,23 +55,20 @@ def self.watch(dir:, extensions:, polling: false)
change = /^(?<action>create|remove|modify):(?<path>.*)/.match(data.strip)

next unless change
path = Pathname(change[:path]).relative_path_from(Dir.pwd).to_s

modified, added, removed = result = [[], [], []]
case change[:action]
when 'modify' then modified << change[:path]
when 'create' then added << change[:path]
when 'remove' then removed << change[:path]
when 'modify' then modified << path
when 'create' then added << path
when 'remove' then removed << path
end

yield result
end
end
end
end

def self.extensions_regex(extensions)
Regexp.new("\\.(?:#{extensions.join("|")})$")
end
end

module Watchexec
Expand All @@ -85,7 +78,6 @@ def self.installed?

def self.watch(dir:, extensions:, polling: false)
command = "watchexec --exts #{extensions.join(',')} -w #{dir} --emit-events-to stdio --no-meta --only-emit-events"
files = VersionControl.files(extensions: extensions).zip([]).to_h

watch_rd, watch_wr = IO.pipe
# Process needs its own process group otherwise the process gets killed on INT signal
Expand All @@ -101,11 +93,15 @@ def self.watch(dir:, extensions:, polling: false)
end

Thread.new do
files = VersionControl.files(extensions: extensions).zip([]).to_h

loop do
ready = IO.select([watch_rd])
readable_connections = ready[0]
readable_connections.each do |conn|
data = conn.readpartial(4096)
# Watchexec is not great at figuring out whether a file has been deleted and comes as an update.
# This is why we're not looking at the action like we do with Listen.
change = /^(?:create|remove|rename|modify):(?<path>.*)/.match(data.strip)

next unless change
Expand All @@ -129,41 +125,6 @@ def self.watch(dir:, extensions:, polling: false)
end
end
end

# require 'open3'
# Thread.new do
# files = VersionControl.files(extensions: extensions).zip([]).to_h

# Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
# loop do
# ready = IO.select([stdout])
# readable_connections = ready[0]
# readable_connections.each do |conn|
# data = conn.readpartial(4096)
# change = /^(?:create|remove|rename|modify):(?<path>.*)/.match(data.strip)

# next unless change

# path = Pathname(change[:path]).relative_path_from(Dir.pwd).to_s
# file_exist = File.exist?(path)
# file_cached = files.key?(path)

# modified, added, removed = result = [[], [], []]
# if file_exist && file_cached
# modified << path
# elsif file_exist && !file_cached
# added << path
# files[path] = nil
# elsif !file_exist && file_cached
# removed << path
# files.delete(path)
# end

# yield result
# end
# end
# end
# end
end
end
end
Expand Down
57 changes: 57 additions & 0 deletions lib/scripts/listen
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env ruby

$stdout.sync = true

require 'listen'
require 'optparse'

options = {}
OptionParser.new do |opts|
opts.banner = "Usage: scripts/listen.rb [options]"

opts.on("--exts rb,js,ts", Array, "Extensions to watch for") do |list|
options[:extensions] = list
end

opts.on("--polling BOOLEAN", "Force Listen to use polling") do |value|
options[:polling] = value
end

opts.on("-w", "--watch .", "Directory to listen to") do |value|
options[:dir] = value
end

opts.on("-h", "--help", "Prints help") do
puts opts
exit
end
end.parse!

unless options.key?(:extensions)
raise ArgumentError, 'must provide the files extensions to watch for'
end

unless options.key?(:polling)
raise ArgumentError, 'must provide the polling option'
end

unless options.key?(:dir)
raise ArgumentError, 'must provide the directory path to watch'
end

def extensions_regex(extensions)
Regexp.new("\\.(?:#{extensions.join("|")})$")
end

Listen.adapter_warn_behavior = :log

Listen.to(options[:dir], only: extensions_regex(options[:extensions]), polling: options[:polling]) do |modified, added, removed|
if modified.any?
$stdout.puts "modify:#{modified.first}"
elsif added.any?
$stdout.puts "create:#{added.first}"
elsif removed.any?
$stdout.puts "remove:#{removed.first}"
end
end.start
sleep

0 comments on commit 3668fef

Please sign in to comment.