57 static bool alreadyUpdating =
false;
59 static List<string> lastErrorMessage =
new List<string>();
60 static bool supressedMessage =
false;
61 static string lastSessionToken;
72 IWebDriver tmpDriver =
null;
73 ChromiumOptions options =
null;
74 bool waitForDebugger =
false;
75 string userAgent =
GPAL.GPALSettings.UserAgent;
77 if (
null != browserSettings.BrowserDriver)
78 return browserSettings.BrowserDriver;
82 ChromeProfileManager.SweepAbandonedTempProfiles();
84 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Running workflow for Automation Engine [{browserSettings.AutomationEngine}]",
null, GPALObjectType.None);
86 if (
null == browserSettings.DriverLocation)
88 browserSettings.DriverLocation = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6);
90 if (
true ==
GPAL.GPALSettings.AutoUpdateWebDriver &&
false == alreadyUpdating)
92 alreadyUpdating =
true;
93 DriverHelper.UpdateDriver(browserSettings);
101 if (BrowserType.Chrome == browserSettings.BrowserType || BrowserType.Edge == browserSettings.BrowserType)
103 if (BrowserType.Chrome == browserSettings.BrowserType)
104 options =
new ChromeOptions();
106 options =
new EdgeOptions();
110 if (
null != browserSettings.DialogsAccepted)
111 options.UnhandledPromptBehavior =
true == browserSettings.DialogsAccepted
112 ? UnhandledPromptBehavior.Accept
113 : UnhandledPromptBehavior.Dismiss;
115 if (
true == browserSettings.UseExistingBrowser)
117 options.DebuggerAddress =
"127.0.0.1:" + browserSettings.ExistingBrowserPort.ToString();
118 waitForDebugger =
true;
122 if (
null == browserSettings.DebugPort &&
true == browserSettings.UsePuppeteer &&
false == browserSettings.DebugPipe)
126 browserSettings.DebugPort = FindFreePort();
127 browserSettings.PuppeteerUrl = $
"http://127.0.0.1:{browserSettings.DebugPort}";
131 if (
false ==
string.IsNullOrEmpty(browserSettings.DownloadLocation))
132 options.AddUserProfilePreference(
"download.default_directory", browserSettings.DownloadLocation);
136 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileUserName) ||
false ==
string.IsNullOrEmpty(browserSettings.ProfileName))
140 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileName))
146 options.AddArguments($
"--profile-directory={FindProfileDirectory(browserSettings.ProfileDataDirectory, browserSettings.ProfileName)}");
149 else if (
true ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
152 browserSettings.ProfileDataDirectory = ChromeProfileManager.CreateTempUserProfile(
154 browserSettings.PromptForDownload,
155 browserSettings.OpenPDFExternally,
156 browserSettings.LoadImages,
157 browserSettings.UseOttoMagic
159 browserSettings.TempProfileCreated =
true;
165 if (
false == browserSettings.TempProfileCreated &&
false ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
166 browserSettings.PreviousDownloadPreferences = ChromeProfileManager.ApplyDownloadPreferences(
167 browserSettings.ProfileDataDirectory,
168 browserSettings.DownloadLocation,
169 browserSettings.PromptForDownload,
170 browserSettings.OpenPDFExternally);
175 if (
false == browserSettings.TempProfileCreated)
176 RefuseIfProfileIsOpen(browserSettings);
178 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
179 options.AddArgument($
"--user-data-dir={browserSettings.ProfileDataDirectory.Replace("\\
", "/
")}");
189 options.AddArguments(
"--disable-extensions");
195 string disableFeatures =
"TranslateUI,Translate";
197 if (BrowserType.Edge == browserSettings.BrowserType)
199 disableFeatures +=
",msShoppingTrigger,msShopping,msEdgeShoppingUI,msEdgeShoppingList";
202 if (
true == browserSettings.TempProfileCreated)
203 disableFeatures +=
",msImplicitSignin,msEdgeImplicitSignin,msIdentityImplicitSignin";
206 options.AddArgument($
"--disable-features={disableFeatures}");
211 if (
true == browserSettings.TempProfileCreated)
213 options.AddArgument(
"--disable-sync");
214 options.AddArgument(
"--disable-signin-promo");
218 options.AddArgument(
"--disable-infobars");
219 options.AddArgument(
"--disable-session-crashed-bubble");
220 options.AddArgument(
"--hide-crash-restore-bubble");
221 options.AddArgument(
"--noerrdialogs");
223 if (
true == browserSettings.UseHeadless)
226 options.AddArgument(
"--headless=new");
235 options.AddArgument(
"--disable-notifications");
237 options.AddArgument(
"--disable-popup-blocking");
244 string currUserAgent;
246 if (
false == browserSettings.UserAgentFromBrowser)
247 currUserAgent = ResolveUserAgent(browserSettings);
250 if (BrowserType.Chrome == browserSettings.BrowserType)
251 tmpDriver =
new ChromeDriver(browserSettings.DriverLocation, (ChromeOptions)options);
253 tmpDriver =
new EdgeDriver(browserSettings.DriverLocation, (EdgeOptions)options);
255 IJavaScriptExecutor js = (IJavaScriptExecutor)tmpDriver;
256 currUserAgent = (String)js.ExecuteScript(
"return navigator.userAgent");
260 options.AddArgument($
"--user-agent={currUserAgent.Replace("Headless
", "")}");
262 options.AddUserProfilePreference(
"download.prompt_for_download",
false);
266 if (
true == browserSettings.PromptForDownload)
267 options.AddUserProfilePreference(
"download.prompt_for_download",
true);
269 options.AddUserProfilePreference(
"download.prompt_for_download",
false);
276 options.AddUserProfilePreference(
"download.directory_upgrade",
true);
278 options.AddUserProfilePreference(
"plugins.always_open_pdf_externally", browserSettings.OpenPDFExternally);
279 options.AddArguments($
"--blink-settings=imagesEnabled={browserSettings.LoadImages.ToString().ToLower()}");
280 options.AddUserProfilePreference(
"disable-popup-blocking", $
"{!browserSettings.BlockPopUps}");
283 if (browserSettings.StealthType.HasFlag(StealthType.DarkMode))
284 options.AddArgument(
"--force-dark-mode");
287 if (
true == browserSettings.DebugPipe)
288 options.AddArgument($
"--remote-debugging-pipe --enable-unsafe-extension-debugging");
289 else if (
null != browserSettings.DebugPort)
290 options.AddArgument($
"--remote-debugging-port={browserSettings.DebugPort.Value}");
297 options.AddArguments(
"--disable-blink-features=AutomationControlled");
300 options.AddExcludedArgument(
"enable-automation");
301 if (options is ChromeOptions chromeOpts)
302 chromeOpts.AddAdditionalChromeOption(
"useAutomationExtension",
false);
303 else if (options is EdgeOptions edgeOpts)
304 edgeOpts.AddAdditionalEdgeOption(
"useAutomationExtension",
false);
307 if (browserSettings.StealthType.HasFlag(StealthType.PatchDriver))
309 if (BrowserType.Edge == browserSettings.BrowserType)
310 EdgePatcher.PatchEdgeDriver(browserSettings.Browser);
312 ChromePatcher.PatchChromeDriver(browserSettings.Browser);
321 options.SetLoggingPreference(OpenQA.Selenium.LogType.Performance, LogLevel.All);
323 tmpDriver = GetWebDriver(browserSettings, options);
330 else if (BrowserType.FireFox == browserSettings.BrowserType)
333 FirefoxDriverService firefoxDriverService = FirefoxDriverService.CreateDefaultService(browserSettings.DriverLocation);
334 FirefoxOptions ffOptions =
new FirefoxOptions();
336 if (
null != browserSettings.DialogsAccepted)
337 ffOptions.UnhandledPromptBehavior =
true == browserSettings.DialogsAccepted
338 ? UnhandledPromptBehavior.Accept
339 : UnhandledPromptBehavior.Dismiss;
340 if (
true == browserSettings.UseExistingBrowser)
342 firefoxDriverService.HideCommandPromptWindow =
true;
343 firefoxDriverService.Port = 0;
344 firefoxDriverService.Host =
"127.0.0.1";
345 firefoxDriverService.ConnectToRunningBrowser =
true;
348 firefoxDriverService.BrowserCommunicationPort = browserSettings.ExistingBrowserPort ?? 2828;
350 tmpDriver =
new FirefoxDriver(firefoxDriverService, ffOptions);
352 waitForDebugger =
true;
357 ffOptions.AddArgument($
"-width={browserSettings.WindowSize.Width}");
358 ffOptions.AddArgument($
"-height={browserSettings.WindowSize.Height}");
360 if (
true == browserSettings.UseHeadless)
364 ffOptions.AddArgument(
"-headless");
367 ffOptions.AddArgument(
"-disable-notifications");
374 if (
null != browserSettings.DebugPort)
376 firefoxDriverService.Port = (int)browserSettings.DebugPort.Value;
378 if (
true == browserSettings.UsePuppeteer)
379 browserSettings.PuppeteerUrl = $
"http://127.0.0.1:{browserSettings.DebugPort}";
381 if (
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory) &&
string.IsNullOrEmpty(browserSettings.ProfileName) &&
string.IsNullOrEmpty(browserSettings.ProfileUserName))
382 ffOptions.Profile =
new OpenQA.Selenium.Firefox.FirefoxProfileManager().GetProfile(
"default");
384 else if (
true == browserSettings.UsePuppeteer)
389 if (
null == browserSettings.DebugPort)
390 browserSettings.DebugPort = FindFreePort();
392 browserSettings.PuppeteerUrl = $
"http://127.0.0.1:{browserSettings.DebugPort}";
395 if (
null != browserSettings.DebugPort)
396 ffOptions.AddArgument($
"--remote-debugging-port={browserSettings.DebugPort.Value} ");
398 ffOptions.SetPreference(
"print.always_print_silent",
true);
399 ffOptions.SetPreference(
"print.show_print_progress",
false);
405 ffOptions.SetPreference(
"pdfjs.disabled", browserSettings.OpenPDFExternally || browserSettings.UseHeadless);
407 if (
false == browserSettings.PromptForDownload)
408 ffOptions.SetPreference(
"browser.helperApps.neverAsk.saveToDisk",
GPAL.GPALSettings.FirefoxDirectDownloadMimeTypes);
410 ffOptions.SetPreference(
"browser.helperApps.neverAsk.saveToDisk",
"");
415 ffOptions.SetPreference(
"browser.download.useDownloadDir",
false == browserSettings.PromptForDownload);
417 if (
false == browserSettings.LoadImages)
418 ffOptions.SetPreference(
"permissions.default.image", 0);
420 ffOptions.SetPreference(
"permissions.default.image", 1);
424 ffOptions.SetPreference(
"dom.popup_maximum",
true == browserSettings.BlockPopUps ? 0 : 20);
426 ffOptions.SetPreference(
"safebrowsing.enabled",
false);
428 if (
false ==
string.IsNullOrEmpty(browserSettings.DownloadLocation))
430 ffOptions.SetPreference(
"browser.download.dir", browserSettings.DownloadLocation);
431 ffOptions.SetPreference(
"browser.download.folderList", 2);
434 ffOptions.SetPreference(
"dom.webdriver.enabled",
false);
440 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileUserName) ||
false ==
string.IsNullOrEmpty(browserSettings.ProfileName))
442 ffOptions.AddArguments(
"-profile", (browserSettings.ProfileName ?? browserSettings.ProfileUserName).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
443 else if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
444 ffOptions.AddArguments(
"-profile", browserSettings.ProfileDataDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
462 if (
true == browserSettings.HiddenDesktop)
466 int hiddenPort = StartHiddenDriver(browserSettings);
468 tmpDriver =
new OpenQA.Selenium.Remote.RemoteWebDriver(
new Uri($
"http://127.0.0.1:{hiddenPort}"), ffOptions);
472 FirefoxDriverService firefoxService = FirefoxDriverService.CreateDefaultService(browserSettings.DriverLocation);
473 firefoxService.HideCommandPromptWindow =
true;
475 tmpDriver =
new FirefoxDriver(firefoxService, ffOptions);
478 browserSettings.ServiceDriverPid = firefoxService.ProcessId;
486 browserSettings.Process = SeleniumBrowserProcess(tmpDriver,
"moz:processID");
497 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to start browser driver for [{browserSettings.BrowserType}].", browserSettings.Browser, GPALObjectType.Browser, ex);
502 if (
null != tmpDriver)
504 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Browser [{browserSettings.BrowserType}] launched.", browserSettings, GPALObjectType.Other);
506 browserSettings.BrowserDriver = tmpDriver;
513 var edgeGhostBrowser = browserSettings.Browser as
Browser;
514 if (BrowserType.Edge == browserSettings.BrowserType &&
null != edgeGhostBrowser)
515 try { edgeGhostBrowser.GhostTabUrl = tmpDriver.Url; }
519 tmpDriver.Manage().Timeouts().PageLoad.Add(System.TimeSpan.FromSeconds(30));
523 browserSettings.CurrentURL =
"about:blank";
525 if (
true == browserSettings.Maximize)
526 tmpDriver.Manage().Window.
Maximize();
527 else if (
true == browserSettings.Minimize)
528 tmpDriver.Manage().Window.Minimize();
529 else if (
true == browserSettings.FullScreen)
530 tmpDriver.Manage().Window.FullScreen();
531 else if (
false == browserSettings.WindowSize.IsEmpty)
534 var windowSize =
new Size(browserSettings.WindowSize.Width, browserSettings.WindowSize.Height);
535 tmpDriver.Manage().Window.Size = windowSize;
538 var windowPosition =
new Point(browserSettings.WindowSize.Left, browserSettings.WindowSize.Top);
539 tmpDriver.Manage().Window.Position = windowPosition;
542 if (
true == waitForDebugger)
545 if (BrowserType.Chrome == browserSettings.BrowserType)
547 DriverHelper.GetChromeVersion(out browserSettings.Version);
549 else if (BrowserType.Edge == browserSettings.BrowserType)
551 DriverHelper.GetEdgeVersion(out browserSettings.Version);
553 else if (BrowserType.FireFox == browserSettings.BrowserType)
555 DriverHelper.GetFirefoxVersion(out browserSettings.Version);
558 CheckDocumentReady(((
Browser)browserSettings.Browser),
true);
578 var parameters =
new Dictionary<string, object>
580 {
"behavior",
"allow" },
581 {
"downloadPath", directory }
584 ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.setDownloadBehavior", parameters);
597 internal static string ResolveUserAgent(
BrowserSettings browserSettings)
599 string userAgent = browserSettings.OverrideUserAgent;
601 if (
true ==
string.IsNullOrWhiteSpace(userAgent))
602 userAgent =
GPAL.GPALSettings.UserAgent;
604 if (
true ==
string.IsNullOrWhiteSpace(userAgent))
622 internal static void PresentCredentials(
Browser browser, GPALUrl url)
625 WebAuthType authType = credential.WebAuthType;
626 string header = AuthorizationHeader(credential, authType);
628 browser.BrowserSettings.CredentialsPresented = DateTime.UtcNow;
631 if (WebAuthType.Form == authType || WebAuthType.None == authType)
634 if (
true ==
string.IsNullOrEmpty(header) || BrowserType.FireFox == browser.BrowserSettings.BrowserType ||
true == browser.UseOttoMagic)
635 CredentialsInUrl(browser, url, credential);
638 string name = AuthorizationHeaderName(authType);
639 Dictionary<string, object> headers =
new Dictionary<string, object> { { name, header } };
641 if (
true == browser.UsePuppeteer)
642 browser.PuppeteerCommunicator.SetExtraHttpHeaders(headers).GetAwaiter().GetResult();
644 ((OpenQA.Selenium.Chromium.ChromiumDriver)browser.BrowserDriver).ExecuteCdpCommand(
"Network.setExtraHTTPHeaders",
645 new Dictionary<string, object> { {
"headers", headers } });
647 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Credentials presented as [{authType}] for [{url?.Url}]", browser, GPALObjectType.Browser);
656 internal static string AuthorizationHeader(Credentials credential, WebAuthType authType)
660 if (WebAuthType.Basic == authType || WebAuthType.Proxy == authType)
661 value =
"Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($
"{credential.Username}:{credential.Password}"));
662 else if (WebAuthType.Bearer == authType)
663 value =
"Bearer " + credential.AccessToken;
664 else if (WebAuthType.XAuthToken == authType || WebAuthType.XApiKey == authType || WebAuthType.ApiKey == authType)
665 value = credential.AccessToken;
675 internal static string AuthorizationHeaderName(WebAuthType authType)
677 string name =
"Authorization";
679 if (WebAuthType.Proxy == authType)
680 name =
"Proxy-Authorization";
681 else if (WebAuthType.XAuthToken == authType)
682 name =
"X-Auth-Token";
683 else if (WebAuthType.XApiKey == authType)
685 else if (WebAuthType.ApiKey == authType)
698 static void CredentialsInUrl(Browser browser, GPALUrl url, Credentials credential)
700 UriBuilder builder =
new UriBuilder(url?.Url)
702 UserName = Uri.EscapeDataString(credential.Username ??
string.Empty),
703 Password = Uri.EscapeDataString(credential.Password ??
string.Empty)
705 GPALUrl credentialed =
new GPALUrl(builder.Uri.ToString());
709 if (
true == browser.UseOttoMagic)
710 browser.MagicHelper.GoTo(credentialed);
711 else if (
true == browser.UsePuppeteer)
712 browser.PuppeteerClient.GoTo(credentialed).Execute();
716 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Credentials presented to [{builder.Host}], answering its challenge", browser, GPALObjectType.Browser);
733 private static Process SeleniumBrowserProcess(IWebDriver driver,
string capability)
735 Process retVal =
null;
739 object pid = (driver as IHasCapabilities)?.Capabilities?.GetCapability(capability);
742 retVal = Process.GetProcessById(Convert.ToInt32(pid));
748 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"No browser process behind the driver for [{capability}]",
null, GPALObjectType.Browser, ex);
753 internal static void WaitForBrowserToExit(BrowserSettings browserSettings,
int timeoutMs)
757 browserSettings.Process?.WaitForExit(timeoutMs);
761 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not wait on the browser process before restoring [{browserSettings.ProfileDataDirectory}]", browserSettings.Browser, GPALObjectType.Browser, ex);
780 string directory = Path.GetDirectoryName(destination);
781 string filename = Path.GetFileName(destination);
783 var enableDownloadCommandParameters =
new Dictionary<string, object>
785 {
"behavior",
"allow" },
786 {
"downloadPath", directory },
787 {
"filename", filename }
790 if (BrowserType.Chrome == browserSettings.BrowserType || BrowserType.Edge == browserSettings.BrowserType)
791 ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.setDownloadBehavior", enableDownloadCommandParameters);
808 internal const string CallRecorderScript =
@"
810 if (window.__gpalCalls) return;
812 window.__gpalCalls = [];
814 var record = function (url, method, headers, body, type) {
815 var call = { url: String(url), method: method || 'GET', type: type, headers: headers || {}, postData: body || null, status: 0 };
816 window.__gpalCalls.push(call);
820 var nativeFetch = window.fetch;
823 window.fetch = function (input, init) {
824 var url = input && input.url ? input.url : input;
826 var options = init || {};
828 // a Headers object, a plain object or an array of pairs, all of which fetch accepts
829 if (options.headers && options.headers.forEach)
830 options.headers.forEach(function (v, k) { headers[k] = v; });
831 else if (options.headers)
832 Object.keys(options.headers).forEach(function (k) { headers[k] = options.headers[k]; });
834 var call = record(url, options.method, headers, options.body, 'Fetch');
836 return nativeFetch.apply(this, arguments).then(function (response) {
837 call.status = response.status;
839 }, function (error) {
845 var nativeOpen = XMLHttpRequest.prototype.open;
846 var nativeSend = XMLHttpRequest.prototype.send;
847 var nativeSetHeader = XMLHttpRequest.prototype.setRequestHeader;
849 XMLHttpRequest.prototype.open = function (method, url) {
850 this.__gpalCall = { method: method, url: url, headers: {} };
851 return nativeOpen.apply(this, arguments);
854 XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
855 if (this.__gpalCall) this.__gpalCall.headers[name] = value;
856 return nativeSetHeader.apply(this, arguments);
859 XMLHttpRequest.prototype.send = function (body) {
860 var pending = this.__gpalCall;
863 var call = record(pending.url, pending.method, pending.headers, body, 'XHR');
864 this.addEventListener('loadend', function () { call.status = this.status; });
867 return nativeSend.apply(this, arguments);
872 public static string SeleniumAddScriptToEvaluateOnNewDocument(
BrowserSettings browserSettings,
string script)
874 if (BrowserType.Chrome != browserSettings.BrowserType && BrowserType.Edge != browserSettings.BrowserType)
876 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{browserSettings.BrowserType}] does not support Page.addScriptToEvaluateOnNewDocument", browserSettings.Browser, GPALObjectType.Browser);
880 var result = ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.addScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
883 }) as Dictionary<string, object>;
885 return result?[
"identifier"]?.ToString();
896 if (BrowserType.Chrome != browserSettings.BrowserType && BrowserType.Edge != browserSettings.BrowserType)
898 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{browserSettings.BrowserType}] does not support Page.removeScriptToEvaluateOnNewDocument", browserSettings.Browser, GPALObjectType.Browser);
902 ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.removeScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
904 {
"identifier", identifier }
917 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
918 string roamingAppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
919 string profilePath =
string.Empty;
921 switch (browserSettings.BrowserType)
923 case BrowserType.Chrome:
924 profilePath = Path.Combine(localAppData,
"Google",
"Chrome",
"User Data");
926 case BrowserType.FireFox:
927 profilePath = Path.Combine(roamingAppData,
"Mozilla",
"Firefox",
"Profiles");
929 case BrowserType.Edge:
930 profilePath = Path.Combine(localAppData,
"Microsoft",
"Edge",
"User Data");
933 Exception ex =
new Exception(
"Unsupported Browser Type");
934 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unsupported browser type for loading user profile [{browserSettings.ProfileUserName}]: [{browserSettings.BrowserType.ToString()}]", ex);
938 string newProfilePath = profilePath;
941 if (
false ==
string.IsNullOrEmpty(browserSettings.ProfileUserName) &&
false == getDefaultDirectory)
943 newProfilePath = Path.Combine(profilePath, browserSettings.ProfileUserName);
947 if (!Directory.Exists(newProfilePath))
948 RefuseProfile($
"[{browserSettings.BrowserType}] has no profile directory [{newProfilePath}] for user name [{browserSettings.ProfileUserName}]");
950 return newProfilePath;
966 if (
false == Directory.Exists(userDataDirectory))
967 RefuseProfile($
"Looked for profile [{profileName}] in [{userDataDirectory}], which is not there");
969 string[] profileDirectories = Directory.GetDirectories(userDataDirectory);
970 List<string> matches =
new List<string>();
972 foreach (
string profileDirectory
in profileDirectories)
974 string preferencesFile = Path.Combine(profileDirectory,
"Preferences");
976 if (File.Exists(preferencesFile))
978 string profileNameFromFile = GetProfileNameFromPreferencesFile(preferencesFile);
980 if (
null != profileNameFromFile && profileNameFromFile.Equals(profileName, StringComparison.OrdinalIgnoreCase))
981 matches.Add(
new DirectoryInfo(profileDirectory).Name);
985 if (0 < matches.Count)
989 if (1 < matches.Count)
991 string choices =
string.Join(
", ", matches.Select(directory =>
992 $
"[{directory}] ({SignedInSaid(Path.Combine(userDataDirectory, directory))})"));
995 $
"[{matches.Count}] profiles are named [{profileName}]: {choices}. Using [{matches[0]}]. Name the directory instead of the display name to say which one you meant",
996 null, GPALObjectType.Browser);
1003 if (
true == Directory.Exists(Path.Combine(userDataDirectory, profileName)))
1008 RefuseProfile($
"No profile named [{profileName}] in [{userDataDirectory}], by display name or directory name");
1014 private static string SignedInSaid(
string profileDirectory)
1016 string retVal =
"cannot tell";
1020 string preferencesFile = Path.Combine(profileDirectory,
"Preferences");
1022 if (
true == File.Exists(preferencesFile))
1024 JObject preferences = JObject.Parse(File.ReadAllText(preferencesFile));
1025 JArray accounts = preferences.SelectToken(
"account_info") as JArray;
1027 retVal =
null != accounts && 0 < accounts.Count ?
"signed in" :
"not signed in";
1039 private static void RefuseProfile(
string message)
1041 GPAL.PublishSimpleEvent(GPALEventType.ERROR, message,
null, GPALObjectType.Browser);
1043 throw new GPALException($
"{GPAL.MyMethodName()}: {message}");
1051 static string GetProfileNameFromPreferencesFile(
string preferencesFilePath)
1055 string preferencesContent = File.ReadAllText(preferencesFilePath);
1056 JObject preferencesJson = JObject.Parse(preferencesContent);
1059 JToken givenNameToken = preferencesJson.SelectToken(
"profile.name");
1060 if (givenNameToken !=
null)
1062 return givenNameToken.ToString();
1081 using (RegistryKey key = Registry.LocalMachine.OpenSubKey(
GPAL.GPALSettings.FirefoxBinaryPathRegistry))
1086 string firefoxPath = key.GetValue(
null)?.ToString();
1087 if (!
string.IsNullOrEmpty(firefoxPath))
1103 var handles = browserSettings.BrowserDriver.WindowHandles;
1104 foreach (var handle
in handles)
1106 browserSettings.BrowserDriver.SwitchTo().Window(handle);
1107 if (URL.Contains(browserSettings.BrowserDriver.Url) || browserSettings.BrowserDriver.Url.Contains(URL))
1124 List<GPALElement> matchedElems =
null;
1125 ReadOnlyCollection<GPALElement> elems =
null;
1126 bool clicked =
false;
1138 browser.CurrentUOW.NextPageButton.WebSelectorFoundResults =
null;
1139 browser.CurrentUOW.NextPageButton.WebSelectorMatchedResults =
null;
1142 sel.WebSelectorFoundResults =
null;
1143 sel.WebSelectorMatchedResults =
null;
1146 elems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, browser.CurrentUOW.
NextPageButton, out
bool matchedAll, out matchedElems,
null,
false);
1149 if (
null != elems && 0 < elems.Count &&
true == matchedAll)
1154 if (
false ==
string.IsNullOrEmpty(elems[0].Href))
1156 string href = elems[0].Href;
1159 browser.BrowserSettings.ObeyRobotsTxt =
false;
1165 Stopwatch arriving = Stopwatch.StartNew();
1168 if (
false == href.Equals(here, StringComparison.OrdinalIgnoreCase))
1169 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Waiting for [{href}], the page showing is [{here}]", browser, GPALObjectType.Browser);
1171 while (
false == href.Equals(here, StringComparison.OrdinalIgnoreCase)
1172 && browser.
BrowserSettings.WaitForDocumentReadyTimeoutMs > arriving.ElapsedMilliseconds)
1174 System.Threading.Thread.Sleep(250);
1179 if (
false == href.Equals(here, StringComparison.OrdinalIgnoreCase))
1180 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Gave up waiting for [{href}] after [{arriving.ElapsedMilliseconds}] ms, the page showing is [{here}]", browser, GPALObjectType.Browser);
1182 browser.CurrentUOW = saveUOW;
1183 browser.BrowserSettings.ObeyRobotsTxt = obeySave;
1184 browser.BrowserSettings.CurrentURL = href;
1199 catch (Exception ex)
1201 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Exception thrown", browser, GPALObjectType.Browser, ex);
1206 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Unable to find Next Page Button", browser, GPALObjectType.Browser);
1212 HardwareHelper.SendKey(VK_END);
1216 CheckDocumentReady(browser);
1239 List<ReadOnlyCollection<GPALElement>> rowsOfColumns =
new List<ReadOnlyCollection<GPALElement>>();
1240 List<ReadOnlyCollection<GPALElement>> rowsOfElements =
new List<ReadOnlyCollection<GPALElement>>();
1243 List<SmallSelectorNode> interactionInfo =
new List<SmallSelectorNode>();
1253 interactionInfo.Clear();
1254 rowsOfElements.Clear();
1255 rowsOfColumns.Clear();
1256 browser.CurrentUOW.MatchedRowIndexes.Clear();
1257 browser.CurrentUOW.ColCount = 0;
1258 browser.CurrentUOW.RowCount = 0;
1259 browser.CurrentUOW.PartialMatch =
false;
1263 if (SelectorType.Selector != sel.SelectorType)
1267 List<GPALElement> matchedElems =
null;
1268 ReadOnlyCollection<GPALElement> tmpElems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, sel, out
bool matchedAll, out matchedElems);
1269 rowsOfElements.Add(tmpElems);
1270 browser.CurrentUOW.ColCount++;
1272 if (
false == matchedAll)
1273 browser.CurrentUOW.PartialMatch |=
true;
1275 if (0 < tmpElems?.Count &&
true == matchedAll)
1278 browser.CurrentUOW.RowCount = Math.Max(browser.CurrentUOW.RowCount, (
int.MaxValue == browser.CurrentUOW.
WithAllThatMatch ? tmpElems.Count : browser.CurrentUOW.
WithAllThatMatch));
1280 else if (0 < matchedElems?.Count &&
false == matchedAll)
1289 if (-1 != (tmpIdx = tmpElems.IndexOf(webElement)))
1290 if (
false == browser.CurrentUOW.MatchedRowIndexes.Contains(
new KeyValuePair<int, string>(tmpIdx, sel.AttributeName)))
1291 browser.CurrentUOW.MatchedRowIndexes.Add(tmpIdx, sel.AttributeName);
1295 browser.CurrentUOW.RowCount = (int.MaxValue == browser.CurrentUOW.WithAllThatMatch ? browser.CurrentUOW.MatchedRowIndexes.Count : browser.CurrentUOW.
WithAllThatMatch);
1299 if (0 < matchedElems?.Count || 0 < tmpElems?.Count)
1300 interactionInfo.Add(
new SmallSelectorNode() { InteractionType = sel.
InteractionType, OffsetX = sel.
OffsetX, OffsetY = sel.
OffsetY, DeltaX = sel.
DeltaX, DeltaY = sel.DeltaY });
1306 List<GPALElement> newColumn =
new List<GPALElement>();
1308 browser.CurrentUOW.RowCount = browser.CurrentUOW.MatchedRowIndexes.Count;
1312 foreach (ReadOnlyCollection<GPALElement> webElements
in rowsOfElements)
1316 foreach (KeyValuePair<int, string> idx
in browser.CurrentUOW.MatchedRowIndexes)
1317 newColumn.Add(webElements[idx.Key]);
1318 rowsOfColumns.Add(
new ReadOnlyCollection<GPALElement>(newColumn));
1319 newColumn =
new List<GPALElement>();
1324 foreach (ReadOnlyCollection<GPALElement> webElements
in rowsOfElements)
1325 rowsOfColumns.Add(webElements);
1329 browser.CurrentUOW.ElementGrid.Clear();
1332 if (0 != browser.CurrentUOW.ColCount)
1334 for (
int cnt = 0; cnt < browser.CurrentUOW.RowCount; cnt++)
1335 browser.CurrentUOW.ElementGrid.AddRow(
new List<UnitOfWork.ElementNode>(browser.CurrentUOW.ColCount));
1337 int columnIdx = 0; ;
1342 foreach (ReadOnlyCollection<GPALElement> column
in rowsOfColumns)
1344 rowCnt = lastRowCnt;
1347 if (
true == ElementHelper.IsElementEditable(elem))
1348 browser.CurrentUOW.ElementGrid[rowCnt].Add(
1352 InteractionType = interactionInfo[columnIdx].InteractionType,
1353 OffsetX = interactionInfo[columnIdx].OffsetX,
1354 OffsetY = interactionInfo[columnIdx].OffsetY
1372 if (tokenIdx < tokens.Count())
1376 foreach (List<UnitOfWork.ElementNode> elementNode
in browser.CurrentUOW.ElementGrid)
1379 List<string> currentRow = tokens[tokenIdx];
1381 foreach (
string token
in currentRow)
1383 if (elementIdx < elementNode.Count &&
null != elementNode[elementIdx])
1385 ElementHelper.ScrollIntoView(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].InteractionType);
1389 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
1390 ElementHelper.HardwareFillInFrom(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
1394 case WriteMode.Append:
1398 case WriteMode.Insert:
1402 case WriteMode.Overwrite:
1409 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
1410 ElementHelper.HardwareFillInFrom(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
1414 case WriteMode.Append:
1418 case WriteMode.Insert:
1422 case WriteMode.Overwrite:
1429 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
1430 ElementHelper.HardwareFillInFrom(browser, elementNode[elementIdx].
GPALElement, elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
1431 else if (InteractionType.JavaScript == elementNode[elementIdx].InteractionType)
1432 ElementHelper.JavaScriptFillInFrom(browser, elementNode[elementIdx].
GPALElement, token.ToString(), writeMode);
1434 ElementHelper.SeleniumFillInFrom(browser, elementNode[elementIdx].
GPALElement, token.ToString(), writeMode);
1441 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Unused token [{token}] from tokens [{string.Join(",
", currentRow)}] in row [{tokenIdx + 1}]", browser, GPALObjectType.Browser);
1445 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{writeMode}] text [{token.ToString()}] in element [{elementNode[elementIdx - 1].GPALElement.TagName}]", browser, GPALObjectType.Browser);
1449 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"NOT WRITTEN: [{writeMode}] text [{token.ToString()}] in element [NOT FOUND].", browser, GPALObjectType.Browser);
1455 while (elementIdx < elementNode.Count &&
null != elementNode[elementIdx++])
1456 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No token for control: <[{elementNode[elementIdx - 1].GPALElement.GetType()}]> : [{elementNode[elementIdx - 1].GPALElement.Text}]", browser, GPALObjectType.Browser);
1460 CallIfStatus handled = 0;
1464 if (
null != browser.CurrentUOW.CallAfterFillIn &&
true == rowsOfColumns.Any())
1466 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Invoking CallAfterFillIn [{browser.CurrentUOW.CallAfterFillIn.Method.Name}]", browser, GPALObjectType.Browser);
1467 handled = browser.CurrentUOW.CallAfterFillIn(browser, tokens, tokenIdx);
1472 browser.CurrentUOW = safeUOW;
1487 if (CallIfStatus.Terminate == handled)
1489 string str = $
"CallAfterFillIn handler [{browser.CurrentUOW.CallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.";
1499 lastRowCnt += rowCnt;
1507 if (tokenIdx == tokens.Count())
1518 private const int NavigationPollMs = 50;
1521 private const string CdpWindowPrefix =
"CDwindow-";
1522 private const string MozProcessIdCapability =
"moz:processID";
1540 static string ReadyStatusOrNavError(
Browser browser,
string sessionToken)
1544 if (
true == status?.ToLower().Contains(
"nav-error"))
1547 browser.RaiseOnFail(GPALFailure.Navigation, status);
1552 public static void CheckDocumentReady(Browser browser,
bool overrideSetting =
false)
1554 bool hasURL =
null != browser?.BrowserSettings.CurrentURL &&
false ==
"about:blank".Equals(browser.BrowserSettings.CurrentURL);
1559 bool wasSuppressed =
true == browser?.BrowserSettings.SuppressJavaScriptFailures;
1560 string sessionToken = Guid.NewGuid().ToString();
1562 if (
null != browser)
1563 browser.BrowserSettings.SuppressJavaScriptFailures =
true;
1567 if ((
true == browser?.BrowserSettings.WaitOnDocumentReady &&
true == hasURL) ||
true == overrideSetting)
1569 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Waiting up to [{browser.BrowserSettings.WaitForDocumentReadyTimeoutMs}] ms on document.ready");
1573 if (
true == browser?.UseOttoMagic)
1574 WaitForValueOrTime(browser,
"complete, interactive, nav-error", () => ReadyStatusOrNavError(browser, sessionToken), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1575 else if (
true == browser?.UsePuppeteer)
1576 WaitForValueOrTime(browser,
"complete, interactive", () => ((PuppeteerClient)browser.PuppeteerClient).GetReadyStatus(sessionToken).Execute<
object>(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1578 WaitForValueOrTime(browser,
"complete, interactive", () =>
JsCheckReadyState(browser, sessionToken), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1580 else if (
true == browser.BrowserSettings.WaitOnNetworkIdle &&
true == hasURL)
1582 if (
true == browser.UsePuppeteer)
1583 WaitForValueOrTime(browser,
"true", () => browser.PuppeteerClient.CheckNetworkIdle().WithSessionToken(sessionToken).Execute<
bool>().ToString(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1584 else if (
true == browser.BrowserSettings.UseOttoMagic)
1585 WaitForValueOrTime(browser,
"true", () =>
WaitForNetworkIdle(browser, browser.BrowserSettings.NetworkIdleTimeoutMs, browser.BrowserSettings.NetworkIdleMaxConnections, browser.BrowserSettings.NetworkIdlePruneMs, sessionToken).ToString(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1587 WaitForValueOrTime(browser,
"true", () =>
WaitForNetworkIdle(browser, sessionToken).ToString(), browser.BrowserSettings.WaitForDocumentReadyTimeoutMs);
1592 if (
null != browser)
1593 browser.BrowserSettings.SuppressJavaScriptFailures = wasSuppressed;
1618 internal static void FetchWithTokens(Browser browser, GPALRequest request)
1620 IGPALGrid<string> tokens = browser.CurrentUOW.FetchTokens;
1621 int required = request.TokenCount();
1622 string name = request.Name ?? request.Path;
1624 if (
null == tokens && 0 < required)
1625 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch of [{name}] uses [{required}] tokens but no token source was given. Use .WithTokensFrom before .Fetch.", browser, GPALObjectType.Browser);
1629 int rowCount = tokens?.Rows ?? 1;
1631 for (
int tokenIdx = 0; tokenIdx < rowCount; tokenIdx++)
1633 List<string> row =
null == tokens ? null : tokens[tokenIdx];
1637 if (
null != row && row.Count < required)
1638 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch of [{name}] needs [{required}] tokens but row [{tokenIdx + 1}] has [{row.Count}]. Skipping the row.", browser, GPALObjectType.Browser);
1640 FetchRow(browser, request, row, tokenIdx);
1652 static void FetchRow(Browser browser, GPALRequest request, List<string> row,
int tokenIdx)
1654 int mark = browser.CurrentUOW.FetchResults.Count;
1655 string name = request.Name ?? request.Path;
1657 for (
int page = 0; page < browser.CurrentUOW.PageCount; page++)
1659 string where =
null == row ? $
"Page [{page + 1}]" : $
"Row [{tokenIdx + 1}] page [{page + 1}]";
1661 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Fetching [{name}]. {where}.", browser, GPALObjectType.Browser);
1663 string body = FetchInPage(browser, request, page, row);
1671 string why = $
"Fetch of [{name}] returned nothing on {where.ToLower()}.";
1673 GPAL.PublishSimpleEvent(GPALEventType.ERROR, why, browser, GPALObjectType.Browser);
1676 else if (200 > browser.ServerResponseCode || 300 <= browser.ServerResponseCode)
1680 string why = $
"Fetch of [{name}] returned HTTP [{browser.ServerResponseCode}] on {where.ToLower()}.";
1682 GPAL.PublishSimpleEvent(GPALEventType.WARNING, why, browser, GPALObjectType.Browser);
1683 body = why + Environment.NewLine + body;
1686 browser.CurrentUOW.FetchResults.Add(body);
1689 if (
null != request.AfterFetch)
1691 UnitOfWork safeUOW = browser.CurrentUOW;
1692 List<string> results = browser.CurrentUOW.FetchResults.GetRange(mark, browser.CurrentUOW.FetchResults.Count - mark);
1694 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Invoking CallAfterFetch [{request.AfterFetch.Method.Name}]", browser, GPALObjectType.Browser);
1696 CallIfStatus handled = request.AfterFetch(browser, results, browser.CurrentUOW.FetchTokens, tokenIdx);
1699 browser.CurrentUOW = safeUOW;
1701 if (CallIfStatus.Terminate == handled)
1703 string str = $
"CallAfterFetch handler [{request.AfterFetch.Method.Name}] requested program termination.";
1704 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
1705 throw new GPALException($
"{GPAL.MyMethodName()}: " + str);
1714 internal static string FetchInPage(Browser browser, GPALRequest request,
int page, List<string> row)
1718 string envelope = FetchEnvelope(browser, request.ResolveUrl(page, row), request.Method, request.ResolveBody(page, row), request.ContentType, request.HeaderPairs(page, row),
false);
1720 return UnpackFetchResponse(browser, envelope);
1735 static string FetchEnvelope(Browser browser,
string url,
string method,
string body,
string contentType,
string[] headers,
bool asBytes)
1737 string envelope =
null;
1739 if (
true == browser.UseSelenium)
1740 envelope = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteAsyncScript(
1741 FetchSetupScript(url, method, body, contentType, headers, asBytes) +
@"
1742 var done = arguments[arguments.length - 1];
1744 fetch(target.toString(), options)
1747 .catch(function (error) { done(gpalFailure(error)); });
1749 else if (
true == browser.UsePuppeteer)
1750 envelope = browser.PuppeteerClient
1754 .WithContentType(contentType)
1755 .WithHeaders(headers)
1758 else if (
true == browser.UseOttoMagic)
1759 envelope = browser.MagicHelper.Fetch(url, method, body, contentType, headers, asBytes);
1761 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch is not implemented for [{browser.BrowserSettings.AutomationEngine}] yet.", browser, GPALObjectType.Browser);
1774 internal static string LiveUserAgent(Browser browser)
1776 string retVal =
null;
1778 if (
true == browser.UseSelenium)
1779 retVal = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteScript(
"return navigator.userAgent") as string;
1780 else if (
true == browser.UsePuppeteer)
1781 retVal = browser.PuppeteerClient.GetUserAgent().Execute();
1782 else if (
true == browser.UseOttoMagic)
1783 retVal = Unquoted(browser.MagicHelper.GetUserAgent());
1795 static string Unquoted(
string value)
1797 string retVal = value;
1799 while (
false ==
string.IsNullOrEmpty(retVal) &&
true == retVal.StartsWith(
"\"") &&
true == retVal.EndsWith(
"\"") && 1 < retVal.Length)
1801 string unwrapped = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(retVal);
1803 if (unwrapped == retVal)
1820 internal static string LiveAcceptLanguage(Browser browser)
1822 string languages =
null;
1823 string retVal =
null;
1825 if (
true == browser.UseSelenium)
1826 languages = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteScript(
"return navigator.languages.join(',')") as string;
1827 else if (
true == browser.UsePuppeteer)
1828 languages = browser.PuppeteerClient.GetLanguages().Execute();
1829 else if (
true == browser.UseOttoMagic)
1830 languages = Unquoted(browser.MagicHelper.GetLanguages());
1832 if (
false ==
string.IsNullOrWhiteSpace(languages))
1833 retVal = AcceptLanguageFrom(languages.Split(
','), browser.BrowserSettings.BrowserType);
1851 static string AcceptLanguageFrom(
string[] languages, Enums.BrowserType browserType)
1853 List<string> weighted =
new List<string>();
1855 for (
int index = 0; index < languages.Length; index++)
1857 string language = languages[index].Trim();
1859 if (
true ==
string.IsNullOrEmpty(language))
1862 if (Enums.BrowserType.FireFox == browserType)
1863 language = language.ToLowerInvariant();
1866 weighted.Add(language);
1871 double quality = Math.Round(1.0 - (0.1 * index), 1);
1874 weighted.Add($
"{language};q={quality.ToString("0.#
", System.Globalization.CultureInfo.InvariantCulture)}");
1878 return string.Join(
",", weighted);
1892 internal static byte[] FetchBytes(Browser browser,
string url,
string[] headers, out
string lastModified)
1894 byte[] bytes =
null;
1895 JObject response = UnpackFetchEnvelope(browser, FetchEnvelope(browser, url,
"GET",
null,
null, headers,
true));
1896 string encoded = response?[
"body"]?.ToString();
1898 lastModified = response?[
"lastModified"]?.ToString();
1900 if (
false ==
string.IsNullOrEmpty(encoded))
1901 bytes = Convert.FromBase64String(encoded);
1913 static string UnpackFetchResponse(Browser browser,
string envelope)
1915 return UnpackFetchEnvelope(browser, envelope)?[
"body"]?.ToString();
1924 static JObject UnpackFetchEnvelope(Browser browser,
string envelope)
1926 JObject response =
null;
1930 browser.ServerResponseCode = 0;
1932 if (
true ==
string.IsNullOrEmpty(envelope))
1933 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{browser.BrowserSettings.AutomationEngine}] returned nothing for the fetch, so the page never answered", browser, GPALObjectType.Browser);
1936 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Fetch envelope [{envelope}]", browser, GPALObjectType.Browser);
1940 if (
false == envelope.TrimStart().StartsWith(
"{"))
1941 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Fetch response was not understood. Received [{envelope}]", browser, GPALObjectType.Browser);
1944 response = JObject.Parse(envelope);
1945 browser.ServerResponseCode = response[
"status"]?.Value<
int>() ?? 0;
1949 if (
false ==
string.IsNullOrEmpty(response[
"error"]?.ToString()))
1950 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The page could not make the request [{response["error
"]}]", browser, GPALObjectType.Browser);
1970 internal static string FetchSetupScript(
string url,
string method,
string body,
string contentType,
string[] headers,
bool asBytes)
1973 var target = new URL({Newtonsoft.Json.JsonConvert.SerializeObject(url)}, location.href);
1974 var reqBody = {Newtonsoft.Json.JsonConvert.SerializeObject(body)};
1975 var contentType = {Newtonsoft.Json.JsonConvert.SerializeObject(contentType)};
1976 var headerPairs = {Newtonsoft.Json.JsonConvert.SerializeObject(headers ?? new string[0])};
1977 var asBytes = {(true == asBytes ? "true" : "false")};
1979 var options = {{ method: {Newtonsoft.Json.JsonConvert.SerializeObject(method ?? "GET
")}, credentials: 'include', headers: {{}} }};
1980 for (var h = 0; h < headerPairs.length; h += 2)
1981 options.headers[headerPairs[h]] = headerPairs[h + 1];
1984 options.body = reqBody;
1985 if (!options.headers['Content-Type'])
1986 options.headers['Content-Type'] = contentType;
1989 // one envelope for every engine, so the C# side has a single shape to unpack. text() would put a
1990 // replacement character wherever a byte is not valid utf-8, which is most of any real file, so a
1991 // file is read as an arraybuffer and base64 encoded instead. Written with promises rather than
1992 // await so the same function serves selenium's callback and puppeteer's awaited expression.
1993 // String.fromCharCode is applied over slices because one call with a few hundred thousand
1994 // arguments overflows the stack
1995 function gpalEnvelope(response) {{
1997 ? response.arrayBuffer().then(function (buffer) {{
1998 var bytes = new Uint8Array(buffer), binary = '', slice = 0x8000;
1999 for (var i = 0; i < bytes.length; i += slice)
2000 binary += String.fromCharCode.apply(null, bytes.subarray(i, i + slice));
2001 return btoa(binary);
2004 ).then(function (payload) {{
2005 return JSON.stringify({{
2006 status: response.status,
2008 encoding: asBytes ? 'base64' : null,
2009 lastModified: response.headers.get('Last-Modified')
2014 // a fetch the page could not make still answers in the envelope shape, carrying the reason.
2015 // returning nothing here is what made a refused request and a bug look identical from C#
2016 function gpalFailure(error) {{
2017 return JSON.stringify({{
2022 error: String((error && error.message) || error) + ' requesting ' + target.toString()
2027 public static object ExecuteJavaScriptObj(
string executeMe, IBrowser browser, params
object[] args)
2029 dynamic retVal =
null;
2033 if (browser.UseOttoMagic)
2035 retVal = browser.MagicHelper.ExecuteJavaScript(executeMe);
2037 else if (
true == browser.UsePuppeteer)
2039 retVal = browser.PuppeteerClient.ExecuteJavaScript(executeMe).WithParameters(args).Execute<
object>();
2040 retVal = retVal.result.value;
2044 IJavaScriptExecutor js = (IJavaScriptExecutor)browser.BrowserDriver;
2045 retVal = js.ExecuteScript(executeMe, args);
2048 catch (GPALException)
2052 catch (Exception ex)
2055 bool suppressed =
true == (browser as Browser)?.BrowserSettings.SuppressJavaScriptFailures;
2057 if (
false == suppressed)
2058 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
$@"Failed to execute javascript. Continuing.", browser, GPALObjectType.Browser, ex);
2085 private static string UnwrapScriptResult(
string result)
2087 string retVal = result;
2092 if (
false ==
string.IsNullOrWhiteSpace(retVal) &&
true == retVal.TrimStart().StartsWith(
"{"))
2096 Newtonsoft.Json.Linq.JObject remote = Newtonsoft.Json.Linq.JObject.Parse(retVal);
2098 if (
null != remote[
"type"] &&
null != remote[
"value"]
2099 && remote.Properties().All(each =>
"type" == each.Name ||
"value" == each.Name ||
"description" == each.Name ||
"className" == each.Name ||
"subtype" == each.Name))
2101 Newtonsoft.Json.Linq.JToken value = remote[
"value"];
2103 retVal = Newtonsoft.Json.Linq.JTokenType.String == value.Type
2105 : Newtonsoft.Json.JsonConvert.SerializeObject(value);
2115 while (
false ==
string.IsNullOrEmpty(retVal) && retVal.StartsWith(
"\"") && retVal.EndsWith(
"\""))
2119 string peeled = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(retVal);
2121 if (peeled == retVal)
2150 public static string ExecuteJavaScript(IBrowser browser,
string executeMe, params
object[] args)
2152 string retVal = RunScript(browser, executeMe, args);
2155 if (
false == browser.UseSelenium &&
true == retVal?.Contains(
"Illegal return statement"))
2156 retVal = RunScript(browser,
"(function(){" + Environment.NewLine + executeMe + Environment.NewLine +
"})()", args);
2161 private static string RunScript(IBrowser browser,
string executeMe, params
object[] args)
2163 dynamic retVal =
null;
2167 if (browser.UseOttoMagic)
2169 retVal = browser.MagicHelper.ExecuteJavaScript(executeMe);
2171 else if (
true == browser.UsePuppeteer)
2173 if (
true == args.Any())
2174 retVal = browser.PuppeteerClient.ExecuteJavaScript(executeMe).WithParameters(args).Execute<
object>();
2176 retVal = browser.PuppeteerClient.ExecuteJavaScript(executeMe).Execute<
object>();
2178 if (
Puppeteer.resultExtractors !=
null &&
Puppeteer.resultExtractors.TryGetValue(DevToolsMethods.RuntimeEvaluate, out var extractor))
2179 retVal = extractor((Newtonsoft.Json.Linq.JObject)retVal.result);
2181 retVal = retVal.result.ToObject();
2185 IJavaScriptExecutor js = (IJavaScriptExecutor)browser.BrowserDriver;
2186 if (
true == args?.Any())
2187 retVal = js.ExecuteScript(executeMe, args);
2189 retVal = js.ExecuteScript(executeMe);
2198 catch (Exception ex)
2201 bool suppressed =
true == (browser as Browser)?.BrowserSettings.SuppressJavaScriptFailures;
2203 if (
false == suppressed)
2204 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
$@"Failed to execute javascript. Continuing.", browser, GPALObjectType.Browser, ex);
2208 return UnwrapScriptResult(retVal?.ToString());
2217 public static string GetCurrentUrl(BrowserSettings browserSettings)
2219 string currentUrl =
null;
2221 if (
true == browserSettings.UseOttoMagic)
2222 currentUrl = browserSettings.MagicHelper.GetCurrentUrl();
2223 else if (
true == browserSettings.UsePuppeteer)
2224 currentUrl = browserSettings.PuppeteerClient.GetCurrentUrl().Execute();
2226 currentUrl = browserSettings.Browser.BrowserDriver.Url;
2231 if (
true == currentUrl?.StartsWith(
"\"") &&
true == currentUrl.EndsWith(
"\""))
2232 currentUrl = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(currentUrl);
2245 string readyState =
"loading";
2247 if (lastSessionToken != sessionToken)
2249 lastErrorMessage.Clear();
2250 supressedMessage =
false;
2251 lastSessionToken = sessionToken;
2256 if (browser.UseOttoMagic)
2258 readyState = browser.BrowserSettings.MagicHelper.GetReadyStatus();
2259 if (
null == readyState)
2260 readyState =
@"loading";
2262 readyState = readyState.Trim(
'"');
2266 bool withReturn =
false == browser.UsePuppeteer;
2269 readyState =
ExecuteJavaScript(browser, $
"{(true == withReturn ? "return " : "")}document.readyState");
2276 catch (Exception ex)
2279 $
"Error executing JavaScript",
2280 null, GPALObjectType.None, ex);
2283 string currentErrorMessage = $
"Document.Ready status [{readyState}]";
2285 if (
false == lastErrorMessage.Contains(currentErrorMessage))
2287 lastErrorMessage.Add(currentErrorMessage);
2288 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage);
2289 supressedMessage =
false;
2291 else if (
false == supressedMessage)
2293 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
2294 supressedMessage =
true;
2308 public static void SeleniumGoToUrl(GPALUrl url, BrowserSettings browserSettings)
2310 string normalizedTarget = url.ToString().TrimEnd(
'/').ToLowerInvariant();
2315 browserSettings.BrowserDriver.Navigate().GoToUrl(url);
2318 System.Threading.Thread.Sleep(400);
2321 if (browserSettings.BrowserType != BrowserType.FireFox)
2324 var performanceLogs = browserSettings.BrowserDriver.Manage().Logs.GetLog(
"performance");
2326 int? bestStatus =
null;
2327 string bestMatchedUrl =
null;
2328 int candidatePriority =
int.MaxValue;
2330 foreach (var entry
in performanceLogs)
2332 string message = entry.Message;
2334 if (message.IndexOf(
"Network.responseReceived", StringComparison.Ordinal) == -1)
2338 if (message.IndexOf(
"\"url\":\"", StringComparison.Ordinal) == -1 ||
2339 message.IndexOf(normalizedTarget, StringComparison.OrdinalIgnoreCase) == -1)
2343 var statusMatch = System.Text.RegularExpressions.Regex.Match(
2345 @"""status(?:Code)?""\s*:\s*(\d+)",
2346 System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.CultureInvariant
2349 if (!statusMatch.Success)
2353 if (!
int.TryParse(statusMatch.Groups[1].Value, out status))
2356 if (status < 100 || status > 599)
2363 else if (status >= 200 && status < 300)
2365 else if (status >= 300 && status < 400)
2367 else if (status >= 400 && status < 500)
2369 else if (status >= 500)
2374 bool shouldUpdate =
false;
2376 if (!bestStatus.HasValue)
2378 shouldUpdate =
true;
2380 else if (priority < candidatePriority)
2382 shouldUpdate =
true;
2384 else if (priority == candidatePriority && status == 304)
2386 shouldUpdate =
true;
2391 bestStatus = status;
2392 candidatePriority = priority;
2395 var urlMatch = System.Text.RegularExpressions.Regex.Match(message,
@"""url"":""([^""]+)""");
2396 if (urlMatch.Success)
2398 bestMatchedUrl = urlMatch.Groups[1].Value;
2403 if (bestStatus.HasValue)
2405 browserSettings.ServerResponseCode = bestStatus.Value;
2420 browserSettings.ServerResponseCode = 0;
2422 GPALEventType.DEBUG,
2423 $
"No valid HTTP status captured in performance logs for [{url}]",
2425 GPALObjectType.Other
2434 var result = ((OpenQA.Selenium.IJavaScriptExecutor)browserSettings.BrowserDriver)
2435 .ExecuteScript(
"return performance.getEntriesByType('navigation')[0]?.responseStatus ?? 0;");
2436 browserSettings.ServerResponseCode = result !=
null &&
int.TryParse(result.ToString(), out
int ffStatus) && ffStatus > 0 ? ffStatus : 0;
2440 browserSettings.ServerResponseCode = 0;
2444 catch (GPALException)
2448 catch (Exception ex)
2450 browserSettings.ServerResponseCode = -1;
2451 GPAL.PublishSimpleEvent(
2452 GPALEventType.EXCEPTION,
2453 $
"Unexpected error navigting to [{url}]",
2455 GPALObjectType.Other,
2475 foreach (var name
in namesOfProcessesToKill)
2493 string userAgent =
null;
2497 if (
true == browser.UseOttoMagic)
2499 else if (
true == browser.UsePuppeteer)
2500 userAgent = browser.PuppeteerClient.GetUserAgent().Execute<
string>();
2508 catch (Exception ex)
2510 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get live user agent.", browser, GPALObjectType.Browser, ex);
2513 if (
true ==
string.IsNullOrEmpty(userAgent))
2514 userAgent = MagicHelper.GetUserAgentString(browser.BrowserType);
2518 if (
true == userAgent?.StartsWith(
"\"") &&
true == userAgent.EndsWith(
"\""))
2519 userAgent = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(userAgent);
2530 public static bool TestUrlForPDF(Browser browser,
string url)
2535 bool retVal =
false;
2538 if (0 <= url.IndexOf(
".pdf", 0, StringComparison.OrdinalIgnoreCase))
2541 using (HttpClient client =
new HttpClient())
2543 client.DefaultRequestHeaders.Add(
"User-Agent",
GetUserAgent(browser));
2548 using (HttpRequestMessage request =
new HttpRequestMessage(HttpMethod.Head, url))
2550 HttpResponseMessage response = client.SendAsync(request).Result;
2552 if (response.IsSuccessStatusCode)
2555 string contentType = response.Content.Headers.ContentType?.MediaType;
2556 if (contentType !=
null && contentType.Equals(
"application/pdf", StringComparison.OrdinalIgnoreCase))
2561 else if (response.Content.Headers.ContentDisposition !=
null)
2563 string fileName = response.Content.Headers.ContentDisposition.FileName;
2564 if (!
string.IsNullOrEmpty(fileName) && fileName.EndsWith(
".pdf", StringComparison.OrdinalIgnoreCase))
2572 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to verify URL [{url}] via HEAD request. Status code [{response.StatusCode}]");
2576 catch (GPALException)
2580 catch (Exception ex)
2582 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Exception during HEAD request for [{url}]",
null, GPALObjectType.None, ex);
2587 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"GPAL detected PDF file opened in browser [{url}]");
2602 internal const int PdfFetchAttempts = 3;
2603 internal const int PdfFetchWaitMs = 1500;
2612 internal static bool IsPdf(
byte[] content)
2614 return 4 < content?.Length
2615 &&
'%' == content[0] &&
'P' == content[1] &&
'D' == content[2] &&
'F' == content[3];
2617 public static bool DownloadPdfFile(
string pdfUrl,
string savePath, IBrowser browser)
2620 string filePath =
$@"{FileHelper.EnsureDirectoryEndsWithBackslash(Path.GetDirectoryName(savePath))}{System.IO.Path.GetFileName(pdfUrl)}";
2621 if (
true == File.Exists(filePath))
2623 File.Move(filePath, savePath);
2628 filePath =
$@"{FileHelper.GetDefaultDownloadDirectory(browser)}{Path.GetFileName(pdfUrl)}";
2629 if (
true == File.Exists(filePath))
2631 File.Move(filePath, savePath);
2642 for (
int attempt = 0; attempt < PdfFetchAttempts; attempt++)
2644 byte[] fetched = FetchBytes((Browser)browser, pdfUrl,
null, out
string _);
2646 if (
true == IsPdf(fetched))
2648 File.WriteAllBytes(savePath, fetched);
2649 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"PDF file [{pdfUrl}] fetched from the page to [{savePath}]");
2653 if (attempt < PdfFetchAttempts - 1)
2655 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{pdfUrl}] answered with [{fetched?.Length ?? 0}] bytes that are not a pdf, waiting for the file itself", browser, GPALObjectType.Browser);
2656 Thread.Sleep(PdfFetchWaitMs);
2657 pdfUrl =
GetCurrentUrl(((Browser)browser).BrowserSettings) ?? pdfUrl;
2660 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);
2665 using (HttpClient client =
new HttpClient())
2667 client.DefaultRequestHeaders.Add(
"User-Agent",
GetUserAgent(browser));
2672 byte[] pdfContent = client.GetByteArrayAsync(pdfUrl).Result;
2674 if (
false == IsPdf(pdfContent))
2676 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{pdfUrl}] answered with [{pdfContent?.Length ?? 0}] bytes that are not a pdf. Nothing written to [{savePath}]", browser, GPALObjectType.Browser);
2681 File.WriteAllBytes(savePath, pdfContent);
2682 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"PDF file [{pdfUrl}] downloaded successfully to [{savePath}]");
2685 catch (GPALException)
2689 catch (Exception ex)
2691 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"PDF file NOT downloaded from [{pdfUrl}]", ex);
2705 internal static IWebDriver GetWebDriver(BrowserSettings browserSettings, dynamic options)
2707 IWebDriver tmpDriver =
null;
2711 if (
true == browserSettings.HiddenDesktop)
2715 int hiddenPort = StartHiddenDriver(browserSettings);
2717 tmpDriver =
new OpenQA.Selenium.Remote.RemoteWebDriver(
new Uri($
"http://127.0.0.1:{hiddenPort}"), options);
2719 else if (BrowserType.Chrome == browserSettings.BrowserType)
2722 ChromeDriverService chromeService = ChromeDriverService.CreateDefaultService(browserSettings.DriverLocation);
2723 chromeService.HideCommandPromptWindow =
true;
2725 ChromeOptions chromeOptions = (ChromeOptions)options;
2727 tmpDriver =
new ChromeDriver(chromeService, chromeOptions);
2730 browserSettings.ServiceDriverPid = chromeService.ProcessId;
2734 EdgeDriverService edgeService = EdgeDriverService.CreateDefaultService(browserSettings.DriverLocation);
2735 edgeService.HideCommandPromptWindow =
true;
2737 EdgeOptions edgeOptions = (EdgeOptions)options;
2739 tmpDriver =
new EdgeDriver(edgeService, edgeOptions);
2742 browserSettings.ServiceDriverPid = edgeService.ProcessId;
2744 }
catch (Exception ex)
2746 string url =
string.Empty;
2747 switch (browserSettings.BrowserType)
2749 case Enums.BrowserType.Chrome:
2750 url = GPAL.GPALSettings.ChromeDriverUpdateURL;
2752 case Enums.BrowserType.Edge:
2753 url = GPAL.GPALSettings.EdgeDriverUpdateURL;
2755 case Enums.BrowserType.FireFox:
2756 url = GPAL.GPALSettings.FirefoxDriverUpdateURL;
2759 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Please manually update the driver @ [{url}]..", browserSettings.Browser, GPALObjectType.Browser, ex);
2763 #region <KillProcess>
2769 private static class DriverInfos
2771 public static DriverInfo Chrome =
new DriverInfo(
2772 executableFileName:
GPAL.GPALSettings.ChromeDriverFilename,
2773 processName:
"chromedriver",
2774 browserName:
"chrome"
2777 public static DriverInfo FireFox =
new DriverInfo(
2778 executableFileName:
GPAL.GPALSettings.FirefoxDriverFilename,
2779 processName:
"geckodriver",
2780 browserName:
"firefox"
2783 public static DriverInfo Edge =
new DriverInfo(
2784 executableFileName:
GPAL.GPALSettings.EdgeDriverFilename,
2785 processName:
"MSEdgeDriver",
2795 private class DriverInfo
2803 public DriverInfo(
string executableFileName,
string processName,
string browserName)
2805 ExecutableFileName = executableFileName;
2806 ProcessName = processName;
2807 BrowserName = browserName;
2809 public string ExecutableFileName {
get;
private set; }
2810 public string ProcessName {
get;
private set; }
2811 public string BrowserName {
get;
private set; }
2818 public string Name {
get;
set; }
2819 public int Found {
get;
set; } = 0;
2820 public int Killed {
get;
set; } = 0;
2821 public bool Success {
get;
set; } =
false;
2831 List<KillProcessesResult> result =
new List<KillProcessesResult>();
2833 alreadyUpdating =
false;
2837 result.Add(
KillProcess(DriverInfos.FireFox.ProcessName));
2838 result.Add(
KillProcess(DriverInfos.Chrome.ProcessName));
2839 result.Add(
KillProcess(DriverInfos.Edge.ProcessName));
2843 }
catch (Exception ex)
2845 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to kill processes", ex);
2855 private const int MaxParentHops = 8;
2857 internal const int GracefulExitMs = 10000;
2858 internal const int ProfileReleaseWaitMs = 15000;
2859 internal const int ProfileReleaseCheckMs = 250;
2871 private static void CloseBrowserGracefully(Browser aBrowser,
bool endItAll)
2875 if (
true == aBrowser.UsePuppeteer)
2878 if (
false == aBrowser.BrowserSettings.Process?.HasExited)
2880 aBrowser.BrowserSettings.PuppeteerCommunicator.SendCommand(DevToolsMethods.BrowserClose,
new Dictionary<string, object>(),
null).GetAwaiter().GetResult();
2881 aBrowser.PuppeteerCommunicator._readerReadyTcs.SetCanceled();
2883 if (
false == aBrowser.PuppeteerCommunicator._usePipes)
2884 aBrowser.PuppeteerCommunicator.CloseOutputWebSocketAsync().GetAwaiter();
2887 else if (
true == aBrowser.UseSelenium &&
null != aBrowser.BrowserDriver)
2893 Task closing = Task.Run(() =>
2895 if (
true == endItAll)
2896 aBrowser.BrowserDriver?.Quit();
2898 aBrowser.BrowserDriver?.Close();
2901 closing.Wait(GracefulExitMs);
2917 if (
true == aBrowser.UseOttoMagic &&
true == aBrowser.IsAlive)
2918 aBrowser.MagicHelper.CloseBrowser();
2920 if (
false == aBrowser.BrowserSettings.Process?.HasExited)
2921 aBrowser.BrowserSettings.Process?.CloseMainWindow();
2929 public static KillProcessesResult[] KillAllRunningProcesses(
bool killWebDrivers, IBrowser browser)
2931 List<KillProcessesResult> result =
new List<KillProcessesResult>();
2936 int browserCount = GPAL.Browsers.Count;
2938 if (0 == browserCount)
2950 foreach (Browser aBrowser
in GPAL.Browsers)
2952 if (aBrowser == browser ||
null == browser)
2955 bool endItAll = (aBrowser == browser &&
true == killWebDrivers) ||
null == browser;
2962 CloseBrowserGracefully(aBrowser, endItAll);
2964 if (
null != aBrowser.BrowserSettings.Process)
2967 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{aBrowser.BrowserSettings.BrowserType}] waiting on process [{aBrowser.BrowserSettings.Process.Id}] to exit", aBrowser, GPALObjectType.Browser);
2971 if (
false == aBrowser.BrowserSettings.Process.WaitForExit(GracefulExitMs))
2973 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);
2974 result.Add(
KillProcess(aBrowser.BrowserSettings.Process));
2982 if (
true == aBrowser.UseSelenium &&
true == endItAll)
2984 int pid = aBrowser.BrowserSettings.ServiceDriverPid;
2986 GPAL.PublishSimpleEvent(GPALEventType.INFO,
">>> Disposing browser driver");
2991 KillProcessTree(pid);
2997 result.Add(killProcessesResult);
3002 aBrowser.BrowserSettings.ServiceDriverPid = 0;
3006 Thread.Sleep(1_000);
3014 if (
true == aBrowser.BrowserSettings.TempProfileCreated)
3015 ChromeProfileManager.RemoveTempUserProfile(aBrowser.BrowserSettings.ProfileDataDirectory);
3019 ChromeProfileManager.SweepAbandonedTempProfiles();
3023 catch (Exception ex)
3025 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to kill processes", ex);
3057 return result.ToArray();
3075 internal static void EndWindowlessSuccessor(BrowserSettings browserSettings)
3077 int ourPid = browserSettings.Process?.Id ?? 0;
3082 WaitForBrowserToExit(browserSettings, Browser.ProfileRestoreWaitMs);
3088 using (var search =
new System.Management.ManagementObjectSearcher(
3089 $
"SELECT ProcessId, CommandLine FROM Win32_Process WHERE ParentProcessId = {ourPid}"))
3090 foreach (System.Management.ManagementObject found in search.Get())
3092 string commandLine = found[
"CommandLine"]?.ToString() ??
string.Empty;
3094 if (
true == commandLine.Contains(
"--no-startup-window"))
3096 int successor = Convert.ToInt32(found[
"ProcessId"]);
3099 $
"Ending the windowless browser [{successor}] left behind by [{ourPid}], which would hold the profile open",
3100 browserSettings, GPALObjectType.Browser);
3102 KillProcessTree(successor);
3106 catch (Exception ex)
3108 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not look for a windowless browser left behind by [{ourPid}]",
3109 browserSettings, GPALObjectType.Browser, ex);
3114 private static void KillProcessTree(
int pid)
3116 if (pid <= 0)
return;
3120 var startInfo =
new ProcessStartInfo
3122 FileName =
"taskkill",
3123 Arguments = $
"/PID {pid} /T /F",
3124 WindowStyle = ProcessWindowStyle.Hidden,
3125 CreateNoWindow =
true,
3126 UseShellExecute =
false
3129 using (Process taskkillProcess = Process.Start(startInfo))
3131 taskkillProcess?.WaitForExit(5000);
3145 if (
null != process)
3152 Name = process.ProcessName;
3158 result.Success =
true;
3163 process.WaitForExit();
3165 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Killed process [{result.Name}]");
3185 var processes = Process.GetProcessesByName(processName);
3187 result.Found = processes.Count();
3189 if (result.Found > 0)
3191 foreach (var process
in processes)
3198 process.WaitForExit();
3204 }
while (
false == process.HasExited);
3207 result.Success =
true;
3210 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Killed process [{processName}]");
3215 result.Success =
true;
3220 #endregion <KillProcess>
3222 private static Stopwatch watch =
new System.Diagnostics.Stopwatch();
3223 private static int lastX = 0, lastY = 0;
3248 private const string PortsLogName =
"ports-in-use.log";
3251 private const string NativeHostName =
"gpal.rest.api.nativeapp";
3257 private static readonly
string[] NativeHostKeys =
3259 @"Software\\Google\\Chrome\\NativeMessagingHosts\\" + NativeHostName,
3260 @"Software\\Chromium\\NativeMessagingHosts\\" + NativeHostName,
3261 @"Software\\Microsoft\\Edge\\NativeMessagingHosts\\" + NativeHostName,
3262 @"Software\\WOW6432Node\\Google\\Chrome\\NativeMessagingHosts\\" + NativeHostName,
3263 @"Software\\WOW6432Node\\Chromium\\NativeMessagingHosts\\" + NativeHostName,
3264 @"Software\\WOW6432Node\\Microsoft\\Edge\\NativeMessagingHosts\\" + NativeHostName
3274 string retVal =
null;
3276 foreach (RegistryKey root
in new[] { Registry.CurrentUser, Registry.LocalMachine })
3278 foreach (
string keyPath
in NativeHostKeys)
3281 using (RegistryKey key = root.OpenSubKey(keyPath))
3283 string manifestPath = key?.GetValue(
null) as string;
3285 if (
true ==
string.IsNullOrWhiteSpace(manifestPath) ||
false == File.Exists(manifestPath))
3290 dynamic manifest = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(manifestPath));
3291 string exePath = manifest?.path;
3293 if (
false ==
string.IsNullOrWhiteSpace(exePath) &&
true == File.Exists(exePath))
3300 catch (Exception ex)
3302 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not read the native host registration at [{keyPath}]",
null, GPALObjectType.None, ex);
3305 if (
false ==
string.IsNullOrWhiteSpace(retVal))
3327 public static int LiveRestApiHosts(out List<(
int processId,
int port)> hosts)
3329 hosts =
new List<(int processId, int port)>();
3332 string folder =
true ==
string.IsNullOrWhiteSpace(exePath) ? null : Path.GetDirectoryName(exePath);
3333 string path =
true ==
string.IsNullOrWhiteSpace(folder) ? null : Path.Combine(folder, PortsLogName);
3336 if (
true ==
string.IsNullOrWhiteSpace(path) ||
false == File.Exists(path))
3342 using (FileStream stream =
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
3343 using (StreamReader reader =
new StreamReader(stream))
3344 foreach (
string line
in reader.ReadToEnd().Split(
new[] {
'\r',
'\n' }, StringSplitOptions.RemoveEmptyEntries))
3346 string[] halves = line.Split(
':');
3348 if (2 == halves.Length
3349 &&
true ==
int.TryParse(halves[0], out
int pid)
3350 &&
true ==
int.TryParse(halves[1], out
int port)
3351 &&
false == hosts.Any(each => each.processId == pid))
3352 hosts.Add((pid, port));
3355 catch (Exception ex)
3357 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not read the host list at [{path}]",
null, GPALObjectType.None, ex);
3363 internal static bool RestApiAnswering(
string baseUrl)
3365 bool retVal =
false;
3367 if (
false ==
string.IsNullOrWhiteSpace(baseUrl))
3370 System.Net.HttpWebRequest ask = (System.Net.HttpWebRequest)System.Net.WebRequest.Create($
"{baseUrl}status");
3372 ask.ReadWriteTimeout = 1000;
3374 using (System.Net.HttpWebResponse answer = (System.Net.HttpWebResponse)ask.GetResponse())
3375 retVal = System.Net.HttpStatusCode.OK == answer.StatusCode;
3377 catch (System.Net.WebException)
3385 public static void TopBrowser(Browser browser,
bool moveDontClick,
bool forceClick =
false)
3392 bool needsTopping = (browser.CurrentUOW?.CurrentSelector?.SelectorSettings?.SelectorPaths
3393 ?.Any(path => path.SelectorPathType == SelectorPathType.Image) ??
false)
3394 || InteractionType.Hardware == browser.CurrentUOW?.CurrentSelector?.InteractionType
3395 ||
true == browser.BrowserSettings.UseHardware;
3398 if (
true == browser.BrowserSettings.UseHeadless ||
false == needsTopping)
3406 if (
true == moveDontClick ||
true == forceClick)
3408 if (
true == watch.IsRunning)
3410 if (1000 > watch.ElapsedMilliseconds)
3414 watch = System.Diagnostics.Stopwatch.StartNew();
3416 HardwareHelper.GetCursorPos(out Point currCursorPos);
3418 if (
true == browser.BrowserSettings.UseHardware ||
true == GPAL.UseHardware || Enums.BrowserType.FireFox == browser.BrowserSettings.BrowserType ||
true == moveDontClick ||
true == forceClick)
3422 Rectangle rect = ElementHelper.GetWindowRectangle(browser);
3425 Rectangle outerRect = ElementHelper.GetWindowOuterRectangle(browser);
3427 if (
true == moveDontClick)
3432 var rnd =
new Random();
3433 switch (rnd.Next(4))
3435 case 0: x = rect.Left + rnd.Next(-50, rect.Width + 50); y = outerRect.Top - 20;
break;
3436 case 1: x = rect.Left + rnd.Next(-50, rect.Width + 50); y = rect.Bottom + 50;
break;
3437 case 2: x = rect.Left - 60; y = rect.Top + rnd.Next(0, rect.Height);
break;
3438 default: x = rect.Right + 60; y = rect.Top + rnd.Next(0, rect.Height);
break;
3443 Rectangle workingArea = Screen.PrimaryScreen.WorkingArea;
3444 x = Math.Min(Math.Max(workingArea.Left, x), workingArea.Right - 1);
3445 y = Math.Min(Math.Max(workingArea.Top, y), workingArea.Bottom - 1);
3452 x = outerRect.Left + 3;
3453 y = outerRect.Top + 3;
3460 catch (GPALException)
3464 catch (Exception ex)
3466 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get browser coords. Retrying.", browser, GPALObjectType.Browser, ex);
3471 if (
true == GPAL.SimulateMouse ||
true == browser.CurrentUOW.CurrentSelector?.SimulateMouse)
3472 HardwareHelper.MoveMouse(x, y, 10, 10);
3474 HardwareHelper.MoveMouse(x, y);
3476 if (
false == moveDontClick)
3478 HardwareHelper.HardwareClick(x, y, ClickType.LeftClick);
3481 if (
true == GPAL.SimulateMouse ||
true == browser.CurrentUOW.CurrentSelector?.SimulateMouse)
3482 HardwareHelper.MoveMouse(currCursorPos.X, currCursorPos.Y, 10, 10);
3484 HardwareHelper.MoveMouse(currCursorPos.X, currCursorPos.Y);
3487 else if (
true == browser.UseSelenium)
3495 browser.BrowserDriver.SwitchTo().Window(browser.BrowserDriver.CurrentWindowHandle);
3497 catch (GPALException)
3501 catch (Exception ex)
3503 string tmpStr =
"Faling back to windows topping.";
3504 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to top browser using Selenium. {(false == GPAL.NoFallbackRecoveryActions ? tmpStr : String.Empty)})", browser, GPALObjectType.Browser, ex);
3505 if (
false == GPAL.NoFallbackRecoveryActions)
3506 WindowHelper.TopBrowser(browser.BrowserSettings.Process, browser.BrowserSettings.ServiceDriverPid);
3512 if (
false == WindowHelper.TopBrowser(browser.BrowserSettings.Process, browser.BrowserSettings.ServiceDriverPid))
3513 throw new GPALException(
"Browser has exited");
3543 internal static bool CameFromProcess(
int processId,
int ancestorId)
3545 bool retVal =
false;
3546 int walking = processId;
3548 for (
int hop = 0; hop < MaxParentHops && 0 != walking &&
false == retVal; hop++)
3550 if (walking == ancestorId)
3553 walking = ParentOf(walking);
3565 private static int ParentOf(
int processId)
3571 using (var searcher =
new System.Management.ManagementObjectSearcher(
3572 $
"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}"))
3573 foreach (System.Management.ManagementObject each in searcher.Get())
3575 retVal = Convert.ToInt32(each[
"ParentProcessId"]);
3577 catch (Exception ex)
3579 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not ask who started process [{processId}]",
null, GPALObjectType.Browser, ex);
3594 internal static void RefuseIfProfileIsNotARoot(BrowserSettings browserSettings)
3596 string given = browserSettings.ProfileDataDirectory;
3598 if (
true ==
string.IsNullOrEmpty(given) || BrowserType.FireFox == browserSettings.BrowserType)
return;
3601 if (
false == File.Exists(Path.Combine(given,
"Preferences")))
return;
3603 string message = $
"[{given}] is a browser profile, not a user data directory. Chromium would build a new empty profile inside it and run that instead. Give the directory that holds it and name the profile, or copy it out to a directory of its own";
3605 GPAL.PublishSimpleEvent(GPALEventType.ERROR, message,
null, GPALObjectType.Browser);
3607 throw new GPALException($
"{GPAL.MyMethodName()}: {message}");
3610 internal static void RefuseIfProfileIsOpen(BrowserSettings browserSettings)
3612 RefuseIfProfileIsNotARoot(browserSettings);
3614 string profile = browserSettings.ProfileDataDirectory;
3616 if (
false ==
string.IsNullOrEmpty(profile))
3618 string wanted = NormalizedDirectory(profile);
3619 bool isFirefox = BrowserType.FireFox == browserSettings.BrowserType;
3621 string processName =
true == isFirefox ?
"firefox.exe"
3622 : BrowserType.Edge == browserSettings.BrowserType ?
"msedge.exe"
3626 string profileArgument =
true == isFirefox
3627 ?
"-profile\\s+(\"[^\"]*\"|[^\\s]*)"
3628 :
"--user-data-dir=(\"[^\"]*\"|[^\\s]*)";
3633 string defaultRoot =
true == isFirefox
3637 holder = ProfileHolder(profile, processName, wanted, profileArgument, defaultRoot);
3644 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Profile [{profile}] is still held by [{processName}] process [{holder}], waiting up to [{ProfileReleaseWaitMs / 1000}] seconds for it to be let go",
null, GPALObjectType.Browser);
3646 Stopwatch waited = Stopwatch.StartNew();
3648 while (0 != holder && ProfileReleaseWaitMs > waited.ElapsedMilliseconds)
3650 System.Threading.Thread.Sleep(ProfileReleaseCheckMs);
3652 holder = ProfileHolder(profile, processName, wanted, profileArgument, defaultRoot);
3656 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Profile [{profile}] was let go after [{waited.ElapsedMilliseconds}] ms",
null, GPALObjectType.Browser);
3661 string message = $
"Profile [{profile}] is already open in [{processName}] process [{holder}]. Close it, or give this run a profile of its own";
3663 GPAL.PublishSimpleEvent(GPALEventType.ERROR, message,
null, GPALObjectType.Browser);
3665 throw new GPALException($
"{GPAL.MyMethodName()}: {message}");
3680 private static int ProfileHolder(
string profile,
string processName,
string wanted,
string profileArgument,
string defaultRoot)
3686 using (var search =
new System.Management.ManagementObjectSearcher(
3687 $
"SELECT ProcessId, CommandLine FROM Win32_Process WHERE Name = '{processName}'"))
3689 foreach (System.Management.ManagementObject found in search.Get())
3691 string commandLine = found[
"CommandLine"]?.ToString();
3693 if (
true ==
string.IsNullOrEmpty(commandLine))
continue;
3698 if (
true == commandLine.Contains(
"--type="))
continue;
3700 Match match = Regex.Match(commandLine, profileArgument);
3703 string holds =
true == match.Success
3704 ? NormalizedDirectory(match.Groups[1].Value.Trim(
'"'))
3707 if (
true ==
string.IsNullOrEmpty(holds))
continue;
3709 if (
true == wanted.Equals(holds, StringComparison.OrdinalIgnoreCase))
3711 retVal = Convert.ToInt32(found[
"ProcessId"]);
3717 catch (Exception ex)
3720 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not check whether [{profile}] is already open",
null, GPALObjectType.Browser, ex);
3727 private static string NormalizedDirectory(
string path)
3729 string retVal = path;
3733 retVal = Path.GetFullPath(path).TrimEnd(
'\\',
'/');
3737 retVal = path.TrimEnd(
'\\',
'/');
3745 private static readonly
string[] browserOwnedHeaders =
3747 "referer",
"user-agent",
"host",
"origin",
"cookie",
"content-length",
3748 "connection",
"accept-encoding",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
3749 "sec-ch-ua-arch",
"sec-ch-ua-model",
"sec-ch-ua-full-version-list",
"sec-ch-device-memory",
3750 "sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site"
3758 internal static bool BrowserOwnedHeader(
string name)
3760 return Array.Exists(browserOwnedHeaders, owned => owned.Equals(name, StringComparison.OrdinalIgnoreCase));
3768 private static readonly HashSet<int> portsHandedOut =
new HashSet<int>();
3771 private static int lastPortHandedOut = 0;
3781 internal static int StartHiddenDriver(BrowserSettings browserSettings)
3783 int retVal = FindFreePort();
3785 string portArgument;
3787 switch (browserSettings.BrowserType)
3789 case BrowserType.FireFox:
3790 driverName =
"geckodriver.exe";
3791 portArgument = $
"--port {retVal}";
3793 case BrowserType.Edge:
3794 driverName =
"msedgedriver.exe";
3795 portArgument = $
"--port={retVal}";
3798 driverName =
"chromedriver.exe";
3799 portArgument = $
"--port={retVal}";
3803 string driverPath = Path.Combine(browserSettings.DriverLocation ?? AppDomain.CurrentDomain.BaseDirectory, driverName);
3807 if (
false == File.Exists(driverPath))
3808 throw new GPALException($
"No [{driverName}] in [{Path.GetDirectoryName(driverPath)}], which a hidden run needs because Selenium Manager cannot fetch one here");
3810 Native.STARTUPINFO startup =
new Native.STARTUPINFO
3812 cb = (uint)Marshal.SizeOf<Native.STARTUPINFO>(),
3813 lpDesktop = HiddenDesktop.StartupNameFor(browserSettings.HiddenDesktopName)
3816 if (
false == Native.CreateProcess(
null,
$@"""{driverPath}"" {portArgument}", IntPtr.Zero, IntPtr.Zero,
false,
3817 Native.CREATE_NO_WINDOW, IntPtr.Zero,
null, ref startup, out var started))
3818 throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), $
"Could not start [{driverName}] on [{browserSettings.HiddenDesktopName}]");
3820 Native.CloseHandle(started.hProcess);
3821 Native.CloseHandle(started.hThread);
3825 browserSettings.ServiceDriverPid = (int)started.dwProcessId;
3827 if (
false == WaitForPort(retVal, 30_000))
3828 throw new GPALException($
"[{driverName}] never answered on port [{retVal}]");
3839 private static bool WaitForPort(
int port,
int timeoutInMs)
3841 bool retVal =
false;
3842 DateTime giveUpAt = DateTime.Now.AddMilliseconds(timeoutInMs);
3844 while (
false == retVal && DateTime.Now < giveUpAt)
3848 using (TcpClient probe =
new TcpClient())
3850 probe.Connect(
"127.0.0.1", port);
3854 catch (SocketException)
3869 internal static int FindFreePort(
int startPort = 0xdead)
3873 lock (portsHandedOut)
3876 int from = Math.Max(startPort, lastPortHandedOut + 1);
3878 for (
int port = from; port <= 65535 && 0 == retVal; port++)
3880 if (
true == portsHandedOut.Contains(port))
3885 TcpListener probe =
null;
3889 probe =
new TcpListener(System.Net.IPAddress.Loopback, port);
3893 portsHandedOut.Add(port);
3894 lastPortHandedOut = port;
3897 catch (SocketException)
3909 throw new GPALException($
"No free ports available between [{startPort}] and [65535].");
3932 internal static string DocumentStamp(Browser browser)
3936 string stamp =
true == browser.UseOttoMagic
3937 ? browser.MagicHelper.GetDocumentStamp()
3938 : ExecuteJavaScriptObj(
"return performance.timeOrigin + '|' + location.href;", browser)?.ToString();
3944 private static string OriginFromStamp(
string stamp)
3946 int bar = stamp?.IndexOf(
'|') ?? -1;
3947 string origin = 0 <= bar ? stamp.Substring(0, bar) :
null;
3952 private static string UrlFromStamp(
string stamp)
3954 int bar = stamp?.IndexOf(
'|') ?? -1;
3955 string url = 0 <= bar ? stamp.Substring(bar + 1) :
null;
3960 internal static object MarkDocument(Browser browser)
3967 if (
true == browser.UseSelenium)
3968 mark = browser.BrowserDriver.FindElement(By.TagName(
"html"));
3969 else if (
true == browser.UsePuppeteer ||
true == browser.UseOttoMagic)
3970 mark = DocumentStamp(browser);
3983 internal static bool DocumentReplaced(Browser browser,
object mark)
3985 bool replaced =
false;
3987 if (
true == browser.UseSelenium && mark is IWebElement html)
3988 try { _ = html.TagName; }
3989 catch (StaleElementReferenceException) { replaced =
true; }
3990 catch (NoSuchElementException) { replaced =
true; }
3991 else if (mark is
string priorStamp)
3993 string nowOrigin = OriginFromStamp(DocumentStamp(browser));
3994 string wasOrigin = OriginFromStamp(priorStamp);
3996 replaced =
false ==
string.IsNullOrEmpty(nowOrigin)
3997 &&
false ==
string.IsNullOrEmpty(wasOrigin)
3998 && wasOrigin != nowOrigin;
4016 internal static bool WaitForNavigationOrTime(Browser browser,
string priorUrl,
object documentMark,
int timeOutAfterMs)
4018 bool navigated =
false;
4019 bool documentGone =
false;
4020 bool urlChanged =
false;
4021 string lastUrl = priorUrl;
4022 DateTime timeout = DateTime.Now.AddMilliseconds(timeOutAfterMs);
4023 GPALEventType publishToConsole = GPAL.GPALSettings.ConsoleEvents;
4024 GPALEventType publishToDebug = GPAL.GPALSettings.DebugEvents;
4026 GPAL.GPALSettings.ConsoleEvents = GPALEventType.NONE;
4027 GPAL.GPALSettings.DebugEvents = GPALEventType.NONE;
4031 if (
true == browser.IsAlive)
4036 if (documentMark is
string priorStamp)
4038 string nowStamp = DocumentStamp(browser);
4039 string nowOrigin = OriginFromStamp(nowStamp);
4040 string wasOrigin = OriginFromStamp(priorStamp);
4044 documentGone =
false ==
string.IsNullOrEmpty(nowOrigin)
4045 &&
false ==
string.IsNullOrEmpty(wasOrigin)
4046 && wasOrigin != nowOrigin;
4048 lastUrl = UrlFromStamp(nowStamp) ?? lastUrl;
4052 documentGone = DocumentReplaced(browser, documentMark);
4053 lastUrl = browser.GetSetCurrentUrl();
4056 urlChanged =
false == UrlHelper.AreEquivalent(priorUrl, lastUrl);
4058 navigated =
true == documentGone ||
true == urlChanged;
4061 if (
false == navigated && DateTime.Now < timeout)
4062 Thread.Sleep(NavigationPollMs);
4064 while (
false == navigated && DateTime.Now < timeout);
4068 GPAL.GPALSettings.ConsoleEvents = publishToConsole;
4069 GPAL.GPALSettings.DebugEvents = publishToDebug;
4074 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
4075 $
"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}]",
4076 browser, GPALObjectType.Browser);
4092 internal const int WaitForValuePollMs = 250;
4094 internal static void WaitForValueOrTime(Browser browser,
string resultsExpected, Func<dynamic> action,
int timeOutAfterMs)
4096 DateTime timeout = DateTime.Now.AddMilliseconds(timeOutAfterMs);
4097 string taskReturnValue;
4101 string[] tokens = resultsExpected.Split(
',').Select(token => token.Trim()).ToArray();
4103 if (
true == browser.IsAlive)
4106 dynamic result = action();
4111 var task = (Task<string>)result;
4112 task.GetAwaiter().GetResult();
4115 var taskType = task.GetType();
4116 if (taskType.IsGenericType)
4118 var taskResult = taskType.GetProperty(
"Result")?.GetValue(task);
4119 taskReturnValue = taskResult?.ToString() ?? resultsExpected;
4123 taskReturnValue = tokens[0];
4129 taskReturnValue = result?.ToString() ?? resultsExpected;
4132 foreach (
string token
in tokens)
4133 if (
true == taskReturnValue.ToLower().Contains(token))
4139 Thread.Sleep(WaitForValuePollMs);
4141 while (DateTime.Now < timeout);
4144 #region NetworkHelpers
4155 int timeoutMs = browser.BrowserSettings.NetworkIdleTimeoutMs;
4156 int maxConnections = browser.BrowserSettings.NetworkIdleMaxConnections;
4157 int pruneMs = browser.BrowserSettings.NetworkIdlePruneMs;
4159 DateTime timeout = DateTime.Now.AddMilliseconds(timeoutMs);
4160 DateTime lastChange = DateTime.Now;
4162 if (lastSessionToken != sessionToken)
4164 lastErrorMessage.Clear();
4165 supressedMessage =
false;
4166 lastSessionToken = sessionToken;
4171 string currentErrorMessage = $
"Waiting up to [{timeoutMs}] ms for network to idle to [{maxConnections}] connections for [{pruneMs}] ms";
4173 if (
false == lastErrorMessage.Contains(currentErrorMessage))
4175 lastErrorMessage.Add(currentErrorMessage);
4176 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage, browser, GPALObjectType.Browser);
4177 supressedMessage =
false;
4179 else if (
false == supressedMessage)
4181 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
4182 supressedMessage =
true;
4185 while (DateTime.Now < timeout)
4188 int inflight = Convert.ToInt32(BrowserHelper.ExecuteJavaScriptObj(
4189 "return window.__gpalNetworkMonitor ? window.__gpalNetworkMonitor.inflight : 0;",
4193 if (inflight != lastCount)
4195 lastCount = inflight;
4196 lastChange = DateTime.Now;
4200 if (inflight == 0 && (DateTime.Now - lastChange).TotalMilliseconds >= pruneMs)
4202 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Network IS Idle. Status [true]");
4209 currentErrorMessage = $
"Network IS NOT Idle. Status [false]";
4211 if (
false == lastErrorMessage.Contains(currentErrorMessage))
4213 lastErrorMessage.Add(currentErrorMessage);
4214 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage, browser, GPALObjectType.Browser);
4215 supressedMessage =
false;
4217 else if (
false == supressedMessage)
4219 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
4220 supressedMessage =
true;
4319 int timeoutMs = 500,
4320 int maxConnections = 0,
4322 string sessionToken =
null)
4324 Exception innerEx =
null;
4327 if (lastSessionToken != sessionToken)
4329 lastErrorMessage.Clear();
4330 supressedMessage =
false;
4331 lastSessionToken = sessionToken;
4334 string currentErrorMessage = $
"Waiting up to [{timeoutMs}] ms for network to idle to [{maxConnections}] connections for [{pruneMs}] ms";
4336 if (
false == lastErrorMessage.Contains(currentErrorMessage))
4338 lastErrorMessage.Add(currentErrorMessage);
4340 supressedMessage =
false;
4342 else if (
false == supressedMessage)
4344 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
4345 supressedMessage =
true;
4348 DateTime startTime = DateTime.Now;
4349 while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
4351 string response = browser.
OttoMagicClient.WithEndpoint(ApiEndpoint.CheckNetworkIdle).WithMaxConnections(maxConnections).WithPruneMs(pruneMs).WithTimeoutMs(timeoutMs).Execute();
4353 if (
false ==
string.IsNullOrEmpty(response))
4355 bool retVal = response.Contains(
"idle");
4357 currentErrorMessage = $
"Network IS{(retVal ? "" : " NOT
")} Idle. Status [{retVal}]";
4359 if (
false == lastErrorMessage.Contains(currentErrorMessage))
4361 lastErrorMessage.Add(currentErrorMessage);
4363 supressedMessage =
false;