31 public string Name {
get;
internal set; }
32 public string Executable {
get;
internal set; }
34 private readonly List<string> _arguments =
new List<string>();
35 private readonly Dictionary<string, string> _environment =
new Dictionary<string, string>();
36 private string _workingDirectory;
37 private Action<string> _outputHandler;
38 private Action<string> _errorHandler;
39 private Process _process;
40 private bool _hasWaitedOrExited;
48 Executable = executable;
61 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cannot add arguments after starting.",
null, GPALObjectType.None);
66 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Argument cannot be null.",
null, GPALObjectType.None,
new ArgumentNullException(nameof(argument)));
69 _arguments.Add(argument);
82 if (
null == directory)
84 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"A working directory cannot be null",
null, GPALObjectType.None);
88 _workingDirectory = directory;
102 if (
null == name ||
null == value)
104 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"An environment variable needs a name and a value, given [{name ?? "NULL
"}] and [{value ?? "NULL
"}]",
null, GPALObjectType.None);
108 _environment[name] = value;
121 _outputHandler = outputHandler;
133 _errorHandler = errorHandler;
143 private static string Quoted(
string argument)
145 if (
false == argument.Contains(
" ") &&
false == argument.Contains(
"\""))
148 return "\"" + argument.Replace(
"\"",
"\\\"") +
"\"";
153 private static readonly HashSet<string> runsDirectly =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
155 ".exe",
".com",
".bat",
".cmd"
165 private static bool IsDocument(
string path)
167 string name = path.Trim().Trim(
'"');
168 name = name.Substring(Math.Max(name.LastIndexOf(
'\\'), name.LastIndexOf(
'/')) + 1);
170 int dot = name.LastIndexOf(
'.');
172 return 0 <= dot && dot < name.Length - 1 &&
false == runsDirectly.Contains(name.Substring(dot));
175 public IAllowRunningExecutable Launch()
177 if (_process !=
null)
179 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Process already started.",
null, GPALObjectType.None);
185 bool opensWithTheShell = Executable.StartsWith(
"http://", StringComparison.OrdinalIgnoreCase)
186 || Executable.StartsWith(
"https://", StringComparison.OrdinalIgnoreCase)
187 ||
true == IsDocument(Executable);
191 bool wantsOurOwnProcess =
null != _outputHandler ||
null != _errorHandler || 0 < _environment.Count;
193 if (
true == opensWithTheShell &&
true == wantsOurOwnProcess)
195 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{Executable}] opens with the shell, which has no output to read and no environment to set. Those settings are ignored",
null, GPALObjectType.None);
196 wantsOurOwnProcess =
false;
197 _outputHandler =
null;
198 _errorHandler =
null;
201 _environment.Clear();
204 bool shellExecute = opensWithTheShell &&
false == wantsOurOwnProcess;
206 _process =
new Process
208 StartInfo =
new ProcessStartInfo
210 FileName = Executable,
211 Arguments =
string.Join(
" ", _arguments.Select(Quoted)),
212 UseShellExecute = shellExecute,
213 RedirectStandardOutput =
null != _outputHandler,
214 RedirectStandardError =
null != _errorHandler,
215 CreateNoWindow = !shellExecute
219 if (
false ==
string.IsNullOrEmpty(_workingDirectory))
220 _process.StartInfo.WorkingDirectory = _workingDirectory;
222 foreach (KeyValuePair<string, string> variable
in _environment)
223 _process.StartInfo.EnvironmentVariables[variable.Key] = variable.Value;
226 if (
null != _outputHandler)
227 _process.OutputDataReceived += (sender, e) => {
if (
null != e.Data) _outputHandler(e.Data); };
229 if (
null != _errorHandler)
230 _process.ErrorDataReceived += (sender, e) => {
if (
null != e.Data) _errorHandler(e.Data); };
237 if (
null != _outputHandler)
238 _process.BeginOutputReadLine();
240 if (
null != _errorHandler)
241 _process.BeginErrorReadLine();
245 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to start [{Executable}]",
null, GPALObjectType.None, ex);
258 bool retVal =
null != _process &&
false == _hasWaitedOrExited &&
false == _process.HasExited;
270 if (
true == Started(
"WaitForExit"))
272 _process.WaitForExit(milliseconds);
273 _hasWaitedOrExited =
true;
284 if (
false == Started(
"Kill") ||
false ==
IsRunning())
288 _hasWaitedOrExited =
true;
296 if (
false == Started(
"KillTree") ||
false ==
IsRunning())
299 KillProcessTree(_process.Id);
301 _hasWaitedOrExited =
true;
303 private void KillProcessTree(
int pid)
308 using (var searcher =
new ManagementObjectSearcher(
309 $
"SELECT * FROM Win32_Process WHERE ParentProcessId = {pid}"))
311 foreach (ManagementObject obj
in searcher.Get())
313 int childPid = Convert.ToInt32(obj[
"ProcessId"]);
314 KillProcessTree(childPid);
317 Process.GetProcessById(childPid).Kill();
319 catch (ArgumentException)
328 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to kill process tree for PID [{pid}]",
null, GPALObjectType.None, ex);
331 public Launcher ToGPALObject()
343 private bool Started(
string wanted)
345 bool retVal =
null != _process;
348 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{wanted}] needs a process, and [{Executable ?? "no executable
"}] has not been launched",
null, GPALObjectType.None);
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...