lib/run_loop/device.rb in run_loop-1.5.1 vs lib/run_loop/device.rb in run_loop-1.5.2

- old
+ new

@@ -101,13 +101,13 @@ end # @!visibility private def to_s if simulator? - "#<Simulator: #{name} #{udid} #{instruction_set}>" + "#<Simulator: #{name} (#{version.to_s}) #{udid} #{instruction_set}>" else - "#<Device: #{name} #{udid}>" + "#<Device: #{name} (#{version.to_s}) #{udid}>" end end # @!visibility private def inspect @@ -186,40 +186,256 @@ else raise 'Finding the instruction set of a device requires a third-party tool like ideviceinfo' end end + # @!visibility private + # The device `state` is reported by the simctl tool. + # + # The expected values from simctl are: + # + # * Booted + # * Shutdown + # * Shutting Down + # + # To handle exceptional cases, there are these two additional states: + # + # * Unavailable # Should never occur + # * Unknown # A stub for future changes + def update_simulator_state + if physical_device? + raise RuntimeError, 'This method is available only for simulators' + end + + @state = fetch_simulator_state + end + + # @!visibility private def simulator_root_dir @simulator_root_dir ||= lambda { return nil if physical_device? File.join(CORE_SIMULATOR_DEVICE_DIR, udid) }.call end + # @!visibility private def simulator_accessibility_plist_path @simulator_accessibility_plist_path ||= lambda { return nil if physical_device? File.join(simulator_root_dir, 'data/Library/Preferences/com.apple.Accessibility.plist') }.call end + # @!visibility private def simulator_preferences_plist_path @simulator_preferences_plist_path ||= lambda { return nil if physical_device? File.join(simulator_root_dir, 'data/Library/Preferences/com.apple.Preferences.plist') }.call end + # @!visibility private def simulator_log_file_path @simulator_log_file_path ||= lambda { return nil if physical_device? File.join(CORE_SIMULATOR_LOGS_DIR, udid, 'system.log') }.call end + # @!visibility private + def simulator_device_plist + @simulator_device_plist ||= lambda do + return nil if physical_device? + File.join(simulator_root_dir, 'device.plist') + end.call + end + + # @!visibility private + # Is this the first launch of this Simulator? + # + # TODO Needs unit and integration tests. + def simulator_first_launch? + megabytes = simulator_data_dir_size + + if version >= RunLoop::Version.new('9.0') + megabytes < 20 + elsif version >= RunLoop::Version.new('8.0') + megabytes < 12 + else + megabytes < 8 + end + end + + # @!visibility private + # The size of the simulator data/ directory. + # + # TODO needs unit tests. + def simulator_data_dir_size + path = File.join(simulator_root_dir, 'data') + args = ['du', '-m', '-d', '0', path] + hash = xcrun.exec(args) + hash[:out].split(' ').first.to_i + end + + # @!visibility private + # + # Waits for three conditions: + # + # 1. The SHA sum of the simulator data/ directory to be stable. + # 2. No more log messages are begin generated + # 3. 1 and 2 must hold for 2 seconds. + # + # When the simulator version is >= iOS 9 _and_ it is the first launch of + # the simulator after a reset or a new simulator install, a fourth condition + # is added: + # + # 4. The first three conditions must be met a second time. + def simulator_wait_for_stable_state + require 'securerandom' + + quiet_time = 2 + delay = 0.5 + + first_launch = false + + if version >= RunLoop::Version.new('9.0') + first_launch = simulator_data_dir_size < 20 + end + + now = Time.now + timeout = 30 + poll_until = now + timeout + quiet = now + quiet_time + + is_stable = false + + path = File.join(simulator_root_dir, 'data') + current_sha = nil + sha_fn = lambda do |data_dir| + begin + # Directory.directory_digest has a blocking read. Typically, it + # returns in < 0.3 seconds. + Timeout.timeout(2, TimeoutError) do + RunLoop::Directory.directory_digest(data_dir) + end + rescue => e + RunLoop.log_error(e) if RunLoop::Environment.debug? + SecureRandom.uuid + end + end + + current_line = nil + + while Time.now < poll_until do + latest_sha = sha_fn.call(path) + latest_line = last_line_from_simulator_log_file + + is_stable = current_sha == latest_sha && current_line == latest_line + + if is_stable + if Time.now > quiet + if first_launch + RunLoop.log_debug('First launch detected - allowing additional time to stabilize') + first_launch = false + sleep 1.2 + quiet = Time.now + quiet_time + else + break + end + else + quiet = Time.now + quiet_time + end + end + + current_sha = latest_sha + current_line = latest_line + sleep delay + end + + if is_stable + elapsed = Time.now - now + stabilized = elapsed - quiet_time + RunLoop.log_debug("Simulator stable after #{stabilized} seconds") + RunLoop.log_debug("Waited a total of #{elapsed} seconds for simulator to stabilize") + else + RunLoop.log_debug("Timed out: simulator not stable after #{timeout} seconds") + end + end + private + # @!visibility private + # TODO write a unit test. + def last_line_from_simulator_log_file + file = simulator_log_file_path + + return nil if !File.exist?(file) + + debug = RunLoop::Environment.debug? + + begin + io = File.open(file, 'r') + io.seek(-100, IO::SEEK_END) + + line = io.readline + rescue StandardError => e + RunLoop.log_error("Caught #{e} while reading simulator log file") if debug + ensure + io.close if io && !io.closed? + end + + line + end + + # @!visibility private + def xcrun + RunLoop::Xcrun.new + end + + # @!visibility private + def detect_state_from_line(line) + + if line[/unavailable/, 0] + RunLoop.log_debug("Simulator state is unavailable: #{line}") + return 'Unavailable' + end + + state = line[/(Booted|Shutdown|Shutting Down)/,0] + + if state.nil? + RunLoop.log_debug("Simulator state is unknown: #{line}") + 'Unknown' + else + state + end + end + + # @!visibility private + def fetch_simulator_state + if physical_device? + raise RuntimeError, 'This method is available only for simulators' + end + + args = ['simctl', 'list', 'devices'] + hash = xcrun.exec(args) + out = hash[:out] + + matched_line = out.split("\n").find do |line| + line.include?(udid) + end + + if matched_line.nil? + raise RuntimeError, + "Expected a simulator with udid '#{udid}', but found none" + end + + detect_state_from_line(matched_line) + end + + # @!visibility private CORE_SIMULATOR_DEVICE_DIR = File.expand_path('~/Library/Developer/CoreSimulator/Devices') + + # @!visibility private CORE_SIMULATOR_LOGS_DIR = File.expand_path('~/Library/Logs/CoreSimulator') # TODO Is this a good idea? It speeds up rspec tests by a factor of ~2x... SIM_CONTROL = RunLoop::SimControl.new end