GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
DesktopGuard.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.Diagnostics;
19using System.IO;
20using static GenerallyPositive.Enums;
21
23{
38 internal static class DesktopGuard
39 {
41 internal const string ScriptName = "GPALReturnToDefaultDesktop.ps1";
42
44 private const int DefaultDeadlineInSeconds = 600;
45
46 // written next to the binary rather than kept as a file in the repo, so the copy that runs is always the
47 // copy this version of GPAL means and is always beside whatever exe used a hidden desktop
48 private const string script = @"param([int]$ParentPid = 0, [int]$DeadlineSeconds = 0)
49
50# Puts the screen back on the Default desktop.
51#
52# GPAL writes this file next to the binary that used a hidden desktop, and runs it in the background while a
53# peek is on, so a process that dies holding the screen still hands it back.
54#
55# Run it with no arguments to get a stranded screen back by hand. Nothing on a hidden desktop can launch it, so
56# the way in is Ctrl+Alt+Del, Task Manager, File, Run new task, and the path to the .cmd beside this file.
57
58Add-Type -Namespace GPAL -Name Desktops -MemberDefinition @'
59[DllImport(""user32.dll"", SetLastError = true, CharSet = CharSet.Unicode)]
60public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
61
62[DllImport(""user32.dll"", SetLastError = true)]
63public static extern bool SwitchDesktop(IntPtr hDesktop);
64
65[DllImport(""user32.dll"", SetLastError = true)]
66public static extern bool CloseDesktop(IntPtr hDesktop);
67'@
68
69$DesktopSwitchDesktop = 0x0100
70
71if ($ParentPid -gt 0) {
72 $deadline = (Get-Date).AddSeconds($(if ($DeadlineSeconds -gt 0) { $DeadlineSeconds } else { 600 }))
73
74 while ((Get-Date) -lt $deadline) {
75 if ($null -eq (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue)) { break }
76
77 Start-Sleep -Milliseconds 500
78 }
79}
80
81# switching to the desktop already on the screen does nothing, so this is safe whether anything is stranded or not
82$default = [GPAL.Desktops]::OpenDesktop('Default', 0, $false, $DesktopSwitchDesktop)
83
84if ($default -ne [IntPtr]::Zero) {
85 [void][GPAL.Desktops]::SwitchDesktop($default)
86 [void][GPAL.Desktops]::CloseDesktop($default)
87}
88";
89
91 internal static string Folder
92 {
93 get { return AppDomain.CurrentDomain.BaseDirectory; }
94 }
95
101 internal static string Write()
102 {
103 string retVal = null;
104 string path = Path.Combine(Folder, ScriptName);
105
106 // caught rather than thrown, because a folder that cannot be written to is a reason to peek without
107 // a guard and not a reason to fail the peek
108 try
109 {
110 File.WriteAllText(path, script);
111
112 retVal = path;
113 }
114 catch (Exception ex)
115 {
116 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Could not put the desktop recovery script in [{Folder}]", null, GPALObjectType.Browser, ex);
117 }
118
119 return retVal;
120 }
121
128 internal static Process Start(int forMs)
129 {
130 Process retVal = null;
131 string path = Write();
132
133 if (null != path)
134 {
135 int seconds = 0 < forMs ? (forMs / 1000) + 30 : DefaultDeadlineInSeconds;
136
137 ProcessStartInfo starting = new ProcessStartInfo
138 {
139 FileName = "powershell.exe",
140 Arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden " +
141 $"-File \"{path}\" -ParentPid {Process.GetCurrentProcess().Id} -DeadlineSeconds {seconds}",
142 UseShellExecute = false,
143 CreateNoWindow = true,
144 };
145
146 // caught rather than thrown for the same reason Write catches: no guard is worse than a guard,
147 // and both are better than no peek
148 try
149 {
150 retVal = Process.Start(starting);
151 }
152 catch (Exception ex)
153 {
154 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Could not start the desktop guard from [{path}]", null, GPALObjectType.Browser, ex);
155 }
156 }
157
158 return retVal;
159 }
160
166 internal static void Stop(Process guard)
167 {
168 if (null != guard)
169 {
170 // caught rather than thrown because this runs on the way out of a peek, where the screen is
171 // already back and a stray powershell is not worth taking the caller down for
172 try
173 {
174 if (false == guard.HasExited)
175 guard.Kill();
176 }
177 catch (Exception ex)
178 {
179 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Could not stop the desktop guard", null, GPALObjectType.Browser, ex);
180 }
181
182 guard.Dispose();
183 }
184 }
185 }
186}