| // <copyright file="RemoteSeleniumServer.cs" company="Selenium Committers"> |
| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. 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. |
| // </copyright> |
| |
| using System; |
| using System.Diagnostics; |
| using System.IO; |
| using System.Net; |
| using System.Net.Http; |
| using System.Net.Sockets; |
| using System.Threading.Tasks; |
| using Bazel; |
| using NUnit.Framework; |
| using OpenQA.Selenium.Internal; |
| |
| namespace OpenQA.Selenium.Environment; |
| |
| #nullable enable |
| |
| public class RemoteSeleniumServer |
| { |
| private Process? webserverProcess; |
| private readonly string serverJarName = @"java/src/org/openqa/selenium/grid/selenium_server_deploy.jar"; |
| private readonly string gridExecutableName = @"java/src/org/openqa/selenium/grid/selenium"; |
| private readonly string projectRoot; |
| private readonly bool autoStartServer; |
| private int serverPort; |
| |
| public static Uri ServerUri { get; private set; } = new Uri("http://127.0.0.1:6000/wd/hub/"); |
| |
| public RemoteSeleniumServer(string projectRoot, bool autoStartServer) |
| { |
| this.projectRoot = projectRoot; |
| this.autoStartServer = autoStartServer; |
| serverPort = autoStartServer ? FindAvailablePort() : ServerUri.Port; |
| if (autoStartServer) |
| { |
| UpdateServerUri(serverPort); |
| } |
| } |
| |
| public async Task StartAsync() |
| { |
| if (autoStartServer && (webserverProcess == null || webserverProcess.HasExited)) |
| { |
| const int maxAttempts = 5; |
| Exception? lastStartException = null; |
| for (int attempt = 1; attempt <= maxAttempts; attempt++) |
| { |
| var (executable, arguments) = FindServer(serverPort); |
| |
| webserverProcess = new Process(); |
| webserverProcess.StartInfo.FileName = executable; |
| webserverProcess.StartInfo.Arguments = arguments; |
| webserverProcess.StartInfo.WorkingDirectory = projectRoot; |
| webserverProcess.Start(); |
| DateTime timeout = DateTime.Now.Add(TimeSpan.FromSeconds(30)); |
| bool isRunning = false; |
| // Poll until the webserver is correctly serving pages. |
| using var httpClient = new HttpClient(); |
| |
| while (!isRunning && DateTime.Now < timeout) |
| { |
| try |
| { |
| using var response = await httpClient.GetAsync(StatusUri); |
| |
| if (response.StatusCode == HttpStatusCode.OK) |
| { |
| isRunning = true; |
| } |
| } |
| catch (Exception ex) when (ex is HttpRequestException || ex is TimeoutException) |
| { |
| lastStartException = ex; |
| } |
| } |
| |
| if (isRunning) |
| { |
| return; |
| } |
| |
| TestContext.Progress.WriteLine( |
| $"Remote Selenium server not ready on {StatusUri} (attempt {attempt}/{maxAttempts}). " + |
| $"Last error: {lastStartException?.Message ?? "none"}"); |
| |
| if (webserverProcess != null && !webserverProcess.HasExited) |
| { |
| webserverProcess.Kill(); |
| webserverProcess = null; |
| } |
| |
| if (attempt < maxAttempts) |
| { |
| serverPort = FindAvailablePort(); |
| UpdateServerUri(serverPort); |
| } |
| } |
| |
| throw new TimeoutException( |
| $"Could not start the remote selenium server in 30 seconds on port {serverPort}. " + |
| $"Last error: {lastStartException?.Message ?? "none"}"); |
| } |
| } |
| |
| private (string executable, string arguments) FindServer(int port) |
| { |
| string serverArgs = |
| $"standalone --port {port} --selenium-manager true --enable-managed-downloads true"; |
| |
| // Check Bazel runfiles first (for bazel test) |
| Exception? runfilesException = null; |
| try |
| { |
| var runfiles = Runfiles.Create(); |
| |
| // Check for grid executable in runfiles (use java -jar for all platforms) |
| string gridPath = runfiles.Rlocation("_main/" + gridExecutableName); |
| if (!string.IsNullOrEmpty(gridPath) && File.Exists(gridPath)) |
| { |
| return ("java", $"-jar \"{gridPath}\" {serverArgs}"); |
| } |
| } |
| catch (Exception ex) |
| { |
| // Runfiles not available (not running under Bazel) |
| runfilesException = ex; |
| } |
| |
| // Check bazel-bin path for grid executable |
| string bazelBinGridPath = Path.Combine(projectRoot, "bazel-bin", gridExecutableName.Replace('/', Path.DirectorySeparatorChar)); |
| if (File.Exists(bazelBinGridPath)) |
| { |
| return ("java", $"-jar \"{bazelBinGridPath}\" {serverArgs}"); |
| } |
| |
| // Check traditional path for JAR (for non-Bazel runs) |
| string traditionalPath = Path.Combine(projectRoot, serverJarName.Replace('/', Path.DirectorySeparatorChar)); |
| if (File.Exists(traditionalPath)) |
| { |
| return ("java", $"-jar \"{traditionalPath}\" {serverArgs}"); |
| } |
| |
| string runfilesDetail = runfilesException == null |
| ? string.Empty |
| : $" Runfiles error: {runfilesException.Message}"; |
| throw new FileNotFoundException( |
| $"Selenium server not found - please build it using: bazel build grid.{runfilesDetail}"); |
| } |
| |
| public async Task StopAsync() |
| { |
| if (autoStartServer && webserverProcess != null && !webserverProcess.HasExited) |
| { |
| using (var httpClient = new HttpClient()) |
| { |
| Exception? shutdownException = null; |
| try |
| { |
| using var response = await httpClient.GetAsync(ShutdownUri); |
| } |
| catch (Exception ex) when (ex is HttpRequestException || ex is TimeoutException) |
| { |
| shutdownException = ex; |
| } |
| if (shutdownException != null) |
| { |
| TestContext.Progress.WriteLine( |
| $"Remote Selenium server shutdown request failed at {ShutdownUri}: {shutdownException.Message}"); |
| } |
| } |
| |
| webserverProcess.WaitForExit(10000); |
| if (!webserverProcess.HasExited) |
| { |
| webserverProcess.Kill(); |
| } |
| |
| webserverProcess.Dispose(); |
| webserverProcess = null; |
| } |
| } |
| |
| private Uri StatusUri => new Uri($"http://localhost:{serverPort}/wd/hub/status"); |
| |
| private Uri ShutdownUri => |
| new Uri($"http://localhost:{serverPort}/selenium-server/driver?cmd=shutDownSeleniumServer"); |
| |
| private static int FindAvailablePort() |
| { |
| for (int attempt = 0; attempt < 10; attempt++) |
| { |
| int port = PortUtilities.FindFreePort(); |
| if (IsPortAvailable(port)) |
| { |
| return port; |
| } |
| } |
| |
| return PortUtilities.FindFreePort(); |
| } |
| |
| private static bool IsPortAvailable(int port) |
| { |
| try |
| { |
| using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); |
| socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); |
| return true; |
| } |
| catch (SocketException) |
| { |
| return false; |
| } |
| } |
| |
| private static void UpdateServerUri(int port) |
| { |
| ServerUri = new Uri($"http://127.0.0.1:{port}/wd/hub/"); |
| } |
| } |