GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
Puppeteer.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.Concurrent;
19using System.Collections.Generic;
20using System.Diagnostics;
21using System.IO;
22using System.Linq;
23using System.Management.Instrumentation;
24using System.Net;
25using System.Net.WebSockets;
26using System.Runtime.InteropServices;
27using System.Text;
28using System.Text.Json;
29using System.Text.Json.Serialization;
30using System.Threading;
31using System.Threading.Tasks;
32using System.Windows.Forms;
33using Microsoft.Win32;
34using static GenerallyPositive.Enums;
35
37{
38 public class Puppeteer
39 {
40 internal string webSocketUrl { get; set; } = null;
41 internal static Dictionary<DevToolsMethods, Func<Newtonsoft.Json.Linq.JObject, object>> resultExtractors { get; set; }
42
43 public static Process LaunchBrowser(IBrowser browser, GPALUrl URL)
44 {
45 Process process = browser.MagicHelper.LaunchBrowser((Browser)browser, URL);
46
47 return process;
48 }
49 public static async Task<string> GetActiveTab(PuppeteerCommunicator communicator, string sessionId)
50 {
51 dynamic targets = await communicator.SendCommand<object>(DevToolsMethods.TargetGetTargets, new { }, null).ConfigureAwait(false);
52
53 // Check if targets and result exist
54 if (targets == null || targets.result == null || targets.result.targetInfos == null)
55 {
56 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "No targets found after closing tab", null, GPALObjectType.None);
57 return null;
58 }
59 foreach (dynamic target in targets.result.targetInfos)
60 {
61 string targetType = target.type?.ToString();
62 string targetUrl = target.url?.ToString();
63 if (targetType == "page" && targetUrl != null)
64 {
65 // Return the first page target as a reasonable guess for the active tab
66 // You can add additional filters (e.g., URL or browserContextId) if needed
67 return target.targetId?.ToString();
68 }
69 }
70
71 // Log available page URLs for debugging
72 var availableUrls = string.Join(", ", ((Newtonsoft.Json.Linq.JArray)targets.result.targetInfos)
73 .Where(t => t["type"]?.ToString() == "page")
74 .Select(t => t["url"]?.ToString() ?? "null"));
75 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No page target found after closing tab. Available page URLs: [{availableUrls}]", null, GPALObjectType.None);
76
77 return null;
78 }
88 internal static string GetWebSocketUrl(string puppeteerUrl, int waitMilliseconds = 20_000)
89 {
90 string retVal = null;
91 Exception lastException = null;
92 Stopwatch waited = Stopwatch.StartNew();
93 int attempts = 0;
94
95 while (null == retVal && waited.ElapsedMilliseconds < waitMilliseconds)
96 {
97 attempts++;
98
99 try
100 {
101 // the loopback address rather than the name, here and nowhere else. Asking for localhost
102 // resolves, tries IPv6 first and waits out a refused connection before falling back, and
103 // this call is made in a loop while a browser is still starting. Everything else keeps the
104 // name, because an extension's permissions and a browser's origin checks are written
105 // against localhost and treat the two as different places
106 HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{puppeteerUrl.Replace("localhost", "127.0.0.1")}/json/version");
107 request.Method = "GET";
108 request.Timeout = 2_000;
109 HttpWebResponse response = (HttpWebResponse)request.GetResponse();
110 using StreamReader reader = new StreamReader(response.GetResponseStream());
111 string json = reader.ReadToEnd();
112 var versionInfo = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
113 retVal = versionInfo["webSocketDebuggerUrl"]; // e.g., ws://localhost:<port>/devtools/browser/<id>
114 }
115 catch (Exception ex)
116 {
117 lastException = ex;
118 Thread.Sleep(250);
119 }
120 }
121
122 waited.Stop();
123
124 // said out loud when it took more than the first ask, because a run that starts slowly on one machine
125 // and not another is worth knowing about before it becomes a failure
126 if (null != retVal && 1 < attempts)
127 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Browser debug port at [{puppeteerUrl}] answered after [{waited.ElapsedMilliseconds}]ms and [{attempts}] attempts", null, GPALObjectType.None);
128 else if (null == retVal)
129 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving Puppeteer WebSocket URL from [{puppeteerUrl}], no answer in [{waited.ElapsedMilliseconds}]ms over [{attempts}] attempts", null, GPALObjectType.None, lastException);
130
131 return retVal;
132 }
133
134
148
152 internal static string GetChromePath()
153 {
154 try
155 {
156 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
157
158 return ResolveExePath(
159 "chrome.exe",
160 new[]
161 {
162 Path.Combine(localAppData, @"Google\Chrome\Application\chrome.exe")
163 },
164 new[]
165 {
166 @"C:\Program Files\Google\Chrome\Application\chrome.exe",
167 @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
168 },
169 new[]
170 {
171 @"C:\Program Files\Google\Chrome Beta\Application\chrome.exe",
172 @"C:\Program Files\Google\Chrome Dev\Application\chrome.exe"
173 }
174 );
175 }
176 catch (Exception ex)
177 {
178 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving Chrome path.", null, GPALObjectType.None, ex);
179 return null;
180 }
181 }
182
186 internal static string GetEdgePath()
187 {
188 try
189 {
190 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
191
192 return ResolveExePath(
193 "msedge.exe",
194 new[]
195 {
196 Path.Combine(localAppData, @"Microsoft\Edge\Application\msedge.exe"),
197 Path.Combine(localAppData, @"Microsoft\Edge Beta\Application\msedge.exe"),
198 Path.Combine(localAppData, @"Microsoft\Edge Dev\Application\msedge.exe"),
199 Path.Combine(localAppData, @"Microsoft\Edge SxS\Application\msedge.exe")
200 },
201 new[]
202 {
203 @"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
204 @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
205 },
206 new string[0]
207 );
208 }
209 catch (Exception ex)
210 {
211 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving MS Edge path.", null, GPALObjectType.None, ex);
212 return null;
213 }
214 }
215
219 internal static string GetFirefoxPath()
220 {
221 try
222 {
223 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
224
225 return ResolveExePath(
226 "firefox.exe",
227 new[]
228 {
229 Path.Combine(localAppData, @"Mozilla Firefox\firefox.exe"),
230 Path.Combine(localAppData, @"Mozilla Firefox Nightly\firefox.exe")
231 },
232 new[]
233 {
234 @"C:\Program Files\Mozilla Firefox\firefox.exe",
235 @"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
236 },
237 new[]
238 {
239 @"C:\Program Files\Firefox Developer Edition\firefox.exe"
240 }
241 );
242 }
243 catch (Exception ex)
244 {
245 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving Firefox path.", null, GPALObjectType.None, ex);
246 return null;
247 }
248 }
249
253 private static string ResolveExePath(string exeName, string[] userPaths, string[] systemPaths, string[] variantPaths)
254 {
255 string result = null;
256
257 try
258 {
259 string keyPath = @"Software\Microsoft\Windows\CurrentVersion\App Paths\" + exeName;
260
261 Func<string>[] resolvers = new Func<string>[]
262 {
263 () => GetFromRegistry(RegistryHive.LocalMachine, keyPath, RegistryView.Registry64),
264 () => GetFromRegistry(RegistryHive.LocalMachine, keyPath, RegistryView.Registry32),
265 () => GetFromRegistry(RegistryHive.CurrentUser, keyPath, RegistryView.Default),
266 () => GetFromWhere(exeName),
267 () => GetFromPaths(userPaths),
268 () => GetFromPaths(systemPaths),
269 () => GetFromPaths(variantPaths)
270 };
271
272 foreach (var resolver in resolvers)
273 {
274 if (result == null)
275 {
276 result = resolver();
277 }
278 }
279 }
280 catch
281 {
282 // Intentionally silent; caller handles logging
283 }
284
285 return result;
286 }
287
291 private static string GetFromRegistry(RegistryHive hive, string keyPath, RegistryView view)
292 {
293 try
294 {
295 using (var baseKey = RegistryKey.OpenBaseKey(hive, view))
296 using (var key = baseKey.OpenSubKey(keyPath))
297 {
298 if (key != null)
299 {
300 object value = key.GetValue(string.Empty);
301 if (value != null)
302 {
303 string path = value.ToString();
304 if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
305 {
306 return path;
307 }
308 }
309 }
310 }
311 }
312 catch
313 {
314 // Ignore and continue fallback chain
315 }
316
317 return null;
318 }
319
323 private static string GetFromWhere(string exeName)
324 {
325 try
326 {
327 var psi = new ProcessStartInfo
328 {
329 FileName = "where",
330 Arguments = exeName,
331 RedirectStandardOutput = true,
332 UseShellExecute = false,
333 CreateNoWindow = true
334 };
335
336 using (var process = Process.Start(psi))
337 {
338 if (process != null)
339 {
340 string line = process.StandardOutput.ReadLine();
341 if (!string.IsNullOrWhiteSpace(line) && File.Exists(line))
342 {
343 return line;
344 }
345 }
346 }
347 }
348 catch
349 {
350 // Ignore and continue fallback chain
351 }
352
353 return null;
354 }
355
359 private static string GetFromPaths(string[] paths)
360 {
361 if (paths == null)
362 {
363 return null;
364 }
365
366 foreach (var path in paths)
367 {
368 if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
369 {
370 return path;
371 }
372 }
373
374 return null;
375 }
376
377
378 public static string GetScreenMaxSize()
379 {
380 int maxWidth = 0;
381 int maxHeight = 0;
382
383 foreach (Screen screen in Screen.AllScreens)
384 {
385 if (screen.Bounds.Width > maxWidth)
386 {
387 maxWidth = screen.Bounds.Width;
388 }
389
390 if (screen.Bounds.Height > maxHeight)
391 {
392 maxHeight = screen.Bounds.Height;
393 }
394 }
395
396 return $"{maxWidth},{maxHeight}";
397 }
398 public static string GetScreenWidth()
399 {
400 int maxWidth = 0;
401
402 foreach (Screen screen in Screen.AllScreens)
403 {
404 if (screen.Bounds.Width > maxWidth)
405 {
406 maxWidth = screen.Bounds.Width;
407 }
408 }
409
410 return $"{maxWidth}";
411 }
412 public static string GetScreenHeight()
413 {
414 int maxHeight = 0;
415
416 foreach (Screen screen in Screen.AllScreens)
417 {
418 if (screen.Bounds.Height > maxHeight)
419 {
420 maxHeight = screen.Bounds.Height;
421 }
422 }
423
424 return $"{maxHeight}";
425 }
426
427 public static int GetScreenWidthInt()
428 {
429 int maxWidth = 0;
430
431 foreach (Screen screen in Screen.AllScreens)
432 {
433 if (screen.Bounds.Width > maxWidth)
434 {
435 maxWidth = screen.Bounds.Width;
436 }
437 }
438
439 return maxWidth;
440 }
441 public static int GetScreenHeightInt()
442 {
443 int maxHeight = 0;
444
445 foreach (Screen screen in Screen.AllScreens)
446 {
447 if (screen.Bounds.Height > maxHeight)
448 {
449 maxHeight = screen.Bounds.Height;
450 }
451 }
452
453 return maxHeight;
454 }
455
456 }
457}
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
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568