GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
DriverHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using Microsoft.Win32;
18using Newtonsoft.Json.Linq;
19using OpenQA.Selenium;
20using System;
21using System.Collections.Generic;
22using System.Diagnostics;
23using System.IO;
24using System.IO.Compression;
25using System.Linq;
26using System.Net.Http;
27using System.Text;
28using System.Threading.Tasks;
29using static GenerallyPositive.Enums;
30
32{
33 internal static class DriverHelper
34 {
35 static IBrowser browser = null;
36 static string chromeVersionForSelector = string.Empty;
37 static string edgeVersionForSelector = string.Empty;
38 static bool updateSuccess = false;
39 static GPALFile versionFile;
40 static string version = string.Empty;
41 static string url;
42 static Selector showMoreFiles = GPAL.Selector.WithXPath(GPAL.GPALSettings.FirefoxDriverShowAllXpath).WithSelectorName("ShowMoreFilesFF").ToGPALObject();
43
44 public static void UpdateDriver(BrowserSettings browserSettings)
45 {
46 Selector tdSelector = null;
47
48 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Driver Auto Update running for [{browserSettings.BrowserType}]", browserSettings, GPALObjectType.Other);
49
50 // firefox does not scrape a page. Geckodriver publishes its releases, and it is not versioned against
51 // the browser, so neither the releases page nor the Firefox version can say whether we are current
52 if (BrowserType.FireFox == browserSettings.BrowserType)
53 {
54 UpdateFirefoxDriver(browserSettings);
55 return;
56 }
57
58 browser =
59 GPAL.Browser.WithBrowserType(browserSettings.BrowserType).WithOpenPDFExternally(false).WithDriverLocation(browserSettings.DriverLocation).ToGPALObject();
60
61 if (false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
62 browser.WithProfileDataDirectory(browserSettings.ProfileDataDirectory);
63
64 browser
65 .WithAutomationEngine(browserSettings.AutomationEngine);
66
67 switch (browser.BrowserType)
68 {
69 case Enums.BrowserType.Chrome:
70 chromeVersionForSelector = GetChromeVersion(out version);
71 tdSelector = CreateChromeSelector();
72 url = GPAL.GPALSettings.ChromeDriverUpdateURL;
73 break;
74 case Enums.BrowserType.Edge:
75 edgeVersionForSelector = GetEdgeVersion(out version);
76 tdSelector = CreateEdgeSelector();
77 url = GPAL.GPALSettings.EdgeDriverUpdateURL;
78 break;
79 case Enums.BrowserType.FireFox:
80 GetFirefoxVersion(out version);
81 tdSelector = CreateFirefoxSelector();
82 url = GPAL.GPALSettings.FirefoxDriverUpdateURL;
83 break;
84 }
85
86 IGPALGrid<string> lastUpdatedVersion = GPAL.Grid.ToGPALObject();
87 string driverLocation = FileHelper.EnsureDirectoryEndsWithBackslash(browserSettings.DriverLocation); // might not be set by user, could be system determined
88
89 // find our verions.yaml and read what version we last updated on
90 string yamlFile = Path.Combine(driverLocation, $"{browserSettings.BrowserType}.version.yaml");
91 versionFile = yamlFile;
92 GPAL.Converter.WithInput(versionFile).SaveTo(ref lastUpdatedVersion);
93
94 if (true == lastUpdatedVersion.Contains(version))
95 {
96 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Web driver already current for version [{version}]");
97 return; // already updated
98 }
99
100 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Attempting to update web driver for browser version [{version}]");
101
102 browser
103 .Get(url);
104
105 if (BrowserType.FireFox == browserSettings.BrowserType)
106 {
107 browser.LeftClick(showMoreFiles)
108 .WaitFor(1_000);
109 }
110
111 browser
112 .WithSelector(tdSelector)
113 .StartWorkflow();
114 }
115
125 private static void UpdateFirefoxDriver(BrowserSettings browserSettings)
126 {
127 string path = FileHelper.EnsureDirectoryEndsWithBackslash(browserSettings.DriverLocation);
128 string driver = GPAL.GPALSettings.FirefoxDriverFilename;
129 string zipFilename = GPAL.GPALSettings.FirefoxDriverZipFilename;
130 string[] nameParts = zipFilename.Split('*');
131 string installed = GetGeckodriverVersion($"{path}{driver}");
132
133 url = GPAL.GPALSettings.FirefoxDriverUpdateURL;
134 versionFile = Path.Combine(path, $"{BrowserType.FireFox}.version.yaml");
135
136 try
137 {
138 using (HttpClient httpClient = new HttpClient())
139 {
140 httpClient.DefaultRequestHeaders.Add("User-Agent", MagicHelper.GetUserAgentString(BrowserType.FireFox));
141
142 JObject release = JObject.Parse(httpClient.GetStringAsync(url).Result);
143 string latest = release["tag_name"]?.ToString().TrimStart('v');
144
145 JToken asset = release["assets"]?.FirstOrDefault(a =>
146 true == a["name"].ToString().StartsWith(nameParts[0], StringComparison.OrdinalIgnoreCase) &&
147 true == a["name"].ToString().EndsWith(nameParts[nameParts.Length - 1], StringComparison.OrdinalIgnoreCase));
148
149 if (true == string.Equals(installed, latest, StringComparison.OrdinalIgnoreCase))
150 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Web driver already current for geckodriver [{installed}]", browserSettings, GPALObjectType.Browser);
151 else if (null == asset)
152 {
153 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Geckodriver [{latest}] publishes no asset matching [{zipFilename}]", browserSettings, GPALObjectType.Browser);
154 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Please update driver manually at [{url}]", browserSettings, GPALObjectType.Browser);
155 }
156 else
157 DownloadAndExtractDriver(asset["browser_download_url"].ToString(), path, zipFilename.Replace("*", ""), driver, latest);
158 }
159 }
160 catch (Exception ex)
161 {
162 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to check for a geckodriver update at [{url}]", browserSettings, GPALObjectType.Browser, ex);
163 }
164 }
171 private static string GetGeckodriverVersion(string driverPath)
172 {
173 string installed = null;
174
175 if (true == File.Exists(driverPath))
176 {
177 Process process = Process.Start(new ProcessStartInfo
178 {
179 FileName = driverPath,
180 Arguments = "--version",
181 RedirectStandardOutput = true,
182 UseShellExecute = false,
183 CreateNoWindow = true
184 });
185
186 string[] parts = (process.StandardOutput.ReadLine() ?? string.Empty).Split(' ');
187 process.WaitForExit();
188
189 if (1 < parts.Length)
190 installed = parts[1];
191 }
192
193 return installed;
194 }
203 private static void DownloadAndExtractDriver(string href, string path, string zipFilename, string driver, string driverVersion)
204 {
205 File.Delete($"{path}{zipFilename}");
206 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Trying to download driver [{href}] to [{$"{path}{zipFilename}"}]");
207
208 using (HttpClient httpClient = new HttpClient())
209 {
210 httpClient.DefaultRequestHeaders.Add("User-Agent", MagicHelper.GetUserAgentString(BrowserType.FireFox));
211 File.WriteAllBytes($"{path}{zipFilename}", httpClient.GetByteArrayAsync(href).Result);
212 }
213
214 using (ZipArchive archive = ZipFile.OpenRead($"{path}{zipFilename}"))
215 {
216 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Extracting [{driver}] from zip file");
217
218 ZipArchiveEntry entryToExtract = archive.Entries.FirstOrDefault(e => e.Name.Equals(driver, StringComparison.OrdinalIgnoreCase));
219
220 if (null != entryToExtract)
221 {
222 string destinationFilePath = Path.Combine(path, entryToExtract.Name);
223
224 if (true == File.Exists(destinationFilePath))
225 File.Delete(destinationFilePath);
226
227 entryToExtract.ExtractToFile(destinationFilePath);
228 }
229 }
230
231 File.Delete($"{path}{zipFilename}");
232
233 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{BrowserType.FireFox}] web driver updated to geckodriver [{driverVersion}]");
234
235 IGPALGrid<string> gPALGrid = GPAL.Grid.ToGPALObject();
236 gPALGrid.AddRow(new List<string> { BrowserType.FireFox.ToString(), driverVersion });
237
238 GPAL.Converter.WithInput(gPALGrid).SaveTo(versionFile);
239 }
240 public static CallIfStatus CallIfFound(IBrowser browser, List<IGPALElement> foundElements, List<IGPALElement> matchedElements, Selector selector, bool matchedAll)
241 {
242 if (true == updateSuccess)
243 return CallIfStatus.Handled;
244
245 // last element will be highest available version which could be dev, beta or upcoming. first will be stable
246 GPALElement webElement = (GPALElement)matchedElements[0];
247 // string href = webElement.GetAttribute("href");
248 string href = string.Empty;
249 string path = ((Browser)browser).BrowserSettings.DriverLocation ?? ".\\";
250 string filename = string.Empty;
251 string driver = string.Empty;
252
253 switch (browser.BrowserType)
254 {
255 case Enums.BrowserType.Chrome:
256 filename = GPAL.GPALSettings.ChromeDriverZipFilename;
257 driver = GPAL.GPALSettings.ChromeDriverFilename;
258 href = webElement.Text;
259 break;
260 case Enums.BrowserType.Edge:
261 filename = GPAL.GPALSettings.EdgeDriverZipFilename;
262 driver = GPAL.GPALSettings.EdgeDriverFilename;
263 href = webElement.GetAttribute("href");
264 break;
265 case Enums.BrowserType.FireFox:
266 filename = GPAL.GPALSettings.FirefoxDriverZipFilename;
267 driver = GPAL.GPALSettings.FirefoxDriverFilename;
268 href = webElement.GetAttribute("href");
269 break;
270 }
271
272 browser.Close(true); // release chromedriver we are using...
273 //System.Threading.Thread.Sleep(2_000);
274
275 // NOTE: firefox special case "geckodriver*win64.zip" // must have * to split on for creating the selector, but we save to this filename
276 filename = filename.Replace("*", "");
277
278 File.Delete($"{path}{filename}");
279 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Trying to download driver [{href}] to [{$"{path}{filename}"}]");
280
281 try
282 {
283 using (HttpClient httpClient = new HttpClient())
284 {
285 httpClient.DefaultRequestHeaders.Add("User-Agent", MagicHelper.GetUserAgentString(browser.BrowserType));
286 byte[] data = httpClient.GetByteArrayAsync(href).Result;
287 File.WriteAllBytes($"{path}{filename}", data);
288 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{$"{path}{filename}"}] downloaded");
289 }
290 }
291 catch (Exception ex)
292 {
293 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to download from [{href}]", browser, GPALObjectType.Browser, ex);
294 }
295
296 if (true == File.Exists($"{path}{filename}"))
297 {
298 using (ZipArchive archive = ZipFile.OpenRead($"{path}{filename}"))
299 {
300 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Extracting [{driver}] from zip file", browser, GPALObjectType.Browser);
301
302 ZipArchiveEntry entryToExtract = archive.Entries.FirstOrDefault(e => e.Name.Equals(driver, StringComparison.OrdinalIgnoreCase));
303
304 if (entryToExtract != null)
305 {
306 string destinationFilePath = Path.Combine(path, entryToExtract.Name);
307
308 if (File.Exists(destinationFilePath))
309 File.Delete(destinationFilePath);
310
311 entryToExtract.ExtractToFile(destinationFilePath);
312 }
313 }
314
315 File.Delete($"{path}{filename}");
316 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{browser.BrowserType}] web driver updated for version [{version}]. NOTE: Geckodriver is not tied to browser version.", browser, Enums.GPALObjectType.Browser);
317 IGPALGrid<string> gPALGrid = GPAL.Grid.ToGPALObject();
318 gPALGrid.AddRow(new List<string> { browser.BrowserType.ToString(), version });
319
320 GPAL.Converter.WithInput(gPALGrid).SaveTo(versionFile);
321 }
322
323 // updateSuccess = true; - bug - don't need to set?
324 return CallIfStatus.Handled;
325 }
326 public static CallIfStatus CallIfNotFound(IBrowser browser, List<IGPALElement> foundElements, List<IGPALElement> matchedElements, Selector selector, bool matchedAll)
327 {
328 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Auto web driver update unable to find driver for version [{version}]", browser, Enums.GPALObjectType.Browser);
329 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Please update driver manually at [{url}]", browser, Enums.GPALObjectType.Browser);
330 browser.Close(true);
331
332 return CallIfStatus.Handled; // we handled, we will not terminate the workflow, but pershaps we should. the old driver could still work fine, don't call global handlers (if any)
333 }
334 private static Selector CreateSelector(string xpathExpression)
335 {
336 return GPAL.Selector
337 .WithXPath(xpathExpression) // generic xpath, use .WithAllThatMatch(#)
338 .CallIfNotFound(CallIfNotFound)
339 .CallIfFound(CallIfFound)
340 .WithSelectorName("DriverDownloadSelector")
341 .ToGPALObject();
342 }
343 private static Selector CreateChromeSelector()
344 {
345 string xpathExpression = GPAL.GPALSettings.ChromeDriverXpathExpression.Replace("@chromeVersionForSelector", chromeVersionForSelector).Replace("@chromeDriverZipFileName", GPAL.GPALSettings.ChromeDriverZipFilename);
346 return CreateSelector(xpathExpression);
347 }
348 internal static string GetChromeVersion(out string fullVersion)
349 {
350 string version = null;
351 RegistryKey regKey = null;
352
353 try
354 {
355 regKey = Registry.CurrentUser.OpenSubKey(GPAL.GPALSettings.ChromeVersionRegistry);
356 version = regKey.GetValue("version").ToString();
357 }
358 catch (Exception ex)
359 {
360 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Unable to get Chrome version from registry", null, Enums.GPALObjectType.None, ex);
361 }
362 finally
363 {
364 regKey?.Close();
365 }
366 fullVersion = version;
367
368 return string.Join(".", version.Split('.').Take(3)); // NOTEL CAVEAT: 144.0.7559.96 - we are taking 3 but that doesn't always work, but anything less and we will update every time
369 }
370 private static Selector CreateEdgeSelector()
371 {
372 string xpathExpression = GPAL.GPALSettings.EdgeDriverXpathExpression.Replace("@edgeDriverZipFilename", GPAL.GPALSettings.EdgeDriverZipFilename).Replace("@edgeVersionForSelector", edgeVersionForSelector);
373 return CreateSelector(xpathExpression);
374 }
375 internal static string GetEdgeVersion(out string fullVersion)
376 {
377 string version = null;
378 RegistryKey regKey = null;
379
380 try
381 {
382 regKey = Registry.CurrentUser.OpenSubKey(GPAL.GPALSettings.EdgeVersionRegistry);
383 version = regKey.GetValue("version").ToString();
384 }
385 catch (Exception ex)
386 {
387 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Unable to get Edge version from registry", null, Enums.GPALObjectType.None, ex);
388 }
389 finally
390 {
391 regKey?.Close();
392 }
393
394 fullVersion = version;
395 return version.Split('.')[0];
396 }
397 private static Selector CreateFirefoxSelector()
398 {
399 string[] nameParts = GPAL.GPALSettings.FirefoxDriverZipFilename.Split('*');
400 string xpathExpression = GPAL.GPALSettings.FirefoxDriverXpath.Replace("@firefoxDriverName1", nameParts[1]); //.Replace("@firefoxDriverName2", nameParts[1]);
401 Selector tmpSelector = CreateSelector(xpathExpression);
402 tmpSelector.ContainsHRef(nameParts[1]);
403
404 return tmpSelector;
405 }
406 internal static string GetFirefoxVersion(out string fullVersion)
407 {
408 string version = null;
409 RegistryKey regKey = null;
410
411 try
412 {
413 regKey = Registry.CurrentUser.OpenSubKey(GPAL.GPALSettings.FirefoxVersionRegistry);
414 version = regKey.GetValue("CurrentVersion")?.ToString();
415 if (true == string.IsNullOrEmpty(version))
416 {
417 regKey = Registry.CurrentUser.OpenSubKey(GPAL.GPALSettings.FirefoxVersionRegistry2);
418 version = regKey.GetValue("CurrentVersion")?.ToString();
419 }
420 }
421 catch (Exception ex)
422 {
423 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Unable to get Firefox version from registry", null, Enums.GPALObjectType.None, ex);
424 }
425 finally
426 {
427 regKey?.Close();
428 }
429 fullVersion = version;
430
431 return version.Split('.')[0];
432 }
433 }
434}
435
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Definition Browser.cs:68
IAllowBrowserSettingsOrGoTo WithBrowserType(BrowserType browserType)
Specify which browser to run the workflow in. NOTE: Chrome and Edge act mostly the same,...
Definition Browser.cs:488