GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
Launcher.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 DocumentFormat.OpenXml.Wordprocessing;
18using System;
19using System.Collections.Generic;
20using System.Diagnostics;
21using System.Linq;
22using System.Management;
23using System.Text;
24using System.Threading.Tasks;
25using static GenerallyPositive.Enums;
26
27namespace GenerallyPositive
28{
29 // written by Grok
31 {
32 public string Name { get; internal set; }
33 public string Executable { get; internal set; }
34
35 private readonly List<string> _arguments = new List<string>();
36 private Process _process;
37 private bool _hasWaitedOrExited;
38
39 public Launcher()
40 {
41 }
42
43 public IAllowExecutableSetup WithExecutable(string executable)
44 {
45 Executable = executable;
46 return this;
47 }
48
49 public IAllowExecutableSetup WithName(string name)
50 {
51 Name = name;
52 return this;
53 }
54 public IAllowExecutableSetup WithArgument(string argument)
55 {
56 if (_process != null)
57 {
58 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot add arguments after starting.", null, GPALObjectType.None);
59 return this; // Return to allow chaining, but operation is skipped
60 }
61 if (argument == null)
62 {
63 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Argument cannot be null.", null, GPALObjectType.None, new ArgumentNullException(nameof(argument)));
64 return this;
65 }
66 _arguments.Add(argument);
67 return this;
68 }
69
70 public IAllowRunningExecutable Launch()
71 {
72 if (_process != null)
73 {
74 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Process already started.", null, GPALObjectType.None);
75 return this;
76 }
77
78 bool shellExecute = Executable.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
79 || Executable.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
80
81 _process = new Process
82 {
83 StartInfo = new ProcessStartInfo
84 {
85 FileName = Executable,
86 Arguments = string.Join(" ", _arguments),
87 UseShellExecute = shellExecute,
88 RedirectStandardOutput = false,
89 RedirectStandardError = false,
90 CreateNoWindow = !shellExecute
91 }
92 };
93
94 try
95 {
96 _process.Start();
97 return this;
98 }
99 catch (Exception ex)
100 {
101 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to start [{Executable}]", null, GPALObjectType.None, ex);
102 return this; // Return to allow chaining despite failure
103 }
104 }
105
106 public IAllowRunningExecutable RedirectOutput(Action<string> outputHandler)
107 {
108 EnsureRunning(); // This will publish an event if invalid
109 if (_process.StartInfo.RedirectStandardOutput)
110 {
111 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Output already redirected.", null, GPALObjectType.None);
112 return this;
113 }
114
115 _process.StartInfo.RedirectStandardOutput = true;
116 _process.OutputDataReceived += (sender, e) => { if (e.Data != null) outputHandler(e.Data); };
117 if (!_process.HasExited) _process.BeginOutputReadLine();
118 return this;
119 }
120
121 public IAllowRunningExecutable RedirectError(Action<string> errorHandler)
122 {
123 EnsureRunning();
124 if (_process.StartInfo.RedirectStandardError)
125 {
126 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Error already redirected.", null, GPALObjectType.None);
127 return this;
128 }
129
130 _process.StartInfo.RedirectStandardError = true;
131 _process.ErrorDataReceived += (sender, e) => { if (e.Data != null) errorHandler(e.Data); };
132 if (!_process.HasExited) _process.BeginErrorReadLine();
133 return this;
134 }
135
136 public IAllowRunningExecutable SetWorkingDirectory(string directory)
137 {
138 EnsureRunning();
139 if (directory == null)
140 {
141 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Working directory cannot be null.", null, GPALObjectType.None, new ArgumentNullException(nameof(directory)));
142 return this;
143 }
144 _process.StartInfo.WorkingDirectory = directory;
145 return this;
146 }
147
148 public IAllowRunningExecutable SetEnvironmentVariable(string name, string value)
149 {
150 EnsureRunning();
151 if (name == null)
152 {
153 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Environment variable name cannot be null.", null, GPALObjectType.None, new ArgumentNullException(nameof(name)));
154 return this;
155 }
156 if (value == null)
157 {
158 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Environment variable value cannot be null.", null, GPALObjectType.None, new ArgumentNullException(nameof(value)));
159 return this;
160 }
161 _process.StartInfo.EnvironmentVariables[name] = value;
162 return this;
163 }
164
165 public IAllowRunningExecutable IsRunning()
166 {
167 EnsureRunning(); // Logs event if not running
168 return this;
169 }
170
171 public IAllowWaitingExecutable WaitForExit(int milliseconds)
172 {
173 EnsureRunning();
174 _process.WaitForExit(milliseconds);
175 _hasWaitedOrExited = true;
176 return this;
177 }
178
179 public void Kill()
180 {
181 EnsureStarted();
182 if (!_process.HasExited)
183 {
184 _process.Kill();
185 _process.Dispose();
186 _hasWaitedOrExited = true;
187 }
188 }
189
190 public void KillTree()
191 {
192 EnsureStarted();
193 if (!_process.HasExited)
194 {
195 KillProcessTree(_process.Id); // Use internal PID
196 _process.Kill(); // Ensure the root process is killed
197 _process.Dispose();
198 _hasWaitedOrExited = true;
199 }
200 }
201 private void KillProcessTree(int pid)
202 {
203 try
204 {
205 // Use WMI to find child processes
206 using (var searcher = new ManagementObjectSearcher(
207 $"SELECT * FROM Win32_Process WHERE ParentProcessId = {pid}"))
208 {
209 foreach (ManagementObject obj in searcher.Get())
210 {
211 int childPid = Convert.ToInt32(obj["ProcessId"]);
212 KillProcessTree(childPid); // Recursively kill children
213 try
214 {
215 Process.GetProcessById(childPid).Kill();
216 }
217 catch (ArgumentException)
218 {
219 // Process already exited, ignore
220 }
221 }
222 }
223 }
224 catch (Exception ex)
225 {
226 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to kill process tree for PID [{pid}]", null, GPALObjectType.None, ex);
227 }
228 }
229 public Launcher ToGPALObject()
230 {
231 return this;
232 }
233
234 private void EnsureStarted()
235 {
236 if (_process == null)
237 {
238 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Process not started. Call Start() first.", null, GPALObjectType.None);
239 return;
240 }
241 }
242
243 private void EnsureRunning()
244 {
245 EnsureStarted();
246 if (_process.HasExited || _hasWaitedOrExited)
247 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{Executable}] is not running.", null, GPALObjectType.None);
248 else
249 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{Executable}] is running.", null, GPALObjectType.None);
250 return;
251 }
252 }
253}
254
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