Skip to content

Commit

Permalink
feat: add directConnect feature (#627)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
KazuCocoa authored Aug 12, 2023
1 parent 04eac27 commit a550e79
Show file tree
Hide file tree
Showing 11 changed files with 564 additions and 6 deletions.
48 changes: 48 additions & 0 deletions src/Appium.Net/Appium/Android/AndroidDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,54 @@ public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions,
{
}


/// <summary>
/// Initializes a new instance of the AndroidDriver class using the specified remote address, Appium options and AppiumClientConfig.
/// </summary>
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public AndroidDriver(Uri remoteAddress, DriverOptions driverOptions, AppiumClientConfig clientConfig)
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
{
}

/// <summary>
/// Initializes a new instance of the AndroidDriver class using the specified Appium local service, Appium options and AppiumClientConfig,
/// </summary>
/// <param name="service">the specified Appium local service</param>
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions, AppiumClientConfig clientConfig)
: base(service, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
{
}

/// <summary>
/// Initializes a new instance of the AndroidDriver class using the specified remote address, Appium options, command timeout and AppiumClientConfig.
/// </summary>
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public AndroidDriver(Uri remoteAddress, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
{
}

/// <summary>
/// Initializes a new instance of the AndroidDriver class using the specified Appium local service, Appium options, command timeout and AppiumClientConfig,
/// </summary>
/// <param name="service">the specified Appium local service</param>
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
: base(service, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
{
}


public void StartActivity(string appPackage, string appActivity, string appWaitPackage = "",
string appWaitActivity = "", bool stopApp = true) =>
AndroidCommandExecutionHelper.StartActivity(this, appPackage, appActivity, appWaitPackage, appWaitActivity,
Expand Down
26 changes: 24 additions & 2 deletions src/Appium.Net/Appium/AppiumDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,37 @@ public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions)
}

public AppiumDriver(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
: this(new AppiumCommandExecutor(remoteAddress, commandTimeout), appiumOptions)
: this(remoteAddress, appiumOptions, DefaultCommandTimeout, AppiumClientConfig.DefaultConfig())
{
}

public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, TimeSpan commandTimeout)
: this(new AppiumCommandExecutor(service, commandTimeout), appiumOptions)
: this(service, appiumOptions, DefaultCommandTimeout, AppiumClientConfig.DefaultConfig())
{
}


public AppiumDriver(Uri remoteAddress, ICapabilities appiumOptions, AppiumClientConfig clientConfig)
: this(new AppiumCommandExecutor(remoteAddress, DefaultCommandTimeout, clientConfig), appiumOptions)
{
}

public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, AppiumClientConfig clientConfig)
: this(new AppiumCommandExecutor(service, DefaultCommandTimeout, clientConfig), appiumOptions)
{
}

public AppiumDriver(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
: this(new AppiumCommandExecutor(remoteAddress, commandTimeout, clientConfig), appiumOptions)
{
}

public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
: this(new AppiumCommandExecutor(service, commandTimeout, clientConfig), appiumOptions)
{
}


#endregion Constructors

#region Public Methods
Expand Down
46 changes: 46 additions & 0 deletions src/Appium.Net/Appium/Mac/MacDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,51 @@ public MacDriver(AppiumLocalService service, AppiumOptions AppiumOptions, TimeSp
: base(service, SetPlatformToCapabilities(AppiumOptions, Platform), commandTimeout)
{
}

/// <summary>
/// Initializes a new instance of the MacDriver class using the specified remote address, Appium options and AppiumClientConfig.
/// </summary>
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public MacDriver(Uri remoteAddress, DriverOptions driverOptions, AppiumClientConfig clientConfig)
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
{
}

/// <summary>
/// Initializes a new instance of the MacDriver class using the specified Appium local service, Appium options and AppiumClientConfig,
/// </summary>
/// <param name="service">the specified Appium local service</param>
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public MacDriver(AppiumLocalService service, DriverOptions driverOptions, AppiumClientConfig clientConfig)
: base(service, SetPlatformToCapabilities(driverOptions, Platform), clientConfig)
{
}

/// <summary>
/// Initializes a new instance of the MacDriver class using the specified remote address, Appium options, command timeout and AppiumClientConfig.
/// </summary>
/// <param name="remoteAddress">URI containing the address of the WebDriver remote server (e.g. http://127.0.0.1:4723/wd/hub).</param>
/// <param name="driverOptions">An <see cref="DriverOptions"/> object containing the Appium options.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public MacDriver(Uri remoteAddress, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
: base(remoteAddress, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
{
}

/// <summary>
/// Initializes a new instance of the MacDriver class using the specified Appium local service, Appium options, command timeout and AppiumClientConfig,
/// </summary>
/// <param name="service">the specified Appium local service</param>
/// <param name="driverOptions">An <see cref="ICapabilities"/> object containing the Appium options.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
/// <param name="clientConfig">An instance of <see cref="AppiumClientConfig"/></param>
public MacDriver(AppiumLocalService service, DriverOptions driverOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
: base(service, SetPlatformToCapabilities(driverOptions, Platform), commandTimeout, clientConfig)
{
}
}
}
41 changes: 41 additions & 0 deletions src/Appium.Net/Appium/Service/AppiumClientConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//See the NOTICE file distributed with this work for additional
//information regarding copyright ownership.
//You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.

namespace OpenQA.Selenium.Appium.Service
{

public class AppiumClientConfig
{
/// <summary>
/// Return the default Appium Client Config
/// </summary>
/// <returns>An AppiumClientConfig instance</returns>
public static AppiumClientConfig DefaultConfig()
{
return new AppiumClientConfig();
}

/// <summary>
/// Gets or sets the directConnect feature availability.
/// If this flag is true and the target server supports
/// https://appiumpro.com/editions/86-connecting-directly-to-appium-hosts-in-distributed-environments,
/// the AppiumCommandExecutor will follow the response directConnect direction.
///
/// AppiumClientConfig clientConfig = AppiumClientConfig.DefaultConfig();
/// clientConfig.DirectConnect = true;
///
/// </summary>
public bool DirectConnect { get; set; }
}
}
62 changes: 58 additions & 4 deletions src/Appium.Net/Appium/Service/AppiumCommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
//See the License for the specific language governing permissions and
//limitations under the License.

using Newtonsoft.Json;
using OpenQA.Selenium.Remote;
using System;
using System.Collections.Generic;

namespace OpenQA.Selenium.Appium.Service
{
Expand All @@ -23,6 +25,9 @@ internal class AppiumCommandExecutor : ICommandExecutor
private ICommandExecutor RealExecutor;
private bool isDisposed;
private const string IdempotencyHeader = "X-Idempotency-Key";
private AppiumClientConfig ClientConfig;

private TimeSpan CommandTimeout;

private static ICommandExecutor CreateRealExecutor(Uri remoteAddress, TimeSpan commandTimeout)
{
Expand All @@ -34,16 +39,20 @@ private AppiumCommandExecutor(ICommandExecutor realExecutor)
RealExecutor = realExecutor;
}

internal AppiumCommandExecutor(Uri url, TimeSpan timeForTheServerResponding)
internal AppiumCommandExecutor(Uri url, TimeSpan timeForTheServerResponding, AppiumClientConfig clientConfig)
: this(CreateRealExecutor(url, timeForTheServerResponding))
{
CommandTimeout = timeForTheServerResponding;
Service = null;
ClientConfig = clientConfig;
}

internal AppiumCommandExecutor(AppiumLocalService service, TimeSpan timeForTheServerResponding)
internal AppiumCommandExecutor(AppiumLocalService service, TimeSpan timeForTheServerResponding, AppiumClientConfig clientConfig)
: this(CreateRealExecutor(service.ServiceUrl, timeForTheServerResponding))
{
CommandTimeout = timeForTheServerResponding;
Service = service;
ClientConfig = clientConfig;
}

public Response Execute(Command commandToExecute)
Expand All @@ -56,9 +65,15 @@ public Response Execute(Command commandToExecute)
{
Service?.Start();
RealExecutor = ModifyNewSessionHttpRequestHeader(RealExecutor);

result = RealExecutor.Execute(commandToExecute);
RealExecutor = UpdateExecutor(result, RealExecutor);
}
else
{
result = RealExecutor.Execute(commandToExecute);
}

result = RealExecutor.Execute(commandToExecute);
return result;
}
catch (Exception e)
Expand Down Expand Up @@ -89,11 +104,50 @@ private ICommandExecutor ModifyNewSessionHttpRequestHeader(ICommandExecutor comm
{
if (commandExecutor == null) throw new ArgumentNullException(nameof(commandExecutor));
var modifiedCommandExecutor = commandExecutor as HttpCommandExecutor;

modifiedCommandExecutor.SendingRemoteHttpRequest += (sender, args) =>
args.AddHeader(IdempotencyHeader, Guid.NewGuid().ToString());

return modifiedCommandExecutor;
}


/// <summary>
/// Return an instance of AppiumCommandExecutor.
/// If the executor can use as-is, this method will return the given executor without any updates.
/// </summary>
/// <param name="result">The result of the command execution.</param>
/// <param name="currentExecutor">Current ICommandExecutor instance.</param>
/// <returns>A ICommandExecutor instance</returns>
private ICommandExecutor UpdateExecutor(Response result, ICommandExecutor currentExecutor)
{
if (ClientConfig.DirectConnect == false) {
return currentExecutor;
}

var newExecutor = GetNewExecutorWithDirectConnect(result);
if (newExecutor == null) {
return currentExecutor;
}

return newExecutor;
}

/// <summary>
/// Returns a new command executor if the response had directConnect.
/// </summary>
/// <param name="result">The result of the command execution.</param>
/// <returns>A ICommandExecutor instance or null</returns>
private ICommandExecutor GetNewExecutorWithDirectConnect(Response response)
{
var newUri = new DirectConnect(response).GetUri();
if (newUri != null) {
return new HttpCommandExecutor(newUri, CommandTimeout);
}

return null;
}

public void Dispose() => Dispose(true);

protected void Dispose(bool disposing)
Expand All @@ -114,4 +168,4 @@ public bool TryAddCommand(string commandName, CommandInfo info)
return this.RealExecutor.TryAddCommand(commandName, info);
}
}
}
}
82 changes: 82 additions & 0 deletions src/Appium.Net/Appium/Service/DirectConnect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//See the NOTICE file distributed with this work for additional
//information regarding copyright ownership.
//You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.

using System;
using System.Collections.Generic;

namespace OpenQA.Selenium.Appium.Service
{
public class DirectConnect
{
private const string DIRECT_CONNECT_PROTOCOL = "directConnectProtocol";
private const string DIRECT_CONNECT_HOST = "directConnectHost";
private const string DIRECT_CONNECT_PORT = "directConnectPort";
private const string DIRECT_CONNECT_PATH = "directConnectPath";

private readonly string Protocol;
private readonly string Host;
private readonly string Port;
private readonly string Path;


/// <summary>
/// Create a direct connect instance from the given received response.
/// </summary>
public DirectConnect(Response response)
{

this.Protocol = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_PROTOCOL);
this.Host = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_HOST);
this.Port = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_PORT);
this.Path = GetDirectConnectValue((Dictionary<string, object>)response.Value, DIRECT_CONNECT_PATH);
}

/// <summary>
/// Returns a URL instance built with members in the DirectConnect instance.
/// </summary>
/// <returns>A Uri instance</returns>
public Uri GetUri() {
if (this.Protocol == null || this.Host == null || this.Port == null || this.Path == null) {
return null;
}

if (this.Protocol != "https")
{
return null;
}

return new Uri(this.Protocol + "://" + this.Host + ":" + this.Port + this.Path);
}

/// <summary>
/// Returns a value of instance built with members in the DirectConnect instance.
/// </summary>
/// <param name="value">The value of the 'value' key in the response body.</param>
/// <param name="keyName">The key name to get the value.</param>
/// <returns>A string value or null</returns>
private string GetDirectConnectValue(Dictionary<string, object> value, string keyName)
{
if (value.ContainsKey("appium:" + keyName))
{
return value["appium:" + keyName].ToString();
}

if (value.ContainsKey(keyName)) {
return value[keyName].ToString();
}

return null;
}
}
}
Loading

0 comments on commit a550e79

Please sign in to comment.