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;
40using static OpenCvSharp.XImgProc.CvXImgProc;
41
43{
44 public static class Native
45 {
46 // Constants
47 public const uint DUPLICATE_SAME_ACCESS = 0x00000002;
48 public const uint HANDLE_FLAG_INHERIT = 0x00000001;
49 public const uint STARTF_USESTDHANDLES = 0x00000100;
50 public const uint STARTF_USESHOWWINDOW = 0x00000001;
51 public const uint CREATE_NO_WINDOW = 0x08000000;
52 public const uint STILL_ACTIVE = 259;
53 public const ushort SW_HIDE = 0;
54 public const ushort SW_SHOWNORMAL = 1;
55
56 // Proc/thread attribute constant (if using STARTUPINFOEX)
57 public const int PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002;
58
59 // SECURITY_ATTRIBUTES
60 [StructLayout(LayoutKind.Sequential)]
61 public struct SECURITY_ATTRIBUTES
62 {
63 public int nLength;
64 public IntPtr lpSecurityDescriptor;
65 public int bInheritHandle; // BOOL as int
66 }
67
68 // STARTUPINFO (standard)
69 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
70 public struct STARTUPINFO
71 {
72 public uint cb;
73 public string lpReserved;
74 public string lpDesktop;
75 public string lpTitle;
76 public uint dwX;
77 public uint dwY;
78 public uint dwXSize;
79 public uint dwYSize;
80 public uint dwXCountChars;
81 public uint dwYCountChars;
82 public uint dwFillAttribute;
83 public uint dwFlags;
84 public ushort wShowWindow;
85 public ushort cbReserved2;
86 public IntPtr lpReserved2;
87 public IntPtr hStdInput;
88 public IntPtr hStdOutput;
89 public IntPtr hStdError;
90 }
91
92 // Extended STARTUPINFO for attribute list
93 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
94 public struct STARTUPINFOEX
95 {
96 public STARTUPINFO StartupInfo;
97 public IntPtr lpAttributeList;
98 }
99
100 // PROCESS_INFORMATION
101 [StructLayout(LayoutKind.Sequential)]
103 {
104 public IntPtr hProcess;
105 public IntPtr hThread;
106 public uint dwProcessId;
107 public uint dwThreadId;
108 }
109
110 // P/Invokes
111
112 [DllImport("kernel32.dll", SetLastError = true)]
113 public static extern bool CreatePipe(
114 out IntPtr hReadPipe,
115 out IntPtr hWritePipe,
116 ref SECURITY_ATTRIBUTES lpPipeAttributes,
117 uint nSize);
118
119 [DllImport("kernel32.dll", SetLastError = true)]
120 public static extern bool SetHandleInformation(
121 IntPtr hObject,
122 uint dwMask,
123 uint dwFlags);
124
125 [DllImport("kernel32.dll", SetLastError = true)]
126 public static extern bool GetHandleInformation(
127 IntPtr hObject,
128 out uint lpdwFlags);
129
130 [DllImport("kernel32.dll", SetLastError = true)]
131 public static extern bool DuplicateHandle(
132 IntPtr hSourceProcessHandle,
133 IntPtr hSourceHandle,
134 IntPtr hTargetProcessHandle,
135 out IntPtr lpTargetHandle,
136 uint dwDesiredAccess,
137 bool bInheritHandle,
138 uint dwOptions);
139
140 [DllImport("kernel32.dll")]
141 public static extern IntPtr GetCurrentProcess();
142
143 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
144 public static extern bool CreateProcess(
145 string lpApplicationName,
146 string lpCommandLine,
147 IntPtr lpProcessAttributes,
148 IntPtr lpThreadAttributes,
149 bool bInheritHandles,
150 uint dwCreationFlags,
151 IntPtr lpEnvironment,
152 string lpCurrentDirectory,
153 ref STARTUPINFO lpStartupInfo,
154 out PROCESS_INFORMATION lpProcessInformation);
155
156 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
157 public static extern bool CreateProcess(
158 string lpApplicationName,
159 string lpCommandLine,
160 IntPtr lpProcessAttributes,
161 IntPtr lpThreadAttributes,
162 bool bInheritHandles,
163 uint dwCreationFlags,
164 IntPtr lpEnvironment,
165 string lpCurrentDirectory,
166 ref STARTUPINFOEX lpStartupInfoEx,
167 out PROCESS_INFORMATION lpProcessInformation);
168
169 [DllImport("kernel32.dll", SetLastError = true)]
170 public static extern bool InitializeProcThreadAttributeList(
171 IntPtr lpAttributeList,
172 int dwAttributeCount,
173 int dwFlags,
174 ref IntPtr lpSize);
175
176 [DllImport("kernel32.dll", SetLastError = true)]
177 public static extern bool UpdateProcThreadAttribute(
178 IntPtr lpAttributeList,
179 uint dwFlags,
180 IntPtr attribute,
181 IntPtr lpValue,
182 IntPtr cbSize,
183 IntPtr lpPreviousValue,
184 IntPtr lpReturnSize);
185
186 [DllImport("kernel32.dll")]
187 public static extern void DeleteProcThreadAttributeList(
188 IntPtr lpAttributeList);
189
190 [DllImport("kernel32.dll", SetLastError = true)]
191 public static extern bool PeekNamedPipe(
192 IntPtr hNamedPipe,
193 IntPtr lpBuffer,
194 uint nBufferSize,
195 IntPtr lpBytesRead,
196 out uint lpTotalBytesAvail,
197 IntPtr lpBytesLeftThisMessage);
198
199 [DllImport("kernel32.dll", SetLastError = true)]
200 public static extern bool GetExitCodeProcess(
201 IntPtr hProcess,
202 out uint lpExitCode);
203
204 // Added: TerminateProcess to allow force-killing a process handle
205 [DllImport("kernel32.dll", SetLastError = true)]
206 public static extern bool TerminateProcess(
207 IntPtr hProcess,
208 uint uExitCode);
209
210 [DllImport("kernel32.dll", SetLastError = true)]
211 public static extern bool CloseHandle(
212 IntPtr hObject);
213
214 [DllImport("kernel32.dll", SetLastError = true)]
215 public static extern IntPtr GetStdHandle(int nStdHandle);
216 }
217 public class WindowInfo
218 {
219 public string URL { get; set; }
220 public bool ActiveWindow { get; set; }
221 }
222 public class WindowTuple
223 {
224 public string WindowId { get; set; }
225 public string Url { get; set; }
226
227 public WindowTuple(string windowId, string url)
228 {
229 WindowId = windowId;
230 Url = url;
231 }
232
233 public WindowTuple()
234 { }
235 }
236
237 public class TabTuple
238 {
239 public int TabId { get; set; }
240 public string Url { get; set; }
241
242 public TabTuple(int TabId, string Url)
243 {
244 this.TabId = TabId;
245 this.Url = Url;
246 }
247 public TabTuple()
248 { }
249 }
250
251 public class MagicHelper : IMagicHelper
252 {
253 internal MagicHelper(Browser browser)
254 {
255 this.browser = browser;
256 }
257
258 // The extension announces the port it picked by broadcasting to a fixed udp port, and the message is a bare
259 // number with nothing in it that says which browser sent it. So only one browser can be launching and
260 // listening at a time: a second listener cannot bind the socket, and if it could it would have no way to
261 // tell whose port it just read. This gate holds a launch from starting until the one before it has its port.
262 // Machine wide by name, because two GPAL applications race each other exactly the way two browsers do.
263 private static readonly Mutex ottoHandshakeGate = new Mutex(false, "GPAL.OttoMagic.PortHandshake");
264 private const int OttoHandshakeGateTimeoutMs = 60_000;
265
266 List<string> lastErrorMessage = new List<string>();
267 bool supressedMessage = false;
268 string lastSessionToken;
269 IBrowser browser { get; set; } = null;
270
271 private IRESTClient _client;
272 private string _clientApiBase;
273
281 internal IRESTClient Client
282 {
283 get
284 {
285 if (null == _client)
286 _client = (IRESTClient)GPAL.RESTClient;
287
288 string url = ((Browser)browser)?.BrowserSettings.RestApiBaseUrl;
289
290 // re-pointed rather than set once: a browser that relaunches lands on a new port, and the
291 // client the workflow is holding has to follow it
292 if (false == string.IsNullOrEmpty(url) && url != _clientApiBase)
293 {
294 _client.WithAPIBase(url);
295 _clientApiBase = url;
296 }
297
298 return _client;
299 }
300 }
301
302 #region Commands
307 public void Back()
308 {
309 Client.Back().Execute();
310 }
311
312 public string CaptureVisibleTab()
313 {
314 return Client.CaptureVisibleTab().Execute();
315 }
328 public string CheckNetworkIdle(int? maxConnections = null, int? timeoutMs = null, int? pruneMs = null)
329 {
330 BrowserSettings settings = ((Browser)browser)?.BrowserSettings;
331
332 // the shorthand CheckNetworkIdle(int) hands back a type the other two cannot be said on, so
333 // the endpoint is named and all three go on together
334 string retVal = Client
335 .WithEndpoint(ApiEndpoint.CheckNetworkIdle)
336 .WithMaxConnections(maxConnections ?? settings?.NetworkIdleMaxConnections ?? 0)
337 .WithTimeoutMs(timeoutMs ?? settings?.NetworkIdleTimeoutMs ?? 30_000)
338 .WithPruneMs(pruneMs ?? settings?.NetworkIdlePruneMs ?? 3_000)
339 .Execute();
340
341 return retVal;
342 }
343
350 public TabTuple CloseTab(dynamic URLorTabId = null)
351 {
352 if (null == URLorTabId)
353 return Client.CloseTab().Execute<TabTuple>();
354 else if (URLorTabId is string v)
355 return Client.CloseTab(v).Execute<TabTuple>();
356 else if (URLorTabId is GPALUrl u)
357 return Client.CloseTab(u).Execute<TabTuple>();
358 else if (URLorTabId is int i)
359 return Client.CloseTab(URLorTabId).Execute<TabTuple>();
360 else
361 return Client.CloseTab().Execute<TabTuple>();
362
363 }
364
375 public bool DeleteStorage(WebsiteStorageType storageType, bool deleteAcrossOrigins = false, string domain = null, string path = null, string key = null, string storeName = null)
376 {
377 IAllowRESTStorageOptions fluentObject = null;
378
379 fluentObject = Client.DeleteStorage(storageType);
380
381 if (false == string.IsNullOrEmpty(domain))
382 fluentObject.WithStorageDomain(domain);
383
384 if (false == string.IsNullOrEmpty(path))
385 fluentObject.WithStoragePath(path);
386
387 if (false == string.IsNullOrEmpty(key))
388 fluentObject.WithStorageKey(key);
389
390 if (false == string.IsNullOrEmpty(storeName))
391 fluentObject.WithStorageStoreName(storeName);
392
393 fluentObject.WithDeleteAcrossOrigins(deleteAcrossOrigins);
394
395 fluentObject.Execute();
396
397 return true;
398 }
399
408 public string GetStorage(WebsiteStorageType storageType, string domain = null, string path = null, string key = null, string storeName = null)
409 {
410 IAllowRESTStorageOptions fluentObject = null;
411
412 fluentObject = Client.GetStorage(storageType);
413
414 if (false == string.IsNullOrEmpty(domain))
415 fluentObject.WithStorageDomain(domain);
416
417 if (false == string.IsNullOrEmpty(path))
418 fluentObject.WithStoragePath(path);
419
420 if (false == string.IsNullOrEmpty(key))
421 fluentObject.WithStorageKey(key);
422
423 if (false == string.IsNullOrEmpty(storeName))
424 fluentObject.WithStorageStoreName(storeName);
425
426 return fluentObject.Execute();
427 }
428
429 public bool SetStorage(WebsiteStorageType storageType, string data, string domain = null, string path = null, string key = null, string storeName = null)
430 {
431
432 IAllowRESTStorageOptions fluentObject = null;
433
434 fluentObject = Client.SetStorage(storageType);
435
436 if (false == string.IsNullOrEmpty(domain))
437 fluentObject.WithStorageDomain(domain);
438
439 if (false == string.IsNullOrEmpty(path))
440 fluentObject.WithStoragePath(path);
441
442 if (false == string.IsNullOrEmpty(key))
443 fluentObject.WithStorageKey(key);
444
445 if (false == string.IsNullOrEmpty(storeName))
446 fluentObject.WithStorageStoreName(storeName);
447
448 if (false == string.IsNullOrEmpty(data))
449 fluentObject.WithData(data);
450
451 fluentObject.Execute<bool>();
452
453 return true;
454 }
455
460 public void Forward()
461 {
462 Client.Forward().Execute();
463 }
464
469 public void FullScreen()
470 {
471 Client.FullScreen().Execute();
472 }
473
480 public string Get(GPALUrl URL)
481 {
482 bool areRobotsAllowed = true;
483
484 URL.ForUrl(GetFullUrl(URL?.Url, browser, out areRobotsAllowed));
485
486 if (null != browser)
487 ((Browser)browser)._areRobotsAllowed = areRobotsAllowed;
488
489 var result = Client.GoTo(URL?.Url).Execute();
490 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
491
492 return result;
493 }
494
501 public string GetContentAndCss(string elementId)
502 {
503 return Client.GetContentAndCss(elementId).Execute();
504 }
505 public Dictionary<string, object> GetCssAttributes(string elementId)
506 {
507 return Client.GetCssAttributes(elementId).Execute<Dictionary<string, object>>();
508 }
509 public Dictionary<string, object> GetDomAttributes(string elementId)
510 {
511 return Client.GetDomAttributes(elementId).Execute<Dictionary<string, object>>();
512 }
513 public Dictionary<string, object> GetDomProperties(string elementId)
514 {
515 return Client.GetDomProperties(elementId).Execute< Dictionary<string, object>>();
516 }
517
523 public string GetCurrentUrl()
524 {
525 return Client.GetCurrentUrl().Execute();
526 }
527
534 public string GetReadyStatus(string sessionToken)
535 {
536 string retVal = null;
537
538 if (lastSessionToken != sessionToken)
539 {
540 lastErrorMessage.Clear();
541 supressedMessage = false;
542 lastSessionToken = sessionToken;
543 }
544
545 dynamic obj = Client.GetReadyStatus().Execute();
546 if ("null" != obj && "NOTOK" != obj)
547 retVal = obj ?? @"""loading""";
548 else
549 retVal = "complete";
550
551 string currentErrorMessage = $"Document.Ready status [{retVal}]";
552
553 if (false == lastErrorMessage.Contains(currentErrorMessage))
554 {
555 lastErrorMessage.Add(currentErrorMessage);
556 GPAL.PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage, null, GPALObjectType.None);
557 supressedMessage = false;
558 }
559 else if (false == supressedMessage)
560 {
561 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
562 supressedMessage = true;
563 }
564
565 return retVal;
566 }
567
574 public GPALElement GetShadowRoot(string elementId)
575 {
576 return Client.GetShadowRoot(elementId).Execute<GPALElement>();
577 }
578
586 {
587 if (null == URL)
588 URL = new GPALUrl("google.com");
589
590 bool areRobotsAllowed = true;
591
592 URL.ForUrl(GetFullUrl(URL.Url, browser, out areRobotsAllowed));
593
594 if (null != browser)
595 ((Browser)browser)._areRobotsAllowed = (bool) areRobotsAllowed;
596
597 TabTuple retVal = Client.GoTo(URL.Url).Execute<TabTuple>();
598 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
599
600 return retVal;
601 }
602
609 public TabTuple GotoTab(dynamic URLorTabId)
610 {
611 if (URLorTabId is GPALUrl v)
612 {
613 TabTuple tt = Client.GoToTab(v).Execute<TabTuple>();
614 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
615 return tt;
616 }
617 else if (URLorTabId is TabTuple tuple)
618 {
619 TabTuple tt = Client.GoToTab(tuple.TabId).Execute<TabTuple>();
620 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
621 return tt;
622 }
623 else
624 {
625 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"URLorTabId is unknown type [{URLorTabId.GetType()}]", this, GPALObjectType.Other);
626 return new TabTuple();
627 }
628 }
629
635 public void SwitchToElement(string elementId)
636 {
637 Client.SwitchToElement(elementId).Execute();
638 }
639
645 public void InFrame(string elementId)
646 {
647 Client.SwitchToElement(elementId).Execute();
648 }
649
654 public void Minimize()
655 {
656 Client.Minimize().Execute();
657 }
658
663 public void Maximize()
664 {
665 Client.Maximize().Execute();
666 }
667
673 public void MoveTo(string elementId)
674 {
675 Client.ScrollIntoView(elementId).Execute();
676 Client.Hover(elementId).Execute();
677 }
678
683 public void Normal()
684 {
685 Client.Normal().Execute();
686 }
687
694 public TabTuple NewTab(GPALUrl URL = null)
695 {
696 if (null == URL)
697 URL = new GPALUrl();
698
699 URL.ForUrl(GetFullUrl(URL.Url, browser, out ((Browser)browser)._areRobotsAllowed));
700
701 TabTuple retVal = Client.NewTab(URL.Url).Execute<TabTuple>();
702 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
703
704 return retVal;
705 }
706
713 {
714 return Client.NextTab().Execute<TabTuple>();
715 }
716
723 public int TabCount()
724 {
725 return Client.TabCount().Execute<int>();
726 }
727
734 {
735 return Client.NextWindow().Execute<WindowTuple>();
736 }
737
743 public void OverrideReferrer(string referrer)
744 {
745 Client.OverrideReferrer(referrer).Execute();
746 }
747
753 public void SetUserAgent(string userAgent)
754 {
755 Client.SetUserAgent(userAgent).Execute();
756 }
757
762 public void PageDown()
763 {
764 Client.PageDown().Execute();
765 }
766
771 public void PageEnd()
772 {
773 Client.PageEnd().Execute();
774 }
775
780 public void PageTop()
781 {
782 Client.PageTop().Execute();
783 }
784
789 public void PageUp()
790 {
791 Client.PageUp().Execute();
792 }
793
797 public void PressModifierKey(ModifierKeys modifierKeys)
798 {
799 Client.PressModifierKey(modifierKeys).Execute();
800 }
801
807 {
808 return Client.PreviousTab().Execute<TabTuple>();
809 }
810
811
818 {
819 return Client.PreviousWindow().Execute<WindowTuple>();
820 }
821
825 public void Refresh()
826 {
827 Client.Refresh().Execute();
828 }
829
834 public void ReleaseModifierKey(ModifierKeys modifierKeys)
835 {
836 Client.ReleaseModifierKey(modifierKeys).Execute();
837 }
838
843 public void Restore()
844 {
845 Client.Restore().Execute();
846 }
847
853 public void ScrollWindowByHorizontal(int pixels)
854 {
855 Client.ScrollWindowByHorizontal(pixels).Execute();
856 }
857
863 public void ScrollWindowByVertical(int pixels)
864 {
865 Client.ScrollWindowByVertical(pixels).Execute();
866 }
867
873 {
874 Client.StealthOverrideReferrer().Execute();
875 }
876
886 public void WithDownloadFile(string downloadPath)
887 {
888 Client.SetDownloadFilename(Path.GetFileName(downloadPath)).Execute();
889 }
890
896 public int WindowScreenLeft()
897 {
898 return Client.WindowScreenLeft().Execute<int>();
899 }
900
906 public int WindowScreenTop()
907 {
908 return Client.WindowScreenTop().Execute<int>();
909 }
910
916 public void Focus(string elementId)
917 {
918 Client.Focus(elementId).Execute();
919 }
920
926 public void FireChangeEvent(string elementId)
927 {
928 Client.FireChangeEvent(elementId).Execute();
929 }
930
935 public Rectangle GetWindowRectangle()
936 {
937 Rectangle rect = new Rectangle();
938 var result = Client.GetWindowRectangle().Execute<Dictionary<string, object>>();
939 Dictionary<string, object> obj = null;
940
941 if (null != result)
942 {
943 obj = (Dictionary<string, object>)result;
944
945 var left = obj.ContainsKey("Left") ? Convert.ToInt32(obj["Left"]) : obj.ContainsKey("left") ? Convert.ToInt32(obj["left"]) : obj.ContainsKey("X") ? Convert.ToInt32(obj["X"]) : obj.ContainsKey("x") ? Convert.ToInt32(obj["x"]) : 0;
946 var top = obj.ContainsKey("Top") ? Convert.ToInt32(obj["Top"]) : obj.ContainsKey("top") ? Convert.ToInt32(obj["top"]) : obj.ContainsKey("Y") ? Convert.ToInt32(obj["Y"]) : obj.ContainsKey("y") ? Convert.ToInt32(obj["y"]) : 0;
947 var width = obj.ContainsKey("Width") ? Convert.ToInt32(obj["Width"]) : obj.ContainsKey("width") ? Convert.ToInt32(obj["width"]) : 0;
948 var height = obj.ContainsKey("Height") ? Convert.ToInt32(obj["Height"]) : obj.ContainsKey("height") ? Convert.ToInt32(obj["height"]) : 0;
949
950 rect = new Rectangle(left, top, width, height);
951 }
952
953 return rect;
954 }
955
960 public void ScrollIntoView(string elementId)
961 {
962 Client.ScrollIntoView(elementId).Execute();
963 }
964
971 public void ScrollElement(string elementId, int hPixels, int vPixels)
972 {
973 Client.ScrollElement(elementId).WithHPixels(hPixels).WithVPixels(vPixels).Execute();
974 }
975
981 public string GetElementAttributeHash(string elementId)
982 {
983 return Client.GetElementAttributeHash(elementId).Execute();
984 }
985
991 public List<GPALElement> GetOptions(string elementId)
992 {
993 List<GPALElement> tmpElements = Client.GetOptions(elementId).Execute<List<GPALElement>>();
995 return tmpElements;
996 }
997
1001 public string GetPageSource()
1002 {
1003 return Client.GetPageSource().Execute();
1004 }
1005
1011 public GPALElement GetParentNode(string elementId)
1012 {
1013 return Client.GetParentNode(elementId).Execute<List<GPALElement>>()[0];
1014 }
1015
1023 public string GetAttribute(string elementId, string attribute)
1024 {
1025 return Client.GetAttribute(elementId).WithAttribute(attribute).Execute();
1026 }
1027
1035 public void SetAttribute(string elementId, string attribute, string value)
1036 {
1037 Client.SetAttribute(elementId).WithAttribute(attribute).WithValue(value).Execute();
1038 }
1039
1040 public void SetRange(string elementId, int rangeValue)
1041 {
1042 Client.SetRange(elementId).WithRangeValue(rangeValue).Execute();
1043 }
1044
1051 public void SetValueFromElement(string srcSelector, string destElementId)
1052 {
1053 Client.WithElementId(srcSelector).SetValueFrom(destElementId).Execute();
1054 }
1055
1061 public void LeftClick(string elementId)
1062 {
1063 Client.LeftClick(elementId).Execute();
1064 Thread.Sleep(500);
1065 }
1066
1072 public void LeftDoubleClick(string elementId)
1073 {
1074 Client.LeftDoubleClick(elementId).Execute();
1075 }
1076
1082 public void MiddleClick(string elementId)
1083 {
1084 Client.MiddleClick(elementId).Execute();
1085 Thread.Sleep(500);
1086 }
1087
1093 public void RightClick(string elementId)
1094 {
1095 Client.RightClick(elementId).Execute();
1096 }
1097
1098 public void SelectClick(string elementId, dynamic indexOrValue)
1099 {
1100 if (indexOrValue is int idx)
1101 Client.SelectClick(elementId).WithSelectIndex(idx).Execute();
1102 else
1103 Client.SelectClick(elementId).WithSelectValue(indexOrValue).Execute();
1104 }
1105
1112 public bool IsVisibleInViewport(string elementId)
1113 {
1114 return Client.IsVisibleInViewport(elementId).Execute<bool>();
1115 }
1116
1122 public bool IsEndOfPage()
1123 {
1124 return Client.IsEndOfPage().Execute<bool>();
1125 }
1126
1132 public void Hover(string elementId)
1133 {
1134 Client.Hover(elementId).Execute();
1135 }
1136
1146 public void DragAndDrop(string elementId, int deltaX, int deltaY, int offsetX = 0, int offsetY = 0)
1147 {
1148 Client.DragAndDrop(elementId).WithDeltaX(deltaX).WithDeltaY(deltaY).WithOffsetX(offsetX).WithOffsetY(offsetY).Execute();
1149 }
1150
1157 {
1158 return Client.WindowInnerHeight().Execute<int>();
1159 }
1160
1166 public int WindowInnerWidth()
1167 {
1168 return Client.WindowInnerWidth().Execute<int>();
1169 }
1170
1177 {
1178 return Client.WindowOuterHeight().Execute<int>();
1179 }
1180
1186 public int WindowOuterWidth()
1187 {
1188 return Client.WindowOuterWidth().Execute<int>();
1189 }
1190
1203 public string Fetch(string url, string method = null, string body = null, string contentType = null, string[] headers = null, bool asBytes = false)
1204 {
1205 string retVal = Client
1206 .Fetch(url)
1207 .WithVerb(method)
1208 .WithBody(body)
1209 .WithContentType(contentType)
1210 .WithHeaders(headers)
1211 .WithBytes(asBytes)
1212 .Execute();
1213
1214 // the extension answers NOTOK when the page could not make the request, and the caller reads null as
1215 // the failure, the same as the other engines
1216 return "NOTOK" == retVal || "null" == retVal ? null : retVal;
1217 }
1218
1225 {
1226 return Client.WindowPageOffsetX().Execute<int>();
1227 }
1228
1235 {
1236 return Client.WindowPageOffsetY().Execute<int>();
1237 }
1238
1252 public bool ElementFromPoint(string elementId, int x, int y)
1253 {
1254 return true == Client.ElementFromPoint(elementId, x, y).Execute<bool>();
1255 }
1256
1257 public Rectangle GetBoundingClientRect(string elementId)
1258 {
1259 Rectangle rect = new Rectangle(0,0,0,0);
1260 var result = Client.GetBoundingClientRect(elementId).Execute<Dictionary<string, object>>();
1261 Dictionary<string, object> obj = null;
1262
1263 if (null != result)
1264 {
1265 obj = (Dictionary<string, object>)result;
1266
1267 var left = obj.ContainsKey("Left") ? Convert.ToInt32(obj["Left"]) : obj.ContainsKey("left") ? Convert.ToInt32(obj["left"]) : obj.ContainsKey("X") ? Convert.ToInt32(obj["X"]) : obj.ContainsKey("x") ? Convert.ToInt32(obj["x"]) : 0;
1268 var top = obj.ContainsKey("Top") ? Convert.ToInt32(obj["Top"]) : obj.ContainsKey("top") ? Convert.ToInt32(obj["top"]) : obj.ContainsKey("Y") ? Convert.ToInt32(obj["Y"]) : obj.ContainsKey("y") ? Convert.ToInt32(obj["y"]) : 0;
1269 var width = obj.ContainsKey("Width") ? Convert.ToInt32(obj["Width"]) : obj.ContainsKey("width") ? Convert.ToInt32(obj["width"]) : 0;
1270 var height = obj.ContainsKey("Height") ? Convert.ToInt32(obj["Height"]) : obj.ContainsKey("height") ? Convert.ToInt32(obj["height"]) : 0;
1271
1272 rect = new Rectangle(left, top, width, height);
1273 }
1274
1275 return rect;
1276 }
1277
1283 public void HideElement(string elementId)
1284 {
1285 Client.HideElement(elementId).Execute();
1286 }
1287
1293 public void InjectScript(string script)
1294 {
1295 string answered = Client.InjectScript(script).Execute();
1296
1297 // the extension answers with the id it registered under, or with an error. thrown away, a script that
1298 // never registered looks exactly like one that did until the page fails to behave, which is a long way
1299 // from here. the usual cause is Allow user scripts still being off for the extension
1300 if (true == answered?.Contains("error"))
1301 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"The extension did not register the script [{answered}]", this, GPALObjectType.Other);
1302 }
1303
1312 public string ExecuteJavaScript(string script)
1313 {
1314 return Client.ExecuteJavaScript(script).Execute();
1315 }
1316
1322 {
1323 Client.ClearInjectedScripts().Execute();
1324 }
1325
1334 public bool IsClickAble(string elementId)
1335 {
1336 return Client.IsClickable(elementId).Execute<bool>();
1337 }
1338
1346 public bool IsDisplayed(string elementId)
1347 {
1348 return Client.IsDisplayed(elementId).Execute<bool>();
1349 }
1350
1358 public bool IsEnabled(string elementId)
1359 {
1360 return Client.IsEnabled(elementId).Execute<bool>();
1361 }
1362
1366 public void ClearReferrer()
1367 {
1368 Client.ClearReferrer().Execute();
1369 }
1370
1377 public GPALElement Evaluate(string xpath)
1378 {
1379 return Client.Evaluate(xpath).Execute<GPALElement>();
1380 }
1381
1388 public List<GPALElement> EvaluateAll(string xpath)
1389 {
1390 return Client.EvaluateAll(xpath).Execute<List<GPALElement>>();
1391 }
1392
1398 public GPALElement EvaluatePersistent(string xpath)
1399 {
1400 return Client.EvaluatePersistent(xpath).Execute<GPALElement>();
1401 }
1402
1409 public List<GPALElement> EvaluateAllPersistent(string xpath)
1410 {
1411 return Client.EvaluateAllPersistent(xpath).Execute<List<GPALElement>>();
1412 }
1413
1419 public string GetUserAgent()
1420 {
1421 return Client.GetUserAgent().Execute();
1422 }
1423
1429 public string GetLanguages()
1430 {
1431 return Client.GetLanguages().Execute();
1432 }
1433
1443 public void CaptureCalls(bool capture = true, string urlFragment = null, bool clear = false)
1444 {
1445 IAllowRESTCaptureOptions capturing = Client.CaptureCalls(capture);
1446
1447 if (false == string.IsNullOrWhiteSpace(urlFragment)) capturing.WithCallFilter(urlFragment);
1448 if (true == clear) capturing.WithClearCalls(true);
1449
1450 capturing.Execute();
1451 }
1452
1458 public string GetCapturedCalls()
1459 {
1460 return Client.GetCapturedCalls().Execute();
1461 }
1462
1469 public void FillInAppend(string elementId, string text)
1470 {
1471 Client.FillInAppend(elementId).WithText(text).Execute();
1472 }
1473
1480 public void FillInInsert(string elementId, string text)
1481 {
1482 Client.FillInInsert(elementId).WithText(text).Execute();
1483 }
1484
1491 public void FillInOverwrite(string elementId, string text)
1492 {
1493 Client.FillInOverwrite(elementId).WithText(text).Execute();
1494 }
1495
1502 public void LeftClickAndDownload(string elementId, string filenameAndPath)
1503 {
1504 Client.LeftClickAndDownload(elementId).WithDownloadFile(Path.GetFileName(filenameAndPath)).Execute();
1505 }
1506
1512 public void LeftClickAndUpload(string elementId, string filenameAndPath)
1513 {
1514 Client.LeftClickAndUpload(elementId).WithUploadFile(filenameAndPath).Execute();
1515 }
1516
1522 public void LeftClickAndUpload(string elementId, GPALFile filenamesAndPaths)
1523 {
1524 Client.LeftClickAndUpload(elementId).WithUploadFiles(filenamesAndPaths).Execute();
1525 }
1526
1527
1534 public GPALElement QuerySelector(string css)
1535 {
1536 var elem = Client.QuerySelector(css).Execute<GPALElement>();
1537 return elem;
1538 }
1539
1546 public List<GPALElement> QuerySelectors(string css)
1547 {
1548 return Client.QuerySelectors(css).Execute<List<GPALElement>>();
1549 }
1550
1558 {
1559 var persist = Client.QueryPersistentSelector(css).Execute<GPALElement>();
1560
1561 return persist;
1562 }
1563
1570 public List<GPALElement> QueryPersistentSelectors(string css)
1571 {
1572 return Client.QueryPersistentSelectors(css).Execute<List<GPALElement>>();
1573 }
1574
1580 public void ScrollWindow(int hPixels, int vPixels)
1581 {
1582 // Note: Requires RESTClient to implement ScrollWindow, or use ExecuteJavaScript as fallback
1583 Client.ScrollWindow(hPixels, vPixels).Execute();
1584 }
1585
1591 public void SendKey(byte vkcode)
1592 {
1593 Client.SendKey(vkcode).Execute();
1594 }
1595
1602 public void SendString(string text, int delayMs = 0)
1603 {
1604 Client.SendString(text, delayMs).Execute();
1605 }
1606
1612 public void SubmitForm(string elementId)
1613 {
1614 Client.SubmitForm(elementId).Execute();
1615 }
1616
1622 {
1623 Client.SwitchToDefaultContent().Execute();
1624 }
1625
1631 public void SwitchToShadowRoot(string elementId)
1632 {
1633 Client.SwitchToShadowRoot(elementId).Execute();
1634 }
1635
1636 #region Window Handling
1637 public WindowTuple OpenWindow(GPALUrl URL = null)
1638 {
1639 if (null == URL)
1640 URL = new GPALUrl();
1641
1642 URL.ForUrl(GetFullUrl(URL.Url, browser, out ((Browser)browser)._areRobotsAllowed));
1643
1644 WindowTuple retVal = Client.OpenWindow(URL.Url).Execute<WindowTuple>();
1645 browser.ServerResponseCode = ((RESTClient)Client).StatusCode;
1646
1647 return retVal;
1648 }
1649
1650 public WindowTuple GoToWindow(dynamic urlOrId)
1651 {
1652 if (urlOrId is string v)
1653 return Client.GoToWindow(v).Execute<WindowTuple>();
1654 else
1655 return Client.GoToWindow(urlOrId.ToString()).Execute<WindowTuple>();
1656 }
1662 public string CloseBrowser()
1663 {
1664 return Client.CloseBrowser().Execute();
1665 }
1666
1667 public WindowTuple CloseWindow(dynamic urlOrId)
1668 {
1669 if (urlOrId is string v)
1670 return Client.CloseWindow(v).Execute<WindowTuple>();
1671 else
1672 return Client.CloseWindow(urlOrId.ToString()).Execute<WindowTuple>();
1673 }
1674
1675 public WindowTuple GetCurrentWindow()
1676 {
1677 return Client.GetCurrentWindow().Execute<WindowTuple>();
1678 }
1679 #endregion Window Handling
1680 #region GPAL Studio
1686 public string GetBrowserSettings()
1687 {
1688 return Client.GetBrowserSettings().Execute();
1689 }
1690
1696 public string GetGpalSettings()
1697 {
1698 return Client.GetGpalSettings().Execute();
1699 }
1700
1706 public string GetSettings()
1707 {
1708 return Client.GetSettings().Execute();
1709 }
1710
1716 public string GetWorkflow()
1717 {
1718 return Client.GetWorkflow().Execute();
1719 }
1720 #endregion GPAL Studio
1721 #endregion Commands
1722 #region Helpers
1738 static void PublishBrowserOutput(BrowserSettings browserSettings, DataReceivedEventArgs data)
1739 {
1740 if (null != data.Data)
1741 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $"[{browserSettings.BrowserType}] {data.Data}", browserSettings, GPALObjectType.Other);
1742 }
1743 public Process LaunchBrowser(Browser browser, GPALUrl URL, List<string> additionalArguments = null)
1744 {
1745 BrowserSettings browserSettings = browser.BrowserSettings;
1746 bool created = false;
1747 int secondsToWait = 30;
1748 TimeSpan timeout = TimeSpan.FromSeconds(secondsToWait);
1749
1750 if (null == browserSettings)
1751 {
1752 // 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?
1753 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "BrowserSettings parameter is null. Using chrome puppeteer port.", null, GPALObjectType.Other);
1754 browser = (Browser)GPAL.Browser.ToGPALObject();
1755 browserSettings = ((Browser)browser).BrowserSettings;
1756 }
1757
1758 if (null != browserSettings.ExistingBrowserPort)
1759 {
1760 foreach (Browser b in GPAL.Browsers)
1761 if (b != browser && b.BrowserSettings.ExistingBrowserPort == browserSettings.ExistingBrowserPort)
1762 {
1763 browser.PuppeteerClient = b.PuppeteerClient;
1764 browser.PuppeteerCommunicator = b.PuppeteerCommunicator;
1765
1766 return b.Process;
1767 }
1768 }
1769
1770 if (null == URL || null == URL?.Url)
1771 URL = "https://www.google.com";
1772
1773 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
1774
1775 if (false == ((Browser)browser).AreRobotsAllowed && true == ((Browser)browser).BrowserSettings.ObeyRobotsTxt)
1776 {
1777 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);
1778 URL.ForUrl("https://google.com");
1779 }
1780
1781 bool headless = browserSettings.UseHeadless;
1782 var chromeArguments = additionalArguments ?? new List<string>();
1783 string browserPath;
1784
1785 // Get browser executable path
1786 switch (browserSettings.BrowserType)
1787 {
1788 case BrowserType.Chrome:
1789 browserPath = Puppeteer.GetChromePath();
1790 break;
1791 case BrowserType.Edge:
1792 browserPath = Puppeteer.GetEdgePath();
1793 break;
1794 case BrowserType.FireFox:
1795 browserPath = Puppeteer.GetFirefoxPath();
1796 break;
1797 default:
1798 Exception ex = new Exception("Unsupported Browser Type");
1799 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unsupported browser type [{browserSettings.BrowserType}]", browserSettings, GPALObjectType.Other, ex);
1800 return null;
1801 }
1802
1803 if (string.IsNullOrEmpty(browserPath) || false == File.Exists(browserPath))
1804 {
1805 string msg = $"[{browserSettings.BrowserType}] executable not found at [{browserPath}]";
1806 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg, browserSettings, GPALObjectType.Other);
1807 throw new NotFoundException(msg);
1808 }
1809
1810 // Common arguments for Chrome/Edge
1811 if (browserSettings.BrowserType == BrowserType.Chrome || browserSettings.BrowserType == BrowserType.Edge)
1812 {
1813 string userAgent = null;
1814
1815 // Profile settings
1816 if (false == string.IsNullOrEmpty(browserSettings.ProfileUserName) || false == string.IsNullOrEmpty(browserSettings.ProfileName))
1817 {
1818 browserSettings.ProfileDataDirectory = BrowserHelper.GetBrowserProfileDirectory(browserSettings);
1819 if (false == string.IsNullOrEmpty(browserSettings.ProfileName))
1820 {
1821 string profileDir = BrowserHelper.FindProfileDirectory(browserSettings.ProfileDataDirectory, browserSettings.ProfileName) ?? browserSettings.ProfileName;
1822 chromeArguments.Add($"--profile-directory={profileDir}");
1823 }
1824 }
1825 else if (true == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
1826 {
1827 browserSettings.ProfileDataDirectory = ChromeProfileManager.CreateTempUserProfile(
1828 browserSettings.DownloadLocation ?? FileHelper.GetDefaultDownloadDirectory(browser),
1829 browserSettings.PromptForDownload,
1830 browserSettings.OpenPDFExternally,
1831 browserSettings.LoadImages,
1832 browserSettings.UseOttoMagic
1833 );
1834 browserSettings.TempProfileCreated = true;
1835 }
1836
1837 // prompting and the pdf viewer are profile preferences. the extension cannot set either one and
1838 // there is no CDP here, so a supplied profile has to be written to for the workflow's settings to
1839 // mean anything, and Browser.Close puts it back once the browser is gone. selenium and puppeteer
1840 // get this in BrowserHelper while their options are built, and firefox gets it from CreateUserJs
1841 // below, so chromium under the extension was the one path running on whatever the profile said
1842 if (false == browserSettings.TempProfileCreated && false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
1843 browserSettings.PreviousDownloadPreferences = ChromeProfileManager.ApplyDownloadPreferences(
1844 browserSettings.ProfileDataDirectory,
1845 browserSettings.DownloadLocation,
1846 browserSettings.PromptForDownload,
1847 browserSettings.OpenPDFExternally);
1848
1849 // a profile can only be owned by one browser. launching onto one that is already open hands the
1850 // command line to that browser and exits, so the extension never loads and GPALRestAPI never
1851 // announces a port. selenium has refused this since it was written; this path did not, which is
1852 // why an OttoMagic run on a held profile failed later saying nothing about the profile
1853 if (false == browserSettings.TempProfileCreated)
1854 BrowserHelper.RefuseIfProfileIsOpen(browserSettings);
1855
1856 // Debugging
1857 if (true == browserSettings.UsePuppeteer)
1858 {
1859 if (true == browserSettings.DebugPipe)
1860 chromeArguments.Add("--remote-debugging-pipe --enable-unsafe-extension-debugging --remote-debugging-port=0");
1861 else
1862 {
1863 // asked for rather than assumed: a second browser starting alongside this one has to be
1864 // given a different port, and 0xdead for everybody is how they end up in one session
1865 if (null == browserSettings.DebugPort)
1866 browserSettings.DebugPort = BrowserHelper.FindFreePort();
1867
1868 browserSettings.PuppeteerUrl = $"http://localhost:{browserSettings.DebugPort}";
1869 chromeArguments.Add($"--remote-debugging-port={browserSettings.DebugPort.Value}");
1870 }
1871
1872 // *******************************************************************
1873 // NOTE: CAVEAT: suppose a time comes to run with some sort of extension, this isn't causing any harm, just annoying...
1874 // 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
1875 // *******************************************************************
1876 // chromeArguments.Add("--disable-extensions"); // no ottomagic
1877 // *******************************************************************
1878 // *******************************************************************
1879 }
1880
1881 // set windowsize if provided or not - new
1882 if (false == browserSettings.FullScreen &&
1883 false == browserSettings.Maximize &&
1884 false == browserSettings.Minimize)
1885 {
1886 if (false == browserSettings.WindowSize.IsEmpty)
1887 {
1888 // explicit WindowSize provided - must set screen-info in headless=new
1889 // otherwise CDP interactions fail with "Browser window not found"
1890 chromeArguments.Add($"--window-size={browserSettings.WindowSize.Width},{browserSettings.WindowSize.Height}");
1891
1892 // the rectangle carries where as well as how big, and a workflow running several browsers
1893 // wants them beside each other rather than stacked. left at the origin it is not asked for
1894 if (0 != browserSettings.WindowSize.X || 0 != browserSettings.WindowSize.Y)
1895 chromeArguments.Add($"--window-position={browserSettings.WindowSize.X},{browserSettings.WindowSize.Y}");
1896 }
1897 else if (browserSettings.UseHeadless)
1898 chromeArguments.Add("--start-fullscreen");
1899 }
1900 else if (true == browserSettings.FullScreen)
1901 chromeArguments.Add("--start-fullscreen");
1902 else if (true == browserSettings.Maximize)
1903 chromeArguments.Add("--start-maximized");
1904
1905 // Anti-bot detection
1906 //if (true == myBrowserSettings.UsePuppeteer || AutomationEngine.PuppeteerPortHW == myBrowserSettings.AutomationEngine)
1907 //{
1908 // chromeArguments.Add("--disable-blink-features=AutomationControlled");
1909 chromeArguments.Add("--excludeSwitches=enable-automation");
1910 chromeArguments.Add("--useAutomationExtension=false");
1911 //}
1912
1913 // Core arguments to suppress popups and stabilize CDP
1914 chromeArguments.Add("--no-first-run");
1915 chromeArguments.Add("--no-default-browser-check");
1916 //chromeArguments.Add("--no-default-browser-check");
1917 //chromeArguments.Add("--disable-default-apps");
1918 //chromeArguments.Add("--disable-features=TranslateUI,FirstRun,ChromeWhatsNewUI,InterestCohort");
1919 //chromeArguments.Add("--disable-background-networking");
1920 //chromeArguments.Add("--disable-background-timer-throttling");
1921 //chromeArguments.Add("--disable-renderer-backgrounding");
1922 //chromeArguments.Add("--disable-backgrounding-occluded-windows");
1923 //chromeArguments.Add("--no-service-autorun");
1924 //chromeArguments.Add("--disable-client-side-phishing-detection");
1925 //chromeArguments.Add("--disable-sync");
1926 chromeArguments.Add("--disable-dev-shm-usage");
1927
1928 // avoid chrome overlays on the rendered area
1929 chromeArguments.Add("--disable-infobars");
1930 chromeArguments.Add("--disable-session-crashed-bubble");
1931 chromeArguments.Add("--hide-crash-restore-bubble");
1932 chromeArguments.Add("--noerrdialogs");
1933 // one --disable-features switch only, chromium takes the last one rather than merging them.
1934 // edge runs its own shopping content scripts on retail pages, and their constant dom mutation is
1935 // what has DOM.getDocument hanging there while the same page is fine under chrome
1936 string disableFeatures = "TranslateUI,Translate";
1937
1938 if (BrowserType.Edge == browserSettings.BrowserType)
1939 disableFeatures += ",msShoppingTrigger,msShopping,msEdgeShoppingUI,msEdgeShoppingList";
1940
1941 chromeArguments.Add($"--disable-features={disableFeatures}");
1942 chromeArguments.Add("--load-media-router-component-extension");
1943 chromeArguments.Add("--enable-media-router");
1944 chromeArguments.Add("--force-discovery");
1945 chromeArguments.Add("--allow-local-network-access");
1946
1947 if (false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
1948 chromeArguments.Add($@"--user-data-dir=""{browserSettings.ProfileDataDirectory.Replace("\\", "/")}""");
1949
1950 // Headless mode
1951 if (headless)
1952 {
1953 chromeArguments.Add("--headless=new");
1954 chromeArguments.Add("--disable-notifications");
1955 chromeArguments.Add("--disable-gpu");
1956 if (AutomationEngine.PuppeteerPipe == browserSettings.AutomationEngine || AutomationEngine.PuppeteerPipeHW == browserSettings.AutomationEngine)
1957 chromeArguments.Add("--no-sandbox"); // REQUIRED FOR PIPE IN HEADLESS + TEMP PROFILE
1958
1959 // Fetch user-agent
1960 userAgent = GetUserAgent(browserSettings, chromeArguments);
1961
1962 userAgent = userAgent?.Trim('"');
1963
1964 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Headless [{browserSettings.BrowserType}] using UserAgent [{userAgent}]", browserSettings, GPALObjectType.Other);
1965
1966 chromeArguments.Add($"--user-agent=\"{userAgent}\"");
1967 }
1968 else
1969 {
1970 if (false == browserSettings.BlockPopUps)
1971 chromeArguments.Add("--disable-popup-blocking");
1972 }
1973
1974 // Dark mode
1975 if (browserSettings.StealthType.HasFlag(StealthType.DarkMode))
1976 chromeArguments.Add("--force-dark-mode");
1977
1978 // Safe browsing - ads latency to each browse
1979 //chromeArguments.Add("--safebrowsing.enabled");
1980
1981 // full accessibility for using UIAutomation (it is a fallback for method: GetWindowRectangle to get the content dimensions
1982 // no longer needed, we don't use uiautomation to calculate browser coords
1983 // this creates extra work = slower browser response
1984 // chromeArguments.Add("--force-renderer-accessibility");
1985 }
1986 else if (browserSettings.BrowserType == BrowserType.FireFox)
1987 {
1988 // Firefox arguments
1989 var firefoxArguments = new List<string>();
1990
1991 if (headless)
1992 {
1993 firefoxArguments.Add("-headless");
1994 firefoxArguments.Add($"-width={Puppeteer.GetScreenWidth()}");
1995 firefoxArguments.Add($"-height={Puppeteer.GetScreenHeight()}");
1996 firefoxArguments.Add("--disable-notifications");
1997 }
1998 // nothing goes here. a headful firefox on OttoMagic is driven through the extension's rest api,
1999 // on the port GPALRestAPI announces over the handshake, so it has no use for a debugging port.
2000 // note firefox has no --websocket-port: it drops the flag and takes the port number for a url
2001
2002 // Call firefoxhelper.CreateUserJs to load set these preferences
2003 // firefox has a unique requirement for loading a browser extension on demand
2004 // we have to create an .xpi file and get the extension id, then save it to a profile dir
2005 // then specify that profile dir
2006 if (true == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
2007 browserSettings.ProfileDataDirectory = FirefoxProfileManager.CreateTempProfileDirectory();
2008
2009 // same rule as chromium: one browser to a profile. a firefox already on this one takes the launch
2010 // and leaves the run waiting for a browser that was never started
2011 if (false == browserSettings.TempProfileCreated)
2012 BrowserHelper.RefuseIfProfileIsOpen(browserSettings);
2013
2014 browserSettings.PreviousFirefoxPreferences = FirefoxProfileManager.CreateUserJs(browserSettings.ProfileDataDirectory, browserSettings);
2015
2016 if (false == string.IsNullOrEmpty(browserSettings.ProfileUserName) || false == string.IsNullOrEmpty(browserSettings.ProfileName))
2017 firefoxArguments.Add($"-profile \"{(browserSettings.ProfileName ?? browserSettings.ProfileUserName).TrimEnd('\\')}\"");
2018 else if (false == string.IsNullOrEmpty(browserSettings.ProfileDataDirectory))
2019 firefoxArguments.Add($"-profile \"{browserSettings.ProfileDataDirectory.TrimEnd('\\')}\"");
2020
2021 chromeArguments = firefoxArguments; // Reuse chromeArguments for Process.Start
2022 }
2023
2024 bool holdingHandshakeGate = false; // set when this launch owns the OttoMagic port handshake
2025 UdpClient udpHandshake = null; // out here so a launch that throws before the handshake still closes it
2026
2027 // Launch browser
2028 try
2029 {
2030 // we will do a goto. for various reason this might not work and for various reasons this might work better
2031
2032 // NOTE: we can't go to the real url on lauch, we have to apply stealth first :(
2033 if (null != URL && false == string.IsNullOrEmpty(URL.Url))
2034 chromeArguments.Add(URL?.Url);
2035
2036 if (AutomationEngine.PuppeteerPipe == browserSettings.AutomationEngine || AutomationEngine.PuppeteerPipeHW == browserSettings.AutomationEngine)
2037 browserSettings.DebugPipe = true;
2038
2039 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Running workflow for Automation Engine [{browserSettings.AutomationEngine}]", null, GPALObjectType.None);
2040
2041 if (true == browserSettings.UseOttoMagic)
2042 {
2043 // wait for any launch already in flight to have read its port before we take the socket.
2044 // An abandoned gate means the process holding it died, and we are the owner now
2045 try { holdingHandshakeGate = ottoHandshakeGate.WaitOne(OttoHandshakeGateTimeoutMs); }
2046 catch (AbandonedMutexException) { holdingHandshakeGate = true; }
2047
2048 if (false == holdingHandshakeGate)
2049 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);
2050
2051 udpHandshake = new UdpClient(47623);
2052 udpHandshake.Client.ReceiveTimeout = 30000;
2053
2054 KeepFromChildren(udpHandshake);
2055 }
2056
2057 int firefoxLauncherPid = 0;
2058
2059 if (true == browserSettings.DebugPipe)
2060 {
2061 var sa = new Native.SECURITY_ATTRIBUTES
2062 {
2063 nLength = Marshal.SizeOf<Native.SECURITY_ATTRIBUTES>(),
2064 bInheritHandle = 1,
2065 lpSecurityDescriptor = IntPtr.Zero
2066 };
2067
2068 IntPtr hProcess = IntPtr.Zero;
2069 IntPtr reservedPtr = IntPtr.Zero;
2070 AnonymousPipeServerStream stdinPipe = null, stdoutPipe = null, stderrPipe = null;
2071
2072 try
2073 {
2074 // Create anonymous pipes. bInheritHandle is set to true via HandleInheritability.Inheritable
2075 stdinPipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable, 1048576);
2076 stdoutPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable, 1048576);
2077 stderrPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable, 1048576);
2078
2079 // Build command line
2080 var chromeCommandLine = new StringBuilder();
2081 chromeCommandLine.Append($@"""{browserPath}""");
2082 chromeCommandLine.Append(" ");
2083 chromeCommandLine.Append(string.Join(" ", chromeArguments));
2084
2085 // Create process
2086 var startupInfo = new Native.STARTUPINFO
2087 {
2088 cb = (uint)Marshal.SizeOf<Native.STARTUPINFO>(),
2089 // named outright on the one launch we build ourselves, rather than left to inherit
2090 // from the launching thread. null is the desktop the caller is on
2091 lpDesktop = true == browserSettings.HiddenDesktop ? HiddenDesktop.StartupNameFor(browserSettings.HiddenDesktopName) : null,
2092 dwFlags = Native.STARTF_USESHOWWINDOW | Native.STARTF_USESTDHANDLES, // Use standard handles flag
2093 wShowWindow = (ushort)(headless ? Native.SW_HIDE : Native.SW_SHOWNORMAL),
2094 hStdInput = stdinPipe.ClientSafePipeHandle.DangerousGetHandle(), // Assign to standard input
2095 hStdOutput = stdoutPipe.ClientSafePipeHandle.DangerousGetHandle(), // Assign to standard output
2096 hStdError = stderrPipe.ClientSafePipeHandle.DangerousGetHandle(), // Assign to standard error
2097 //cbReserved2 = 0, // Reset unused fields
2098 //lpReserved2 = IntPtr.Zero
2099 };
2100
2101 // Call CreateProcess with bInheritHandles set to true
2102 created = Native.CreateProcess(
2103 null,
2104 chromeCommandLine.ToString(),
2105 IntPtr.Zero,
2106 IntPtr.Zero,
2107 true, // Inherit handles
2108 0,
2109 IntPtr.Zero,
2110 browserSettings.ProfileDataDirectory,
2111 ref startupInfo,
2112 out var processInfo2);
2113
2114 if (!created)
2115 throw new Win32Exception(Marshal.GetLastWin32Error(), "PuppeteerPipe CreateProcess failed.");
2116
2117 hProcess = processInfo2.hProcess;
2118
2119 // Assign streams
2120 browserSettings.InboundPipe = stdinPipe;
2121 browserSettings.OutboundPipe = stdoutPipe;
2122 browserSettings.ErrorPipe = stderrPipe;
2123
2124 // Capture stderr
2125 string stderr = "";
2126 var errorReader = new StreamReader(stderrPipe, Encoding.UTF8, true, 4096, false);
2127 _ = Task.Run(async () =>
2128 {
2129 try
2130 {
2131 char[] buffer = new char[4096];
2132 int read;
2133 while ((read = await errorReader.ReadAsync(buffer, 0, buffer.Length)) > 0)
2134 {
2135 stderr += new string(buffer, 0, read);
2136 }
2137 if (Native.GetExitCodeProcess(hProcess, out uint finalExitCode) && finalExitCode != 0)
2138 {
2139 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Chrome exited with code: [{finalExitCode}], Stderr: [{stderr}]", browserSettings, GPALObjectType.Other);
2140 }
2141 }
2142 catch (GPALException)
2143 {
2144 throw;
2145 }
2146 catch (Exception ex)
2147 {
2148 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error reading stderr", browserSettings, GPALObjectType.Other, ex);
2149 }
2150 finally
2151 {
2152 errorReader.Dispose();
2153 }
2154 });
2155
2156 try
2157 {
2158 // Wait/verify process
2159 if (!Native.GetExitCodeProcess(hProcess, out uint exitCode))
2160 throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to get process exit code.");
2161 if (exitCode != Native.STILL_ACTIVE)
2162 throw new GPALException($"Chrome exited. Exit code: {exitCode}, Stderr: {stderr}, Check {Path.Combine(browserSettings.ProfileDataDirectory, "chrome_log.txt")}");
2163
2164 browserSettings.Process = Process.GetProcessById((int)processInfo2.dwProcessId);
2165 browserSettings.Process.EnableRaisingEvents = true;
2166 }
2167 catch (ArgumentException ex)
2168 {
2169 if (Native.GetExitCodeProcess(hProcess, out uint exitCode) && exitCode != Native.STILL_ACTIVE)
2170 throw new GPALException($"Chrome exited early. Exit code: {exitCode}, Stderr: {stderr}");
2171 throw new GPALException($"Failed to get Chrome process by ID: {ex.Message}, Stderr: {stderr}");
2172 }
2173 }
2174 catch (GPALException)
2175 {
2176 throw;
2177 }
2178 catch (Exception ex)
2179 {
2180 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to launch Chrome with pipes", browserSettings, GPALObjectType.Other, ex);
2181 throw;
2182 }
2183 finally
2184 {
2185 if (reservedPtr != IntPtr.Zero)
2186 Marshal.FreeHGlobal(reservedPtr);
2187 if (hProcess != IntPtr.Zero)
2188 Native.CloseHandle(hProcess);
2189 if (!created)
2190 {
2191 stdinPipe?.Dispose();
2192 stdoutPipe?.Dispose();
2193 stderrPipe?.Dispose();
2194 }
2195 }
2196 }
2197 else if (true == browserSettings.HiddenDesktop)
2198 {
2199 // built by hand because ProcessStartInfo has nowhere to put lpDesktop, and a child started
2200 // without one named takes the desktop of the parent process rather than of the thread that
2201 // started it. Naming it is the only thing that puts the browser on another desktop.
2202 // The cost is the browser's own stdout and stderr, which only ever reach DEEPDEBUG
2203 Native.STARTUPINFO hiddenStartup = new Native.STARTUPINFO
2204 {
2205 cb = (uint)Marshal.SizeOf<Native.STARTUPINFO>(),
2206 lpDesktop = HiddenDesktop.StartupNameFor(browserSettings.HiddenDesktopName)
2207 };
2208
2209 string hiddenCommandLine = $@"""{browserPath}"" {string.Join(" ", chromeArguments)}";
2210
2211 if (false == Native.CreateProcess(null, hiddenCommandLine, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref hiddenStartup, out var hiddenProcess))
2212 throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not start [{browserSettings.BrowserType}] on [{browserSettings.HiddenDesktopName}]");
2213
2214 Native.CloseHandle(hiddenProcess.hProcess);
2215 Native.CloseHandle(hiddenProcess.hThread);
2216
2217 browserSettings.Process = Process.GetProcessById((int)hiddenProcess.dwProcessId);
2218
2219 // Capture the launcher PID immediately before it exits (Firefox only).
2220 if (BrowserType.FireFox == browserSettings.BrowserType)
2221 firefoxLauncherPid = browserSettings.Process?.Id ?? 0;
2222 }
2223 else
2224 {
2225 browserSettings.Process = Process.Start(new ProcessStartInfo
2226 {
2227 FileName = browserPath,
2228 Arguments = string.Join(" ", chromeArguments),
2229 UseShellExecute = false,
2230 // chrome and edge announce "DevTools listening on ws://..." and a stream of their own
2231 // internal ERROR lines on stderr. left alone the child inherits this console and writes
2232 // them straight into the middle of the workflow's output
2233 RedirectStandardOutput = true,
2234 RedirectStandardError = true
2235 });
2236
2237 // the streams have to be drained or the browser stalls once the pipe buffer fills. none of it
2238 // is protocol here, the devtools connection is over the port, so it goes no further than
2239 // DEEPDEBUG for anyone who wants to read it
2240 browserSettings.Process.OutputDataReceived += (sender, data) => PublishBrowserOutput(browserSettings, data);
2241 browserSettings.Process.ErrorDataReceived += (sender, data) => PublishBrowserOutput(browserSettings, data);
2242 browserSettings.Process.BeginOutputReadLine();
2243 browserSettings.Process.BeginErrorReadLine();
2244
2245 // Capture the launcher PID immediately before it exits (Firefox only).
2246 if (BrowserType.FireFox == browserSettings.BrowserType)
2247 firefoxLauncherPid = browserSettings.Process?.Id ?? 0;
2248
2249 DateTime deadline = DateTime.UtcNow + timeout;
2250
2251 // we have no window to check to ensure the browser launched in headless
2252 // so check the process flag
2253 // for windowed, wait for the window to open
2254 if (true == headless)
2255 {
2256 if (true == browserSettings.Process.HasExited)
2257 {
2258 string msg = $"[{browserSettings.BrowserType}] did not start. Exiting.";
2259 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg);
2260 throw new TimeoutException(msg);
2261 }
2262 }
2263 else
2264 {
2265 while (browserSettings.Process.MainWindowHandle == IntPtr.Zero && !browserSettings.Process.HasExited)
2266 {
2267 if (DateTime.UtcNow > deadline)
2268 {
2269 string msg = $"[{browserSettings.BrowserType}] did not start in [{secondsToWait}] seconds. Exiting.";
2270 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg);
2271 throw new TimeoutException(msg);
2272 }
2273
2274 Thread.Sleep(100); // prevents busy-wait CPU spike
2275 }
2276
2277 // the window exists now, so it goes where the workflow asked. this is the only placement
2278 // that works for every browser: the arguments above are chromium's, and firefox has no
2279 // command line way to be given a position at all. the arguments still earn their keep by
2280 // opening the window at the right size, so there is no jump to watch
2281 if (false == browserSettings.WindowSize.IsEmpty &&
2282 false == browserSettings.FullScreen &&
2283 false == browserSettings.Maximize &&
2284 false == browserSettings.Minimize)
2285 {
2286 browserSettings.Process.Refresh();
2287 WindowHelper.ResizeWindow(browserSettings.Process, browserSettings.WindowSize);
2288 }
2289 }
2290 }
2291
2292 if (true == browserSettings.UseOttoMagic)
2293 {
2294 try
2295 {
2296 var remoteEp = new IPEndPoint(IPAddress.Any, 0);
2297 int restPort = 0;
2298 int firstHeard = 0;
2299 int launchedPid = browserSettings.Process?.Id ?? 0;
2300
2301 // every browser carrying the extension announces a port, whatever engine is driving it, so
2302 // what arrives here is not necessarily this browser's. each announcement says which host
2303 // sent it, and a host is started by the browser hosting the extension, so an announcement
2304 // belongs to this launch when its host traces back to the process this launch started.
2305 // the others are somebody else's and are left for whoever is waiting on them
2306 while (0 == restPort)
2307 {
2308 byte[] data;
2309
2310 // nothing more is coming. an announcement that could not be traced is better than no
2311 // browser at all, so one that was heard is taken now, with the reason it is a guess
2312 try { data = udpHandshake.Receive(ref remoteEp); }
2313 catch (SocketException) when (0 != firstHeard)
2314 {
2315 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No GPALRestAPI traced back to this browser, so port [{firstHeard}] is being used on the strength of it being the only one announced. An older GPALRestAPI cannot say which browser it belongs to", browserSettings, GPALObjectType.Browser);
2316
2317 restPort = firstHeard;
2318 break;
2319 }
2320
2321 string[] said = Encoding.UTF8.GetString(data).Trim().Split(' ');
2322
2323 // a browser that never heard the port has no way to reach GPALRestAPI, and everything
2324 // after this would be asking a url that was never worked out. it stops here instead,
2325 // rather than carrying on and failing somewhere that does not say why
2326 if (false == int.TryParse(said[0], out int announced))
2327 throw new InvalidOperationException($"GPALRestAPI announced [{string.Join(" ", said)}], which is not a port number");
2328
2329 // an older GPALRestAPI sends the port on its own and cannot be traced to a browser.
2330 // it is kept as the fallback rather than used straight away, so an announcement that
2331 // can be confirmed still wins if one turns up
2332 if (2 > said.Length || false == int.TryParse(said[1], out int hostPid))
2333 {
2334 if (0 == firstHeard)
2335 firstHeard = announced;
2336
2337 continue;
2338 }
2339
2340 if (0 == launchedPid || true == BrowserHelper.CameFromProcess(hostPid, launchedPid))
2341 restPort = announced;
2342 else
2343 {
2344 if (0 == firstHeard)
2345 firstHeard = announced;
2346
2347 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);
2348 }
2349 }
2350
2351 browserSettings.RestApiBaseUrl = $"http://localhost:{restPort}/";
2352 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"GPALRestAPI ready on port [{restPort}].", browserSettings, GPALObjectType.Browser);
2353 }
2354 catch (GPALException)
2355 {
2356 throw;
2357 }
2358 catch (Exception ex)
2359 {
2360 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"GPALRestAPI never announced a port, so this browser cannot reach it", browserSettings, GPALObjectType.Browser, ex);
2361 throw;
2362 }
2363 finally
2364 {
2365 udpHandshake?.Close();
2366
2367 // the port is read and the socket is free, so the next launch can go. Everything below here
2368 // is this browser's own business and does not touch the handshake
2369 if (true == holdingHandshakeGate)
2370 {
2371 ottoHandshakeGate.ReleaseMutex();
2372 holdingHandshakeGate = false;
2373 }
2374 }
2375
2376 // Firefox launches via a wrapper that exits immediately; the real process is a child.
2377 // Windows preserves ParentProcessId even after the parent exits, so we can query
2378 // for the direct firefox.exe child of the launcher PID we captured at start.
2379 if (firefoxLauncherPid > 0 &&
2380 (browserSettings.Process == null || browserSettings.Process.HasExited))
2381 {
2382 try
2383 {
2384 using (var searcher = new ManagementObjectSearcher(
2385 $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {firefoxLauncherPid} AND Name = 'firefox.exe'"))
2386 {
2387 foreach (ManagementObject obj in searcher.Get())
2388 {
2389 int childPid = Convert.ToInt32(obj["ProcessId"]);
2390 try { browserSettings.Process = System.Diagnostics.Process.GetProcessById(childPid); break; }
2391 catch { }
2392 }
2393 }
2394 }
2395 catch { }
2396 }
2397 }
2398 else if (true == browserSettings.UsePuppeteer)
2399 {
2400 _ = browserSettings.PuppeteerCommunicator; // initialize before we start using
2401 }
2402
2403 // Handle maximize/minimize
2404 if (browserSettings.Maximize)
2405 {
2406 try
2407 {
2408 if (true == browserSettings.UseOttoMagic)
2409 // Use maximize window via extension - the browser we just launched, not whoever's helper was called
2410 browser.MagicHelper.Client.Maximize().Execute();
2411 else if (true == browserSettings.UsePuppeteer)
2412 browserSettings.PuppeteerCommunicator.Maximize().GetAwaiter().GetResult();
2413 }
2414 catch (GPALException)
2415 {
2416 throw;
2417 }
2418 catch (Exception ex)
2419 {
2420 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to maximize browser window", browserSettings, GPALObjectType.Other, ex);
2421 }
2422 }
2423 else if (browserSettings.Minimize)
2424 {
2425 try
2426 {
2427 if (true == browserSettings.UseOttoMagic)
2428 browser.MagicHelper.Client.Minimize().Execute();
2429 else if (true == browserSettings.UsePuppeteer)
2430 browserSettings.PuppeteerCommunicator.Minimize().GetAwaiter().GetResult();
2431 }
2432 catch (GPALException)
2433 {
2434 throw;
2435 }
2436 catch (Exception ex)
2437 {
2438 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to minimize browser window", browserSettings, GPALObjectType.Other, ex);
2439 }
2440 }
2441 else if (browserSettings.FullScreen)
2442 {
2443 try
2444 {
2445 if (true == browserSettings.UseOttoMagic)
2446 browser.MagicHelper.Client.FullScreen().Execute();
2447 else if (true == browserSettings.UsePuppeteer)
2448 browserSettings.PuppeteerCommunicator.FullScreen().GetAwaiter().GetResult();
2449 }
2450 catch (GPALException)
2451 {
2452 throw;
2453 }
2454 catch (Exception ex)
2455 {
2456 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to full screen browser window", browserSettings, GPALObjectType.Other, ex);
2457 }
2458 }
2459
2460 // which exe, which process and which port, together on one line. a workflow driving a browser
2461 // it did not start looks identical to one driving its own until these three are compared
2462 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Browser [{browserSettings.BrowserType}] is process [{browserSettings.Process?.Id}] from [{browserPath}] on port [{browserSettings.DebugPort}] at [{browserSettings.PuppeteerUrl}]", browserSettings, GPALObjectType.Other);
2463
2464 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Browser [{browserSettings.BrowserType}] launched.", browserSettings, GPALObjectType.Other);
2465 }
2466 catch (GPALException)
2467 {
2468 throw;
2469 }
2470 catch (Exception ex)
2471 {
2472 // said, then handed to whoever asked for the browser. it does not end the application here: a
2473 // scheduler running ten workflows loses the other nine, and a form loses itself, over one browser
2474 // that would not start. Nothing catches it, the uncaught handler still cleans up and exits
2475 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to launch browser [{browserSettings.BrowserType}] for URL [{URL}]", browserSettings, GPALObjectType.Other, ex);
2476 throw;
2477 }
2478 finally
2479 {
2480 // a launch that failed before it ever read a port still has to let the next one in, and it has to
2481 // give up the socket as well. Holding 47623 past a failed launch is not a slow browser, it is the
2482 // next OttoMagic launch in this application failing the moment it asks for the socket
2483 udpHandshake?.Close();
2484
2485 if (true == holdingHandshakeGate)
2486 ottoHandshakeGate.ReleaseMutex();
2487 }
2488
2489 return browserSettings.Process;
2490 }
2491
2498 public bool TopBrowser(Process process) => WindowHelper.TopBrowser(process);
2499 // create legit cookies some antibot sites use, captcha, recaptcha, others
2510 /*
2511 [Obsolete]
2512 internal bool LetsDoSomeSurfing(BrowserSettings browserSettings)
2513 {
2514 bool retVal = false;
2515 if (browserSettings.StealthType.HasFlag(StealthType.GenerateRealCookies))
2516 {
2517 //browserSettings.Browser.WithWaitOnIdleConnection(true); // taking really long...
2518 for (int i = 0; i < 10; i++)
2519 {
2520 string targetUrl = GetTargetUrl();
2521 int sleepTime = 2000 + new Random().Next(1500);
2522 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Generating real cookie with visit to [{targetUrl}]. Waiting [{sleepTime}] ms before next visit.", browserSettings.Browser, GPALObjectType.Browser);
2523 browserSettings.Browser.GoTo(targetUrl);
2524 Thread.Sleep(sleepTime);
2525 _ = browserSettings.Browser.PageDown;
2526 _ = browserSettings.Browser.PageDown;
2527 _ = browserSettings.Browser.PageDown;
2528 _ = browserSettings.Browser.PageDown;
2529 }
2530 browserSettings.Browser.WithWaitOnIdleConnection(false);
2531 retVal = true;
2532 }
2533 return retVal;
2534 }
2535 */
2536
2541 public static string GetRandomStarWarsQuery()
2542 {
2543 IRESTClient restClient = GPAL.RESTClient.WithAPIBase("https://swapi.info/api/").ToGPALObject();
2544
2545 var endpoints = new[]
2546 {
2547 ("people", 83, "name"), // e.g., "Yoda"
2548 ("planets", 60, "name"), // e.g., "Alderaan"
2549 ("films", 6, "title"), // e.g., "A New Hope"
2550 ("species", 37, "name"), // e.g., "Wookiee"
2551 ("vehicles", 39, "name"), // e.g., "X-wing"
2552 ("starships", 36, "name") // e.g., "Millennium Falcon"
2553 };
2554
2555 string FetchQuery(string endpoint, int maxId, string field, int retries = 3)
2556 {
2557 for (int attempt = 0; attempt < retries; attempt++)
2558 {
2559 int id = new Random().Next(1, maxId + 1);
2560 var response = restClient.WithEndpoint($"{endpoint}/{id}").Execute<SwapiResponse>(false);
2561 if (response != null)
2562 {
2563 return field == "name" ? response.name : response.title;
2564 }
2565 Thread.Sleep(100 * (int)Math.Pow(2, attempt)); // Backoff: 100ms, 200ms, 400ms
2566 }
2567 return null; // Let caller handle failure
2568 }
2569
2570 // Select random primary endpoint
2571 var (endpoint, maxId, field) = endpoints[new Random().Next(endpoints.Length)];
2572 string query = FetchQuery(endpoint, maxId, field);
2573
2574 // Fallback to another endpoint if null
2575 if (query == null)
2576 {
2577 var fallbackEndpoint = endpoints[new Random().Next(endpoints.Length)];
2578 query = FetchQuery(fallbackEndpoint.Item1, fallbackEndpoint.Item2, fallbackEndpoint.Item3);
2579 if (query == null) // Last resort, pick another
2580 {
2581 fallbackEndpoint = endpoints[new Random().Next(endpoints.Length)];
2582 query = FetchQuery(fallbackEndpoint.Item1, fallbackEndpoint.Item2, fallbackEndpoint.Item3) ?? "star wars"; // Absolute last fallback
2583 }
2584 }
2585
2586 if (new Random().NextDouble() > 0.4) // 60% chance to add second term
2587 {
2588 var (otherEndpoint, otherMaxId, otherField) = endpoints[new Random().Next(endpoints.Length)];
2589 string otherQuery = FetchQuery(otherEndpoint, otherMaxId, otherField);
2590 if (otherQuery != null)
2591 {
2592 query += $" {otherQuery}"; // e.g., "Yoda Millennium Falcon"
2593 }
2594 else
2595 {
2596 var fallbackEndpoint = endpoints[new Random().Next(endpoints.Length)];
2597 otherQuery = FetchQuery(fallbackEndpoint.Item1, fallbackEndpoint.Item2, fallbackEndpoint.Item3);
2598 query += otherQuery != null ? $" {otherQuery}" : ""; // Skip if still null
2599 }
2600 }
2601
2602 return query;
2603 }
2604
2605 // visit various google sites to generate legit cookies using random search terms
2610 private string GetTargetUrl()
2611 {
2612 var urls = new[]
2613 {
2614 $"https://www.google.com/search?q={Uri.EscapeDataString(GetRandomStarWarsQuery())}", // Search
2615 $"https://www.youtube.com/results?search_query={Uri.EscapeDataString(GetRandomStarWarsQuery())}", // Search
2616 $"https://maps.google.com/?q={Uri.EscapeDataString(GetRandomStarWarsQuery())}", // Search
2617 "https://news.google.com/" // Static
2618 };
2619 return urls[new Random().Next(urls.Length)];
2620 }
2621 private class SwapiResponse
2622 {
2623 public string name { get; set; }
2624 public string title { get; set; }
2625 }
2626
2627 public static string GetFullUrl(string url, IBrowser browser, out bool areRobotsAllowed, bool noRobotsCheck = false)
2628 {
2629 List<string> robotsTxt = new List<string>();
2630 string normalized = url;
2631 areRobotsAllowed = true;
2632
2633 try
2634 {
2635 // Default to HTTPS only if no scheme supplied
2636 if (!Uri.TryCreate(url, UriKind.Absolute, out _))
2637 {
2638 normalized = "https://" + url;
2639 }
2640
2641 // files the browser merely opens or downloads (sitemap.xml, pdfs, images...) are not pages, skip the checks
2642 if (false == noRobotsCheck && true == ((Browser)browser).BrowserSettings.ObeyRobotsTxt && false == UrlHelper.IsNonHtmlResource(normalized))
2643 areRobotsAllowed = UrlHelper.CheckRobotsTxt(normalized, browser, robotsTxt);
2644
2645 if (true == url.StartsWith("https://"))
2646 return url;
2647
2648 if (string.IsNullOrWhiteSpace(url) || UrlHelper.AreEquivalent(url, "https://google.com"))
2649 return "https://google.com";
2650
2651 }
2652 catch (GPALException)
2653 {
2654 throw;
2655 }
2656 catch (Exception ex)
2657 {
2658 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error getting full url", browser, GPALObjectType.Other, ex);
2659 }
2660
2661 return normalized;
2662 }
2663
2669 public static int CalculateHash(Dictionary<string, object> attributes)
2670 {
2671 // 1. Collect and format attributes into a list of strings.
2672 // We use LINQ's OrderBy to sort the dictionary entries by key
2673 // before converting them to strings, ensuring consistent order.
2674 var attrList = attributes.OrderBy(kvp => kvp.Key)
2675 .Select(kvp => $"{kvp.Key}={kvp.Value}")
2676 .ToList();
2677
2678 // 2. Join the sorted attributes into a single string.
2679 string str = string.Join("|", attrList);
2680
2681 // 3. Implement the custom hashing algorithm.
2682 int hash = 0;
2683 foreach (char c in str)
2684 {
2685 // The bitwise operations from the JS code are translated to C#
2686 hash = ((hash << 5) - hash + c) | 0;
2687 }
2688
2689 return hash;
2690 }
2691
2699 public static string GetUserAgentString(BrowserType browserType)
2700 {
2701 // construct headless user-agent
2702 string chromeVersion;
2703 string edgeVersion;
2704 string firefoxVersion;
2705 string retval = null;
2706
2707 if (BrowserType.Chrome == browserType)
2708 {
2709 DriverHelper.GetChromeVersion(out chromeVersion);
2710 // a dotless or empty version means detection failed; slicing to IndexOf(-1) would throw
2711 if (null != chromeVersion && 0 < chromeVersion.IndexOf('.'))
2712 chromeVersion = chromeVersion.AsSpan().Slice(0, chromeVersion.IndexOf('.')).ToString(); // NOTE: fastest way to get the first token
2713 retval = String.Format(GPAL.GPALSettings.ChromeUserAgentTemplate, chromeVersion);
2714 }
2715 else if (BrowserType.Edge == browserType)
2716 {
2717 DriverHelper.GetEdgeVersion(out edgeVersion);
2718 // a dotless or empty version means detection failed; slicing to IndexOf(-1) would throw
2719 if (null != edgeVersion && 0 < edgeVersion.IndexOf('.'))
2720 edgeVersion = edgeVersion.AsSpan().Slice(0, edgeVersion.IndexOf('.')).ToString(); // NOTE: fastest way to get the first token
2721 retval = String.Format(GPAL.GPALSettings.EdgeUserAgentTemplate, edgeVersion);
2722 }
2723 else if (BrowserType.FireFox == browserType)
2724 {
2725 DriverHelper.GetFirefoxVersion(out firefoxVersion);
2726 // a dotless or empty version means detection failed; slicing to IndexOf(-1) would throw
2727 if (null != firefoxVersion && 0 < firefoxVersion.IndexOf('.'))
2728 firefoxVersion = firefoxVersion.AsSpan().Slice(0, firefoxVersion.IndexOf('.')).ToString(); // NOTE: fastest way to get the first token
2729 retval = String.Format(GPAL.GPALSettings.FirefoxUserAgentTemplate, firefoxVersion);
2730 }
2731
2732 return retval;
2733 }
2734
2743 private static void KeepFromChildren(UdpClient client)
2744 {
2745 if (false == Native.SetHandleInformation(client.Client.Handle, Native.HANDLE_FLAG_INHERIT, 0))
2746 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
2747 $"Could not take the inherit flag off the handshake socket, so the next OttoMagic launch may find [47623] still held",
2748 null, GPALObjectType.Browser, new Win32Exception(Marshal.GetLastWin32Error()));
2749 }
2750
2751 internal string GetUserAgent(BrowserSettings browserSettings, List<string> chromeArguments)
2752 {
2753 string userAgent = null;
2754 string browserPath = null;
2755
2756 // starting a whole browser to ask it its own name is the accurate answer and the expensive one, so it
2757 // happens only when the workflow asked for it. Otherwise what is already known is good enough, and it
2758 // is the same string GPAL's own requests present
2759 if (false == browserSettings.UserAgentFromBrowser)
2760 return BrowserHelper.ResolveUserAgent(browserSettings);
2761
2762 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Getting user agent for headless use.", chromeArguments, GPALObjectType.Other);
2763
2764 // Get browser executable path
2765 switch (browserSettings.BrowserType)
2766 {
2767 case BrowserType.Chrome:
2768 browserPath = Puppeteer.GetChromePath();
2769 break;
2770 case BrowserType.Edge:
2771 browserPath = Puppeteer.GetEdgePath();
2772 break;
2773 case BrowserType.FireFox:
2774 browserPath = Puppeteer.GetFirefoxPath();
2775 break;
2776 default:
2777 Exception ex = new Exception("Unsupported Browser Type");
2778 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unsupported browser type [{browserSettings.BrowserType}]", browser, GPALObjectType.Browser, ex);
2779 return GetUserAgentString(browserSettings.BrowserType);
2780 }
2781
2782 UdpClient probeHandshake = null;
2783 bool holdingProbeGate = false;
2784
2785 try
2786 {
2787 List<string> ourArgs = new List<string>(chromeArguments);
2788
2789 // same reason as the launch above: every browser gets its own port or they share a session
2790 if (null == browserSettings.DebugPort)
2791 browserSettings.DebugPort = BrowserHelper.FindFreePort();
2792
2793 if (BrowserType.FireFox != browserSettings.BrowserType)
2794 ourArgs.Add($"--remote-debugging-port={browserSettings.DebugPort.Value}");
2795 else
2796 {
2797 // firefox is asked over the rest api, so listen before starting it: the browser announces the
2798 // port it picked the way every OttoMagic browser does, and nothing else is listening this early
2799 // in a launch. Without it the question goes to whichever browser happens to already be up
2800 try { holdingProbeGate = ottoHandshakeGate.WaitOne(OttoHandshakeGateTimeoutMs); }
2801 catch (AbandonedMutexException) { holdingProbeGate = true; }
2802
2803 probeHandshake = new UdpClient(47623);
2804 probeHandshake.Client.ReceiveTimeout = 30000;
2805
2806 KeepFromChildren(probeHandshake);
2807 }
2808
2809 browserSettings.Process = Process.Start(new ProcessStartInfo
2810 {
2811 FileName = browserPath,
2812 Arguments = string.Join(" ", ourArgs),
2813 UseShellExecute = false
2814 });
2815
2816 // Thread.Sleep(3000);
2817
2818 // NOTE: always use puppeteer to get the useragent, it's very reliable, except ff which doesn't use puppeteer
2819 if (BrowserType.FireFox == browserSettings.BrowserType)
2820 {
2821 var probeEndPoint = new IPEndPoint(IPAddress.Any, 0);
2822 string probePortText = Encoding.UTF8.GetString(probeHandshake.Receive(ref probeEndPoint)).Trim();
2823
2824 // ask the browser we just launched, on the port it just announced
2825 userAgent = true == int.TryParse(probePortText, out int probePort)
2826 ? ((IRESTClient)GPAL.RESTClient.WithAPIBase($"http://localhost:{probePort}/").ToGPALObject()).GetUserAgent().Execute()
2827 : Client.GetUserAgent().Execute();
2828 }
2829 else
2830 {
2831 AutomationEngine aeSave = browserSettings.AutomationEngine;
2832 browserSettings.AutomationEngine = AutomationEngine.PuppeteerPort;
2833 _ = browserSettings.PuppeteerCommunicator;
2834 userAgent = browserSettings.PuppeteerClient.GetUserAgent().Execute();
2835 browserSettings.AutomationEngine = aeSave;
2836 }
2837 userAgent = userAgent.Replace("Headless", "");
2838
2839 //if (true == browserSettings.UsePuppeteer)
2840 {
2841 var parameters = new Dictionary<string, object>();
2842 // gracefully close the browser, release resources, shutdown chrome
2843 //browserSettings.PuppeteerCommunicator.SendCommand(DevToolsMethods.BrowserClose, parameters, null).GetAwaiter().GetResult();
2844 browserSettings.PuppeteerCommunicator._readerReadyTcs?.SetCanceled();
2845 if (false == browserSettings.PuppeteerCommunicator._usePipes)
2846 browserSettings.PuppeteerCommunicator.CloseOutputWebSocketAsync().GetAwaiter();
2847 }
2848
2849 // NOTE: KLUDGE: might not be required but we are, at least for puppeteer getting an error relaunching chrome after getting the useragent
2850 // - we added puppeteer browserclose rather than a hard kill
2851 // Lock file can not be created! Error code: 32
2852 // 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.
2853
2854 // kill the opened chrome and any children used to get the agent string
2855 try
2856 {
2857 var psi = new ProcessStartInfo
2858 {
2859 FileName = "taskkill",
2860 Arguments = $"/PID {browserSettings.Process.Id} /F /T",
2861 CreateNoWindow = true,
2862 UseShellExecute = false
2863 };
2864 Process.Start(psi).WaitForExit();
2865 }
2866 catch { }
2867
2868 Thread.Sleep(1_000);
2869
2870 if (null != browserSettings.Process && false == browserSettings.Process.HasExited)
2871 {
2872 try
2873 {
2874 browserSettings.Process.Kill();
2875 browserSettings.Process.Close();
2876 }
2877 catch { }
2878 }
2879
2880 Thread.Sleep(1_000);
2881
2882 // GPALRestAPI is not killed here: it shuts itself down when the extension's port closes, and killing
2883 // it by name would take down every other browser's rest api too
2884 browserSettings.PuppeteerClient = null;
2885 browserSettings.PuppeteerCommunicator = null;
2886 browserSettings.Process = null;
2887
2888 }
2889 catch (GPALException)
2890 {
2891 throw;
2892 }
2893 catch (Exception ex)
2894 {
2895 userAgent = GetUserAgentString(browserSettings.BrowserType);
2896 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to fetch user-agent using [{userAgent}].", browserSettings, GPALObjectType.Other, ex);
2897 }
2898 finally
2899 {
2900 probeHandshake?.Close();
2901
2902 // the throwaway browser is gone, so the socket and the gate go back before the real launch asks for them
2903 if (true == holdingProbeGate)
2904 ottoHandshakeGate.ReleaseMutex();
2905 }
2906
2907 return false == string.IsNullOrEmpty(userAgent) ? userAgent : GetUserAgentString(browserSettings.BrowserType);
2908 }
2909 #endregion Helpers
2910 }
2911
2912 public class WindowHelper
2913 {
2914 [DllImport("user32.dll")]
2915 [return: MarshalAs(UnmanagedType.Bool)]
2916 private static extern bool SetForegroundWindow(IntPtr hWnd);
2917
2918 [DllImport("user32.dll")]
2919 [return: MarshalAs(UnmanagedType.Bool)]
2920 private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
2921
2922 [DllImport("user32.dll")]
2923 [return: MarshalAs(UnmanagedType.Bool)]
2924 private static extern bool IsIconic(IntPtr hWnd);
2925
2926 [DllImport("user32.dll")]
2927 private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
2928
2929 [DllImport("user32.dll")]
2930 private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
2931
2932 [DllImport("user32.dll")]
2933 private static extern bool IsWindowVisible(IntPtr hWnd);
2934
2935 [DllImport("user32.dll")]
2936 private static extern int GetWindowTextLength(IntPtr hWnd);
2937
2938 private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
2939 private const int SW_RESTORE = 9;
2940
2949 public static bool TopBrowser(Process process, int processId = 0)
2950 {
2951 if ((null == process || true == process?.HasExited) && 0 == processId)
2952 {
2953 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to top browser. Browser has exited.", process, GPALObjectType.Other);
2954 return false;
2955 }
2956
2957 IntPtr windowHandle = IntPtr.Zero;
2958
2959 // 1. If a process ID is explicitly passed, scan the OS for its visible window
2960 if (processId > 0)
2961 {
2962 windowHandle = FindWindowByProcessId(processId);
2963 }
2964
2965 // 2. Fallback to your original logic if the PID scan failed or wasn't requested
2966 if (IntPtr.Zero == windowHandle)
2967 {
2968 // Fixed: Removed process.Handle fallback because process handles cannot be passed to ShowWindow/SetForegroundWindow
2969 windowHandle = process.MainWindowHandle;
2970 }
2971
2972 if (IntPtr.Zero == windowHandle)
2973 {
2974 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to top browser. window handle is IntPtr.Zero", process, GPALObjectType.Other);
2975 return false;
2976 }
2977
2978 // only a minimized window needs restoring. SW_RESTORE on a maximized one un-maximizes it back to
2979 // whatever size it was before, so topping the browser was quietly undoing .Maximize - it maximized,
2980 // then the first navigation topped it and it came back to its startup size
2981 if (true == IsIconic(windowHandle))
2982 ShowWindow(windowHandle, SW_RESTORE);
2983
2984 bool success = SetForegroundWindow(windowHandle);
2985 if (false == success)
2986 {
2987 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to top browser. SetForegroundWindow returned false.", process, GPALObjectType.Other);
2988 }
2989
2990 return success;
2991 }
2992
2993 private static IntPtr FindWindowByProcessId(int processId)
2994 {
2995 IntPtr foundHandle = IntPtr.Zero;
2996
2997 // The visible browser window may belong to the target process OR a descendant of it. Firefox is the
2998 // classic case: geckodriver (the pid we are handed) spawns firefox.exe as a child, and the window
2999 // belongs to that child - a direct pid match finds nothing. So match any visible top-level window
3000 // whose owning pid is the target or a descendant. Prefer a titled window (the main browser window
3001 // has a title; content/helper windows generally do not) so we do not grab a stray popup.
3002 HashSet<int> targetPids = GetProcessAndDescendantIds(processId);
3003
3004 EnumWindows(delegate (IntPtr hWnd, IntPtr lParam)
3005 {
3006 if (IsWindowVisible(hWnd))
3007 {
3008 GetWindowThreadProcessId(hWnd, out uint windowPid);
3009 if (targetPids.Contains((int)windowPid))
3010 {
3011 foundHandle = hWnd; // remember the last match, but keep looking for a titled one
3012 if (0 < GetWindowTextLength(hWnd))
3013 return false; // titled window - this is the main browser window, stop
3014 }
3015 }
3016 return true; // Keep looking
3017 }, IntPtr.Zero);
3018
3019 return foundHandle;
3020 }
3021
3028 private static HashSet<int> GetProcessAndDescendantIds(int rootPid)
3029 {
3030 var result = new HashSet<int> { rootPid };
3031
3032 try
3033 {
3034 var frontier = new Queue<int>();
3035 frontier.Enqueue(rootPid);
3036
3037 while (0 < frontier.Count)
3038 {
3039 int parent = frontier.Dequeue();
3040 using (var searcher = new ManagementObjectSearcher(
3041 $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {parent}"))
3042 {
3043 foreach (ManagementObject obj in searcher.Get())
3044 {
3045 int childPid = Convert.ToInt32(obj["ProcessId"]);
3046 if (result.Add(childPid))
3047 frontier.Enqueue(childPid);
3048 }
3049 }
3050 }
3051 }
3052 catch { /* WMI unavailable - fall back to just the root pid */ }
3053
3054 return result;
3055 }
3056
3057
3058 // Define the Windows API function
3059 [DllImport("user32.dll", SetLastError = true)]
3060 private static extern bool SetWindowPos(
3061 IntPtr hWnd,
3062 IntPtr hWndInsertAfter,
3063 int X,
3064 int Y,
3065 int cx,
3066 int cy,
3067 uint uFlags);
3068
3069 // Define the Windows API constant for getting the window's current position and size
3070 [DllImport("user32.dll")]
3071 private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
3072
3073 // Define the RECT structure for GetWindowRect
3074 [StructLayout(LayoutKind.Sequential)]
3075 private struct RECT
3076 {
3077 public int Left;
3078 public int Top;
3079 public int Right;
3080 public int Bottom;
3081 }
3082
3083 private const uint SWP_NOMOVE = 0x0002;
3084 private const uint SWP_NOSIZE = 0x0001;
3085 private const uint SWP_NOZORDER = 0x0004;
3086
3095 public static void ResizeWindow(
3096 Process process,
3097 Rectangle inRect)
3098 {
3099 // Check if the process has a main window handle
3100 if (process.MainWindowHandle == IntPtr.Zero)
3101 {
3102 return;
3103 }
3104
3105 int maxWwidth = Puppeteer.GetScreenWidthInt();
3106 int maxHeight = Puppeteer.GetScreenHeightInt();
3107
3108 if (inRect.Width > maxWwidth)
3109 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Desired browser width [{inRect.Width}] is greater than the screen width [{maxWwidth}]");
3110
3111 if (inRect.Height > maxHeight)
3112 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Desired browser height [{inRect.Height}] is greater than the screen height [{maxHeight}]");
3113
3114 // Determine the flags for SetWindowPos based on null parameters
3115 uint uFlags = SWP_NOZORDER;
3116
3117 // Call the Windows API function
3118 SetWindowPos(
3119 process.MainWindowHandle,
3120 IntPtr.Zero, // No change to z-order
3121 inRect.Left,
3122 inRect.Top,
3123 inRect.Width,
3124 inRect.Height,
3125 uFlags);
3126 }
3127 }
3128}
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
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....
string GetReadyStatus(string sessionToken)
Gets the ready status NOTE: if no page is loaded, this will return null cause there is no content scr...
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...
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");.
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....
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()
Surf ten websites before starting the workflow to generate some cookies and history captcha,...
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();.
void LeftClickAndDownload(string elementId, string filenameAndPath)
Performs a left click and download on an element by ID browser.MagicHelper.LeftClickAndDownload("link...
void StealthOverrideReferrer()
Overrides referrer stealthily browser.MagicHelper.StealthOverrideReferrer();.
void ScrollWindowByVertical(int pixels)
Scrolls the window vertically by pixels browser.MagicHelper.ScrollWindowByVertical(100);.
void SubmitForm(string elementId)
Submits a form by element ID browser.MagicHelper.SubmitForm("form123");.
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 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....
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)
Asks whether the network has been quiet, and for how long it is willing to wait to find out....
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 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 ScrollWindowByHorizontal(int pixels)
Scrolls the window horizontally by pixels browser.MagicHelper.ScrollWindowByHorizontal(100);.
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:914
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:2406
static IAllowBrowserTypeOrGoto Browser
Instantiates a new fluent Browser object.
Definition GPAL.cs:617
Fluent REST client for making API calls with a chained interface. Supports defining workflows,...
Definition RESTClient.cs:57