Stroika Library 3.0d24
 
Loading...
Searching...
No Matches
ProcessRunner.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include <sstream>
7
8#if qStroika_Foundation_Common_Platform_POSIX
9#include <fcntl.h>
10#include <signal.h>
11#include <sys/resource.h>
12#include <sys/stat.h>
13#include <sys/types.h>
14#include <sys/wait.h>
15#include <unistd.h>
16#endif
17#if qStroika_Foundation_Common_Platform_MacOS
18#include <dirent.h>
19#endif
20
26#include "Stroika/Foundation/Containers/Sequence.h"
28#if qStroika_Foundation_Common_Platform_Windows
29#include "Stroika/Foundation/Execution/Platform/Windows/Exception.h"
30#endif
31#include "Stroika/Foundation/Execution/Activity.h"
32#include "Stroika/Foundation/Execution/CommandLine.h"
33#include "Stroika/Foundation/Execution/Exceptions.h"
35#include "Stroika/Foundation/Execution/Module.h"
40#include "Stroika/Foundation/IO/FileSystem/FileUtils.h"
43#include "Stroika/Foundation/Memory/Common.h"
46#include "Stroika/Foundation/Streams/MemoryStream.h"
47#include "Stroika/Foundation/Streams/TextToBinary.h"
48
49#include "ProcessRunner.h"
50
51using std::byte;
52
53using namespace Stroika::Foundation;
56using namespace Stroika::Foundation::Debug;
57using namespace Stroika::Foundation::Execution;
58using namespace Stroika::Foundation::Streams;
59using namespace Stroika::Foundation::Traversal;
60
62using Memory::MakeSharedPtr;
64
65// Comment this in to turn on aggressive noisy DbgTrace in this module
66// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
67
68#if USE_NOISY_TRACE_IN_THIS_MODULE_
69#include <fstream>
70#endif
71
72#if qStroika_Foundation_Common_Platform_POSIX
73namespace {
74 // no-except cuz the exception will show up in tracelog, and nothing useful to do, and could be quite bad to except cuz mostly used
75 // in cleanup, and could cause leaks
76 inline void CLOSE_ (int& fd) noexcept
77 {
78 if (fd >= 0) [[likely]] {
79 IgnoreExceptionsForCall (Handle_ErrNoResultInterruption ([fd] () -> int { return ::close (fd); }));
80 fd = -1;
81 }
82 }
83}
84#endif
85
86#if qStroika_Foundation_Common_Platform_POSIX
87namespace {
88 pid_t DoFork_ ()
89 {
90 // we may want to use vfork or some such. But for AIX, it appears best to use f_fork
91 // https://www.ibm.com/support/knowledgecenter/ssw_aix_72/com.ibm.aix.basetrf1/fork.htm
92 // -- LGP 2016-03-31
93 return ::fork ();
94 }
95}
96#endif
97
98#if qStroika_Foundation_Common_Platform_POSIX
99#include <spawn.h>
100namespace {
101 // https://www.ibm.com/support/knowledgecenter/ssw_aix_53/com.ibm.aix.basetechref/doc/basetrf1/posix_spawn.htm%23posix_spawn
102 // http://www.systutorials.com/37124/a-posix_spawn-example-in-c-to-create-child-process-on-linux/
103
104 constexpr bool kUseSpawn_ = false; // 1/2 implemented
105}
106extern char** environ;
107#endif
108
109#if qStroika_Foundation_Common_Platform_Windows
110namespace {
111 class AutoHANDLE_ {
112 public:
113 AutoHANDLE_ (HANDLE h = INVALID_HANDLE_VALUE)
114 : fHandle{h}
115 {
116 }
117 AutoHANDLE_ (const AutoHANDLE_&) = delete;
118 ~AutoHANDLE_ ()
119 {
120 Close ();
121 }
122 AutoHANDLE_& operator= (const AutoHANDLE_& rhs)
123 {
124 if (this != &rhs) {
125 Close ();
126 fHandle = rhs.fHandle;
127 }
128 return *this;
129 }
130 operator HANDLE () const
131 {
132 return fHandle;
133 }
134 HANDLE* operator& ()
135 {
136 return &fHandle;
137 }
138 void Close ()
139 {
140 if (fHandle != INVALID_HANDLE_VALUE) {
141 Verify (::CloseHandle (fHandle));
142 fHandle = INVALID_HANDLE_VALUE;
143 }
144 }
145 void ReplaceHandleAsNonInheritable ()
146 {
147 HANDLE result = INVALID_HANDLE_VALUE;
148 Verify (::DuplicateHandle (::GetCurrentProcess (), fHandle, ::GetCurrentProcess (), &result, 0, FALSE, DUPLICATE_SAME_ACCESS));
149 Verify (::CloseHandle (fHandle));
150 fHandle = result;
151 }
152
153 public:
154 HANDLE fHandle;
155 };
156 inline void SAFE_HANDLE_CLOSER_ (HANDLE* h)
157 {
158 RequireNotNull (h);
159 if (*h != INVALID_HANDLE_VALUE) {
160 Verify (::CloseHandle (*h));
161 *h = INVALID_HANDLE_VALUE;
162 }
163 }
164}
165#endif
166
167namespace {
168 template <Common::IAnyOf<char, wchar_t> CHAR_T>
169 struct String2ContigArrayCStrs_ {
170 StackBuffer<CHAR_T> fBytesBuffer;
171 StackBuffer<CHAR_T*, 10 * sizeof (void*)> fPtrsBuffer;
172 String2ContigArrayCStrs_ (const Mapping<basic_string<CHAR_T>, basic_string<CHAR_T>>& data)
173 : String2ContigArrayCStrs_{data.template Map<Iterable<basic_string<CHAR_T>>> (
174 [] (auto kvp) -> basic_string<CHAR_T> { return kvp.fKey + SDKSTR ("=") + kvp.fValue; })}
175 {
176 }
177 String2ContigArrayCStrs_ (const Iterable<basic_string<CHAR_T>>& data)
178 {
179 StackBuffer<size_t> argsIdx;
180 size_t bufferIndex = 0;
181 for (const basic_string<CHAR_T>& i : data) {
182 fBytesBuffer.push_back (span{i});
183 fBytesBuffer.push_back ('\0');
184 argsIdx.push_back (bufferIndex);
185 bufferIndex = fBytesBuffer.GetSize ();
186 }
187 fBytesBuffer.push_back ('\0'); // not sure - maybe not needed for UNIX, but needed on windows (cuz not using double fPtrsBuffer)
188 auto freeze = fBytesBuffer.begin ();
189 for (size_t i : argsIdx) {
190 fPtrsBuffer.push_back (freeze + i);
191 }
192 fPtrsBuffer.push_back (nullptr);
193 }
194 String2ContigArrayCStrs_ () = delete;
195 String2ContigArrayCStrs_ (const String2ContigArrayCStrs_&) = delete;
196 String2ContigArrayCStrs_ (String2ContigArrayCStrs_&&) = delete;
197 };
198}
199
200#if qStroika_Foundation_Common_Platform_Windows
201namespace {
202// still unsure if needed/useful - I now think the PeekNamedPipe stuff is NOT needed, but
203// I can turn it on if needed -- LGP 2009-05-07
204//#define qUsePeekNamedPipe_ 1
205#ifndef qUsePeekNamedPipe_
206#define qUsePeekNamedPipe_ 0
207#endif
208 /*
209 * This code should all work with the smaller buffer sizes, but is more efficient with larger buffers.
210 * Just set to use the smaller buffers to stress test and debug.
211 *
212 * There is some subtle but serious bug with my pipe code - and that APPEARS to just be that
213 * WaitForMultipleObjects doesn't work with PIPEs.
214 *
215 * I COULD just rewrite a lot of this code to NOT use PIPES - but actual files. That might solve the problem
216 * because they never 'fill up'.
217 *
218 * Alternatively - it might be that my switch to ASYNC mode (PIPE_NOWAIT) was a bad idea. Maybe if I got
219 * rid of that - the WAIT code could be made to work? Not sure.
220 *
221 * Anyhow - this appears to be adequate for now...
222 *
223 * -- LGP 2006-10-17
224 */
225 constexpr size_t kPipeBufSize_ = 256 * 1024;
226 constexpr size_t kReadBufSize_ = 32 * 1024;
227}
228#endif
229
230/*
231 ********************************************************************************
232 ***************** Execution::ProcessRunner::Exception **************************
233 ********************************************************************************
234 */
235String ProcessRunner::Exception::mkMsg_ (const String& errorMessage, const optional<String>& stderrSubset,
236 const optional<ExitStatusType>& wExitStatus, const optional<SignalID>& wTermSig)
237{
239 sb << errorMessage;
240 {
242 if (wExitStatus) {
243 extraMsg << "exit status {}"_f(int (*wExitStatus));
244 }
245 if (wTermSig) {
246 if (not extraMsg.empty ()) {
247 extraMsg << ", "sv;
248 }
249 extraMsg << "terminated by signal {}"_f(int (*wTermSig));
250 }
251 if (not extraMsg.empty ()) {
252 sb << ": "sv << extraMsg;
253 }
254 }
255 if (stderrSubset) {
256 sb << " (captured stderr: "sv
257 << stderrSubset->ReplaceAll ("\\s+"_RegEx, " "sv).LimitLength (100, StringShorteningPreference::ePreferKeepRight) << ")"sv;
258 }
259 return sb;
260}
261
262/*
263 ********************************************************************************
264 **************** Execution::ProcessRunner::ProcessResultType *******************
265 ********************************************************************************
266 */
267void ProcessRunner::ProcessResultType::ThrowIfFailed ()
268{
269 if (fExitStatus and *fExitStatus != 0) {
270 Throw (Exception{"Child process failed"sv, nullopt, *fExitStatus});
271 }
272 if (fTerminatedByUncaughtSignalNumber and *fTerminatedByUncaughtSignalNumber != 0) {
273 Throw (Exception{"Child process failed"sv, nullopt, nullopt, *fTerminatedByUncaughtSignalNumber});
274 }
275}
276
278{
279 StringBuilder sb;
280 sb << "{"sv;
281 if (fExitStatus) {
282 sb << "exitStatus: "sv << fExitStatus;
283 }
284 if (fTerminatedByUncaughtSignalNumber) {
285 if (fExitStatus) {
286 sb << ", "sv;
287 }
288 sb << "terminatedByUncaughtSignalNumber: "sv << fTerminatedByUncaughtSignalNumber;
289 }
290 sb << "}"sv;
291 return sb;
292}
293
294/*
295 ********************************************************************************
296 **************** Execution::ProcessRunner::BackgroundProcess *******************
297 ********************************************************************************
298 */
299ProcessRunner::BackgroundProcess::BackgroundProcess ()
300 : fRep_{MakeSharedPtr<Rep_> ()}
301{
302}
303
305{
306 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
307 Thread::Ptr t{fRep_->fProcessRunner};
309 if (auto o = GetProcessResult ()) {
310 if (o->fExitStatus and o->fExitStatus != ExitStatusType{}) {
311 AssertNotReached (); // I don't think this can happen since it should have resulted in a propagated exception
312 }
313 if (o->fTerminatedByUncaughtSignalNumber) {
314 AssertNotReached (); // I don't think this can happen since it should have resulted in a propagated exception
315 }
316 }
317}
318
320{
321 // tmphack impl
322 Time::TimePointSeconds runUntil = Time::GetTickCount () + timeout;
323 do {
324 if (auto pr = GetChildProcessID ()) {
325 return;
326 }
327 Sleep (1);
328 } while (runUntil > Time::GetTickCount ());
329}
330
332{
333 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
334 Thread::Ptr t{fRep_->fProcessRunner};
335 t.WaitForDone (timeout);
336}
337
339{
340 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
341 Thread::Ptr t{fRep_->fProcessRunner};
342 t.Join (timeout);
343 // if he asserts in PropagateIfException () are wrong, I may need to call that here!
344}
345
347{
348 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
349 Thread::Ptr t{fRep_->fProcessRunner};
350 t.JoinUntil (timeoutAt);
351 // if he asserts in PropagateIfException () are wrong, I may need to call that here!
352}
353
355{
356 TraceContextBumper ctx{"ProcessRunner::BackgroundProcess::Terminate"};
357 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
358 // @todo? set thread to null when done -
359 //
360 // @todo - Note - UNTESTED, and probably not 100% right (esp error checking!!!
361 //
362 if (optional<pid_t> o = fRep_->fDetailedRunnableRep_->fRunningPID.load ()) {
363#if qStroika_Foundation_Common_Platform_POSIX
364 ::kill (SIGTERM, *o);
365#elif qStroika_Foundation_Common_Platform_Windows
366 // @todo - if this OpenProcess gives us any trouble, we can return the handle directory from the 'CreateRunnable' where we invoke the process
367 HANDLE processHandle = ::OpenProcess (PROCESS_TERMINATE, false, *o);
368 if (processHandle != nullptr) {
369 ::TerminateProcess (processHandle, 1);
370 ::CloseHandle (processHandle);
371 }
372 else {
373 DbgTrace ("::OpenProcess returned null: GetLastError () = {}"_f, GetLastError ());
374 }
375#else
377#endif
378 }
379}
381{
382 StringBuilder sb;
383 sb << "{"sv;
384 if (fRep_ and fRep_->fDetailedRunnableRep_) {
385 sb << "processID: "sv << fRep_->fDetailedRunnableRep_->fRunningPID.load ();
386 sb << ", processResult: "sv << fRep_->fDetailedRunnableRep_->fProcessResult.load ();
387 }
388 sb << "}"sv;
389 return sb;
390}
391
392/*
393 ********************************************************************************
394 ************************** Execution::ProcessRunner ****************************
395 ********************************************************************************
396 */
397
398namespace {
400 {
401 return kRawEnvironment ();
402 }
404 {
405 return r;
406 }
408 {
410 for (auto i : env) {
411 r.Add (i.fKey.AsSDKString (), i.fValue.AsSDKString ());
412 }
413 return r;
414 }
416 {
417 Mapping<SDKString, SDKString> r = getEnv_ ();
418#if qStroika_Foundation_Common_Platform_POSIX
419 SDKString path = replacePath.Join<SDKString> ([] (const filesystem::path& p) -> SDKString { return p; }, SDKString{":"sv});
420#elif qStroika_Foundation_Common_Platform_Windows
421 SDKString path = replacePath.Join<SDKString> ([] (const filesystem::path& p) -> SDKString { return p; }, SDKString{L";"sv});
422#endif
423 r.Add (SDKSTR ("PATH"), path);
424 return r;
425 }
426}
427
428ProcessRunner::ProcessRunner (const String& commandLine, const Options& o)
429 : ProcessRunner{commandLine.ContainsAny ({'\'', '\"', '<', '>', '|', '$', '{', '}'}) ? CommandLine{kDefaultShell, commandLine} : CommandLine{commandLine}, o}
430{
431}
432
434 Time::DurationSeconds timeout)
435{
436 TraceContextBumper ctx{"ProcessRunner::Run"};
437 Require (not fOptions_.fDetached);
438 auto activity = LazyEvalActivity ([this] () -> String { return "running '{}'"_f(this->GetCommandLine ()); });
439 DeclareActivity currentActivity{&activity};
440 if (timeout == Time::kInfinity) {
441 fStdIn_ = in;
442 fStdOut_ = out;
443 fStdErr_ = error;
444 auto [runable, results] = CreateDetailedRunnable_ ();
445 runable (); // after runnable called, results should be ready
446 results->fProcessResult.load ().value_or (ProcessResultType{}).ThrowIfFailed ();
447 }
448 else {
449 // Use 'BackgroundProcess' to get a thread we can interrupt when time is up, for timeout
450 BackgroundProcess bp = RunInBackground (in, out, error);
451 [[maybe_unused]] auto&& cleanup = Finally ([&] () noexcept { bp.Terminate (); });
452 bp.Join (timeout);
453 bp.PropagateIfException ();
454 // If we didn't timeout, then the process must have completed, so we must have a process result
455 bp.GetProcessResult ().value_or (ProcessResultType{}).ThrowIfFailed ();
456 }
457}
458
459void ProcessRunner::Run (optional<ProcessResultType>* processResult, ProgressMonitor::Updater /*progress*/, Time::DurationSeconds timeout)
460{
461 TraceContextBumper ctx{"ProcessRunner::Run"}; //DEPREACTED API.... LOSE
462 if (timeout == Time::kInfinity) {
463 if (processResult == nullptr) {
464 CreateSimpleRunnable_ () ();
465 }
466 else {
467 auto [runnable, prDetails] = CreateDetailedRunnable_ ();
468#if qCompilerAndStdLib_NamedAutoLocalBindingNotCapturable_Buggy
469 auto pd2 = prDetails;
470 [[maybe_unused]] auto&& cleanup = Finally ([&] () noexcept { *processResult = pd2->fProcessResult.load (); });
471#else
472 [[maybe_unused]] auto&& cleanup = Finally ([&] () noexcept { *processResult = prDetails->fProcessResult.load (); });
473#endif
474 runnable ();
475 }
476 }
477 else {
478 if (processResult == nullptr) {
479 Thread::Ptr t = Thread::New (CreateSimpleRunnable_ (), Thread::eAutoStart, "ProcessRunner thread"_k);
480 t.Join (timeout);
481 }
482 else {
483 auto [runnable, prDetails] = CreateDetailedRunnable_ ();
484#if qCompilerAndStdLib_NamedAutoLocalBindingNotCapturable_Buggy
485 auto pd2 = prDetails;
486 [[maybe_unused]] auto&& cleanup = Finally ([&] () noexcept { *processResult = pd2->fProcessResult.load (); });
487#else
488 [[maybe_unused]] auto&& cleanup = Finally ([&] () noexcept { *processResult = prDetails->fProcessResult.load (); });
489#endif
490 Thread::Ptr t = Thread::New (runnable, Thread::eAutoStart, "ProcessRunner thread"_k);
491 t.Join (timeout);
492 }
493 }
494}
495
496auto ProcessRunner::Run (const String& cmdStdInValue, const StringOptions& stringOpts, Time::DurationSeconds timeout) -> tuple<String, String>
497{
498 AssertExternallySynchronizedChecker::WriteContext declareContext{fThisAssertExternallySynchronized_};
499 MemoryStream::Ptr<byte> useStdIn = MemoryStream::New<byte> ();
500 MemoryStream::Ptr<byte> useStdOut = MemoryStream::New<byte> ();
501 MemoryStream::Ptr<byte> useStdErr = MemoryStream::New<byte> ();
502
503 auto mkReadStream = [&] (const InputStream::Ptr<byte>& readFromBinStrm) {
504 return stringOpts.fInputCodeCvt ? BinaryToText::Reader::New (readFromBinStrm, *stringOpts.fInputCodeCvt)
505 : BinaryToText::Reader::New (readFromBinStrm);
506 };
507 try {
508 // Prefill stream
509 if (not cmdStdInValue.empty ()) {
510 auto outStream = stringOpts.fOutputCodeCvt ? TextToBinary::Writer::New (useStdIn, *stringOpts.fOutputCodeCvt)
511 : TextToBinary::Writer::New (useStdIn);
512 outStream.Write (cmdStdInValue);
513 }
514 Assert (useStdIn.GetReadOffset () == 0);
515
516 Run (useStdIn, useStdOut, useStdErr, timeout);
517
518 // get and return results from 'useStdOut' etc
519 Assert (useStdOut.GetReadOffset () == 0);
520 Assert (useStdErr.GetReadOffset () == 0);
521 return make_tuple (mkReadStream (useStdOut).ReadAll (), mkReadStream (useStdErr).ReadAll ());
522 }
523 catch (const Exception& e) {
524 String out = mkReadStream (useStdOut).ReadAll ();
525 String err = mkReadStream (useStdErr).ReadAll ();
526#if qStroika_Foundation_Debug_DefaultTracingOn
527 DbgTrace ("Captured stdout: {}"_f, out);
528 DbgTrace ("Captured stderr: {}"_f, err);
529#endif
530 Throw (Exception{e.fFailureMessage, err, e.fExitStatus, e.fTermSignal});
531 Throw (Exception{this->fArgs_.As<String> (), "{}: output: {}, stderr: {}"_f(e.As<String> (), out, err)});
532 }
533 catch (...) {
534 String out = mkReadStream (useStdOut).ReadAll ();
535 String err = mkReadStream (useStdErr).ReadAll ();
536#if qStroika_Foundation_Debug_DefaultTracingOn
537 DbgTrace ("Captured stdout: {}"_f, out);
538 DbgTrace ("Captured stderr: {}"_f, err);
539#endif
540 exception_ptr e = current_exception ();
541 Throw (NestedException{"{} (stderr: {})"_f(e, err), e});
542 }
543}
544
546 const OutputStream::Ptr<byte>& error)
547{
548 TraceContextBumper ctx{"ProcessRunner::RunInBackground"};
549 if (fOptions_.fDetached) {
550 Require (in == nullptr and out == nullptr and error == nullptr); // may lift this restriction in future releases --LGP 2026-01-16
551 }
552 this->fStdIn_ = in;
553 this->fStdOut_ = out;
554 this->fStdErr_ = error;
555 BackgroundProcess result;
556 auto [runnable, prDetails] = CreateDetailedRunnable_ ();
557 result.fRep_->fDetailedRunnableRep_ = prDetails;
558 if (fOptions_.fDetached) {
559 runnable ();
560 }
561 else {
562 result.fRep_->fProcessRunner = Thread::New (runnable, Thread::eAutoStart, "ProcessRunner background thread"sv);
563 }
564 return result;
565}
566
568{
569 TraceContextBumper ctx{"ProcessRunner::RunInBackground"}; // DEPRECATED OVERLOAD
570 BackgroundProcess result;
571 auto [runnable, prDetails] = CreateDetailedRunnable_ ();
572 result.fRep_->fDetailedRunnableRep_ = prDetails;
573 if (fOptions_.fDetached) {
574 runnable ();
575 }
576 else {
577 result.fRep_->fProcessRunner = Thread::New (runnable, Thread::eAutoStart, "ProcessRunner background thread"sv);
578 }
579 return result;
580}
581
585 [[maybe_unused]] ProgressMonitor::Updater progress)
586{
587 TraceContextBumper ctx{"ProcessRunner::RunInBackground"}; // DEPRECATED OVERLOAD
588 return RunInBackground (in, out, error);
589}
590
591[[deprecated ("Since Stroika v3.0d23d")]] void ProcessRunner::Run (const Streams::InputStream::Ptr<byte>& in,
595{
596 TraceContextBumper ctx{"ProcessRunner::Run"}; // DEPRECATED OVERLOAD
597 return Run (in, out, error, timeout);
598}
599[[deprecated ("Since Stroika v3.0d23d")]] tuple<Characters::String, Characters::String>
600ProcessRunner::Run (const Characters::String& cmdStdInValue, const StringOptions& stringOpts, ProgressMonitor::Updater /*progress*/,
601 Time::DurationSeconds timeout)
602{
603 TraceContextBumper ctx{"ProcessRunner::Run"}; // DEPRECATED OVERLOAD
604 return Run (cmdStdInValue, stringOpts, timeout);
605}
606
607#if qStroika_Foundation_Common_Platform_MacOS
608namespace {
609 void closefrom_ (int lowfd)
610 {
611 DIR* dir = ::opendir ("/dev/fd");
612 if (dir == nullptr) {
613 // Fallback to a blind loop if /dev/fd isn't accessible
614 int maxFD = ::getdtablesize ();
615 for (int i = lowfd; i < maxFD; i++) {
616 ::close (i);
617 }
618 return;
619 }
620 for (struct dirent* entry; (entry = ::readdir (dir)) != nullptr;) {
621 char* endptr = nullptr;
622 long fd = ::strtol (entry->d_name, &endptr, 10);
623 // Ensure it's a valid numerical file descriptor
624 if (*endptr == '\0' and fd >= lowfd and fd != ::dirfd (dir)) {
625 ::close (static_cast<int> (fd));
626 }
627 }
628 ::closedir (dir);
629 }
630}
631#endif
632
633#if qStroika_Foundation_Common_Platform_POSIX
634// @todo Good Candidate for REWRITE - this is a MESS!
635void ProcessRunner::Process_Runner_POSIX_ (const shared_ptr<DetailedRunnableRep_>& runneeDetails,
636 [[maybe_unused]] const optional<filesystem::path>& executable, const CommandLine& cmdLine,
637 const ProcessRunner::Options& options, const InputStream::Ptr<byte>& in,
639{
640 optional<mode_t> umask = options.fChildUMask;
641 filesystem::path useCWD = options.fWorkingDirectory.value_or (IO::FileSystem::WellKnownLocations::GetTemporary ());
643 "...,cmdLine='{}',currentDir='{}',..."_f, cmdLine,
644 String{useCWD}.LimitLength (50, StringShorteningPreference::ePreferKeepRight))};
645
646 // track the last few bytes of stderr to include in possible exception messages
647 char trailingStderrBuf[256];
648 char* trailingStderrBufNextByte2WriteAt = begin (trailingStderrBuf);
649 size_t trailingStderrBufNWritten{};
650
651 /*
652 * NOTE:
653 * From http://linux.die.net/man/2/pipe
654 * "The array pipefd is used to return two file descriptors referring to the ends
655 * of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to
656 * the write end of the pipe"
657 */
658 int jStdin[2]{-1, -1};
659 int jStdout[2]{-1, -1};
660 int jStderr[2]{-1, -1};
661 [[maybe_unused]] auto&& cleanup = Finally ([&] () noexcept {
662 ::CLOSE_ (jStdin[0]);
663 ::CLOSE_ (jStdin[1]);
664 ::CLOSE_ (jStdout[0]);
665 ::CLOSE_ (jStdout[1]);
666 ::CLOSE_ (jStderr[0]);
667 ::CLOSE_ (jStderr[1]);
668 });
669 if (in) {
670 Handle_ErrNoResultInterruption ([&jStdin] () -> int { return ::pipe (jStdin); });
671 }
672 else {
673 jStdin[0] = ::open ("/dev/null", O_RDONLY);
674 }
675 if (out) {
676 Handle_ErrNoResultInterruption ([&jStdout] () -> int { return ::pipe (jStdout); });
677 }
678 else {
679 jStdout[1] = ::open ("/dev/null", O_WRONLY);
680 }
681 if (err) {
682 Handle_ErrNoResultInterruption ([&jStderr] () -> int { return ::pipe (jStderr); });
683 }
684 else {
685 jStderr[1] = ::open ("/dev/null", O_WRONLY);
686 }
687 // assert cuz code below needs to be more careful if these can overlap 0..2
688 Assert (in == nullptr or (jStdin[0] >= 3 and jStdin[1] >= 3));
689 Assert (out == nullptr or (jStdout[0] >= 3 and jStdout[1] >= 3));
690 Assert (err == nullptr or (jStderr[0] >= 3 and jStderr[1] >= 3));
691 DbgTrace ("jStdout[0-CHILD] = {} and jStdout[1-PARENT] = {}"_f, jStdout[0], jStdout[1]);
692
693 /*
694 * Note: Important to do all this code before the fork, because once we fork, we, lose other threads
695 * but share copy of RAM, so they COULD have mutexes locked! And we could deadlock waiting on them, so after
696 * fork, we are VERY limited as to what we can safely do.
697 */
698 const char* thisEXEPath_cstr = nullptr;
699 char** thisEXECArgv = nullptr;
700
701 String2ContigArrayCStrs_<char> execDataArgs{
702 cmdLine.GetArguments ().Map<Iterable<string>> ([] (auto si) { return si.AsNarrowSDKString (); })};
703 thisEXEPath_cstr = execDataArgs.fBytesBuffer.data ();
704 thisEXECArgv = execDataArgs.fPtrsBuffer.data ();
705
706 /*
707 * If the file is not accessible, and using fork/exec, we wont find that out til the execvp,
708 * and then there wont be a good way to propagate the error back to the caller.
709 *
710 * @todo for now - this code only checks access for absolute/full path, and we should also check using
711 * PATH and https://linux.die.net/man/3/execvp confstr(_CS_PATH)
712 */
713 if (not kUseSpawn_ and thisEXEPath_cstr[0] == '/' and ::access (thisEXEPath_cstr, R_OK | X_OK) < 0) {
714 errno_t e = errno; // save in case overwritten
715#if USE_NOISY_TRACE_IN_THIS_MODULE_
716 DbgTrace ("failed to access exe path so throwing: exe path='{}'"_f, String::FromNarrowSDKString (thisEXEPath_cstr));
717#endif
718 ThrowPOSIXErrNo (e);
719 }
720
721 pid_t childPID{};
722 if (kUseSpawn_) {
723 posix_spawn_file_actions_t file_actions{};
724 /// @see http://stackoverflow.com/questions/13893085/posix-spawnp-and-piping-child-output-to-a-string
725 // not quite right - maybe not that close
726 /*
727 * move arg stdin/out/err to 0/1/2 file-descriptors. Don't bother with variants that can handle errors/exceptions cuz we cannot really here...
728 */
729 {
730 posix_spawn_file_actions_init (&file_actions);
731 posix_spawn_file_actions_addclose (&file_actions, jStdin[0]);
732 posix_spawn_file_actions_addclose (&file_actions, jStdin[0]);
733 posix_spawn_file_actions_adddup2 (&file_actions, jStdout[1], 1);
734 posix_spawn_file_actions_addclose (&file_actions, jStdout[0]);
735 posix_spawn_file_actions_adddup2 (&file_actions, jStderr[1], 2);
736 posix_spawn_file_actions_addclose (&file_actions, jStderr[1]);
737 }
738 posix_spawnattr_t* attr = nullptr;
739 int status = ::posix_spawnp (&childPID, thisEXEPath_cstr, &file_actions, attr, thisEXECArgv, environ);
740 if (status != 0) {
741 ThrowPOSIXErrNo (status);
742 }
743 }
744 else {
745 childPID = DoFork_ ();
746 ThrowPOSIXErrNoIfNegative (childPID);
747 if (childPID == 0) {
748 if (umask) {
749 (void)::umask (*umask);
750 }
751 try {
752 /*
753 * In child process. Don't DBGTRACE here, or do anything that could raise an exception. In the child process
754 * this would be bad...
755 */
756 DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wunused-result\"")
757 (void)::chdir (useCWD.c_str ());
758 DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wunused-result\"")
759 if (options.fDetached) {
760 /*
761 * See http://pubs.opengroup.org/onlinepubs/007904875/functions/setsid.html
762 * This is similar to setpgrp () but makes doing setpgrp unnecessary.
763 * This is also similar to setpgid (0, 0) - but makes doing that unneeded.
764 *
765 * Avoid signals like SIGHUP when the terminal session ends as well as potentially SIGTTIN and SIGTTOU
766 *
767 * @see http://stackoverflow.com/questions/8777602/why-must-detach-from-tty-when-writing-a-linux-daemon
768 *
769 * Tried using
770 * #if defined _DEFAULT_SOURCE
771 * daemon (0, 0);
772 * #endif
773 * to workaround systemd defaulting to KillMode=control-group
774 */
775 (void)::setsid ();
776 }
777 {
778 /*
779 * move arg stdin/out/err to 0/1/2 file-descriptors. Don't bother with variants that can handle errors/exceptions cuz we cannot really here...
780 */
781 int useSTDIN = jStdin[0];
782 int useSTDOUT = jStdout[1];
783 int useSTDERR = jStderr[1];
784 Assert (useSTDIN >= 0 and useSTDOUT >= 0 and useSTDERR >= 0); // parent can have -1 FDs, but child always has legit FDs
785 ::close (0);
786 ::close (1);
787 ::close (2);
788 ::dup2 (useSTDIN, 0);
789 ::dup2 (useSTDOUT, 1);
790 ::dup2 (useSTDERR, 2);
791 ::close (jStdin[0]);
792 ::close (jStdin[1]);
793 ::close (jStdout[0]);
794 ::close (jStdout[1]);
795 ::close (jStderr[0]);
796 ::close (jStderr[1]);
797 }
798 constexpr bool kCloseAllExtraneousFDsInChild_ = true;
799 if (kCloseAllExtraneousFDsInChild_) {
800 // close all but stdin, stdout, and stderr in child fork
801#if qStroika_Foundation_Common_Platform_MacOS
802 ::closefrom_ (3);
803#else
804 ::closefrom (3);
805#endif
806 }
807 [[maybe_unused]] int r = ::execvp (thisEXEPath_cstr, thisEXECArgv);
808#if USE_NOISY_TRACE_IN_THIS_MODULE_
809 {
810 ofstream myfile;
811 myfile.open ("/tmp/Stroika-ProcessRunner-Exec-Failed-Debug-File.txt");
812 myfile << "thisEXEPath_cstr = " << thisEXEPath_cstr << endl;
813 myfile << "r = " << r << " and errno = " << errno << endl;
814 }
815#endif
816 ::_exit (EXIT_FAILURE);
817 }
818 catch (...) {
819 ::_exit (EXIT_FAILURE);
820 }
821 }
822 }
823 // we got here, the spawn succeeded, or the fork succeeded, and we are the parent process
824 Assert (childPID > 0);
825 {
826 constexpr size_t kStackBufReadAtATimeSize_ = 10 * 1024;
827
828#if USE_NOISY_TRACE_IN_THIS_MODULE_
829 DbgTrace ("In Parent Fork: child process PID={}"_f, childPID);
830#endif
831 if (runneeDetails != nullptr) {
832 runneeDetails->fRunningPID.store (childPID);
833 }
834 /*
835 * WE ARE PARENT
836 */
837 int& useSTDIN = jStdin[1];
838 int& useSTDOUT = jStdout[0];
839 int& useSTDERR = jStderr[0];
840 {
841 CLOSE_ (jStdin[0]);
842 CLOSE_ (jStdout[1]);
843 CLOSE_ (jStderr[1]);
844 }
845
846 // To incrementally read from stderr and stderr as we write to stdin, we must assure
847 // our pipes are non-blocking
848 if (useSTDIN != -1) {
849 ThrowPOSIXErrNoIfNegative (::fcntl (useSTDIN, F_SETFL, fcntl (useSTDIN, F_GETFL, 0) | O_NONBLOCK));
850 }
851 if (useSTDOUT != -1) {
852 ThrowPOSIXErrNoIfNegative (::fcntl (useSTDOUT, F_SETFL, fcntl (useSTDOUT, F_GETFL, 0) | O_NONBLOCK));
853 }
854 if (useSTDERR != -1) {
855 ThrowPOSIXErrNoIfNegative (::fcntl (useSTDERR, F_SETFL, fcntl (useSTDERR, F_GETFL, 0) | O_NONBLOCK));
856 }
857
858 // Throw if any errors except EINTR (which is ignored) or EAGAIN (would block)
859 auto readALittleFromProcess = [&] (int fd, const OutputStream::Ptr<byte>& stream, bool write2StdErrCache, bool* eof = nullptr,
860 bool* maybeMoreData = nullptr) -> void {
861 if (fd == -1) {
862 if (maybeMoreData != nullptr) {
863 *maybeMoreData = false;
864 }
865 if (eof != nullptr) {
866 *eof = true;
867 }
868 return;
869 }
870 uint8_t buf[kStackBufReadAtATimeSize_];
871 int nBytesRead = 0; // int cuz we must allow for errno = EAGAIN error result = -1,
872#if USE_NOISY_TRACE_IN_THIS_MODULE_
873 int skipedThisMany{};
874#endif
875 while ((nBytesRead = ::read (fd, buf, sizeof (buf))) > 0) {
876 Assert (nBytesRead <= sizeof (buf));
877 if (stream != nullptr) {
878 stream.Write (span{buf, static_cast<size_t> (nBytesRead)});
879 }
880 if (write2StdErrCache) {
881 for (size_t i = 0; i < nBytesRead; ++i) {
882 Assert (&trailingStderrBuf[0] <= trailingStderrBufNextByte2WriteAt and trailingStderrBufNextByte2WriteAt < end (trailingStderrBuf));
883 *trailingStderrBufNextByte2WriteAt = buf[i];
884 ++trailingStderrBufNWritten;
885 ++trailingStderrBufNextByte2WriteAt;
886 if (trailingStderrBufNextByte2WriteAt == end (trailingStderrBuf)) {
887 trailingStderrBufNextByte2WriteAt = begin (trailingStderrBuf);
888 }
889 Assert (&trailingStderrBuf[0] <= trailingStderrBufNextByte2WriteAt and trailingStderrBufNextByte2WriteAt < end (trailingStderrBuf));
890 }
891 }
892#if USE_NOISY_TRACE_IN_THIS_MODULE_
893 if (errno == EAGAIN) {
894 // If we get lots of EAGAINS, just skip logging them to avoid spamming the tracelog
895 if (skipedThisMany++ < 100) {
896 continue;
897 }
898 else {
899 DbgTrace ("skipped {} spamming EAGAINs"_f, skipedThisMany);
900 skipedThisMany = 0;
901 }
902 }
903 buf[(nBytesRead == std::size (buf)) ? (std::size (buf) - 1) : nBytesRead] = '\0';
904 DbgTrace ("read from process (fd={}) nBytesRead = {}: {}"_f, fd, nBytesRead,
905 String::FromNarrowSDKString (reinterpret_cast<const char*> (buf)));
906#endif
907 }
908#if USE_NOISY_TRACE_IN_THIS_MODULE_
909 DbgTrace ("from (fd={}) nBytesRead = {}, errno={}"_f, fd, nBytesRead, errno);
910#endif
911 if (nBytesRead < 0) {
912 if (errno != EINTR and errno != EAGAIN) {
913 ThrowPOSIXErrNo (errno);
914 }
915 }
916 if (eof != nullptr) {
917 *eof = (nBytesRead == 0);
918 }
919 if (maybeMoreData != nullptr) {
920 *maybeMoreData = (nBytesRead > 0) or (nBytesRead < 0 and errno == EINTR);
921 }
922 };
923 auto readSoNotBlocking = [&] (int fd, const OutputStream::Ptr<byte>& stream, bool write2StdErrCache) {
924 bool maybeMoreData = true;
925 while (maybeMoreData) {
926 readALittleFromProcess (fd, stream, write2StdErrCache, nullptr, &maybeMoreData);
927 }
928 };
929 auto readTilEOF = [&] (int fd, const OutputStream::Ptr<byte>& stream, bool write2StdErrCache) {
930 if (fd == -1) {
931 return;
932 }
933 WaitForIOReady waiter{fd};
934 bool eof = false;
935 while (not eof) {
936 (void)waiter.WaitQuietly (1s);
937 readALittleFromProcess (fd, stream, write2StdErrCache, &eof);
938 }
939 };
940
941 if (options.fDetached) {
942 Assert (in == nullptr and useSTDOUT == -1 and useSTDERR == -1); // so should skip read/write
943 }
944
945 if (in != nullptr) {
946 byte stdinBuf[kStackBufReadAtATimeSize_];
947 // read to 'in' til it reaches EOF (returns 0). But don't fully block, cuz we want to at least trickle in the stdout/stderr data
948 // even if no input is ready to send to child.
949 while (true) {
950 if (optional<span<byte>> bytesReadFromStdIn = in.ReadNonBlocking (span{stdinBuf})) {
951 Assert (bytesReadFromStdIn->size () <= std::size (stdinBuf));
952 if (bytesReadFromStdIn->empty ()) {
953 break;
954 }
955 else {
956 const byte* e = bytesReadFromStdIn->data () + bytesReadFromStdIn->size ();
957 for (const byte* i = bytesReadFromStdIn->data (); i != e;) {
958 // read stuff from stdout, stderr while pushing to stdin, so that we don't get the PIPE buf too full
959 readSoNotBlocking (useSTDOUT, out, false);
960 readSoNotBlocking (useSTDERR, err, true);
961 int bytesWritten = ThrowPOSIXErrNoIfNegative (Handle_ErrNoResultInterruption ([useSTDIN, i, e] () {
962 int tmp = ::write (useSTDIN, i, e - i);
963 // NOTE: https://linux.die.net/man/2/write appears to indicate on pipe full, write could return 0, or < 0 with errno = EAGAIN, or EWOULDBLOCK
964 if (tmp < 0 and (errno == EAGAIN or errno == EWOULDBLOCK)) {
965 tmp = 0;
966 }
967 return tmp;
968 }));
969 Assert (bytesWritten >= 0);
970 Assert (bytesWritten <= (e - i));
971 i += bytesWritten;
972 if (bytesWritten == 0) {
973 // don't busy wait, but not clear how long to wait? Maybe should only sleep if readSoNotBlocking above returns no change
974 //
975 // OK - this is clearly wrong - @see https://github.com/SophistSolutions/Stroika/issues/725 (STK-589) - Fix performance of ProcessRunner - use select / poll instead of sleep when write to pipe returns 0
976 //
977 Sleep (1ms);
978 }
979 }
980 }
981 }
982 else {
983 // nothing on input stream, so pull from stdout, stderr, and wait a little to avoid busy-waiting
984 readSoNotBlocking (useSTDOUT, out, false);
985 readSoNotBlocking (useSTDERR, err, true);
986 Sleep (100ms);
987 }
988 }
989 }
990 // in case child process reads from its STDIN to EOF
991 CLOSE_ (useSTDIN);
992
993 readTilEOF (useSTDOUT, out, false);
994 readTilEOF (useSTDERR, err, true);
995
996 // Wait for child if not detached (future versions might handle differently, we mix detached with not waiting for child to finish in this routine)
997 if (not options.fDetached) {
998 // not sure we need?
999 int status = 0;
1000 int flags = 0; // FOR NOW - HACK - but really must handle sig-interruptions...
1001 int result =
1002 Handle_ErrNoResultInterruption ([childPID, &status, flags] () -> int { return ::waitpid (childPID, &status, flags); });
1003 // throw / warn if result other than child exited normally
1004 if (runneeDetails != nullptr) {
1005 // not sure what it means if result != childPID??? - I think cannot happen cuz we pass in childPID, less result=-1
1006 runneeDetails->fProcessResult.store (ProcessRunner::ProcessResultType{
1007 WIFEXITED (status) ? WEXITSTATUS (status) : optional<int>{}, WIFSIGNALED (status) ? WTERMSIG (status) : optional<int>{}});
1008 }
1009 else if (result != childPID or not WIFEXITED (status) or WEXITSTATUS (status) != 0) {
1010 // @todo fix this message
1011 DbgTrace ("childPID={}, result={}, status={}, WIFEXITED={}, WEXITSTATUS={}, WIFSIGNALED={}"_f, static_cast<int> (childPID),
1012 result, status, WIFEXITED (status), WEXITSTATUS (status), WIFSIGNALED (status));
1013 StringBuilder stderrMsg;
1014 if (trailingStderrBufNWritten > std::size (trailingStderrBuf)) {
1015 stderrMsg << "..."sv;
1016 stderrMsg << String::FromLatin1 (Memory::ConstSpan (span{trailingStderrBufNextByte2WriteAt, end (trailingStderrBuf)}));
1017 }
1018 stderrMsg << String::FromLatin1 (Memory::ConstSpan (span{begin (trailingStderrBuf), trailingStderrBufNextByte2WriteAt}));
1019 Throw (ProcessRunner::Exception{"Spawned program"sv, stderrMsg.str (), WIFEXITED (status) ? WEXITSTATUS (status) : optional<uint8_t>{},
1020 WIFSIGNALED (status) ? WTERMSIG (status) : optional<uint8_t>{}});
1021 }
1022 }
1023 }
1024}
1025#endif
1026
1027#if qStroika_Foundation_Common_Platform_Windows
1028void ProcessRunner::Process_Runner_Windows_ (const shared_ptr<DetailedRunnableRep_>& runneeDetails, const optional<filesystem::path>& executable,
1029 const CommandLine& cmdLine, const ProcessRunner::Options& options, const InputStream::Ptr<byte>& in,
1030 const OutputStream::Ptr<byte>& out, const OutputStream::Ptr<byte>& err)
1031{
1032 filesystem::path useCWD = options.fWorkingDirectory.value_or (IO::FileSystem::WellKnownLocations::GetTemporary ());
1033 TraceContextBumper ctx{"{}::Process_Runner_Windows_", Stroika_Foundation_Debug_OptionalizeTraceArgs (
1034 "...,cmdLine='{}',currentDir={},..."_f, cmdLine,
1035 String{useCWD}.LimitLength (50, StringShorteningPreference::ePreferKeepRight))};
1036
1037 /*
1038 * o Build directory into which we can copy the JAR file plugin,
1039 * o create STDIN/STDOUT file handles to send/grab results
1040 * o Run the process, waiting for it to finish.
1041 * o Grab results from STDOUT file.
1042 * o Cleanup created directory.
1043 */
1044
1045 // use AutoHANDLE so these are automatically closed at the end of the procedure, whether it ends normally or via
1046 // exception.
1047 AutoHANDLE_ jStdin[2]{INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE};
1048 AutoHANDLE_ jStdout[2]{INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE};
1049 AutoHANDLE_ jStderr[2]{INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE};
1050
1051 PROCESS_INFORMATION processInfo{};
1052 processInfo.hProcess = INVALID_HANDLE_VALUE;
1053 processInfo.hThread = INVALID_HANDLE_VALUE;
1054
1055 try {
1056 {
1057 SECURITY_DESCRIPTOR sd{};
1058 Verify (::InitializeSecurityDescriptor (&sd, SECURITY_DESCRIPTOR_REVISION));
1059 Verify (::SetSecurityDescriptorDacl (&sd, true, 0, false));
1060 SECURITY_ATTRIBUTES sa = {sizeof (SECURITY_ATTRIBUTES), &sd, true};
1061 if (in) {
1062 Verify (::CreatePipe (&jStdin[1], &jStdin[0], &sa, kPipeBufSize_));
1063 }
1064 if (out) {
1065 Verify (::CreatePipe (&jStdout[1], &jStdout[0], &sa, kPipeBufSize_));
1066 }
1067 if (err) {
1068 Verify (::CreatePipe (&jStderr[1], &jStderr[0], &sa, kPipeBufSize_));
1069 }
1070 /*
1071 * Make sure the ends of the pipe WE hang onto are not inheritable, because otherwise the READ
1072 * wont return EOF (until the last one is closed).
1073 */
1074 if (in) {
1075 jStdin[0].ReplaceHandleAsNonInheritable ();
1076 }
1077 if (out) {
1078 jStdout[1].ReplaceHandleAsNonInheritable ();
1079 }
1080 if (err) {
1081 jStderr[1].ReplaceHandleAsNonInheritable ();
1082 }
1083 }
1084
1085 STARTUPINFO startInfo{
1086 .cb = sizeof (startInfo), .dwFlags = STARTF_USESTDHANDLES, .hStdInput = jStdin[1], .hStdOutput = jStdout[0], .hStdError = jStderr[0]};
1087
1088 DWORD createProcFlags{NORMAL_PRIORITY_CLASS};
1089 if (options.fCreateNoWindow) {
1090 createProcFlags |= CREATE_NO_WINDOW;
1091 }
1092 else if (options.fDetached) {
1093 // DETACHED_PROCESS ignored if CREATE_NO_WINDOW
1094 createProcFlags |= DETACHED_PROCESS;
1095 }
1096
1097 {
1098 // UNCLEAR; visual studio system() impl uses true; docs not clear
1099 // BUT - when I use false I get "unknown file: error: C++ exception with description "Spawned program 'echo hi mom' failed: error: 1" thrown in the test body." for some tests
1100 bool bInheritHandles = true;
1101
1102 TCHAR cmdLineBuf[32768]; // crazy MSFT definition! - why this should need to be non-const!
1103 Characters::CString::Copy (cmdLineBuf, std::size (cmdLineBuf), cmdLine.As<String> ().AsSDKString ().c_str ());
1104
1105 optional<filesystem::path> useEXEPath = executable;
1106
1107 // WARN if EXE not in path...
1108#if qStroika_Foundation_Debug_AssertionsChecked
1109 if (useEXEPath) {
1110 if (!FindExecutableInPath (*useEXEPath)) {
1111 DbgTrace ("Warning: Cannot find exe '{}' in PATH ({})"_f, useEXEPath, kPath ());
1112 }
1113 }
1114 else {
1115 // not sure we want to do this? - since first thing could be magic interpreted by shell, like set
1116 auto cmdArgs = cmdLine.GetArguments ();
1117 if (cmdArgs.size () >= 1) {
1118 filesystem::path exe2Find = cmdArgs[0].As<filesystem::path> ();
1119 if (!FindExecutableInPath (exe2Find)) {
1120 DbgTrace ("Warning: Cannot find exe '{}' in PATH ({})"_f, exe2Find, kPath ());
1121 }
1122 }
1123 }
1124#endif
1125
1126 unique_ptr<String2ContigArrayCStrs_<SDKChar>> envBuffer;
1127 LPVOID lpEnvironment = nullptr;
1128 if (options.fEnvironment) {
1129 if (auto oep = get_if<Sequence<filesystem::path>> (&options.fEnvironment.value ())) {
1130 envBuffer = make_unique<String2ContigArrayCStrs_<SDKChar>> (getEnv_ (*oep));
1131 }
1132 else if (auto om = get_if<Mapping<String, String>> (&options.fEnvironment.value ())) {
1133 envBuffer = make_unique<String2ContigArrayCStrs_<SDKChar>> (getEnv_ (*om));
1134 }
1135 else if (auto oms = get_if<Mapping<SDKString, SDKString>> (&options.fEnvironment.value ())) {
1136 envBuffer = make_unique<String2ContigArrayCStrs_<SDKChar>> (getEnv_ (*oms));
1137 }
1138 AssertNotNull (envBuffer);
1139 lpEnvironment = envBuffer->fBytesBuffer.data (); // need to adjust createProcFlags for type used...
1140 if constexpr (same_as<SDKChar, wchar_t>) {
1141 createProcFlags |= CREATE_UNICODE_ENVIRONMENT;
1142 }
1143 }
1144
1145 // see https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
1146 // for complex rules for interpreting nullptr in appname (first) arg, and cmdLineBuf... But mostly - the idea - is
1147 // it runs the search path algorithm and tries to do the right thing
1149 ::CreateProcess (useEXEPath == nullopt ? nullptr : useEXEPath->c_str (), cmdLineBuf, nullptr, nullptr, bInheritHandles,
1150 createProcFlags, lpEnvironment, useCWD.c_str (), &startInfo, &processInfo));
1151 }
1152
1153 if (runneeDetails != nullptr) {
1154 runneeDetails->fRunningPID.store (processInfo.dwProcessId);
1155 }
1156
1157 {
1158 /*
1159 * Remove our copy of the stdin/stdout/stderr which belong to the child (so EOF will work properly).
1160 */
1161 jStdin[1].Close ();
1162 jStdout[0].Close ();
1163 jStderr[0].Close ();
1164 }
1165
1166 AutoHANDLE_& useSTDIN = jStdin[0];
1167 Assert (jStdin[1] == INVALID_HANDLE_VALUE);
1168 AutoHANDLE_& useSTDOUT = jStdout[1];
1169 Assert (jStdout[0] == INVALID_HANDLE_VALUE);
1170 AutoHANDLE_& useSTDERR = jStderr[1];
1171 Assert (jStderr[0] == INVALID_HANDLE_VALUE);
1172
1173 constexpr size_t kStackBufReadAtATimeSize_ = 10 * 1024;
1174
1175 auto readAnyAvailableAndCopy2StreamWithoutBlocking = [] (HANDLE p, const OutputStream::Ptr<byte>& o) {
1176 if (p == INVALID_HANDLE_VALUE) {
1177 return;
1178 }
1179 byte buf[kReadBufSize_];
1180#if qUsePeekNamedPipe_
1181 DWORD nBytesAvail{};
1182#endif
1183 DWORD nBytesRead{};
1184 // Read normally blocks, we don't want to because we may need to write more before it can output
1185 // and we may need to timeout
1186 while (
1187#if qUsePeekNamedPipe_
1188 ::PeekNamedPipe (p, nullptr, nullptr, nullptr, &nBytesAvail, nullptr) and nBytesAvail != 0 and
1189#endif
1190 ::ReadFile (p, buf, sizeof (buf), &nBytesRead, nullptr) and nBytesRead > 0) {
1191 if (o != nullptr) {
1192 o.Write (span{buf, nBytesRead});
1193 }
1194#if USE_NOISY_TRACE_IN_THIS_MODULE_
1195 buf[(nBytesRead == std::size (buf)) ? (std::size (buf) - 1) : nBytesRead] = byte{'\0'};
1196 DbgTrace ("read from process (fd={}) nBytesRead = {}: {}"_f, p, nBytesRead, buf);
1197#endif
1198 }
1199 };
1200
1201 if (options.fDetached) {
1202 Assert (useSTDIN == INVALID_HANDLE_VALUE and useSTDOUT == INVALID_HANDLE_VALUE and useSTDERR == INVALID_HANDLE_VALUE); // so should skip read/write
1203 SAFE_HANDLE_CLOSER_ (&processInfo.hProcess);
1204 SAFE_HANDLE_CLOSER_ (&processInfo.hThread);
1205 return;
1206 }
1207
1208 Assert (processInfo.hProcess != INVALID_HANDLE_VALUE); // not sure why I have if test here - think throw above should prevent this from being INVALID_HANDLE_VALUE --LGP 2026-01-16
1209 if (processInfo.hProcess != INVALID_HANDLE_VALUE) {
1210 {
1211 {
1212 /*
1213 * Set the pipe endpoints to non-blocking mode.
1214 */
1215 auto mkPipeNoWait_ = [] (HANDLE ioHandle) -> void {
1216 if (ioHandle != INVALID_HANDLE_VALUE) {
1217 DWORD mode = 0;
1218 Verify (::GetNamedPipeHandleState (ioHandle, &mode, nullptr, nullptr, nullptr, nullptr, 0));
1219 mode |= PIPE_NOWAIT;
1220 Verify (::SetNamedPipeHandleState (ioHandle, &mode, nullptr, nullptr));
1221 }
1222 };
1223 mkPipeNoWait_ (useSTDIN);
1224 mkPipeNoWait_ (useSTDOUT);
1225 mkPipeNoWait_ (useSTDERR);
1226 }
1227
1228 /*
1229 * Fill child-process' stdin with the source document.
1230 */
1231 if (in != nullptr) {
1232 byte stdinBuf[kStackBufReadAtATimeSize_];
1233 // blocking read to 'in' til it reaches EOF (returns 0)
1234 while (size_t nbytes = in.ReadBlocking (span{stdinBuf}).size ()) {
1235 Assert (nbytes <= std::size (stdinBuf));
1236 const byte* p = begin (stdinBuf);
1237 const byte* e = p + nbytes;
1238 while (p < e) {
1239 DWORD written = 0;
1240 if (::WriteFile (useSTDIN, p, Math::PinToMaxForType<DWORD> (e - p), &written, nullptr) == 0) {
1241 DWORD lastErr = ::GetLastError ();
1242 // sometimes we fail because the target process hasn't read enough and the pipe is full.
1243 // Unfortunately - MSFT doesn't seem to have a single clear error message nor any clear
1244 // documentation about what WriteFile () returns in this case... So there maybe other errors
1245 // that are innocuous that may cause is to prematurely terminate our 'RunExternalProcess'.
1246 // -- LGP 2009-05-07
1247 if (lastErr != ERROR_SUCCESS and lastErr != ERROR_NO_MORE_FILES and lastErr != ERROR_PIPE_BUSY and lastErr != ERROR_NO_DATA) {
1248 DbgTrace ("in RunExternalProcess_ - throwing {} while fill in stdin"_f, lastErr);
1249 ThrowSystemErrNo (lastErr);
1250 }
1251 }
1252 Assert (written <= static_cast<size_t> (e - p));
1253 p += written;
1254 // in case we are failing to write to the stdIn because of blocked output on an outgoing pipe
1255 if (p < e) {
1256 readAnyAvailableAndCopy2StreamWithoutBlocking (useSTDOUT, out);
1257 readAnyAvailableAndCopy2StreamWithoutBlocking (useSTDERR, err);
1258 }
1259 if (p < e and written == 0) {
1260 // if we have more to write, but that the target process hasn't consumed it yet - don't spin trying to
1261 // send it data - back off a little
1262 Sleep (100ms);
1263 }
1264#if 0
1265 // Do timeout handling at a higher level
1266 if (Time::GetTickCount () > timeoutAt) {
1267 DbgTrace (_T ("process timed out (writing initial data) - so throwing up!"));
1268 // then we've timed out - kill the process and DON'T return the partial result!
1269 (void)::TerminateProcess (processInfo.hProcess, -1); // if it exceeded the timeout - kill it (could already be done by now - in which case - this will be ignored - fine...
1270 Throw (Execution::Platform::Windows::Exception (ERROR_TIMEOUT));
1271 }
1272#endif
1273 }
1274 }
1275 }
1276
1277 // in case invoked sub-process is reading, and waiting for EOF before processing...
1278 useSTDIN.Close ();
1279 }
1280
1281 /*
1282 * Must keep reading while waiting - in case the child emits so much information that it
1283 * fills the OS PIPE buffer.
1284 */
1285 int timesWaited = 0;
1286 while (true) {
1287 /*
1288 * It would be nice to be able to WAIT on the PIPEs - but that doesn't appear to work when they
1289 * are in ASYNCRONOUS mode.
1290 *
1291 * So - instead - just wait a very short period, and then retry polling the pipes for more data.
1292 * -- LGP 2006-10-17
1293 */
1294 HANDLE events[1] = {processInfo.hProcess};
1295
1296 // We don't want to busy wait too much, but if its fast (with java, that's rare ;-)) don't want to wait
1297 // too long needlessly...
1298 //
1299 // Also - its not exactly a busy-wait. Its just a wait between reading stuff to avoid buffers filling. If the
1300 // process actually finishes, it will change state and the wait should return immediately.
1301 double remainingTimeout = (timesWaited <= 5) ? 0.1 : 0.5;
1302 DWORD waitResult =
1303 ::WaitForMultipleObjects (static_cast<DWORD> (std::size (events)), events, false, static_cast<int> (remainingTimeout * 1000));
1304 ++timesWaited;
1305
1306 readAnyAvailableAndCopy2StreamWithoutBlocking (useSTDOUT, out);
1307 readAnyAvailableAndCopy2StreamWithoutBlocking (useSTDERR, err);
1308 switch (waitResult) {
1309 case WAIT_OBJECT_0: {
1310#if USE_NOISY_TRACE_IN_THIS_MODULE_
1311 DbgTrace ("external process finished (DONE)"_f);
1312#endif
1313 // timeoutAt = -1.0f; // force out of loop
1314 goto DoneWithProcess;
1315 } break;
1316 case WAIT_TIMEOUT: {
1317 DbgTrace ("still waiting for external process output (WAIT_TIMEOUT)"_f);
1318 }
1319 }
1320 }
1321
1322 DoneWithProcess:
1323 DWORD processExitCode{};
1324 Verify (::GetExitCodeProcess (processInfo.hProcess, &processExitCode));
1325
1326 SAFE_HANDLE_CLOSER_ (&processInfo.hProcess);
1327 SAFE_HANDLE_CLOSER_ (&processInfo.hThread);
1328
1329 readAnyAvailableAndCopy2StreamWithoutBlocking (useSTDOUT, out);
1330 readAnyAvailableAndCopy2StreamWithoutBlocking (useSTDERR, err);
1331
1332 if (runneeDetails == nullptr) {
1333 if (processExitCode != 0) {
1334 Throw (ProcessRunner::Exception{"Child process failed"sv, nullopt, processExitCode});
1335 }
1336 }
1337 else {
1338#if USE_NOISY_TRACE_IN_THIS_MODULE_
1339 DbgTrace ("storing process status (ExitCode): {}"_f, static_cast<int> (processExitCode));
1340#else
1341 if (processExitCode != 0) {
1342 DbgTrace ("storing process status (ExitCode): {}"_f, static_cast<int> (processExitCode));
1343 }
1344#endif
1345 runneeDetails->fProcessResult.store (ProcessRunner::ProcessResultType{static_cast<int> (processExitCode)});
1346 }
1347 }
1348 }
1349 catch (...) {
1350 // sadly and confusingly, CreateProcess() appears to set processInfo.hProcess and processInfo.hThread to nullptr - at least on some failures
1351 if (processInfo.hProcess != INVALID_HANDLE_VALUE and processInfo.hProcess != nullptr) {
1352 (void)::TerminateProcess (processInfo.hProcess, static_cast<UINT> (-1)); // if it exceeded the timeout - kill it
1353 SAFE_HANDLE_CLOSER_ (&processInfo.hProcess);
1354 SAFE_HANDLE_CLOSER_ (&processInfo.hThread);
1355 }
1356 ReThrow ();
1357 }
1358}
1359#endif
1360
1361tuple<function<void ()>, shared_ptr<ProcessRunner::DetailedRunnableRep_>> ProcessRunner::CreateDetailedRunnable_ ()
1362{
1363#if USE_NOISY_TRACE_IN_THIS_MODULE_
1364 TraceContextBumper ctx{"ProcessRunner::CreateDetailedRunnable_"};
1365#endif
1366 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
1367 auto resultDetails = MakeSharedPtr<DetailedRunnableRep_> ();
1368 return make_tuple (
1369 [resultDetails, exe = this->fExecutable_, cmdLine = this->fArgs_, options = fOptions_, in = fStdIn_, out = fStdOut_, err = fStdErr_] () {
1370#if USE_NOISY_TRACE_IN_THIS_MODULE_
1371 TraceContextBumper ctx{"ProcessRunner::CreateDetailedRunnable_::{}::Runner..."};
1372#endif
1373 auto activity = LazyEvalActivity{[&] () { return "executing '{}'"_f(cmdLine); }};
1374 DeclareActivity currentActivity{&activity};
1375#if qStroika_Foundation_Common_Platform_POSIX
1376 Process_Runner_POSIX_ (resultDetails, exe, cmdLine, options, in, out, err);
1377#elif qStroika_Foundation_Common_Platform_Windows
1378 Process_Runner_Windows_ (resultDetails, exe, cmdLine, options, in, out, err);
1379#endif
1380 },
1381 resultDetails);
1382}
1383
1384function<void ()> ProcessRunner::CreateSimpleRunnable_ ()
1385{
1386 TraceContextBumper ctx{"ProcessRunner::CreateSimpleRunnable_"};
1387 Assert (not fOptions_.fDetached); // for now at least, assume detached case handled in details create runner
1388 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
1389 return [exe = this->fExecutable_, cmdLine = this->fArgs_, options = fOptions_, in = fStdIn_, out = fStdOut_, err = fStdErr_] () {
1390#if USE_NOISY_TRACE_IN_THIS_MODULE_
1391 TraceContextBumper ctx{"ProcessRunner::CreateSimpleRunnable_::{}::Runner..."};
1392#endif
1393 auto activity = LazyEvalActivity{[&] () { return "executing '{}'"_f(cmdLine); }};
1394 DeclareActivity currentActivity{&activity};
1395#if qStroika_Foundation_Common_Platform_POSIX
1396 Process_Runner_POSIX_ (nullptr, exe, cmdLine, options, in, out, err);
1397#elif qStroika_Foundation_Common_Platform_Windows
1398 Process_Runner_Windows_ (nullptr, exe, cmdLine, options, in, out, err);
1399#endif
1400 };
1401}
#define AssertNotNull(p)
Definition Assertions.h:334
#define AssertNotImplemented()
Definition Assertions.h:402
#define RequireNotNull(p)
Definition Assertions.h:348
#define AssertNotReached()
Definition Assertions.h:356
#define Verify(c)
Definition Assertions.h:420
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
InlineBuffer< T, BUF_SIZE > StackBuffer
Store variable sized (BUF_SIZE elements) array on the stack (.
Definition StackBuffer.h:80
#define DbgTrace
Definition Trace.h:317
#define Stroika_Foundation_Debug_OptionalizeTraceArgs(...)
Definition Trace.h:278
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual String LimitLength(size_t maxLen, StringShorteningPreference keepPref=StringShorteningPreference::ePreferKeepLeft) const
return the first maxLen (or fewer if string shorter) characters of this string (adding ellipsis if tr...
Definition String.inl:747
static String FromNarrowSDKString(const char *from)
Definition String.inl:472
nonvirtual SDKString AsSDKString() const
Definition String.inl:808
static String FromLatin1(const CHAR_T *cString)
Definition String.inl:357
A Mapping uniquely associates two elements: a key and a value (use Assocation to allow multiple value...
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:188
A generalization of a vector: a container whose elements are keyed by the natural numbers.
unique_lock< AssertExternallySynchronizedChecker > WriteContext
Instantiate AssertExternallySynchronizedChecker::WriteContext to designate an area of code where prot...
shared_lock< const AssertExternallySynchronizedChecker > ReadContext
Instantiate AssertExternallySynchronizedChecker::ReadContext to designate an area of code where prote...
nonvirtual Sequence< String > GetArguments() const
nonvirtual T As(ARGS... args) const
Exception<> is a replacement (subclass) for any std c++ exception class (e.g. the default 'std::excep...
Definition Exceptions.h:157
NestedException contains a new higher level error message (typically based on argument basedOnExcepti...
Definition Exceptions.h:212
nonvirtual void WaitForStarted(Time::DurationSeconds timeout=Time::kInfinity) const
wait until GetChildProcessID () returns a valid answer, or until the process failed to start (in whic...
nonvirtual void WaitForDone(Time::DurationSeconds timeout=Time::kInfinity) const
nonvirtual void Join(Time::DurationSeconds timeout=Time::kInfinity) const
Join () does WaitForDone () and throw exception if there was any error (see PropagateIfException).
nonvirtual void JoinUntil(Time::TimePointSeconds timeoutAt) const
WaitForDoneUntil () and throw exception if there was any error (see PropagateIfException).
Run an external command, with stdin/stdout/stderr as strings or as streams - like perl backticks.
nonvirtual void Run(const Streams::InputStream::Ptr< byte > &in, const Streams::OutputStream::Ptr< byte > &out=nullptr, const Streams::OutputStream::Ptr< byte > &error=nullptr, Time::DurationSeconds timeout=Time::kInfinity)
Run the command synchronously, returning its output, and THROW on any failure.
ProcessRunner()=delete
Construct ProcessRunner with a CommandLine to run (doesn't actually RUN til you call Run or RunInBack...
nonvirtual BackgroundProcess RunInBackground(const Streams::InputStream::Ptr< byte > &in=nullptr, const Streams::OutputStream::Ptr< byte > &out=nullptr, const Streams::OutputStream::Ptr< byte > &error=nullptr)
Run the given external command/process (set by constructor) - with the given arguments in the backgro...
Thread::Ptr is a (unsynchronized) smart pointer referencing an internally synchronized std::thread ob...
Definition Thread.h:334
nonvirtual void Join(Time::DurationSeconds timeout=Time::kInfinity) const
Wait for the pointed-to thread to be done. If the thread completed with an exception (other than thre...
Definition Thread.inl:276
nonvirtual void WaitForDone(Time::DurationSeconds timeout=Time::kInfinity) const
Definition Thread.inl:286
nonvirtual void JoinUntil(Time::TimePointSeconds timeoutAt) const
Wait for the pointed-to thread to be done. If the thread completed with an exception (other than thre...
Definition Thread.inl:280
nonvirtual void ThrowIfDoneWithException() const
Definition Thread.cpp:778
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
nonvirtual optional< ElementType > ReadBlocking() const
ReadBlocking () reads either a single element, or fills in argument intoBuffer - but never blocks (no...
nonvirtual optional< span< ElementType > > ReadNonBlocking(span< ElementType > intoBuffer) const
read into intoBuffer - returning nullopt if would block, and else returning subspan of input with rea...
OutputStream<>::Ptr is Smart pointer to a stream-based sink of data.
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:238
nonvirtual RESULT_T Join(const CONVERT_TO_RESULT &convertToResult=kDefaultToStringConverter<>, const COMBINER &combiner=Characters::kDefaultStringCombiner) const
ape the JavaScript/python 'join' function - take the parts of 'this' iterable and combine them into a...
nonvirtual Iterator< T > begin() const
Support for ranged for, and STL syntax in general.
basic_string< SDKChar > SDKString
Definition SDKString.h:38
Ptr New(const function< void()> &fun2CallOnce, const optional< Characters::String > &name, const optional< Configuration > &configuration)
Definition Thread.cpp:874
void Sleep(Time::Duration seconds2Wait)
Definition Sleep.inl:101
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
const LazyInitialized< Containers::Sequence< filesystem::path > > kPath
Definition Module.cpp:144
EXPECTED::value_type ThrowIfFailed(const EXPECTED &e)
Definition Throw.inl:158
void ThrowPOSIXErrNo(errno_t errNo=errno)
treats errNo as a POSIX errno value, and throws a SystemError (subclass of @std::system_error) except...
auto Handle_ErrNoResultInterruption(CALL call) -> decltype(call())
Handle UNIX EINTR system call behavior - fairly transparently - just effectively removes them from th...
auto Finally(FUNCTION &&f) -> Private_::FinallySentry< FUNCTION >
Definition Finally.inl:31
const LazyInitialized< Containers::Mapping< Characters::SDKString, Characters::SDKString > > kRawEnvironment
convert getenv() to a Mapping of SDKString (in case some issue with charactor set conversion)
Definition Module.cpp:180
optional< filesystem::path > FindExecutableInPath(const filesystem::path &fn)
If fn refers to an executable - return it (using kPATH, and kPathEXT as appropriate)
Definition Module.cpp:245
int pid_t
TODO - maybe move this to configuraiotn module???
Definition Module.h:34
INT_TYPE ThrowPOSIXErrNoIfNegative(INT_TYPE returnCode)
Ptr New(const InputStream::Ptr< byte > &src, optional< AutomaticCodeCvtFlags > codeCvtFlags={}, optional< SeekableFlag > seekable={}, ReadAhead readAhead=eReadAheadAllowed)
Create an InputStream::Ptr<Character> from the arguments (usually binary source) - which can be used ...
Ptr New(const Streams::OutputStream::Ptr< byte > &src, const Characters::CodeCvt<> &char2OutputConverter)