-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
"Port" leaking in C# WebDriver.dll #4162
Comments
Right now none of drivers except to standalone support keep-alive connections (e.g. ChromeDriver doesn't support it) so I don't think there is anything we can do to improve the behavior. I'll leave that open for @jimevans to have a look, maybe I misunderstood the problem. |
Alex, I mainly testing on Chrome and Firefox, therefore, I mentioned these two. Just checked the code base, this problem seems happen to all the C# Drivers within the WebDriver dll: Chrome, Edge, Firefox, IE, Opera, Safar, and RemoteWebDriver. I encountered the problem when I ran my test classes in parallel mode, it crashed the Selenium WebDriver. I have 20 test classes, and each of them consists of 8 to 15 test cases. The login to the test sites takes quite a while, so I decided login using NUnit's OneTimeSetup and OneTimeTearDown. Then I realised my test crashed from time to time, no matter I ran them in VS 2017, or Windows Powser Shell. When it happens, all the test browsers were freeze, no more movement, but I knew they haven't finished yet. All I can do is terminate the job and restart the test and hope the next run won't crash. After eliminated all other possibilities: OS, test frameworks...etc, I narrowed down to the FirefoxDriver and ChromeDriver C# class. And found that for HttpCommandExecutor, the KeepAlive option = true did actually keep the port open after the response received, and HttpWebRequest object out of the scope of the function and ready for GC collection. The ports that being held only got released when ChromeDrive.Quite() called. HttpCommandExecutor is being used for all Selenium C# class webdriver to send commands to the native OS browser driver. After realized the cause of the bug, my immediate work around was to open and quit ChromeDriver/FirefoxDriver (I tested both) at each case, although it doubles the over all testing time, but the tests didn't crash any more, they all finished to the end, test report was collected. I then took the WebDriver source code and recompiled with HttpCommandExcecutor.KeepAlive option = false, all of sudden, the weird situation is gone and my tests not hung any more. Analysis:
To reproduce in a simple scenario, the following should work: Open ChromeDriver from C# code, set up a while loop, within the while loop, navigate to one site, say google.com.au and repeat the navigation say 100,000 times. I expect the each navigation request to chromedriver.exe hold up one port. The program should hang at the middle of running. The fix is simple, simply remove that KeepAlive option for external call, and within the send request method, change it to false. Another option, is to make sure all call to the constructor public HttpCommandExecutor(Uri addressOfRemoteServer, TimeSpan timeout, bool enableKeepAlive), the enableKeepAlive is set to false. I am not sure whether this is really bug in the .Net runtime though. At least, we found the work around. Kind regards Ken |
Ok, I think I understand the problem, but it's going deep into internals of .NET bindings, so let's wait for Jim to respond. |
Any news about this issue ? It is planned to solve it soon ? Because I'm also having the same issue since one week and currently finding a workaround... |
For the workaround, I had to recompile the dotnet project with the option
set to off.
…On 17 Oct 2017 5:52 pm, "Lauriane Savary" ***@***.***> wrote:
Any news about this issue ? It is planned to solve it soon ? Because I'm
also having the same issue since one week and currently finding a
workaround...
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#4162 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ALkRUN5H-bxtLPZ2Hg1OI6lz1er0mvKhks5stE7EgaJpZM4N2uBj>
.
|
Latest chromedriver (2.35) contains a workaround for the keep-alive problem, it allows to actually reuse connections. But it's not a fix, it's a workaround. In order to make it work client must send The problem is that when using HTTP 1.1 HttpWebRequest assumes that keep-alive is on by default and doesn't send the header. I haven't found the proper way (without reflection) to forcibly add a header so far but can confirm adding it works. Any ideas? Also, +1 to deinternalizing |
if you are running into port exhaustion, you should check your TCP/IP settings and tune your network stack accordingly. The default ephemeral port range in some desktop operating systems (like Windows) is not nearly sufficient for applications doing high socket throughput. |
@cgoldberg in my current case it's more complicated than that. I can only say that the average test creates about 3000 connections, now imagine a test suite and think about time those connections are in TIME_WAIT and some new tests are starting at this moment. No tuning will help, especially when we will run this in parallel |
Hi all,
In our case, we had to turn the flag off and recompile the driver to fix
the issue.
…On 22 Feb 2018 8:38 am, "Vasya Aksyonov" ***@***.***> wrote:
@cgoldberg <https://github.com/cgoldberg> in my current case it's more
complicated than that. I can only say that the average test creates about
3000 connections, now imagine a test suite and think about time those
connections are in TIME_WAIT and some new tests are starting at this
moment. No tuning will help, especially when we will run this in parallel
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#4162 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ALkRUBCwf8AtJslpupFfYP1xQbFBOrMVks5tXIy-gaJpZM4N2uBj>
.
|
Hey everyone, I've tried everything that I can find and am still having these issues. I'm using the most recent version of chromedriver (v2.40) and have forced Is there something I'm missing when it comes to the mentioned workaround? I'm trying to do browser automation on a relatively large scale and can't get around this roadblock. |
Hi Bryce,
In my case, I have made changes to two files.
HttpCommandExecutor.cs
internal class HttpCommandExecutor : ICommandExecutor
{
private const string JsonMimeType = "application/json";
private const string ContentTypeHeader = JsonMimeType +
";charset=utf-8";
private const string RequestAcceptHeader = JsonMimeType + ",
image/png";
private Uri remoteServerUri;
private TimeSpan serverResponseTimeout;
private bool enableKeepAlive;
private CommandInfoRepository commandInfoRepository = new
WebDriverWireProtocolCommandInfoRepository();
/// <summary>
/// Initializes a new instance of the <see
cref="HttpCommandExecutor"/> class
/// </summary>
/// <param name="addressOfRemoteServer">Address of the WebDriver
Server</param>
/// <param name="timeout">The timeout within which the server must
respond.</param>
public HttpCommandExecutor(Uri addressOfRemoteServer, TimeSpan
timeout)
: this(addressOfRemoteServer, timeout, true)
{
}
DriverServiceCommandExecutor.cs :
internal class DriverServiceCommandExecutor : ICommandExecutor
{
private DriverService service;
private HttpCommandExecutor internalExecutor;
/// <summary>
/// Initializes a new instance of the <see
cref="DriverServiceCommandExecutor"/> class.
/// </summary>
/// <param name="driverService">The <see cref="DriverService"/>
that drives the browser.</param>
/// <param name="commandTimeout">The maximum amount of time to wait
for each command.</param>
public DriverServiceCommandExecutor(DriverService driverService,
TimeSpan commandTimeout)
: this(driverService, commandTimeout, false)
{
}
After recompiled the solution, you may want to use ILSpy to inspect the
generated dlls to ensure the changes are actually inside.
Ken Han
…On Sat, 16 Jun 2018 at 06:33, Bryce Barbara ***@***.***> wrote:
Hey everyone,
I've tried everything that I can find and am still having these issues.
I'm using the most recent version of chromedriver (v2.40) and have forced
HttpCommandExecutor.enableKeepAlive to false at all times and still end
up with ports in TIME_WAIT. Just as a test, I've forced the header Connection:
close to be sent with the request and that also doesn't work.
Is there something I'm missing when it comes to the mentioned workaround?
I'm trying to do browser automation on a relatively large scale and can't
get around this roadblock.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#4162 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ALkRUCpAgau6NYA4iXsX_3rrZk_uy6pGks5t9BoNgaJpZM4N2uBj>
.
|
Hi @BryceBarbara , Please find the patches for the change.
|
accidentally closed the issue. Reopening... |
having sockets in TIME_WAIT state is totally normal.. that's the way TCP works. Are you experiencing actual port exhaustion or simply seeing some sockets in TIME_WAIT state? @kenhan168 if your patch fixes something, why not submit a Pull Request? |
Hi all, Pull request is up: #6162 |
How can disabling keep-alive resolve port exhaustion issue? If you close connection after a request you'll have to engage a new port for the next request. Quite contrary, keep-alive options should help us if all parties implement it properly. |
The keep-alive setting didn't work when I reported the issue initially.
The intention was good, however, the existing connection was not able to be
recycled. This ended with port exhaustion. In addition, the keep-alive
setting is stateful design, which restricts scaling.
To test the port exhaustion, open a chromedriver from WebDriver, then issue
the command open a website within a while loop of true, then you should be
able to reproduce the port exhaustion issue.
Ken Han
Mobile: +61 (0) 466 675 144
+61 (0) 430 817 085
Email: [email protected]
…On Mon, 16 Jul 2018 at 17:34, Alexei Barantsev ***@***.***> wrote:
How can disabling keep-alive resolve port exhaustion issue? If you close
connection after a request you'll have to engage a new port for the next
request.
Quite contrary, keep-alive options should help us if all parties implement
it properly.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#4162 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ALkRUE-jgVYmlfUIFQ7fzVzqsre6NhwFks5uHEHygaJpZM4N2uBj>
.
|
As stated by @outring earlier on this thread WebRequest under HTTP 1.1. assumes keep-alive by default and so doesn't send it when KeepAlive is true on the actual WebRequest object. ChromeDriver.exe 2.35 and up does not make the same assumption and so you MUST actually pass the header. What worked for me is the forcing of keep-alive in the WebRequest. Though as was said reflection is required to do so but it works fine. Here's the code I used:
This version is for Selenium Web Driver 3.14. It's slightly different under 3.13.x but the principle is the same. |
@cgoldberg Sorry for the delay! I do actually experience port exhaustion and had to lower the OS timeout duration to 30 seconds (default is 3 minutes) to avoid the system periodically throwing exceptions. I'm running at least 80 instances of headless chrome on some jumbo machine so my use case might be a pretty rare edge-case. I've yet to get the time to investigate Selenium Grid which might serve me better in the long-run. |
@kenhan168 When will the #6162 be merged. Also please let me know if upgrading to v3.14.0 will solve anything related to the keepalive failure ? It says - Additionally, this change exposes the IsKeepAliveEnabled property to make |
Hi Parin,
Unfortunately, I don't have merge permission.
Ken Han
…On Sat, 6 Oct 2018 at 04:13, Parin ***@***.***> wrote:
@kenhan168 <https://github.com/kenhan168> When will the #6162
<#6162> be merged.
We use Selenium C# bindings with Nunit to run 10 parallel test sessions in
Saucelabs cloud. Here is my packages.config file
Each session uses a thread safe Webdriver instance. Each session uses the
same webdriver instance till the end of that session.
We have been getting this error for random tests at random times, for a
long time now. It has become a hassle as it gives false negatives.
OpenQA.Selenium.WebDriverException : A exception with a null response was
thrown sending an HTTP request to the remote WebDriver server for URLhttp://
ondemand.saucelabs.com/wd/hub/session/4121bd78c22940e9a6deaf615b7c143f/execute_async.
*The status of the exception was KeepAliveFailure, and the message was:
The underlying connection was closed: A connection that was expected to be
kept alive was closed by the server. ----> System.Net.WebException : The
underlying connection was closed: A connection that was expected to be kept
alive was closed by the server.*
Also please let me know if upgrading to v3.14.0 will solve anything
related to the keepalive failure ?
https://github.com/SeleniumHQ/selenium/blob/master/dotnet/CHANGELOG
It says -
*Additionally, this change exposes the IsKeepAliveEnabled property to make
it easier to set whether or not the "keep alive" header is sent when
communicating between the language bindings and the remote end WebDriver
implementation.*
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#4162 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ALkRULeOaszyKuBSA9duHk0Ua9ThevRqks5uh6FhgaJpZM4N2uBj>
.
|
Is there any progress on this issue? I ran into the same issue and it took me days to find out what actually happens here. |
Folks who are experiencing this issue, can you please weigh in what framework you're using? In particular, I'm interested in whether you're using .NET Framework or .NET Core. It looks like there's a fundamental difference in how the .NET |
As far as I know, nowadays it is recommended to use the HttpClient instead of HttpWebRequest. And also the async methods should be used instead. But of course that would mean a lot of interface changes. The whole selenium API would become async. @jimevans |
@TFTomSun I’ve already done that work, and yes, As for why the .NET bindings haven’t migrated to it, there are ways to mask the async changes and not change the API. But the bindings support versions of .NET as far back as 3.5, and |
@jimevans If you did that work already, can you provide it? Then I could use the HttpExecutor that uses only the HttpClient Actually, I would not suggest to provide an API that uses async calls under the hood without exposing async endpoints. First of all, you don't fully benefit from the async stuff then and there are also known issues with blocking async calls. Is support back to .NET 3.5 still required? Is there still any supported operating system that has that installed by default? |
here's the link to regarding HttpClient usage in HttpWebRequest on .NET Core |
By “did that work,” I meant “created a simple console application outside the Selenium library that just did raw HTTP calls,” not “changed Selenium to use HttpClient and see what happens.” I’d be happy to share that work, but it would be of no benefit whatsoever to your use of Selenium. As for “known issues with blocking async calls,” I’d be interested in some further information on that. As near as I can tell, the IL generated by the compiler for And dropping support for .NET 3.5 and 4.0 without notice and a transition period seems actively hostile to users. |
I've created a HttpCommandExecutor based on the HttpClient: public static class SeleniumHttpClientExecutionExtensions
{
public static IWebDriver UseHttpClient(this IWebDriver driver)
{
var executor = driver.GetFieldValue<ICommandExecutor>("executor");
if (executor is DriverServiceCommandExecutor driverServiceExecutor)
{
driverServiceExecutor.SetFieldValue("internalExecutor",
driverServiceExecutor.HttpExecutor.ToHttpClientExecutor());
}
else if (executor is HttpCommandExecutor httpCommandExecutor)
{
driver.SetFieldValue("executor", httpCommandExecutor.ToHttpClientExecutor());
}
else
{
throw new ApplicationException("Could not find the http command executor");
}
return driver;
}
}
internal class HttpClientCommandExecutor : HttpCommandExecutor
{
private Uri RemoteServerUri { get; }
private TimeSpan ServerResponseTimeout { get; }
public HttpClientCommandExecutor(Uri addressOfRemoteServer, TimeSpan timeout, bool enableKeepAlive) : base(addressOfRemoteServer, timeout, enableKeepAlive)
{
RemoteServerUri = this.GetFieldValue<Uri>("remoteServerUri");
ServerResponseTimeout = this.GetFieldValue<TimeSpan>("serverResponseTimeout");
}
public override Response Execute(Command commandToExecute)
{
if (commandToExecute == null)
throw new ArgumentNullException(nameof(commandToExecute), "commandToExecute cannot be null");
CommandInfo commandInfo = this.CommandInfoRepository.GetCommandInfo(commandToExecute.Name);
Response response = this.CreateResponse(this.MakeHttpRequest(new HttpRequestInfo(RemoteServerUri, commandToExecute, commandInfo)).GetAwaiter().GetResult());
if (commandToExecute.Name == DriverCommand.NewSession && response.IsSpecificationCompliant)
this.SetFieldValue<CommandInfoRepository>("commandInfoRepository",new W3CWireProtocolCommandInfoRepository());
return response;
}
private ConcurrentDictionary<string, HttpClient> HttpClientCache { get; } = new ConcurrentDictionary<string, HttpClient>();
private HttpClient CreateOrGetHttpClient(string method, string userInfo)
{
return this.HttpClientCache.GetOrAdd(method + "_"+userInfo,k=>
{
var httpClientHandler = new HttpClientHandler();
var client = new HttpClient(httpClientHandler);
var requestHeaders = client.DefaultRequestHeaders;
if (!string.IsNullOrEmpty(userInfo) && userInfo.Contains(":"))
{
string[] strArray = this.RemoteServerUri.UserInfo.Split(new char[1]
{
':'
}, 2);
httpClientHandler.Credentials =
(ICredentials)new NetworkCredential(strArray[0], strArray[1]);
httpClientHandler.PreAuthenticate = true;
}
string str = string.Format((IFormatProvider)CultureInfo.InvariantCulture,
"selenium/{0} (.net {1})", (object)ResourceUtilities.AssemblyVersion,
(object)ResourceUtilities.PlatformFamily);
requestHeaders.UserAgent.ParseAdd(str);
//httpClientHandler.Method = method;
client.Timeout = this.ServerResponseTimeout;
requestHeaders.Accept.ParseAdd("application/json, image/png");
requestHeaders.Add("Connection", this.IsKeepAliveEnabled ? "keep-alive" : "close");
//httpClientHandler.KeepAlive = this.Inner.IsKeepAliveEnabled;
httpClientHandler.Proxy = this.Proxy;
// httpClientHandler.ServicePoint.ConnectionLimit = 2000;
httpClientHandler.MaxConnectionsPerServer = 2000;
if (method == "GET")
requestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue()
{ NoCache = true };
if (method == "POST")
{
requestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
{
CharSet = "utf-8",
});
}
return client;
});
}
private async Task<HttpResponseInfo> MakeHttpRequest(HttpRequestInfo requestInfo)
{
var client = this.CreateOrGetHttpClient(requestInfo.HttpMethod, requestInfo.FullUri.UserInfo);
//HttpWebRequest request = WebRequest.Create(requestInfo.FullUri) as HttpWebRequest;
SendingRemoteHttpRequestEventArgs eventArgs = new SendingRemoteHttpRequestEventArgs(null, requestInfo.RequestBody);
this.OnSendingRemoteHttpRequest(eventArgs);
//try
//{
HttpResponseMessage response;
if (requestInfo.HttpMethod == "POST")
{
byte[] bytes = Encoding.UTF8.GetBytes(eventArgs.RequestBody);
response = await client.PostAsync(requestInfo.FullUri, new ByteArrayContent(bytes, 0, bytes.Length));
}
else
{
response = await client.GetAsync(requestInfo.FullUri);
}
if (response.StatusCode == HttpStatusCode.RequestTimeout)
throw new WebDriverException(string.Format((IFormatProvider)CultureInfo.InvariantCulture, "The HTTP request to the remote WebDriver server for URL {0} timed out after {1} seconds.", (object)requestInfo.FullUri, (object)this.ServerResponseTimeout.TotalSeconds));
if (response.Content == null)
{
throw new WebDriverException(string.Format((IFormatProvider)CultureInfo.InvariantCulture, "A exception with a null response was thrown sending an HTTP request to the remote WebDriver server for URL {0}. The status of the exception was {1}, and the message was: {2}", requestInfo.FullUri, response.StatusCode));
}
//}
//catch (WebException ex)
//{
// response = ex.Response as HttpWebResponse;
// if (ex.Status == WebExceptionStatus.Timeout)
// throw new WebDriverException(string.Format((IFormatProvider)CultureInfo.InvariantCulture, "The HTTP request to the remote WebDriver server for URL {0} timed out after {1} seconds.", (object)request.RequestUri.AbsoluteUri, (object)this.serverResponseTimeout.TotalSeconds), (Exception)ex);
// if (ex.Response == null)
// throw new WebDriverException(string.Format((IFormatProvider)CultureInfo.InvariantCulture, "A exception with a null response was thrown sending an HTTP request to the remote WebDriver server for URL {0}. The status of the exception was {1}, and the message was: {2}", (object)request.RequestUri.AbsoluteUri, (object)ex.Status, (object)ex.Message), (Exception)ex);
//}
//if (response == null)
// throw new WebDriverException("No response from server for url " + requestInfo.FullUri);
HttpResponseInfo httpResponseInfo = new HttpResponseInfo();
httpResponseInfo.Body = await response.Content.ReadAsStringAsync();
httpResponseInfo.ContentType = response.Content.Headers.ContentType.ToString();
httpResponseInfo.StatusCode = response.StatusCode;
return httpResponseInfo;
}
private Response CreateResponse(HttpResponseInfo stuff)
{
Response response = new Response();
string body = stuff.Body;
if (stuff.ContentType != null && stuff.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
response = Response.FromJson(body);
else
response.Value = (object)body;
if (this.CommandInfoRepository.SpecificationLevel < 1 && (stuff.StatusCode < HttpStatusCode.OK || stuff.StatusCode >= HttpStatusCode.BadRequest))
{
if (stuff.StatusCode >= HttpStatusCode.BadRequest && stuff.StatusCode < HttpStatusCode.InternalServerError)
response.Status = WebDriverResult.UnhandledError;
else if (stuff.StatusCode >= HttpStatusCode.InternalServerError)
{
if (stuff.StatusCode == HttpStatusCode.NotImplemented)
response.Status = WebDriverResult.UnknownCommand;
else if (response.Status == WebDriverResult.Success)
response.Status = WebDriverResult.UnhandledError;
}
else
response.Status = WebDriverResult.UnhandledError;
}
if (response.Value is string)
response.Value = (object)((string)response.Value).Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
return response;
}
private class HttpRequestInfo
{
public HttpRequestInfo(Uri serverUri, Command commandToExecute, CommandInfo commandInfo)
{
this.FullUri = commandInfo.CreateCommandUri(serverUri, commandToExecute);
this.HttpMethod = commandInfo.Method;
this.RequestBody = commandToExecute.ParametersAsJsonString;
}
public Uri FullUri { get; set; }
public string HttpMethod { get; set; }
public string RequestBody { get; set; }
}
private class HttpResponseInfo
{
public HttpStatusCode StatusCode { get; set; }
public string Body { get; set; }
public string ContentType { get; set; }
}
}
internal static class InternalExtensions
{
private static IEnumerable<Type> BaseTypes(this Type type)
{
while (type.BaseType != null)
{
yield return type.BaseType;
type = type.BaseType;
}
}
internal static FieldInfo GetFieldInfo(this Object value, String memberName, BindingFlags? bindingFlags = null)
{
bindingFlags = bindingFlags ?? BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
var type = value.GetType();
return new[] { type }.Concat(type.BaseTypes()).Select(t => t.GetField(memberName, bindingFlags.Value)).First(
f => f != null);
}
internal static TValue GetFieldValue<TValue>(this Object instance, string fieldName)
{
return (TValue)(instance.GetFieldInfo(fieldName)).GetValue(instance);
}
internal static void SetFieldValue<TValue>(this Object instance, string fieldName, TValue value)
{
(instance.GetFieldInfo(fieldName)).SetValue(instance, value);
}
internal static HttpClientCommandExecutor ToHttpClientExecutor(this HttpCommandExecutor httpExecutor)
{
return new HttpClientCommandExecutor(
httpExecutor.GetFieldValue<Uri>("remoteServerUri"),
httpExecutor.GetFieldValue<TimeSpan>("serverResponseTimeout"),
httpExecutor.IsKeepAliveEnabled);
}
} Usage: [Test]
public void PortExhaustingBug()
{
IWebDriver anyDriver = new ChromeDriver(ChromeDriverDirectoryPath);
// Use the http client instead of HttpWebRequest
anyDriver.UseHttpClient();
anyDriver.Navigate().GoToUrl("https://www.google.com");
var searchButton = anyDriver.FindElement(By.Name("btnK"));
for (int i = 0; i < 100000; ++i)
{
var isEnabled = Api.Create.Function(() => searchButton.Enabled).Retry(2, 1.Seconds(), true);
Assert.IsTrue(isEnabled);
}
}
|
@jimevans I tried to reuse as much as possible from the existing logic / code and make that stuff attachable to the existing selenium implementation. |
@TFTomSun There are some changes in approach that I would make to that code to suit my own personal coding style and that of the Selenium project. I'm not going to add this particular code to the Selenium project at this time, because (1) it requires reflection, (2) it will not compile on all of the framework versions that we are required to support, and (3) it's likely the framework support matrix will be changing in Selenium 4.x, possibly even dropping all framework support that does not have I have created a demo project containing my version of the code. It will not run as a drop-in replacement with the shipping public release (3.141.0), as the sample code relies on extension points that I've only just added (specifically 8227f97 and 7fdf318). |
@jimevans anyway, I can provide a nuget package that can be used with the current selenium version. If someone is interested, please leave a request here. |
Hey, look, another instance of someone insulting me and my work without taking any time to consider any factors that may have led to such a (nearly 10 year old!) design decision (like preferring consistency of operation between language bindings). And make no mistake, calling it “bad code” is an insult, no matter how carefully couched In “widely seen as” language. All this for a project that, as much as people rely on it for their day-to-day work, people don’t get paid to maintain. Thanks for a great start to my day @TFTomSun. For the record, among the changes I made in my implementation were to restrict to the use of a single |
@jimevans The only change that I understood is, that its better to use one client. That's fine, a little change. I didn't know about the possibility that you can specify additional headers to the default once via the SendAsync method. But instead of keeping the deferred initialization pattern you choose to modify the state of your object (the client field) within a function call. And you made that decision yesterday, not 10 years ago. Have a look at the Lazy class or the classic way of implement lazy properties. Sometimes it's also good to accept that something was not done well instead of feeling insulted. And fixing that remotedriver behavior (and for me it would be a fix) could be done always, it's never to late. You blamed me before that I don't want to support the community... I provided a fix, I can also provide a package if necessary. That's all what I can do for the community :) |
And when called out on being rude, I took responsibility for it and apologized, without caveat. In any case, I’m moving on from this issue. There is a workaround available, which will likely be made obsolete in 4.0. |
I didn't attack you personally or assumed something. I just stated facts. I'm really sorry, if you felt insulted. I have just a hard time with the current selenium c# client architecture to attach my features and workaround bugs. If you consider to improve the architecture and make it more open and extendable, I would highly appreciate it. :) In case you consider it, feel free to contact me for some input. Additionally, I'm also looking forward to have the fix built into selenium soon. In the end we all want the same, an intuitive and easy to use web automation client. I hope we are fine :) And thanks for pointing me into the right direction, that the direct use of HttpClient will solve the issue. |
Hi Guys, This is really nice information and conversation so far. I am facing the exact issue in my C# automation project. Please let us know when the fix will be available in nuget package for .net core 2.2? |
Hi, I still receive error for HttpClient - .net core 2.2. Can you please assist me how to fix this issue? Only one usage of each socket address (protocol/network address/port) is normally permitted FYI - I am referring https://github.com/jimevans/HttpClientCommandExecutor repository |
@sanjaykulkarni04 I don't know about the repo you mentioned, but the following solution works. I've tested it with chrome and firefox. Just put all together, and use it the following way:
Code to add to your project: public static class SeleniumHttpClientExecutionExtensions
{
public static IWebDriver UseHttpClient(this IWebDriver driver)
{
var executor = driver.GetFieldValue<ICommandExecutor>("executor");
if (executor is DriverServiceCommandExecutor driverServiceExecutor)
{
driverServiceExecutor.SetFieldValue("internalExecutor",
driverServiceExecutor.HttpExecutor.ToHttpClientExecutor());
}
else if (executor is HttpCommandExecutor httpCommandExecutor)
{
driver.SetFieldValue("executor", httpCommandExecutor.ToHttpClientExecutor());
}
else
{
throw new ApplicationException("Could not find the http command executor");
}
return driver;
}
}
internal class HttpClientCommandExecutor : HttpCommandExecutor
{
private const string Utf8CharsetType = "utf-8";
private const string JsonMimeType = "application/json";
private Uri RemoteServerUri { get; }
private TimeSpan ServerResponseTimeout { get; }
private Lazy<HttpClient> HttpClient { get; }
public HttpClientCommandExecutor(Uri addressOfRemoteServer, TimeSpan timeout, bool enableKeepAlive) : base(addressOfRemoteServer, timeout, enableKeepAlive)
{
RemoteServerUri = this.GetFieldValue<Uri>("remoteServerUri");
ServerResponseTimeout = this.GetFieldValue<TimeSpan>("serverResponseTimeout");
this.HttpClient = new Lazy<HttpClient>(this.CreateClient);
}
public override Response Execute(Command commandToExecute)
{
if (commandToExecute == null)
throw new ArgumentNullException(nameof(commandToExecute), "commandToExecute cannot be null");
CommandInfo commandInfo = this.CommandInfoRepository.GetCommandInfo(commandToExecute.Name);
Response response = this.CreateResponse(this.MakeHttpRequest(new HttpRequestInfo(RemoteServerUri, commandToExecute, commandInfo)).GetAwaiter().GetResult());
if (commandToExecute.Name == DriverCommand.NewSession && response.IsSpecificationCompliant)
this.SetFieldValue<CommandInfoRepository>("commandInfoRepository",new W3CWireProtocolCommandInfoRepository());
return response;
}
private HttpClient CreateClient()
{
var httpClientHandler = new HttpClientHandler();
var client = new HttpClient(httpClientHandler);
var requestHeaders = client.DefaultRequestHeaders;
var userInfo = this.RemoteServerUri.UserInfo;
if (!string.IsNullOrEmpty(userInfo) && userInfo.Contains(":"))
{
string[] userInfoComponents = userInfo.Split(new[] { ':' }, 2);
httpClientHandler.Credentials = new NetworkCredential(userInfoComponents[0], userInfoComponents[1]);
httpClientHandler.PreAuthenticate = true;
}
var str = string.Format(CultureInfo.InvariantCulture,
"selenium/{0} (.net {1})", ResourceUtilities.AssemblyVersion,
ResourceUtilities.PlatformFamily);
requestHeaders.UserAgent.ParseAdd(str);
client.Timeout = this.ServerResponseTimeout;
requestHeaders.Accept.ParseAdd("application/json, image/png");
requestHeaders.Connection.ParseAdd(this.IsKeepAliveEnabled ? "keep-alive" : "close");
httpClientHandler.Proxy = this.Proxy;
httpClientHandler.MaxConnectionsPerServer = 2000;
return client;
}
private async Task<HttpResponseInfo> MakeHttpRequest(HttpRequestInfo requestInfo)
{
var eventArgs = new SendingRemoteHttpRequestEventArgs(null, requestInfo.RequestBody);
this.OnSendingRemoteHttpRequest(eventArgs);
var requestMessage = new HttpRequestMessage
{
Method = new HttpMethod(requestInfo.HttpMethod),
RequestUri = requestInfo.FullUri,
};
if (requestInfo.HttpMethod == CommandInfo.GetCommand)
{
var cacheControlHeader = new CacheControlHeaderValue();
cacheControlHeader.NoCache = true;
requestMessage.Headers.CacheControl = cacheControlHeader;
}
else if (requestInfo.HttpMethod == CommandInfo.PostCommand)
{
var acceptHeader = new MediaTypeWithQualityHeaderValue(JsonMimeType);
acceptHeader.CharSet = Utf8CharsetType;
requestMessage.Headers.Accept.Add(acceptHeader);
byte[] bytes = Encoding.UTF8.GetBytes(eventArgs.RequestBody);
requestMessage.Content = new ByteArrayContent(bytes, 0, bytes.Length);
}
var response = await this.HttpClient.Value.SendAsync(requestMessage);
if (response.StatusCode == HttpStatusCode.RequestTimeout)
{
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, "The HTTP request to the remote WebDriver server for URL {0} timed out after {1} seconds.", requestInfo.FullUri, this.ServerResponseTimeout.TotalSeconds));
}
if (response.Content == null)
{
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, "A exception with a null response was thrown sending an HTTP request to the remote WebDriver server for URL {0}. The status of the exception was {1}", requestInfo.FullUri, response.StatusCode));
}
var httpResponseInfo = new HttpResponseInfo
{
Body = Encoding.UTF8.GetString(await response.Content.ReadAsByteArrayAsync()),
ContentType = response.Content.Headers.ContentType.ToString(),
StatusCode = response.StatusCode
};
return httpResponseInfo;
}
private Response CreateResponse(HttpResponseInfo stuff)
{
var response = new Response();
string body = stuff.Body;
if (stuff.ContentType != null && stuff.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
response = Response.FromJson(body);
else
response.Value = body;
if (this.CommandInfoRepository.SpecificationLevel < 1 && (stuff.StatusCode < HttpStatusCode.OK || stuff.StatusCode >= HttpStatusCode.BadRequest))
{
if (stuff.StatusCode >= HttpStatusCode.BadRequest && stuff.StatusCode < HttpStatusCode.InternalServerError)
response.Status = WebDriverResult.UnhandledError;
else if (stuff.StatusCode >= HttpStatusCode.InternalServerError)
{
if (stuff.StatusCode == HttpStatusCode.NotImplemented)
response.Status = WebDriverResult.UnknownCommand;
else if (response.Status == WebDriverResult.Success)
response.Status = WebDriverResult.UnhandledError;
}
else
response.Status = WebDriverResult.UnhandledError;
}
if (response.Value is string)
response.Value = ((string)response.Value).Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
return response;
}
private class HttpRequestInfo
{
public HttpRequestInfo(Uri serverUri, Command commandToExecute, CommandInfo commandInfo)
{
this.FullUri = commandInfo.CreateCommandUri(serverUri, commandToExecute);
this.HttpMethod = commandInfo.Method;
this.RequestBody = commandToExecute.ParametersAsJsonString;
}
public Uri FullUri { get; set; }
public string HttpMethod { get; set; }
public string RequestBody { get; set; }
}
private class HttpResponseInfo
{
public HttpStatusCode StatusCode { get; set; }
public string Body { get; set; }
public string ContentType { get; set; }
}
}
internal static class InternalExtensions
{
private static IEnumerable<Type> BaseTypes(this Type type)
{
while (type.BaseType != null)
{
yield return type.BaseType;
type = type.BaseType;
}
}
internal static FieldInfo GetFieldInfo(this Object value, String memberName, BindingFlags? bindingFlags = null)
{
bindingFlags = bindingFlags ?? BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
var type = value.GetType();
return new[] { type }.Concat(type.BaseTypes()).Select(t => t.GetField(memberName, bindingFlags.Value)).First(
f => f != null);
}
internal static TValue GetFieldValue<TValue>(this Object instance, string fieldName)
{
if (instance == null) throw new ArgumentNullException(nameof(instance));
return (TValue)(instance.GetFieldInfo(fieldName)).GetValue(instance);
}
internal static void SetFieldValue<TValue>(this Object instance, string fieldName, TValue value)
{
(instance.GetFieldInfo(fieldName)).SetValue(instance, value);
}
internal static HttpClientCommandExecutor ToHttpClientExecutor(this HttpCommandExecutor httpExecutor)
{
var result = new HttpClientCommandExecutor(
httpExecutor.GetFieldValue<Uri>("remoteServerUri"),
httpExecutor.GetFieldValue<TimeSpan>("serverResponseTimeout"),
httpExecutor.IsKeepAliveEnabled);
result.SetFieldValue("commandInfoRepository", httpExecutor.CommandInfoRepository);
return result;
}
}
I'll create a nuget package out of it as soon as i have some free time. |
@TFTomSun Thank you so much for providing the correct code. Sometime this works but sometime not.. There is inconsistent behavior. I get below error. Can you please advise? ---- System.Threading.Tasks.TaskCanceledException : The operation was canceled. |
@sanjaykulkarni04 can you reduce your code to a small sample that has this issue sometimes and post it here? seems like the result of a call is not correctly awaited somewhere. But there's a synchronization already within the code I provided. At the moment I would assume that it is a side effect of your code environment, therefore try to reproduce the issue it with a small green field example (e.g. a console application), please :) |
@TFTomSun I am not able to reproduce this issue locally. We face this issue only in AWS environment where we run automation scripts in parallel. Meanwhile how to increase **
**? default value it 1 min |
@sanjaykulkarni04 How does your setup look like?
To make the server response time accessible make the following changes: public static IWebDriver UseHttpClient(this IWebDriver driver, TimeSpan? serverResponseTimeOut = null)
{
var executor = driver.GetFieldValue<ICommandExecutor>("executor");
if (executor is DriverServiceCommandExecutor driverServiceExecutor)
{
driverServiceExecutor.SetFieldValue("internalExecutor",
driverServiceExecutor.HttpExecutor.ToHttpClientExecutor(serverResponseTimeOut));
}
else if (executor is HttpCommandExecutor httpCommandExecutor)
{
driver.SetFieldValue("executor", httpCommandExecutor.ToHttpClientExecutor(serverResponseTimeOut));
}
else
{
throw new ApplicationException("Could not find the http command executor");
}
return driver;
}
internal static HttpClientCommandExecutor ToHttpClientExecutor(this HttpCommandExecutor httpExecutor, TimeSpan? serverResponseTimeout = null)
{
var result = new HttpClientCommandExecutor(
httpExecutor.GetFieldValue<Uri>("remoteServerUri"),
serverResponseTimeout?? httpExecutor.GetFieldValue<TimeSpan>("serverResponseTimeout"),
httpExecutor.IsKeepAliveEnabled);
result.SetFieldValue("commandInfoRepository", httpExecutor.CommandInfoRepository);
return result;
} Both methods exist already, you just have to add the serverResponseTimeout parameter as shown. |
@TFTomSun Thank for quick response.. Here are answer to your questions - Do you use SeleniumGrid? No Is the parallel execution on the same EC2 instance? Yes Is the parallelism done by you, or is it part of a test framework? by xunit unit test framework How many webdriver instances are you creating in one test run? each test script/class has separate instance.. Whereas each test class has multiple test methods having single web driver instance. e.g. If I run 4 test scripts in parallel, 4 web driver instances will be created in separate/different process. |
@TFTomSun One more scenario here, we have a setup on automation machine and we kickoff automation to run test script on automation machine from team city (CI) server . Unfortunately we receive below error. FYI - When we run test script manually on automation box it works fine. System.AggregateException : One or more errors occurred. (The operation was canceled.) If I don't use your code (driver.UseHttpClient();) still I receive below error. Can you please assist me what is configuration issue on automation box? _System.AggregateException : One or more errors occurred. (The HTTP request to the remote WebDriver server for URL http://localhost:49536/session/ee7e0934027b0072483aecd0614ba544/window/current/maximize timed out after 60 seconds.) (The following constructor parameters did not have matching fixture data: HeaderSummaryTestFixture ----- Inner Stack Trace #1 (OpenQA.Selenium.WebDriverException) ----- |
FYI - Above issue has been resolved by adding below code - var options = new ChromeOptions(); |
@kenhan168 - Can you please share code that you have wrote to disable connection keep alive? It will be very helpful. I am using C# with .Net 4.5 version and am running into same issue. Even compiled version of WebDriver.dll will also work for me. Thanks in advance. |
@hrchokshi Have you tried the 4.0 alpha? The bindings have been rewritten to use the newer HttpClient, and should no longer exhibit this behavior. |
The .NET bindings have been updated to use the |
Meta -
OS:
Windows 10, Windows Server 2008 R2
Selenium Version:
WebDriver 3.4.0
Browser:
Firefox and Chrome
Browser Version:
Firefox: 53.0.3 (32 bits)
Chrome: Version 59.0.3071.86 (Official Build) (64-bit)
Expected Behavior -
WebDriver suppose to close the ports after each commands sent to ChromeDriver or GeckoDriver
Actual Behavior -
WebDriver keeps the ports open until the WebDriver quit().
This caused the ports run out from the OS when I had 8 parallel tests running together without open and close ChromeDriver or FirefoxDriver.
Steps to reproduce -
Run the Selenium Tests with ChromeDriver or FirefoxDriver with any simple action, e.g. Navigate to a website. Repeat this step for 1000 time.
Open a Windows Command Prompt, type:
netstat -a -b
You will find the ChromeDriver/GeckoDriver holds up about 1000 ports without releasing.
I have amended the code in OpenQA.Selenium.Remote.DriverServiceCommandExecutor.cs, line 37
old code: this(driverService, commandTimeout, true)
new code: this(driverService, commandTimeout, false)
and
OpenQA.Selenium.Remote.HttpCommandExecutor.cs, line 46:
old code: : this(addressOfRemoteServer, timeout, true)
new code: : this(addressOfRemoteServer, timeout, false)
Retest again with the above step, we can see the ChromeDriver / Gecko Driver doesn't hold up the port any more.
It seems for HttpWebRequest object, even it is out of scope, if its' KeepAlive property is true, the .net runtime holds it until the WebDriver is quit and closed.
In theory, I can't think of HttpCommandExecutor should have HttpWebRequest should have KeepAlive = true as it looks like HttpCommandExecutor is running stateless commands. Maybe you can consider to remove the passing in of the attribute to avoid future problem.
Kind regards
Ken Han
Self Service Tech Lead
Fairfax Media Australia
The text was updated successfully, but these errors were encountered: