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 HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{puppeteerUrl}/json/version");
102 request.Method = "GET";
103 request.Timeout = 2_000;
104 HttpWebResponse response = (HttpWebResponse)request.GetResponse();
105 using StreamReader reader = new StreamReader(response.GetResponseStream());
106 string json = reader.ReadToEnd();
107 var versionInfo = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
108 retVal = versionInfo["webSocketDebuggerUrl"]; // e.g., ws://localhost:<port>/devtools/browser/<id>
109 }
110 catch (Exception ex)
111 {
112 lastException = ex;
113 Thread.Sleep(250);
114 }
115 }
116
117 waited.Stop();
118
119 // said out loud when it took more than the first ask, because a run that starts slowly on one machine
120 // and not another is worth knowing about before it becomes a failure
121 if (null != retVal && 1 < attempts)
122 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Browser debug port at [{puppeteerUrl}] answered after [{waited.ElapsedMilliseconds}]ms and [{attempts}] attempts", null, GPALObjectType.None);
123 else if (null == retVal)
124 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving Puppeteer WebSocket URL from [{puppeteerUrl}], no answer in [{waited.ElapsedMilliseconds}]ms over [{attempts}] attempts", null, GPALObjectType.None, lastException);
125
126 return retVal;
127 }
128
129
143
147 internal static string GetChromePath()
148 {
149 try
150 {
151 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
152
153 return ResolveExePath(
154 "chrome.exe",
155 new[]
156 {
157 Path.Combine(localAppData, @"Google\Chrome\Application\chrome.exe")
158 },
159 new[]
160 {
161 @"C:\Program Files\Google\Chrome\Application\chrome.exe",
162 @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
163 },
164 new[]
165 {
166 @"C:\Program Files\Google\Chrome Beta\Application\chrome.exe",
167 @"C:\Program Files\Google\Chrome Dev\Application\chrome.exe"
168 }
169 );
170 }
171 catch (Exception ex)
172 {
173 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving Chrome path.", null, GPALObjectType.None, ex);
174 return null;
175 }
176 }
177
181 internal static string GetEdgePath()
182 {
183 try
184 {
185 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
186
187 return ResolveExePath(
188 "msedge.exe",
189 new[]
190 {
191 Path.Combine(localAppData, @"Microsoft\Edge\Application\msedge.exe"),
192 Path.Combine(localAppData, @"Microsoft\Edge Beta\Application\msedge.exe"),
193 Path.Combine(localAppData, @"Microsoft\Edge Dev\Application\msedge.exe"),
194 Path.Combine(localAppData, @"Microsoft\Edge SxS\Application\msedge.exe")
195 },
196 new[]
197 {
198 @"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
199 @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
200 },
201 new string[0]
202 );
203 }
204 catch (Exception ex)
205 {
206 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving MS Edge path.", null, GPALObjectType.None, ex);
207 return null;
208 }
209 }
210
214 internal static string GetFirefoxPath()
215 {
216 try
217 {
218 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
219
220 return ResolveExePath(
221 "firefox.exe",
222 new[]
223 {
224 Path.Combine(localAppData, @"Mozilla Firefox\firefox.exe"),
225 Path.Combine(localAppData, @"Mozilla Firefox Nightly\firefox.exe")
226 },
227 new[]
228 {
229 @"C:\Program Files\Mozilla Firefox\firefox.exe",
230 @"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
231 },
232 new[]
233 {
234 @"C:\Program Files\Firefox Developer Edition\firefox.exe"
235 }
236 );
237 }
238 catch (Exception ex)
239 {
240 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"ERROR retrieving Firefox path.", null, GPALObjectType.None, ex);
241 return null;
242 }
243 }
244
248 private static string ResolveExePath(string exeName, string[] userPaths, string[] systemPaths, string[] variantPaths)
249 {
250 string result = null;
251
252 try
253 {
254 string keyPath = @"Software\Microsoft\Windows\CurrentVersion\App Paths\" + exeName;
255
256 Func<string>[] resolvers = new Func<string>[]
257 {
258 () => GetFromRegistry(RegistryHive.LocalMachine, keyPath, RegistryView.Registry64),
259 () => GetFromRegistry(RegistryHive.LocalMachine, keyPath, RegistryView.Registry32),
260 () => GetFromRegistry(RegistryHive.CurrentUser, keyPath, RegistryView.Default),
261 () => GetFromWhere(exeName),
262 () => GetFromPaths(userPaths),
263 () => GetFromPaths(systemPaths),
264 () => GetFromPaths(variantPaths)
265 };
266
267 foreach (var resolver in resolvers)
268 {
269 if (result == null)
270 {
271 result = resolver();
272 }
273 }
274 }
275 catch
276 {
277 // Intentionally silent; caller handles logging
278 }
279
280 return result;
281 }
282
286 private static string GetFromRegistry(RegistryHive hive, string keyPath, RegistryView view)
287 {
288 try
289 {
290 using (var baseKey = RegistryKey.OpenBaseKey(hive, view))
291 using (var key = baseKey.OpenSubKey(keyPath))
292 {
293 if (key != null)
294 {
295 object value = key.GetValue(string.Empty);
296 if (value != null)
297 {
298 string path = value.ToString();
299 if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
300 {
301 return path;
302 }
303 }
304 }
305 }
306 }
307 catch
308 {
309 // Ignore and continue fallback chain
310 }
311
312 return null;
313 }
314
318 private static string GetFromWhere(string exeName)
319 {
320 try
321 {
322 var psi = new ProcessStartInfo
323 {
324 FileName = "where",
325 Arguments = exeName,
326 RedirectStandardOutput = true,
327 UseShellExecute = false,
328 CreateNoWindow = true
329 };
330
331 using (var process = Process.Start(psi))
332 {
333 if (process != null)
334 {
335 string line = process.StandardOutput.ReadLine();
336 if (!string.IsNullOrWhiteSpace(line) && File.Exists(line))
337 {
338 return line;
339 }
340 }
341 }
342 }
343 catch
344 {
345 // Ignore and continue fallback chain
346 }
347
348 return null;
349 }
350
354 private static string GetFromPaths(string[] paths)
355 {
356 if (paths == null)
357 {
358 return null;
359 }
360
361 foreach (var path in paths)
362 {
363 if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
364 {
365 return path;
366 }
367 }
368
369 return null;
370 }
371
372
373 public static string GetScreenMaxSize()
374 {
375 int maxWidth = 0;
376 int maxHeight = 0;
377
378 foreach (Screen screen in Screen.AllScreens)
379 {
380 if (screen.Bounds.Width > maxWidth)
381 {
382 maxWidth = screen.Bounds.Width;
383 }
384
385 if (screen.Bounds.Height > maxHeight)
386 {
387 maxHeight = screen.Bounds.Height;
388 }
389 }
390
391 return $"{maxWidth},{maxHeight}";
392 }
393 public static string GetScreenWidth()
394 {
395 int maxWidth = 0;
396
397 foreach (Screen screen in Screen.AllScreens)
398 {
399 if (screen.Bounds.Width > maxWidth)
400 {
401 maxWidth = screen.Bounds.Width;
402 }
403 }
404
405 return $"{maxWidth}";
406 }
407 public static string GetScreenHeight()
408 {
409 int maxHeight = 0;
410
411 foreach (Screen screen in Screen.AllScreens)
412 {
413 if (screen.Bounds.Height > maxHeight)
414 {
415 maxHeight = screen.Bounds.Height;
416 }
417 }
418
419 return $"{maxHeight}";
420 }
421
422 public static int GetScreenWidthInt()
423 {
424 int maxWidth = 0;
425
426 foreach (Screen screen in Screen.AllScreens)
427 {
428 if (screen.Bounds.Width > maxWidth)
429 {
430 maxWidth = screen.Bounds.Width;
431 }
432 }
433
434 return maxWidth;
435 }
436 public static int GetScreenHeightInt()
437 {
438 int maxHeight = 0;
439
440 foreach (Screen screen in Screen.AllScreens)
441 {
442 if (screen.Bounds.Height > maxHeight)
443 {
444 maxHeight = screen.Bounds.Height;
445 }
446 }
447
448 return maxHeight;
449 }
450
451 }
452}
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:2406