From c669818cd08279125fd615f2958af0444c93233d Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 19 Apr 2026 11:53:40 -0700 Subject: [PATCH] chore: add timeout and ip tune up --- lib/appium_lib_core/driver.rb | 22 +++++++++++++++--- test/unit/driver_test.rb | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/appium_lib_core/driver.rb b/lib/appium_lib_core/driver.rb index 65833129..15f1cfb6 100644 --- a/lib/appium_lib_core/driver.rb +++ b/lib/appium_lib_core/driver.rb @@ -15,6 +15,7 @@ require 'uri' require 'socket' require 'ipaddr' +require 'timeout' module Appium # The struct for 'location' @@ -115,6 +116,7 @@ def valid? # Do not allow loopback, link-local, unspecified and multicast addresses for # direct connect since they are not accessible from outside of the server. + DNS_RESOLVE_TIMEOUT_SECONDS = 30 LOOPBACK_RANGES = [IPAddr.new('127.0.0.0/8'), IPAddr.new('::1/128')].freeze LINK_LOCAL_RANGES = [IPAddr.new('169.254.0.0/16'), IPAddr.new('fe80::/10')].freeze UNSPECIFIED_RANGES = [IPAddr.new('0.0.0.0/32'), IPAddr.new('::/128')].freeze @@ -128,13 +130,27 @@ def valid? def resolve_addresses(host) normalized_host = host.to_s.delete_prefix('[').delete_suffix(']') - Socket.getaddrinfo(normalized_host, nil).map { |entry| entry[3] }.uniq + # If the host is already an IP literal, skip DNS resolution to avoid blocking calls + return [normalized_host] if ip_literal?(normalized_host) + + Timeout.timeout(DNS_RESOLVE_TIMEOUT_SECONDS) do + Socket.getaddrinfo(normalized_host, nil).map { |entry| entry[3] }.uniq + end + rescue Timeout::Error + ::Appium::Logger.warn("DNS resolution for '#{host}' timed out after #{DNS_RESOLVE_TIMEOUT_SECONDS}s") + [] rescue SocketError => e - error_message = "Failed to resolve host '#{host}' for direct connect: #{e.message}" - ::Appium::Logger.warn(error_message) + ::Appium::Logger.warn("Failed to resolve host '#{host}' for direct connect: #{e.message}") [] end + def ip_literal?(host) + IPAddr.new(host) + true + rescue IPAddr::InvalidAddressError + false + end + def disallowed?(ip) address = IPAddr.new ip DISALLOWED_RANGES.any? { |range| range.include? address } diff --git a/test/unit/driver_test.rb b/test/unit/driver_test.rb index 91a39b42..2cd058c8 100644 --- a/test/unit/driver_test.rb +++ b/test/unit/driver_test.rb @@ -199,6 +199,7 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix appActivity: 'io.appium.android.apis.ApiDemos', someCapability: 'some_capability', 'appium:directConnectProtocol' => 'https', + # Not the best, but to include tests with host name 'appium:directConnectHost' => 'appium.io', 'appium:directConnectPort' => '8888', 'appium:directConnectPath' => '/wd/hub' @@ -331,6 +332,48 @@ def test_direct_connect_loopback_host_falls_back_to_original_url assert_not_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts') end + def test_direct_connect_dns_timeout_falls_back_to_original_url + response = { + value: { + sessionId: '1234567890', + capabilities: { + platformName: :android, + automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2', + app: 'test/functional/app/ApiDemos-debug.apk', + platformVersion: '7.1.1', + deviceName: 'Android Emulator', + appPackage: 'io.appium.android.apis', + appActivity: 'io.appium.android.apis.ApiDemos', + someCapability: 'some_capability', + directConnectProtocol: 'http', + directConnectHost: 'slow.example.internal', + directConnectPort: '8888', + directConnectPath: '/wd/hub' + } + } + }.to_json + + stub_request(:post, 'http://127.0.0.1:4723/session') + .to_return(headers: HEADER, status: 200, body: response) + + stub_request(:post, 'http://127.0.0.1:4723/session/1234567890/timeouts') + .with(body: { implicit: 30_000 }.to_json) + .to_return(headers: HEADER, status: 200, body: { value: nil }.to_json) + + Socket.stub(:getaddrinfo, ->(*) { raise Timeout::Error }) do + core = ::Appium::Core.for(Caps.android_direct) + driver = core.start_driver + + uri = driver.send(:bridge).http.send(:server_url) + assert_equal 'http', uri.scheme + assert_equal '127.0.0.1', uri.host + assert_equal 4723, uri.port + + assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1) + assert_not_requested(:post, 'http://slow.example.internal:8888/wd/hub/session/1234567890/timeouts') + end + end + def test_default_timeout_for_http_client_with_direct_no_path android_mock_create_session_w3c_direct_no_path = lambda do |core| response = {