GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
MagicHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.ComponentModel;
20using System.Diagnostics;
21using System.Drawing;
22using System.IO;
23using System.IO.Pipes;
24using System.Linq;
25using System.Management;
26using System.Net;
27using System.Net.Sockets;
28using System.Runtime.InteropServices;
29using System.Security.Policy;
30using System.Text;
31using System.Threading;
32using System.Threading.Tasks;
33using DocumentFormat.OpenXml.Office2010.PowerPoint;
35using Microsoft.Win32.SafeHandles;
36using OpenQA.Selenium;
37using OpenQA.Selenium.DevTools;
39using static GenerallyPositive.Enums;
40
42{
43 public static class Native
44 {
45 // Constants
46 public const uint DUPLICATE_SAME_ACCESS = 0x00000002;
47 public const uint HANDLE_FLAG_INHERIT = 0x00000001;
48 public const uint STARTF_USESTDHANDLES = 0x00000100;
49 public const uint STARTF_USESHOWWINDOW = 0x00000001;
50 public const uint CREATE_NO_WINDOW = 0x08000000;
51 public const uint STILL_ACTIVE = 259;
52 public const ushort SW_HIDE = 0;
53 public const ushort SW_SHOWNORMAL = 1;
54
55 // Proc/thread attribute constant (if using STARTUPINFOEX)
56 public const int PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002;
57
58 // SECURITY_ATTRIBUTES
59 [StructLayout(LayoutKind.Sequential)]
60 public struct SECURITY_ATTRIBUTES
61 {
62 public int nLength;
63 public IntPtr lpSecurityDescriptor;
64 public int bInheritHandle; // BOOL as int
65 }
66
67 // STARTUPINFO (standard)
68 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
69 public struct STARTUPINFO
70 {
71 public uint cb;
72 public string lpReserved;
73 public string lpDesktop;
74 public string lpTitle;
75 public uint dwX;
76 public uint dwY;
77 public uint dwXSize;
78 public uint dwYSize;
79 public uint dwXCountChars;
80 public uint dwYCountChars;
81 public uint dwFillAttribute;
82 public uint dwFlags;
83 public ushort wShowWindow;
84 public ushort cbReserved2;
85 public IntPtr lpReserved2;
86 public IntPtr hStdInput;
87 public IntPtr hStdOutput;
88 public IntPtr hStdError;
89 }
90
91 // Extended STARTUPINFO for attribute list
92 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
93 public struct STARTUPINFOEX
94 {
95 public STARTUPINFO StartupInfo;
96 public IntPtr lpAttributeList;
97 }
98
99 // PROCESS_INFORMATION
100 [StructLayout(LayoutKind.Sequential)]
102 {
103 public IntPtr hProcess;
104 public IntPtr hThread;
105 public uint dwProcessId;
106 public uint dwThreadId;
107 }
108
109 // P/Invokes
110
111 [DllImport("kernel32.dll", SetLastError = true)]
112 public static extern bool CreatePipe(
113 out IntPtr hReadPipe,
114 out IntPtr hWritePipe,
115 ref SECURITY_ATTRIBUTES lpPipeAttributes,
116 uint nSize);
117
118 [DllImport("kernel32.dll", SetLastError = true)]
119 public static extern bool SetHandleInformation(
120 IntPtr hObject,
121 uint dwMask,
122 uint dwFlags);
123
124 [DllImport("kernel32.dll", SetLastError = true)]
125 public static extern bool GetHandleInformation(
126 IntPtr hObject,
127 out uint lpdwFlags);
128
129 [DllImport("kernel32.dll", SetLastError = true)]
130 public static extern bool DuplicateHandle(
131 IntPtr hSourceProcessHandle,
132 IntPtr hSourceHandle,
133 IntPtr hTargetProcessHandle,
134 out IntPtr lpTargetHandle,
135 uint dwDesiredAccess,
136 bool bInheritHandle,
137 uint dwOptions);
138
139 [DllImport("kernel32.dll")]
140 public static extern IntPtr GetCurrentProcess();
141
142 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
143 public static extern bool CreateProcess(
144 string lpApplicationName,
145 string lpCommandLine,
146 IntPtr lpProcessAttributes,
147 IntPtr lpThreadAttributes,
148 bool bInheritHandles,
149 uint dwCreationFlags,
150 IntPtr lpEnvironment,
151 string lpCurrentDirectory,
152 ref STARTUPINFO lpStartupInfo,
153 out PROCESS_INFORMATION lpProcessInformation);
154
155 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
156 public static extern bool CreateProcess(
157 string lpApplicationName,
158 string lpCommandLine,
159 IntPtr lpProcessAttributes,
160 IntPtr lpThreadAttributes,
161 bool bInheritHandles,
162 uint dwCreationFlags,
163 IntPtr lpEnvironment,
164 string lpCurrentDirectory,
165 ref STARTUPINFOEX lpStartupInfoEx,
166 out PROCESS_INFORMATION lpProcessInformation);
167
168 [DllImport("kernel32.dll", SetLastError = true)]
169 public static extern bool InitializeProcThreadAttributeList(
170 IntPtr lpAttributeList,
171 int dwAttributeCount,
172 int dwFlags,
173 ref IntPtr lpSize);
174
175 [DllImport("kernel32.dll", SetLastError = true)]
176 public static extern bool UpdateProcThreadAttribute(
177 IntPtr lpAttributeList,
178 uint dwFlags,
179 IntPtr attribute,
180 IntPtr lpValue,
181 IntPtr cbSize,
182 IntPtr lpPreviousValue,
183 IntPtr lpReturnSize);
184
185 [DllImport("kernel32.dll")]
186 public static extern void DeleteProcThreadAttributeList(
187 IntPtr lpAttributeList);
188
189 [DllImport("kernel32.dll", SetLastError = true)]
190 public static extern bool PeekNamedPipe(
191 IntPtr hNamedPipe,
192 IntPtr lpBuffer,
193 uint nBufferSize,
194 IntPtr lpBytesRead,
195 out uint lpTotalBytesAvail,
196 IntPtr lpBytesLeftThisMessage);
197
198 [DllImport("kernel32.dll", SetLastError = true)]
199 public static extern bool GetExitCodeProcess(
200 IntPtr hProcess,
201 out uint lpExitCode);
202
203 // Added: TerminateProcess to allow force-killing a process handle
204 [DllImport("kernel32.dll", SetLastError = true)]
205 public static extern bool TerminateProcess(
206 IntPtr hProcess,
207 uint uExitCode);
208
209 [DllImport("kernel32.dll", SetLastError = true)]
210 public static extern bool CloseHandle(
211 IntPtr hObject);
212
213 [DllImport("kernel32.dll", SetLastError = true)]
214 public static extern IntPtr GetStdHandle(int nStdHandle);
215 }
216 public class WindowInfo
217 {
218 public string URL { get; set; }
219 public bool ActiveWindow { get; set; }
220 }
221 public class WindowTuple
222 {
223 public string WindowId { get; set; }
224 public string Url { get; set; }
225
226 public WindowTuple(string windowId, string url)
227 {
228 WindowId = windowId;
229 Url = url;
230 }
231
232 public WindowTuple()
233 { }
234 }
235
236 public class TabTuple
237 {
238 public int TabId { get; set; }
239 public string Url { get; set; }
240
241 public TabTuple(int TabId, string Url)
242 {
243 this.TabId = TabId;
244 this.Url = Url;
245 }
246 public TabTuple()
247 { }
248 }
249
250 public class MagicHelper : IMagicHelper
251 {
252 internal MagicHelper(Browser browser)
253 {
254 this.browser = browser;
255 }
256
257 // GPALRestAPI announces its port and process id to a fixed udp port, and only one listener can bind that
258 // port at a time. This gate holds a launch from starting until the one before it has its port.
259 // Machine wide by name, because two GPAL applications race each other exactly the way two browsers do.
260 private static readonly Mutex ottoHandshakeGate = new Mutex(false, "GPAL.OttoMagic.PortHandshake");
261 private const int OttoHandshakeGateTimeoutMs = 60_000;
262
263 List<string> lastErrorMessage = new List<string>();
264 bool supressedMessage = false;
265 string lastSessionToken;
266 IBrowser browser { get; set; } = null;
267
268 private IRESTClient _client;
269 private string _clientApiBase;
270
278 internal IRESTClient Client
279 {
280 get
281 {
282 if (null == _client)
283 _client = (IRESTClient)GPAL.RESTClient;
284
285 string url = ((Browser)browser)?.BrowserSettings.RestApiBaseUrl;
286
287 // re-pointed rather than set once: a browser that relaunches lands on a new port, and the
288 // client the workflow is holding has to follow it
289 if (false == string.IsNullOrEmpty(url) && url != _clientApiBase)
290 {
291 _client.WithAPIBase(url);
292 _clientApiBase = url;
293 }
294
295 return _client;
296 }
297 }
298
299 #region Commands
304 public void Back()
305 {
306 Client.Back().Execute();
307 }
308
309 public string CaptureVisibleTab()
310 {
311 return Client.CaptureVisibleTab().Execute();
312 }
325 public string CheckNetworkIdle(int? maxConnections = null, int? timeoutMs = null, int? pruneMs = null)
326 {
327 BrowserSettings settings = ((Browser)browser)?.BrowserSettings;
328
329 // CheckNetworkIdle(int) returns an interface that does not expose the other two parameters, so the
330 // endpoint is set directly
331 string retVal = Client
332 .WithEndpoint(ApiEndpoint.CheckNetworkIdle)
333 .WithMaxConnections(maxConnections ?? settings?.NetworkIdleMaxConnections ?? 0)
334 .WithTimeoutMs(timeoutMs ?? settings?.NetworkIdleTimeoutMs ?? 30_000)
335 .WithPruneMs(pruneMs ?? settings?.NetworkIdlePruneMs ?? 3_000)
336 .Execute();
337
338 return retVal;
339 }
340
347 public TabTuple CloseTab(dynamic URLorTabId = null)
348 {
349 if (null == URLorTabId)
350 return Client.CloseTab().Execute<TabTuple>();
351 else if (URLorTabId is string v)
352 return Client.CloseTab(v).Execute<TabTuple>();
353 else if (URLorTabId is GPALUrl u)
354 return Client.CloseTab(u).Execute<TabTuple>();
355 else if (URLorTabId is int i)
356 return Client.CloseTab(URLorTabId).Execute<TabTuple>();
357 else
358 return Client.CloseTab().Execute<TabTuple>();
359
360 }
361
372 public bool DeleteStorage(WebsiteStorageType storageType, bool deleteAcrossOrigins = false, string domain = null, string path = null, string key = null, string storeName = null)
373 {
374 IAllowRESTStorageOptions fluentObject = null;
375
376 fluentObject = Client.DeleteStorage(storageType);
377
378 if (false == string.IsNullOrEmpty(domain))
379 fluentObject.WithStorageDomain(domain);
380
381 if (false == string.IsNullOrEmpty(path))
382 fluentObject.WithStoragePath(path);
383
384 if (false == string.IsNullOrEmpty(key))
385 fluentObject.WithStorageKey(key);
386
387 if (false == string.IsNullOrEmpty(storeName))
388 fluentObject.WithStorageStoreName(storeName);
389
390 // delete-storage takes storageType, domain, path, key and storeName. no domain is what deletes
391 // across origins, so there is no parameter to send for it and sending one had every delete
392 // refused before it went out
393 string deleteResult = fluentObject.Execute();
394
395 return null != deleteResult;
396 }
397
406 public string GetStorage(WebsiteStorageType storageType, string domain = null, string path = null, string key = null, string storeName = null)
407 {
408 IAllowRESTStorageOptions fluentObject = null;
409
410 fluentObject = Client.GetStorage(storageType);
411
412 if (false == string.IsNullOrEmpty(domain))
413 fluentObject.WithStorageDomain(domain);
414
415 if (false == string.IsNullOrEmpty(path))
416 fluentObject.WithStoragePath(path);
417
418 if (false == string.IsNullOrEmpty(key))
419 fluentObject.WithStorageKey(key);
420
421 if (false == string.IsNullOrEmpty(storeName))
422 fluentObject.WithStorageStoreName(storeName);
423
424 return fluentObject.Execute();
425 }
426
427 public bool SetStorage(WebsiteStorageType storageType, string data, string domain = null, string path = null, string key = null, string storeName = null)
428 {
429
430 IAllowRESTStorageOptions fluentObject = null;
431
432 fluentObject = Client.SetStorage(storageType);
433
434 if (false == string.IsNullOrEmpty(domain))
435 fluentObject.WithStorageDomain(domain);
436
437 if (false == string.IsNullOrEmpty(path))
438 fluentObject.WithStoragePath(path);
439
440 if (false == string.IsNullOrEmpty(key))
441 fluentObject.WithStorageKey(key);
442
443 if (false == string.IsNullOrEmpty(storeName))
444 fluentObject.WithStorageStoreName(storeName);
445
446 if (false == string.IsNullOrEmpty(data))
447 fluentObject.WithData(data);
448
449 return fluentObject.Execute<bool>();
450 }
451
456 public void Forward()
457 {
458 Client.Forward().Execute();
459 }
460
465 public void FullScreen()
466 {
467 Client.FullScreen().Execute();
468 }
469
476 public string Get(GPALUrl URL)
477 {
478 bool areRobotsAllowed = true;
479
480 URL.ForUrl(GetFullUrl(URL?.Url, browser, out areRobotsAllowed));
481
482 if (null != browser)
483 ((Browser)browser)._areRobotsAllowed = areRobotsAllowed;
484
485 var result = Client.GoTo(URL?.Url).Execute();
486 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
487
488 return result;
489 }
490
497 public string GetContentAndCss(string elementId)
498 {
499 string retVal = Client.GetContentAndCss(elementId).Execute();
500
501 // the extension answers with a json string, quotes and all, and escapes inside it. left as it was, the
502 // quotes printed as a line of their own above and below the page, and every \" inside an attribute
503 // became part of its value
504 if (true == retVal?.StartsWith("\"") && true == retVal.EndsWith("\"") && 1 < retVal.Length)
505 retVal = Newtonsoft.Json.JsonConvert.DeserializeObject<string>(retVal);
506
507 return retVal;
508 }
509 public Dictionary<string, object> GetCssAttributes(string elementId)
510 {
511 return Client.GetCssAttributes(elementId).Execute<Dictionary<string, object>>();
512 }
513 public Dictionary<string, object> GetDomAttributes(string elementId)
514 {
515 return Client.GetDomAttributes(elementId).Execute<Dictionary<string, object>>();
516 }
517 public Dictionary<string, object> GetDomProperties(string elementId)
518 {
519 return Client.GetDomProperties(elementId).Execute< Dictionary<string, object>>();
520 }
521
527 public string GetCurrentUrl()
528 {
529 return Client.GetCurrentUrl().Execute();
530 }
531
538 public string GetReadyStatus()
539 {
540 return GetReadyStatus(null);
541 }
542
549 internal string GetReadyStatus(string sessionToken)
550 {
551 string retVal = null;
552
553 if (lastSessionToken != sessionToken)
554 {
555 lastErrorMessage.Clear();
556 supressedMessage = false;
557 lastSessionToken = sessionToken;
558 }
559
560 dynamic obj = Client.GetReadyStatus().Execute();
561 if ("null" != obj && "NOTOK" != obj)
562 retVal = obj ?? @"""loading""";
563 else
564 retVal = "complete";
565
566 string currentErrorMessage = $"Document.Ready status [{retVal}]";
567
568 if (false == lastErrorMessage.Contains(currentErrorMessage))
569 {
570 lastErrorMessage.Add(currentErrorMessage);
571 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage, null, GPALObjectType.None);
572 supressedMessage = false;
573 }
574 else if (false == supressedMessage)
575 {
576 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
577 supressedMessage = true;
578 }
579
580 return retVal;
581 }
582
589 public GPALElement GetShadowRoot(string elementId)
590 {
591 return Client.GetShadowRoot(elementId).Execute<GPALElement>();
592 }
593
601 {
602 if (null == URL)
603 URL = new GPALUrl("google.com");
604
605 bool areRobotsAllowed = true;
606
607 URL.ForUrl(GetFullUrl(URL.Url, browser, out areRobotsAllowed));
608
609 if (null != browser)
610 ((Browser)browser)._areRobotsAllowed = (bool) areRobotsAllowed;
611
612 TabTuple retVal = Client.GoTo(URL.Url).Execute<TabTuple>();
613 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
614
615 return retVal;
616 }
617
624 public TabTuple GotoTab(dynamic URLorTabId)
625 {
626 if (URLorTabId is GPALUrl v)
627 {
628 TabTuple tt = Client.GoToTab(v).Execute<TabTuple>();
629 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
630 return tt;
631 }
632 else if (URLorTabId is TabTuple tuple)
633 {
634 TabTuple tt = Client.GoToTab(tuple.TabId).Execute<TabTuple>();
635 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
636 return tt;
637 }
638 else
639 {
640 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"URLorTabId is unknown type [{URLorTabId.GetType()}]", this, GPALObjectType.Other);
641 return new TabTuple();
642 }
643 }
644
650 public void SwitchToElement(string elementId)
651 {
652 Client.SwitchToElement(elementId).Execute();
653 }
654
660 public void InFrame(string elementId)
661 {
662 Client.SwitchToElement(elementId).Execute();
663 }
664
669 public void Minimize()
670 {
671 Client.Minimize().Execute();
672 }
673
678 public void Maximize()
679 {
680 Client.Maximize().Execute();
681 }
682
688 public void MoveTo(string elementId)
689 {
690 Client.ScrollIntoView(elementId).Execute();
691 Client.Hover(elementId).Execute();
692 }
693
698 public void Normal()
699 {
700 Client.Normal().Execute();
701 }
702
709 public TabTuple NewTab(GPALUrl URL = null)
710 {
711 if (null == URL)
712 URL = new GPALUrl();
713
714 URL.ForUrl(GetFullUrl(URL.Url, browser, out ((Browser)browser)._areRobotsAllowed));
715
716 TabTuple retVal = Client.NewTab(URL.Url).Execute<TabTuple>();
717 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
718
719 return retVal;
720 }
721
728 {
729 return Client.NextTab().Execute<TabTuple>();
730 }
731
738 public int TabCount()
739 {
740 return Client.TabCount().Execute<int>();
741 }
742
749 {
750 return Client.NextWindow().Execute<WindowTuple>();
751 }
752
758 public void OverrideReferrer(string referrer)
759 {
760 Client.OverrideReferrer(referrer).Execute();
761 }
762
768 public void SetUserAgent(string userAgent)
769 {
770 Client.SetUserAgent(userAgent).Execute();
771 }
772
777 public void PageDown()
778 {
779 Client.PageDown().Execute();
780 }
781
786 public void PageEnd()
787 {
788 Client.PageEnd().Execute();
789 }
790
795 public void PageTop()
796 {
797 Client.PageTop().Execute();
798 }
799
804 public void PageUp()
805 {
806 Client.PageUp().Execute();
807 }
808
812 public void PressModifierKey(ModifierKeys modifierKeys)
813 {
814 Client.PressModifierKey(modifierKeys).Execute();
815 }
816
822 {
823 return Client.PreviousTab().Execute<TabTuple>();
824 }
825
826
833 {
834 return Client.PreviousWindow().Execute<WindowTuple>();
835 }
836
840 public void Refresh()
841 {
842 Client.Refresh().Execute();
843 }
844
849 public void ReleaseModifierKey(ModifierKeys modifierKeys)
850 {
851 Client.ReleaseModifierKey(modifierKeys).Execute();
852 }
853
858 public void Restore()
859 {
860 Client.Restore().Execute();
861 }
862
868 public void ScrollWindowHorizontal(int pixels)
869 {
870 Client.ScrollWindowByHorizontal(pixels).Execute();
871 }
872
878 public void ScrollWindowVertical(int pixels)
879 {
880 Client.ScrollWindowByVertical(pixels).Execute();
881 }
882
887 public void SetGoogleReferrer()
888 {
889 Client.SetGoogleReferrer().Execute();
890 }
891
901 public void WithDownloadFile(string downloadPath)
902 {
903 Client.SetDownloadFilename(Path.GetFileName(downloadPath)).Execute();
904 }
905
911 public int WindowScreenLeft()
912 {
913 return Client.WindowScreenLeft().Execute<int>();
914 }
915
921 public int WindowScreenTop()
922 {
923 return Client.WindowScreenTop().Execute<int>();
924 }
925
931 public void Focus(string elementId)
932 {
933 Client.Focus(elementId).Execute();
934 }
935
941 public void FireChangeEvent(string elementId)
942 {
943 Client.FireChangeEvent(elementId).Execute();
944 }
945
950 public Rectangle GetWindowRectangle()
951 {
952 Rectangle rect = new Rectangle();
953 var result = Client.GetWindowRectangle().Execute<Dictionary<string, object>>();
954 Dictionary<string, object> obj = null;
955
956 if (null != result)
957 {
958 obj = (Dictionary<string, object>)result;
959
960 // the extension answers { X, Y, Width, Height }
961 var left = obj.ContainsKey("X") ? Convert.ToInt32(obj["X"]) : 0;
962 var top = obj.ContainsKey("Y") ? Convert.ToInt32(obj["Y"]) : 0;
963 var width = obj.ContainsKey("Width") ? Convert.ToInt32(obj["Width"]) : 0;
964 var height = obj.ContainsKey("Height") ? Convert.ToInt32(obj["Height"]) : 0;
965
966 rect = new Rectangle(left, top, width, height);
967 }
968
969 return rect;
970 }
971
976 public void ScrollIntoView(string elementId)
977 {
978 Client.ScrollIntoView(elementId).Execute();
979 }
980
987 public void ScrollElement(string elementId, int hPixels, int vPixels)
988 {
989 Client.ScrollElement(elementId).WithHPixels(hPixels).WithVPixels(vPixels).Execute();
990 }
991
997 public string GetElementAttributeHash(string elementId)
998 {
999 return Client.GetElementAttributeHash(elementId).Execute();
1000 }
1001
1007 public List<GPALElement> GetOptions(string elementId)
1008 {
1009 List<GPALElement> tmpElements = Client.GetOptions(elementId).Execute<List<GPALElement>>();
1011 return tmpElements;
1012 }
1013
1017 public string GetPageSource()
1018 {
1019 return Client.GetPageSource().Execute();
1020 }
1021
1027 public GPALElement GetParentNode(string elementId)
1028 {
1029 return Client.GetParentNode(elementId).Execute<List<GPALElement>>()[0];
1030 }
1031
1039 public string GetAttribute(string elementId, string attribute)
1040 {
1041 return Client.GetAttribute(elementId).WithAttribute(attribute).Execute();
1042 }
1043
1051 public void SetAttribute(string elementId, string attribute, string value)
1052 {
1053 Client.SetAttribute(elementId).WithAttribute(attribute).WithValue(value).Execute();
1054 }
1055
1056 public void SetRange(string elementId, int rangeValue)
1057 {
1058 Client.SetRange(elementId).WithRangeValue(rangeValue).Execute();
1059 }
1060
1067 public void SetValueFromElement(string srcSelector, string destElementId)
1068 {
1069 Client.SetValueFrom(destElementId).WithElementId(srcSelector).Execute();
1070 }
1071
1077 public void LeftClick(string elementId)
1078 {
1079 Client.LeftClick(elementId).Execute();
1080 Thread.Sleep(500);
1081 }
1082
1088 public void LeftDoubleClick(string elementId)
1089 {
1090 Client.LeftDoubleClick(elementId).Execute();
1091 }
1092
1098 public void MiddleClick(string elementId)
1099 {
1100 Client.MiddleClick(elementId).Execute();
1101 Thread.Sleep(500);
1102 }
1103
1109 public void RightClick(string elementId)
1110 {
1111 Client.RightClick(elementId).Execute();
1112 }
1113
1114 public void SelectClick(string elementId, dynamic indexOrValue)
1115 {
1116 if (indexOrValue is int idx)
1117 Client.SelectClick(elementId).WithSelectIndex(idx).Execute();
1118 else
1119 Client.SelectClick(elementId).WithSelectValue(indexOrValue).Execute();
1120 }
1121
1128 public bool IsVisibleInViewport(string elementId)
1129 {
1130 return Client.IsVisibleInViewport(elementId).Execute<bool>();
1131 }
1132
1138 public bool IsEndOfPage()
1139 {
1140 return Client.IsEndOfPage().Execute<bool>();
1141 }
1142
1148 public void Hover(string elementId)
1149 {
1150 Client.Hover(elementId).Execute();
1151 }
1152
1162 public void DragAndDrop(string elementId, int deltaX, int deltaY, int offsetX = 0, int offsetY = 0)
1163 {
1164 Client.DragAndDrop(elementId).WithDeltaX(deltaX).WithDeltaY(deltaY).WithOffsetX(offsetX).WithOffsetY(offsetY).Execute();
1165 }
1166
1173 {
1174 return Client.WindowInnerHeight().Execute<int>();
1175 }
1176
1182 public int WindowInnerWidth()
1183 {
1184 return Client.WindowInnerWidth().Execute<int>();
1185 }
1186
1193 {
1194 return Client.WindowOuterHeight().Execute<int>();
1195 }
1196
1202 public int WindowOuterWidth()
1203 {
1204 return Client.WindowOuterWidth().Execute<int>();
1205 }
1206
1219 public string Fetch(string url, string method = null, string body = null, string contentType = null, string[] headers = null, bool asBytes = false)
1220 {
1221 string retVal = Client
1222 .Fetch(url)
1223 .WithVerb(method)
1224 .WithBody(body)
1225 .WithContentType(contentType)
1226 .WithHeaders(headers)
1227 .WithBytes(asBytes)
1228 .Execute();
1229
1230 // the extension answers NOTOK when the page could not make the request, and the caller reads null as
1231 // the failure, the same as the other engines
1232 return "NOTOK" == retVal || "null" == retVal ? null : retVal;
1233 }
1234
1241 {
1242 return Client.WindowPageOffsetX().Execute<int>();
1243 }
1244
1251 {
1252 return Client.WindowPageOffsetY().Execute<int>();
1253 }
1254
1268 public bool ElementFromPoint(string elementId, int x, int y)
1269 {
1270 return true == Client.ElementFromPoint(elementId, x, y).Execute<bool>();
1271 }
1272
1281 public bool TrustedLeftClick(int x, int y, int holdMs = 0)
1282 {
1283 return true == Client.TrustedLeftClick(x, y, holdMs).Execute<bool>();
1284 }
1285
1294 public bool TrustedMiddleClick(int x, int y, int holdMs = 0)
1295 {
1296 return true == Client.TrustedMiddleClick(x, y, holdMs).Execute<bool>();
1297 }
1298
1299 public Rectangle GetBoundingClientRect(string elementId)
1300 {
1301 Rectangle rect = new Rectangle(0,0,0,0);
1302 var result = Client.GetBoundingClientRect(elementId).Execute<Dictionary<string, object>>();
1303 Dictionary<string, object> obj = null;
1304
1305 if (null != result)
1306 {
1307 obj = (Dictionary<string, object>)result;
1308
1309 // the extension answers with the element's DOMRect, lower case: x, y, width, height, top, right, bottom, left
1310 var left = obj.ContainsKey("left") ? Convert.ToInt32(obj["left"]) : 0;
1311 var top = obj.ContainsKey("top") ? Convert.ToInt32(obj["top"]) : 0;
1312 var width = obj.ContainsKey("width") ? Convert.ToInt32(obj["width"]) : 0;
1313 var height = obj.ContainsKey("height") ? Convert.ToInt32(obj["height"]) : 0;
1314
1315 rect = new Rectangle(left, top, width, height);
1316 }
1317
1318 return rect;
1319 }
1320
1326 public void HideElement(string elementId)
1327 {
1328 Client.HideElement(elementId).Execute();
1329 }
1330
1336 public void InjectScript(string script)
1337 {
1338 string answered = Client.InjectScript(script).Execute();
1339
1340 // the extension answers with the id it registered under, or with an error. thrown away, a script that
1341 // never registered looks exactly like one that did until the page fails to behave, which is a long way
1342 // from here. the usual cause is Allow user scripts still being off for the extension
1343 if (true == answered?.Contains("error"))
1344 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"The extension did not register the script [{answered}]", this, GPALObjectType.Other);
1345 }
1346
1355 public string ExecuteJavaScript(string script)
1356 {
1357 return Client.ExecuteJavaScript(script).Execute();
1358 }
1359
1367 public string GetDocumentStamp()
1368 {
1369 string retVal = Client.GetDocumentStamp().Execute();
1370
1371 // the extension answers with a json string, quotes and all. left on, the closing quote rode on the end
1372 // of the url half, and every click compared that url with the unquoted one it started from and read
1373 // as a navigation
1374 if (true == retVal?.StartsWith("\"") && true == retVal.EndsWith("\"") && 1 < retVal.Length)
1375 retVal = Newtonsoft.Json.JsonConvert.DeserializeObject<string>(retVal);
1376
1377 return retVal;
1378 }
1379
1387 public string GetHydrationScripts()
1388 {
1389 return Client.GetHydrationScripts().Execute();
1390 }
1391
1397 {
1398 Client.ClearInjectedScripts().Execute();
1399 }
1400
1409 public bool IsClickAble(string elementId)
1410 {
1411 return Client.IsClickable(elementId).Execute<bool>();
1412 }
1413
1421 public bool IsDisplayed(string elementId)
1422 {
1423 return Client.IsDisplayed(elementId).Execute<bool>();
1424 }
1425
1433 public bool IsEnabled(string elementId)
1434 {
1435 return Client.IsEnabled(elementId).Execute<bool>();
1436 }
1437
1441 public void ClearReferrer()
1442 {
1443 Client.ClearReferrer().Execute();
1444 }
1445
1452 public GPALElement Evaluate(string xpath)
1453 {
1454 return Client.Evaluate(xpath).Execute<GPALElement>();
1455 }
1456
1463 public List<GPALElement> EvaluateAll(string xpath)
1464 {
1465 return Client.EvaluateAll(xpath).Execute<List<GPALElement>>();
1466 }
1467
1473 public GPALElement EvaluatePersistent(string xpath)
1474 {
1475 return Client.EvaluatePersistent(xpath).Execute<GPALElement>();
1476 }
1477
1484 public List<GPALElement> EvaluateAllPersistent(string xpath)
1485 {
1486 return Client.EvaluateAllPersistent(xpath).Execute<List<GPALElement>>();
1487 }
1488
1494 public string GetUserAgent()
1495 {
1496 return Client.GetUserAgent().Execute();
1497 }
1498
1504 public string GetLanguages()
1505 {
1506 return Client.GetLanguages().Execute();
1507 }
1508
1518 public void CaptureCalls(bool capture = true, string urlFragment = null, bool clear = false)
1519 {
1520 IAllowRESTCaptureOptions capturing = Client.CaptureCalls(capture);
1521
1522 if (false == string.IsNullOrWhiteSpace(urlFragment)) capturing.WithCallFilter(urlFragment);
1523 if (true == clear) capturing.WithClearCalls(true);
1524
1525 capturing.Execute();
1526 }
1527
1533 public string GetCapturedCalls()
1534 {
1535 return Client.GetCapturedCalls().Execute();
1536 }
1537
1544 public void FillInAppend(string elementId, string text)
1545 {
1546 Client.FillInAppend(elementId).WithText(text).Execute();
1547 }
1548
1555 public void FillInInsert(string elementId, string text)
1556 {
1557 Client.FillInInsert(elementId).WithText(text).Execute();
1558 }
1559
1566 public void FillInOverwrite(string elementId, string text)
1567 {
1568 Client.FillInOverwrite(elementId).WithText(text).Execute();
1569 }
1570
1577 public void LeftClickAndDownload(string elementId, string filenameAndPath)
1578 {
1579 Client.LeftClickAndDownload(elementId).WithDownloadFile(Path.GetFileName(filenameAndPath)).Execute();
1580 }
1581
1587 public void LeftClickAndUpload(string elementId, string filenameAndPath)
1588 {
1589 Client.LeftClickAndUpload(elementId).WithUploadFile(filenameAndPath).Execute();
1590 }
1591
1597 public void LeftClickAndUpload(string elementId, GPALFile filenamesAndPaths)
1598 {
1599 Client.LeftClickAndUpload(elementId).WithUploadFiles(filenamesAndPaths).Execute();
1600 }
1601
1602
1609 public GPALElement QuerySelector(string css)
1610 {
1611 var elem = Client.QuerySelector(css).Execute<GPALElement>();
1612 return elem;
1613 }
1614
1621 public List<GPALElement> QuerySelectors(string css)
1622 {
1623 return Client.QuerySelectors(css).Execute<List<GPALElement>>();
1624 }
1625
1633 {
1634 var persist = Client.QueryPersistentSelector(css).Execute<GPALElement>();
1635
1636 return persist;
1637 }
1638
1645 public List<GPALElement> QueryPersistentSelectors(string css)
1646 {
1647 return Client.QueryPersistentSelectors(css).Execute<List<GPALElement>>();
1648 }
1649
1655 public void ScrollWindow(int hPixels, int vPixels)
1656 {
1657 // Note: Requires RESTClient to implement ScrollWindow, or use ExecuteJavaScript as fallback
1658 Client.ScrollWindow(hPixels, vPixels).Execute();
1659 }
1660
1666 public void SendKey(byte vkcode)
1667 {
1668 Client.SendKey(vkcode).Execute();
1669 }
1670
1677 public void SendString(string text, int delayMs = 0)
1678 {
1679 Client.SendString(text, delayMs).Execute();
1680 }
1681
1682
1688 {
1689 Client.SwitchToDefaultContent().Execute();
1690 }
1691
1697 public void SwitchToShadowRoot(string elementId)
1698 {
1699 Client.SwitchToShadowRoot(elementId).Execute();
1700 }
1701
1702 #region Window Handling
1703 public WindowTuple OpenWindow(GPALUrl URL = null)
1704 {
1705 if (null == URL)
1706 URL = new GPALUrl();
1707
1708 URL.ForUrl(GetFullUrl(URL.Url, browser, out ((Browser)browser)._areRobotsAllowed));
1709
1710 WindowTuple retVal = Client.OpenWindow(URL.Url).Execute<WindowTuple>();
1711 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
1712
1713 return retVal;
1714 }
1715
1716 public WindowTuple GoToWindow(dynamic urlOrId)
1717 {
1718 if (urlOrId is string v)
1719 return Client.GoToWindow(v).Execute<WindowTuple>();
1720 else
1721 return Client.GoToWindow(urlOrId.ToString()).Execute<WindowTuple>();
1722 }
1728 public string CloseBrowser()
1729 {
1730 return Client.CloseBrowser().Execute();
1731 }
1732
1733 public WindowTuple CloseWindow(dynamic urlOrId)
1734 {
1735 if (urlOrId is string v)
1736 return Client.CloseWindow(v).Execute<WindowTuple>();
1737 else
1738 return Client.CloseWindow(urlOrId.ToString()).Execute<WindowTuple>();
1739 }
1740
1741 public WindowTuple GetCurrentWindow()
1742 {
1743 return Client.GetCurrentWindow().Execute<WindowTuple>();
1744 }
1745 #endregion Window Handling
1746 #region GPAL Studio
1752 public string GetBrowserSettings()
1753 {
1754 return Client.GetBrowserSettings().Execute();
1755 }
1756
1762 public string GetGpalSettings()
1763 {
1764 return Client.GetGpalSettings().Execute();
1765 }
1766
1772 public string GetSettings()
1773 {
1774 return Client.GetSettings().Execute();
1775 }
1776
1782 public string GetWorkflow()
1783 {
1784 return Client.GetWorkflow().Execute();
1785 }
1786 #endregion GPAL Studio
1787 #endregion Commands
1788 #region Helpers
1804 static void PublishBrowserOutput(BrowserSettings browserSettings, DataReceivedEventArgs data)
1805 {
1806 if (null != data.Data)
1807 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"[{browserSettings.BrowserType}] {data.Data}", browserSettings, GPALObjectType.Other);
1808 }
1818 private static void AdoptFirefoxChild(BrowserSettings browserSettings, int launcherPid, DateTime deadline)
1819 {
1820 if (1 > launcherPid) return;
1821
1822 while (DateTime.UtcNow < deadline)
1823 {
1824 int childPid = 0;
1825
1826 try
1827 {
1828 using (var searcher = new ManagementObjectSearcher(
1829 $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {launcherPid} AND Name = 'firefox.exe'"))
1830 {
1831 foreach (ManagementObject found in searcher.Get())
1832 {
1833 childPid = Convert.ToInt32(found["ProcessId"]);
1834 break;
1835 }
1836 }
1837 }
1838 catch (Exception)
1839 {
1840 // A WMI failure leaves the launcher process in place
1841 return;
1842 }
1843
1844 if (0 != childPid)
1845 {
1846 try
1847 {
1848 browserSettings.Process = System.Diagnostics.Process.GetProcessById(childPid);
1849
1850 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"FireFox launcher [{launcherPid}] handed over to browser process [{childPid}]", browserSettings, GPALObjectType.Browser);
1851
1852 return;
1853 }
1854 catch (Exception)
1855 {
1856 // Exited between being listed and being opened
1857 }
1858 }
1859
1860 Thread.Sleep(100);
1861 }
1862 }
1863
1864 public Process LaunchBrowser(Browser browser, GPALUrl URL, List<string> additionalArguments = null)
1865 {
1866 BrowserSettings browserSettings = browser.BrowserSettings;
1867 bool created = false;
1868 int secondsToWait = 30;
1869 TimeSpan timeout = TimeSpan.FromSeconds(secondsToWait);
1870
1871 if (null == browserSettings)
1872 {
1873 // this should only be called when we are using Oauth credentials, to log someone in? we could require a browser passed into credentials, but why?
1874 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "BrowserSettings parameter is null. Using chrome puppeteer port.", null, GPALObjectType.Other);
1875 browser = (Browser)GPAL.Browser.ToGPALObject();
1876 browserSettings = ((Browser)browser).BrowserSettings;
1877 }
1878
1879 if (null != browserSettings.ExistingBrowserPort)
1880 {
1881 // the browser being attached to opened that port with .WithCDPDebugPort, so DebugPort is where it
1882 // is listening and ExistingBrowserPort is only ever set on the workflows attaching to it. matching
1883 // one attacher against another never found the browser itself, so the port was taken as free, a
1884 // second browser was launched on the next one up, and nothing was listening where it then looked
1885 foreach (Browser b in GPAL.Browsers)
1886 if (b != browser
1887 && (b.BrowserSettings.DebugPort == browserSettings.ExistingBrowserPort
1888 || b.BrowserSettings.ExistingBrowserPort == browserSettings.ExistingBrowserPort))
1889 {
1890 browser.PuppeteerClient = b.PuppeteerClient;
1891 browser.PuppeteerCommunicator = b.PuppeteerCommunicator;
1892
1893 return b.Process;
1894 }
1895 }
1896
1897 // every launch, not only the ones that ask for a temp profile, so a run that supplies its own profile
1898 // still clears what a killed run left behind
1899 ChromeProfileManager.SweepAbandonedTempProfiles();
1900
1901 if (null == URL || null == URL?.Url)
1902 URL = BrowserType.FireFox != browserSettings.BrowserType ? "about:blank" : "google.com";
1903
1904 URL.ForUrl(GetFullUrl(URL?.Url, browser, out ((Browser)browser)._areRobotsAllowed, true)); // on launch we now open to google.com, don't do a robots.txt check on launch
1905
1906 if (false == ((Browser)browser).AreRobotsAllowed && true == ((Browser)browser).BrowserSettings.ObeyRobotsTxt)
1907 {
1908 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Visiting [{URL?.Url}] is disallowed by robots.txt and your request to honor it. Not going to URL. Workflow will fail.", browser, GPALObjectType.Browser);
1909 URL.ForUrl("https://google.com");
1910 }
1911
1912 bool headless = browserSettings.UseHeadless;
1913 var chromeArguments = additionalArguments ?? new List<string>();
1914 string browserPath;
1915
1916 // Get browser executable path
1917 switch (browserSettings.BrowserType)
1918 {
1919 case BrowserType.Chrome:
1920 browserPath = Puppeteer.GetChromePath();
1921 break;
1922 case BrowserType.Edge:
1923 browserPath = Puppeteer.GetEdgePath();
1924 break;
1925 case BrowserType.FireFox:
1926 browserPath = Puppeteer.GetFirefoxPath();
1927 break;
1928 default:
1929 Exception ex = new Exception("Unsupported Browser Type");
1930 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unsupported browser type [{browserSettings.BrowserType}]", browserSettings, GPALObjectType.Other, ex);
1931 return null;
1932 }
1933
1934 if (string.IsNullOrEmpty(browserPath) || false == File.Exists(browserPath))
1935 {
1936 string msg = $"[{browserSettings.BrowserType}] executable not found at [{browserPath}]";
1937 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg, browserSettings, GPALObjectType.Other);
1938 throw new NotFoundException(msg);
1939 }
1940
1941 // Common arguments for Chrome/Edge
1942 if (browserSettings.BrowserType == BrowserType.Chrome || browserSettings.BrowserType == BrowserType.Edge)
1943 {
1944 string userAgent = null;
1945
1946 // Profile settings
1947 if (false == string.IsNullOrEmpty(browserSettings.ProfileUserName) || false == string.IsNullOrEmpty(browserSettings.ProfileName))
1948 {
1949 browserSettings.ProfileDataDirectory = BrowserHelper.GetBrowserProfileDirectory(browserSettings);
1950 if (false == string.IsNullOrEmpty(browserSettings.ProfileName))
1951 {
1952 // FindProfileDirectory returns a directory or throws
1953 // Quoted, as --user-data-dir below is. These arguments are joined into one command
1954 // line and Chrome directory names contain spaces, so --profile-directory=Profile 4
1955 // would split into a different profile plus a bare 4 that Chrome opens as a url
1956 chromeArguments.Add($@"--profile-directory=""{BrowserHelper.FindProfileDirectory(browserSettings.ProfileDataDirectory, browserSettings.ProfileName)}""");
1957 }
1958 }
1959 else if (true == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
1960 {
1961 browserSettings.ProfileDataDirectory = ChromeProfileManager.CreateTempUserProfile(
1962 browserSettings.DownloadLocation ?? FileHelper.GetDefaultDownloadDirectory(browser),
1963 browserSettings.PromptForDownload,
1964 browserSettings.OpenPDFExternally,
1965 browserSettings.LoadImages,
1966 browserSettings.UseOttoMagic
1967 );
1968 browserSettings.TempProfileCreated = true;
1969 }
1970
1971 // prompting and the pdf viewer are profile preferences. the extension cannot set either one and
1972 // there is no CDP here, so a supplied profile has to be written to for the workflow's settings to
1973 // mean anything, and Browser.Close puts it back once the browser is gone. selenium and puppeteer
1974 // get this in BrowserHelper while their options are built, and firefox gets it from CreateUserJs
1975 // below, so chromium under the extension was the one path running on whatever the profile said
1976 if (false == browserSettings.TempProfileCreated && false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
1977 browserSettings.PreviousDownloadPreferences = ChromeProfileManager.ApplyDownloadPreferences(
1978 browserSettings.ProfileDataDirectory,
1979 browserSettings.DownloadLocation,
1980 browserSettings.PromptForDownload,
1981 browserSettings.OpenPDFExternally);
1982
1983 // a profile can only be owned by one browser. launching onto one that is already open hands the
1984 // command line to that browser and exits, so the extension never loads and GPALRestAPI never
1985 // announces a port. selenium has refused this since it was written; this path did not, which is
1986 // why an OttoMagic run on a held profile failed later saying nothing about the profile
1987 if (false == browserSettings.TempProfileCreated)
1988 BrowserHelper.RefuseIfProfileIsOpen(browserSettings);
1989
1990 // Debugging
1991 if (true == browserSettings.UsePuppeteer)
1992 {
1993 if (true == browserSettings.DebugPipe)
1994 chromeArguments.Add("--remote-debugging-pipe --enable-unsafe-extension-debugging --remote-debugging-port=0");
1995 else
1996 {
1997 // asked for rather than assumed: a second browser starting alongside this one has to be
1998 // given a different port, and 0xdead for everybody is how they end up in one session
1999 if (null == browserSettings.DebugPort)
2000 browserSettings.DebugPort = BrowserHelper.FindFreePort();
2001
2002 browserSettings.PuppeteerUrl = $"http://localhost:{browserSettings.DebugPort}";
2003 chromeArguments.Add($"--remote-debugging-port={browserSettings.DebugPort.Value}");
2004 }
2005
2006 // *******************************************************************
2007 // NOTE: CAVEAT: suppose a time comes to run with some sort of extension, this isn't causing any harm, just annoying...
2008 // not keen on disabling extensions, they make the bot look alive, but somehow we score low on recaptcha with ottomagic, no idea why, puppeteer is awesome
2009 // *******************************************************************
2010 // chromeArguments.Add("--disable-extensions"); // no ottomagic
2011 // *******************************************************************
2012 // *******************************************************************
2013 }
2014
2015 // set windowsize if provided or not - new
2016 if (false == browserSettings.FullScreen &&
2017 false == browserSettings.Maximize &&
2018 false == browserSettings.Minimize)
2019 {
2020 if (false == browserSettings.WindowSize.IsEmpty)
2021 {
2022 // explicit WindowSize provided - must set screen-info in headless=new
2023 // otherwise CDP interactions fail with "Browser window not found"
2024 chromeArguments.Add($"--window-size={browserSettings.WindowSize.Width},{browserSettings.WindowSize.Height}");
2025
2026 // the rectangle carries where as well as how big, and a workflow running several browsers
2027 // wants them beside each other rather than stacked. left at the origin it is not asked for
2028 if (0 != browserSettings.WindowSize.X || 0 != browserSettings.WindowSize.Y)
2029 chromeArguments.Add($"--window-position={browserSettings.WindowSize.X},{browserSettings.WindowSize.Y}");
2030 }
2031 else if (browserSettings.UseHeadless)
2032 chromeArguments.Add("--start-fullscreen");
2033 }
2034 else if (true == browserSettings.FullScreen)
2035 chromeArguments.Add("--start-fullscreen");
2036 else if (true == browserSettings.Maximize)
2037 chromeArguments.Add("--start-maximized");
2038
2039 // Anti-bot detection
2040 //if (true == myBrowserSettings.UsePuppeteer || AutomationEngine.PuppeteerPortHW == myBrowserSettings.AutomationEngine)
2041 //{
2042 // chromeArguments.Add("--disable-blink-features=AutomationControlled");
2043 chromeArguments.Add("--excludeSwitches=enable-automation");
2044 chromeArguments.Add("--useAutomationExtension=false");
2045 //}
2046
2047 // Core arguments to suppress popups and stabilize CDP
2048 chromeArguments.Add("--no-first-run");
2049 chromeArguments.Add("--no-default-browser-check");
2050 //chromeArguments.Add("--no-default-browser-check");
2051 //chromeArguments.Add("--disable-default-apps");
2052 //chromeArguments.Add("--disable-features=TranslateUI,FirstRun,ChromeWhatsNewUI,InterestCohort");
2053 //chromeArguments.Add("--disable-background-networking");
2054 //chromeArguments.Add("--disable-background-timer-throttling");
2055 //chromeArguments.Add("--disable-renderer-backgrounding");
2056 //chromeArguments.Add("--disable-backgrounding-occluded-windows");
2057 //chromeArguments.Add("--no-service-autorun");
2058 //chromeArguments.Add("--disable-client-side-phishing-detection");
2059 chromeArguments.Add("--disable-dev-shm-usage");
2060
2061 // Edge signs a new profile in with the Windows account and syncs favourites, settings and
2062 // extensions down onto it, and each extension sync installs opens a page. Applied to temp
2063 // profiles only; a supplied profile keeps whatever sync settings it has
2064 if (true == browserSettings.TempProfileCreated)
2065 {
2066 chromeArguments.Add("--disable-sync");
2067 chromeArguments.Add("--disable-signin-promo");
2068 }
2069
2070 // avoid chrome overlays on the rendered area
2071 chromeArguments.Add("--disable-infobars");
2072 chromeArguments.Add("--disable-session-crashed-bubble");
2073 chromeArguments.Add("--hide-crash-restore-bubble");
2074 chromeArguments.Add("--noerrdialogs");
2075 // one --disable-features switch only, chromium takes the last one rather than merging them.
2076 // edge runs its own shopping content scripts on retail pages, and their constant dom mutation is
2077 // what has DOM.getDocument hanging there while the same page is fine under chrome
2078 string disableFeatures = "TranslateUI,Translate";
2079
2080 if (BrowserType.Edge == browserSettings.BrowserType)
2081 {
2082 disableFeatures += ",msShoppingTrigger,msShopping,msEdgeShoppingUI,msEdgeShoppingList";
2083
2084 // Edge implicit sign-in, which --disable-sync does not cover
2085 if (true == browserSettings.TempProfileCreated)
2086 disableFeatures += ",msImplicitSignin,msEdgeImplicitSignin,msIdentityImplicitSignin";
2087 }
2088
2089 chromeArguments.Add($"--disable-features={disableFeatures}");
2090 chromeArguments.Add("--load-media-router-component-extension");
2091 chromeArguments.Add("--enable-media-router");
2092 chromeArguments.Add("--force-discovery");
2093 chromeArguments.Add("--allow-local-network-access");
2094
2095 if (false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
2096 chromeArguments.Add($@"--user-data-dir=""{browserSettings.ProfileDataDirectory.Replace("\\", "/")}""");
2097
2098 // the same switch the selenium options path adds. Said on the command line rather than as a
2099 // profile preference because it applies whatever profile the run lands on, including a temp one,
2100 // and because this is the only route the extension and puppeteer have: neither can set it later
2101 chromeArguments.Add($"--blink-settings=imagesEnabled={browserSettings.LoadImages.ToString().ToLower()}");
2102
2103 // Headless mode
2104 if (headless)
2105 {
2106 chromeArguments.Add("--headless=new");
2107 chromeArguments.Add("--disable-notifications");
2108 chromeArguments.Add("--disable-gpu");
2109 if (AutomationEngine.PuppeteerPipe == browserSettings.AutomationEngine || AutomationEngine.PuppeteerPipeHW == browserSettings.AutomationEngine)
2110 chromeArguments.Add("--no-sandbox"); // REQUIRED FOR PIPE IN HEADLESS + TEMP PROFILE
2111
2112 // Fetch user-agent
2113 userAgent = GetUserAgent(browserSettings, chromeArguments);
2114
2115 userAgent = userAgent?.Trim('"');
2116
2117 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Headless [{browserSettings.BrowserType}] using UserAgent [{userAgent}]", browserSettings, GPALObjectType.Other);
2118
2119 chromeArguments.Add($"--user-agent=\"{userAgent}\"");
2120 }
2121 else
2122 {
2123 if (false == browserSettings.BlockPopUps)
2124 chromeArguments.Add("--disable-popup-blocking");
2125 }
2126
2127 // Dark mode
2128 if (browserSettings.StealthType.HasFlag(StealthType.DarkMode))
2129 chromeArguments.Add("--force-dark-mode");
2130
2131 // Safe browsing - ads latency to each browse
2132 //chromeArguments.Add("--safebrowsing.enabled");
2133
2134 // full accessibility for using UIAutomation (it is a fallback for method: GetWindowRectangle to get the content dimensions
2135 // no longer needed, we don't use uiautomation to calculate browser coords
2136 // this creates extra work = slower browser response
2137 // chromeArguments.Add("--force-renderer-accessibility");
2138 }
2139 else if (browserSettings.BrowserType == BrowserType.FireFox)
2140 {
2141 // Firefox arguments
2142 var firefoxArguments = new List<string>();
2143
2144 if (headless)
2145 {
2146 firefoxArguments.Add("-headless");
2147 firefoxArguments.Add($"-width={Puppeteer.GetScreenWidth()}");
2148 firefoxArguments.Add($"-height={Puppeteer.GetScreenHeight()}");
2149 firefoxArguments.Add("--disable-notifications");
2150 }
2151 // nothing goes here. a headful firefox on OttoMagic is driven through the extension's rest api,
2152 // on the port GPALRestAPI announces over the handshake, so it has no use for a debugging port.
2153 // note firefox has no --websocket-port: it drops the flag and takes the port number for a url
2154
2155 // Call firefoxhelper.CreateUserJs to load set these preferences
2156 // firefox has a unique requirement for loading a browser extension on demand
2157 // we have to create an .xpi file and get the extension id, then save it to a profile dir
2158 // then specify that profile dir
2159 if (true == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
2160 {
2161 browserSettings.ProfileDataDirectory = FirefoxProfileManager.CreateTempProfileDirectory();
2162
2163 // said the way the chromium path says it, so Browser.Close deletes this profile. without it
2164 // every firefox run on the extension left its temp profile on disk
2165 browserSettings.TempProfileCreated = true;
2166 }
2167
2168 // same rule as chromium: one browser to a profile. a firefox already on this one takes the launch
2169 // and leaves the run waiting for a browser that was never started
2170 if (false == browserSettings.TempProfileCreated)
2171 BrowserHelper.RefuseIfProfileIsOpen(browserSettings);
2172
2173 // the preferences are written either way. what is kept is what was there before, and only for a
2174 // profile the workflow supplied: a temp profile is deleted on close, so there is nothing to put
2175 // back and nothing left to put it back into
2176 if (false == browserSettings.TempProfileCreated)
2177 browserSettings.PreviousFirefoxPreferences = FirefoxProfileManager.CreateUserJs(browserSettings.ProfileDataDirectory, browserSettings);
2178 else
2179 FirefoxProfileManager.CreateUserJs(browserSettings.ProfileDataDirectory, browserSettings);
2180
2181 if (false == string.IsNullOrEmpty(browserSettings.ProfileUserName) || false == string.IsNullOrEmpty(browserSettings.ProfileName))
2182 firefoxArguments.Add($"-profile \"{(browserSettings.ProfileName ?? browserSettings.ProfileUserName).TrimEnd('\\')}\"");
2183 else if (false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
2184 firefoxArguments.Add($"-profile \"{browserSettings.ProfileDataDirectory.TrimEnd('\\')}\"");
2185
2186 chromeArguments = firefoxArguments; // Reuse chromeArguments for Process.Start
2187 }
2188
2189 bool holdingHandshakeGate = false; // set when this launch owns the OttoMagic port handshake
2190 UdpClient udpHandshake = null; // out here so a launch that throws before the handshake still closes it
2191
2192 // Launch browser
2193 try
2194 {
2195 // we will do a goto. for various reason this might not work and for various reasons this might work better
2196
2197 // NOTE: we can't go to the real url on lauch, we have to apply stealth first :(
2198 if (null != URL && false == string.IsNullOrEmpty(URL.Url))
2199 chromeArguments.Add(URL?.Url);
2200
2201 if (AutomationEngine.PuppeteerPipe == browserSettings.AutomationEngine || AutomationEngine.PuppeteerPipeHW == browserSettings.AutomationEngine)
2202 browserSettings.DebugPipe = true;
2203
2204 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Running workflow for Automation Engine [{browserSettings.AutomationEngine}]", null, GPALObjectType.None);
2205
2206 if (true == browserSettings.UseOttoMagic)
2207 {
2208 // wait for any launch already in flight to have read its port before we take the socket.
2209 // An abandoned gate means the process holding it died, and we are the owner now
2210 try { holdingHandshakeGate = ottoHandshakeGate.WaitOne(OttoHandshakeGateTimeoutMs); }
2211 catch (AbandonedMutexException) { holdingHandshakeGate = true; }
2212
2213 if (false == holdingHandshakeGate)
2214 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Waited [{OttoHandshakeGateTimeoutMs}] ms for another OttoMagic browser to finish its port handshake. Launching anyway, this browser may not get a port of its own.", browserSettings, GPALObjectType.Browser);
2215
2216 udpHandshake = new UdpClient(47623);
2217 udpHandshake.Client.ReceiveTimeout = 30000;
2218
2219 KeepFromChildren(udpHandshake);
2220 }
2221
2222 int firefoxLauncherPid = 0;
2223
2224 if (true == browserSettings.DebugPipe)
2225 {
2226 var sa = new Native.SECURITY_ATTRIBUTES
2227 {
2228 nLength = Marshal.SizeOf<Native.SECURITY_ATTRIBUTES>(),
2229 bInheritHandle = 1,
2230 lpSecurityDescriptor = IntPtr.Zero
2231 };
2232
2233 IntPtr hProcess = IntPtr.Zero;
2234 IntPtr reservedPtr = IntPtr.Zero;
2235 AnonymousPipeServerStream stdinPipe = null, stdoutPipe = null, stderrPipe = null;
2236
2237 try
2238 {
2239 // Create anonymous pipes. bInheritHandle is set to true via HandleInheritability.Inheritable
2240 stdinPipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable, 1048576);
2241 stdoutPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable, 1048576);
2242 stderrPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable, 1048576);
2243
2244 // Build command line
2245 var chromeCommandLine = new StringBuilder();
2246 chromeCommandLine.Append($@"""{browserPath}""");
2247 chromeCommandLine.Append(" ");
2248 chromeCommandLine.Append(string.Join(" ", chromeArguments));
2249
2250 // Create process
2251 var startupInfo = new Native.STARTUPINFO
2252 {
2253 cb = (uint)Marshal.SizeOf<Native.STARTUPINFO>(),
2254 // named outright on the one launch we build ourselves, rather than left to inherit
2255 // from the launching thread. null is the desktop the caller is on
2256 lpDesktop = true == browserSettings.HiddenDesktop ? HiddenDesktop.StartupNameFor(browserSettings.HiddenDesktopName) : null,
2257 dwFlags = Native.STARTF_USESHOWWINDOW | Native.STARTF_USESTDHANDLES, // Use standard handles flag
2258 wShowWindow = (ushort)(headless ? Native.SW_HIDE : Native.SW_SHOWNORMAL),
2259 hStdInput = stdinPipe.ClientSafePipeHandle.DangerousGetHandle(), // Assign to standard input
2260 hStdOutput = stdoutPipe.ClientSafePipeHandle.DangerousGetHandle(), // Assign to standard output
2261 hStdError = stderrPipe.ClientSafePipeHandle.DangerousGetHandle(), // Assign to standard error
2262 //cbReserved2 = 0, // Reset unused fields
2263 //lpReserved2 = IntPtr.Zero
2264 };
2265
2266 // Call CreateProcess with bInheritHandles set to true
2267 created = Native.CreateProcess(
2268 null,
2269 chromeCommandLine.ToString(),
2270 IntPtr.Zero,
2271 IntPtr.Zero,
2272 true, // Inherit handles
2273 0,
2274 IntPtr.Zero,
2275 browserSettings.ProfileDataDirectory,
2276 ref startupInfo,
2277 out var processInfo2);
2278
2279 if (!created)
2280 throw new Win32Exception(Marshal.GetLastWin32Error(), "PuppeteerPipe CreateProcess failed.");
2281
2282 hProcess = processInfo2.hProcess;
2283
2284 // Assign streams
2285 browserSettings.InboundPipe = stdinPipe;
2286 browserSettings.OutboundPipe = stdoutPipe;
2287 browserSettings.ErrorPipe = stderrPipe;
2288
2289 // Capture stderr
2290 string stderr = "";
2291 var errorReader = new StreamReader(stderrPipe, Encoding.UTF8, true, 4096, false);
2292 _ = Task.Run(async () =>
2293 {
2294 try
2295 {
2296 char[] buffer = new char[4096];
2297 int read;
2298 while ((read = await errorReader.ReadAsync(buffer, 0, buffer.Length)) > 0)
2299 {
2300 stderr += new string(buffer, 0, read);
2301 }
2302 if (Native.GetExitCodeProcess(hProcess, out uint finalExitCode) && finalExitCode != 0)
2303 {
2304 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Chrome exited with code: [{finalExitCode}], Stderr: [{stderr}]", browserSettings, GPALObjectType.Other);
2305 }
2306 }
2307 catch (GPALException)
2308 {
2309 throw;
2310 }
2311 catch (Exception ex)
2312 {
2313 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error reading stderr", browserSettings, GPALObjectType.Other, ex);
2314 }
2315 finally
2316 {
2317 errorReader.Dispose();
2318 }
2319 });
2320
2321 try
2322 {
2323 // Wait/verify process
2324 if (!Native.GetExitCodeProcess(hProcess, out uint exitCode))
2325 throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to get process exit code.");
2326 if (exitCode != Native.STILL_ACTIVE)
2327 throw new GPALException($"Chrome exited. Exit code: {exitCode}, Stderr: {stderr}, Check {Path.Combine(browserSettings.ProfileDataDirectory, "chrome_log.txt")}");
2328
2329 browserSettings.Process = Process.GetProcessById((int)processInfo2.dwProcessId);
2330 browserSettings.Process.EnableRaisingEvents = true;
2331 }
2332 catch (ArgumentException ex)
2333 {
2334 if (Native.GetExitCodeProcess(hProcess, out uint exitCode) && exitCode != Native.STILL_ACTIVE)
2335 throw new GPALException($"Chrome exited early. Exit code: {exitCode}, Stderr: {stderr}");
2336 throw new GPALException($"Failed to get Chrome process by ID: {ex.Message}, Stderr: {stderr}");
2337 }
2338 }
2339 catch (GPALException)
2340 {
2341 throw;
2342 }
2343 catch (Exception ex)
2344 {
2345 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to launch Chrome with pipes", browserSettings, GPALObjectType.Other, ex);
2346 throw;
2347 }
2348 finally
2349 {
2350 if (reservedPtr != IntPtr.Zero)
2351 Marshal.FreeHGlobal(reservedPtr);
2352 if (hProcess != IntPtr.Zero)
2353 Native.CloseHandle(hProcess);
2354 if (!created)
2355 {
2356 stdinPipe?.Dispose();
2357 stdoutPipe?.Dispose();
2358 stderrPipe?.Dispose();
2359 }
2360 }
2361 }
2362 else if (true == browserSettings.HiddenDesktop)
2363 {
2364 // built by hand because ProcessStartInfo has nowhere to put lpDesktop, and a child started
2365 // without one named takes the desktop of the parent process rather than of the thread that
2366 // started it. Naming it is the only thing that puts the browser on another desktop.
2367 // The cost is the browser's own stdout and stderr, which only ever reach DEEPDEBUG
2368 Native.STARTUPINFO hiddenStartup = new Native.STARTUPINFO
2369 {
2370 cb = (uint)Marshal.SizeOf<Native.STARTUPINFO>(),
2371 lpDesktop = HiddenDesktop.StartupNameFor(browserSettings.HiddenDesktopName)
2372 };
2373
2374 string hiddenCommandLine = $@"""{browserPath}"" {string.Join(" ", chromeArguments)}";
2375
2376 if (false == Native.CreateProcess(null, hiddenCommandLine, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref hiddenStartup, out var hiddenProcess))
2377 throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not start [{browserSettings.BrowserType}] on [{browserSettings.HiddenDesktopName}]");
2378
2379 Native.CloseHandle(hiddenProcess.hProcess);
2380 Native.CloseHandle(hiddenProcess.hThread);
2381
2382 browserSettings.Process = Process.GetProcessById((int)hiddenProcess.dwProcessId);
2383
2384 // Capture the launcher PID immediately before it exits (Firefox only).
2385 if (BrowserType.FireFox == browserSettings.BrowserType)
2386 firefoxLauncherPid = browserSettings.Process?.Id ?? 0;
2387 }
2388 else
2389 {
2390 browserSettings.Process = Process.Start(new ProcessStartInfo
2391 {
2392 FileName = browserPath,
2393 Arguments = string.Join(" ", chromeArguments),
2394 UseShellExecute = false,
2395 // chrome and edge announce "DevTools listening on ws://..." and a stream of their own
2396 // internal ERROR lines on stderr. left alone the child inherits this console and writes
2397 // them straight into the middle of the workflow's output
2398 RedirectStandardOutput = true,
2399 RedirectStandardError = true
2400 });
2401
2402 // the streams have to be drained or the browser stalls once the pipe buffer fills. none of it
2403 // is protocol here, the devtools connection is over the port, so it goes no further than
2404 // DEEPDEBUG for anyone who wants to read it
2405 browserSettings.Process.OutputDataReceived += (sender, data) => PublishBrowserOutput(browserSettings, data);
2406 browserSettings.Process.ErrorDataReceived += (sender, data) => PublishBrowserOutput(browserSettings, data);
2407 browserSettings.Process.BeginOutputReadLine();
2408 browserSettings.Process.BeginErrorReadLine();
2409
2410 // Capture the launcher PID immediately before it exits (Firefox only).
2411 if (BrowserType.FireFox == browserSettings.BrowserType)
2412 firefoxLauncherPid = browserSettings.Process?.Id ?? 0;
2413
2414 DateTime deadline = DateTime.UtcNow + timeout;
2415
2416 // Firefox launches through a process that exits once the browser is up, so the launched
2417 // process does not own the window. Run before the window wait and the resize below, both
2418 // of which need the process that does. ParentProcessId survives the parent exiting
2419 if (BrowserType.FireFox == browserSettings.BrowserType)
2420 AdoptFirefoxChild(browserSettings, firefoxLauncherPid, deadline);
2421
2422 // we have no window to check to ensure the browser launched in headless
2423 // so check the process flag
2424 // for windowed, wait for the window to open
2425 if (true == headless)
2426 {
2427 if (true == browserSettings.Process.HasExited)
2428 {
2429 string msg = $"[{browserSettings.BrowserType}] did not start. Exiting.";
2430 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg);
2431 throw new TimeoutException(msg);
2432 }
2433 }
2434 else
2435 {
2436 while (browserSettings.Process.MainWindowHandle == IntPtr.Zero && !browserSettings.Process.HasExited)
2437 {
2438 if (DateTime.UtcNow > deadline)
2439 {
2440 string msg = $"[{browserSettings.BrowserType}] did not start in [{secondsToWait}] seconds. Exiting.";
2441 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg);
2442 throw new TimeoutException(msg);
2443 }
2444
2445 Thread.Sleep(100); // prevents busy-wait CPU spike
2446 }
2447
2448 // the window exists now, so it goes where the workflow asked. this is the only placement
2449 // that works for every browser: the arguments above are chromium's, and firefox has no
2450 // command line way to be given a position at all. the arguments still earn their keep by
2451 // opening the window at the right size, so there is no jump to watch
2452 if (false == browserSettings.WindowSize.IsEmpty &&
2453 false == browserSettings.FullScreen &&
2454 false == browserSettings.Maximize &&
2455 false == browserSettings.Minimize)
2456 {
2457 browserSettings.Process.Refresh();
2458 WindowHelper.ResizeWindow(browserSettings.Process, browserSettings.WindowSize);
2459 }
2460 }
2461 }
2462
2463 if (true == browserSettings.UseOttoMagic)
2464 {
2465 try
2466 {
2467 var remoteEp = new IPEndPoint(IPAddress.Any, 0);
2468 int restPort = 0;
2469 int firstHeard = 0;
2470 int launchedPid = browserSettings.Process?.Id ?? 0;
2471
2472 // every browser carrying the extension announces a port, whatever engine is driving it, so
2473 // what arrives here is not necessarily this browser's. each announcement says which host
2474 // sent it, and a host is started by the browser hosting the extension, so an announcement
2475 // belongs to this launch when its host traces back to the process this launch started.
2476 // the others are somebody else's and are left for whoever is waiting on them
2477 while (0 == restPort)
2478 {
2479 byte[] data;
2480
2481 // nothing more is coming. an announcement that could not be traced is better than no
2482 // browser at all, so one that was heard is taken now, with the reason it is a guess
2483 try { data = udpHandshake.Receive(ref remoteEp); }
2484 catch (SocketException) when (0 != firstHeard)
2485 {
2486 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No GPALRestAPI traced back to this browser, so port [{firstHeard}] is being used on the strength of it being the first one announced", browserSettings, GPALObjectType.Browser);
2487
2488 restPort = firstHeard;
2489 break;
2490 }
2491
2492 string[] said = Encoding.UTF8.GetString(data).Trim().Split(' ');
2493
2494 // GPALRestAPI announces its port and its process id. a browser that never heard the port
2495 // has no way to reach GPALRestAPI, and everything after this would be asking a url that
2496 // was never worked out. it stops here instead, rather than carrying on and failing
2497 // somewhere that does not say why
2498 if (2 > said.Length || false == int.TryParse(said[0], out int announced) || false == int.TryParse(said[1], out int hostPid))
2499 throw new InvalidOperationException($"GPALRestAPI announced [{string.Join(" ", said)}], which is not a port and a process id");
2500
2501 if (0 == launchedPid || true == BrowserHelper.CameFromProcess(hostPid, launchedPid))
2502 restPort = announced;
2503 else
2504 {
2505 if (0 == firstHeard)
2506 firstHeard = announced;
2507
2508 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Port [{announced}] was announced by another browser's GPALRestAPI [{hostPid}], so this launch is still waiting for its own", browserSettings, GPALObjectType.Browser);
2509 }
2510 }
2511
2512 browserSettings.RestApiBaseUrl = $"http://localhost:{restPort}/";
2513 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"GPALRestAPI ready on port [{restPort}].", browserSettings, GPALObjectType.Browser);
2514 }
2515 catch (GPALException)
2516 {
2517 throw;
2518 }
2519 catch (Exception ex)
2520 {
2521 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"GPALRestAPI never announced a port, so this browser cannot reach it", browserSettings, GPALObjectType.Browser, ex);
2522 throw;
2523 }
2524 finally
2525 {
2526 udpHandshake?.Close();
2527
2528 // the port is read and the socket is free, so the next launch can go. Everything below here
2529 // is this browser's own business and does not touch the handshake
2530 if (true == holdingHandshakeGate)
2531 {
2532 ottoHandshakeGate.ReleaseMutex();
2533 holdingHandshakeGate = false;
2534 }
2535 }
2536
2537 // Firefox launches via a wrapper that exits immediately; the real process is a child.
2538 // Windows preserves ParentProcessId even after the parent exits, so we can query
2539 // for the direct firefox.exe child of the launcher PID we captured at start.
2540 if (firefoxLauncherPid > 0 &&
2541 (browserSettings.Process == null || browserSettings.Process.HasExited))
2542 {
2543 try
2544 {
2545 using (var searcher = new ManagementObjectSearcher(
2546 $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {firefoxLauncherPid} AND Name = 'firefox.exe'"))
2547 {
2548 foreach (ManagementObject obj in searcher.Get())
2549 {
2550 int childPid = Convert.ToInt32(obj["ProcessId"]);
2551 try { browserSettings.Process = System.Diagnostics.Process.GetProcessById(childPid); break; }
2552 catch { }
2553 }
2554 }
2555 }
2556 catch { }
2557 }
2558 }
2559 else if (true == browserSettings.UsePuppeteer)
2560 {
2561 _ = browserSettings.PuppeteerCommunicator; // initialize before we start using
2562 }
2563
2564 // Handle maximize/minimize
2565 if (browserSettings.Maximize)
2566 {
2567 try
2568 {
2569 if (true == browserSettings.UseOttoMagic)
2570 // Use maximize window via extension - the browser we just launched, not whoever's helper was called
2571 browser.MagicHelper.Client.Maximize().Execute();
2572 else if (true == browserSettings.UsePuppeteer)
2573 browserSettings.PuppeteerCommunicator.Maximize().GetAwaiter().GetResult();
2574 }
2575 catch (GPALException)
2576 {
2577 throw;
2578 }
2579 catch (Exception ex)
2580 {
2581 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to maximize browser window", browserSettings, GPALObjectType.Other, ex);
2582 }
2583 }
2584 else if (browserSettings.Minimize)
2585 {
2586 try
2587 {
2588 if (true == browserSettings.UseOttoMagic)
2589 browser.MagicHelper.Client.Minimize().Execute();
2590 else if (true == browserSettings.UsePuppeteer)
2591 browserSettings.PuppeteerCommunicator.Minimize().GetAwaiter().GetResult();
2592 }
2593 catch (GPALException)
2594 {
2595 throw;
2596 }
2597 catch (Exception ex)
2598 {
2599 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to minimize browser window", browserSettings, GPALObjectType.Other, ex);
2600 }
2601 }
2602 else if (browserSettings.FullScreen)
2603 {
2604 try
2605 {
2606 if (true == browserSettings.UseOttoMagic)
2607 browser.MagicHelper.Client.FullScreen().Execute();
2608 else if (true == browserSettings.UsePuppeteer)
2609 browserSettings.PuppeteerCommunicator.FullScreen().GetAwaiter().GetResult();
2610 }
2611 catch (GPALException)
2612 {
2613 throw;
2614 }
2615 catch (Exception ex)
2616 {
2617 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to full screen browser window", browserSettings, GPALObjectType.Other, ex);
2618 }
2619 }
2620
2621 // Executable, process id and port on one line, which is what distinguishes a browser this run
2622 // started from one it attached to
2623 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Browser [{browserSettings.BrowserType}] is process [{browserSettings.Process?.Id}] from [{browserPath}] on port [{browserSettings.DebugPort}] at [{browserSettings.PuppeteerUrl}]", browserSettings, GPALObjectType.Other);
2624
2625 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Browser [{browserSettings.BrowserType}] launched.", browserSettings, GPALObjectType.Other);
2626 }
2627 catch (GPALException)
2628 {
2629 throw;
2630 }
2631 catch (Exception ex)
2632 {
2633 // said, then handed to whoever asked for the browser. it does not end the application here: a
2634 // scheduler running ten workflows loses the other nine, and a form loses itself, over one browser
2635 // that would not start. Nothing catches it, the uncaught handler still cleans up and exits
2636 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to launch browser [{browserSettings.BrowserType}] for URL [{URL}]", browserSettings, GPALObjectType.Other, ex);
2637 throw;
2638 }
2639 finally
2640 {
2641 // a launch that failed before it ever read a port still has to let the next one in, and it has to
2642 // give up the socket as well. Holding 47623 past a failed launch is not a slow browser, it is the
2643 // next OttoMagic launch in this application failing the moment it asks for the socket
2644 udpHandshake?.Close();
2645
2646 if (true == holdingHandshakeGate)
2647 ottoHandshakeGate.ReleaseMutex();
2648 }
2649
2650 return browserSettings.Process;
2651 }
2652
2659 public bool TopBrowser(Process process) => WindowHelper.TopBrowser(process);
2660
2665 public static string GetRandomStarWarsQuery()
2666 {
2667 IRESTClient restClient = GPAL.RESTClient.WithAPIBase("https://swapi.info/api/").ToGPALObject();
2668
2669 var endpoints = new[]
2670 {
2671 ("people", 83, "name"), // e.g., "Yoda"
2672 ("planets", 60, "name"), // e.g., "Alderaan"
2673 ("films", 6, "title"), // e.g., "A New Hope"
2674 ("species", 37, "name"), // e.g., "Wookiee"
2675 ("vehicles", 39, "name"), // e.g., "X-wing"
2676 ("starships", 36, "name") // e.g., "Millennium Falcon"
2677 };
2678
2679 string FetchQuery(string endpoint, int maxId, string field, int retries = 3)
2680 {
2681 for (int attempt = 0; attempt < retries; attempt++)
2682 {
2683 int id = new Random().Next(1, maxId + 1);
2684 var response = restClient.WithEndpoint($"{endpoint}/{id}").Execute<SwapiResponse>(false);
2685 if (response != null)
2686 {
2687 return field == "name" ? response.name : response.title;
2688 }
2689 Thread.Sleep(100 * (int)Math.Pow(2, attempt)); // Backoff: 100ms, 200ms, 400ms
2690 }
2691 return null; // Let caller handle failure
2692 }
2693
2694 // Select random primary endpoint
2695 var (endpoint, maxId, field) = endpoints[new Random().Next(endpoints.Length)];
2696 string query = FetchQuery(endpoint, maxId, field);
2697
2698 // Fallback to another endpoint if null
2699 if (query == null)
2700 {
2701 var fallbackEndpoint = endpoints[new Random().Next(endpoints.Length)];
2702 query = FetchQuery(fallbackEndpoint.Item1, fallbackEndpoint.Item2, fallbackEndpoint.Item3);
2703 if (query == null) // Last resort, pick another
2704 {
2705 fallbackEndpoint = endpoints[new Random().Next(endpoints.Length)];
2706 query = FetchQuery(fallbackEndpoint.Item1, fallbackEndpoint.Item2, fallbackEndpoint.Item3) ?? "star wars"; // Absolute last fallback
2707 }
2708 }
2709
2710 if (new Random().NextDouble() > 0.4) // 60% chance to add second term
2711 {
2712 var (otherEndpoint, otherMaxId, otherField) = endpoints[new Random().Next(endpoints.Length)];
2713 string otherQuery = FetchQuery(otherEndpoint, otherMaxId, otherField);
2714 if (otherQuery != null)
2715 {
2716 query += $" {otherQuery}"; // e.g., "Yoda Millennium Falcon"
2717 }
2718 else
2719 {
2720 var fallbackEndpoint = endpoints[new Random().Next(endpoints.Length)];
2721 otherQuery = FetchQuery(fallbackEndpoint.Item1, fallbackEndpoint.Item2, fallbackEndpoint.Item3);
2722 query += otherQuery != null ? $" {otherQuery}" : ""; // Skip if still null
2723 }
2724 }
2725
2726 return query;
2727 }
2728
2729 private class SwapiResponse
2730 {
2731 public string name { get; set; }
2732 public string title { get; set; }
2733 }
2734
2735 public static string GetFullUrl(string url, IBrowser browser, out bool areRobotsAllowed, bool noRobotsCheck = false)
2736 {
2737 List<string> robotsTxt = new List<string>();
2738 string normalized = url;
2739 areRobotsAllowed = true;
2740
2741 try
2742 {
2743 // Default to HTTPS only if no scheme supplied
2744 if (!Uri.TryCreate(url, UriKind.Absolute, out _))
2745 {
2746 normalized = "https://" + url;
2747 }
2748
2749 // files the browser merely opens or downloads (sitemap.xml, pdfs, images...) are not pages, skip the checks
2750 if (false == noRobotsCheck && true == ((Browser)browser).BrowserSettings.ObeyRobotsTxt && false == UrlHelper.IsNonHtmlResource(normalized))
2751 areRobotsAllowed = UrlHelper.CheckRobotsTxt(normalized, browser, robotsTxt);
2752
2753 if (true == url.StartsWith("https://"))
2754 return url;
2755
2756 if (string.IsNullOrWhiteSpace(url) || UrlHelper.AreEquivalent(url, "https://google.com"))
2757 return "https://google.com";
2758
2759 }
2760 catch (GPALException)
2761 {
2762 throw;
2763 }
2764 catch (Exception ex)
2765 {
2766 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error getting full url", browser, GPALObjectType.Other, ex);
2767 }
2768
2769 return normalized;
2770 }
2771
2777 public static int CalculateHash(Dictionary<string, object> attributes)
2778 {
2779 // 1. Collect and format attributes into a list of strings.
2780 // We use LINQ's OrderBy to sort the dictionary entries by key
2781 // before converting them to strings, ensuring consistent order.
2782 var attrList = attributes.OrderBy(kvp => kvp.Key)
2783 .Select(kvp => $"{kvp.Key}={kvp.Value}")
2784 .ToList();
2785
2786 // 2. Join the sorted attributes into a single string.
2787 string str = string.Join("|", attrList);
2788
2789 // 3. Implement the custom hashing algorithm.
2790 int hash = 0;
2791 foreach (char c in str)
2792 {
2793 // The bitwise operations from the JS code are translated to C#
2794 hash = ((hash << 5) - hash + c) | 0;
2795 }
2796
2797 return hash;
2798 }
2799
2807 public static string GetUserAgentString(BrowserType browserType)
2808 {
2809 // construct headless user-agent
2810 string chromeVersion;
2811 string edgeVersion;
2812 string firefoxVersion;
2813 string retval = null;
2814
2815 if (BrowserType.Chrome == browserType)
2816 {
2817 DriverHelper.GetChromeVersion(out chromeVersion);
2818 // a dotless or empty version means detection failed; slicing to IndexOf(-1) would throw
2819 if (null != chromeVersion && 0 < chromeVersion.IndexOf('.'))
2820 chromeVersion = chromeVersion.AsSpan().Slice(0, chromeVersion.IndexOf('.')).ToString(); // NOTE: fastest way to get the first token
2821 retval = String.Format(GPAL.GPALSettings.ChromeUserAgentTemplate, chromeVersion);
2822 }
2823 else if (BrowserType.Edge == browserType)
2824 {
2825 DriverHelper.GetEdgeVersion(out edgeVersion);
2826 // a dotless or empty version means detection failed; slicing to IndexOf(-1) would throw
2827 if (null != edgeVersion && 0 < edgeVersion.IndexOf('.'))
2828 edgeVersion = edgeVersion.AsSpan().Slice(0, edgeVersion.IndexOf('.')).ToString(); // NOTE: fastest way to get the first token
2829 retval = String.Format(GPAL.GPALSettings.EdgeUserAgentTemplate, edgeVersion);
2830 }
2831 else if (BrowserType.FireFox == browserType)
2832 {
2833 DriverHelper.GetFirefoxVersion(out firefoxVersion);
2834 // a dotless or empty version means detection failed; slicing to IndexOf(-1) would throw
2835 if (null != firefoxVersion && 0 < firefoxVersion.IndexOf('.'))
2836 firefoxVersion = firefoxVersion.AsSpan().Slice(0, firefoxVersion.IndexOf('.')).ToString(); // NOTE: fastest way to get the first token
2837 retval = String.Format(GPAL.GPALSettings.FirefoxUserAgentTemplate, firefoxVersion);
2838 }
2839
2840 return retval;
2841 }
2842
2851 private static void KeepFromChildren(UdpClient client)
2852 {
2853 if (false == Native.SetHandleInformation(client.Client.Handle, Native.HANDLE_FLAG_INHERIT, 0))
2854 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
2855 $"Could not take the inherit flag off the handshake socket, so the next OttoMagic launch may find [47623] still held",
2856 null, GPALObjectType.Browser, new Win32Exception(Marshal.GetLastWin32Error()));
2857 }
2858
2859 internal string GetUserAgent(BrowserSettings browserSettings, List<string> chromeArguments)
2860 {
2861 string userAgent = null;
2862 string browserPath = null;
2863
2864 // starting a whole browser to ask it its own name is the accurate answer and the expensive one, so it
2865 // happens only when the workflow asked for it. Otherwise what is already known is good enough, and it
2866 // is the same string GPAL's own requests present
2867 if (false == browserSettings.UserAgentFromBrowser)
2868 return BrowserHelper.ResolveUserAgent(browserSettings);
2869
2870 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Getting user agent for headless use.", chromeArguments, GPALObjectType.Other);
2871
2872 // Get browser executable path
2873 switch (browserSettings.BrowserType)
2874 {
2875 case BrowserType.Chrome:
2876 browserPath = Puppeteer.GetChromePath();
2877 break;
2878 case BrowserType.Edge:
2879 browserPath = Puppeteer.GetEdgePath();
2880 break;
2881 case BrowserType.FireFox:
2882 browserPath = Puppeteer.GetFirefoxPath();
2883 break;
2884 default:
2885 Exception ex = new Exception("Unsupported Browser Type");
2886 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unsupported browser type [{browserSettings.BrowserType}]", browser, GPALObjectType.Browser, ex);
2887 return GetUserAgentString(browserSettings.BrowserType);
2888 }
2889
2890 UdpClient probeHandshake = null;
2891 bool holdingProbeGate = false;
2892
2893 try
2894 {
2895 List<string> ourArgs = new List<string>(chromeArguments);
2896
2897 // same reason as the launch above: every browser gets its own port or they share a session
2898 if (null == browserSettings.DebugPort)
2899 browserSettings.DebugPort = BrowserHelper.FindFreePort();
2900
2901 if (BrowserType.FireFox != browserSettings.BrowserType)
2902 ourArgs.Add($"--remote-debugging-port={browserSettings.DebugPort.Value}");
2903 else
2904 {
2905 // firefox is asked over the rest api, so listen before starting it: the browser announces the
2906 // port it picked the way every OttoMagic browser does, and nothing else is listening this early
2907 // in a launch. Without it the question goes to whichever browser happens to already be up
2908 try { holdingProbeGate = ottoHandshakeGate.WaitOne(OttoHandshakeGateTimeoutMs); }
2909 catch (AbandonedMutexException) { holdingProbeGate = true; }
2910
2911 probeHandshake = new UdpClient(47623);
2912 probeHandshake.Client.ReceiveTimeout = 30000;
2913
2914 KeepFromChildren(probeHandshake);
2915 }
2916
2917 browserSettings.Process = Process.Start(new ProcessStartInfo
2918 {
2919 FileName = browserPath,
2920 Arguments = string.Join(" ", ourArgs),
2921 UseShellExecute = false
2922 });
2923
2924 // Thread.Sleep(3000);
2925
2926 // NOTE: always use puppeteer to get the useragent, it's very reliable, except ff which doesn't use puppeteer
2927 if (BrowserType.FireFox == browserSettings.BrowserType)
2928 {
2929 var probeEndPoint = new IPEndPoint(IPAddress.Any, 0);
2930 string probePortText = Encoding.UTF8.GetString(probeHandshake.Receive(ref probeEndPoint)).Trim();
2931
2932 // ask the browser we just launched, on the port it just announced
2933 userAgent = true == int.TryParse(probePortText, out int probePort)
2934 ? ((IRESTClient)GPAL.RESTClient.WithAPIBase($"http://localhost:{probePort}/").ToGPALObject()).GetUserAgent().Execute()
2935 : Client.GetUserAgent().Execute();
2936 }
2937 else
2938 {
2939 AutomationEngine aeSave = browserSettings.AutomationEngine;
2940 browserSettings.AutomationEngine = AutomationEngine.PuppeteerPort;
2941 _ = browserSettings.PuppeteerCommunicator;
2942 userAgent = browserSettings.PuppeteerClient.GetUserAgent().Execute();
2943 browserSettings.AutomationEngine = aeSave;
2944 }
2945 userAgent = userAgent.Replace("Headless", "");
2946
2947 //if (true == browserSettings.UsePuppeteer)
2948 {
2949 var parameters = new Dictionary<string, object>();
2950 // gracefully close the browser, release resources, shutdown chrome
2951 //browserSettings.PuppeteerCommunicator.SendCommand(DevToolsMethods.BrowserClose, parameters, null).GetAwaiter().GetResult();
2952 browserSettings.PuppeteerCommunicator._readerReadyTcs?.SetCanceled();
2953 if (false == browserSettings.PuppeteerCommunicator._usePipes)
2954 browserSettings.PuppeteerCommunicator.CloseOutputWebSocketAsync().GetAwaiter();
2955 }
2956
2957 // NOTE: KLUDGE: might not be required but we are, at least for puppeteer getting an error relaunching chrome after getting the useragent
2958 // - we added puppeteer browserclose rather than a hard kill
2959 // Lock file can not be created! Error code: 32
2960 // Failed to create a ProcessSingleton for your profile directory. This means that running multiple instances would start multiple browser processes rather than opening a new window in the existing process. Aborting now to avoid profile corruption.
2961
2962 // kill the opened chrome and any children used to get the agent string
2963 try
2964 {
2965 var psi = new ProcessStartInfo
2966 {
2967 FileName = "taskkill",
2968 Arguments = $"/PID {browserSettings.Process.Id} /F /T",
2969 CreateNoWindow = true,
2970 UseShellExecute = false
2971 };
2972 Process.Start(psi).WaitForExit();
2973 }
2974 catch { }
2975
2976 Thread.Sleep(1_000);
2977
2978 if (null != browserSettings.Process && false == browserSettings.Process.HasExited)
2979 {
2980 try
2981 {
2982 browserSettings.Process.Kill();
2983 browserSettings.Process.Close();
2984 }
2985 catch { }
2986 }
2987
2988 Thread.Sleep(1_000);
2989
2990 // GPALRestAPI is not killed here: it shuts itself down when the extension's port closes, and killing
2991 // it by name would take down every other browser's rest api too
2992 browserSettings.PuppeteerClient = null;
2993 browserSettings.PuppeteerCommunicator = null;
2994 browserSettings.Process = null;
2995
2996 }
2997 catch (GPALException)
2998 {
2999 throw;
3000 }
3001 catch (Exception ex)
3002 {
3003 userAgent = GetUserAgentString(browserSettings.BrowserType);
3004 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to fetch user-agent using [{userAgent}].", browserSettings, GPALObjectType.Other, ex);
3005 }
3006 finally
3007 {
3008 probeHandshake?.Close();
3009
3010 // the throwaway browser is gone, so the socket and the gate go back before the real launch asks for them
3011 if (true == holdingProbeGate)
3012 ottoHandshakeGate.ReleaseMutex();
3013 }
3014
3015 return false == string.IsNullOrEmpty(userAgent) ? userAgent : GetUserAgentString(browserSettings.BrowserType);
3016 }
3017 #endregion Helpers
3018 }
3019
3020 public class WindowHelper
3021 {
3022 [DllImport("user32.dll")]
3023 [return: MarshalAs(UnmanagedType.Bool)]
3024 private static extern bool SetForegroundWindow(IntPtr hWnd);
3025
3026 [DllImport("user32.dll")]
3027 [return: MarshalAs(UnmanagedType.Bool)]
3028 private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
3029
3030 [DllImport("user32.dll")]
3031 [return: MarshalAs(UnmanagedType.Bool)]
3032 private static extern bool IsIconic(IntPtr hWnd);
3033
3034 [DllImport("user32.dll")]
3035 private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
3036
3037 [DllImport("user32.dll")]
3038 private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
3039
3040 [DllImport("user32.dll")]
3041 private static extern bool IsWindowVisible(IntPtr hWnd);
3042
3043 [DllImport("user32.dll")]
3044 private static extern int GetWindowTextLength(IntPtr hWnd);
3045
3046 private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
3047 private const int SW_RESTORE = 9;
3048
3057 public static bool TopBrowser(Process process, int processId = 0)
3058 {
3059 if ((null == process || true == process?.HasExited) && 0 == processId)
3060 {
3061 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to top browser. Browser has exited.", process, GPALObjectType.Other);
3062 return false;
3063 }
3064
3065 IntPtr windowHandle = IntPtr.Zero;
3066
3067 // 1. If a process ID is explicitly passed, scan the OS for its visible window
3068 if (processId > 0)
3069 {
3070 windowHandle = FindWindowByProcessId(processId);
3071 }
3072
3073 // 2. Fallback to your original logic if the PID scan failed or wasn't requested
3074 if (IntPtr.Zero == windowHandle)
3075 {
3076 // Fixed: Removed process.Handle fallback because process handles cannot be passed to ShowWindow/SetForegroundWindow
3077 windowHandle = process.MainWindowHandle;
3078 }
3079
3080 if (IntPtr.Zero == windowHandle)
3081 {
3082 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to top browser. window handle is IntPtr.Zero", process, GPALObjectType.Other);
3083 return false;
3084 }
3085
3086 // only a minimized window needs restoring. SW_RESTORE on a maximized one un-maximizes it back to
3087 // whatever size it was before, so topping the browser was quietly undoing .Maximize - it maximized,
3088 // then the first navigation topped it and it came back to its startup size
3089 if (true == IsIconic(windowHandle))
3090 ShowWindow(windowHandle, SW_RESTORE);
3091
3092 bool success = SetForegroundWindow(windowHandle);
3093 if (false == success)
3094 {
3095 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to top browser. SetForegroundWindow returned false.", process, GPALObjectType.Other);
3096 }
3097
3098 return success;
3099 }
3100
3101 private static IntPtr FindWindowByProcessId(int processId)
3102 {
3103 IntPtr foundHandle = IntPtr.Zero;
3104
3105 // The visible browser window may belong to the target process OR a descendant of it. Firefox is the
3106 // classic case: geckodriver (the pid we are handed) spawns firefox.exe as a child, and the window
3107 // belongs to that child - a direct pid match finds nothing. So match any visible top-level window
3108 // whose owning pid is the target or a descendant. Prefer a titled window (the main browser window
3109 // has a title; content/helper windows generally do not) so we do not grab a stray popup.
3110 HashSet<int> targetPids = GetProcessAndDescendantIds(processId);
3111
3112 EnumWindows(delegate (IntPtr hWnd, IntPtr lParam)
3113 {
3114 if (IsWindowVisible(hWnd))
3115 {
3116 GetWindowThreadProcessId(hWnd, out uint windowPid);
3117 if (targetPids.Contains((int)windowPid))
3118 {
3119 foundHandle = hWnd; // remember the last match, but keep looking for a titled one
3120 if (0 < GetWindowTextLength(hWnd))
3121 return false; // titled window - this is the main browser window, stop
3122 }
3123 }
3124 return true; // Keep looking
3125 }, IntPtr.Zero);
3126
3127 return foundHandle;
3128 }
3129
3136 private static HashSet<int> GetProcessAndDescendantIds(int rootPid)
3137 {
3138 var result = new HashSet<int> { rootPid };
3139
3140 try
3141 {
3142 var frontier = new Queue<int>();
3143 frontier.Enqueue(rootPid);
3144
3145 while (0 < frontier.Count)
3146 {
3147 int parent = frontier.Dequeue();
3148 using (var searcher = new ManagementObjectSearcher(
3149 $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {parent}"))
3150 {
3151 foreach (ManagementObject obj in searcher.Get())
3152 {
3153 int childPid = Convert.ToInt32(obj["ProcessId"]);
3154 if (result.Add(childPid))
3155 frontier.Enqueue(childPid);
3156 }
3157 }
3158 }
3159 }
3160 catch { /* WMI unavailable - fall back to just the root pid */ }
3161
3162 return result;
3163 }
3164
3165
3166 // Define the Windows API function
3167 [DllImport("user32.dll", SetLastError = true)]
3168 private static extern bool SetWindowPos(
3169 IntPtr hWnd,
3170 IntPtr hWndInsertAfter,
3171 int X,
3172 int Y,
3173 int cx,
3174 int cy,
3175 uint uFlags);
3176
3177 // Define the Windows API constant for getting the window's current position and size
3178 [DllImport("user32.dll")]
3179 private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
3180
3181 // Define the RECT structure for GetWindowRect
3182 [StructLayout(LayoutKind.Sequential)]
3183 private struct RECT
3184 {
3185 public int Left;
3186 public int Top;
3187 public int Right;
3188 public int Bottom;
3189 }
3190
3191 private const uint SWP_NOMOVE = 0x0002;
3192 private const uint SWP_NOSIZE = 0x0001;
3193 private const uint SWP_NOZORDER = 0x0004;
3194
3203 public static void ResizeWindow(
3204 Process process,
3205 Rectangle inRect)
3206 {
3207 // Check if the process has a main window handle
3208 if (process.MainWindowHandle == IntPtr.Zero)
3209 {
3210 return;
3211 }
3212
3213 int maxWwidth = Puppeteer.GetScreenWidthInt();
3214 int maxHeight = Puppeteer.GetScreenHeightInt();
3215
3216 if (inRect.Width > maxWwidth)
3217 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Desired browser width [{inRect.Width}] is greater than the screen width [{maxWwidth}]");
3218
3219 if (inRect.Height > maxHeight)
3220 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Desired browser height [{inRect.Height}] is greater than the screen height [{maxHeight}]");
3221
3222 // Determine the flags for SetWindowPos based on null parameters
3223 uint uFlags = SWP_NOZORDER;
3224
3225 // Call the Windows API function
3226 SetWindowPos(
3227 process.MainWindowHandle,
3228 IntPtr.Zero, // No change to z-order
3229 inRect.Left,
3230 inRect.Top,
3231 inRect.Width,
3232 inRect.Height,
3233 uFlags);
3234 }
3235 }
3236}
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Definition Browser.cs:68
Represents a URL with optional pre-navigation storage cleanup / inspection actions....
Definition GPALUrl.cs:53
IAllowGPALUrlStorageType ForUrl(string url)
Changes (or sets) the target URL for this builder instance.
Definition GPALUrl.cs:106
string Url
Gets the target URL string.
Definition GPALUrl.cs:69
void SetGoogleReferrer()
Overrides referrer stealthily browser.MagicHelper.SetGoogleReferrer();.
bool IsEnabled(string elementId)
Checks if an element is enabled NOTE: when called from GPAL, we will always provide one element or se...
void Hover(string elementId)
Hovers over an element by ID browser.MagicHelper.Hover("element123");.
void MoveTo(string elementId)
Scrolls an element into view by ID browser.MagicHelper.ScrollIntoView("element123");.
void ScrollIntoView(string elementId)
Scrolls an element into view by ID browser.MagicHelper.ScrollIntoView("element123");.
bool ElementFromPoint(string elementId, int x, int y)
Gets the bounding client rect of an element by ID string rect = browser.MagicHelper....
bool IsVisibleInViewport(string elementId)
Checks if an element is visible in viewport by ID bool visible = browser.MagicHelper....
string GetBrowserSettings()
Gets browser settings string settings = browser.MagicHelper.GetBrowserSettings();.
void SwitchToElement(string elementId)
Switches context to an element browser.MagicHelper.InElement();NOTE: InFrame and InElement are identi...
GPALElement GetParentNode(string elementId)
Gets the parent node of an element by ID string parent = browser.MagicHelper.GetParentNode("element12...
string GetHydrationScripts()
The page title, the NEXT_DATA payload and any React Flight chunks, as json. Read from the document r...
void ScrollWindow(int hPixels, int vPixels)
Scrolls the window by horizontal and vertical pixels browser.MagicHelper.ScrollWindow(100,...
List< GPALElement > GetOptions(string elementId)
Get select menu option items.
void SwitchToShadowRoot(string elementId)
Switches to a shadow root by element ID browser.MagicHelper.SwitchToShadowRoot("shadow123");.
TabTuple CloseTab(dynamic URLorTabId=null)
Closes a tab by URL or tab ID TabTuple closed = browser.MagicHelper.CloseTab("https://example....
GPALElement QuerySelector(string css)
Queries a single element using a CSS selector string elementId = browser.MagicHelper....
GPALElement QueryPersistentSelector(string css)
Queries a single element using a CSS selector string elementId = browser.MagicHelper....
void ClearReferrer()
Clears the referrer header browser.MagicHelper.ClearReferrer();.
int WindowPageOffsetX()
Gets the window page offset X int offset = browser.MagicHelper.WindowPageOffsetX();.
string GetStorage(WebsiteStorageType storageType, string domain=null, string path=null, string key=null, string storeName=null)
Get the type of storage type specified, if any parameter is missing, then that is a wildcard to get a...
string ExecuteJavaScript(string script)
Runs a script once, on the page that is open, and hands back what it returned. Where InjectScript reg...
void LeftClick(string elementId)
Performs a left click on an element by ID browser.MagicHelper.LeftClick("element123");.
bool TrustedLeftClick(int x, int y, int holdMs=0)
Clicks a viewport point through the browser's own input pipeline, so the page reads the event as trus...
string GetSettings()
Gets general settings string settings = browser.MagicHelper.GetSettings();.
GPALElement EvaluatePersistent(string xpath)
Evaluates an XPath expression to locate a single element string elementId = browser....
void SetUserAgent(string userAgent)
Overrides the browser's user agent string browser.MagicHelper.SetUserAgent("Mozilla/5....
string GetReadyStatus()
Gets the ready status NOTE: if no page is loaded, this will return null cause there is no content scr...
TabTuple GotoTab(dynamic URLorTabId)
Navigates to a tab by URL or tab ID TabTuple tab = browser.MagicHelper.GotoTab("https://example....
int WindowOuterHeight()
Gets the window outer height int height = browser.MagicHelper.WindowOuterHeight();.
void Back()
Navigates browser history back browser.MagicHelper.Back();.
void SendKey(byte vkcode)
Sends a single key browser.MagicHelper.SendKey("Enter");.
string Fetch(string url, string method=null, string body=null, string contentType=null, string[] headers=null, bool asBytes=false)
Issues an API request from inside the page, so it carries the session the browser has already earned:...
WindowTuple PreviousWindow()
Switches to the previous window WindowTuple window = browser.MagicHelper.PreviousWindow();.
void LeftClickAndUpload(string elementId, GPALFile filenamesAndPaths)
Performs a left click and upload on an element by ID browser.MagicHelper.LeftClickAndUpload("link123"...
void Focus(string elementId)
Focuses an element by ID browser.MagicHelper.Focus("element123");.
void ReleaseModifierKey(ModifierKeys modifierKeys)
Press the specified modifier keys.
void Restore()
Restores the browser window browser.MagicHelper.Restore();.
int WindowInnerHeight()
Gets the window inner height int height = browser.MagicHelper.WindowInnerHeight();.
TabTuple PreviousTab()
Switches to the previous tab TabTuple tab = browser.MagicHelper.PreviousTab();.
bool TopBrowser(Process process)
Brings the browser window to the top bool success = browser.MagicHelper.TopBrowser(process);.
TabTuple NextTab()
Switches to the next tab TabTuple tab = browser.MagicHelper.NextTab();.
void ClearInjectedScripts()
Removes all scripts previously registered via InjectScript. browser.MagicHelper.ClearInjectedScripts(...
static string GetRandomStarWarsQuery()
Use the Star Wars API to generate random search terms for our google cookies we will create.
bool IsDisplayed(string elementId)
Checks if an element is visible, can also just check element.Displayed NOTE: when called from GPAL,...
void LeftDoubleClick(string elementId)
Performs a left double click on an element by ID browser.MagicHelper.LeftDoubleClick("element123");.
TabTuple GoTo(GPALUrl URL)
Navigates to a URL TabTuple tab = browser.MagicHelper.Goto("https://example.com");.
WindowTuple NextWindow()
Switches to the next window WindowTuple window = browser.MagicHelper.NextWindow();.
string GetUserAgent()
Gets the user agent string userAgent = browser.MagicHelper.GetUserAgent();.
void MiddleClick(string elementId)
Performs a left click on an element by ID browser.MagicHelper.LeftClick("element123");.
int WindowInnerWidth()
Gets the window inner width int width = browser.MagicHelper.WindowInnerWidth();.
string GetCurrentUrl()
Gets the current URL string url = browser.MagicHelper.GetCurrentUrl();.
string GetDocumentStamp()
The url and the document identity, as timeOrigin and url separated by a bar. A dedicated endpoint ra...
void LeftClickAndDownload(string elementId, string filenameAndPath)
Performs a left click and download on an element by ID browser.MagicHelper.LeftClickAndDownload("link...
void PageEnd()
Scrolls to the page end browser.MagicHelper.PageEnd();.
string GetElementAttributeHash(string elementId)
Gets the attribute hash of an element string hash = browser.MagicHelper.GetElementAttributeHash();.
void FillInOverwrite(string elementId, string text)
Append text to an element browser.MagicHelper.FillInOverwrite("input123", "Hello World");.
List< GPALElement > EvaluateAllPersistent(string xpath)
Evaluates an XPath expression to locate multiple elements string[] elementIds = browser....
void Maximize()
Maximizes the browser window browser.MagicHelper.Maximize();.
string CloseBrowser()
Closes every window the browser has, which is how an extension asks the browser hosting it to quit....
Rectangle GetWindowRectangle()
Returns the browser window inner/outer height/widge { y: window.innerHeight, x: window....
int WindowPageOffsetY()
Gets the window page offset Y int offset = browser.MagicHelper.WindowPageOffsetY();.
void HideElement(string elementId)
Hides an element by ID browser.MagicHelper.HideElement("element123");.
void Minimize()
Minimizes the browser window browser.MagicHelper.Minimize();.
string GetLanguages()
Gets the browser's preferred languages, most preferred first string languages = browser....
void FillInAppend(string elementId, string text)
Append text to an element browser.MagicHelper.FillInAppend("input123", "Hello World");.
void ScrollWindowHorizontal(int pixels)
Scrolls the window horizontally by pixels browser.MagicHelper.ScrollWindowByHorizontal(100);.
void Normal()
Restores the browser window to normal browser.MagicHelper.Normal();.
string GetAttribute(string elementId, string attribute)
Gets an attribute of an element by ID string value = browser.MagicHelper.GetAttribute("element123",...
GPALElement GetShadowRoot(string elementId)
Gets the shadow root for an elementId GPALElement root = browser.MagicHelper.GetShadowRoot(elementId)...
List< GPALElement > QuerySelectors(string css)
Queries multiple elements using a CSS selector string[] elementIds = browser.MagicHelper....
bool IsClickAble(string elementId)
Checks if an element is clickable by ID NOTE: when called from GPAL, we will always provide one eleme...
TabTuple NewTab(GPALUrl URL=null)
Opens a new tab with optional URL TabTuple tab = browser.MagicHelper.NewTab("https://example....
void FullScreen()
Sets browser to full screen browser.MagicHelper.FullScreen();.
int WindowScreenLeft()
Gets the window screen left position int left = browser.MagicHelper.WindowScreenLeft();.
void DragAndDrop(string elementId, int deltaX, int deltaY, int offsetX=0, int offsetY=0)
Drags an element by ID and drops it at an offset from its current position browser....
List< GPALElement > EvaluateAll(string xpath)
Evaluates an XPath expression to locate multiple elements string[] elementIds = browser....
bool TrustedMiddleClick(int x, int y, int holdMs=0)
Middle clicks a viewport point through the browser's own input pipeline, so the page reads the event ...
void CaptureCalls(bool capture=true, string urlFragment=null, bool clear=false)
Starts or stops recording what the page asks for. The extension watches with webRequest,...
void PageTop()
Scrolls to the page top browser.MagicHelper.PageTop();.
int WindowScreenTop()
Gets the window screen top position int top = browser.MagicHelper.WindowScreenTop();.
string GetCapturedCalls()
Everything recorded so far, oldest first. string json = browser.MagicHelper.GetCapturedCalls();.
void SwitchToDefaultContent()
Switches to the default content browser.MagicHelper.SwitchToDefaultContent();.
string GetContentAndCss(string elementId)
Gets content and CSS for a elementId string content = browser.MagicHelper.GetContentAndCss(elementId)...
void ScrollElement(string elementId, int hPixels, int vPixels)
Scrolls an element by horizontal and vertical pixels browser.MagicHelper.ScrollElement(100,...
bool IsEndOfPage()
Checks if the page is at the end bool end = browser.MagicHelper.IsEndOfPage();.
string CheckNetworkIdle(int? maxConnections=null, int? timeoutMs=null, int? pruneMs=null)
Reports whether the network is idle. Each parameter is optional and defaults to the browser settin...
int WindowOuterWidth()
Gets the window outer width int width = browser.MagicHelper.WindowOuterWidth();.
void FillInInsert(string elementId, string text)
Inserts text to an element (prolly the same as append for the moment) browser.MagicHelper....
void SendString(string text, int delayMs=0)
Sends a string of text browser.MagicHelper.SendString("Hello World");.
void ScrollWindowVertical(int pixels)
Scrolls the window vertically by pixels browser.MagicHelper.ScrollWindowByVertical(100);.
void Forward()
Navigates browser history forward browser.MagicHelper.Forward();.
void PageDown()
Scrolls the page down browser.MagicHelper.PageDown();.
void SetAttribute(string elementId, string attribute, string value)
Sets an attribute of an element by ID browser.MagicHelper.SetAttribute("element123",...
void SetValueFromElement(string srcSelector, string destElementId)
Sets the value of a destination element from a source element, by CSS selector browser....
string GetWorkflow()
Gets the workflow string workflow = browser.MagicHelper.GetWorkflow();.
void WithDownloadFile(string downloadPath)
Names the file the next download should land under. Only the leaf name is sent. The extension names ...
List< GPALElement > QueryPersistentSelectors(string css)
Queries multiple elements using a CSS selector string[] elementIds = browser.MagicHelper....
void PressModifierKey(ModifierKeys modifierKeys)
Press the specified modifier keys.
GPALElement Evaluate(string xpath)
Evaluates an XPath expression to locate a single element string elementId = browser....
void OverrideReferrer(string referrer)
Overrides the referrer URL browser.MagicHelper.OverrideReferrer("https://google.com");.
void LeftClickAndUpload(string elementId, string filenameAndPath)
Performs a left click and upload on an element by ID browser.MagicHelper.LeftClickAndUpload("link123"...
static string GetUserAgentString(BrowserType browserType)
Get chrome or edge useragent string based upon what is installed on this system This looks up the ver...
void Refresh()
Refreshes the current page browser.MagicHelper.Refresh();.
void FireChangeEvent(string elementId)
Fires a change event on an element by ID browser.MagicHelper.FireChangeEvent("element123");.
int TabCount()
How many tabs the browser actually has open, which is not the same as how many GPAL opened....
void RightClick(string elementId)
Performs a right click on an element by ID browser.MagicHelper.RightClick("element123");.
void PageUp()
Scrolls the page up browser.MagicHelper.PageUp();.
void InjectScript(string script)
Registers a script to run on every new document load. Persists until ClearInjectedScripts is called....
string GetPageSource()
Get the html source of the current page.
string Get(GPALUrl URL)
Navigates to a URL string result = browser.MagicHelper.Get("https://example.com");.
void InFrame(string elementId)
Switches context to iframe browser.MagicHelper.InFrame();NOTE: InFrame and InElement are identical in...
string GetGpalSettings()
Gets GPAL settings string settings = browser.MagicHelper.GetGpalSettings();.
static int CalculateHash(Dictionary< string, object > attributes)
Calculates a consistent integer hash from a dictionary of attributes.
bool DeleteStorage(WebsiteStorageType storageType, bool deleteAcrossOrigins=false, string domain=null, string path=null, string key=null, string storeName=null)
Delete the type of storage type specified, if any parameter is missing, then that is a wildcard to de...
static void ResizeWindow(Process process, Rectangle inRect)
Resizes and repositions a process's main window using optional parameters.
static bool TopBrowser(Process process, int processId=0)
Brings the browser window to the foreground. Not useful in headlesss or OttoMagic mode where we are t...
Pseudo element used in Applications and Browser workflows for image matching and unified automation....
ISearchContext GetShadowRoot()
Returns the ShadowRoot if available.
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowRESTEndpoint RESTClient
Instantiate a new fluent RESTClient.
Definition GPAL.cs:950
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568
Fluent REST client for making API calls with a chained interface. Supports defining workflows,...
Definition RESTClient.cs:56