# frozen_string_literal: true

#
# Ruby Bug: Addrinfo.getaddrinfo(AF_UNSPEC) deadlocks after fork on macOS
# for hostnames with no AAAA (IPv6) records.
#
# The bug is probabilistic — it depends on mDNSResponder's internal state at
# the moment of fork. More parent-side DNS activity increases the probability.
# This script runs multiple trials in an attempt to demonstrate the issue.
#
# Usage:
#   ruby ruby_getaddrinfo_fork_bug.rb
#

require 'socket'
require 'timeout'

NUM_TRIALS = 50

puts '=' * 70
puts 'getaddrinfo(AF_UNSPEC) deadlock after fork on macOS'
puts '=' * 70
puts
puts "Ruby:     #{RUBY_DESCRIPTION}"
puts "Platform: #{RUBY_PLATFORM}"
puts "PID:      #{Process.pid}"
puts

def fork_test(timeout: 6)
  rd, wr = IO.pipe
  pid = fork do
    rd.close
    begin
      wr.write(yield)
    rescue StandardError => error
      wr.write("EXCEPTION:#{error.class}:#{error.message}")
    ensure
      wr.close
    end
  end
  wr.close
  begin
    out = Timeout.timeout(timeout + 2) do
      Process.waitpid(pid)
      rd.read
    end
  rescue Timeout::Error
    begin
      Process.kill('KILL', pid)
    rescue StandardError
      nil
    end
    begin
      Process.waitpid(pid)
    rescue StandardError
      nil
    end
    out = 'DEADLOCK'
  end
  rd.close
  out
end

# Test 1: Resolve an IPv4-only host, fork, child resolves same host
puts "--- Test 1: getaddrinfo(httpbin.org, AF_UNSPEC) — #{NUM_TRIALS} trials ---"
puts '(httpbin.org has NO AAAA records)'
puts

deadlocks = 0
NUM_TRIALS.times do
  # Resolve in parent (or re-resolve — each trial is a fresh getaddrinfo call)
  Addrinfo.getaddrinfo('httpbin.org', 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)

  result = fork_test do
    Timeout.timeout(5) do
      Addrinfo.getaddrinfo('httpbin.org', 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)
    end
    'OK'
  rescue Timeout::Error
    'DEADLOCK'
  end

  dl = result == 'DEADLOCK'
  deadlocks += 1 if dl
  $stdout.write(dl ? 'X' : '.')
  $stdout.flush
end

puts
puts "  Result: #{deadlocks}/#{NUM_TRIALS} deadlocked"
puts

# Test 2: Increase probability with more DNS activity — resolve multiple hosts before forking
puts "--- Test 2: Resolve multiple hosts first, then fork — #{NUM_TRIALS} trials ---"
puts

deadlocks2 = 0
NUM_TRIALS.times do
  # Resolve several hosts (mix of IPv4-only and dual-stack) to increase
  # mDNSResponder internal state complexity
  %w[httpbin.org www.github.com api.github.com www.google.com rubygems.org
     example.com www.cloudflare.com stackoverflow.com].each do |h|
    Addrinfo.getaddrinfo(h, 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)
  rescue StandardError
    nil
  end

  result = fork_test do
    Timeout.timeout(5) do
      Addrinfo.getaddrinfo('httpbin.org', 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)
    end
    'OK'
  rescue Timeout::Error
    'DEADLOCK'
  end

  dl = result == 'DEADLOCK'
  deadlocks2 += 1 if dl
  $stdout.write(dl ? 'X' : '.')
  $stdout.flush
end

puts
puts "  Result: #{deadlocks2}/#{NUM_TRIALS} deadlocked"
puts

# Test 3: Control — dual-stack host (www.google.com has AAAA records)
puts "--- Test 3: Control — dual-stack host (www.google.com) — #{NUM_TRIALS} trials ---"
puts

deadlocks3 = 0
NUM_TRIALS.times do
  %w[httpbin.org www.github.com www.google.com rubygems.org].each do |h|
    Addrinfo.getaddrinfo(h, 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)
  rescue StandardError
    nil
  end

  result = fork_test do
    Timeout.timeout(5) do
      Addrinfo.getaddrinfo('www.google.com', 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)
    end
    'OK'
  rescue Timeout::Error
    'DEADLOCK'
  end

  dl = result == 'DEADLOCK'
  deadlocks3 += 1 if dl
  $stdout.write(dl ? 'X' : '.')
  $stdout.flush
end

puts
puts "  Result: #{deadlocks3}/#{NUM_TRIALS} deadlocked"
puts

# Test 4: Workaround — AF_INET avoids the deadlock
puts "--- Test 4: Workaround — AF_INET instead of AF_UNSPEC — #{NUM_TRIALS} trials ---"
puts

deadlocks4 = 0
NUM_TRIALS.times do
  # Use AF_UNSPEC first
  %w[httpbin.org www.github.com api.github.com].each do |h|
    Addrinfo.getaddrinfo(h, 'https', Socket::AF_UNSPEC, Socket::SOCK_STREAM)
  rescue StandardError
    nil
  end

  result = fork_test do
    Timeout.timeout(5) do
      # Use AF_INET instead of AF_UNSPEC
      Addrinfo.getaddrinfo('httpbin.org', 'https', Socket::AF_INET, Socket::SOCK_STREAM)
    end
    'OK'
  rescue Timeout::Error
    'DEADLOCK'
  end

  dl = result == 'DEADLOCK'
  deadlocks4 += 1 if dl
  $stdout.write(dl ? 'X' : '.')
  $stdout.flush
end

puts
puts "  Result: #{deadlocks4}/#{NUM_TRIALS} deadlocked"
puts

# Summary
puts '=' * 70
puts 'SUMMARY'
puts '=' * 70
puts
puts "  Test 1 (single IPv4-only host):     #{deadlocks}/#{NUM_TRIALS} deadlocked"
puts "  Test 2 (multi-host warmup):         #{deadlocks2}/#{NUM_TRIALS} deadlocked"
puts "  Test 3 (dual-stack host control):   #{deadlocks3}/#{NUM_TRIALS} deadlocked"
puts "  Test 4 (AF_INET workaround):        #{deadlocks4}/#{NUM_TRIALS} deadlocked"
puts

if (deadlocks + deadlocks2).positive? && (deadlocks3 + deadlocks4).zero?
  puts 'BUG CONFIRMED: getaddrinfo(AF_UNSPEC) deadlocks after fork for'
  puts 'IPv4-only hosts. Dual-stack hosts and AF_INET are not affected.'
elsif (deadlocks + deadlocks2).positive?
  puts 'BUG REPRODUCED with some anomalies — see individual results.'
else
  puts 'Bug did not reproduce in this run. Try running again — the issue'
  puts 'is probabilistic and depends on mDNSResponder internal timing.'
end
