#!/usr/bin/ruby
require 'json'
require 'open3'
prelaunch_simulator = ARGV[0] || ''
def platform_for_runtime(runtime)
runtime['identifier'].gsub(/com.apple.CoreSimulator.SimRuntime.([^-]+)-.*/, '\1')
end
def platform_for_device_type(device_type)
case device_type['identifier']
when /Watch/
'watchOS'
when /TV/
'tvOS'
else
'iOS'
end
end
def simctl(args)
# When running on a machine with Xcode 11 installed, Xcode 10 sometimes
# incorrectly thinks that it has not completed its first-run installation.
# This results in it printing errors related to that to stdout in front of
# the actual JSON output that we want.
Open3.popen3('xcrun simctl ' + args) do |stdin, stdout, strerr, wait_thr|
while line = stdout.gets
if not line.start_with? 'Install'
return line + stdout.read, wait_thr.value.exitstatus
end
end
end
end
def wait_for_core_simulator_service
# Run until we get a result since switching simulator versions often causes CoreSimulatorService to throw an exception.
while simctl('list devices')[0].empty?
end
end
def running_devices(devices)
devices.select { |device| device['state'] != 'Shutdown' }
end
def shutdown_simulator_devices(devices)
# Shut down any simulators that need it.
running_devices(devices).each do |device|
puts "Shutting down simulator #{device['udid']}"
system("xcrun simctl shutdown #{device['udid']}") or puts " Failed to shut down simulator #{device['udid']}"
end
end
attempts = 0
begin
# Kill all the current simulator processes as they may be from a different Xcode version
print 'Killing running Simulator processes...'
while system('pgrep -q Simulator')
system('pkill Simulator 2>/dev/null')
system('pkill -9 update_dyld_sim_shared_cache 2>/dev/null')
# CoreSimulatorService doesn't exit when sent SIGTERM
system('pkill -9 Simulator 2>/dev/null')
end
wait_for_core_simulator_service
puts ' done!'
print 'Shut down existing simulator devices...'
# Shut down any running simulator devices. This may take multiple attempts if some
# simulators are currently in the process of booting or being created.
all_available_devices = []
(0..5).each do |shutdown_attempt|
begin
devices_json = simctl('list devices -j')[0]
all_devices = JSON.parse(devices_json)['devices'].flat_map { |_, devices| devices }
rescue JSON::ParserError
sleep shutdown_attempt if shutdown_attempt > 0
next
end
# Exclude devices marked as unavailable as they're from a different version of Xcode.
all_available_devices = all_devices.reject { |device| device['availability'] =~ /unavailable/ }
break if running_devices(all_available_devices).empty?
shutdown_simulator_devices all_available_devices
sleep shutdown_attempt if shutdown_attempt > 0
end
puts ' done!'
# Delete all simulators.
print 'Deleting all simulators...'
(0..5).each do |delete_attempt|
break if all_available_devices.empty?
all_available_devices.each do |device|
simctl("delete #{device['udid']}")
end
begin
devices_json = simctl('list devices -j')[0]
all_devices = JSON.parse(devices_json)['devices'].flat_map { |_, devices| devices }
rescue JSON::ParserError
sleep shutdown_attempt if shutdown_attempt > 0
next
end
all_available_devices = all_devices.reject { |device| device['availability'] =~ /unavailable/ }
break if all_available_devices.empty?
end
puts ' done!'
if not all_available_devices.empty?
raise "Failed to delete devices #{all_available_devices}"
end
# Recreate all simulators.
runtimes = JSON.parse(simctl('list runtimes -j')[0])['runtimes']
device_types = JSON.parse(simctl('list devicetypes -j')[0])['devicetypes']
runtimes_by_platform = Hash.new { |hash, key| hash[key] = [] }
runtimes.each do |runtime|
next unless runtime['availability'] == '(available)' || runtime['isAvailable'] == true
runtimes_by_platform[platform_for_runtime(runtime)] << runtime
end
firstOnly = prelaunch_simulator == '-firstOnly'
print 'Creating fresh simulators...'
device_types.each do |device_type|
platform = platform_for_device_type(device_type)
runtimes_by_platform[platform].each do |runtime|
output, ec = simctl("create '#{device_type['name']}' '#{device_type['identifier']}' '#{runtime['identifier']}' 2>&1")
if ec == 0
if firstOnly
# We only want to create a single simulator for each device type so
# skip the rest.
runtimes_by_platform[platform] = []
break
else
next
end
end
# Not all runtime and device pairs are valid as newer simulator runtimes
# don't support older devices. The exact error code for this changes
# every few versions of Xcode, so this just lists all the ones we've
# seen.
next if /domain=com.apple.CoreSimulator.SimError, code=(?<code>\d+)/ =~ output and [161, 162, 163, 403].include? code.to_i
puts "Failed to create device of type #{device_type['identifier']} with runtime #{runtime['identifier']}:"
output.each_line do |line|
puts " #{line}"
end
end
end
puts ' done!'
if firstOnly
exit 0
end
if prelaunch_simulator.include? 'tvos'
print 'Booting Apple TV simulator...'
system("xcrun simctl boot 'Apple TV'") or raise "Failed to boot Apple TV simulator"
else
print 'Booting iPhone 8 simulator...'
system("xcrun simctl boot 'iPhone 8'") or raise "Failed to boot iPhone 8 simulator"
end
puts ' done!'
print 'Waiting for dyld shared cache to update...'
10.times do
break unless system('pgrep -q update_dyld_sim_shared_cache')
sleep 15
end
puts ' done!'
rescue => e
if (attempts += 1) < 5
puts ''
puts e.message
e.backtrace.each { |line| puts line }
puts ''
puts 'Retrying...'
retry
end
system('ps auxwww')
system('xcrun simctl list')
raise
end