58 static bool alreadyUpdating =
false;
60 static List<string> lastErrorMessage =
new List<string>();
61 static bool supressedMessage =
false;
62 static string lastSessionToken;
73 IWebDriver tmpDriver =
null;
74 ChromiumOptions options =
null;
75 bool waitForDebugger =
false;
76 string userAgent =
GPAL.GPALSettings.UserAgent;
78 if (
null != browserSettings.BrowserDriver)
79 return browserSettings.BrowserDriver;
81 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Running workflow for Automation Engine [{browserSettings.AutomationEngine}]",
null, GPALObjectType.None);
83 if (
null == browserSettings.DriverLocation)
85 browserSettings.DriverLocation = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6);
87 if (
true ==
GPAL.GPALSettings.AutoUpdateWebDriver &&
false == alreadyUpdating)
89 alreadyUpdating =
true;
90 DriverHelper.UpdateDriver(browserSettings);
98 if (BrowserType.Chrome == browserSettings.BrowserType || BrowserType.Edge == browserSettings.BrowserType)
100 if (BrowserType.Chrome == browserSettings.BrowserType)
101 options =
new ChromeOptions();
103 options =
new EdgeOptions();
107 if (
null != browserSettings.DialogsAccepted)
108 options.UnhandledPromptBehavior =
true == browserSettings.DialogsAccepted
109 ? UnhandledPromptBehavior.Accept
110 : UnhandledPromptBehavior.Dismiss;
112 if (
true == browserSettings.UseExistingBrowser)
114 options.DebuggerAddress =
"127.0.0.1:" + browserSettings.ExistingBrowserPort.ToString();
115 waitForDebugger =
true;
119 if (
null == browserSettings.DebugPort &&
true == browserSettings.UsePuppeteer &&
false == browserSettings.DebugPipe)
123 browserSettings.DebugPort = FindFreePort();
124 browserSettings.PuppeteerUrl = $
"http://localhost:{browserSettings.DebugPort}";
128 if (
false ==
string.IsNullOrEmpty(browserSettings.DownloadLocation))
129 options.AddUserProfilePreference(
"download.default_directory", browserSettings.DownloadLocation);
133 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileUserName) ||
false ==
string.IsNullOrEmpty(browserSettings.ProfileName))
137 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileName))
142 string profileName =
FindProfileDirectory(browserSettings.ProfileDataDirectory, browserSettings.ProfileName);
143 options.AddArguments($
"--profile-directory={profileName ?? browserSettings.ProfileName}");
146 else if (
true ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
149 browserSettings.ProfileDataDirectory = ChromeProfileManager.CreateTempUserProfile(
151 browserSettings.PromptForDownload,
152 browserSettings.OpenPDFExternally,
153 browserSettings.LoadImages,
154 browserSettings.UseOttoMagic
156 browserSettings.TempProfileCreated =
true;
162 if (
false == browserSettings.TempProfileCreated &&
false ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
163 browserSettings.PreviousDownloadPreferences = ChromeProfileManager.ApplyDownloadPreferences(
164 browserSettings.ProfileDataDirectory,
165 browserSettings.DownloadLocation,
166 browserSettings.PromptForDownload,
167 browserSettings.OpenPDFExternally);
172 if (
false == browserSettings.TempProfileCreated)
173 RefuseIfProfileIsOpen(browserSettings);
175 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
176 options.AddArgument($
"--user-data-dir={browserSettings.ProfileDataDirectory.Replace("\\
", "/
")}");
186 options.AddArguments(
"--disable-extensions");
192 string disableFeatures =
"TranslateUI,Translate";
194 if (BrowserType.Edge == browserSettings.BrowserType)
195 disableFeatures +=
",msShoppingTrigger,msShopping,msEdgeShoppingUI,msEdgeShoppingList";
197 options.AddArgument($
"--disable-features={disableFeatures}");
200 options.AddArgument(
"--disable-infobars");
201 options.AddArgument(
"--disable-session-crashed-bubble");
202 options.AddArgument(
"--hide-crash-restore-bubble");
203 options.AddArgument(
"--noerrdialogs");
205 if (
true == browserSettings.UseHeadless)
208 options.AddArgument(
"--headless=new");
217 options.AddArgument(
"--disable-notifications");
219 options.AddArgument(
"--disable-popup-blocking");
226 string currUserAgent;
228 if (
false == browserSettings.UserAgentFromBrowser)
229 currUserAgent = ResolveUserAgent(browserSettings);
232 if (BrowserType.Chrome == browserSettings.BrowserType)
233 tmpDriver =
new ChromeDriver(browserSettings.DriverLocation, (ChromeOptions)options);
235 tmpDriver =
new EdgeDriver(browserSettings.DriverLocation, (EdgeOptions)options);
237 IJavaScriptExecutor js = (IJavaScriptExecutor)tmpDriver;
238 currUserAgent = (String)js.ExecuteScript(
"return navigator.userAgent");
242 options.AddArgument($
"--user-agent={currUserAgent.Replace("Headless
", "")}");
244 options.AddUserProfilePreference(
"download.prompt_for_download",
false);
248 if (
true == browserSettings.PromptForDownload)
249 options.AddUserProfilePreference(
"download.prompt_for_download",
true);
251 options.AddUserProfilePreference(
"download.prompt_for_download",
false);
258 options.AddUserProfilePreference(
"download.directory_upgrade",
true);
260 options.AddUserProfilePreference(
"plugins.always_open_pdf_externally", browserSettings.OpenPDFExternally);
261 options.AddArguments($
"--blink-settings=imagesEnabled={browserSettings.LoadImages.ToString().ToLower()}");
262 options.AddUserProfilePreference(
"disable-popup-blocking", $
"{!browserSettings.BlockPopUps}");
265 if (browserSettings.StealthType.HasFlag(StealthType.DarkMode))
266 options.AddArgument(
"--force-dark-mode");
269 if (
true == browserSettings.DebugPipe)
270 options.AddArgument($
"--remote-debugging-pipe --enable-unsafe-extension-debugging");
271 else if (
null != browserSettings.DebugPort)
272 options.AddArgument($
"--remote-debugging-port={browserSettings.DebugPort.Value}");
279 options.AddArguments(
"--disable-blink-features=AutomationControlled");
282 options.AddExcludedArgument(
"enable-automation");
283 if (options is ChromeOptions chromeOpts)
284 chromeOpts.AddAdditionalChromeOption(
"useAutomationExtension",
false);
285 else if (options is EdgeOptions edgeOpts)
286 edgeOpts.AddAdditionalEdgeOption(
"useAutomationExtension",
false);
289 if (browserSettings.StealthType.HasFlag(StealthType.PatchDriver))
291 if (BrowserType.Edge == browserSettings.BrowserType)
292 EdgePatcher.PatchEdgeDriver(browserSettings.Browser);
294 ChromePatcher.PatchChromeDriver(browserSettings.Browser);
303 options.SetLoggingPreference(OpenQA.Selenium.LogType.Performance, LogLevel.All);
305 tmpDriver = GetWebDriver(browserSettings, options);
312 else if (BrowserType.FireFox == browserSettings.BrowserType)
315 FirefoxDriverService firefoxDriverService = FirefoxDriverService.CreateDefaultService(browserSettings.DriverLocation);
316 FirefoxOptions ffOptions =
new FirefoxOptions();
318 if (
null != browserSettings.DialogsAccepted)
319 ffOptions.UnhandledPromptBehavior =
true == browserSettings.DialogsAccepted
320 ? UnhandledPromptBehavior.Accept
321 : UnhandledPromptBehavior.Dismiss;
322 if (
true == browserSettings.UseExistingBrowser)
324 firefoxDriverService.HideCommandPromptWindow =
true;
325 firefoxDriverService.Port = 0;
326 firefoxDriverService.Host =
"127.0.0.1";
327 firefoxDriverService.ConnectToRunningBrowser =
true;
330 firefoxDriverService.BrowserCommunicationPort = browserSettings.ExistingBrowserPort ?? 2828;
332 tmpDriver =
new FirefoxDriver(firefoxDriverService, ffOptions);
334 waitForDebugger =
true;
339 ffOptions.AddArgument($
"-width={browserSettings.WindowSize.Width}");
340 ffOptions.AddArgument($
"-height={browserSettings.WindowSize.Height}");
342 if (
true == browserSettings.UseHeadless)
346 ffOptions.AddArgument(
"-headless");
349 ffOptions.AddArgument(
"-disable-notifications");
356 if (
null != browserSettings.DebugPort)
360 firefoxDriverService.Port = (int)browserSettings.DebugPort.Value;
365 if (
true == browserSettings.UsePuppeteer)
366 browserSettings.PuppeteerUrl = $
"http://localhost:{browserSettings.DebugPort}";
368 if (
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory) &&
string.IsNullOrEmpty(browserSettings.ProfileName) &&
string.IsNullOrEmpty(browserSettings.ProfileUserName))
369 ffOptions.Profile =
new OpenQA.Selenium.Firefox.FirefoxProfileManager().GetProfile(
"default");
371 else if (
true == browserSettings.UsePuppeteer)
377 if (
null == browserSettings.DebugPort)
378 browserSettings.DebugPort = FindFreePort();
380 browserSettings.PuppeteerUrl = $
"http://localhost:{browserSettings.DebugPort}";
383 if (
null != browserSettings.DebugPort)
384 ffOptions.AddArgument($
"--remote-debugging-port={browserSettings.DebugPort.Value} ");
386 ffOptions.SetPreference(
"print.always_print_silent",
true);
387 ffOptions.SetPreference(
"print.show_print_progress",
false);
393 ffOptions.SetPreference(
"pdfjs.disabled", browserSettings.OpenPDFExternally || browserSettings.UseHeadless);
395 if (
false == browserSettings.PromptForDownload)
396 ffOptions.SetPreference(
"browser.helperApps.neverAsk.saveToDisk",
GPAL.GPALSettings.FirefoxDirectDownloadMimeTypes);
398 ffOptions.SetPreference(
"browser.helperApps.neverAsk.saveToDisk",
"");
400 if (
false == browserSettings.LoadImages)
401 ffOptions.SetPreference(
"permissions.default.image", 0);
403 ffOptions.SetPreference(
"permissions.default.image", 1);
407 ffOptions.SetPreference(
"dom.popup_maximum",
true == browserSettings.BlockPopUps ? 0 : 20);
409 ffOptions.SetPreference(
"safebrowsing.enabled",
false);
411 if (
false ==
string.IsNullOrEmpty(browserSettings.DownloadLocation))
413 ffOptions.SetPreference(
"browser.download.dir", browserSettings.DownloadLocation);
414 ffOptions.SetPreference(
"browser.download.folderList", 2);
417 ffOptions.SetPreference(
"dom.webdriver.enabled",
false);
419 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileUserName) ||
false ==
string.IsNullOrEmpty(browserSettings.ProfileName))
421 ffOptions.AddArgument($
"-profile {browserSettings.ProfileName ?? browserSettings.ProfileUserName}");
422 else if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
423 ffOptions.AddArgument($
"-profile {browserSettings.ProfileDataDirectory}");
441 if (
true == browserSettings.HiddenDesktop)
445 int hiddenPort = StartHiddenDriver(browserSettings);
447 tmpDriver =
new OpenQA.Selenium.Remote.RemoteWebDriver(
new Uri($
"http://localhost:{hiddenPort}"), ffOptions);
451 FirefoxDriverService firefoxService = FirefoxDriverService.CreateDefaultService(browserSettings.DriverLocation);
452 firefoxService.HideCommandPromptWindow =
true;
454 tmpDriver =
new FirefoxDriver(firefoxService, ffOptions);
457 browserSettings.ServiceDriverPid = firefoxService.ProcessId;
465 browserSettings.Process = SeleniumBrowserProcess(tmpDriver,
"moz:processID");
476 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to start browser driver for [{browserSettings.BrowserType}].", browserSettings.Browser, GPALObjectType.Browser, ex);
481 if (
null != tmpDriver)
483 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Browser [{browserSettings.BrowserType}] launched.", browserSettings, GPALObjectType.Other);
485 browserSettings.BrowserDriver = tmpDriver;
492 var edgeGhostBrowser = browserSettings.Browser as
Browser;
493 if (BrowserType.Edge == browserSettings.BrowserType &&
null != edgeGhostBrowser)
494 try { edgeGhostBrowser.GhostTabUrl = tmpDriver.Url; }
498 tmpDriver.Manage().Timeouts().PageLoad.Add(System.TimeSpan.FromSeconds(30));
502 browserSettings.CurrentURL =
"https://google.com";
504 if (
true == browserSettings.Maximize)
505 tmpDriver.Manage().Window.
Maximize();
506 else if (
true == browserSettings.Minimize)
507 tmpDriver.Manage().Window.Minimize();
508 else if (
true == browserSettings.FullScreen)
509 tmpDriver.Manage().Window.FullScreen();
510 else if (
false == browserSettings.WindowSize.IsEmpty)
513 var windowSize =
new Size(browserSettings.WindowSize.Width, browserSettings.WindowSize.Height);
514 tmpDriver.Manage().Window.Size = windowSize;
517 var windowPosition =
new Point(browserSettings.WindowSize.Left, browserSettings.WindowSize.Top);
518 tmpDriver.Manage().Window.Position = windowPosition;
521 if (
true == waitForDebugger)
524 if (BrowserType.Chrome == browserSettings.BrowserType)
526 DriverHelper.GetChromeVersion(out browserSettings.Version);
528 else if (BrowserType.Edge == browserSettings.BrowserType)
530 DriverHelper.GetEdgeVersion(out browserSettings.Version);
532 else if (BrowserType.FireFox == browserSettings.BrowserType)
534 DriverHelper.GetFirefoxVersion(out browserSettings.Version);
537 CheckDocumentReady(((
Browser)browserSettings.Browser),
true);
557 var parameters =
new Dictionary<string, object>
559 {
"behavior",
"allow" },
560 {
"downloadPath", directory }
563 ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.setDownloadBehavior", parameters);
576 internal static string ResolveUserAgent(
BrowserSettings browserSettings)
578 string userAgent = browserSettings.OverrideUserAgent;
580 if (
true ==
string.IsNullOrWhiteSpace(userAgent))
581 userAgent =
GPAL.GPALSettings.UserAgent;
583 if (
true ==
string.IsNullOrWhiteSpace(userAgent))
601 internal static void PresentCredentials(
Browser browser, GPALUrl url)
604 WebAuthType authType = credential.WebAuthType;
605 string header = AuthorizationHeader(credential, authType);
607 browser.BrowserSettings.CredentialsPresented = DateTime.UtcNow;
610 if (WebAuthType.Form == authType || WebAuthType.None == authType)
613 if (
true ==
string.IsNullOrEmpty(header) || BrowserType.FireFox == browser.BrowserSettings.BrowserType ||
true == browser.UseOttoMagic)
614 CredentialsInUrl(browser, url, credential);
617 string name = AuthorizationHeaderName(authType);
618 Dictionary<string, object> headers =
new Dictionary<string, object> { { name, header } };
620 if (
true == browser.UsePuppeteer)
621 browser.PuppeteerCommunicator.SetExtraHttpHeaders(headers).GetAwaiter().GetResult();
623 ((OpenQA.Selenium.Chromium.ChromiumDriver)browser.BrowserDriver).ExecuteCdpCommand(
"Network.setExtraHTTPHeaders",
624 new Dictionary<string, object> { {
"headers", headers } });
626 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Credentials presented as [{authType}] for [{url?.Url}]", browser, GPALObjectType.Browser);
635 internal static string AuthorizationHeader(Credentials credential, WebAuthType authType)
639 if (WebAuthType.Basic == authType || WebAuthType.Proxy == authType)
640 value =
"Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($
"{credential.Username}:{credential.Password}"));
641 else if (WebAuthType.Bearer == authType)
642 value =
"Bearer " + credential.AccessToken;
643 else if (WebAuthType.XAuthToken == authType || WebAuthType.XApiKey == authType || WebAuthType.ApiKey == authType)
644 value = credential.AccessToken;
654 internal static string AuthorizationHeaderName(WebAuthType authType)
656 string name =
"Authorization";
658 if (WebAuthType.Proxy == authType)
659 name =
"Proxy-Authorization";
660 else if (WebAuthType.XAuthToken == authType)
661 name =
"X-Auth-Token";
662 else if (WebAuthType.XApiKey == authType)
664 else if (WebAuthType.ApiKey == authType)
677 static void CredentialsInUrl(Browser browser, GPALUrl url, Credentials credential)
679 UriBuilder builder =
new UriBuilder(url?.Url)
681 UserName = Uri.EscapeDataString(credential.Username ??
string.Empty),
682 Password = Uri.EscapeDataString(credential.Password ??
string.Empty)
684 GPALUrl credentialed =
new GPALUrl(builder.Uri.ToString());
688 if (
true == browser.UseOttoMagic)
689 browser.MagicHelper.GoTo(credentialed);
690 else if (
true == browser.UsePuppeteer)
691 browser.PuppeteerClient.GoTo(credentialed).Execute();
695 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Credentials presented to [{builder.Host}], answering its challenge", browser, GPALObjectType.Browser);
712 private static Process SeleniumBrowserProcess(IWebDriver driver,
string capability)
714 Process retVal =
null;
718 object pid = (driver as IHasCapabilities)?.Capabilities?.GetCapability(capability);
721 retVal = Process.GetProcessById(Convert.ToInt32(pid));
727 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"No browser process behind the driver for [{capability}]",
null, GPALObjectType.Browser, ex);
732 internal static void WaitForBrowserToExit(BrowserSettings browserSettings,
int timeoutMs)
736 browserSettings.Process?.WaitForExit(timeoutMs);
740 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not wait on the browser process before restoring [{browserSettings.ProfileDataDirectory}]", browserSettings.Browser, GPALObjectType.Browser, ex);
759 string directory = Path.GetDirectoryName(destination);
760 string filename = Path.GetFileName(destination);
762 var enableDownloadCommandParameters =
new Dictionary<string, object>
764 {
"behavior",
"allow" },
765 {
"downloadPath", directory },
766 {
"filename", filename }
769 if (BrowserType.Chrome == browserSettings.BrowserType || BrowserType.Edge == browserSettings.BrowserType)
770 ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.setDownloadBehavior", enableDownloadCommandParameters);
787 internal const string CallRecorderScript =
@"
789 if (window.__gpalCalls) return;
791 window.__gpalCalls = [];
793 var record = function (url, method, headers, body, type) {
794 var call = { url: String(url), method: method || 'GET', type: type, headers: headers || {}, postData: body || null, status: 0 };
795 window.__gpalCalls.push(call);
799 var nativeFetch = window.fetch;
802 window.fetch = function (input, init) {
803 var url = input && input.url ? input.url : input;
805 var options = init || {};
807 // a Headers object, a plain object or an array of pairs, all of which fetch accepts
808 if (options.headers && options.headers.forEach)
809 options.headers.forEach(function (v, k) { headers[k] = v; });
810 else if (options.headers)
811 Object.keys(options.headers).forEach(function (k) { headers[k] = options.headers[k]; });
813 var call = record(url, options.method, headers, options.body, 'Fetch');
815 return nativeFetch.apply(this, arguments).then(function (response) {
816 call.status = response.status;
818 }, function (error) {
824 var nativeOpen = XMLHttpRequest.prototype.open;
825 var nativeSend = XMLHttpRequest.prototype.send;
826 var nativeSetHeader = XMLHttpRequest.prototype.setRequestHeader;
828 XMLHttpRequest.prototype.open = function (method, url) {
829 this.__gpalCall = { method: method, url: url, headers: {} };
830 return nativeOpen.apply(this, arguments);
833 XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
834 if (this.__gpalCall) this.__gpalCall.headers[name] = value;
835 return nativeSetHeader.apply(this, arguments);
838 XMLHttpRequest.prototype.send = function (body) {
839 var pending = this.__gpalCall;
842 var call = record(pending.url, pending.method, pending.headers, body, 'XHR');
843 this.addEventListener('loadend', function () { call.status = this.status; });
846 return nativeSend.apply(this, arguments);
851 public static string SeleniumAddScriptToEvaluateOnNewDocument(
BrowserSettings browserSettings,
string script)
853 if (BrowserType.Chrome != browserSettings.BrowserType && BrowserType.Edge != browserSettings.BrowserType)
855 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{browserSettings.BrowserType}] does not support Page.addScriptToEvaluateOnNewDocument", browserSettings.Browser, GPALObjectType.Browser);
859 var result = ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.addScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
862 }) as Dictionary<string, object>;
864 return result?[
"identifier"]?.ToString();
875 if (BrowserType.Chrome != browserSettings.BrowserType && BrowserType.Edge != browserSettings.BrowserType)
877 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{browserSettings.BrowserType}] does not support Page.removeScriptToEvaluateOnNewDocument", browserSettings.Browser, GPALObjectType.Browser);
881 ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.removeScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
883 {
"identifier", identifier }
894 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
895 string profilePath =
string.Empty;
897 switch (browserSettings.BrowserType)
899 case BrowserType.Chrome:
900 profilePath = Path.Combine(localAppData,
"Google",
"Chrome",
"User Data");
902 case BrowserType.FireFox:
903 profilePath = Path.Combine(localAppData,
"Mozilla",
"Firefox",
"Profiles");
905 case BrowserType.Edge:
906 profilePath = Path.Combine(localAppData,
"Microsoft",
"Edge",
"User Data");
909 Exception ex =
new Exception(
"Unsupported Browser Type");
910 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unsupported browser type for loading user profile [{browserSettings.ProfileUserName}]: [{browserSettings.BrowserType.ToString()}]", ex);
914 string newProfilePath = profilePath;
917 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileUserName) &&
false == getDefaultDirectory)
919 newProfilePath = Path.Combine(profilePath, browserSettings.ProfileUserName);
921 if (!Directory.Exists(newProfilePath))
923 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"User name [{browserSettings.ProfileUserName}] profile directory does not exist : [{browserSettings.BrowserType.ToString()}]");
928 return newProfilePath;
941 string[] profileDirectories = Directory.GetDirectories(userDataDirectory);
943 foreach (
string profileDirectory
in profileDirectories)
945 string preferencesFile = Path.Combine(profileDirectory,
"Preferences");
947 if (File.Exists(preferencesFile))
949 string profileNameFromFile = GetProfileNameFromPreferencesFile(preferencesFile);
950 if (
null != profileNameFromFile && profileNameFromFile.Equals(profileName, StringComparison.OrdinalIgnoreCase))
952 return new DirectoryInfo(profileDirectory).Name;
965 static string GetProfileNameFromPreferencesFile(
string preferencesFilePath)
969 string preferencesContent = File.ReadAllText(preferencesFilePath);
970 JObject preferencesJson = JObject.Parse(preferencesContent);
973 JToken givenNameToken = preferencesJson.SelectToken(
"profile.name");
974 if (givenNameToken !=
null)
976 return givenNameToken.ToString();
995 using (RegistryKey key = Registry.LocalMachine.OpenSubKey(
GPAL.GPALSettings.FirefoxBinaryPathRegistry))
1000 string firefoxPath = key.GetValue(
null)?.ToString();
1001 if (!
string.IsNullOrEmpty(firefoxPath))
1017 var handles = browserSettings.BrowserDriver.WindowHandles;
1018 foreach (var handle
in handles)
1020 browserSettings.BrowserDriver.SwitchTo().Window(handle);
1021 if (URL.Contains(browserSettings.BrowserDriver.Url) || browserSettings.BrowserDriver.Url.Contains(URL))
1038 List<GPALElement> matchedElems =
null;
1039 ReadOnlyCollection<GPALElement> elems =
null;
1040 bool clicked =
false;
1052 browser.CurrentUOW.NextPageButton.WebSelectorFoundResults =
null;
1053 browser.CurrentUOW.NextPageButton.WebSelectorMatchedResults =
null;
1056 sel.WebSelectorFoundResults =
null;
1057 sel.WebSelectorMatchedResults =
null;
1060 elems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, browser.CurrentUOW.
NextPageButton, out
bool matchedAll, out matchedElems,
null,
false);
1063 if (
null != elems && 0 < elems.Count &&
true == matchedAll)
1068 if (
false ==
string.IsNullOrEmpty(elems[0].Href))
1070 string href = elems[0].Href;
1073 browser.BrowserSettings.ObeyRobotsTxt =
false;
1075 browser.CurrentUOW = saveUOW;
1076 browser.BrowserSettings.ObeyRobotsTxt = obeySave;
1077 browser.BrowserSettings.CurrentURL = href;
1092 catch (Exception ex)
1094 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Exception thrown", browser, GPALObjectType.Browser, ex);
1099 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Unable to find Next Page Button", browser, GPALObjectType.Browser);
1105 HardwareHelper.SendKey(VK_END);
1109 CheckDocumentReady(browser);
1132 List<ReadOnlyCollection<GPALElement>> rowsOfColumns =
new List<ReadOnlyCollection<GPALElement>>();
1133 List<ReadOnlyCollection<GPALElement>> rowsOfElements =
new List<ReadOnlyCollection<GPALElement>>();
1136 List<SmallSelectorNode> interactionInfo =
new List<SmallSelectorNode>();
1146 interactionInfo.Clear();
1147 rowsOfElements.Clear();
1148 rowsOfColumns.Clear();
1149 browser.CurrentUOW.MatchedRowIndexes.Clear();
1150 browser.CurrentUOW.ColCount = 0;
1151 browser.CurrentUOW.RowCount = 0;
1152 browser.CurrentUOW.PartialMatch =
false;
1156 if (SelectorType.Selector != sel.SelectorType)
1160 List<GPALElement> matchedElems =
null;
1161 ReadOnlyCollection<GPALElement> tmpElems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, sel, out
bool matchedAll, out matchedElems);
1162 rowsOfElements.Add(tmpElems);
1163 browser.CurrentUOW.ColCount++;
1165 if (
false == matchedAll)
1166 browser.CurrentUOW.PartialMatch |=
true;
1168 if (0 < tmpElems?.Count &&
true == matchedAll)
1171 browser.CurrentUOW.RowCount = Math.Max(browser.CurrentUOW.RowCount, (
int.MaxValue == browser.CurrentUOW.
WithAllThatMatch ? tmpElems.Count : browser.CurrentUOW.
WithAllThatMatch));
1173 else if (0 < matchedElems?.Count &&
false == matchedAll)
1182 if (-1 != (tmpIdx = tmpElems.IndexOf(webElement)))
1183 if (
false == browser.CurrentUOW.MatchedRowIndexes.Contains(
new KeyValuePair<int, string>(tmpIdx, sel.AttributeName)))
1184 browser.CurrentUOW.MatchedRowIndexes.Add(tmpIdx, sel.AttributeName);
1188 browser.CurrentUOW.RowCount = (int.MaxValue == browser.CurrentUOW.WithAllThatMatch ? browser.CurrentUOW.MatchedRowIndexes.Count : browser.CurrentUOW.
WithAllThatMatch);
1192 if (0 < matchedElems?.Count || 0 < tmpElems?.Count)
1193 interactionInfo.Add(
new SmallSelectorNode() { InteractionType = sel.
InteractionType, OffsetX = sel.
OffsetX, OffsetY = sel.
OffsetY, DeltaX = sel.
DeltaX, DeltaY = sel.DeltaY });
1199 List<GPALElement> newColumn =
new List<GPALElement>();
1201 browser.CurrentUOW.RowCount = browser.CurrentUOW.MatchedRowIndexes.Count;
1205 foreach (ReadOnlyCollection<GPALElement> webElements
in rowsOfElements)
1209 foreach (KeyValuePair<int, string> idx
in browser.CurrentUOW.MatchedRowIndexes)
1210 newColumn.Add(webElements[idx.Key]);
1211 rowsOfColumns.Add(
new ReadOnlyCollection<GPALElement>(newColumn));
1212 newColumn =
new List<GPALElement>();
1217 foreach (ReadOnlyCollection<GPALElement> webElements
in rowsOfElements)
1218 rowsOfColumns.Add(webElements);
1222 browser.CurrentUOW.ElementGrid.Clear();
1225 if (0 != browser.CurrentUOW.ColCount)
1227 for (
int cnt = 0; cnt < browser.CurrentUOW.RowCount; cnt++)
1228 browser.CurrentUOW.ElementGrid.AddRow(
new List<UnitOfWork.ElementNode>(browser.CurrentUOW.ColCount));
1230 int columnIdx = 0; ;
1235 foreach (ReadOnlyCollection<GPALElement> column
in rowsOfColumns)
1237 rowCnt = lastRowCnt;
1240 if (
true == ElementHelper.IsElementEditable(elem))
1241 browser.CurrentUOW.ElementGrid[rowCnt].Add(
1245 InteractionType = interactionInfo[columnIdx].InteractionType,
1246 OffsetX = interactionInfo[columnIdx].OffsetX,
1247 OffsetY = interactionInfo[columnIdx].OffsetY
1265 if (tokenIdx < tokens.Count())
1269 foreach (List<UnitOfWork.ElementNode> elementNode
in browser.CurrentUOW.ElementGrid)
1272 List<string> currentRow = tokens[tokenIdx];
1274 foreach (
string token
in currentRow)
1276 if (elementIdx < elementNode.Count &&
null != elementNode[elementIdx])
1278 ElementHelper.ScrollIntoView(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].InteractionType);
1282 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
1283 ElementHelper.HardwareFillInFrom(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
1287 case WriteMode.Append:
1291 case WriteMode.Insert:
1295 case WriteMode.Overwrite:
1302 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
1303 ElementHelper.HardwareFillInFrom(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
1307 case WriteMode.Append:
1311 case WriteMode.Insert:
1315 case WriteMode.Overwrite:
1322 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
1323 ElementHelper.HardwareFillInFrom(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
1324 else if (InteractionType.JavaScript == elementNode[elementIdx].InteractionType)
1325 ElementHelper.JavaScriptFillInFrom(browser, elementNode[elementIdx].
GPALElement, token.ToString(), writeMode);
1327 ElementHelper.SeleniumFillInFrom(browser, elementNode[elementIdx].
GPALElement, token.ToString(), writeMode);
1334 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Unused token [{token}] from tokens [{string.Join(",
", currentRow)}] in row [{tokenIdx + 1}]", browser, GPALObjectType.Browser);
1338 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{writeMode}] text [{token.ToString()}] in element [{elementNode[elementIdx - 1].GPALElement.TagName}]", browser, GPALObjectType.Browser);
1342 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"NOT WRITTEN: [{writeMode}] text [{token.ToString()}] in element [NOT FOUND].", browser, GPALObjectType.Browser);
1348 while (elementIdx < elementNode.Count &&
null != elementNode[elementIdx++])
1349 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No token for control: <[{elementNode[elementIdx - 1].GPALElement.GetType()}]> : [{elementNode[elementIdx - 1].GPALElement.Text}]", browser, GPALObjectType.Browser);
1353 CallIfStatus handled = 0;
1357 if (
null != browser.CurrentUOW.CallAfterFillIn &&
true == rowsOfColumns.Any())
1359 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Invoking CallAfterFillIn [{browser.CurrentUOW.CallAfterFillIn.Method.Name}]", browser, GPALObjectType.Browser);
1360 handled = browser.CurrentUOW.CallAfterFillIn(browser, tokens, tokenIdx);
1365 browser.CurrentUOW = safeUOW;
1380 if (CallIfStatus.Terminate == handled)
1382 string str = $
"CallAfterFillIn handler [{browser.CurrentUOW.CallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.";
1392 lastRowCnt += rowCnt;
1400 if (tokenIdx == tokens.Count())
1408 private static bool doNotPublishEvent =
false;
1412 private const int NavigationPollMs = 50;
1415 private const string CdpWindowPrefix =
"CDwindow-";
1416 private const string MozProcessIdCapability =
"moz:processID";
1435 static string ReadyStatusOrNavError(
Browser browser,
string sessionToken)
1439 if (
true == status?.ToLower().Contains(
"nav-error"))
1442 browser.RaiseOnFail(GPALFailure.Navigation, status);
1447 public static void CheckDocumentReady(Browser browser,
bool overrideSetting =
false)
1449 bool hasURL =
null != browser?.BrowserSettings.CurrentURL &&
false ==
"https://google.com".Equals(browser.BrowserSettings.CurrentURL);
1451 doNotPublishEvent =
true;
1452 string sessionToken = Guid.NewGuid().ToString();
1454 if ((
true == browser?.BrowserSettings.WaitOnDocumentReady &&
true == hasURL) ||
true == overrideSetting)
1456 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Waiting up to [{browser.BrowserSettings.WaitForDocumentReadyTimeoutMs}] ms on document.ready");
1460 if (
true == browser?.UseOttoMagic)
1461 WaitForValueOrTime(browser,
"complete, interactive, nav-error", () => ReadyStatusOrNavError(browser, sessionToken), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1462 else if (
true == browser?.UsePuppeteer)
1463 WaitForValueOrTime(browser,
"complete, interactive", () => browser.PuppeteerClient.GetReadyStatus(sessionToken).Execute<
object>(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1465 WaitForValueOrTime(browser,
"complete, interactive", () =>
JsCheckReadyState(browser, sessionToken), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1467 else if (
true == browser.BrowserSettings.WaitOnNetworkIdle &&
true == hasURL)
1469 if (
true == browser.UsePuppeteer)
1470 WaitForValueOrTime(browser,
"true", () => browser.PuppeteerClient.CheckNetworkIdle().WithSessionToken(sessionToken).Execute<
bool>().ToString(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1471 else if (
true == browser.BrowserSettings.UseOttoMagic)
1472 WaitForValueOrTime(browser,
"true", () =>
WaitForNetworkIdle(browser, browser.BrowserSettings.NetworkIdleTimeoutMs, browser.BrowserSettings.NetworkIdleMaxConnections, browser.BrowserSettings.NetworkIdlePruneMs, sessionToken).ToString(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1474 WaitForValueOrTime(browser,
"true", () =>
WaitForNetworkIdle(browser, sessionToken).ToString(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1476 doNotPublishEvent =
false;
1500 internal static void FetchWithTokens(Browser browser, GPALRequest request)
1502 IGPALGrid<string> tokens = browser.CurrentUOW.FetchTokens;
1503 int required = request.TokenCount();
1504 string name = request.Name ?? request.Path;
1506 if (
null == tokens && 0 < required)
1507 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch of [{name}] uses [{required}] tokens but no token source was given. Use .WithTokensFrom before .Fetch.", browser, GPALObjectType.Browser);
1511 int rowCount = tokens?.Rows ?? 1;
1513 for (
int tokenIdx = 0; tokenIdx < rowCount; tokenIdx++)
1515 List<string> row =
null == tokens ? null : tokens[tokenIdx];
1519 if (
null != row && row.Count < required)
1520 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch of [{name}] needs [{required}] tokens but row [{tokenIdx + 1}] has [{row.Count}]. Skipping the row.", browser, GPALObjectType.Browser);
1522 FetchRow(browser, request, row, tokenIdx);
1534 static void FetchRow(Browser browser, GPALRequest request, List<string> row,
int tokenIdx)
1536 int mark = browser.CurrentUOW.FetchResults.Count;
1537 string name = request.Name ?? request.Path;
1539 for (
int page = 0; page < browser.CurrentUOW.PageCount; page++)
1541 string where =
null == row ? $
"Page [{page + 1}]" : $
"Row [{tokenIdx + 1}] page [{page + 1}]";
1543 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Fetching [{name}]. {where}.", browser, GPALObjectType.Browser);
1545 string body = FetchInPage(browser, request, page, row);
1553 string why = $
"Fetch of [{name}] returned nothing on {where.ToLower()}.";
1555 GPAL.PublishSimpleEvent(GPALEventType.ERROR, why, browser, GPALObjectType.Browser);
1558 else if (200 > browser.ServerResponseCode || 300 <= browser.ServerResponseCode)
1562 string why = $
"Fetch of [{name}] returned HTTP [{browser.ServerResponseCode}] on {where.ToLower()}.";
1564 GPAL.PublishSimpleEvent(GPALEventType.WARNING, why, browser, GPALObjectType.Browser);
1565 body = why + Environment.NewLine + body;
1568 browser.CurrentUOW.FetchResults.Add(body);
1571 if (
null != request.AfterFetch)
1573 UnitOfWork safeUOW = browser.CurrentUOW;
1574 List<string> results = browser.CurrentUOW.FetchResults.GetRange(mark, browser.CurrentUOW.FetchResults.Count - mark);
1576 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Invoking CallAfterFetch [{request.AfterFetch.Method.Name}]", browser, GPALObjectType.Browser);
1578 CallIfStatus handled = request.AfterFetch(browser, results, browser.CurrentUOW.FetchTokens, tokenIdx);
1581 browser.CurrentUOW = safeUOW;
1583 if (CallIfStatus.Terminate == handled)
1585 string str = $
"CallAfterFetch handler [{request.AfterFetch.Method.Name}] requested program termination.";
1586 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
1587 throw new GPALException($
"{GPAL.MyMethodName()}: " + str);
1596 internal static string FetchInPage(Browser browser, GPALRequest request,
int page, List<string> row)
1600 string envelope = FetchEnvelope(browser, request.ResolveUrl(page, row), request.Method, request.ResolveBody(page, row), request.ContentType, request.HeaderPairs(page, row),
false);
1602 return UnpackFetchResponse(browser, envelope);
1617 static string FetchEnvelope(Browser browser,
string url,
string method,
string body,
string contentType,
string[] headers,
bool asBytes)
1619 string envelope =
null;
1621 if (
true == browser.UseSelenium)
1622 envelope = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteAsyncScript(
1623 FetchSetupScript(url, method, body, contentType, headers, asBytes) +
@"
1624 var done = arguments[arguments.length - 1];
1626 fetch(target.toString(), options)
1629 .catch(function (error) { done(null); });
1631 else if (
true == browser.UsePuppeteer)
1632 envelope = browser.PuppeteerClient
1636 .WithContentType(contentType)
1637 .WithHeaders(headers)
1640 else if (
true == browser.UseOttoMagic)
1641 envelope = browser.MagicHelper.Fetch(url, method, body, contentType, headers, asBytes);
1643 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch is not implemented for [{browser.BrowserSettings.AutomationEngine}] yet.", browser, GPALObjectType.Browser);
1656 internal static string LiveUserAgent(Browser browser)
1658 string retVal =
null;
1660 if (
true == browser.UseSelenium)
1661 retVal = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteScript(
"return navigator.userAgent") as string;
1662 else if (
true == browser.UsePuppeteer)
1663 retVal = browser.PuppeteerClient.GetUserAgent().Execute();
1664 else if (
true == browser.UseOttoMagic)
1665 retVal = Unquoted(browser.MagicHelper.GetUserAgent());
1677 static string Unquoted(
string value)
1679 string retVal = value;
1681 while (
false ==
string.IsNullOrEmpty(retVal) &&
true == retVal.StartsWith(
"\"") &&
true == retVal.EndsWith(
"\"") && 1 < retVal.Length)
1683 string unwrapped = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(retVal);
1685 if (unwrapped == retVal)
1702 internal static string LiveAcceptLanguage(Browser browser)
1704 string languages =
null;
1705 string retVal =
null;
1707 if (
true == browser.UseSelenium)
1708 languages = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteScript(
"return navigator.languages.join(',')") as string;
1709 else if (
true == browser.UsePuppeteer)
1710 languages = browser.PuppeteerClient.GetLanguages().Execute();
1711 else if (
true == browser.UseOttoMagic)
1712 languages = Unquoted(browser.MagicHelper.GetLanguages());
1714 if (
false ==
string.IsNullOrWhiteSpace(languages))
1715 retVal = AcceptLanguageFrom(languages.Split(
','), browser.BrowserSettings.BrowserType);
1733 static string AcceptLanguageFrom(
string[] languages, Enums.BrowserType browserType)
1735 List<string> weighted =
new List<string>();
1737 for (
int index = 0; index < languages.Length; index++)
1739 string language = languages[index].Trim();
1741 if (
true ==
string.IsNullOrEmpty(language))
1744 if (Enums.BrowserType.FireFox == browserType)
1745 language = language.ToLowerInvariant();
1748 weighted.Add(language);
1753 double quality = Math.Round(1.0 - (0.1 * index), 1);
1756 weighted.Add($
"{language};q={quality.ToString("0.#
", System.Globalization.CultureInfo.InvariantCulture)}");
1760 return string.Join(
",", weighted);
1774 internal static byte[] FetchBytes(Browser browser,
string url,
string[] headers, out
string lastModified)
1776 byte[] bytes =
null;
1777 JObject response = UnpackFetchEnvelope(browser, FetchEnvelope(browser, url,
"GET",
null,
null, headers,
true));
1778 string encoded = response?[
"body"]?.ToString();
1780 lastModified = response?[
"lastModified"]?.ToString();
1782 if (
false ==
string.IsNullOrEmpty(encoded))
1783 bytes = Convert.FromBase64String(encoded);
1795 static string UnpackFetchResponse(Browser browser,
string envelope)
1797 return UnpackFetchEnvelope(browser, envelope)?[
"body"]?.ToString();
1806 static JObject UnpackFetchEnvelope(Browser browser,
string envelope)
1808 JObject response =
null;
1812 browser.ServerResponseCode = 0;
1814 if (
false ==
string.IsNullOrEmpty(envelope))
1816 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Fetch envelope [{envelope}]", browser, GPALObjectType.Browser);
1820 string unpacked = envelope.TrimStart().StartsWith(
"\"")
1821 ? Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(envelope)
1826 if (
false == (unpacked?.TrimStart().StartsWith(
"{") ??
false))
1827 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch response was not understood. Received [{unpacked}]", browser, GPALObjectType.Browser);
1830 response = JObject.Parse(unpacked);
1831 browser.ServerResponseCode = response[
"status"]?.Value<
int>() ?? 0;
1835 if (
false ==
string.IsNullOrEmpty(response[
"error"]?.ToString()))
1836 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The page could not make the request [{response["error
"]}]", browser, GPALObjectType.Browser);
1856 internal static string FetchSetupScript(
string url,
string method,
string body,
string contentType,
string[] headers,
bool asBytes)
1859 var target = new URL({Newtonsoft.Json.JsonConvert.SerializeObject(url)}, location.href);
1860 var reqBody = {Newtonsoft.Json.JsonConvert.SerializeObject(body)};
1861 var contentType = {Newtonsoft.Json.JsonConvert.SerializeObject(contentType)};
1862 var headerPairs = {Newtonsoft.Json.JsonConvert.SerializeObject(headers ?? new string[0])};
1863 var asBytes = {(true == asBytes ? "true" : "false")};
1865 var options = {{ method: {Newtonsoft.Json.JsonConvert.SerializeObject(method ?? "GET
")}, credentials: 'include', headers: {{}} }};
1866 for (var h = 0; h < headerPairs.length; h += 2)
1867 options.headers[headerPairs[h]] = headerPairs[h + 1];
1870 options.body = reqBody;
1871 if (!options.headers['Content-Type'])
1872 options.headers['Content-Type'] = contentType;
1875 // one envelope for every engine, so the C# side has a single shape to unpack. text() would put a
1876 // replacement character wherever a byte is not valid utf-8, which is most of any real file, so a
1877 // file is read as an arraybuffer and base64 encoded instead. Written with promises rather than
1878 // await so the same function serves selenium's callback and puppeteer's awaited expression.
1879 // String.fromCharCode is applied over slices because one call with a few hundred thousand
1880 // arguments overflows the stack
1881 function gpalEnvelope(response) {{
1883 ? response.arrayBuffer().then(function (buffer) {{
1884 var bytes = new Uint8Array(buffer), binary = '', slice = 0x8000;
1885 for (var i = 0; i < bytes.length; i += slice)
1886 binary += String.fromCharCode.apply(null, bytes.subarray(i, i + slice));
1887 return btoa(binary);
1890 ).then(function (payload) {{
1891 return JSON.stringify({{
1892 status: response.status,
1894 encoding: asBytes ? 'base64' : null,
1895 lastModified: response.headers.get('Last-Modified')
1901 public static object ExecuteJavaScriptObj(
string executeMe, IBrowser browser, params
object[] args)
1903 dynamic retVal =
null;
1907 if (browser.UseOttoMagic)
1909 retVal = browser.MagicHelper.ExecuteJavaScript(executeMe);
1911 else if (
true == browser.UsePuppeteer)
1913 retVal = browser.PuppeteerClient.ExecuteJavaScript(executeMe).WithParameters(args).Execute<
object>();
1914 retVal = retVal.result.value;
1918 IJavaScriptExecutor js = (IJavaScriptExecutor)browser.BrowserDriver;
1919 retVal = js.ExecuteScript(executeMe, args);
1922 catch (GPALException)
1926 catch (Exception ex)
1928 if (
false == doNotPublishEvent)
1929 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
$@"Failed to execute javascript. Continuing.", browser, GPALObjectType.Browser, ex);
1956 private static string UnwrapScriptResult(
string result)
1958 string retVal = result;
1963 if (
false ==
string.IsNullOrWhiteSpace(retVal) &&
true == retVal.TrimStart().StartsWith(
"{"))
1967 Newtonsoft.Json.Linq.JObject remote = Newtonsoft.Json.Linq.JObject.Parse(retVal);
1969 if (
null != remote[
"type"] &&
null != remote[
"value"]
1970 && remote.Properties().All(each =>
"type" == each.Name ||
"value" == each.Name ||
"description" == each.Name ||
"className" == each.Name ||
"subtype" == each.Name))
1972 Newtonsoft.Json.Linq.JToken value = remote[
"value"];
1974 retVal = Newtonsoft.Json.Linq.JTokenType.String == value.Type
1976 : Newtonsoft.Json.JsonConvert.SerializeObject(value);
1986 while (
false ==
string.IsNullOrEmpty(retVal) && retVal.StartsWith(
"\"") && retVal.EndsWith(
"\""))
1990 string peeled = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(retVal);
1992 if (peeled == retVal)
2021 public static string ExecuteJavaScript(IBrowser browser,
string executeMe, params
object[] args)
2023 string retVal = RunScript(browser, executeMe, args);
2026 if (
false == browser.UseSelenium &&
true == retVal?.Contains(
"Illegal return statement"))
2027 retVal = RunScript(browser,
"(function(){" + Environment.NewLine + executeMe + Environment.NewLine +
"})()", args);
2032 private static string RunScript(IBrowser browser,
string executeMe, params
object[] args)
2034 dynamic retVal =
null;
2038 if (browser.UseOttoMagic)
2040 retVal = browser.MagicHelper.ExecuteJavaScript(executeMe);
2042 else if (
true == browser.UsePuppeteer)
2044 if (
true == args.Any())
2045 retVal = browser.PuppeteerClient.ExecuteJavaScript(executeMe).WithParameters(args).Execute<
object>();
2047 retVal = browser.PuppeteerClient.ExecuteJavaScript(executeMe).Execute<
object>();
2049 if (
Puppeteer.resultExtractors !=
null &&
Puppeteer.resultExtractors.TryGetValue(DevToolsMethods.RuntimeEvaluate, out var extractor))
2050 retVal = extractor((Newtonsoft.Json.Linq.JObject)retVal.result);
2052 retVal = retVal.result.ToObject();
2056 IJavaScriptExecutor js = (IJavaScriptExecutor)browser.BrowserDriver;
2057 if (
true == args?.Any())
2058 retVal = js.ExecuteScript(executeMe, args);
2060 retVal = js.ExecuteScript(executeMe);
2065 catch (GPALException)
2069 catch (Exception ex)
2071 if (
false == doNotPublishEvent)
2072 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION,
$@"Failed to execute javascript. Continuing.", browser, GPALObjectType.Browser, ex);
2076 return UnwrapScriptResult(retVal?.ToString());
2085 public static string GetCurrentUrl(BrowserSettings browserSettings)
2087 string currentUrl =
null;
2089 if (
true == browserSettings.UseOttoMagic)
2090 currentUrl = browserSettings.MagicHelper.GetCurrentUrl();
2091 else if (
true == browserSettings.UsePuppeteer)
2092 currentUrl = browserSettings.PuppeteerClient.GetCurrentUrl().Execute();
2094 currentUrl = browserSettings.Browser.BrowserDriver.Url;
2099 if (
true == currentUrl?.StartsWith(
"\"") &&
true == currentUrl.EndsWith(
"\""))
2100 currentUrl = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(currentUrl);
2113 string readyState =
"loading";
2115 if (lastSessionToken != sessionToken)
2117 lastErrorMessage.Clear();
2118 supressedMessage =
false;
2119 lastSessionToken = sessionToken;
2124 if (browser.UseOttoMagic)
2126 readyState = browser.BrowserSettings.MagicHelper.GetReadyStatus();
2127 if (
null == readyState)
2128 readyState =
@"loading";
2130 readyState = readyState.Trim(
'"');
2134 bool withReturn =
false == browser.UsePuppeteer;
2137 readyState =
ExecuteJavaScript(browser, $
"{(true == withReturn ? "return " : "")}document.readyState");
2144 catch (Exception ex)
2146 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
2147 $
"Error executing JavaScript",
2148 null, GPALObjectType.None, ex);
2151 string currentErrorMessage = $
"Document.Ready status [{readyState}]";
2153 if (
false == lastErrorMessage.Contains(currentErrorMessage))
2155 lastErrorMessage.Add(currentErrorMessage);
2156 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage);
2157 supressedMessage =
false;
2159 else if (
false == supressedMessage)
2161 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
2162 supressedMessage =
true;
2176 public static void SeleniumGoToUrl(GPALUrl url, BrowserSettings browserSettings)
2178 string normalizedTarget = url.ToString().TrimEnd(
'/').ToLowerInvariant();
2183 browserSettings.BrowserDriver.Navigate().GoToUrl(url);
2186 System.Threading.Thread.Sleep(400);
2189 if (browserSettings.BrowserType != BrowserType.FireFox)
2192 var performanceLogs = browserSettings.BrowserDriver.Manage().Logs.GetLog(
"performance");
2194 int? bestStatus =
null;
2195 string bestMatchedUrl =
null;
2196 int candidatePriority =
int.MaxValue;
2198 foreach (var entry
in performanceLogs)
2200 string message = entry.Message;
2202 if (message.IndexOf(
"Network.responseReceived", StringComparison.Ordinal) == -1)
2206 if (message.IndexOf(
"\"url\":\"", StringComparison.Ordinal) == -1 ||
2207 message.IndexOf(normalizedTarget, StringComparison.OrdinalIgnoreCase) == -1)
2211 var statusMatch = System.Text.RegularExpressions.Regex.Match(
2213 @"""status(?:Code)?""\s*:\s*(\d+)",
2214 System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.CultureInvariant
2217 if (!statusMatch.Success)
2221 if (!
int.TryParse(statusMatch.Groups[1].Value, out status))
2224 if (status < 100 || status > 599)
2231 else if (status >= 200 && status < 300)
2233 else if (status >= 300 && status < 400)
2235 else if (status >= 400 && status < 500)
2237 else if (status >= 500)
2242 bool shouldUpdate =
false;
2244 if (!bestStatus.HasValue)
2246 shouldUpdate =
true;
2248 else if (priority < candidatePriority)
2250 shouldUpdate =
true;
2252 else if (priority == candidatePriority && status == 304)
2254 shouldUpdate =
true;
2259 bestStatus = status;
2260 candidatePriority = priority;
2263 var urlMatch = System.Text.RegularExpressions.Regex.Match(message,
@"""url"":""([^""]+)""");
2264 if (urlMatch.Success)
2266 bestMatchedUrl = urlMatch.Groups[1].Value;
2271 if (bestStatus.HasValue)
2273 browserSettings.ServerResponseCode = bestStatus.Value;
2288 browserSettings.ServerResponseCode = 0;
2289 GPAL.PublishSimpleEvent(
2290 GPALEventType.DEBUG,
2291 $
"No valid HTTP status captured in performance logs for [{url}]",
2293 GPALObjectType.Other
2302 var result = ((OpenQA.Selenium.IJavaScriptExecutor)browserSettings.BrowserDriver)
2303 .ExecuteScript(
"return performance.getEntriesByType('navigation')[0]?.responseStatus ?? 0;");
2304 browserSettings.ServerResponseCode = result !=
null &&
int.TryParse(result.ToString(), out
int ffStatus) && ffStatus > 0 ? ffStatus : 0;
2308 browserSettings.ServerResponseCode = 0;
2316 catch (Exception ex)
2318 browserSettings.ServerResponseCode = -1;
2320 GPALEventType.EXCEPTION,
2321 $
"Unexpected error navigting to [{url}]",
2323 GPALObjectType.Other,
2339 public static KillProcessesResult
KillProcessesByName(params
string[] namesOfProcessesToKill)
2341 KillProcessesResult result =
null;
2343 foreach (var name
in namesOfProcessesToKill)
2361 string userAgent =
null;
2365 if (
true == browser.UseOttoMagic)
2367 else if (
true == browser.UsePuppeteer)
2368 userAgent = browser.PuppeteerClient.GetUserAgent().Execute<
string>();
2376 catch (Exception ex)
2378 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get live user agent.", browser, GPALObjectType.Browser, ex);
2381 if (
true ==
string.IsNullOrEmpty(userAgent))
2386 if (
true == userAgent?.StartsWith(
"\"") &&
true == userAgent.EndsWith(
"\""))
2387 userAgent = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(userAgent);
2403 bool retVal =
false;
2406 if (0 <= url.IndexOf(
".pdf", 0, StringComparison.OrdinalIgnoreCase))
2409 using (HttpClient client =
new HttpClient())
2411 client.DefaultRequestHeaders.Add(
"User-Agent",
GetUserAgent(browser));
2416 using (HttpRequestMessage request =
new HttpRequestMessage(HttpMethod.Head, url))
2418 HttpResponseMessage response = client.SendAsync(request).Result;
2420 if (response.IsSuccessStatusCode)
2423 string contentType = response.Content.Headers.ContentType?.MediaType;
2424 if (contentType !=
null && contentType.Equals(
"application/pdf", StringComparison.OrdinalIgnoreCase))
2429 else if (response.Content.Headers.ContentDisposition !=
null)
2431 string fileName = response.Content.Headers.ContentDisposition.FileName;
2432 if (!
string.IsNullOrEmpty(fileName) && fileName.EndsWith(
".pdf", StringComparison.OrdinalIgnoreCase))
2440 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to verify URL [{url}] via HEAD request. Status code [{response.StatusCode}]");
2444 catch (GPALException)
2448 catch (Exception ex)
2450 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Exception during HEAD request for [{url}]",
null, GPALObjectType.None, ex);
2455 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"GPAL detected PDF file opened in browser [{url}]");
2470 internal const int PdfFetchAttempts = 3;
2471 internal const int PdfFetchWaitMs = 1500;
2480 internal static bool IsPdf(
byte[] content)
2482 return 4 < content?.Length
2483 &&
'%' == content[0] &&
'P' == content[1] &&
'D' == content[2] &&
'F' == content[3];
2485 public static bool DownloadPdfFile(
string pdfUrl,
string savePath, IBrowser browser)
2488 string filePath =
$@"{FileHelper.EnsureDirectoryEndsWithBackslash(Path.GetDirectoryName(savePath))}{System.IO.Path.GetFileName(pdfUrl)}";
2489 if (
true == File.Exists(filePath))
2491 File.Move(filePath, savePath);
2496 filePath =
$@"{FileHelper.GetDefaultDownloadDirectory(browser)}{Path.GetFileName(pdfUrl)}";
2497 if (
true == File.Exists(filePath))
2499 File.Move(filePath, savePath);
2510 for (
int attempt = 0; attempt < PdfFetchAttempts; attempt++)
2512 byte[] fetched = FetchBytes((Browser)browser, pdfUrl,
null, out
string _);
2514 if (
true == IsPdf(fetched))
2516 File.WriteAllBytes(savePath, fetched);
2517 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"PDF file [{pdfUrl}] fetched from the page to [{savePath}]");
2521 if (attempt < PdfFetchAttempts - 1)
2523 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{pdfUrl}] answered with [{fetched?.Length ?? 0}] bytes that are not a pdf, waiting for the file itself", browser, GPALObjectType.Browser);
2524 Thread.Sleep(PdfFetchWaitMs);
2525 pdfUrl =
GetCurrentUrl(((Browser)browser).BrowserSettings) ?? pdfUrl;
2528 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{pdfUrl}] never answered with a pdf, the page in front of the file did. Nothing written to [{savePath}]", browser, GPALObjectType.Browser);
2533 using (HttpClient client =
new HttpClient())
2535 client.DefaultRequestHeaders.Add(
"User-Agent",
GetUserAgent(browser));
2540 byte[] pdfContent = client.GetByteArrayAsync(pdfUrl).Result;
2542 if (
false == IsPdf(pdfContent))
2544 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{pdfUrl}] answered with [{pdfContent?.Length ?? 0}] bytes that are not a pdf. Nothing written to [{savePath}]", browser, GPALObjectType.Browser);
2549 File.WriteAllBytes(savePath, pdfContent);
2550 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"PDF file [{pdfUrl}] downloaded successfully to [{savePath}]");
2553 catch (GPALException)
2557 catch (Exception ex)
2559 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"PDF file NOT downloaded from [{pdfUrl}]", ex);
2573 internal static IWebDriver GetWebDriver(BrowserSettings browserSettings, dynamic options)
2575 IWebDriver tmpDriver =
null;
2579 if (
true == browserSettings.HiddenDesktop)
2583 int hiddenPort = StartHiddenDriver(browserSettings);
2585 tmpDriver =
new OpenQA.Selenium.Remote.RemoteWebDriver(
new Uri($
"http://localhost:{hiddenPort}"), options);
2587 else if (BrowserType.Chrome == browserSettings.BrowserType)
2590 ChromeDriverService chromeService = ChromeDriverService.CreateDefaultService(browserSettings.DriverLocation);
2591 chromeService.HideCommandPromptWindow =
true;
2593 ChromeOptions chromeOptions = (ChromeOptions)options;
2595 tmpDriver =
new ChromeDriver(chromeService, chromeOptions);
2598 browserSettings.ServiceDriverPid = chromeService.ProcessId;
2602 EdgeDriverService edgeService = EdgeDriverService.CreateDefaultService(browserSettings.DriverLocation);
2603 edgeService.HideCommandPromptWindow =
true;
2605 EdgeOptions edgeOptions = (EdgeOptions)options;
2607 tmpDriver =
new EdgeDriver(edgeService, edgeOptions);
2610 browserSettings.ServiceDriverPid = edgeService.ProcessId;
2612 }
catch (Exception ex)
2614 string url =
string.Empty;
2615 switch (browserSettings.BrowserType)
2617 case Enums.BrowserType.Chrome:
2618 url = GPAL.GPALSettings.ChromeDriverUpdateURL;
2620 case Enums.BrowserType.Edge:
2621 url = GPAL.GPALSettings.EdgeDriverUpdateURL;
2623 case Enums.BrowserType.FireFox:
2624 url = GPAL.GPALSettings.FirefoxDriverUpdateURL;
2627 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Please manually update the driver @ [{url}]..", browserSettings.Browser, GPALObjectType.Browser, ex);
2631 #region <KillProcess>
2637 private static class DriverInfos
2639 public static DriverInfo Chrome =
new DriverInfo(
2640 executableFileName: GPAL.GPALSettings.ChromeDriverFilename,
2641 processName:
"chromedriver",
2642 browserName:
"chrome"
2645 public static DriverInfo FireFox =
new DriverInfo(
2646 executableFileName: GPAL.GPALSettings.FirefoxDriverFilename,
2647 processName:
"geckodriver",
2648 browserName:
"firefox"
2657 public static DriverInfo InternetExplorer =
new DriverInfo(
2658 executableFileName:
"IEDriverServer.exe",
2659 processName:
"IEDriverServer",
2660 browserName:
"internet explorer"
2663 public static DriverInfo Edge =
new DriverInfo(
2664 executableFileName:
GPAL.GPALSettings.EdgeDriverFilename,
2665 processName:
"MSEdgeDriver",
2675 private class DriverInfo
2683 public DriverInfo(
string executableFileName,
string processName,
string browserName)
2685 ExecutableFileName = executableFileName;
2686 ProcessName = processName;
2687 BrowserName = browserName;
2689 public string ExecutableFileName {
get;
private set; }
2690 public string ProcessName {
get;
private set; }
2691 public string BrowserName {
get;
private set; }
2698 public string Name {
get;
set; }
2699 public int Found {
get;
set; } = 0;
2700 public int Killed {
get;
set; } = 0;
2701 public bool Success {
get;
set; } =
false;
2711 List<KillProcessesResult> result =
new List<KillProcessesResult>();
2713 alreadyUpdating =
false;
2717 result.Add(
KillProcess(DriverInfos.FireFox.ProcessName));
2718 result.Add(
KillProcess(DriverInfos.Chrome.ProcessName));
2719 result.Add(
KillProcess(DriverInfos.InternetExplorer.ProcessName));
2720 result.Add(
KillProcess(DriverInfos.Edge.ProcessName));
2724 }
catch (Exception ex)
2726 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to kill processes", ex);
2736 private const int MaxParentHops = 8;
2738 internal const int GracefulExitMs = 10000;
2750 private static void CloseBrowserGracefully(Browser aBrowser,
bool endItAll)
2754 if (
true == aBrowser.UsePuppeteer)
2757 if (
false == aBrowser.BrowserSettings.Process?.HasExited)
2759 aBrowser.BrowserSettings.PuppeteerCommunicator.SendCommand(DevToolsMethods.BrowserClose,
new Dictionary<string, object>(),
null).GetAwaiter().GetResult();
2760 aBrowser.PuppeteerCommunicator._readerReadyTcs.SetCanceled();
2762 if (
false == aBrowser.PuppeteerCommunicator._usePipes)
2763 aBrowser.PuppeteerCommunicator.CloseOutputWebSocketAsync().GetAwaiter();
2766 else if (
true == aBrowser.UseSelenium &&
null != aBrowser.BrowserDriver)
2772 Task closing = Task.Run(() =>
2774 if (
true == endItAll)
2775 aBrowser.BrowserDriver?.Quit();
2777 aBrowser.BrowserDriver?.Close();
2780 closing.Wait(GracefulExitMs);
2796 if (
true == aBrowser.UseOttoMagic &&
true == aBrowser.IsAlive)
2797 aBrowser.MagicHelper.CloseBrowser();
2799 if (
false == aBrowser.BrowserSettings.Process?.HasExited)
2800 aBrowser.BrowserSettings.Process?.CloseMainWindow();
2808 public static KillProcessesResult[] KillAllRunningProcesses(
bool killWebDrivers, IBrowser browser)
2810 List<KillProcessesResult> result =
new List<KillProcessesResult>();
2815 int browserCount = GPAL.Browsers.Count;
2817 if (0 == browserCount)
2829 foreach (Browser aBrowser
in GPAL.Browsers)
2831 if (aBrowser == browser ||
null == browser)
2834 bool endItAll = (aBrowser == browser &&
true == killWebDrivers) ||
null == browser;
2841 CloseBrowserGracefully(aBrowser, endItAll);
2843 if (
null != aBrowser.BrowserSettings.Process)
2848 if (
false == aBrowser.BrowserSettings.Process.WaitForExit(GracefulExitMs))
2850 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{aBrowser.BrowserSettings.BrowserType}] did not shut down within [{GracefulExitMs / 1000}] seconds, killing it. Its profile will read as crashed.", aBrowser, GPALObjectType.Browser);
2851 result.Add(
KillProcess(aBrowser.BrowserSettings.Process));
2859 if (
true == aBrowser.UseSelenium &&
true == endItAll)
2861 int pid = aBrowser.BrowserSettings.ServiceDriverPid;
2863 GPAL.PublishSimpleEvent(GPALEventType.INFO,
">>> Disposing browser driver");
2868 KillProcessTree(pid);
2874 result.Add(killProcessesResult);
2879 aBrowser.BrowserSettings.ServiceDriverPid = 0;
2883 Thread.Sleep(1_000);
2891 if (
true == aBrowser.BrowserSettings.TempProfileCreated)
2892 ChromeProfileManager.RemoveTempUserProfile(aBrowser.BrowserSettings.ProfileDataDirectory);
2896 catch (Exception ex)
2898 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to kill processes", ex);
2930 return result.ToArray();
2948 internal static void EndWindowlessSuccessor(BrowserSettings browserSettings)
2950 int ourPid = browserSettings.Process?.Id ?? 0;
2955 WaitForBrowserToExit(browserSettings, Browser.ProfileRestoreWaitMs);
2961 using (var search =
new System.Management.ManagementObjectSearcher(
2962 $
"SELECT ProcessId, CommandLine FROM Win32_Process WHERE ParentProcessId = {ourPid}"))
2963 foreach (System.Management.ManagementObject found in search.Get())
2965 string commandLine = found[
"CommandLine"]?.ToString() ??
string.Empty;
2967 if (
true == commandLine.Contains(
"--no-startup-window"))
2969 int successor = Convert.ToInt32(found[
"ProcessId"]);
2971 GPAL.PublishSimpleEvent(GPALEventType.INFO,
2972 $
"Ending the windowless browser [{successor}] left behind by [{ourPid}], which would hold the profile open",
2973 browserSettings, GPALObjectType.Browser);
2975 KillProcessTree(successor);
2979 catch (Exception ex)
2981 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not look for a windowless browser left behind by [{ourPid}]",
2982 browserSettings, GPALObjectType.Browser, ex);
2987 private static void KillProcessTree(
int pid)
2989 if (pid <= 0)
return;
2993 var startInfo =
new ProcessStartInfo
2995 FileName =
"taskkill",
2996 Arguments = $
"/PID {pid} /T /F",
2997 WindowStyle = ProcessWindowStyle.Hidden,
2998 CreateNoWindow =
true,
2999 UseShellExecute =
false
3002 using (Process taskkillProcess = Process.Start(startInfo))
3004 taskkillProcess?.WaitForExit(5000);
3018 if (
null != process)
3025 Name = process.ProcessName;
3031 result.Success =
true;
3036 process.WaitForExit();
3054 public static KillProcessesResult
KillProcess(
string processName)
3056 KillProcessesResult result =
new KillProcessesResult { Name = processName };
3058 var processes = Process.GetProcessesByName(processName);
3060 result.Found = processes.Count();
3062 if (result.Found > 0)
3064 foreach (var process
in processes)
3071 process.WaitForExit();
3077 }
while (
false == process.HasExited);
3080 result.Success =
true;
3083 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Killed process [{processName}]");
3088 result.Success =
true;
3093 #endregion <KillProcess>
3095 private static Stopwatch watch =
new System.Diagnostics.Stopwatch();
3096 private static int lastX = 0, lastY = 0;
3121 private const string PortsLogName =
"ports-in-use.log";
3124 private const string NativeHostName =
"gpal.rest.api.nativeapp";
3130 private static readonly
string[] NativeHostKeys =
3132 @"Software\\Google\\Chrome\\NativeMessagingHosts\\" + NativeHostName,
3133 @"Software\\Chromium\\NativeMessagingHosts\\" + NativeHostName,
3134 @"Software\\Microsoft\\Edge\\NativeMessagingHosts\\" + NativeHostName,
3135 @"Software\\WOW6432Node\\Google\\Chrome\\NativeMessagingHosts\\" + NativeHostName,
3136 @"Software\\WOW6432Node\\Chromium\\NativeMessagingHosts\\" + NativeHostName,
3137 @"Software\\WOW6432Node\\Microsoft\\Edge\\NativeMessagingHosts\\" + NativeHostName
3147 string retVal =
null;
3149 foreach (RegistryKey root
in new[] { Registry.CurrentUser, Registry.LocalMachine })
3151 foreach (
string keyPath
in NativeHostKeys)
3154 using (RegistryKey key = root.OpenSubKey(keyPath))
3156 string manifestPath = key?.GetValue(
null) as string;
3158 if (
true ==
string.IsNullOrWhiteSpace(manifestPath) ||
false == File.Exists(manifestPath))
3163 dynamic manifest = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(manifestPath));
3164 string exePath = manifest?.path;
3166 if (
false ==
string.IsNullOrWhiteSpace(exePath) &&
true == File.Exists(exePath))
3173 catch (Exception ex)
3175 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not read the native host registration at [{keyPath}]",
null, GPALObjectType.None, ex);
3178 if (
false ==
string.IsNullOrWhiteSpace(retVal))
3200 public static int LiveRestApiHosts(out List<(
int processId,
int port)> hosts)
3202 hosts =
new List<(int processId, int port)>();
3205 string folder =
true ==
string.IsNullOrWhiteSpace(exePath) ? null : Path.GetDirectoryName(exePath);
3206 string path =
true ==
string.IsNullOrWhiteSpace(folder) ? null : Path.Combine(folder, PortsLogName);
3209 if (
true ==
string.IsNullOrWhiteSpace(path) ||
false == File.Exists(path))
3215 using (FileStream stream =
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
3216 using (StreamReader reader =
new StreamReader(stream))
3217 foreach (
string line
in reader.ReadToEnd().Split(
new[] {
'\r',
'\n' }, StringSplitOptions.RemoveEmptyEntries))
3219 string[] halves = line.Split(
':');
3221 if (2 == halves.Length
3222 &&
true ==
int.TryParse(halves[0], out
int pid)
3223 &&
true ==
int.TryParse(halves[1], out
int port)
3224 &&
false == hosts.Any(each => each.processId == pid))
3225 hosts.Add((pid, port));
3228 catch (Exception ex)
3230 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not read the host list at [{path}]",
null, GPALObjectType.None, ex);
3236 internal static bool RestApiAnswering(
string baseUrl)
3238 bool retVal =
false;
3240 if (
false ==
string.IsNullOrWhiteSpace(baseUrl))
3243 System.Net.HttpWebRequest ask = (System.Net.HttpWebRequest)System.Net.WebRequest.Create($
"{baseUrl}status");
3245 ask.ReadWriteTimeout = 1000;
3247 using (System.Net.HttpWebResponse answer = (System.Net.HttpWebResponse)ask.GetResponse())
3248 retVal = System.Net.HttpStatusCode.OK == answer.StatusCode;
3250 catch (System.Net.WebException)
3258 public static void TopBrowser(Browser browser,
bool moveDontClick,
bool forceClick =
false)
3265 bool needsTopping = (browser.CurrentUOW?.CurrentSelector?.SelectorSettings?.SelectorPaths
3266 ?.Any(path => path.SelectorPathType == SelectorPathType.Image) ??
false)
3267 || InteractionType.Hardware == browser.CurrentUOW?.CurrentSelector?.InteractionType
3268 ||
true == browser.BrowserSettings.UseHardware;
3271 if (
true == browser.BrowserSettings.UseHeadless ||
false == needsTopping)
3279 if (
true == moveDontClick ||
true == forceClick)
3281 if (
true == watch.IsRunning)
3283 if (1000 > watch.ElapsedMilliseconds)
3287 watch = System.Diagnostics.Stopwatch.StartNew();
3289 HardwareHelper.GetCursorPos(out Point currCursorPos);
3291 if (
true == browser.BrowserSettings.UseHardware ||
true == GPAL.UseHardware || Enums.BrowserType.FireFox == browser.BrowserSettings.BrowserType ||
true == moveDontClick ||
true == forceClick)
3295 Rectangle rect = ElementHelper.GetWindowRectangle(browser);
3298 Rectangle outerRect = ElementHelper.GetWindowOuterRectangle(browser);
3300 if (
true == moveDontClick)
3305 var rnd =
new Random();
3306 switch (rnd.Next(4))
3308 case 0: x = rect.Left + rnd.Next(-50, rect.Width + 50); y = outerRect.Top - 20;
break;
3309 case 1: x = rect.Left + rnd.Next(-50, rect.Width + 50); y = rect.Bottom + 50;
break;
3310 case 2: x = rect.Left - 60; y = rect.Top + rnd.Next(0, rect.Height);
break;
3311 default: x = rect.Right + 60; y = rect.Top + rnd.Next(0, rect.Height);
break;
3316 Rectangle workingArea = Screen.PrimaryScreen.WorkingArea;
3317 x = Math.Min(Math.Max(workingArea.Left, x), workingArea.Right - 1);
3318 y = Math.Min(Math.Max(workingArea.Top, y), workingArea.Bottom - 1);
3325 x = outerRect.Left + 3;
3326 y = outerRect.Top + 3;
3333 catch (GPALException)
3337 catch (Exception ex)
3339 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get browser coords. Retrying.", browser, GPALObjectType.Browser, ex);
3344 if (
true == GPAL.SimulateMouse ||
true == browser.CurrentUOW.CurrentSelector?.SimulateMouse)
3345 HardwareHelper.MoveMouse(x, y, 10, 10);
3347 HardwareHelper.MoveMouse(x, y);
3349 if (
false == moveDontClick)
3351 HardwareHelper.HardwareClick(x, y, ClickType.LeftClick);
3354 if (
true == GPAL.SimulateMouse ||
true == browser.CurrentUOW.CurrentSelector?.SimulateMouse)
3355 HardwareHelper.MoveMouse(currCursorPos.X, currCursorPos.Y, 10, 10);
3357 HardwareHelper.MoveMouse(currCursorPos.X, currCursorPos.Y);
3360 else if (
true == browser.UseSelenium)
3368 browser.BrowserDriver.SwitchTo().Window(browser.BrowserDriver.CurrentWindowHandle);
3370 catch (GPALException)
3374 catch (Exception ex)
3376 string tmpStr =
"Faling back to windows topping.";
3377 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to top browser using Selenium. {(false == GPAL.NoFallbackRecoveryActions ? tmpStr : String.Empty)})", browser, GPALObjectType.Browser, ex);
3378 if (
false == GPAL.NoFallbackRecoveryActions)
3379 WindowHelper.TopBrowser(browser.BrowserSettings.Process, browser.BrowserSettings.ServiceDriverPid);
3385 if (
false == WindowHelper.TopBrowser(browser.BrowserSettings.Process, browser.BrowserSettings.ServiceDriverPid))
3386 throw new GPALException(
"Browser has exited");
3416 internal static bool CameFromProcess(
int processId,
int ancestorId)
3418 bool retVal =
false;
3419 int walking = processId;
3421 for (
int hop = 0; hop < MaxParentHops && 0 != walking &&
false == retVal; hop++)
3423 if (walking == ancestorId)
3426 walking = ParentOf(walking);
3438 private static int ParentOf(
int processId)
3444 using (var searcher =
new System.Management.ManagementObjectSearcher(
3445 $
"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}"))
3446 foreach (System.Management.ManagementObject each in searcher.Get())
3448 retVal = Convert.ToInt32(each[
"ParentProcessId"]);
3450 catch (Exception ex)
3452 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not ask who started process [{processId}]",
null, GPALObjectType.Browser, ex);
3458 internal static void RefuseIfProfileIsOpen(BrowserSettings browserSettings)
3460 string profile = browserSettings.ProfileDataDirectory;
3462 if (
false ==
string.IsNullOrEmpty(profile))
3464 string wanted = NormalizedDirectory(profile);
3465 bool isFirefox = BrowserType.FireFox == browserSettings.BrowserType;
3467 string processName =
true == isFirefox ?
"firefox.exe"
3468 : BrowserType.Edge == browserSettings.BrowserType ?
"msedge.exe"
3472 string profileArgument =
true == isFirefox
3473 ?
"-profile\\s+(\"[^\"]*\"|[^\\s]*)"
3474 :
"--user-data-dir=(\"[^\"]*\"|[^\\s]*)";
3480 using (var search =
new System.Management.ManagementObjectSearcher(
3481 $
"SELECT ProcessId, CommandLine FROM Win32_Process WHERE Name = '{processName}'"))
3483 foreach (System.Management.ManagementObject found in search.Get())
3485 string commandLine = found[
"CommandLine"]?.ToString();
3487 if (
true ==
string.IsNullOrEmpty(commandLine))
continue;
3489 Match match = Regex.Match(commandLine, profileArgument);
3491 if (
false == match.Success)
continue;
3493 if (
true == wanted.Equals(NormalizedDirectory(match.Groups[1].Value.Trim(
'"')), StringComparison.OrdinalIgnoreCase))
3495 holder = Convert.ToInt32(found[
"ProcessId"]);
3501 catch (GPALException)
3505 catch (Exception ex)
3508 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not check whether [{profile}] is already open",
null, GPALObjectType.Browser, ex);
3513 string message = $
"Profile [{profile}] is already open in [{processName}] process [{holder}]. Close it, or give this run a profile of its own";
3515 GPAL.PublishSimpleEvent(GPALEventType.ERROR, message,
null, GPALObjectType.Browser);
3517 throw new GPALException($
"{GPAL.MyMethodName()}: {message}");
3524 private static string NormalizedDirectory(
string path)
3526 string retVal = path;
3530 retVal = Path.GetFullPath(path).TrimEnd(
'\\',
'/');
3534 retVal = path.TrimEnd(
'\\',
'/');
3542 private static readonly
string[] browserOwnedHeaders =
3544 "referer",
"user-agent",
"host",
"origin",
"cookie",
"content-length",
3545 "connection",
"accept-encoding",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
3546 "sec-ch-ua-arch",
"sec-ch-ua-model",
"sec-ch-ua-full-version-list",
"sec-ch-device-memory",
3547 "sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site"
3555 internal static bool BrowserOwnedHeader(
string name)
3557 return Array.Exists(browserOwnedHeaders, owned => owned.Equals(name, StringComparison.OrdinalIgnoreCase));
3566 private static readonly HashSet<int> portsHandedOut =
new HashSet<int>();
3569 private static int lastPortHandedOut = 0;
3579 internal static int StartHiddenDriver(BrowserSettings browserSettings)
3581 int retVal = FindFreePort();
3583 string portArgument;
3585 switch (browserSettings.BrowserType)
3587 case BrowserType.FireFox:
3588 driverName =
"geckodriver.exe";
3589 portArgument = $
"--port {retVal}";
3591 case BrowserType.Edge:
3592 driverName =
"msedgedriver.exe";
3593 portArgument = $
"--port={retVal}";
3596 driverName =
"chromedriver.exe";
3597 portArgument = $
"--port={retVal}";
3601 string driverPath = Path.Combine(browserSettings.DriverLocation ?? AppDomain.CurrentDomain.BaseDirectory, driverName);
3605 if (
false == File.Exists(driverPath))
3606 throw new GPALException($
"No [{driverName}] in [{Path.GetDirectoryName(driverPath)}], which a hidden run needs because Selenium Manager cannot fetch one here");
3608 Native.STARTUPINFO startup =
new Native.STARTUPINFO
3610 cb = (uint)Marshal.SizeOf<Native.STARTUPINFO>(),
3611 lpDesktop = HiddenDesktop.StartupNameFor(browserSettings.HiddenDesktopName)
3614 if (
false == Native.CreateProcess(
null,
$@"""{driverPath}"" {portArgument}", IntPtr.Zero, IntPtr.Zero,
false,
3615 Native.CREATE_NO_WINDOW, IntPtr.Zero,
null, ref startup, out var started))
3616 throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), $
"Could not start [{driverName}] on [{browserSettings.HiddenDesktopName}]");
3618 Native.CloseHandle(started.hProcess);
3619 Native.CloseHandle(started.hThread);
3623 browserSettings.ServiceDriverPid = (int)started.dwProcessId;
3625 if (
false == WaitForPort(retVal, 30_000))
3626 throw new GPALException($
"[{driverName}] never answered on port [{retVal}]");
3637 private static bool WaitForPort(
int port,
int timeoutInMs)
3639 bool retVal =
false;
3640 DateTime giveUpAt = DateTime.Now.AddMilliseconds(timeoutInMs);
3642 while (
false == retVal && DateTime.Now < giveUpAt)
3646 using (TcpClient probe =
new TcpClient())
3648 probe.Connect(
"127.0.0.1", port);
3652 catch (SocketException)
3667 internal static int FindFreePort(
int startPort = 0xdead)
3671 lock (portsHandedOut)
3675 int from = Math.Max(startPort, lastPortHandedOut + 1);
3677 for (
int port = from; port <= 65535 && 0 == retVal; port++)
3679 if (
true == portsHandedOut.Contains(port))
3684 TcpListener probe =
null;
3688 probe =
new TcpListener(System.Net.IPAddress.Loopback, port);
3692 portsHandedOut.Add(port);
3693 lastPortHandedOut = port;
3696 catch (SocketException)
3708 throw new GPALException($
"No free ports available between [{startPort}] and [65535].");
3721 internal static object MarkDocument(Browser browser)
3725 if (
true == browser.UseSelenium)
3726 mark = browser.BrowserDriver.FindElement(By.TagName(
"html"));
3727 else if (
true == browser.UsePuppeteer)
3728 mark = browser.PuppeteerCommunicator.DocumentMark().GetAwaiter().GetResult();
3741 internal static bool DocumentReplaced(Browser browser,
object mark)
3743 bool replaced =
false;
3745 if (
true == browser.UseSelenium && mark is IWebElement html)
3746 try { _ = html.TagName; }
3747 catch (StaleElementReferenceException) { replaced =
true; }
3748 catch (NoSuchElementException) { replaced =
true; }
3749 else if (
true == browser.UsePuppeteer && mark is
long marked && 0 < marked)
3750 replaced = marked != browser.PuppeteerCommunicator.DocumentMark().GetAwaiter().GetResult();
3767 internal static bool WaitForNavigationOrTime(Browser browser,
string priorUrl,
object documentMark,
int timeOutAfterMs)
3769 bool navigated =
false;
3770 bool documentGone =
false;
3771 bool urlChanged =
false;
3772 string lastUrl = priorUrl;
3773 DateTime timeout = DateTime.Now.AddMilliseconds(timeOutAfterMs);
3774 GPALEventType publishToConsole = GPAL.GPALSettings.ConsoleEvents;
3775 GPALEventType publishToDebug = GPAL.GPALSettings.DebugEvents;
3777 GPAL.GPALSettings.ConsoleEvents = GPALEventType.NONE;
3778 GPAL.GPALSettings.DebugEvents = GPALEventType.NONE;
3782 if (
true == browser.IsAlive)
3785 documentGone = DocumentReplaced(browser, documentMark);
3786 lastUrl = browser.GetSetCurrentUrl();
3787 urlChanged =
false == UrlHelper.AreEquivalent(priorUrl, lastUrl);
3789 navigated =
true == documentGone ||
true == urlChanged;
3792 if (
false == navigated && DateTime.Now < timeout)
3793 Thread.Sleep(NavigationPollMs);
3795 while (
false == navigated && DateTime.Now < timeout);
3799 GPAL.GPALSettings.ConsoleEvents = publishToConsole;
3800 GPAL.GPALSettings.DebugEvents = publishToDebug;
3805 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
3806 $
"Navigation watch [{(navigated ? "navigated
" : "stayed
")}] document [{(documentGone ? "replaced
" : "same
")}] url [{(urlChanged ? "changed
" : "same
")}] mark [{(documentMark is long mark ? mark.ToString() : null == documentMark ? "none
" : "held
")}] from [{priorUrl}] to [{lastUrl}]",
3807 browser, GPALObjectType.Browser);
3823 internal const int WaitForValuePollMs = 250;
3825 internal static void WaitForValueOrTime(Browser browser,
string resultsExpected, Func<dynamic> action,
int timeOutAfterMs)
3827 DateTime timeout = DateTime.Now.AddMilliseconds(timeOutAfterMs);
3828 string taskReturnValue;
3829 string[] tokens = resultsExpected.Split(
',');
3831 if (
true == browser.IsAlive)
3834 dynamic result = action();
3839 var task = (Task<string>)result;
3840 task.GetAwaiter().GetResult();
3843 var taskType = task.GetType();
3844 if (taskType.IsGenericType)
3846 var taskResult = taskType.GetProperty(
"Result")?.GetValue(task);
3847 taskReturnValue = taskResult?.ToString() ?? resultsExpected;
3851 taskReturnValue = tokens[0];
3857 taskReturnValue = result?.ToString() ?? resultsExpected;
3860 foreach (
string token
in tokens)
3861 if (
true == taskReturnValue.ToLower().Contains(token))
3867 Thread.Sleep(WaitForValuePollMs);
3869 while (DateTime.Now < timeout);
3872 #region NetworkHelpers
3883 int timeoutMs = browser.BrowserSettings.NetworkIdleTimeoutMs;
3884 int maxConnections = browser.BrowserSettings.NetworkIdleMaxConnections;
3885 int pruneMs = browser.BrowserSettings.NetworkIdlePruneMs;
3887 DateTime timeout = DateTime.Now.AddMilliseconds(timeoutMs);
3888 DateTime lastChange = DateTime.Now;
3890 if (lastSessionToken != sessionToken)
3892 lastErrorMessage.Clear();
3893 supressedMessage =
false;
3894 lastSessionToken = sessionToken;
3899 string currentErrorMessage = $
"Waiting up to [{timeoutMs}] ms for network to idle to [{maxConnections}] connections for [{pruneMs}] ms";
3901 if (
false == lastErrorMessage.Contains(currentErrorMessage))
3903 lastErrorMessage.Add(currentErrorMessage);
3904 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage, browser, GPALObjectType.Browser);
3905 supressedMessage =
false;
3907 else if (
false == supressedMessage)
3909 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
3910 supressedMessage =
true;
3913 while (DateTime.Now < timeout)
3916 int inflight = Convert.ToInt32(BrowserHelper.ExecuteJavaScriptObj(
3917 "return window.__gpalNetworkMonitor ? window.__gpalNetworkMonitor.inflight : 0;",
3921 if (inflight != lastCount)
3923 lastCount = inflight;
3924 lastChange = DateTime.Now;
3928 if (inflight == 0 && (DateTime.Now - lastChange).TotalMilliseconds >= pruneMs)
3930 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Network IS Idle. Status [true]");
3937 currentErrorMessage = $
"Network IS NOT Idle. Status [false]";
3939 if (
false == lastErrorMessage.Contains(currentErrorMessage))
3941 lastErrorMessage.Add(currentErrorMessage);
3942 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage, browser, GPALObjectType.Browser);
3943 supressedMessage =
false;
3945 else if (
false == supressedMessage)
3947 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
3948 supressedMessage =
true;
4047 int timeoutMs = 500,
4048 int maxConnections = 0,
4050 string sessionToken =
null)
4052 Exception innerEx =
null;
4055 if (lastSessionToken != sessionToken)
4057 lastErrorMessage.Clear();
4058 supressedMessage =
false;
4059 lastSessionToken = sessionToken;
4062 string currentErrorMessage = $
"Waiting up to [{timeoutMs}] ms for network to idle to [{maxConnections}] connections for [{pruneMs}] ms";
4064 if (
false == lastErrorMessage.Contains(currentErrorMessage))
4066 lastErrorMessage.Add(currentErrorMessage);
4068 supressedMessage =
false;
4070 else if (
false == supressedMessage)
4072 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
4073 supressedMessage =
true;
4076 DateTime startTime = DateTime.Now;
4077 while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
4079 string response = browser.OttoMagicClient.WithEndpoint(ApiEndpoint.CheckNetworkIdle).WithMaxConnections(maxConnections).WithPruneMs(pruneMs).WithTimeoutMs(timeoutMs).Execute();
4081 if (
false ==
string.IsNullOrEmpty(response))
4083 bool retVal = response.Contains(
"idle");
4085 currentErrorMessage = $
"Network IS{(retVal ? "" : " NOT
")} Idle. Status [{retVal}]";
4087 if (
false == lastErrorMessage.Contains(currentErrorMessage))
4089 lastErrorMessage.Add(currentErrorMessage);
4091 supressedMessage =
false;
4093 else if (
false == supressedMessage)
4095 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
4096 supressedMessage =
true;