Skip to content

Commit a550e79

Browse files
authored
feat: add directConnect feature (#627)
* add idea based on 5.0.0 * ok proto * define a class * add AppiumClientConfig * add tests * add more drivers * extract as a private method * fix type * tweak comment * Update DirectConnect.cs * Update AppiumCommandExecutor.cs
1 parent 04eac27 commit a550e79

11 files changed

+564
-6
lines changed

src/Appium.Net/Appium/Android/AndroidDriver.cs

+48
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,54 @@ public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions,
127127
{
128128
}
129129

130+
131+
/// <summary>
132+
/// Initializes a new instance of the AndroidDriver class using the specified remote address, Appium options and AppiumClientConfig.
133+
/// </summary>
134+
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
135+
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
136+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
137+
public AndroidDriver(Uri remoteAddress, DriverOptions driverOptions, AppiumClientConfig clientConfig)
138+
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
139+
{
140+
}
141+
142+
/// <summary>
143+
/// Initializes a new instance of the AndroidDriver class using the specified Appium local service, Appium options and AppiumClientConfig,
144+
/// </summary>
145+
/// <param name="service">the specified Appium local service</param>
146+
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
147+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
148+
public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions, AppiumClientConfig clientConfig)
149+
: base(service, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
150+
{
151+
}
152+
153+
/// <summary>
154+
/// Initializes a new instance of the AndroidDriver class using the specified remote address, Appium options, command timeout and AppiumClientConfig.
155+
/// </summary>
156+
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
157+
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
158+
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
159+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
160+
public AndroidDriver(Uri remoteAddress, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
161+
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
162+
{
163+
}
164+
165+
/// <summary>
166+
/// Initializes a new instance of the AndroidDriver class using the specified Appium local service, Appium options, command timeout and AppiumClientConfig,
167+
/// </summary>
168+
/// <param name="service">the specified Appium local service</param>
169+
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
170+
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
171+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
172+
public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
173+
: base(service, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
174+
{
175+
}
176+
177+
130178
public void StartActivity(string appPackage, string appActivity, string appWaitPackage = "",
131179
string appWaitActivity = "", bool stopApp = true) =>
132180
AndroidCommandExecutionHelper.StartActivity(this, appPackage, appActivity, appWaitPackage, appWaitActivity,

src/Appium.Net/Appium/AppiumDriver.cs

+24-2
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,37 @@ public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions)
7575
}
7676

7777
public AppiumDriver(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
78-
: this(new AppiumCommandExecutor(remoteAddress, commandTimeout), appiumOptions)
78+
: this(remoteAddress, appiumOptions, DefaultCommandTimeout, AppiumClientConfig.DefaultConfig())
7979
{
8080
}
8181

8282
public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, TimeSpan commandTimeout)
83-
: this(new AppiumCommandExecutor(service, commandTimeout), appiumOptions)
83+
: this(service, appiumOptions, DefaultCommandTimeout, AppiumClientConfig.DefaultConfig())
8484
{
8585
}
8686

87+
88+
public AppiumDriver(Uri remoteAddress, ICapabilities appiumOptions, AppiumClientConfig clientConfig)
89+
: this(new AppiumCommandExecutor(remoteAddress, DefaultCommandTimeout, clientConfig), appiumOptions)
90+
{
91+
}
92+
93+
public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, AppiumClientConfig clientConfig)
94+
: this(new AppiumCommandExecutor(service, DefaultCommandTimeout, clientConfig), appiumOptions)
95+
{
96+
}
97+
98+
public AppiumDriver(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
99+
: this(new AppiumCommandExecutor(remoteAddress, commandTimeout, clientConfig), appiumOptions)
100+
{
101+
}
102+
103+
public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
104+
: this(new AppiumCommandExecutor(service, commandTimeout, clientConfig), appiumOptions)
105+
{
106+
}
107+
108+
87109
#endregion Constructors
88110

89111
#region Public Methods

src/Appium.Net/Appium/Mac/MacDriver.cs

+46
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,51 @@ public MacDriver(AppiumLocalService service, AppiumOptions AppiumOptions, TimeSp
113113
: base(service, SetPlatformToCapabilities(AppiumOptions, Platform), commandTimeout)
114114
{
115115
}
116+
117+
/// <summary>
118+
/// Initializes a new instance of the MacDriver class using the specified remote address, Appium options and AppiumClientConfig.
119+
/// </summary>
120+
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
121+
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
122+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
123+
public MacDriver(Uri remoteAddress, DriverOptions driverOptions, AppiumClientConfig clientConfig)
124+
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
125+
{
126+
}
127+
128+
/// <summary>
129+
/// Initializes a new instance of the MacDriver class using the specified Appium local service, Appium options and AppiumClientConfig,
130+
/// </summary>
131+
/// <param name="service">the specified Appium local service</param>
132+
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
133+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
134+
public MacDriver(AppiumLocalService service, DriverOptions driverOptions, AppiumClientConfig clientConfig)
135+
: base(service, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
136+
{
137+
}
138+
139+
/// <summary>
140+
/// Initializes a new instance of the MacDriver class using the specified remote address, Appium options, command timeout and AppiumClientConfig.
141+
/// </summary>
142+
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
143+
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
144+
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
145+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
146+
public MacDriver(Uri remoteAddress, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
147+
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
148+
{
149+
}
150+
151+
/// <summary>
152+
/// Initializes a new instance of the MacDriver class using the specified Appium local service, Appium options, command timeout and AppiumClientConfig,
153+
/// </summary>
154+
/// <param name="service">the specified Appium local service</param>
155+
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
156+
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
157+
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
158+
public MacDriver(AppiumLocalService service, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
159+
: base(service, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
160+
{
161+
}
116162
}
117163
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//Licensed under the Apache License, Version 2.0 (the "License");
2+
//you may not use this file except in compliance with the License.
3+
//See the NOTICE file distributed with this work for additional
4+
//information regarding copyright ownership.
5+
//You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
//Unless required by applicable law or agreed to in writing, software
10+
//distributed under the License is distributed on an "AS IS" BASIS,
11+
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
//See the License for the specific language governing permissions and
13+
//limitations under the License.
14+
15+
namespace OpenQA.Selenium.Appium.Service
16+
{
17+
18+
public class AppiumClientConfig
19+
{
20+
/// <summary>
21+
/// Return the default Appium Client Config
22+
/// </summary>
23+
/// <returns>An AppiumClientConfig instance</returns>
24+
public static AppiumClientConfig DefaultConfig()
25+
{
26+
return new AppiumClientConfig();
27+
}
28+
29+
/// <summary>
30+
/// Gets or sets the directConnect feature availability.
31+
/// If this flag is true and the target server supports
32+
/// https://appiumpro.com/editions/86-connecting-directly-to-appium-hosts-in-distributed-environments,
33+
/// the AppiumCommandExecutor will follow the response directConnect direction.
34+
///
35+
/// AppiumClientConfig clientConfig = AppiumClientConfig.DefaultConfig();
36+
/// clientConfig.DirectConnect = true;
37+
///
38+
/// </summary>
39+
public bool DirectConnect { get; set; }
40+
}
41+
}

src/Appium.Net/Appium/Service/AppiumCommandExecutor.cs

+58-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
//See the License for the specific language governing permissions and
1313
//limitations under the License.
1414

15+
using Newtonsoft.Json;
1516
using OpenQA.Selenium.Remote;
1617
using System;
18+
using System.Collections.Generic;
1719

1820
namespace OpenQA.Selenium.Appium.Service
1921
{
@@ -23,6 +25,9 @@ internal class AppiumCommandExecutor : ICommandExecutor
2325
private ICommandExecutor RealExecutor;
2426
private bool isDisposed;
2527
private const string IdempotencyHeader = "X-Idempotency-Key";
28+
private AppiumClientConfig ClientConfig;
29+
30+
private TimeSpan CommandTimeout;
2631

2732
private static ICommandExecutor CreateRealExecutor(Uri remoteAddress, TimeSpan commandTimeout)
2833
{
@@ -34,16 +39,20 @@ private AppiumCommandExecutor(ICommandExecutor realExecutor)
3439
RealExecutor = realExecutor;
3540
}
3641

37-
internal AppiumCommandExecutor(Uri url, TimeSpan timeForTheServerResponding)
42+
internal AppiumCommandExecutor(Uri url, TimeSpan timeForTheServerResponding, AppiumClientConfig clientConfig)
3843
: this(CreateRealExecutor(url, timeForTheServerResponding))
3944
{
45+
CommandTimeout = timeForTheServerResponding;
4046
Service = null;
47+
ClientConfig = clientConfig;
4148
}
4249

43-
internal AppiumCommandExecutor(AppiumLocalService service, TimeSpan timeForTheServerResponding)
50+
internal AppiumCommandExecutor(AppiumLocalService service, TimeSpan timeForTheServerResponding, AppiumClientConfig clientConfig)
4451
: this(CreateRealExecutor(service.ServiceUrl, timeForTheServerResponding))
4552
{
53+
CommandTimeout = timeForTheServerResponding;
4654
Service = service;
55+
ClientConfig = clientConfig;
4756
}
4857

4958
public Response Execute(Command commandToExecute)
@@ -56,9 +65,15 @@ public Response Execute(Command commandToExecute)
5665
{
5766
Service?.Start();
5867
RealExecutor = ModifyNewSessionHttpRequestHeader(RealExecutor);
68+
69+
result = RealExecutor.Execute(commandToExecute);
70+
RealExecutor = UpdateExecutor(result, RealExecutor);
71+
}
72+
else
73+
{
74+
result = RealExecutor.Execute(commandToExecute);
5975
}
6076

61-
result = RealExecutor.Execute(commandToExecute);
6277
return result;
6378
}
6479
catch (Exception e)
@@ -89,11 +104,50 @@ private ICommandExecutor ModifyNewSessionHttpRequestHeader(ICommandExecutor comm
89104
{
90105
if (commandExecutor == null) throw new ArgumentNullException(nameof(commandExecutor));
91106
var modifiedCommandExecutor = commandExecutor as HttpCommandExecutor;
107+
92108
modifiedCommandExecutor.SendingRemoteHttpRequest += (sender, args) =>
93109
args.AddHeader(IdempotencyHeader, Guid.NewGuid().ToString());
110+
94111
return modifiedCommandExecutor;
95112
}
96113

114+
115+
/// <summary>
116+
/// Return an instance of AppiumCommandExecutor.
117+
/// If the executor can use as-is, this method will return the given executor without any updates.
118+
/// </summary>
119+
/// <param name="result">The result of the command execution.</param>
120+
/// <param name="currentExecutor">Current ICommandExecutor instance.</param>
121+
/// <returns>A ICommandExecutor instance</returns>
122+
private ICommandExecutor UpdateExecutor(Response result, ICommandExecutor currentExecutor)
123+
{
124+
if (ClientConfig.DirectConnect == false) {
125+
return currentExecutor;
126+
}
127+
128+
var newExecutor = GetNewExecutorWithDirectConnect(result);
129+
if (newExecutor == null) {
130+
return currentExecutor;
131+
}
132+
133+
return newExecutor;
134+
}
135+
136+
/// <summary>
137+
/// Returns a new command executor if the response had directConnect.
138+
/// </summary>
139+
/// <param name="result">The result of the command execution.</param>
140+
/// <returns>A ICommandExecutor instance or null</returns>
141+
private ICommandExecutor GetNewExecutorWithDirectConnect(Response response)
142+
{
143+
var newUri = new DirectConnect(response).GetUri();
144+
if (newUri != null) {
145+
return new HttpCommandExecutor(newUri, CommandTimeout);
146+
}
147+
148+
return null;
149+
}
150+
97151
public void Dispose() => Dispose(true);
98152

99153
protected void Dispose(bool disposing)
@@ -114,4 +168,4 @@ public bool TryAddCommand(string commandName, CommandInfo info)
114168
return this.RealExecutor.TryAddCommand(commandName, info);
115169
}
116170
}
117-
}
171+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//Licensed under the Apache License, Version 2.0 (the "License");
2+
//you may not use this file except in compliance with the License.
3+
//See the NOTICE file distributed with this work for additional
4+
//information regarding copyright ownership.
5+
//You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
//Unless required by applicable law or agreed to in writing, software
10+
//distributed under the License is distributed on an "AS IS" BASIS,
11+
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
//See the License for the specific language governing permissions and
13+
//limitations under the License.
14+
15+
using System;
16+
using System.Collections.Generic;
17+
18+
namespace OpenQA.Selenium.Appium.Service
19+
{
20+
public class DirectConnect
21+
{
22+
private const string DIRECT_CONNECT_PROTOCOL = "directConnectProtocol";
23+
private const string DIRECT_CONNECT_HOST = "directConnectHost";
24+
private const string DIRECT_CONNECT_PORT = "directConnectPort";
25+
private const string DIRECT_CONNECT_PATH = "directConnectPath";
26+
27+
private readonly string Protocol;
28+
private readonly string Host;
29+
private readonly string Port;
30+
private readonly string Path;
31+
32+
33+
/// <summary>
34+
/// Create a direct connect instance from the given received response.
35+
/// </summary>
36+
public DirectConnect(Response response)
37+
{
38+
39+
this.Protocol = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_PROTOCOL);
40+
this.Host = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_HOST);
41+
this.Port = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_PORT);
42+
this.Path = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_PATH);
43+
}
44+
45+
/// <summary>
46+
/// Returns a URL instance built with members in the DirectConnect instance.
47+
/// </summary>
48+
/// <returns>A Uri instance</returns>
49+
public Uri GetUri() {
50+
if (this.Protocol == null || this.Host == null || this.Port == null || this.Path == null) {
51+
return null;
52+
}
53+
54+
if (this.Protocol != "https")
55+
{
56+
return null;
57+
}
58+
59+
return new Uri(this.Protocol + "://" + this.Host + ":" + this.Port + this.Path);
60+
}
61+
62+
/// <summary>
63+
/// Returns a value of instance built with members in the DirectConnect instance.
64+
/// </summary>
65+
/// <param name="value">The value of the 'value' key in the response body.</param>
66+
/// <param name="keyName">The key name to get the value.</param>
67+
/// <returns>A string value or null</returns>
68+
private string GetDirectConnectValue(Dictionary<string, object> value, string keyName)
69+
{
70+
if (value.ContainsKey("appium:" + keyName))
71+
{
72+
return value["appium:" + keyName].ToString();
73+
}
74+
75+
if (value.ContainsKey(keyName)) {
76+
return value[keyName].ToString();
77+
}
78+
79+
return null;
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)