Plasma Engine  2.0
Loading...
Searching...
No Matches
Process_win.h
1#include <Foundation/FoundationInternal.h>
2PL_FOUNDATION_INTERNAL_HEADER
3
4#include <Foundation/Logging/Log.h>
5#include <Foundation/System/Process.h>
6#include <future>
7
9{
10 HANDLE m_pipeRead = nullptr;
11 HANDLE m_pipeWrite = nullptr;
12 std::thread m_readThread;
13 std::atomic<bool> m_running = false;
14
15 bool IsRunning() const
16 {
17 return m_running;
18 }
19
20 void Create()
21 {
22 SECURITY_ATTRIBUTES saAttr;
23
24 // Set the bInheritHandle flag so pipe handles are inherited.
25 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
26 saAttr.bInheritHandle = TRUE;
27 saAttr.lpSecurityDescriptor = nullptr;
28
29 // Create a pipe for the child process.
30 if (!CreatePipe(&m_pipeRead, &m_pipeWrite, &saAttr, 0))
31 plLog::Error("plPipeWin: CreatePipe failed");
32
33 // Ensure the read handle to the pipe is not inherited.
34 if (!SetHandleInformation(m_pipeRead, HANDLE_FLAG_INHERIT, 0))
35 plLog::Error("Stdout SetHandleInformation");
36 }
37
38 void Close()
39 {
40 if (m_pipeWrite)
41 {
42 CloseHandle(m_pipeWrite);
43 m_pipeWrite = nullptr;
44
45 if (m_readThread.joinable())
46 {
47 m_readThread.join();
48 }
49 CloseHandle(m_pipeRead);
50 m_pipeRead = nullptr;
51 }
52 }
53
54 static void ReportString(plDelegate<void(plStringView)> func, plHybridArray<char, 256>& ref_temp)
55 {
56 plStringBuilder result;
57
58 plUnicodeUtils::RepairNonUtf8Text(ref_temp.GetData(), ref_temp.GetData() + ref_temp.GetCount(), result);
59 func(result);
60 }
61
62 static void ReportString(plDelegate<void(plStringView)> func, const char* szStart, const char* szEnd)
63 {
65
66 while (szStart < szEnd)
67 {
68 tmp.PushBack(*szStart);
69 ++szStart;
70 }
71
72 ReportString(func, tmp);
73 }
74
75 void StartRead(plDelegate<void(plStringView)>& ref_onStdOut)
76 {
77 if (m_pipeWrite)
78 {
79 m_running = true;
80 m_readThread = std::thread([&]()
81 {
82 plHybridArray<char, 256> overflowBuffer;
83
84 constexpr int BUFSIZE = 512;
85 char chBuf[BUFSIZE];
86 while (true)
87 {
88 DWORD bytesRead = 0;
89 bool res = ReadFile(m_pipeRead, chBuf, BUFSIZE, &bytesRead, nullptr);
90 if (!res || bytesRead == 0)
91 {
92 if (!overflowBuffer.IsEmpty())
93 {
94 ReportString(ref_onStdOut, overflowBuffer);
95 }
96 break;
97 }
98
99 const char* szCurrentPos = chBuf;
100 const char* szEndPos = chBuf + bytesRead;
101
102 while (szCurrentPos < szEndPos)
103 {
104 const char* szFound = plStringUtils::FindSubString(szCurrentPos, "\n", szEndPos);
105 if (szFound)
106 {
107 if (overflowBuffer.IsEmpty())
108 {
109 // If there is nothing in the overflow buffer this is a complete line and can be fired as is.
110 ReportString(ref_onStdOut, szCurrentPos, szFound + 1);
111 }
112 else
113 {
114 // We have data in the overflow buffer so this is the final part of a partial line so we need to complete and fire the overflow buffer.
115
116 while (szCurrentPos < szFound + 1)
117 {
118 overflowBuffer.PushBack(*szCurrentPos);
119 ++szCurrentPos;
120 }
121
122 ReportString(ref_onStdOut, overflowBuffer);
123
124 overflowBuffer.Clear();
125 }
126
127 szCurrentPos = szFound + 1;
128 }
129 else
130 {
131 // This is either the start or a middle segment of a line, append to overflow buffer.
132
133 while (szCurrentPos < szEndPos)
134 {
135 overflowBuffer.PushBack(*szCurrentPos);
136 ++szCurrentPos;
137 }
138 }
139 }
140 }
141 m_running = false;
142 //
143 });
144 }
145 }
146};
147
148struct plProcessImpl
149{
150 plOsProcessHandle m_ProcessHandle = nullptr;
151 plOsProcessHandle m_MainThreadHandle = nullptr;
152 plOsProcessID m_ProcessID = 0;
153 plPipeWin m_pipeStdOut;
154 plPipeWin m_pipeStdErr;
155
156 ~plProcessImpl() { Close(); }
157
158 void Close()
159 {
160 if (m_MainThreadHandle != nullptr)
161 {
162 CloseHandle(m_MainThreadHandle);
163 m_MainThreadHandle = nullptr;
164 }
165
166 if (m_ProcessHandle != nullptr)
167 {
168 CloseHandle(m_ProcessHandle);
169 m_ProcessHandle = nullptr;
170 }
171
172 m_pipeStdOut.Close();
173 m_pipeStdErr.Close();
174 }
175};
176
177plProcess::plProcess()
178{
179 m_pImpl = PL_DEFAULT_NEW(plProcessImpl);
180}
181
182plProcess::~plProcess()
183{
184 if (GetState() == plProcessState::Running)
185 {
186 plLog::Dev("Process still running - terminating '{}'", m_sProcess);
187
188 Terminate().IgnoreResult();
189 }
190
191 // Explicitly clear the implementation here so that member
192 // state (e.g. delegates) used by the impl survives the implementation.
193 m_pImpl.Clear();
194}
195
196plOsProcessHandle plProcess::GetProcessHandle() const
197{
198 return m_pImpl->m_ProcessHandle;
199}
200
201plOsProcessID plProcess::GetProcessID() const
202{
203 return m_pImpl->m_ProcessID;
204}
205
206plOsProcessID plProcess::GetCurrentProcessID()
207{
208 const plOsProcessID processID = GetCurrentProcessId();
209 return processID;
210}
211
212
213// Taken from "Programmatically controlling which handles are inherited by new processes in Win32" by Raymond Chen
214// https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873
215static BOOL CreateProcessWithExplicitHandles(LPCWSTR pApplicationName, LPWSTR pCommandLine, LPSECURITY_ATTRIBUTES pProcessAttributes,
216 LPSECURITY_ATTRIBUTES pThreadAttributes, BOOL inheritHandles, DWORD uiCreationFlags, LPVOID pEnvironment, LPCWSTR pCurrentDirectory,
217 LPSTARTUPINFOW pStartupInfo, LPPROCESS_INFORMATION pProcessInformation,
218 // here is the new stuff
219 DWORD uiHandlesToInherit, HANDLE* pHandlesToInherit)
220{
221 BOOL fSuccess;
222 BOOL fInitialized = FALSE;
223 SIZE_T size = 0;
224 LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = nullptr;
225 fSuccess = uiHandlesToInherit < 0xFFFFFFFF / sizeof(HANDLE) && pStartupInfo->cb == sizeof(*pStartupInfo);
226 if (!fSuccess)
227 {
228 SetLastError(ERROR_INVALID_PARAMETER);
229 }
230
231 if (uiHandlesToInherit > 0)
232 {
233 if (fSuccess)
234 {
235 fSuccess = InitializeProcThreadAttributeList(nullptr, 1, 0, &size) || GetLastError() == ERROR_INSUFFICIENT_BUFFER;
236 }
237 if (fSuccess)
238 {
239 lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(HeapAlloc(GetProcessHeap(), 0, size));
240 fSuccess = lpAttributeList != nullptr;
241 }
242 if (fSuccess)
243 {
244 fSuccess = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size);
245 }
246 if (fSuccess)
247 {
248 fInitialized = TRUE;
249 fSuccess = UpdateProcThreadAttribute(
250 lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, pHandlesToInherit, uiHandlesToInherit * sizeof(HANDLE), nullptr, nullptr);
251 }
252 }
253
254 if (fSuccess)
255 {
256 STARTUPINFOEXW info;
257 ZeroMemory(&info, sizeof(info));
258 info.StartupInfo = *pStartupInfo;
259 info.StartupInfo.cb = sizeof(info);
260 info.lpAttributeList = lpAttributeList;
261
262 // it is both possible to pass in (STARTUPINFOW*)&info OR info.StartupInfo ...
263 fSuccess = CreateProcessW(pApplicationName, pCommandLine, pProcessAttributes, pThreadAttributes, inheritHandles,
264 uiCreationFlags | EXTENDED_STARTUPINFO_PRESENT, pEnvironment, pCurrentDirectory, &info.StartupInfo, pProcessInformation);
265 }
266 if (fInitialized)
267 DeleteProcThreadAttributeList(lpAttributeList);
268 if (lpAttributeList)
269 HeapFree(GetProcessHeap(), 0, lpAttributeList);
270 return fSuccess;
271}
272
273plResult plProcess::Launch(const plProcessOptions& opt, plBitflags<plProcessLaunchFlags> launchFlags /*= plAsyncProcessFlags::None*/)
274{
275 PL_ASSERT_DEV(m_pImpl->m_ProcessHandle == nullptr, "Cannot reuse an instance of plProcess");
276 PL_ASSERT_DEV(m_pImpl->m_ProcessID == 0, "Cannot reuse an instance of plProcess");
277
278 plStringBuilder sProcess = opt.m_sProcess;
279 sProcess.MakeCleanPath();
280 sProcess.ReplaceAll("/", "\\");
281
282 m_sProcess = sProcess;
283 m_OnStdOut = opt.m_onStdOut;
284 m_OnStdError = opt.m_onStdError;
285
286 STARTUPINFOW si;
288 si.cb = sizeof(si);
289 si.dwFlags = STARTF_FORCEOFFFEEDBACK; // do not show a wait cursor while launching the process
290
291 // attention: passing in even a single null handle will fail the handle inheritance entirely,
292 // but CreateProcess will still return success
293 // therefore we must ensure to only pass non-null handles to inherit
294 HANDLE HandlesToInherit[2];
295 plUInt32 uiNumHandlesToInherit = 0;
296
297 if (m_OnStdOut.IsValid())
298 {
299 m_pImpl->m_pipeStdOut.Create();
300 si.hStdOutput = m_pImpl->m_pipeStdOut.m_pipeWrite;
301 si.dwFlags |= STARTF_USESTDHANDLES;
302 HandlesToInherit[uiNumHandlesToInherit++] = m_pImpl->m_pipeStdOut.m_pipeWrite;
303 }
304 if (m_OnStdError.IsValid())
305 {
306 m_pImpl->m_pipeStdErr.Create();
307 si.hStdError = m_pImpl->m_pipeStdErr.m_pipeWrite;
308 si.dwFlags |= STARTF_USESTDHANDLES;
309 HandlesToInherit[uiNumHandlesToInherit++] = m_pImpl->m_pipeStdErr.m_pipeWrite;
310 }
311
312 // in theory this can be used to force the process's main window to be in the background,
313 // but except for SW_HIDE and SW_SHOWMINNOACTIVE this doesn't work, and those are not useful
314 // si.wShowWindow = SW_SHOWNOACTIVATE;
315 // si.dwFlags |= STARTF_USESHOWWINDOW;
316
317 PROCESS_INFORMATION pi;
319
320
321 plStringBuilder sCmdLine;
322 BuildFullCommandLineString(opt, sProcess, sCmdLine);
323
324 DWORD dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT;
325
326 if (opt.m_bHideConsoleWindow)
327 {
328 dwCreationFlags |= CREATE_NO_WINDOW;
329 }
330
331 if (launchFlags.IsSet(plProcessLaunchFlags::Suspended))
332 {
333 dwCreationFlags |= CREATE_SUSPENDED;
334 }
335
336 // We pass nullptr as lpApplicationName as setting it would prevent OpenProcess to run system apps or apps in PATH.
337 // Instead, the module name is pre-pended to lpCommandLine in BuildFullCommandLineString.
338 if (!CreateProcessWithExplicitHandles(nullptr, const_cast<wchar_t*>(plStringWChar(sCmdLine).GetData()),
339 nullptr, // lpProcessAttributes
340 nullptr, // lpThreadAttributes
341 uiNumHandlesToInherit > 0 ? TRUE : FALSE, // bInheritHandles
342 dwCreationFlags,
343 nullptr, // lpEnvironment
344 opt.m_sWorkingDirectory.IsEmpty() ? nullptr : plStringWChar(opt.m_sWorkingDirectory).GetData(),
345 &si, // lpStartupInfo
346 &pi, // lpProcessInformation
347 uiNumHandlesToInherit, // cHandlesToInherit
348 HandlesToInherit // rgHandlesToInherit
349 ))
350 {
351 m_pImpl->m_pipeStdOut.Close();
352 m_pImpl->m_pipeStdErr.Close();
353 plLog::Error("Failed to launch '{} {}' - {}", sProcess, plArgSensitive(sCmdLine, "CommandLine"), plArgErrorCode(GetLastError()));
354 return PL_FAILURE;
355 }
356 m_pImpl->m_pipeStdOut.StartRead(m_OnStdOut);
357 m_pImpl->m_pipeStdErr.StartRead(m_OnStdError);
358
359 m_pImpl->m_ProcessHandle = pi.hProcess;
360 m_pImpl->m_ProcessID = pi.dwProcessId;
361
362 if (launchFlags.IsSet(plProcessLaunchFlags::Suspended))
363 {
364 // store the main thread handle for ResumeSuspended() later
365 m_pImpl->m_MainThreadHandle = pi.hThread;
366 }
367 else
368 {
369 CloseHandle(pi.hThread);
370 }
371
372 if (launchFlags.IsSet(plProcessLaunchFlags::Detached))
373 {
374 Detach();
375 }
376
377 return PL_SUCCESS;
378}
379
380plResult plProcess::ResumeSuspended()
381{
382 if (m_pImpl->m_ProcessHandle == nullptr || m_pImpl->m_MainThreadHandle == nullptr)
383 return PL_FAILURE;
384
385 const DWORD prevSuspendCount = ResumeThread(m_pImpl->m_MainThreadHandle);
386 if (prevSuspendCount != 1)
387 plLog::Warning("plProcess::ResumeSuspended: Unexpected ResumeThread result ({})", plUInt64(prevSuspendCount));
388
389 // invalidate the thread handle, so that we cannot resume the process twice
390 if (!CloseHandle(m_pImpl->m_MainThreadHandle))
391 plLog::Warning("plProcess::ResumeSuspended: Failed to close handle");
392 m_pImpl->m_MainThreadHandle = nullptr;
393
394 return PL_SUCCESS;
395}
396
397plResult plProcess::WaitToFinish(plTime timeout /*= plTime::MakeZero()*/)
398{
399 PL_ASSERT_DEV(m_pImpl->m_ProcessHandle != nullptr, "Launch a process before waiting on it");
400 PL_ASSERT_DEV(m_pImpl->m_ProcessID != 0, "Launch a process before waiting on it");
401
402 DWORD dwTimeout = INFINITE;
403
404 if (timeout.IsPositive())
405 dwTimeout = (DWORD)timeout.GetMilliseconds();
406 else
407 dwTimeout = INFINITE;
408
409 const DWORD res = WaitForSingleObject(m_pImpl->m_ProcessHandle, dwTimeout);
410
411 if (res == WAIT_TIMEOUT)
412 {
413 // the process is not yet finished, the timeout was reached
414 return PL_FAILURE;
415 }
416
417 if (res == WAIT_FAILED)
418 {
419 plLog::Error("Failed to wait for '{}' - {}", m_sProcess, plArgErrorCode(GetLastError()));
420 return PL_FAILURE;
421 }
422
423 // the process has finished
424
425 m_pImpl->m_pipeStdOut.Close();
426 m_pImpl->m_pipeStdErr.Close();
427
428 GetExitCodeProcess(m_pImpl->m_ProcessHandle, reinterpret_cast<DWORD*>(&m_iExitCode));
429
430 return PL_SUCCESS;
431}
432
433plResult plProcess::Execute(const plProcessOptions& opt, plInt32* out_pExitCode /*= nullptr*/)
434{
435 plProcess proc;
436
437 PL_SUCCEED_OR_RETURN(proc.Launch(opt));
438 PL_SUCCEED_OR_RETURN(proc.WaitToFinish());
439
440 if (out_pExitCode != nullptr)
441 {
442 *out_pExitCode = proc.GetExitCode();
443 }
444
445 return PL_SUCCESS;
446}
447
448plResult plProcess::Terminate()
449{
450 PL_ASSERT_DEV(m_pImpl->m_ProcessHandle != nullptr, "Launch a process before terminating it");
451 PL_ASSERT_DEV(m_pImpl->m_ProcessID != 0, "Launch a process before terminating it");
452
453 if (TerminateProcess(m_pImpl->m_ProcessHandle, 0xFFFFFFFF) == FALSE)
454 {
455 const DWORD err = GetLastError();
456 if (err == ERROR_ACCESS_DENIED) // this means the process already terminated, so from our perspective the goal was achieved
457 return PL_SUCCESS;
458
459 plLog::Error("Failed to terminate process '{}' - {}", m_sProcess, plArgErrorCode(GetLastError()));
460 return PL_FAILURE;
461 }
462
463 PL_SUCCEED_OR_RETURN(WaitToFinish());
464
465 return PL_SUCCESS;
466}
467
468plProcessState plProcess::GetState() const
469{
470 if (m_pImpl->m_ProcessHandle == 0)
471 return plProcessState::NotStarted;
472
473 DWORD exitCode = 0;
474 if (GetExitCodeProcess(m_pImpl->m_ProcessHandle, &exitCode) == FALSE)
475 {
476 plLog::Error("Failed to retrieve exit code for process '{}' - {}", m_sProcess, plArgErrorCode(GetLastError()));
477
478 // not sure what kind of errors can happen (probably access denied and such)
479 // have to return something, so lets claim the process is finished
480 return plProcessState::Finished;
481 }
482
483 if (exitCode == STILL_ACTIVE)
484 return plProcessState::Running;
485
486 if (m_ProcessExited.IsZero())
487 {
488 m_ProcessExited = plTime::Now();
489 }
490
491 // Do not consider a process finished if the pipe threads have not exited yet.
492 if (m_pImpl->m_pipeStdOut.IsRunning() || m_pImpl->m_pipeStdErr.IsRunning())
493 {
494 if (plTime::Now() - m_ProcessExited < plTime::MakeFromSeconds(2))
495 {
496 return plProcessState::Running;
497 }
498
499 m_pImpl->m_pipeStdOut.Close();
500 m_pImpl->m_pipeStdErr.Close();
501 }
502
503 m_iExitCode = (plInt32)exitCode;
504 return plProcessState::Finished;
505}
506
507void plProcess::Detach()
508{
509 // throw away the previous plProcessImpl and create a blank one
510 m_pImpl = PL_DEFAULT_NEW(plProcessImpl);
511
512 // reset the exit code to the default
513 m_iExitCode = -0xFFFF;
514}
void PushBack(const T &value)
Pushes value at the end of the array.
Definition ArrayBase_inl.h:333
T * GetData()
Returns a pointer to the array data, or nullptr if the array is empty.
Definition ArrayBase_inl.h:423
void Clear()
Clears the array.
Definition ArrayBase_inl.h:184
bool IsEmpty() const
Returns true, if the array does not contain any elements.
Definition ArrayBase_inl.h:178
plUInt32 GetCount() const
Returns the number of active elements in the array.
Definition ArrayBase_inl.h:172
A hybrid array uses in-place storage to handle the first few elements without any allocation....
Definition HybridArray.h:12
static void Dev(plLogInterface *pInterface, const plFormatString &string)
Status information that is nice to have during development.
static void Error(plLogInterface *pInterface, const plFormatString &string)
An error that needs to be fixed as soon as possible.
Definition Log.cpp:375
static void Warning(plLogInterface *pInterface, const plFormatString &string)
A potential problem or a performance warning. Might be possible to ignore it.
Definition Log.cpp:391
static void ZeroFill(T *pDestination, size_t uiCount=1)
Zeros every byte in the provided memory buffer.
plStringBuilder is a class that is meant for creating and modifying strings.
Definition StringBuilder.h:35
void MakeCleanPath()
Removes "../" where possible, replaces all path separators with /, removes double slashes.
Definition StringBuilder.cpp:751
plUInt32 ReplaceAll(plStringView sSearchFor, plStringView sReplacement)
Replaces all occurrences of szSearchFor by szReplacement. Returns the number of replacements.
Definition StringBuilder.cpp:537
static const char * FindSubString(const char *szSource, const char *szStringToFind, const char *pSourceEnd=plUnicodeUtils::GetMaxStringEnd< char >(), const char *szStringToFindEnd=plUnicodeUtils::GetMaxStringEnd< char >())
Searches for the first occurrence of szStringToFind in szSource.
Definition StringUtils.cpp:550
plStringView represent a read-only sub-string of a larger string, as it can store a dedicated string ...
Definition StringView.h:34
A very simple string class that should only be used to temporarily convert text to the OSes native wc...
Definition StringConversion.h:20
static bool RepairNonUtf8Text(const char *pStartData, const char *pEndData, Container &out_result)
Checks an array of char's, whether it is a valid Utf8 string. If not, it repairs the string,...
Definition StringBuilder_inl.h:213
Converts a windows HRESULT into an error code and a human-readable error message. Pass in GetLastErro...
Definition FormatStringArgs.h:140
Wraps a string that may contain sensitive information, such as user file paths.
Definition FormatStringArgs.h:177
The plBitflags class allows you to work with type-safe bitflags.
Definition Bitflags.h:82
PL_ALWAYS_INLINE bool IsSet(Enum flag) const
Checks if certain flags are set within the bitfield.
Definition Bitflags.h:127
A generic delegate class which supports static functions and member functions.
Definition Delegate.h:76
Definition Process_win.h:9
Definition Process_posix.h:152
Default enum for returning failure or success, instead of using a bool.
Definition Types.h:54
The time class encapsulates a double value storing the time in seconds.
Definition Time.h:12
PL_ALWAYS_INLINE constexpr bool IsPositive() const
Checks for a positive time value. This does not include zero.
Definition Time.h:56
PL_ALWAYS_INLINE static constexpr plTime MakeFromSeconds(double fSeconds)
Creates an instance of plTime that was initialized from seconds.
Definition Time.h:30
static plTime Now()
Gets the current time.
Definition Time_Posix.h:12
constexpr double GetMilliseconds() const
Returns the milliseconds value.
Definition Time_inl.h:25