Skip to content

Commit

Permalink
Fixes/rpa fix chromedriverdownload (#5690)
Browse files Browse the repository at this point in the history
* Elsa.Activities.Rpa.Web

Adding missing implementation of By.CssSelector on GetElements

* Fixing missing ]) on Message input at SendMessageMQTT

* Updating ChromeDriverInstaller.cs to support the new Google API chromeForTesting

https://developer.chrome.com/docs/chromedriver/downloads/version-selection

* Curly braces where inline on namespace

Changed curly braces in  namespace from inline to a new line

---------

Co-authored-by: Hugo Vilaça <[email protected]>
  • Loading branch information
hcfv and hvilacaequus authored Jul 5, 2024
1 parent 2c431f5 commit 6a7b4db
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public SendMqttMessage(IMessageSenderClientFactory messageSenderClientFactory)
Order = 2,
UIHint = ActivityInputUIHints.MultiLine,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string Message { get; set; } = default!;

[ActivityOutput(Hint = "Received message")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public WebActivityWithSelector(IServiceProvider sp) : base(sp)
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The type of selector to be used to identity the element",
Options = new[] { SelectorTypes.ByName, SelectorTypes.ById, SelectorTypes.ByCss },
Options = new[] { SelectorTypes.ByName, SelectorTypes.ById, SelectorTypes.ByCss, SelectorTypes.ByXPath },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? SelectorType { get; set; }
Expand Down Expand Up @@ -50,6 +50,7 @@ public WebActivityWithSelector(IServiceProvider sp) : base(sp)
{
case SelectorTypes.ById: { output.AddRange(driver.FindElements(By.Id(SelectorValue))); break; }
case SelectorTypes.ByName: { output.AddRange(driver.FindElements(By.Name(SelectorValue))); break; }
case SelectorTypes.ByCss: { output.AddRange(driver.FindElements(By.CssSelector(SelectorValue))); break; }
case SelectorTypes.ByXPath: { output.AddRange(driver.FindElements(By.XPath(SelectorValue))); break; }
case SelectorTypes.ByLinkText: { output.AddRange(driver.FindElements(By.LinkText(SelectorValue))); break; }
case SelectorTypes.Advanced:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Newtonsoft.Json;

namespace Elsa.Activities.Rpa.Web.DriverTypes.Chrome
{
public class Chrome
{
[JsonProperty("platform")]
public string Platform { get; set; }

[JsonProperty("url")]
public string Url { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System;

namespace Elsa.Activities.Rpa.Web.DriverTypes.Chrome
{
public class ChromeForTesting
{
[JsonProperty("timestamp")]
public DateTime Timestamp { get; set; }

[JsonProperty("versions")]
public List<ChromeVersion> Versions { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Newtonsoft.Json;

namespace Elsa.Activities.Rpa.Web.DriverTypes.Chrome
{
public class ChromeVersion
{
[JsonProperty("version")]
public string VersionNumber { get; set; }

[JsonProperty("revision")]
public string Revision { get; set; }

[JsonProperty("downloads")]
public Downloads Downloads { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Newtonsoft.Json;

namespace Elsa.Activities.Rpa.Web.DriverTypes.Chrome
{
public class Chromedriver
{
[JsonProperty("platform")] public string Platform { get; set; }

[JsonProperty("url")] public string Url { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Elsa.Activities.Rpa.Web.DriverTypes.Chrome
{
public class Downloads
{
[JsonProperty("chrome")]
public List<Chrome> Chrome { get; set; }

[JsonProperty("chromedriver")]
public List<Chromedriver> Chromedriver { get; set; }
}
}
159 changes: 106 additions & 53 deletions src/activities/Elsa.Activities.Rpa.Web/Services/ChromeDriverInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Elsa.Activities.Rpa.Web.DriverTypes.Chrome;
using Newtonsoft.Json;

namespace Elsa.Activities.Rpa.Web.Services
{
Expand All @@ -17,60 +20,108 @@ public class ChromeDriverInstaller
{
BaseAddress = new Uri("https://chromedriver.storage.googleapis.com/")
};

public Task Install() => Install(null, false);
public Task Install(string chromeVersion) => Install(chromeVersion, false);
public Task Install(bool forceDownload) => Install(null, forceDownload);

public async Task Install(string? chromeVersion, bool forceDownload)
private async Task Install(string? chromeVersion, bool forceDownload)
{
// Instructions from https://chromedriver.chromium.org/downloads/version-selection
// First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81.

chromeVersion ??= await GetChromeVersion();

// Take the Chrome version number, remove the last part,
chromeVersion = chromeVersion[..chromeVersion.LastIndexOf('.')];

// and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_".
// For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626".
var chromeDriverVersionResponse = await HttpClient.GetAsync($"LATEST_RELEASE_{chromeVersion}");
if (!chromeDriverVersionResponse.IsSuccessStatusCode)

// Take the Chrome version number, remove the build number
var chromeVersionWithoutBuiltNumber = chromeVersion[..chromeVersion.LastIndexOf('.')];

var chromeDriverVersion = chromeVersion;

var chromeVersionForTesting = new ChromeVersion();

// After 115 chromedriver changed the way it was downloaded so we need to account this new functionality https://developer.chrome.com/docs/chromedriver/downloads/version-selection
var useChromeForTestingDownloadApi = int.TryParse(chromeVersion.Split(".")[0], out var chromeMainVersionNumber) && chromeMainVersionNumber >= 115;

string zipName;
var driverName = "chromedriver";
string platformName;

var folderName = string.Empty;

if (useChromeForTestingDownloadApi)
{
if (chromeDriverVersionResponse.StatusCode == HttpStatusCode.NotFound)
var chromeToTestResponse = await HttpClient.GetAsync($"https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json");
var jsonResult = await chromeToTestResponse.Content.ReadAsStringAsync();
var chromeForTestingResult = JsonConvert.DeserializeObject<ChromeForTesting>(jsonResult);
chromeVersionForTesting = chromeForTestingResult?.Versions.SingleOrDefault(x => x.VersionNumber == chromeVersion)
?? chromeForTestingResult!.Versions.Last(x => x.VersionNumber.Contains(chromeVersionWithoutBuiltNumber));

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
folderName = RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "chromedriver-win64" : "chromedriver-win32";
zipName = Path.ChangeExtension(folderName, "zip");
driverName = Path.ChangeExtension(driverName, "exe");
platformName = RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "win64" : "win32";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
throw new Exception($"ChromeDriver version not found for Chrome version {chromeVersion}");
folderName = "chromedriver-linux64";
zipName = Path.ChangeExtension(folderName, "zip");
platformName = "linux64";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
folderName = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "chromedriver-mac-arm64" : "chromedriver-mac-x64";
zipName = Path.ChangeExtension(folderName, "zip");
platformName = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "mac-arm64" : "mac-x64";
}
else
{
throw new Exception($"ChromeDriver version request failed with status code: {chromeDriverVersionResponse.StatusCode}, reason phrase: {chromeDriverVersionResponse.ReasonPhrase}");
throw new PlatformNotSupportedException("Your operating system is not supported.");
}
}

var chromeDriverVersion = await chromeDriverVersionResponse.Content.ReadAsStringAsync();

string zipName;
string driverName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
zipName = "chromedriver_win32.zip";
driverName = "chromedriver.exe";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
zipName = "chromedriver_linux64.zip";
driverName = "chromedriver";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
zipName = "chromedriver_mac64.zip";
driverName = "chromedriver";
}
else
{
throw new PlatformNotSupportedException("Your operating system is not supported.");
// and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_".
// For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626".
var chromeDriverVersionResponse = await HttpClient.GetAsync($"LATEST_RELEASE_{chromeVersion}");
if (!chromeDriverVersionResponse.IsSuccessStatusCode)
{
if (chromeDriverVersionResponse.StatusCode == HttpStatusCode.NotFound)
{
throw new Exception($"ChromeDriver version not found for Chrome version {chromeVersion}");
}

throw new Exception($"ChromeDriver version request failed with status code: {chromeDriverVersionResponse.StatusCode}, reason phrase: {chromeDriverVersionResponse.ReasonPhrase}");
}

chromeDriverVersion = await chromeDriverVersionResponse.Content.ReadAsStringAsync();

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
zipName = "chromedriver_win32.zip";
driverName = "chromedriver.exe";
platformName = "win32";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
zipName = "chromedriver_linux64.zip";
driverName = "chromedriver";
platformName = "linux64";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
zipName = "chromedriver_mac64.zip";
driverName = "chromedriver";
platformName = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "mac-arm64" : "mac-x64";
}
else
{
throw new PlatformNotSupportedException("Your operating system is not supported.");
}
}

string targetPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var targetPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
targetPath = Path.Combine(targetPath, driverName);
if (!forceDownload && File.Exists(targetPath))
{
Expand All @@ -85,44 +136,46 @@ public async Task Install(string? chromeVersion, bool forceDownload)
RedirectStandardError = true,
}
)!;
string existingChromeDriverVersion = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();
var existingChromeDriverVersion = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
process.WaitForExit();
process.Kill();

// expected output is something like "ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784})"
// the following line will extract the version number and leave the rest
existingChromeDriverVersion = existingChromeDriverVersion.Split(" ")[1];
if (chromeDriverVersion == existingChromeDriverVersion)
{
return;
}

if (!string.IsNullOrEmpty(error))
{
throw new Exception($"Failed to execute {driverName} --version");
}
}

// Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.)
// Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/".
var driverZipResponse = await HttpClient.GetAsync($"{chromeDriverVersion}/{zipName}");
var driverZipResponse = useChromeForTestingDownloadApi
? await HttpClient.GetAsync(chromeVersionForTesting.Downloads.Chromedriver.Single(x => x.Platform == platformName).Url)
: await HttpClient.GetAsync($"{chromeDriverVersion}/{zipName}");
if (!driverZipResponse.IsSuccessStatusCode)
{
throw new Exception($"ChromeDriver download request failed with status code: {driverZipResponse.StatusCode}, reason phrase: {driverZipResponse.ReasonPhrase}");
}

// this reads the zipfile as a stream, opens the archive,
// and extracts the chromedriver executable to the targetPath without saving any intermediate files to disk
using (var zipFileStream = await driverZipResponse.Content.ReadAsStreamAsync())
using (var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
using (var chromeDriverWriter = new FileStream(targetPath, FileMode.Create))
{
var entry = zipArchive.GetEntry(driverName)!;
var entry = zipArchive.GetEntry($"{folderName}/{driverName}")!;
using Stream chromeDriverStream = entry.Open();
await chromeDriverStream.CopyToAsync(chromeDriverWriter);
}

// on Linux/macOS, you need to add the executable permission (+x) to allow the execution of the chromedriver
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Expand All @@ -140,14 +193,14 @@ public async Task Install(string? chromeVersion, bool forceDownload)
string error = await process.StandardError.ReadToEndAsync();
process.WaitForExit();
process.Kill();

if (!string.IsNullOrEmpty(error))
{
throw new Exception("Failed to make chromedriver executable");
}
}
}

public async Task<string> GetChromeVersion()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand All @@ -157,7 +210,7 @@ public async Task<string> GetChromeVersion()
{
throw new Exception("Google Chrome not found in registry");
}

var fileVersionInfo = FileVersionInfo.GetVersionInfo(chromePath);
return fileVersionInfo.FileVersion;
}
Expand All @@ -180,12 +233,12 @@ public async Task<string> GetChromeVersion()
string error = await process.StandardError.ReadToEndAsync();
process.WaitForExit();
process.Kill();

if (!string.IsNullOrEmpty(error))
{
throw new Exception(error);
}

return output;
}
catch (Exception ex)
Expand All @@ -212,12 +265,12 @@ public async Task<string> GetChromeVersion()
string error = await process.StandardError.ReadToEndAsync();
process.WaitForExit();
process.Kill();

if (!string.IsNullOrEmpty(error))
{
throw new Exception(error);
}

output = output.Replace("Google Chrome ", "");
return output;
}
Expand Down

0 comments on commit 6a7b4db

Please sign in to comment.