Browse Source

Add reproc to handle processes

Panagiotis Christopoulos Charitos 3 years ago
parent
commit
17e6d28e3f
100 changed files with 8337 additions and 416 deletions
  1. 15 18
      AnKi/Importer/ImageImporter.cpp
  2. 2 2
      AnKi/Importer/ImageImporter.h
  3. 2 2
      AnKi/ShaderCompiler/MaliOfflineCompiler.cpp
  4. 10 4
      AnKi/Util/CMakeLists.txt
  5. 216 0
      AnKi/Util/Process.cpp
  6. 15 27
      AnKi/Util/Process.h
  7. 0 300
      AnKi/Util/ProcessPosix.cpp
  8. 0 51
      AnKi/Util/ProcessWindows.cpp
  9. 5 0
      AnKi/Util/StdTypes.h
  10. 7 0
      CMakeLists.txt
  11. 62 12
      Tests/Util/Process.cpp
  12. 1076 0
      ThirdParty/Reproc/CHANGELOG.md
  13. 32 0
      ThirdParty/Reproc/CMakeLists.txt
  14. 21 0
      ThirdParty/Reproc/LICENSE
  15. 311 0
      ThirdParty/Reproc/README.md
  16. 408 0
      ThirdParty/Reproc/cmake/reproc.cmake
  17. 22 0
      ThirdParty/Reproc/reproc++/CMakeLists.txt
  18. 89 0
      ThirdParty/Reproc/reproc++/examples/background.cpp
  19. 76 0
      ThirdParty/Reproc/reproc++/examples/drain.cpp
  20. 83 0
      ThirdParty/Reproc/reproc++/examples/forward.cpp
  21. 24 0
      ThirdParty/Reproc/reproc++/examples/run.cpp
  22. 59 0
      ThirdParty/Reproc/reproc++/include/reproc++/arguments.hpp
  23. 53 0
      ThirdParty/Reproc/reproc++/include/reproc++/detail/array.hpp
  24. 18 0
      ThirdParty/Reproc/reproc++/include/reproc++/detail/type_traits.hpp
  25. 152 0
      ThirdParty/Reproc/reproc++/include/reproc++/drain.hpp
  26. 76 0
      ThirdParty/Reproc/reproc++/include/reproc++/env.hpp
  27. 21 0
      ThirdParty/Reproc/reproc++/include/reproc++/export.hpp
  28. 37 0
      ThirdParty/Reproc/reproc++/include/reproc++/input.hpp
  29. 223 0
      ThirdParty/Reproc/reproc++/include/reproc++/reproc.hpp
  30. 41 0
      ThirdParty/Reproc/reproc++/include/reproc++/run.hpp
  31. 13 0
      ThirdParty/Reproc/reproc++/reproc++-config.cmake.in
  32. 13 0
      ThirdParty/Reproc/reproc++/reproc++.pc.in
  33. 168 0
      ThirdParty/Reproc/reproc++/src/reproc.cpp
  34. 62 0
      ThirdParty/Reproc/reproc/CMakeLists.txt
  35. 62 0
      ThirdParty/Reproc/reproc/examples/drain.c
  36. 21 0
      ThirdParty/Reproc/reproc/examples/env.c
  37. 42 0
      ThirdParty/Reproc/reproc/examples/parent.c
  38. 18 0
      ThirdParty/Reproc/reproc/examples/path.c
  39. 107 0
      ThirdParty/Reproc/reproc/examples/poll.c
  40. 108 0
      ThirdParty/Reproc/reproc/examples/read.c
  41. 19 0
      ThirdParty/Reproc/reproc/examples/run.c
  42. 79 0
      ThirdParty/Reproc/reproc/include/reproc/drain.h
  43. 21 0
      ThirdParty/Reproc/reproc/include/reproc/export.h
  44. 530 0
      ThirdParty/Reproc/reproc/include/reproc/reproc.h
  45. 27 0
      ThirdParty/Reproc/reproc/include/reproc/run.h
  46. 12 0
      ThirdParty/Reproc/reproc/reproc-config.cmake.in
  47. 12 0
      ThirdParty/Reproc/reproc/reproc.pc.in
  48. 10 0
      ThirdParty/Reproc/reproc/resources/argv.c
  49. 7 0
      ThirdParty/Reproc/reproc/resources/deadline.c
  50. 13 0
      ThirdParty/Reproc/reproc/resources/env.c
  51. 15 0
      ThirdParty/Reproc/reproc/resources/io.c
  52. 14 0
      ThirdParty/Reproc/reproc/resources/overflow.c
  53. 10 0
      ThirdParty/Reproc/reproc/resources/path.c
  54. 15 0
      ThirdParty/Reproc/reproc/resources/pid.c
  55. 18 0
      ThirdParty/Reproc/reproc/resources/sleep.h
  56. 7 0
      ThirdParty/Reproc/reproc/resources/stop.c
  57. 21 0
      ThirdParty/Reproc/reproc/resources/working-directory.c
  58. 5 0
      ThirdParty/Reproc/reproc/src/clock.h
  59. 17 0
      ThirdParty/Reproc/reproc/src/clock.posix.c
  60. 14 0
      ThirdParty/Reproc/reproc/src/clock.windows.c
  61. 121 0
      ThirdParty/Reproc/reproc/src/drain.c
  62. 25 0
      ThirdParty/Reproc/reproc/src/error.h
  63. 31 0
      ThirdParty/Reproc/reproc/src/error.posix.c
  64. 62 0
      ThirdParty/Reproc/reproc/src/error.windows.c
  65. 20 0
      ThirdParty/Reproc/reproc/src/handle.h
  66. 42 0
      ThirdParty/Reproc/reproc/src/handle.posix.c
  67. 27 0
      ThirdParty/Reproc/reproc/src/handle.windows.c
  68. 5 0
      ThirdParty/Reproc/reproc/src/init.h
  69. 10 0
      ThirdParty/Reproc/reproc/src/init.posix.c
  70. 28 0
      ThirdParty/Reproc/reproc/src/init.windows.c
  71. 11 0
      ThirdParty/Reproc/reproc/src/macro.h
  72. 137 0
      ThirdParty/Reproc/reproc/src/options.c
  73. 7 0
      ThirdParty/Reproc/reproc/src/options.h
  74. 46 0
      ThirdParty/Reproc/reproc/src/pipe.h
  75. 141 0
      ThirdParty/Reproc/reproc/src/pipe.posix.c
  76. 265 0
      ThirdParty/Reproc/reproc/src/pipe.windows.c
  77. 66 0
      ThirdParty/Reproc/reproc/src/process.h
  78. 501 0
      ThirdParty/Reproc/reproc/src/process.posix.c
  79. 510 0
      ThirdParty/Reproc/reproc/src/process.windows.c
  80. 164 0
      ThirdParty/Reproc/reproc/src/redirect.c
  81. 25 0
      ThirdParty/Reproc/reproc/src/redirect.h
  82. 79 0
      ThirdParty/Reproc/reproc/src/redirect.posix.c
  83. 117 0
      ThirdParty/Reproc/reproc/src/redirect.windows.c
  84. 695 0
      ThirdParty/Reproc/reproc/src/reproc.c
  85. 54 0
      ThirdParty/Reproc/reproc/src/run.c
  86. 88 0
      ThirdParty/Reproc/reproc/src/strv.c
  87. 7 0
      ThirdParty/Reproc/reproc/src/strv.h
  88. 13 0
      ThirdParty/Reproc/reproc/src/utf.h
  89. 3 0
      ThirdParty/Reproc/reproc/src/utf.posix.c
  90. 39 0
      ThirdParty/Reproc/reproc/src/utf.windows.c
  91. 33 0
      ThirdParty/Reproc/reproc/test/argv.c
  92. 43 0
      ThirdParty/Reproc/reproc/test/assert.h
  93. 10 0
      ThirdParty/Reproc/reproc/test/deadline.c
  94. 36 0
      ThirdParty/Reproc/reproc/test/env.c
  95. 39 0
      ThirdParty/Reproc/reproc/test/fork.c
  96. 74 0
      ThirdParty/Reproc/reproc/test/io.c
  97. 17 0
      ThirdParty/Reproc/reproc/test/overflow.c
  98. 41 0
      ThirdParty/Reproc/reproc/test/path.c
  99. 37 0
      ThirdParty/Reproc/reproc/test/pid.c
  100. 32 0
      ThirdParty/Reproc/reproc/test/stop.c

+ 15 - 18
AnKi/Importer/ImageImporter.cpp

@@ -403,9 +403,9 @@ static void linearToSRgbBatch(WeakArray<TVec> pixels, TFunc func)
 		}
 		else
 		{
-			pixel.x() = p.x();
-			pixel.y() = p.y();
-			pixel.z() = p.z();
+			pixel.x() = S(p.x());
+			pixel.y() = S(p.y());
+			pixel.z() = S(p.z());
 		}
 	}
 }
@@ -588,8 +588,9 @@ static void generateSurfaceMipmap(ConstWeakArray<U8, PtrSize> inBuffer, U32 inWi
 }
 
 static ANKI_USE_RESULT Error compressS3tc(GenericMemoryPoolAllocator<U8> alloc, CString tempDirectory,
-										  CString compressonatorPath, ConstWeakArray<U8, PtrSize> inPixels, U32 inWidth,
-										  U32 inHeight, U32 channelCount, Bool hdr, WeakArray<U8, PtrSize> outPixels)
+										  CString compressonatorFilename, ConstWeakArray<U8, PtrSize> inPixels,
+										  U32 inWidth, U32 inHeight, U32 channelCount, Bool hdr,
+										  WeakArray<U8, PtrSize> outPixels)
 {
 	ANKI_ASSERT(inPixels.getSizeInBytes()
 				== PtrSize(inWidth) * inHeight * channelCount * ((hdr) ? sizeof(F32) : sizeof(U8)));
@@ -635,15 +636,13 @@ static ANKI_USE_RESULT Error compressS3tc(GenericMemoryPoolAllocator<U8> alloc,
 
 	ANKI_IMPORTER_LOGV("Will invoke process: compressonatorcli %s %s %s %s %s", args[0].cstr(), args[1].cstr(),
 					   args[2].cstr(), args[3].cstr(), args[4].cstr());
-	ANKI_CHECK(proc.start("compressonatorcli", args,
-						  (compressonatorPath.isEmpty()) ? ConstWeakArray<CString>()
-														 : Array<CString, 2>{{"PATH", compressonatorPath}}));
+	ANKI_CHECK(proc.start(compressonatorFilename, args));
 	CleanupFile ddsCleanup(alloc, ddsFilename);
 	ProcessStatus status;
 	I32 exitCode;
-	ANKI_CHECK(proc.wait(60.0, &status, &exitCode));
+	ANKI_CHECK(proc.wait(60.0_sec, &status, &exitCode));
 
-	if(status != ProcessStatus::NORMAL_EXIT || exitCode != 0)
+	if(!(status == ProcessStatus::NOT_RUNNING && exitCode == 0))
 	{
 		StringAuto errStr(alloc);
 		if(exitCode != 0)
@@ -702,7 +701,7 @@ static ANKI_USE_RESULT Error compressS3tc(GenericMemoryPoolAllocator<U8> alloc,
 }
 
 static ANKI_USE_RESULT Error compressAstc(GenericMemoryPoolAllocator<U8> alloc, CString tempDirectory,
-										  CString astcencPath, ConstWeakArray<U8, PtrSize> inPixels, U32 inWidth,
+										  CString astcencFilename, ConstWeakArray<U8, PtrSize> inPixels, U32 inWidth,
 										  U32 inHeight, U32 inChannelCount, UVec2 blockSize, Bool hdr,
 										  WeakArray<U8, PtrSize> outPixels)
 {
@@ -753,16 +752,14 @@ static ANKI_USE_RESULT Error compressAstc(GenericMemoryPoolAllocator<U8> alloc,
 
 	ANKI_IMPORTER_LOGV("Will invoke process: astcenc-avx2 %s %s %s %s %s", args[0].cstr(), args[1].cstr(),
 					   args[2].cstr(), args[3].cstr(), args[4].cstr());
-	ANKI_CHECK(
-		proc.start("astcenc-avx2", args,
-				   (astcencPath.isEmpty()) ? ConstWeakArray<CString>() : Array<CString, 2>{{"PATH", astcencPath}}));
+	ANKI_CHECK(proc.start(astcencFilename, args));
 
 	CleanupFile astcCleanup(alloc, astcFilename);
 	ProcessStatus status;
 	I32 exitCode;
-	ANKI_CHECK(proc.wait(60.0, &status, &exitCode));
+	ANKI_CHECK(proc.wait(60.0_sec, &status, &exitCode));
 
-	if(status != ProcessStatus::NORMAL_EXIT || exitCode != 0)
+	if(!(status == ProcessStatus::NOT_RUNNING && exitCode == 0))
 	{
 		StringAuto errStr(alloc);
 		if(exitCode != 0)
@@ -1096,7 +1093,7 @@ static ANKI_USE_RESULT Error importImageInternal(const ImageImporterConfig& conf
 
 					surface.m_s3tcPixels.create(s3tcImageSize);
 
-					ANKI_CHECK(compressS3tc(alloc, config.m_tempDirectory, config.m_compressonatorPath,
+					ANKI_CHECK(compressS3tc(alloc, config.m_tempDirectory, config.m_compressonatorFilename,
 											ConstWeakArray<U8, PtrSize>(surface.m_pixels), width, height,
 											ctx.m_channelCount, ctx.m_hdr,
 											WeakArray<U8, PtrSize>(surface.m_s3tcPixels)));
@@ -1126,7 +1123,7 @@ static ANKI_USE_RESULT Error importImageInternal(const ImageImporterConfig& conf
 
 					surface.m_astcPixels.create(astcImageSize);
 
-					ANKI_CHECK(compressAstc(alloc, config.m_tempDirectory, config.m_astcencPath,
+					ANKI_CHECK(compressAstc(alloc, config.m_tempDirectory, config.m_astcencFilename,
 											ConstWeakArray<U8, PtrSize>(surface.m_pixels), width, height,
 											ctx.m_channelCount, config.m_astcBlockSize, ctx.m_hdr,
 											WeakArray<U8, PtrSize>(surface.m_astcPixels)));

+ 2 - 2
AnKi/Importer/ImageImporter.h

@@ -27,8 +27,8 @@ public:
 	U32 m_mipmapCount = MAX_U32;
 	Bool m_noAlpha = true;
 	CString m_tempDirectory;
-	CString m_compressonatorPath; ///< Optional.
-	CString m_astcencPath; ///< Optional.
+	CString m_compressonatorFilename; ///< Optional.
+	CString m_astcencFilename; ///< Optional.
 	UVec2 m_astcBlockSize = UVec2(8u);
 	Bool m_sRgbToLinear = false;
 	Bool m_linearToSRgb = false;

+ 2 - 2
AnKi/ShaderCompiler/MaliOfflineCompiler.cpp

@@ -118,8 +118,8 @@ static Error runMaliOfflineCompilerInternal(CString maliocExecutable, CString sp
 	ANKI_CHECK(proc.start(maliocExecutable, args, {}));
 	ProcessStatus status;
 	I32 exitCode;
-	ANKI_CHECK(proc.wait(10.0_sec, &status, &exitCode));
-	if(status == ProcessStatus::CRASH_EXIT || exitCode != 0)
+	ANKI_CHECK(proc.wait(-1.0, &status, &exitCode));
+	if(exitCode != 0)
 	{
 		StringAuto stderre(tmpAlloc);
 		const Error err = proc.readFromStderr(stderre);

+ 10 - 4
AnKi/Util/CMakeLists.txt

@@ -15,14 +15,14 @@ set(sources
 	Tracer.cpp
 	Serializer.cpp
 	Xml.cpp
-	F16.cpp)
+	F16.cpp
+	Process.cpp)
 
 if(LINUX OR ANDROID OR MACOS)
 	set(sources ${sources}
 		HighRezTimerPosix.cpp
 		FilesystemPosix.cpp
-		ThreadPosix.cpp
-		ProcessPosix.cpp)
+		ThreadPosix.cpp)
 else()
 	set(sources ${sources}
 		HighRezTimerWindows.cpp
@@ -42,4 +42,10 @@ file(GLOB_RECURSE headers *.h)
 
 add_library(AnKiUtil ${sources} ${headers})
 target_compile_definitions(AnKiUtil PRIVATE -DANKI_SOURCE_FILE)
-target_link_libraries(AnKiUtil AnKiTinyXml2)
+
+set(libs AnKiTinyXml2)
+if(LINUX OR WINDOWS)
+	set(libs ${libs} reproc)
+endif()
+
+target_link_libraries(AnKiUtil ${libs})

+ 216 - 0
AnKi/Util/Process.cpp

@@ -0,0 +1,216 @@
+// Copyright (C) 2009-2022, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <AnKi/Util/Process.h>
+#include <AnKi/Util/Array.h>
+#include <ThirdParty/Reproc/reproc/include/reproc/reproc.h>
+
+namespace anki {
+
+Process::~Process()
+{
+	destroy();
+}
+
+void Process::destroy()
+{
+	if(m_handle)
+	{
+		ProcessStatus status;
+		const Error err = getStatus(status);
+		(void)err;
+		if(status == ProcessStatus::RUNNING)
+		{
+			ANKI_UTIL_LOGE("Process is still running. Forgot to wait for it");
+		}
+
+		m_handle = reproc_destroy(m_handle);
+	}
+}
+
+Error Process::start(CString executable, ConstWeakArray<CString> arguments, ConstWeakArray<CString> environment)
+{
+	ANKI_ASSERT(m_handle == nullptr && "Already started");
+
+	// Set args and env
+	Array<const Char*, 64> args;
+	Array<const Char*, 32> env;
+
+	args[0] = executable.cstr();
+	for(U32 i = 0; i < arguments.getSize(); ++i)
+	{
+		args[i + 1] = arguments[i].cstr();
+		ANKI_ASSERT(args[i + 1]);
+	}
+	args[arguments.getSize() + 1] = nullptr;
+
+	for(U32 i = 0; i < environment.getSize(); ++i)
+	{
+		env[i] = environment[i].cstr();
+		ANKI_ASSERT(env[i]);
+	}
+	env[environment.getSize()] = nullptr;
+
+	// Start process
+	m_handle = reproc_new();
+
+	reproc_options options = {};
+
+	options.env.behavior = REPROC_ENV_EXTEND;
+	if(environment.getSize())
+	{
+		options.env.extra = &env[0];
+	}
+	options.nonblocking = true;
+
+	options.redirect.err.type = REPROC_REDIRECT_PIPE;
+
+	I32 ret = reproc_start(m_handle, &args[0], options);
+	if(ret < 0)
+	{
+		ANKI_UTIL_LOGE("reproc_start() failed: %s", reproc_strerror(ret));
+		m_handle = reproc_destroy(m_handle);
+		return Error::USER_DATA;
+	}
+
+	ret = reproc_close(m_handle, REPROC_STREAM_IN);
+	if(ret < 0)
+	{
+		ANKI_UTIL_LOGE("reproc_close() failed: %s. Ignoring", reproc_strerror(ret));
+		m_handle = reproc_destroy(m_handle);
+		return Error::USER_DATA;
+	}
+
+	return Error::NONE;
+}
+
+Error Process::wait(Second timeout, ProcessStatus* pStatus, I32* pExitCode)
+{
+	ANKI_ASSERT(m_handle);
+	ProcessStatus status;
+	I32 exitCode;
+
+	// Compute timeout in ms
+	I32 rtimeout;
+	if(timeout < 0.0)
+	{
+		rtimeout = REPROC_INFINITE;
+	}
+	else
+	{
+		// Cap the timeout to 1h to avoid overflows when converting to ms
+		if(timeout > 1.0_hour)
+		{
+			ANKI_UTIL_LOGW("Timeout unreasonably high (%f sec). Will cap it to 1h", timeout);
+			timeout = 1.0_hour;
+		}
+
+		rtimeout = I32(timeout * 1000.0);
+	}
+
+	// Wait
+	const I32 ret = reproc_wait(m_handle, rtimeout);
+	if(ret == REPROC_ETIMEDOUT)
+	{
+		status = ProcessStatus::RUNNING;
+		exitCode = 0;
+	}
+	else
+	{
+		status = ProcessStatus::NOT_RUNNING;
+		exitCode = ret;
+	}
+
+	if(pStatus)
+	{
+		*pStatus = status;
+	}
+
+	if(pExitCode)
+	{
+		*pExitCode = exitCode;
+	}
+
+	ANKI_ASSERT(!(status == ProcessStatus::RUNNING && timeout < 0.0));
+	return Error::NONE;
+}
+
+Error Process::getStatus(ProcessStatus& status)
+{
+	ANKI_ASSERT(m_handle);
+	ANKI_CHECK(wait(0.0, &status, nullptr));
+	return Error::NONE;
+}
+
+Error Process::kill(ProcessKillSignal k)
+{
+	ANKI_ASSERT(m_handle);
+
+	I32 ret;
+	CString funcName;
+	if(k == ProcessKillSignal::NORMAL)
+	{
+		ret = reproc_terminate(m_handle);
+		funcName = "reproc_terminate";
+	}
+	else
+	{
+		ANKI_ASSERT(k == ProcessKillSignal::FORCE);
+		ret = reproc_kill(m_handle);
+		funcName = "reproc_kill";
+	}
+
+	if(ret < 0)
+	{
+		ANKI_UTIL_LOGE("%s() failed: %s", funcName.cstr(), reproc_strerror(ret));
+		return Error::FUNCTION_FAILED;
+	}
+
+	return Error::NONE;
+}
+
+Error Process::readFromStdout(StringAuto& text)
+{
+	return readCommon(REPROC_STREAM_OUT, text);
+}
+
+Error Process::readFromStderr(StringAuto& text)
+{
+	return readCommon(REPROC_STREAM_ERR, text);
+}
+
+Error Process::readCommon(I32 reprocStream, StringAuto& text)
+{
+	ANKI_ASSERT(m_handle);
+
+	// Limit the iterations in case the process writes to FD constantly
+	U32 maxIterations = 16;
+	while(maxIterations--)
+	{
+		Array<Char, 256> buff;
+
+		const I32 ret =
+			reproc_read(m_handle, REPROC_STREAM(reprocStream), reinterpret_cast<U8*>(&buff[0]), buff.getSize() - 1);
+
+		if(ret == 0 || ret == REPROC_EPIPE || ret == REPROC_EWOULDBLOCK)
+		{
+			// No data or all data have bee read or something that I don't get
+			break;
+		}
+
+		if(ret < 0)
+		{
+			ANKI_UTIL_LOGE("reproc_read() failed: %s", reproc_strerror(ret));
+			return Error::FUNCTION_FAILED;
+		}
+
+		buff[ret] = '\0';
+		text.append(&buff[0]);
+	}
+
+	return Error::NONE;
+}
+
+} // end namespace anki

+ 15 - 27
AnKi/Util/Process.h

@@ -9,6 +9,9 @@
 #include <AnKi/Util/String.h>
 #include <AnKi/Util/WeakArray.h>
 
+// Forward
+struct reproc_t;
+
 namespace anki {
 
 /// @addtogroup util_system
@@ -18,9 +21,7 @@ namespace anki {
 enum class ProcessStatus : U8
 {
 	RUNNING,
-	NOT_RUNNING,
-	NORMAL_EXIT,
-	CRASH_EXIT
+	NOT_RUNNING
 };
 
 /// @memberof Process
@@ -46,19 +47,19 @@ public:
 	/// @param executable The executable to start.
 	/// @param arguments The command line arguments.
 	/// @param environment The environment variables.
-	ANKI_USE_RESULT Error start(CString executable, ConstWeakArray<CString> arguments,
-								ConstWeakArray<CString> environment);
+	ANKI_USE_RESULT Error start(CString executable, ConstWeakArray<CString> arguments = {},
+								ConstWeakArray<CString> environment = {});
 
 	/// Wait for the process to finish.
-	/// @param timeout The time to wait. If 0.0 wait forever.
+	/// @param timeout The time to wait. If it's negative wait forever.
 	/// @param[out] status The exit status
 	/// @param[out] exitCode The exit code if the process has finished.
-	ANKI_USE_RESULT Error wait(Second timeout = 0.0, ProcessStatus* status = nullptr, I32* exitCode = nullptr);
+	ANKI_USE_RESULT Error wait(Second timeout = -1.0, ProcessStatus* status = nullptr, I32* exitCode = nullptr);
 
 	/// Get the status.
 	ANKI_USE_RESULT Error getStatus(ProcessStatus& status);
 
-	/// Kill the process.
+	/// Kill the process. Need to call wait after killing the process.
 	ANKI_USE_RESULT Error kill(ProcessKillSignal k);
 
 	/// Read from stdout.
@@ -67,27 +68,14 @@ public:
 	/// Read from stderr.
 	ANKI_USE_RESULT Error readFromStderr(StringAuto& text);
 
-private:
-#if ANKI_POSIX
-	static constexpr int DEFAULT_EXIT_CODE = -1;
-
-	int m_pid = -1;
-	int m_exitCode = DEFAULT_EXIT_CODE;
-	ProcessStatus m_status = ProcessStatus::NOT_RUNNING;
-	Array<int, 2> m_stdoutPipe = {-1, -1};
-	Array<int, 2> m_stderrPipe = {-1, -1};
+	/// Cleanup a finished process. Call this if you want to start a new process again. Need to have waited before
+	/// calling destroy.
+	void destroy();
 
-	void destroyPipes();
-
-	ANKI_USE_RESULT Error createPipes();
-
-	ANKI_USE_RESULT Error readFromFd(int fd, StringAuto& text) const;
+private:
+	reproc_t* m_handle = nullptr;
 
-	/// Update some members.
-	ANKI_USE_RESULT Error refresh(int waitpidOptions);
-#else
-	// TODO
-#endif
+	ANKI_USE_RESULT Error readCommon(I32 reprocStream, StringAuto& text);
 };
 /// @}
 

+ 0 - 300
AnKi/Util/ProcessPosix.cpp

@@ -1,300 +0,0 @@
-// Copyright (C) 2009-2022, Panagiotis Christopoulos Charitos and contributors.
-// All rights reserved.
-// Code licensed under the BSD License.
-// http://www.anki3d.org/LICENSE
-
-#include <AnKi/Util/Process.h>
-#include <AnKi/Util/Functions.h>
-#include <sys/wait.h>
-#include <fcntl.h>
-#include <errno.h>
-#include <unistd.h>
-
-namespace anki {
-
-Process::~Process()
-{
-	ProcessStatus s;
-	const Error err = getStatus(s);
-	if(err)
-	{
-		ANKI_UTIL_LOGE("Failed to get the status. Expect cleanup errors");
-		return;
-	}
-
-	if(s == ProcessStatus::RUNNING)
-	{
-		ANKI_UTIL_LOGE("Destroying while child process is running");
-		return;
-	}
-
-	destroyPipes();
-}
-
-void Process::destroyPipes()
-{
-	for(U32 i = 0; i < 2; ++i)
-	{
-		close(m_stdoutPipe[i]);
-		close(m_stderrPipe[i]);
-		m_stdoutPipe[i] = m_stderrPipe[i] = -1;
-	}
-}
-
-Error Process::createPipes()
-{
-	destroyPipes();
-
-	if(pipe(m_stdoutPipe.getBegin()) || pipe(m_stderrPipe.getBegin()))
-	{
-		ANKI_UTIL_LOGE("pipe() failed: %s", strerror(errno));
-		return Error::FUNCTION_FAILED;
-	}
-
-	fcntl(m_stdoutPipe[0], F_SETFL, O_NONBLOCK);
-	fcntl(m_stderrPipe[0], F_SETFL, O_NONBLOCK);
-
-	return Error::NONE;
-}
-
-Error Process::start(CString executable, ConstWeakArray<CString> arguments, ConstWeakArray<CString> environment)
-{
-	ANKI_CHECK(refresh(0));
-	if(m_status == ProcessStatus::RUNNING)
-	{
-		ANKI_UTIL_LOGE("Process already running");
-		return Error::USER_DATA;
-	}
-
-	// Create the pipes
-	ANKI_CHECK(createPipes());
-
-	// Fork
-	m_pid = fork();
-	if(m_pid < 0)
-	{
-		ANKI_UTIL_LOGE("fork() failed: %s", strerror(errno));
-	}
-
-	if(m_pid == 0)
-	{
-		// Child
-
-		dup2(m_stdoutPipe[1], 1);
-		close(m_stdoutPipe[0]);
-		m_stdoutPipe[0] = -1;
-
-		dup2(m_stderrPipe[1], 2);
-		close(m_stderrPipe[0]);
-		m_stderrPipe[0] = -1;
-
-		// Set the args
-		char** cargs = static_cast<char**>(malloc((arguments.getSize() + 2) * sizeof(char*)));
-		cargs[0] = static_cast<char*>(malloc(executable.getLength() + 1));
-		strcpy(cargs[0], executable.cstr());
-		U32 i = 0;
-		for(; i < arguments.getSize(); ++i)
-		{
-			cargs[i + 1] = static_cast<char*>(malloc(arguments[i].getLength() + 1));
-			strcpy(cargs[i + 1], arguments[i].cstr());
-		}
-		cargs[i + 1] = nullptr;
-
-		// Set the env
-		for(U32 i = 0; i < environment.getSize(); i += 2)
-		{
-			setenv(environment[i].cstr(), environment[i + 1].cstr(), 1);
-		}
-
-		// Execute file
-		const int execerror = execvp(executable.cstr(), cargs);
-		if(execerror)
-		{
-			printf("execvp() failed: %s", strerror(errno));
-			exit(1);
-		}
-	}
-	else
-	{
-		close(m_stdoutPipe[1]);
-		m_stdoutPipe[1] = -1;
-		close(m_stderrPipe[1]);
-		m_stderrPipe[1] = -1;
-	}
-
-	return Error::NONE;
-}
-
-Error Process::kill(ProcessKillSignal k)
-{
-	if(m_pid < 1)
-	{
-		return Error::NONE;
-	}
-
-	int sig;
-	switch(k)
-	{
-	case ProcessKillSignal::NORMAL:
-		sig = SIGTERM;
-		break;
-	case ProcessKillSignal::FORCE:
-		sig = SIGKILL;
-		break;
-	default:
-		ANKI_ASSERT(0);
-		sig = 0;
-	};
-
-	const pid_t p = ::kill(m_pid, sig);
-	if(p != 0)
-	{
-		ANKI_UTIL_LOGE("kill() failed: %s", strerror(errno));
-		return Error::FUNCTION_FAILED;
-	}
-
-	return Error::NONE;
-}
-
-Error Process::readFromFd(int fd, StringAuto& text) const
-{
-	fd_set readfds;
-	FD_ZERO(&readfds);
-	FD_SET(fd, &readfds);
-
-	timeval timeout;
-	timeout.tv_sec = 0; // Seconds
-	timeout.tv_usec = 10; // Microseconds
-
-	// Limit the iterations in case the process writes to FD constantly
-	U32 maxIterations = 16;
-	while(maxIterations--)
-	{
-		// Check if there are data
-		const int sel = select(1 + fd, &readfds, nullptr, nullptr, &timeout);
-		if(sel == 0)
-		{
-			// Timeout expired
-			break;
-		}
-		else if(sel == -1)
-		{
-			ANKI_UTIL_LOGE("select() failed: %s", strerror(errno));
-			return Error::FUNCTION_FAILED;
-		}
-
-		// Read the data
-		Array<char, 1024> buff;
-		const ssize_t bytesCount = read(fd, buff.getBegin(), buff.getSize() - 1);
-
-		if(bytesCount < 0 && errno != EAGAIN)
-		{
-			// NOTE errno == EINTR if a non-fatal signal arrived
-			ANKI_UTIL_LOGE("read() failed: %s", strerror(errno));
-			return Error::FUNCTION_FAILED;
-		}
-		else if(bytesCount == 0)
-		{
-			break;
-		}
-		else
-		{
-			buff[min<U32>(U32(bytesCount), buff.getSize() - 1)] = '\0';
-			text.append(&buff[0]);
-		}
-	}
-
-	return Error::NONE;
-}
-
-Error Process::readFromStdout(StringAuto& text)
-{
-	return readFromFd(m_stdoutPipe[0], text);
-}
-
-Error Process::readFromStderr(StringAuto& text)
-{
-	return readFromFd(m_stderrPipe[0], text);
-}
-
-Error Process::getStatus(ProcessStatus& status)
-{
-	ANKI_CHECK(refresh(WNOHANG));
-	status = m_status;
-	return Error::NONE;
-}
-
-Error Process::refresh(int waitpidOptions)
-{
-	Error err = Error::NONE;
-	m_status = ProcessStatus::NOT_RUNNING;
-	m_exitCode = DEFAULT_EXIT_CODE;
-
-	if(m_pid == -1)
-	{
-		m_status = ProcessStatus::NOT_RUNNING;
-		m_exitCode = DEFAULT_EXIT_CODE;
-	}
-	else
-	{
-		int status;
-		const pid_t p = waitpid(m_pid, &status, waitpidOptions);
-
-		if(p == -1)
-		{
-			m_status = ProcessStatus::NOT_RUNNING;
-			m_exitCode = DEFAULT_EXIT_CODE;
-			m_pid = -1;
-			ANKI_UTIL_LOGE("waitpid() failed: %s", strerror(errno));
-			err = Error::FUNCTION_FAILED;
-		}
-		else if(p == 0)
-		{
-			m_status = ProcessStatus::RUNNING;
-			m_exitCode = DEFAULT_EXIT_CODE;
-		}
-		else if(WIFEXITED(status))
-		{
-			m_status = ProcessStatus::NORMAL_EXIT;
-			m_exitCode = WEXITSTATUS(status);
-			m_pid = -1;
-		}
-		else if(WIFSIGNALED(status))
-		{
-			m_status = ProcessStatus::CRASH_EXIT;
-			m_exitCode = WTERMSIG(status);
-			m_pid = -1;
-		}
-		else if(WIFSTOPPED(status))
-		{
-			// NOTE child may resume later (and become a zombie)
-			m_status = ProcessStatus::CRASH_EXIT;
-			m_exitCode = WSTOPSIG(status);
-		}
-		else
-		{
-			ANKI_ASSERT(0);
-		}
-	}
-
-	return err;
-}
-
-Error Process::wait(Second timeout, ProcessStatus* status, I32* exitCode)
-{
-	ANKI_CHECK(refresh(0));
-
-	if(status)
-	{
-		*status = m_status;
-	}
-
-	if(exitCode)
-	{
-		*exitCode = m_exitCode;
-	}
-
-	return Error::NONE;
-}
-
-} // end namespace anki

+ 0 - 51
AnKi/Util/ProcessWindows.cpp

@@ -1,51 +0,0 @@
-// Copyright (C) 2009-2022, Panagiotis Christopoulos Charitos and contributors.
-// All rights reserved.
-// Code licensed under the BSD License.
-// http://www.anki3d.org/LICENSE
-
-#include <AnKi/Util/Process.h>
-
-namespace anki {
-
-Process::~Process()
-{
-	// TODO
-}
-
-Error Process::start(CString executable, ConstWeakArray<CString> arguments, ConstWeakArray<CString> environment)
-{
-	// TODO
-	return Error::NONE;
-}
-
-Error Process::kill(ProcessKillSignal k)
-{
-	// TODO
-	return Error::NONE;
-}
-
-Error Process::readFromStdout(StringAuto& text)
-{
-	// TODO
-	return Error::NONE;
-}
-
-Error Process::readFromStderr(StringAuto& text)
-{
-	// TODO
-	return Error::NONE;
-}
-
-Error Process::getStatus(ProcessStatus& status)
-{
-	// TODO
-	return Error::NONE;
-}
-
-Error Process::wait(Second timeout, ProcessStatus* status, I32* exitCode)
-{
-	// TODO
-	return Error::NONE;
-}
-
-} // end namespace anki

+ 5 - 0
AnKi/Util/StdTypes.h

@@ -266,6 +266,11 @@ static constexpr unsigned long long int operator""_GB(unsigned long long int x)
 
 /// @name Time user literals
 /// @{
+static constexpr Second operator""_hour(long double x)
+{
+	return Second(x) * 60.0;
+}
+
 static constexpr Second operator""_sec(long double x)
 {
 	return Second(x);

+ 7 - 0
CMakeLists.txt

@@ -275,6 +275,13 @@ message("++ Configuring glslang")
 add_subdirectory(ThirdParty/Glslang)
 message("++ End configuring glslang")
 
+if(LINUX OR WINDOWS)
+	message("++ Configuring reproc")
+	set(REPROC++ OFF)
+	add_subdirectory(ThirdParty/Reproc)
+	message("++ End configuring reproc")
+endif()
+
 foreach(TMP ${ANKI_EXTERN_SUB_DIRS})
 	add_subdirectory(ThirdParty/${TMP})
 endforeach()

+ 62 - 12
Tests/Util/Process.cpp

@@ -7,29 +7,54 @@
 #include <AnKi/Util/Process.h>
 #include <AnKi/Util/File.h>
 #include <AnKi/Util/HighRezTimer.h>
+#include <AnKi/Util/Filesystem.h>
+
+namespace anki {
+
+static void createBashScript(CString code)
+{
+	File file;
+
+	ANKI_TEST_EXPECT_NO_ERR(file.open("process_test.sh", FileOpenFlag::WRITE));
+	ANKI_TEST_EXPECT_NO_ERR(file.writeText("#!/bin/bash\n%s\n", code.cstr()));
+}
+
+} // end namespace anki
 
 ANKI_TEST(Util, Process)
 {
 	// Simple test
 	if(1)
 	{
+		createBashScript(R"(
+echo Hello from script
+exit 6
+)");
+
 		Process proc;
-		ANKI_TEST_EXPECT_NO_ERR(proc.start("ls", Array<CString, 1>{{"-lt"}}, {}));
-		ANKI_TEST_EXPECT_NO_ERR(proc.wait());
-		HighRezTimer::sleep(1.0);
+		ANKI_TEST_EXPECT_NO_ERR(proc.start("bash", Array<CString, 1>{{"process_test.sh"}}, {}));
+		ProcessStatus status;
+		I32 exitCode;
+		ANKI_TEST_EXPECT_NO_ERR(proc.wait(-1.0, &status, &exitCode));
 
-		HeapAllocator<U8> alloc(allocAligned, nullptr);
-		StringAuto stdOut(alloc);
+		ANKI_TEST_EXPECT_EQ(status, ProcessStatus::NOT_RUNNING);
+		ANKI_TEST_EXPECT_EQ(exitCode, 6);
+
+		// Get stuff again, don't wait this time
+		exitCode = 0;
+		ANKI_TEST_EXPECT_NO_ERR(proc.wait(0.0, &status, &exitCode));
+		ANKI_TEST_EXPECT_EQ(status, ProcessStatus::NOT_RUNNING);
+		ANKI_TEST_EXPECT_EQ(exitCode, 6);
+
+		StringAuto stdOut(HeapAllocator<U8>(allocAligned, nullptr));
 		ANKI_TEST_EXPECT_NO_ERR(proc.readFromStdout(stdOut));
-		ANKI_TEST_LOGI("%s", stdOut.cstr());
+		ANKI_TEST_EXPECT_EQ(stdOut, "Hello from script\n");
 	}
 
 	// Stderr and stdOut
 	if(1)
 	{
-		File file;
-		ANKI_TEST_EXPECT_NO_ERR(file.open("process_test.sh", FileOpenFlag::WRITE));
-		ANKI_TEST_EXPECT_NO_ERR(file.writeText(R"(#!/bin/bash
+		createBashScript(R"(
 x=1
 while [ $x -le 10 ]
 do
@@ -38,17 +63,19 @@ do
 	sleep 1
 	x=$(( $x + 1 ))
 done
-)"));
-		file.close();
+)");
 
 		Process proc;
 		ANKI_TEST_EXPECT_NO_ERR(proc.start("bash", Array<CString, 1>{{"process_test.sh"}}, {}));
 		ProcessStatus status;
 
+		ANKI_TEST_EXPECT_NO_ERR(proc.getStatus(status));
+		ANKI_TEST_EXPECT_EQ(status, ProcessStatus::RUNNING);
+
 		while(true)
 		{
 			ANKI_TEST_EXPECT_NO_ERR(proc.getStatus(status));
-			if(status == ProcessStatus::NORMAL_EXIT)
+			if(status == ProcessStatus::NOT_RUNNING)
 			{
 				break;
 			}
@@ -71,4 +98,27 @@ done
 
 		ANKI_TEST_EXPECT_NO_ERR(proc.wait(0.0, &status));
 	}
+
+	// Read after complete wait & env
+	if(1)
+	{
+		createBashScript(R"(
+echo $ENV_VAR
+sleep 1
+)");
+
+		Process proc;
+		ANKI_TEST_EXPECT_NO_ERR(
+			proc.start("bash", Array<CString, 1>{{"process_test.sh"}}, Array<CString, 1>{{"ENV_VAR=Lala"}}));
+
+		ANKI_TEST_EXPECT_NO_ERR(proc.wait());
+
+		HighRezTimer::sleep(0.5_sec); // Wait a bit more for good measure
+
+		StringAuto stdOut(HeapAllocator<U8>(allocAligned, nullptr));
+		ANKI_TEST_EXPECT_NO_ERR(proc.readFromStdout(stdOut));
+		ANKI_TEST_EXPECT_EQ(stdOut, "Lala\n");
+	}
+
+	ANKI_TEST_EXPECT_NO_ERR(removeFile("process_test.sh"));
 }

+ 1076 - 0
ThirdParty/Reproc/CHANGELOG.md

@@ -0,0 +1,1076 @@
+# Changelog
+
+## 14.2.4
+
+- Bugfix: Fix a memory leak in `reproc_start()` on Windows (thanks @AokiYuune).
+- Bugfix: Fix a memory leak in reproc++ `array` class move constructor.
+- Allow passing zero-sized array's to reproc's `input` option (thanks @lightray22).
+
+## 14.2.3
+
+- Bugfix: Fix sign of EWOULDBLOCK error returned from `reproc_read`.
+
+## 14.2.2
+
+- Bugfix: Disallow using `fork` option when using `reproc_run`.
+
+## 14.2.1
+
+- Bugfix: `reproc_run` now handles forked child processes correctly.
+- Bugfix: Sinks of different types can now be passed to `reproc::drain`.
+- Bugfix: Processes on Windows returning negative exit codes don't cause asserts
+  anymore.
+- Bugfix: Dependency on librt on POSIX (except osx) systems is now explicit in
+  CMake.
+- Bugfix: Added missing stdout redirect option to reproc++.
+
+## 14.2.0
+
+- Added `reproc_pid`/`process::pid` to get the pid of the process
+- Fixed compilation error when including reproc/drain.h in C++ code
+- Added missing extern "C" block to reproc/run.h
+
+## 14.1.0
+
+- `reproc_run`/`reproc::run` now return the exit code of
+  `reproc_stop`/`process::stop` even if the deadline is exceeded.
+- A bug where deadlines wouldn't work was fixed.
+
+## 14.0.0
+
+- Renamed `environment` to `env`.
+- Added configurable behavior to `env` option. Extra environment variables are
+  now added via `env.extra`. Extra environment variables now extend the parent
+  environment by default instead of replacing it.
+
+## 13.0.1
+
+- Bugfix: Reset the `events` parameter of every event source if a deadline
+  expires when calling `reproc_poll`.
+- Bugfix: Return 1 from `reproc_poll` if a deadline expires.
+- Bugfix: Don't block in `reproc_read` on Windows if the `nonblocking` option
+  was specified.
+
+## 13.0.0
+
+- Allow passing empty event sources to `reproc_poll`.
+
+  If the `process` member of a `reproc_event_source` object is `NULL`,
+  `reproc_poll` ignores the event source.
+
+- Return zero from `reproc_poll` if the given timeout expires instead of
+  `REPROC_ETIMEDOUT`.
+
+  In reproc, we follow the general pattern that we don't modify output arguments
+  if an error occurs. However, when `reproc_poll` errors, we still want to set
+  all events of the given event sources to zero. To signal that we're modifying
+  the output arguments if the timeout expires, we return zero instead of
+  `REPROC_ETIMEDOUT`. This is also more consistent with `poll` and `WSAPoll`
+  which have the same behaviour.
+
+- If one or more events occur, return the number of processes with events from
+  `reproc_poll`.
+
+## 12.0.0
+
+### reproc
+
+- Put pipes in blocking mode by default.
+
+  This allows using `reproc_read` and `reproc_write` directly without having to
+  figure out `reproc_poll`.
+
+- Add `nonblocking` option.
+
+  Allows putting pipes back in nonblocking mode if needed.
+
+- Child process stderr streams are now redirected to the parent stderr stream by
+  default.
+
+  Because pipes are blocking again by default, there's a (small) chance of
+  deadlocks if we redirect both stdout and stderr to pipes. Redirecting stderr
+  to the parent by default avoids that issue.
+
+  The other (bigger) issue is that if we redirect stderr to a pipe, there's a
+  good chance users might forget to read from it and discard valuable error
+  output from the child process.
+
+  By redirecting to the parent stderr by default, it's immediately noticeable
+  when a child process is not behaving according to expectations as its error
+  output will appear directly on the parent process stderr stream. Users can
+  then still decide to explicitly discard the output from the stderr stream if
+  needed.
+
+- Turn `timeout` option into an argument for `reproc_poll`.
+
+  While deadlines can differ per process, the timeout is very likely to always
+  be the same so we make it an argument to the only function that uses it,
+  namely `reproc_poll`.
+
+- In `reproc_drain`, call `sink` once with an empty buffer when a stream is
+  closed.
+
+  This allows sinks to handle stream closure if needed.
+
+- Change sinks to return `int` instead of `bool`. If a `sink` returns a non-zero
+  value, `reproc_drain` exits immediately with the same value.
+
+  This change allows sinks to return their own errors without having to store
+  extra state.
+
+- `reproc_sink_string` now returns `REPROC_ENOMEM` from `reproc_drain` if a
+  memory allocation fails and no longer frees any output that was read
+  previously.
+
+  This allows the user to still do something with the remaining output even if a
+  memory allocation failed. On the flipside, it is now required to always call
+  `reproc_free` after calling `reproc_drain` with a sink string, even if it
+  fails.
+
+- Renamed sink.h to drain.h.
+
+  Reflect that sink.h contains `reproc_drain` by renaming it to drain.h.
+
+- Add `REPROC_REDIRECT_PATH` and shorthand `path` options.
+
+### reproc++
+
+- Equivalent changes as those done for reproc.
+
+- Remove use of reprocxx.
+
+  Meson gained support for CMake subprojects containing targets with special
+  characters so we rename directories and CMake targets back to reproc++.
+
+## 11.0.0
+
+### General
+
+- Compilation now happens with compiler extensions disabled (`-std=c99` and
+  `-std=c++11`).
+
+### reproc
+
+- Add `inherit` and `discard` options as shorthands to set all members of the
+  `redirect` options to `REPROC_REDIRECT_INHERIT` and `REPROC_REDIRECT_DISCARD`
+  respectively.
+
+- Add `reproc_run` and `reproc_run_ex` which allows running a process using only
+  a single function.
+
+  Running a simple process with reproc required calling `reproc_new`,
+  `reproc_start`, `reproc_wait` and optionally `reproc_drain` with all the
+  associated error handling. `reproc_run` encapsulates all this boilerplate.
+
+- Add `input` option that writes the given to the child process stdin pipe
+  before starting the process.
+
+  This allows passing input to the process when using `reproc_run`.
+
+- Add `deadline` option that specifies a point in time beyond which
+  `reproc_poll` will return `REPROC_ETIMEDOUT`.
+
+- Add `REPROC_DEADLINE` that makes `reproc_wait` wait until the deadline
+  specified in the `deadline` option has expired.
+
+  By default, if the `deadline` option is set, `reproc_destroy` waits until the
+  deadline expires before sending a `SIGTERM` signal to the child process.
+
+- Add (POSIX only) `fork` option that makes `reproc_start` a safe alternative to
+  `fork` with support for all of reproc's other features.
+
+- Return the amount of bytes written from `reproc_write` and stop handling
+  partial writes.
+
+  Now that reproc uses nonblocking pipes, it doesn't make sense to handle
+  partial writes anymore. The `input` option can be used as an alternative.
+  `reproc_start` keeps writing until all data from `input` is written to the
+  child process stdin pipe or until a call to `reproc_write` returns an error.
+
+- Add `reproc_poll` to query child process events of one or more child
+  processes.
+
+  We now support polling multiple child processes for events. `reproc_poll`
+  mimicks the POSIX `poll` function but instead of pollfds, it takes a list of
+  event sources which consist out of a process, the events we're interested in
+  and an output field which is filled in by `reproc_poll` that contains the
+  events that occurred for that child process.
+
+- Stop reading from both stdout and stderr in `reproc_read`.
+
+  Because we now have `reproc_poll`, `reproc_read` was simplified to again read
+  from the given stream which is passed again as an argument. To avoid
+  deadlocks, call `reproc_poll` to figure out the first stream that has data
+  available to read.
+
+- Support polling for process exit events.
+
+  By adding `REPROC_EVENT_EXIT` to the list of interested events, we can poll
+  for child process exit events.
+
+- Add a dependency on Winsock2 on Windows.
+
+  To implement `reproc_poll`, we redirect to sockets on Windows which allows us
+  to use `WSAPoll` which is required to implement `reproc_poll`. Using sockets
+  on Windows requires linking with the `ws2_32` library. This dependency is
+  automatically handled by CMake and pkg-config.
+
+- Move `in`, `out` and `err` options out of `stdio` directly into `redirect`.
+
+  This reduces the amount of boilerplate when redirecting streams.
+
+- Rename `REPROC_REDIRECT_INHERIT` to `REPROC_REDIRECT_PARENT`.
+
+  `REPROC_REDIRECT_PARENT` more clearly indicates that we're redirecting to the
+  parent's corresponding standard stream.
+
+- Add support for redirecting to operating system handles.
+
+  This allows redirecting to operating system-specific handles. On POSIX
+  systems, this feature expects file descriptors. On Windows, it expects
+  `HANDLE`s or `SOCKET`s.
+
+- Add support for redirecting to `FILE *`s.
+
+  This gives users a cross-platform way to redirect standard streams to files.
+
+- Add support for redirecting stderr to stdout.
+
+  For high-traffic scenarios, it doesn't make sense to allocate a separate pipe
+  for stderr if its output is only going to be combined with the output of
+  stdout. By using `REPROC_REDIRECT_STDOUT` for stderr, its output is written
+  directly to stdout by the child process.
+
+- Turn `redirect`'s `in`, `out` and `err` options into instances of the new
+  `reproc_redirecŧ` struct.
+
+  An enum didn't cut it anymore for the new file and handle redirect options
+  since those require extra fields to allow specifying which file or handle to
+  redirect to.
+
+- Add `redirect.file` option as a shorthand for redirecting stdout and stderr to
+  the same file.
+
+### reproc++
+
+- reproc++ includes mostly the same changes done to reproc so we only document
+  the differences.
+
+- Add `fork` method instead of `fork` option.
+
+  Adding a `fork` option would have required changing `start` to return
+  `std::pair<bool, std::error_code>` to allow determining whether we're in the
+  parent or the child process after a fork. However, the bool return value would
+  only be valid when the fork option was enabled. Thus, this approach would
+  unnecessarily complicate all other use cases of `start` that don't require
+  `fork`. To solve the issue, we made `fork` a separate method instead.
+
+## 10.0.3
+
+### reproc
+
+- Fixed issue where `reproc_wait` would assert when invoked with a timeout of
+  zero on POSIX.
+
+- Fixed issue where `reproc_wait` would not return `REPROC_ETIMEDOUT` when
+  invoked with a timeout of zero on POSIX.
+
+## 10.0.2
+
+- Update CMake project version.
+
+## 10.0.1
+
+### reproc
+
+- Pass `timeout` once via `reproc_options` instead of passing it via
+  `reproc_read`, `reproc_write` and `reproc_drain`.
+
+### reproc++
+
+- Pass `timeout` once via `reproc::options` instead of passing it via
+  `process::read`, `process::write` and `reproc::drain`.
+
+## 10.0.0
+
+### reproc
+
+- Remove `reproc_parse`.
+
+  Instead of checking for `REPROC_EPIPE` (previously
+  `REPROC_ERROR_STREAM_CLOSED`), simply check if the given parser has a full
+  message available. If it doesn't, the output streams closed unexpectedly.
+
+- Remove `reproc_running` and `reproc_exit_status`.
+
+  When calling `reproc_running`, it would wait with a zero timeout if the
+  process was still running and check if the wait timed out. However, a call to
+  wait can fail for other reasons as well which were all ignored by
+  `reproc_running`. Instead of `reproc_running`, A call to `reproc_wait` with a
+  timeout of zero should be used to check if a process is still running.
+  `reproc_wait` now also returns the exit status if the process exits or has
+  already exited which removes the need for `reproc_exit_status`.
+
+- Read from both stdout and stderr in `reproc_read` to avoid deadlocks and
+  indicate which stream `reproc_read` was read from.
+
+  Previously, users would indicate the stream they wanted to read from when
+  calling `reproc_read`. However, this lead to issues with programs that write
+  to both stdout and stderr as a user wouldn't know whether stdout or stderr
+  would have output available to read. Reading from only the stdout stream
+  didn't work as the parent could be blocked on reading from stdout while the
+  child was simultaneously blocked on writing to stderr leading to a deadlock.
+  To get around this, users had to start up a separate thread to read from both
+  stdout and stderr at the same time which was a lot of extra work just to get
+  the output of external programs that write to both stdout and stderr. Now,
+  reproc takes care of avoiding the deadlock by checking which of stdout/stderr
+  can be read from, doing the actual read and indicating to the user which
+  stream was read from.
+
+  Practically, instead of passing `REPROC_STREAM_OUT` or `REPROC_STREAM_ERR` to
+  `reproc_read`, you now pass a pointer to a `REPROC_STREAM` variable instead
+  which `reproc_read` will set to `REPROC_STREAM_OUT` or `REPROC_STREAM_ERR`
+  depending on which stream it read from.
+
+  If both streams have been closed by the child process, `reproc_read` returns
+  `REPROC_EPIPE`.
+
+  Because of the changes to `reproc_read`, `reproc_drain` now also reads from
+  both stdout and stderr and indicates the stream that was read from to the
+  given sink function via an extra argument passed to the sink.
+
+- Read the output of both stdout and stderr into a single contiguous
+  null-terminated string in `reproc_sink_string`.
+
+- Remove the `bytes_written` parameter of `reproc_write`.
+
+  `reproc_write` now always writes `size` bytes to the standard input of the
+  child process. Partial writes do not have to be handled by users anymore and
+  are instead handled by reproc internally.
+
+- Define `_GNU_SOURCE` and `_WIN32_WINNT` only in the implementation files that
+  need them.
+
+  This helps keep track of where we're using functionality that requires extra
+  definitions and makes building reproc in all kinds of build systems simpler as
+  the compiler invocations to build reproc get smaller as a result.
+
+- Change the error handling in the public API to return negative `errno` (POSIX)
+  or `GetLastError` (Windows) style values. `REPROC_ERROR` is replaced by extern
+  constants that are assigned the correct error value based on the platform
+  reproc is built for. Instead of returning `REPROC_ERROR`, most functions in
+  reproc's API now return `int` when they can fail. Because system errors are
+  now returned directly, there's no need anymore for `REPROC_ERROR` and
+  `reproc_error_system` and they has been removed.
+
+  Error handling before 10.0.0:
+
+  ```c
+  REPROC_ERROR error = reproc_start(...);
+  if (error) {
+    goto finish;
+  }
+
+  finish:
+  if (error) {
+    fprintf(stderr, "%s", reproc_strerror(error));
+  }
+  ```
+
+  Error handling from 10.0.0 onwards:
+
+  ```c
+  int r = reproc_start(...);
+  if (r < 0) {
+    goto finish;
+  }
+
+  finish:
+  if (r < 0) {
+    fprintf(stderr, "%s", reproc_strerror(r));
+  }
+  ```
+
+- Hide the internals of `reproc_t`.
+
+  Instances of `reproc_t` are now allocated on the heap by calling `reproc_new`.
+  `reproc_destroy` releases the memory allocated by `reproc_new`.
+
+- Take optional arguments via the `reproc_options` struct in `reproc_start`.
+
+  When using designated initializers, calls to `reproc_start` are now much more
+  readable than before. Using a struct also makes it much easier to set all
+  options to their default values (`reproc_options options = { 0 };`). Finally,
+  we can add more options in further releases without requiring existing users
+  to change their code.
+
+- Support redirecting the child process standard streams to `/dev/null` (POSIX)
+  or `NUL` (Windows) in `reproc_start` via the `redirect` field in
+  `reproc_options`.
+
+  This is especially useful when you're not interested in the output of a child
+  process as redirecting to `/dev/null` doesn't require regularly flushing the
+  output pipes of the process to prevent deadlocks as is the case when
+  redirecting to pipes.
+
+- Support redirecting the child process standard streams to the parent process
+  standard streams in `reproc_starŧ` via the `redirect` field in
+  `reproc_options`.
+
+  This is useful when you want to interleave child process output with the
+  parent process output.
+
+- Modify `reproc_start` and `reproc_destroy` to work like the reproc++ `process`
+  class constructor and destructor.
+
+  The `stop_actions` field in `reproc_options` can be used to define up to three
+  stop actions that are executed when `reproc_destroy` is called if the child
+  process is still running. If no explicit stop actions are given,
+  `reproc_destroy` defaults to waiting indefinitely for the child process to
+  exit.
+
+- Return the amount of bytes read from `reproc_read` if it succeeds.
+
+  This is made possible by the new error handling scheme. Because errors are all
+  negative values, we can use the positive range of an `int` as the normal
+  return value if no errors occur.
+
+- Return the exit status from `reproc_wait` and `reproc_stop` if they succeed.
+
+  Same reasoning as above. If the child process has already exited,
+  `reproc_wait` and `reproc_stop` simply returns the exit status again.
+
+- Do nothing when `NULL` is passed to `reproc_destroy` and always return `NULL`
+  from `reproc_destroy`.
+
+  This allows `reproc_destroy` to be safely called on the same instance multiple
+  times when assigning the result of `reproc_destroy` to the same instance
+  (`process = reproc_destroy(process)`).
+
+- Take stop actions via the `reproc_stop_actions` struct in `reproc_stop`.
+
+  This makes it easier to store stop action configurations both in and outside
+  of reproc.
+
+- Add 256 to signal exit codes returned by `reproc_wait` and `reproc_stop`.
+
+  This prevents conflicts with normal exit codes.
+
+- Add `REPROC_SIGTERM` and `REPROC_SIGKILL` constants to match against signal
+  exit codes.
+
+  These also work on Windows and correspond to the exit codes returned by
+  sending the `CTRL-BREAK` signal and calling `TerminateProcess` respectively.
+
+- Rename `REPROC_CLEANUP` to `REPROC_STOP`.
+
+  Naming the enum after the function it is passed to (`reproc_stop`) is simpler
+  than using a different name.
+
+- Rewrite tests in C using CTest and `assert` and remove doctest.
+
+  Doctest is a great library but we don't really lose anything major by
+  replacing it with CTest and asserts. On the other hand, we lose a dependency,
+  don't need to download stuff from CMake anymore and tests compile
+  significantly faster.
+
+  Tests are now executed by running `cmake --build build --target test`.
+
+- Return `REPROC_EINVAL` from public API functions when passed invalid
+  arguments.
+
+- Make `reproc_strerror` thread-safe.
+
+- Move `reproc_drain` to sink.h.
+
+- Make `reproc_drain` take a separate sink for each output stream. Sinks are now
+  passed via the `reproc_sink` type.
+
+  Using separate sinks for both output streams allows for a lot more
+  flexibility. To use a single sink for both output streams, simply pass the
+  same sink to both the `out` and `err` arguments of `reproc_drain`.
+
+- Turn `reproc_sink_string` and `reproc_sink_discard` into functions that return
+  sinks and hide the actual functions in sink.c.
+
+- Add `reproc_free` to sink.h which must be used to free memory allocated by
+  `reproc_sink_string`.
+
+  This avoids issues with allocating across module (DLL) boundaries on Windows.
+
+- Support passing timeouts to `reproc_read`, `reproc_write` and `reproc_drain`.
+
+  Pass `REPROC_INFINITE` as the timeout to retain the old behaviour.
+
+- Use `int` to represent timeout values.
+
+- Renamed `stop_actions` field of `reproc_options` to `stop`.
+
+### reproc++
+
+- Remove `process::parse`, `process::exit_status` and `process::running`.
+
+  Consequence of the equivalents in reproc being removed.
+
+- Take separate `out` and `err` arguments in the `sink::string` and
+  `sink::ostream` constructors that receive output from the stdout and stderr
+  streams of the child process respectively.
+
+  To combine the output from the stdout and stderr streams, simply pass the same
+  `string` or `ostream` to both the `out` and `err` arguments.
+
+- Modify `process::read` to return a tuple of the stream read from, the amount
+  of bytes read and an error code. The stream read from and amount of bytes read
+  are only valid if `process::read` succeeds.
+
+  `std::tie` can be used pre-C++17 to assign the tuple's contents to separate
+  variables.
+
+- Modify `process::wait` and `process::stop` to return a pair of exit status and
+  error code. The exit status is only valid if `process::wait` or
+  `process::stop` succeeds.
+
+- Alias `reproc::error` to `std::errc`.
+
+  As OS errors are now used everywhere, we can simply use `std::errc` for all
+  error handling instead of defining our own error code.
+
+- Add `signal::terminate` and `signal::kill` constants.
+
+  These are aliases for `REPROC_SIGTERM` and `REPROC_SIGKILL` respectively.
+
+- Inline all sink implementations in sink.hpp.
+
+- Add `sink::thread_safe::string` which is a thread-safe version of
+  `sink::string`.
+
+- Move `process::drain` out of the `process` class and move it to sink.hpp.
+
+  `process.drain(...)` becomes `reproc::drain(process, ...)`.
+
+- Make `reproc::drain` take a separate sink for each output stream.
+
+  Same reasoning as `reproc_drain`.
+
+- Modify all included sinks to support the new `reproc::drain` behaviour.
+
+- Support passing timeouts to `process::read`, `process::write` and
+  `reproc::drain`.
+
+  They still default to waiting indefinitely which matches their old behaviour.
+
+- Renamed `stop_actions` field of `reproc::options` to `stop`.
+
+### CMake
+
+- Drop required CMake version to CMake 3.12.
+- Add CMake 3.16 as a supported CMake version.
+- Build reproc++ with `-pthread` when `REPROC_MULTITHREADED` is enabled.
+
+  See https://github.com/DaanDeMeyer/reproc/issues/24 for more information.
+
+- Add `REPROC_WARNINGS` option (default: `OFF`) to build with compiler warnings.
+- Add `REPROC_DEVELOP` option (default: `OFF`) which enables a lot of options to
+  simplify developing reproc.
+
+  By default, most of reproc's CMake options are disabled to make including
+  reproc in other projects as simple as possible. However, when working on
+  reproc, we usually wants most options enabled instead. To make enabling all
+  options simpler, `REPROC_DEVELOP` was added from which most other options take
+  their default value. As a result, enabling `REPROC_DEVELOP` enables all
+  options related to developing reproc. Additionally, `REPROC_DEVELOP` takes its
+  initial value from an environment variable of the same name so it can be set
+  once and always take effect whenever running CMake on reproc's source tree.
+
+- Add `REPROC_OBJECT_LIBRARIES` option to build CMake object libraries.
+
+  In CMake, linking a library against a static library doesn't actually copy the
+  object files from the static library into the library. Instead, both static
+  libraries have to be installed and depended on by the final executable. By
+  using CMake object libraries, the object files are copied into the depending
+  static library and no extra artifacts are produced.
+
+- Enable `REPROC_INSTALL` by default unless `REPROC_OBJECT_LIBRARIES` is
+  enabled.
+
+  As `REPROC_OBJECT_LIBRARIES` can now be used to depend on reproc without
+  generating extra artifacts, we assume that users not using
+  `REPROC_OBJECT_LIBRARIES` will want to install the produced artifacts.
+
+- Rename reproc++ to reprocxx inside the CMake build files.
+
+  This was done to allow using reproc as a Meson subproject. Meson doesn't
+  accept the '+' character in target names so we use 'x' instead.
+
+- Modify the export headers so that the only extra define necessary is
+  `REPROC_SHARED` when using reproc as a shared library on Windows.
+
+  Naturally, this define is added as a CMake usage requirement and doesn't have
+  to be worried about when using reproc via `add_subdirectory` or
+  `find_package`.
+
+## 9.0.0
+
+### General
+
+- Drop support for Windows XP.
+
+- Add support for custom environments.
+
+  `reproc_start` and `process::start` now take an extra `environment` parameter
+  that allows specifying custom environments.
+
+  **IMPORTANT**: The `environment` parameter was inserted before the
+  `working_directory` parameter so make sure to update existing usages of
+  `reproc_start` and `process::start` so that the `environment` and
+  `working_directory` arguments are specified in the correct order.
+
+  To keep the previous behaviour, pass `nullptr` as the environment to
+  `reproc_start`/`process::start` or use the `process::start` overload without
+  the `environment` parameter.
+
+- Remove `argc` parameter from `reproc_start` and `process::start`.
+
+  We can trivially calculate `argc` internally in reproc since `argv` is
+  required to end with a `NULL` value.
+
+- Improve implementation of `reproc_wait` with a timeout on POSIX systems.
+
+  Instead of spawning a new process to implement the timeout, we now use
+  `sigtimedwait` on Linux and `kqueue` on macOS to wait on `SIGCHLD` signals and
+  check if the process we're waiting on has exited after each received `SIGCHLD`
+  signal.
+
+- Remove `vfork` usage.
+
+  Clang analyzer was indicating a host of errors in our usage of `vfork`. We
+  also discovered tests were behaving differently on macOS depending on whether
+  `vfork` was enabled or disabled. As we do not have the expertise to verify if
+  `vfork` is working correctly, we opt to remove it.
+
+- Ensure passing a custom working directory and a relative executable path
+  behaves consistently on all supported platforms.
+
+  Previously, calling `reproc_start` with a relative executable path combined
+  with a custom working directory would behave differently depending on which
+  platform the code was executed on. On POSIX systems, the relative executable
+  path would be resolved relative to the custom working directory. On Windows,
+  the relative executable path would be resolved relative to the parent process
+  working directory. Now, relative executable paths are always resolved relative
+  to the parent process working directory.
+
+- Reimplement `reproc_drain`/`process::drain` in terms of
+  `reproc_parse`/`process::parse`.
+
+  Like `reproc_parse` and `process::parse`, `reproc_drain` and `process::drain`
+  are now guaranteed to always be called once with an empty buffer before
+  reading any actual data.
+
+  We now also guarantee that the initial empty buffer is not `NULL` or `nullptr`
+  so the received data and size can always be safely passed to `memcpy`.
+
+- Add MinGW support.
+
+  MinGW CI builds were also added to prevent regressions in MinGW support.
+
+### reproc
+
+- Update `reproc_strerror` to return the actual system error string of the error
+  code returned by `reproc_system_error` instead of "system error" when passed
+  `REPROC_ERROR_SYSTEM` as argument.
+
+  This should make debugging reproc errors a lot easier.
+
+- Add `reproc_sink_string` in `sink.h`, a sink that stores all process output in
+  a single null-terminated C string.
+
+- Add `reproc_sink_discard` in `sink.h`, a sink that discards all process
+  output.
+
+### reproc++
+
+- Move sinks into `sink` namespace and remove `_sink` suffix from all sinks.
+
+- Add `discard` sink that discards all output read from a stream.
+
+  This is useful when a child process produces a lot of output that we're not
+  interested in and cannot handle the output stream being closed or full. When
+  this is the case, simply start a thread that drains the stream with a
+  `discard` sink.
+
+- Update `process::start` to work with any kind of string type.
+
+  Every string type that implements a `size` method and the index operator can
+  now be passed in a container to `process::start`. `working_directory` now
+  takes a `const char *` instead of a `std::string *`.
+
+- Fix compilation error when using `process::parse`.
+
+## 8.0.1
+
+- Correctly escape arguments on Windows.
+
+  See [#18](https://github.com/DaanDeMeyer/reproc/issues/18) for more
+  information.
+
+## 8.0.0
+
+- Change `reproc_parse` and `reproc_drain` argument order.
+
+  `context` is now the last argument instead of the first.
+
+- Use `uint8_t *` as buffer type instead of `char *` or `void *`
+
+  `uint8_t *` more clearly indicates reproc is working with buffers of bytes
+  than `char *` and `void *`. We choose `uint8_t *` over `char *` to avoid
+  errors caused by passing data read by reproc directly to functions that expect
+  null-terminated strings (data read by reproc is not null-terminated).
+
+## 7.0.0
+
+### General
+
+- Rework error handling.
+
+  Trying to abstract platform-specific errors in `REPROC_ERROR` and
+  `reproc::errc` turned out to be harder than expected. On POSIX it remains very
+  hard to figure out which errors actually have a chance of happening and
+  matching `reproc::errc` values to `std::errc` values is also ambiguous and
+  prone to errors. On Windows, there's hardly any documentation on which system
+  errors functions can return so 90% of the time we were just returning
+  `REPROC_UNKNOWN_ERROR`. Furthermore, many operating system errors will be
+  fatal for most users and we suspect they'll all be handled similarly (stopping
+  the application or retrying).
+
+  As a result, in this release we stop trying to abstract system errors in
+  reproc. All system errors in `REPROC_ERROR` were replaced by a single value
+  (`REPROC_ERROR_SYSTEM`). `reproc::errc` was renamed to `reproc::error` and
+  turned into an error code instead of an error condition and only contains the
+  reproc-specific errors.
+
+  reproc users can still retrieve the specific system error using
+  `reproc_system_error`.
+
+  reproc++ users can still match against specific system errors using the
+  `std::errc` error condition enum
+  (<https://en.cppreference.com/w/cpp/error/errc>) or print a string
+  presentation of the error using the `message` method of `std::error_code`.
+
+  All values from `REPROC_ERROR` are now prefixed with `REPROC_ERROR` instead of
+  `REPROC` which helps reduce clutter in code completion.
+
+- Azure Pipelines CI now includes Visual Studio 2019.
+
+- Various smaller improvements and fixes.
+
+### CMake
+
+- Introduce `REPROC_MULTITHREADED` to configure whether reproc should link
+  against pthreads.
+
+  By default, `REPROC_MULTITHREADED` is enabled to prevent accidental undefined
+  behaviour caused by forgetting to enable `REPROC_MULTITHREADED`. Advanced
+  users might want to disable `REPROC_MULTITHREADED` when they know for certain
+  their code won't use more than a single thread.
+
+- doctest is now downloaded at configure time instead of being vendored inside
+  the reproc repository.
+
+  doctest is only downloaded if `REPROC_TEST` is enabled.
+
+## 6.0.0
+
+### General
+
+- Added Azure Pipelines CI.
+
+  Azure Pipelines provides 10 parallel jobs which is more than Travis and
+  Appveyor combined. If it turns out to be reliable Appveyor and Travis will
+  likely be dropped in the future. For now, all three are enabled.
+
+- Code cleanup and refactoring.
+
+### CMake
+
+- Renamed `REPROC_TESTS` to `REPROC_TEST`.
+- Renamed test executable from `tests` to `test`.
+
+### reproc
+
+- Renamed `reproc_type` to `reproc_t`.
+
+  We chose `reproc_type` initially because `_t` belongs to POSIX but we switch
+  to using `_t` because `reproc` is a sufficiently unique name that we don't
+  have to worry about naming conflicts.
+
+- reproc now keeps track of whether a process has exited and its exit status.
+
+  Keeping track of whether the child process has exited allows us to remove the
+  restriction that `reproc_wait`, `reproc_terminate`, `reproc_kill` and
+  `reproc_stop` cannot be called again on the same process after completing
+  successfully once. Now, if the process has already exited, these methods don't
+  do anything and return `REPROC_SUCCESS`.
+
+- Added `reproc_running` to allow checking whether a child process is still
+  running.
+
+- Added `reproc_exit_status` to allow querying the exit status of a process
+  after it has exited.
+
+- `reproc_wait` and `reproc_stop` lost their `exit_status` output parameter.
+
+  Use `reproc_exit_status` instead to retrieve the exit status.
+
+### reproc++
+
+- Added `process::running` and `process::exit_status`.
+
+  These delegate to `reproc_running` and `reproc_exit_status` respectively.
+
+- `process::wait` and `process::stop` lost their `exit_status` output parameter.
+
+  Use `process::exit_status` instead.
+
+## 5.0.1
+
+### reproc++
+
+- Fixed compilation error caused by defining `reproc::process`'s move assignment
+  operator as default in the header which is not allowed when a
+  `std::unique_ptr` member of an incomplete type is present.
+
+## 5.0.0
+
+### General
+
+- Added and rewrote implementation documentation.
+- General refactoring and simplification of the source code.
+
+### CMake
+
+- Raised minimum CMake version to 3.13.
+
+  Tests are now added to a single target `reproc-tests` in each subdirectory
+  included with `add_subdirectory`. Dependencies required to run the added tests
+  are added to `reproc-tests` with `target_link_libraries`. Before CMake 3.13,
+  `target_link_libraries` could not modify targets created outside of the
+  current directory which is why CMake 3.13 is needed.
+
+- `REPROC_CI` was renamed to `REPROC_WARNINGS_AS_ERRORS`.
+
+  This is a side effect of upgrading cddm. The variable was renamed in cddm to
+  more clearly indicate its purpose.
+
+- Removed namespace from reproc's targets.
+
+  To link against reproc or reproc++, you now have to link against the target
+  without a namespace prefix:
+
+  ```cmake
+  find_package(reproc) # or add_subdirectory(external/reproc)
+  target_link_libraries(myapp PRIVATE reproc)
+
+  find_package(reproc++) # or add_subdirectory(external/reproc++)
+  target_link_libraries(myapp PRIVATE reproc++)
+  ```
+
+  This change was made because of a change in cddm (a collection of CMake
+  functions to make setting up new projects easier) that removed namespacing and
+  aliases of library targets in favor of namespacing within the target name
+  itself. This change was made because the original target can still conflict
+  with other targets even after adding an alias. This can cause problems when
+  using generic names for targets inside the library itself. An example
+  clarifies the problem:
+
+  Imagine reproc added a target for working with processes asynchronously. In
+  the previous naming scheme, we'd do the following in reproc's CMake build
+  files:
+
+  ```cmake
+  add_library(async "")
+  add_library(reproc::async ALIAS async)
+  ```
+
+  However, there's a non-negligible chance that someone using reproc might also
+  have a target named async which would result in a conflict when using reproc
+  with `add_subdirectory` since there'd be two targets with the same name. With
+  the new naming scheme, we'd do the following instead:
+
+  ```cmake
+  add_library(reproc-async "")
+  ```
+
+  This has almost zero chance of conflicting with user's target names. The
+  advantage is that with this scheme we can use common target names without
+  conflicting with user's target names which was not the case with the previous
+  naming scheme.
+
+### reproc
+
+- Removed undefined behaviour in Windows implementation caused by casting an int
+  to an unsigned int.
+
+- Added a note to `reproc_start` docs about the behaviour of using a executable
+  path relative to the working directory combined with a custom working
+  directory for the child process on different platforms.
+
+- We now retrieve the file descriptor limit in the parent process (using
+  `sysconf`) instead of in the child process because `sysconf` is not guaranteed
+  to be async-signal-safe which all functions called in a child process after
+  forking should be.
+
+- Fixed compilation issue when `ATTRIBUTE_LIST_FOUND` was undefined (#15).
+
+### reproc++
+
+- Generified `process::start` so it works with any container of `std::string`
+  satisfying the
+  [SequenceContainer](https://en.cppreference.com/w/cpp/named_req/SequenceContainer)
+  interface.
+
+## 4.0.0
+
+### General
+
+- Internal improvements and documentation fixes.
+
+### reproc
+
+- Added `reproc_parse` which mimics reproc++'s `process::parse`.
+- Added `reproc_drain` which mimics reproc++'s `process::drain` along with an
+  example that explains how to use it.
+
+  Because C doesn't support lambda's, both of these functions take a function
+  pointer and an extra context argument which is passed to the function pointer
+  each time it is called. The context argument can be used to store any data
+  needed by the given function pointer.
+
+### reproc++
+
+- Renamed the `process::read` overload which takes a parser to `process::parse`.
+
+  This breaking change was done to keep consistency with reproc where we added
+  `reproc_parse`. We couldn't add another `reproc_read` since C doesn't support
+  overloading so we made the decision to rename `process::read` to
+  `process::parse` instead.
+
+- Changed `process::drain` sinks to return a boolean instead of `void`.
+
+  Before this change, the only way to stop draining a process was to throw an
+  exception from the sink. By changing sinks to return `bool`, a sink can tell
+  `drain` to stop if an error occurs by returning `false`. The error itself can
+  be stored in the sink if needed.
+
+## 3.1.3
+
+### CMake
+
+- Update project version in CMakeLists.txt from 3.0.0 to the actual latest
+  version (3.1.3).
+
+## 3.1.2
+
+### pkg-config
+
+- Fix pkg-config install prefix.
+
+## 3.1.0
+
+### CMake
+
+- Added `REPROC_INSTALL_PKGCONFIG` to control whether pkg-config files are
+  installed or not (default: `ON`).
+
+  The vcpkg package manager has no need for the pkg-config files so we added an
+  option to disable installing them.
+
+- Added `REPROC_INSTALL_CMAKECONFIGDIR` and `REPROC_INSTALL_PKGCONFIGDIR` to
+  control where cmake config files and pkg-config files are installed
+  respectively (default: `${CMAKE_INSTALL_LIBDIR}/cmake` and
+  `${CMAKE_INSTALL_LIBDIR}/pkgconfig`).
+
+  reproc already uses the values from `GNUInstallDirs` when generating its
+  install rules which are cache variables that be overridden by users. However,
+  `GNUInstallDirs` does not include variables for the installation directories
+  of CMake config files and pkg-config files. vcpkg requires cmake config files
+  to be installed to a different directory than the directory reproc used until
+  now. These options were added to allow vcpkg to control where the config files
+  are installed to.
+
+## 3.0.0
+
+### General
+
+- Removed support for Doxygen (and as a result `REPROC_DOCS`).
+
+  All the Doxygen directives made the header docstrings rather hard to read
+  directly. Doxygen's output was also too complicated for a simple library such
+  as reproc. Finally, Doxygen doesn't really provide any intuitive support for
+  documenting a set of libraries. I have an idea for a Doxygen alternative using
+  libclang and cmark but I'm not sure when I'll be able to implement it.
+
+### CMake
+
+- Renamed `REPROCXX` option to `REPROC++`.
+
+  `REPROCXX` was initially chosen because CMake didn't recommend using anything
+  other than letters and underscores for variable names. However, `REPROC++`
+  turns out to work without any problems so we use it since it's the expected
+  name for an option to build reproc++.
+
+- Stopped modifying the default `CMAKE_INSTALL_PREFIX` on Windows.
+
+  In 2.0.0, when installing to the default `CMAKE_INSTALL_PREFIX`, you would end
+  up with `C:\Program Files (x86)\reproc` and `C:\Program Files (x86)\reproc++`
+  when installing reproc. In 3.0.0, the default `CMAKE_INSTALL_PREFIX` isn't
+  modified anymore and all libraries are installed to `CMAKE_INSTALL_PREFIX` in
+  exactly the same way as they are on UNIX systems (include and lib
+  subdirectories directly beneath the installation directory). Sticking to the
+  defaults makes it easy to include reproc in various package managers such as
+  vcpkg.
+
+### reproc
+
+- `reproc_terminate` and `reproc_kill` don't call `reproc_wait` internally
+  anymore. `reproc_stop` has been changed to call `reproc_wait` after calling
+  `reproc_terminate` or `reproc_kill` so it still behaves the same.
+
+  Previously, calling `reproc_terminate` on a list of processes would only call
+  `reproc_terminate` on the next process after the previous process had exited
+  or the timeout had expired. This made terminating multiple processes take
+  longer than required. By removing the `reproc_wait` call from
+  `reproc_terminate`, users can first call `reproc_terminate` on all processes
+  before waiting for each of them with `reproc_wait` which makes terminating
+  multiple processes much faster.
+
+- Default to using `vfork` instead of `fork` on POSIX systems.
+
+  This change was made to increase `reproc_start`'s performance when the parent
+  process is using a large amount of memory. In these scenario's, `vfork` can be
+  a lot faster than `fork`. Care is taken to make sure signal handlers in the
+  child don't corrupt the state of the parent process. This change induces an
+  extra constraint in that `set*id` functions cannot be called while a call to
+  `reproc_start` is in process, but this situation is rare enough that the
+  tradeoff for better performance seems worth it.
+
+  A dependency on pthreads had to be added in order to safely use `vfork` (we
+  needed access to `pthread_sigmask`). The CMake and pkg-config files have been
+  updated to automatically find pthreads so users don't have to find it
+  themselves.
+
+- Renamed `reproc_error_to_string` to `reproc_strerror`.
+
+  The C standard library has `strerror` for retrieving a string representation
+  of an error. By using the same function name (prefixed with reproc) for a
+  function that does the same for reproc's errors, new users will immediately
+  know what the function does.
+
+### reproc++
+
+- reproc++ now takes timeouts as `std::chrono::duration` values (more specific
+  `reproc::milliseconds`) instead of unsigned ints.
+
+  Taking the `reproc::milliseconds` type explains a lot more about the expected
+  argument than taking an unsigned int. C++14 also added chrono literals which
+  make constructing `reproc::milliseconds` values a lot more concise
+  (`reproc::milliseconds(2000)` => `2000ms`).

+ 32 - 0
ThirdParty/Reproc/CMakeLists.txt

@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.12...3.21)
+
+project(
+  reproc
+  VERSION 14.2.4
+  DESCRIPTION "Cross-platform C99/C++11 process library"
+  HOMEPAGE_URL "https://github.com/DaanDeMeyer/reproc"
+  LANGUAGES C
+)
+
+# Common options and functions separated for easier reuse in other projects.
+include(cmake/reproc.cmake)
+
+option(REPROC++ "Build reproc++" ${REPROC_DEVELOP})
+option(
+  REPROC_MULTITHREADED
+  "Use `pthread_sigmask` and link against the system's thread library"
+  ON
+)
+
+if(REPROC_MULTITHREADED)
+  set(THREADS_PREFER_PTHREAD_FLAG ON)
+  find_package(Threads REQUIRED)
+  set(REPROC_THREAD_LIBRARY ${CMAKE_THREAD_LIBS_INIT})
+endif()
+
+add_subdirectory(reproc)
+
+if(REPROC++)
+  enable_language(CXX)
+  add_subdirectory(reproc++)
+endif()

+ 21 - 0
ThirdParty/Reproc/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Daan De Meyer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 311 - 0
ThirdParty/Reproc/README.md

@@ -0,0 +1,311 @@
+# reproc
+
+- [What is reproc?](#what-is-reproc)
+- [Features](#features)
+- [Questions](#questions)
+- [Installation](#installation)
+- [Dependencies](#dependencies)
+- [CMake options](#cmake-options)
+- [Documentation](#documentation)
+- [Error handling](#error-handling)
+- [Multithreading](#multithreading)
+- [Gotchas](#gotchas)
+
+## What is reproc?
+
+reproc (Redirected Process) is a cross-platform C/C++ library that simplifies
+starting, stopping and communicating with external programs. The main use case
+is executing command line applications directly from C or C++ code and
+retrieving their output.
+
+reproc consists out of two libraries: reproc and reproc++. reproc is a C99
+library that contains the actual code for working with external programs.
+reproc++ depends on reproc and adapts its API to an idiomatic C++11 API. It also
+adds a few extras that simplify working with external programs from C++.
+
+## Features
+
+- Start any program directly from C or C++ code.
+- Communicate with a program via its standard streams.
+- Wait for a program to exit or forcefully stop it yourself. When forcefully
+  stopping a process you can either allow the process to clean up its resources
+  or stop it immediately.
+- The core library (reproc) is written in C99. An optional C++11 wrapper library
+  (reproc++) with extra features is available for use in C++ applications.
+- Multiple installation methods. Either build reproc as part of your project or
+  use a system installed version of reproc.
+
+## Usage
+
+```c
+#include <reproc/run.h>
+
+int main(void)
+{
+  const char *args[] = { "echo", "Hello, world!", NULL };
+  return reproc_run(args, (reproc_options) { 0 });
+}
+```
+
+## Questions
+
+If you have any questions after reading the readme and documentation you can
+either make an issue or ask questions directly in the reproc
+[gitter](https://gitter.im/reproc/Lobby) channel.
+
+## Installation
+
+**Note: Building reproc requires CMake 3.12 or higher.**
+
+There are multiple ways to get reproc into your project. One way is to build
+reproc as part of your project using CMake. To do this, we first have to get the
+reproc source code into the project. This can be done using any of the following
+options:
+
+- When using CMake 3.11 or later, you can use the CMake `FetchContent` API to
+  download reproc when running CMake. See
+  <https://cliutils.gitlab.io/modern-cmake/chapters/projects/fetch.html> for an
+  example.
+- Another option is to include reproc's repository as a git submodule.
+  <https://cliutils.gitlab.io/modern-cmake/chapters/projects/submodule.html>
+  provides more information.
+- A very simple solution is to just include reproc's source code in your
+  repository. You can download a zip of the source code without the git history
+  and add it to your repository in a separate directory.
+
+After including reproc's source code in your project, it can be built from the
+root CMakeLists.txt file as follows:
+
+```cmake
+add_subdirectory(<path-to-reproc>) # For example: add_subdirectory(external/reproc)
+```
+
+CMake options can be specified before calling `add_subdirectory`:
+
+```cmake
+set(REPROC++ ON)
+add_subdirectory(<path-to-reproc>)
+```
+
+**Note: If the option has already been cached in a previous CMake run, you'll
+have to clear CMake's cache to apply the new default value.**
+
+For more information on configuring reproc's build, see
+[CMake options](#cmake-options).
+
+You can also depend on an installed version of reproc. You can either build and
+install reproc yourself or install reproc via a package manager. reproc is
+available in the following package repositories:
+
+- Arch User Repository (<https://aur.archlinux.org/packages/reproc>)
+- vcpkg (https://github.com/microsoft/vcpkg/tree/master/ports/reproc)
+
+If using a package manager is not an option, you can build and install reproc
+from source (CMake 3.13+):
+
+```sh
+cmake -B build
+cmake --build build
+cmake --install build
+```
+
+Enable the `REPROC_TEST` option and build the `test` target to run the tests
+(CMake 3.13+):
+
+```sh
+cmake -B build -DREPROC_TEST=ON
+cmake --build build
+cmake --build build --target test
+```
+
+After installing reproc your build system will have to find it. reproc provides
+both CMake config files and pkg-config files to simplify finding a reproc
+installation using CMake and pkg-config respectively. Note that reproc and
+reproc++ are separate libraries and as a result have separate config files as
+well. Make sure to search for the one you want to use.
+
+To find an installed version of reproc using CMake:
+
+```cmake
+find_package(reproc) # Find reproc.
+find_package(reproc++) # Find reproc++.
+```
+
+After building reproc as part of your project or finding a installed version of
+reproc, you can link against it from within your CMakeLists.txt file as follows:
+
+```cmake
+target_link_libraries(myapp reproc) # Link against reproc.
+target_link_libraries(myapp reproc++) # Link against reproc++.
+```
+
+From Meson 0.53.2 onwards, reproc can be included as a CMake subproject in Meson
+build scripts. See https://mesonbuild.com/CMake-module.html for more
+information.
+
+## Dependencies
+
+By default, reproc has a dependency on pthreads on POSIX systems (`-pthread`)
+and a dependency on Winsock 2.2 on Windows systems (`-lws2_32`). CMake and
+pkg-config handle these dependencies automatically.
+
+## CMake options
+
+reproc's build can be configured using the following CMake options:
+
+### User
+
+- `REPROC++`: Build reproc++ (default: `${REPROC_DEVELOP}`)
+- `REPROC_TEST`: Build tests (default: `${REPROC_DEVELOP}`)
+
+  Run the tests by running the `test` binary which can be found in the build
+  directory after building reproc.
+
+- `REPROC_EXAMPLES`: Build examples (default: `${REPROC_DEVELOP}`)
+
+  The resulting binaries will be located in the examples folder of each project
+  subdirectory in the build directory after building reproc.
+
+### Advanced
+
+- `REPROC_OBJECT_LIBRARIES`: Build CMake object libraries (default:
+  `${REPROC_DEVELOP}`)
+
+  This is useful to directly include reproc in another library. When building
+  reproc as a static or shared library, it has to be installed alongside the
+  consuming library which makes distributing the consuming library harder. When
+  using object libraries, reproc's object files are included directly into the
+  consuming library and no extra installation is necessary.
+
+  **Note: reproc's object libraries will only link correctly from CMake 3.14
+  onwards.**
+
+  **Note: This option overrides `BUILD_SHARED_LIBS`.**
+
+- `REPROC_INSTALL`: Generate installation rules (default: `ON` unless
+  `REPROC_OBJECT_LIBRARIES` is enabled)
+- `REPROC_INSTALL_CMAKECONFIGDIR`: CMake config files installation directory
+  (default: `${CMAKE_INSTALL_LIBDIR}/cmake`)
+- `REPROC_INSTALL_PKGCONFIG`: Install pkg-config files (default: `ON`)
+- `REPROC_INSTALL_PKGCONFIGDIR`: pkg-config files installation directory
+  (default: `${CMAKE_INSTALL_LIBDIR}/pkgconfig`)
+
+- `REPROC_MULTITHREADED`: Use `pthread_sigmask` and link against the system's
+  thread library (default: `ON`)
+
+### Developer
+
+- `REPROC_DEVELOP`: Configure option default values for development (default:
+  `OFF` unless the `REPROC_DEVELOP` environment variable is set)
+- `REPROC_SANITIZERS`: Build with sanitizers (default: `${REPROC_DEVELOP}`)
+- `REPROC_TIDY`: Run clang-tidy when building (default: `${REPROC_DEVELOP}`)
+- `REPROC_WARNINGS`: Enable compiler warnings (default: `${REPROC_DEVELOP}`)
+- `REPROC_WARNINGS_AS_ERRORS`: Add -Werror or equivalent to the compile flags
+  and clang-tidy (default: `OFF`)
+
+## Documentation
+
+Each function and class is documented extensively in its header file. Examples
+can be found in the examples subdirectory of [reproc](reproc/examples) and
+[reproc++](reproc++/examples).
+
+## Error handling
+
+On failure, Most functions in reproc's API return a negative `errno` (POSIX) or
+`GetLastError` (Windows) style error code. For actionable errors, reproc
+provides constants (`REPROC_ETIMEDOUT`, `REPROC_EPIPE`, ...) that can be used to
+match against the error without having to write platform-specific code. To get a
+string representation of an error, pass it to `reproc_strerror`.
+
+reproc++'s API integrates with the C++ standard library error codes mechanism
+(`std::error_code` and `std::error_condition`). Most methods in reproc++'s API
+return `std::error_code` values that contain the actual system error that
+occurred. You can test against these error codes using values from the
+`std::errc` enum.
+
+See the examples for more information on how to handle errors when using reproc.
+
+Note:
+
+Both reproc and reproc++ APIs take `options` argument that may define one or more
+`stop` actions such as `terminate` or `kill`.
+For that reason if the child process is being terminated or killed using a signal
+on POSIX, the error code will **not** reflect an error.
+
+It's up to the downstream project to *interpret* status codes reflecting unexpected
+behaviors alongside error codes (see this [example](https://github.com/DaanDeMeyer/reproc/issues/68#issuecomment-959074504)).
+
+## Multithreading
+
+Don't call the same operation on the same child process from more than one
+thread at the same time. For example: reading and writing to a child process
+from different threads is fine but waiting on the same child process from two
+different threads at the same time will result in issues.
+
+## Gotchas
+
+- (POSIX) It is strongly recommended to not call `waitpid` on pids of processes
+  started by reproc.
+
+  reproc uses `waitpid` to wait until a process has exited. Unfortunately,
+  `waitpid` cannot be called twice on the same process. This means that
+  `reproc_wait` won't work correctly if `waitpid` has already been called on a
+  child process beforehand outside of reproc.
+
+- It is strongly recommended to make sure each child process actually exits
+  using `reproc_wait` or `reproc_stop`.
+
+  On POSIX, a child process that has exited is a zombie process until the parent
+  process waits on it using `waitpid`. A zombie process takes up resources and
+  can be seen as a resource leak so it is important to make sure all processes
+  exit correctly in a timely fashion.
+
+- It is strongly recommended to try terminating a child process by waiting for
+  it to exit or by calling `reproc_terminate` before resorting to `reproc_kill`.
+
+  When using `reproc_kill` the child process does not receive a chance to
+  perform cleanup which could result in resources being leaked. Chief among
+  these leaks is that the child process will not be able to stop its own child
+  processes. Always try to let a child process exit normally by calling
+  `reproc_terminate` before calling `reproc_kill`. `reproc_stop` is a handy
+  helper function that can be used to perform multiple stop actions in a row
+  with timeouts inbetween.
+
+- (POSIX) It is strongly recommended to ignore the `SIGPIPE` signal in the
+  parent process.
+
+  On POSIX, writing to a closed stdin pipe of a child process will terminate the
+  parent process with the `SIGPIPE` signal by default. To avoid this, the
+  `SIGPIPE` signal has to be ignored in the parent process. If the `SIGPIPE`
+  signal is ignored `reproc_write` will return `REPROC_EPIPE` as expected when
+  writing to a closed stdin pipe.
+
+- While `reproc_terminate` allows the child process to perform cleanup it is up
+  to the child process to correctly clean up after itself. reproc only sends a
+  termination signal to the child process. The child process itself is
+  responsible for cleaning up its own child processes and other resources.
+
+- (Windows) `reproc_kill` is not guaranteed to kill a child process immediately
+  on Windows. For more information, read the Remarks section in the
+  documentation of the Windows `TerminateProcess` function that reproc uses to
+  kill child processes on Windows.
+
+- Child processes spawned via reproc inherit a single extra file handle which is
+  used to wait for the child process to exit. If the child process closes this
+  file handle manually, reproc will wrongly detect the child process has exited.
+  If this handle is further inherited by other processes that outlive the child
+  process, reproc will detect the child process is still running even if it has
+  exited. If data is written to this handle, reproc will also wrongly detect the
+  child process has exited.
+
+- (Windows) It's not possible to detect if a child process closes its stdout or
+  stderr stream before exiting. The parent process will only be notified that a
+  child process output stream is closed once that child process exits.
+
+- (Windows) reproc assumes that Windows creates sockets that are usable as file
+  system objects. More specifically, the default sockets returned by `WSASocket`
+  should have the `XP1_IFS_HANDLES ` flag set. This might not be the case if
+  there are external LSP providers installed on a Windows machine. If this is
+  the case, we recommend removing the software that's providing the extra
+  service providers since they're deprecated and should not be used anymore (see
+  https://docs.microsoft.com/en-us/windows/win32/winsock/categorizing-layered-service-providers-and-applications).

+ 408 - 0
ThirdParty/Reproc/cmake/reproc.cmake

@@ -0,0 +1,408 @@
+include(CheckCCompilerFlag)
+include(CMakePackageConfigHelpers)
+include(GenerateExportHeader)
+include(GNUInstallDirs)
+
+# Developer options
+
+option(REPROC_DEVELOP "Enable all developer options" $ENV{REPROC_DEVELOP})
+option(REPROC_TEST "Build tests" ${REPROC_DEVELOP})
+option(REPROC_EXAMPLES "Build examples" ${REPROC_DEVELOP})
+option(REPROC_WARNINGS "Enable compiler warnings" ${REPROC_DEVELOP})
+option(REPROC_TIDY "Run clang-tidy when building" ${REPROC_DEVELOP})
+
+option(
+  REPROC_SANITIZERS
+  "Build with sanitizers on configurations that support it"
+  ${REPROC_DEVELOP}
+)
+
+option(
+  REPROC_WARNINGS_AS_ERRORS
+  "Add -Werror or equivalent to the compile flags and clang-tidy"
+)
+
+mark_as_advanced(
+  REPROC_TIDY
+  REPROC_SANITIZERS
+  REPROC_WARNINGS_AS_ERRORS
+)
+
+# Installation options
+
+option(REPROC_OBJECT_LIBRARIES "Build CMake object libraries" ${REPROC_DEVELOP})
+
+if(NOT REPROC_OBJECT_LIBRARIES)
+  set(REPROC_INSTALL_DEFAULT ON)
+endif()
+
+option(REPROC_INSTALL "Generate installation rules" ${REPROC_INSTALL_DEFAULT})
+option(REPROC_INSTALL_PKGCONFIG "Install pkg-config files" ON)
+
+set(
+  REPROC_INSTALL_CMAKECONFIGDIR
+  ${CMAKE_INSTALL_LIBDIR}/cmake
+  CACHE STRING "CMake config files installation directory"
+)
+
+set(
+  REPROC_INSTALL_PKGCONFIGDIR
+  ${CMAKE_INSTALL_LIBDIR}/pkgconfig
+  CACHE STRING "pkg-config files installation directory"
+)
+
+mark_as_advanced(
+  REPROC_OBJECT_LIBRARIES
+  REPROC_INSTALL
+  REPROC_INSTALL_PKGCONFIG
+  REPROC_INSTALL_CMAKECONFIGDIR
+  REPROC_INSTALL_PKGCONFIGDIR
+)
+
+# Testing
+
+if(REPROC_TEST)
+  enable_testing()
+endif()
+
+# Build type
+
+if(REPROC_DEVELOP AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
+  set_property(
+    CACHE CMAKE_BUILD_TYPE
+    PROPERTY STRINGS Debug Release MinSizeRel RelWithDebInfo
+  )
+endif()
+
+# clang-tidy
+
+if(REPROC_TIDY)
+  find_program(REPROC_TIDY_PROGRAM clang-tidy)
+  mark_as_advanced(REPROC_TIDY_PROGRAM)
+
+  if(NOT REPROC_TIDY_PROGRAM)
+    message(FATAL_ERROR "clang-tidy not found")
+  endif()
+
+  if(REPROC_WARNINGS_AS_ERRORS)
+    set(REPROC_TIDY_PROGRAM ${REPROC_TIDY_PROGRAM} -warnings-as-errors=*)
+  endif()
+endif()
+
+# Functions
+
+function(reproc_common TARGET LANGUAGE NAME DIRECTORY)
+  if(LANGUAGE STREQUAL C)
+    set(STANDARD 99)
+    target_compile_features(${TARGET} PUBLIC c_std_99)
+  else()
+    # clang-tidy uses the MSVC standard library instead of MinGW's standard
+    # library so we have to use C++14 (because MSVC headers use C++14).
+    if(MINGW AND REPROC_TIDY)
+      set(STANDARD 14)
+    else()
+      set(STANDARD 11)
+    endif()
+
+    target_compile_features(${TARGET} PUBLIC cxx_std_11)
+  endif()
+
+  set_target_properties(${TARGET} PROPERTIES
+    ${LANGUAGE}_STANDARD ${STANDARD}
+    ${LANGUAGE}_STANDARD_REQUIRED ON
+    ${LANGUAGE}_EXTENSIONS OFF
+    OUTPUT_NAME "${NAME}"
+    RUNTIME_OUTPUT_DIRECTORY "${DIRECTORY}"
+    ARCHIVE_OUTPUT_DIRECTORY "${DIRECTORY}"
+    LIBRARY_OUTPUT_DIRECTORY "${DIRECTORY}"
+  )
+
+  if(REPROC_TIDY AND REPROC_TIDY_PROGRAM)
+    set_property(
+      TARGET ${TARGET}
+      # `REPROC_TIDY_PROGRAM` is a list so we surround it with quotes to pass it
+      # as a single argument.
+      PROPERTY ${LANGUAGE}_CLANG_TIDY "${REPROC_TIDY_PROGRAM}"
+    )
+  endif()
+
+  # Common development flags (warnings + sanitizers + colors)
+
+  if(REPROC_WARNINGS)
+    if(MSVC)
+      check_c_compiler_flag(/permissive- REPROC_HAVE_PERMISSIVE)
+
+      target_compile_options(${TARGET} PRIVATE
+        /nologo # Silence MSVC compiler version output.
+        $<$<BOOL:${REPROC_WARNINGS_AS_ERRORS}>:/WX> # -Werror
+        $<$<BOOL:${REPROC_HAVE_PERMISSIVE}>:/permissive->
+      )
+
+      if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15.0)
+        # CMake 3.15 does not add /W3 to the compiler flags by default anymore
+        # so we add /W4 instead.
+        target_compile_options(${TARGET} PRIVATE /W4)
+      endif()
+
+      if(LANGUAGE STREQUAL C)
+        # Disable MSVC warnings that flag C99 features as non-standard.
+        target_compile_options(${TARGET} PRIVATE /wd4204 /wd4221)
+      endif()
+    else()
+      target_compile_options(${TARGET} PRIVATE
+        -Wall
+        -Wextra
+        -pedantic
+        -Wconversion
+        -Wsign-conversion
+        $<$<BOOL:${REPROC_WARNINGS_AS_ERRORS}>:-Werror>
+        $<$<BOOL:${REPROC_WARNINGS_AS_ERRORS}>:-pedantic-errors>
+      )
+
+      if(LANGUAGE STREQUAL C OR CMAKE_CXX_COMPILER_ID MATCHES Clang)
+        target_compile_options(${TARGET} PRIVATE -Wmissing-prototypes)
+      endif()
+    endif()
+
+    if(WIN32)
+      target_compile_definitions(${TARGET} PRIVATE _CRT_SECURE_NO_WARNINGS)
+    endif()
+
+    target_compile_options(${TARGET} PRIVATE
+      $<$<${LANGUAGE}_COMPILER_ID:GNU>:-fdiagnostics-color>
+      $<$<${LANGUAGE}_COMPILER_ID:Clang>:-fcolor-diagnostics>
+    )
+  endif()
+
+  if(REPROC_SANITIZERS AND NOT MSVC AND NOT MINGW)
+    if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15.0)
+      set_property(
+        TARGET ${TARGET}
+        PROPERTY MSVC_RUNTIME_LIBRARY MultiThreaded
+      )
+    endif()
+
+    target_compile_options(${TARGET} PRIVATE
+      -fsanitize=address,undefined
+      -fno-omit-frame-pointer
+    )
+
+    target_link_libraries(${TARGET} PRIVATE -fsanitize=address,undefined)
+  endif()
+endfunction()
+
+function(reproc_library TARGET LANGUAGE)
+  if(REPROC_OBJECT_LIBRARIES)
+    add_library(${TARGET} OBJECT)
+  else()
+    add_library(${TARGET})
+  endif()
+
+  reproc_common(${TARGET} ${LANGUAGE} "" lib)
+
+  if(BUILD_SHARED_LIBS AND NOT REPROC_OBJECT_LIBRARIES)
+    # Enable -fvisibility=hidden and -fvisibility-inlines-hidden.
+    set_target_properties(${TARGET} PROPERTIES
+      ${LANGUAGE}_VISIBILITY_PRESET hidden
+      VISIBILITY_INLINES_HIDDEN true
+    )
+
+    # clang-tidy errors with: unknown argument: '-fno-keep-inline-dllexport'
+    # when enabling `VISIBILITY_INLINES_HIDDEN` on MinGW so we disable it when
+    # running clang-tidy on MinGW.
+    if(MINGW AND REPROC_TIDY)
+      set_property(TARGET ${TARGET} PROPERTY VISIBILITY_INLINES_HIDDEN false)
+    endif()
+
+    # Disable CMake's default export definition.
+    set_property(TARGET ${TARGET} PROPERTY DEFINE_SYMBOL "")
+
+    string(TOUPPER ${TARGET} TARGET_UPPER)
+    string(REPLACE + X TARGET_SANITIZED ${TARGET_UPPER})
+
+    target_compile_definitions(${TARGET} PRIVATE ${TARGET_SANITIZED}_BUILDING)
+    if(WIN32)
+      target_compile_definitions(${TARGET} PUBLIC ${TARGET_SANITIZED}_SHARED)
+    endif()
+  endif()
+
+  # Make sure we follow the popular naming convention for shared libraries on
+  # UNIX systems.
+  set_target_properties(${TARGET} PROPERTIES
+    VERSION ${PROJECT_VERSION}
+    SOVERSION ${PROJECT_VERSION_MAJOR}
+  )
+
+  # Only use the headers from the repository when building. When installing we
+  # want to use the install location of the headers (e.g. /usr/include) as the
+  # include directory instead.
+  target_include_directories(${TARGET} PUBLIC
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+  )
+
+  # Adapted from https://codingnest.com/basic-cmake-part-2/.
+  # Each library is installed separately (with separate config files).
+
+  if(REPROC_INSTALL)
+
+    # Headers
+
+    install(
+      DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
+      DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
+      COMPONENT ${TARGET}-development
+    )
+
+    # Library
+
+    install(
+      TARGETS ${TARGET}
+      EXPORT ${TARGET}-targets
+      RUNTIME
+        DESTINATION ${CMAKE_INSTALL_BINDIR}
+        COMPONENT ${TARGET}-runtime
+      LIBRARY
+        DESTINATION ${CMAKE_INSTALL_LIBDIR}
+        COMPONENT ${TARGET}-runtime
+        NAMELINK_COMPONENT ${TARGET}-development
+      ARCHIVE
+        DESTINATION ${CMAKE_INSTALL_LIBDIR}
+        COMPONENT ${TARGET}-development
+      INCLUDES
+        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
+    )
+
+    if(NOT APPLE)
+      set_property(
+        TARGET ${TARGET}
+        PROPERTY INSTALL_RPATH $ORIGIN
+      )
+    endif()
+
+    # CMake config
+
+    configure_package_config_file(
+      ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}-config.cmake.in
+      ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config.cmake
+      INSTALL_DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET}
+    )
+
+    write_basic_package_version_file(
+      ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config-version.cmake
+      COMPATIBILITY SameMajorVersion
+    )
+
+    install(
+      FILES
+        ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config.cmake
+        ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config-version.cmake
+      DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET}
+      COMPONENT ${TARGET}-development
+    )
+
+    install(
+      EXPORT ${TARGET}-targets
+      DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET}
+      COMPONENT ${TARGET}-development
+    )
+
+    # pkg-config
+
+    if(REPROC_INSTALL_PKGCONFIG)
+      configure_file(
+        ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}.pc.in
+        ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.pc
+        @ONLY
+      )
+
+      install(
+        FILES ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.pc
+        DESTINATION ${REPROC_INSTALL_PKGCONFIGDIR}
+        COMPONENT ${TARGET}-development
+      )
+    endif()
+  endif()
+endfunction()
+
+function(reproc_test TARGET NAME LANGUAGE)
+  if(NOT REPROC_TEST)
+    return()
+  endif()
+
+  if(LANGUAGE STREQUAL C)
+    set(EXTENSION c)
+  else()
+    set(EXTENSION cpp)
+  endif()
+
+  add_executable(${TARGET}-test-${NAME} test/${NAME}.${EXTENSION})
+
+  reproc_common(${TARGET}-test-${NAME} ${LANGUAGE} ${NAME} test)
+  target_link_libraries(${TARGET}-test-${NAME} PRIVATE ${TARGET})
+
+  if(MINGW)
+    target_compile_definitions(${TARGET}-test-${NAME} PRIVATE
+      __USE_MINGW_ANSI_STDIO=1 # Add %zu on Mingw
+    )
+  endif()
+
+  add_test(NAME ${TARGET}-test-${NAME} COMMAND ${TARGET}-test-${NAME})
+
+  if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources/${NAME}.c)
+    target_compile_definitions(${TARGET}-test-${NAME} PRIVATE
+      RESOURCE_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/resources"
+    )
+
+    if (NOT TARGET ${TARGET}-resource-${NAME})
+      add_executable(${TARGET}-resource-${NAME} resources/${NAME}.c)
+      reproc_common(${TARGET}-resource-${NAME} C ${NAME} resources)
+    endif()
+
+    # Make sure the test resource is available when running the test.
+    add_dependencies(${TARGET}-test-${NAME} ${TARGET}-resource-${NAME})
+  endif()
+endfunction()
+
+function(reproc_example TARGET NAME LANGUAGE)
+  cmake_parse_arguments(OPT "" "" "ARGS;DEPENDS" ${ARGN})
+  if(NOT REPROC_EXAMPLES)
+    return()
+  endif()
+
+  if(LANGUAGE STREQUAL C)
+    set(EXTENSION c)
+  else()
+    set(EXTENSION cpp)
+  endif()
+
+  add_executable(${TARGET}-example-${NAME} examples/${NAME}.${EXTENSION})
+
+  reproc_common(${TARGET}-example-${NAME} ${LANGUAGE} ${NAME} examples)
+  target_link_libraries(${TARGET}-example-${NAME} PRIVATE ${TARGET} ${OPT_DEPENDS})
+
+  if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources/${NAME}.c)
+    target_compile_definitions(${TARGET}-example-${NAME} PRIVATE
+      RESOURCE_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/resources"
+    )
+
+    if (NOT TARGET ${TARGET}-resource-${NAME})
+      add_executable(${TARGET}-resource-${NAME} resources/${NAME}.c)
+      reproc_common(${TARGET}-resource-${NAME} C ${NAME} resources)
+    endif()
+
+    # Make sure the example resource is available when running the example.
+    add_dependencies(${TARGET}-example-${NAME} ${TARGET}-resource-${NAME})
+  endif()
+
+  if(REPROC_TEST)
+    if(NOT DEFINED OPT_ARGS)
+      set(OPT_ARGS cmake --help)
+    endif()
+
+    add_test(
+      NAME ${TARGET}-example-${NAME}
+      COMMAND ${TARGET}-example-${NAME} ${OPT_ARGS}
+    )
+  endif()
+endfunction()

+ 22 - 0
ThirdParty/Reproc/reproc++/CMakeLists.txt

@@ -0,0 +1,22 @@
+reproc_library(reproc++ CXX)
+
+target_link_libraries(reproc++ PRIVATE
+  reproc
+  $<$<BOOL:${REPROC_MULTITHREADED}>:Threads::Threads>
+)
+
+target_sources(
+  reproc++
+  PRIVATE src/reproc.cpp
+  # We manually propagate reproc's object files until CMake adds support for
+  # doing it automatically.
+  INTERFACE $<$<BOOL:${REPROC_OBJECT_LIBRARIES}>:$<TARGET_OBJECTS:reproc>>
+)
+
+reproc_example(reproc++ drain CXX)
+reproc_example(reproc++ forward CXX)
+reproc_example(reproc++ run CXX)
+
+if(REPROC_MULTITHREADED)
+  reproc_example(reproc++ background CXX DEPENDS Threads::Threads)
+endif()

+ 89 - 0
ThirdParty/Reproc/reproc++/examples/background.cpp

@@ -0,0 +1,89 @@
+#include <future>
+#include <iostream>
+#include <mutex>
+#include <string>
+
+#include <reproc++/drain.hpp>
+#include <reproc++/reproc.hpp>
+
+static int fail(std::error_code ec)
+{
+  std::cerr << ec.message();
+  return ec.value();
+}
+
+// The background example reads the output of a child process in a background
+// thread and shows how to access the current output in the main thread while
+// the background thread is still running.
+
+// Like the forward example it forwards its arguments to a child process and
+// prints the child process output on stdout.
+int main(int argc, const char **argv)
+{
+  if (argc <= 1) {
+    std::cerr << "No arguments provided. Example usage: "
+              << "./background cmake --help";
+    return EXIT_FAILURE;
+  }
+
+  reproc::process process;
+
+  reproc::stop_actions stop = {
+    { reproc::stop::terminate, reproc::milliseconds(5000) },
+    { reproc::stop::kill, reproc::milliseconds(2000) },
+    {}
+  };
+
+  reproc::options options;
+  options.stop = stop;
+
+  std::error_code ec = process.start(argv + 1, options);
+
+  if (ec == std::errc::no_such_file_or_directory) {
+    std::cerr << "Program not found. Make sure it's available from the PATH.";
+    return ec.value();
+  } else if (ec) {
+    return fail(ec);
+  }
+
+  // We need a mutex along with `output` to prevent the main thread and
+  // background thread from modifying `output` at the same time (`std::string`
+  // is not thread safe).
+  std::string output;
+  std::mutex mutex;
+
+  auto drain_async = std::async(std::launch::async, [&process, &output,
+                                                     &mutex]() {
+    // `sink::thread_safe::string` locks a given mutex before appending to the
+    // given string, allowing working with the string across multiple threads if
+    // the mutex is locked in the other threads as well.
+    reproc::sink::thread_safe::string sink(output, mutex);
+    return reproc::drain(process, sink, sink);
+  });
+
+  // Show new output every 2 seconds.
+  while (drain_async.wait_for(std::chrono::seconds(2)) !=
+         std::future_status::ready) {
+    std::lock_guard<std::mutex> lock(mutex);
+    std::cout << output;
+    // Clear output that's already been flushed to `std::cout`.
+    output.clear();
+  }
+
+  // Flush any remaining output of `process`.
+  std::cout << output;
+
+  // Check if any errors occurred in the background thread.
+  ec = drain_async.get();
+  if (ec) {
+    return fail(ec);
+  }
+
+  int status = 0;
+  std::tie(status, ec) = process.stop(options.stop);
+  if (ec) {
+    return fail(ec);
+  }
+
+  return status;
+}

+ 76 - 0
ThirdParty/Reproc/reproc++/examples/drain.cpp

@@ -0,0 +1,76 @@
+#include <array>
+#include <iostream>
+
+#include <reproc++/drain.hpp>
+#include <reproc++/reproc.hpp>
+
+static int fail(std::error_code ec)
+{
+  std::cerr << ec.message();
+  return ec.value();
+}
+
+// Uses `reproc::drain` to show the output of the given command.
+int main(int argc, const char **argv)
+{
+  if (argc <= 1) {
+    std::cerr << "No arguments provided. Example usage: "
+              << "./drain cmake --help";
+    return EXIT_FAILURE;
+  }
+
+  reproc::process process;
+
+  // reproc++ uses error codes to report errors. If exceptions are preferred,
+  // convert `std::error_code`'s to exceptions using `std::system_error`.
+  std::error_code ec = process.start(argv + 1);
+
+  // reproc++ converts system errors to `std::error_code`'s of the system
+  // category. These can be matched against using values from the `std::errc`
+  // error condition. See https://en.cppreference.com/w/cpp/error/errc for more
+  // information.
+  if (ec == std::errc::no_such_file_or_directory) {
+    std::cerr << "Program not found. Make sure it's available from the PATH.";
+    return ec.value();
+  } else if (ec) {
+    return fail(ec);
+  }
+
+  // `reproc::drain` reads from the stdout and stderr streams of `process` until
+  // both are closed or an error occurs. Providing it with a string sink for a
+  // specific stream makes it store all output of that stream in the string
+  // passed to the string sink. Passing the same sink to both the `out` and
+  // `err` arguments of `reproc::drain` causes the stdout and stderr output to
+  // get stored in the same string.
+  std::string output;
+  reproc::sink::string sink(output);
+  // By default, reproc only redirects stdout to a pipe and not stderr so we
+  // pass `reproc::sink::null` as the sink for stderr here. We could also pass
+  // `sink` but it wouldn't receive any data from stderr.
+  ec = reproc::drain(process, sink, reproc::sink::null);
+  if (ec) {
+    return fail(ec);
+  }
+
+  std::cout << output << std::flush;
+
+  // It's easy to define your own sinks as well. Take a look at `drain.hpp` in
+  // the repository to see how `sink::string` and other sinks are implemented.
+  // The documentation of `reproc::drain` also provides more information on the
+  // requirements a sink should fulfill.
+
+  // By default, The `process` destructor waits indefinitely for the child
+  // process to exit to ensure proper cleanup. See the forward example for
+  // information on how this can be configured. However, when relying on the
+  // `process` destructor, we cannot check the exit status of the process so it
+  // usually makes sense to explicitly wait for the process to exit and check
+  // its exit status.
+
+  int status = 0;
+  std::tie(status, ec) = process.wait(reproc::infinite);
+  if (ec) {
+    return fail(ec);
+  }
+
+  return status;
+}

+ 83 - 0
ThirdParty/Reproc/reproc++/examples/forward.cpp

@@ -0,0 +1,83 @@
+#include <iostream>
+
+#include <reproc++/drain.hpp>
+#include <reproc++/reproc.hpp>
+
+static int fail(std::error_code ec)
+{
+  std::cerr << ec.message();
+  return ec.value();
+}
+
+// The forward example forwards the program arguments to a child process and
+// prints its output on stdout.
+//
+// Example: "./forward cmake --help" will print CMake's help output.
+//
+// This program can be used to verify that manually executing a command and
+// executing it using reproc produces the same output.
+int main(int argc, const char **argv)
+{
+  if (argc <= 1) {
+    std::cerr << "No arguments provided. Example usage: ./forward cmake --help";
+    return EXIT_FAILURE;
+  }
+
+  reproc::process process;
+
+  // Stop actions can be passed to both `process::start` (via `options`) and
+  // `process::stop`. Stop actions passed to `process::start` are passed to
+  // `process::stop` in the `process` destructor. This can be used to make sure
+  // that a child process is always stopped correctly when its corresponding
+  // `process` instance is destroyed.
+  //
+  // Any program can be started with forward so we make sure the child process
+  // is cleaned up correctly by specifying `reproc::terminate` which sends
+  // `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waits five seconds. We also
+  // add the `reproc::kill` flag which sends `SIGKILL` (POSIX) or calls
+  // `TerminateProcess` (Windows) if the process hasn't exited after five
+  // seconds and waits two more seconds for the child process to exit.
+  //
+  // If the `stop_actions` struct passed to `process::start` is
+  // default-initialized, the `process` destructor will wait indefinitely for
+  // the child process to exit.
+  //
+  // Note that C++14 has chrono literals which allows
+  // `reproc::milliseconds(5000)` to be replaced with `5000ms`.
+  reproc::stop_actions stop = {
+    { reproc::stop::noop, reproc::milliseconds(0) },
+    { reproc::stop::terminate, reproc::milliseconds(5000) },
+    { reproc::stop::kill, reproc::milliseconds(2000) }
+  };
+
+  reproc::options options;
+  options.stop = stop;
+
+  // We have the child process inherit the parent's standard streams so the
+  // child process reads directly from the stdin and writes directly to the
+  // stdout/stderr of the parent process.
+  options.redirect.parent = true;
+
+  // Exclude `argv[0]` which is the current program's name.
+  std::error_code ec = process.start(argv + 1, options);
+
+  if (ec == std::errc::no_such_file_or_directory) {
+    std::cerr << "Program not found. Make sure it's available from the PATH.";
+    return ec.value();
+  } else if (ec) {
+    return fail(ec);
+  }
+
+  // Call `process::stop` manually so we can access the exit status. We add
+  // `reproc::wait` with a timeout of ten seconds to give the process time to
+  // exit on its own before sending `SIGTERM`.
+  options.stop.first = { reproc::stop::wait, reproc::milliseconds(10000) };
+
+  int status = 0;
+  std::tie(status, ec) = process.stop(options.stop);
+  if (ec) {
+    return fail(ec);
+  }
+
+  return status;
+}

+ 24 - 0
ThirdParty/Reproc/reproc++/examples/run.cpp

@@ -0,0 +1,24 @@
+#include <iostream>
+
+#include <reproc++/run.hpp>
+
+// Equivalent to reproc's run example but implemented using reproc++.
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  int status = -1;
+  std::error_code ec;
+
+  reproc::options options;
+  options.redirect.parent = true;
+  options.deadline = reproc::milliseconds(5000);
+
+  std::tie(status, ec) = reproc::run(argv + 1, options);
+
+  if (ec) {
+    std::cerr << ec.message() << std::endl;
+  }
+
+  return ec ? ec.value() : status;
+}

+ 59 - 0
ThirdParty/Reproc/reproc++/include/reproc++/arguments.hpp

@@ -0,0 +1,59 @@
+#pragma once
+
+#include <reproc++/detail/array.hpp>
+#include <reproc++/detail/type_traits.hpp>
+
+namespace reproc {
+
+class arguments : public detail::array {
+public:
+  arguments(const char *const *argv) // NOLINT
+      : detail::array(argv, false)
+  {}
+
+  /*!
+  `Arguments` must be iterable as a sequence of strings. Examples of types that
+  satisfy this requirement are `std::vector<std::string>` and
+  `std::array<std::string>`.
+
+  `arguments` has the same restrictions as `argv` in `reproc_start` except
+  that it should not end with `NULL` (`start` allocates a new array which
+  includes the missing `NULL` value).
+  */
+  template <typename Arguments,
+            typename = detail::enable_if_not_char_array<Arguments>>
+  arguments(const Arguments &arguments) // NOLINT
+      : detail::array(from(arguments), true)
+  {}
+
+private:
+  template <typename Arguments>
+  static const char *const *from(const Arguments &arguments);
+};
+
+template <typename Arguments>
+const char *const *arguments::from(const Arguments &arguments)
+{
+  using size_type = typename Arguments::value_type::size_type;
+
+  const char **argv = new const char *[arguments.size() + 1];
+  std::size_t current = 0;
+
+  for (const auto &argument : arguments) {
+    char *string = new char[argument.size() + 1];
+
+    argv[current++] = string;
+
+    for (size_type i = 0; i < argument.size(); i++) {
+      *string++ = argument[i];
+    }
+
+    *string = '\0';
+  }
+
+  argv[current] = nullptr;
+
+  return argv;
+}
+
+}

+ 53 - 0
ThirdParty/Reproc/reproc++/include/reproc++/detail/array.hpp

@@ -0,0 +1,53 @@
+#pragma once
+
+#include <cstddef>
+
+namespace reproc {
+namespace detail {
+
+class array {
+  const char *const *data_;
+  bool owned_;
+
+public:
+  array(const char *const *data, bool owned) noexcept
+      : data_(data), owned_(owned)
+  {}
+
+  array(array &&other) noexcept : data_(other.data_), owned_(other.owned_)
+  {
+    other.data_ = nullptr;
+    other.owned_ = false;
+  }
+
+  array &operator=(array &&other) noexcept
+  {
+    if (&other != this) {
+      data_ = other.data_;
+      owned_ = other.owned_;
+      other.data_ = nullptr;
+      other.owned_ = false;
+    }
+
+    return *this;
+  }
+
+  ~array() noexcept
+  {
+    if (owned_) {
+      for (size_t i = 0; data_[i] != nullptr; i++) {
+        delete[] data_[i];
+      }
+
+      delete[] data_;
+    }
+  }
+
+  const char *const *data() const noexcept
+  {
+    return data_;
+  }
+};
+
+}
+}

+ 18 - 0
ThirdParty/Reproc/reproc++/include/reproc++/detail/type_traits.hpp

@@ -0,0 +1,18 @@
+#pragma once
+
+#include <type_traits>
+
+namespace reproc {
+namespace detail {
+
+template <bool B, typename T = void>
+using enable_if = typename std::enable_if<B, T>::type;
+
+template <typename T>
+using is_char_array = std::is_convertible<T, const char *const *>;
+
+template <typename T>
+using enable_if_not_char_array = enable_if<!is_char_array<T>::value>;
+
+}
+}

+ 152 - 0
ThirdParty/Reproc/reproc++/include/reproc++/drain.hpp

@@ -0,0 +1,152 @@
+#pragma once
+
+#include <mutex>
+#include <ostream>
+#include <string>
+
+#include <reproc++/reproc.hpp>
+
+namespace reproc {
+
+/*!
+`reproc_drain` but takes lambdas as sinks. Return an error code from a sink to
+break out of `drain` early. `out` and `err` expect the following signature:
+
+```c++
+std::error_code sink(stream stream, const uint8_t *buffer, size_t size);
+```
+*/
+template <typename Out, typename Err>
+std::error_code drain(process &process, Out &&out, Err &&err)
+{
+  static constexpr uint8_t initial = 0;
+  std::error_code ec;
+
+  // A single call to `read` might contain multiple messages. By always calling
+  // both sinks once with no data before reading, we give them the chance to
+  // process all previous output before reading from the child process again.
+
+  ec = out(stream::in, &initial, 0);
+  if (ec) {
+    return ec;
+  }
+
+  ec = err(stream::in, &initial, 0);
+  if (ec) {
+    return ec;
+  }
+
+  static constexpr size_t BUFFER_SIZE = 4096;
+  uint8_t buffer[BUFFER_SIZE] = {};
+
+  for (;;) {
+    int events = 0;
+    std::tie(events, ec) = process.poll(event::out | event::err, infinite);
+    if (ec) {
+      ec = ec == error::broken_pipe ? std::error_code() : ec;
+      break;
+    }
+
+    if (events & event::deadline) {
+      ec = std::make_error_code(std::errc::timed_out);
+      break;
+    }
+
+    stream stream = events & event::out ? stream::out : stream::err;
+
+    size_t bytes_read = 0;
+    std::tie(bytes_read, ec) = process.read(stream, buffer, BUFFER_SIZE);
+    if (ec && ec != error::broken_pipe) {
+      break;
+    }
+
+    bytes_read = ec == error::broken_pipe ? 0 : bytes_read;
+
+    // This used to be `auto &sink = stream == stream::out ? out : err;` but
+    // that doesn't actually work if `out` and `err` are not the same type.
+    if (stream == stream::out) {
+      ec = out(stream, buffer, bytes_read);
+    } else {
+      ec = err(stream, buffer, bytes_read);
+    }
+
+    if (ec) {
+      break;
+    }
+  }
+
+  return ec;
+}
+
+namespace sink {
+
+/*! Reads all output into `string`. */
+class string {
+  std::string &string_;
+
+public:
+  explicit string(std::string &string) noexcept : string_(string) {}
+
+  std::error_code operator()(stream stream, const uint8_t *buffer, size_t size)
+  {
+    (void) stream;
+    string_.append(reinterpret_cast<const char *>(buffer), size);
+    return {};
+  }
+};
+
+/*! Forwards all output to `ostream`. */
+class ostream {
+  std::ostream &ostream_;
+
+public:
+  explicit ostream(std::ostream &ostream) noexcept : ostream_(ostream) {}
+
+  std::error_code operator()(stream stream, const uint8_t *buffer, size_t size)
+  {
+    (void) stream;
+    ostream_.write(reinterpret_cast<const char *>(buffer),
+                   static_cast<std::streamsize>(size));
+    return {};
+  }
+};
+
+/*! Discards all output. */
+class discard {
+public:
+  std::error_code
+  operator()(stream stream, const uint8_t *buffer, size_t size) const noexcept
+  {
+    (void) stream;
+    (void) buffer;
+    (void) size;
+
+    return {};
+  }
+};
+
+constexpr discard null = discard();
+
+namespace thread_safe {
+
+/*! `sink::string` but locks the given mutex before invoking the sink. */
+class string {
+  sink::string sink_;
+  std::mutex &mutex_;
+
+public:
+  string(std::string &string, std::mutex &mutex) noexcept
+      : sink_(string), mutex_(mutex)
+  {}
+
+  std::error_code operator()(stream stream, const uint8_t *buffer, size_t size)
+  {
+    std::lock_guard<std::mutex> lock(mutex_);
+    return sink_(stream, buffer, size);
+  }
+};
+
+}
+
+}
+}

+ 76 - 0
ThirdParty/Reproc/reproc++/include/reproc++/env.hpp

@@ -0,0 +1,76 @@
+#pragma once
+
+#include <reproc++/detail/array.hpp>
+#include <reproc++/detail/type_traits.hpp>
+
+namespace reproc {
+
+class env : public detail::array {
+public:
+  enum type {
+    extend,
+    empty,
+  };
+
+  env(const char *const *envp = nullptr) // NOLINT
+      : detail::array(envp, false)
+  {}
+
+  /*!
+  `Env` must be iterable as a sequence of string pairs. Examples of
+  types that satisfy this requirement are `std::vector<std::pair<std::string,
+  std::string>>` and `std::map<std::string, std::string>`.
+
+  The pairs in `env` represent the extra environment variables of the child
+  process and are converted to the right format before being passed as the
+  environment to `reproc_start` via the `env.extra` field of `reproc_options`.
+  */
+  template <typename Env,
+            typename = detail::enable_if_not_char_array<Env>>
+  env(const Env &env) // NOLINT
+      : detail::array(from(env), true)
+  {}
+
+private:
+  template <typename Env>
+  static const char *const *from(const Env &env);
+};
+
+template <typename Env>
+const char *const *env::from(const Env &env)
+{
+  using name_size_type = typename Env::value_type::first_type::size_type;
+  using value_size_type = typename Env::value_type::second_type::size_type;
+
+  const char **envp = new const char *[env.size() + 1];
+  std::size_t current = 0;
+
+  for (const auto &entry : env) {
+    const auto &name = entry.first;
+    const auto &value = entry.second;
+
+    // We add 2 to the size to reserve space for the '=' sign and the NUL
+    // terminator at the end of the string.
+    char *string = new char[name.size() + value.size() + 2];
+
+    envp[current++] = string;
+
+    for (name_size_type i = 0; i < name.size(); i++) {
+      *string++ = name[i];
+    }
+
+    *string++ = '=';
+
+    for (value_size_type i = 0; i < value.size(); i++) {
+      *string++ = value[i];
+    }
+
+    *string = '\0';
+  }
+
+  envp[current] = nullptr;
+
+  return envp;
+}
+
+}

+ 21 - 0
ThirdParty/Reproc/reproc++/include/reproc++/export.hpp

@@ -0,0 +1,21 @@
+#pragma once
+
+#ifndef REPROCXX_EXPORT
+  #ifdef _WIN32
+    #ifdef REPROCXX_SHARED
+      #ifdef REPROCXX_BUILDING
+        #define REPROCXX_EXPORT __declspec(dllexport)
+      #else
+        #define REPROCXX_EXPORT __declspec(dllimport)
+      #endif
+    #else
+      #define REPROCXX_EXPORT
+    #endif
+  #else
+    #ifdef REPROCXX_BUILDING
+      #define REPROCXX_EXPORT __attribute__((visibility("default")))
+    #else
+      #define REPROCXX_EXPORT
+    #endif
+  #endif
+#endif

+ 37 - 0
ThirdParty/Reproc/reproc++/include/reproc++/input.hpp

@@ -0,0 +1,37 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+namespace reproc {
+
+class input {
+  const uint8_t *data_ = nullptr;
+  size_t size_ = 0;
+
+public:
+  input() = default;
+
+  input(const uint8_t *data, size_t size) : data_(data), size_(size) {}
+
+  /*! Implicitly convert from string literals. */
+  template <size_t N>
+  input(const char (&data)[N]) // NOLINT
+      : data_(reinterpret_cast<const uint8_t *>(data)), size_(N)
+  {}
+
+  input(const input &other) = default;
+  input &operator=(const input &) = default;
+
+  const uint8_t *data() const noexcept
+  {
+    return data_;
+  }
+
+  size_t size() const noexcept
+  {
+    return size_;
+  }
+};
+
+}

+ 223 - 0
ThirdParty/Reproc/reproc++/include/reproc++/reproc.hpp

@@ -0,0 +1,223 @@
+#pragma once
+
+#include <chrono>
+#include <cstdint>
+#include <cstdio>
+#include <memory>
+#include <system_error>
+#include <utility>
+
+#include <reproc++/arguments.hpp>
+#include <reproc++/env.hpp>
+#include <reproc++/export.hpp>
+#include <reproc++/input.hpp>
+
+// Forward declare `reproc_t` so we don't have to include reproc.h in the
+// header.
+struct reproc_t;
+
+/*! The `reproc` namespace wraps all reproc++ declarations. `process` wraps
+reproc's API inside a C++ class. To avoid exposing reproc's API when using
+reproc++ all structs, enums and constants of reproc have a replacement in
+reproc++. Only differences in behaviour compared to reproc are documented. Refer
+to reproc.h and the examples for general information on how to use reproc. */
+namespace reproc {
+
+/*! Conversion from reproc `errno` constants to `std::errc` constants:
+https://en.cppreference.com/w/cpp/error/errc */
+using error = std::errc;
+
+namespace signal {
+
+REPROCXX_EXPORT extern const int kill;
+REPROCXX_EXPORT extern const int terminate;
+
+}
+
+/*! Timeout values are passed as `reproc::milliseconds` instead of `int` in
+reproc++. */
+using milliseconds = std::chrono::duration<int, std::milli>;
+
+REPROCXX_EXPORT extern const milliseconds infinite;
+REPROCXX_EXPORT extern const milliseconds deadline;
+
+enum class stop {
+  noop,
+  wait,
+  terminate,
+  kill,
+};
+
+struct stop_action {
+  stop action;
+  milliseconds timeout;
+};
+
+struct stop_actions {
+  stop_action first;
+  stop_action second;
+  stop_action third;
+};
+
+#if defined(_WIN32)
+using handle = void *;
+#else
+using handle = int;
+#endif
+
+struct redirect {
+  enum type {
+    default_, // Unfortunately, both `default` and `auto` are keywords.
+    pipe,
+    parent,
+    discard,
+    // stdout would conflict with a macro on Windows.
+    stdout_,
+    // Unfortunately, class members and nested enum members can't have the same
+    // name.
+    handle_,
+    file_,
+    path_,
+  };
+
+  enum type type;
+  reproc::handle handle;
+  FILE *file;
+  const char *path;
+};
+
+struct options {
+  struct {
+    env::type behavior;
+    /*! Implicitly converts from any STL container of string pairs to the
+    environment format expected by `reproc_start`. */
+    class env extra;
+  } env = {};
+
+  const char *working_directory = nullptr;
+
+  struct {
+    redirect in;
+    redirect out;
+    redirect err;
+    bool parent;
+    bool discard;
+    FILE *file;
+    const char *path;
+  } redirect = {};
+
+  struct stop_actions stop = {};
+  reproc::milliseconds timeout = reproc::milliseconds(0);
+  reproc::milliseconds deadline = reproc::milliseconds(0);
+  /*! Implicitly converts from string literals to the pointer size pair expected
+  by `reproc_start`. */
+  class input input;
+  bool nonblocking = false;
+
+  /*! Make a shallow copy of `options`. */
+  static options clone(const options &other)
+  {
+    struct options clone;
+    clone.env.behavior = other.env.behavior;
+    // Make sure we make a shallow copy of `environment`.
+    clone.env.extra = other.env.extra.data();
+    clone.working_directory = other.working_directory;
+    clone.redirect = other.redirect;
+    clone.stop = other.stop;
+    clone.timeout = other.timeout;
+    clone.deadline = other.deadline;
+    clone.input = other.input;
+
+    return clone;
+  }
+};
+
+enum class stream {
+  in,
+  out,
+  err,
+};
+
+class process;
+
+namespace event {
+
+enum {
+  in = 1 << 0,
+  out = 1 << 1,
+  err = 1 << 2,
+  exit = 1 << 3,
+  deadline = 1 << 4,
+};
+
+struct source {
+  class process &process;
+  int interests;
+  int events;
+};
+
+}
+
+REPROCXX_EXPORT std::error_code poll(event::source *sources,
+                                     size_t num_sources,
+                                     milliseconds timeout = infinite);
+
+/*! Improves on reproc's API by adding RAII and changing the API of some
+functions to be more idiomatic C++. */
+class process {
+
+public:
+  REPROCXX_EXPORT process();
+  REPROCXX_EXPORT ~process() noexcept;
+
+  // Enforce unique ownership of child processes.
+  REPROCXX_EXPORT process(process &&other) noexcept;
+  REPROCXX_EXPORT process &operator=(process &&other) noexcept;
+
+  /*! `reproc_start` but implicitly converts from STL containers to the
+  arguments format expected by `reproc_start`. */
+  REPROCXX_EXPORT std::error_code start(const arguments &arguments,
+                                        const options &options = {}) noexcept;
+
+  REPROCXX_EXPORT std::pair<int, std::error_code> pid() noexcept;
+
+  /*! Sets the `fork` option in `reproc_options` and calls `start`. Returns
+  `true` in the child process and `false` in the parent process. */
+  REPROCXX_EXPORT std::pair<bool, std::error_code>
+  fork(const options &options = {}) noexcept;
+
+  /*! Shorthand for `reproc::poll` that only polls this process. Returns a pair
+  of (events, error). */
+  REPROCXX_EXPORT std::pair<int, std::error_code>
+  poll(int interests, milliseconds timeout = infinite);
+
+  /*! `reproc_read` but returns a pair of (bytes read, error). */
+  REPROCXX_EXPORT std::pair<size_t, std::error_code>
+  read(stream stream, uint8_t *buffer, size_t size) noexcept;
+
+  /*! reproc_write` but returns a pair of (bytes_written, error). */
+  REPROCXX_EXPORT std::pair<size_t, std::error_code>
+  write(const uint8_t *buffer, size_t size) noexcept;
+
+  REPROCXX_EXPORT std::error_code close(stream stream) noexcept;
+
+  /*! `reproc_wait` but returns a pair of (status, error). */
+  REPROCXX_EXPORT std::pair<int, std::error_code>
+  wait(milliseconds timeout) noexcept;
+
+  REPROCXX_EXPORT std::error_code terminate() noexcept;
+
+  REPROCXX_EXPORT std::error_code kill() noexcept;
+
+  /*! `reproc_stop` but returns a pair of (status, error). */
+  REPROCXX_EXPORT std::pair<int, std::error_code>
+  stop(stop_actions stop) noexcept;
+
+private:
+  REPROCXX_EXPORT friend std::error_code
+  poll(event::source *sources, size_t num_sources, milliseconds timeout);
+
+  std::unique_ptr<reproc_t, reproc_t *(*) (reproc_t *)> impl_;
+};
+
+}

+ 41 - 0
ThirdParty/Reproc/reproc++/include/reproc++/run.hpp

@@ -0,0 +1,41 @@
+#pragma once
+
+#include <reproc++/drain.hpp>
+#include <reproc++/reproc.hpp>
+
+namespace reproc {
+
+template <typename Out, typename Err>
+std::pair<int, std::error_code>
+run(const arguments &arguments, const options &options, Out &&out, Err &&err)
+{
+  process process;
+  std::error_code ec;
+
+  ec = process.start(arguments, options);
+  if (ec) {
+    return { -1, ec };
+  }
+
+  ec = drain(process, std::forward<Out>(out), std::forward<Err>(err));
+  if (ec) {
+    return { -1, ec };
+  }
+
+  return process.stop(options.stop);
+}
+
+inline std::pair<int, std::error_code> run(const arguments &arguments,
+                                           const options &options = {})
+{
+  struct options modified = options::clone(options);
+
+  if (!options.redirect.discard && options.redirect.file == nullptr &&
+      options.redirect.path == nullptr) {
+    modified.redirect.parent = true;
+  }
+
+  return run(arguments, modified, sink::null, sink::null);
+}
+
+}

+ 13 - 0
ThirdParty/Reproc/reproc++/reproc++-config.cmake.in

@@ -0,0 +1,13 @@
+@PACKAGE_INIT@
+
+set(REPROC_MULTITHREADED @REPROC_MULTITHREADED@)
+
+include(CMakeFindDependencyMacro)
+find_dependency(reproc @PROJECT_VERSION@)
+
+if(REPROC_MULTITHREADED)
+  set(THREADS_PREFER_PTHREAD_FLAG ON)
+  find_dependency(Threads)
+endif()
+
+include(${CMAKE_CURRENT_LIST_DIR}/@[email protected])

+ 13 - 0
ThirdParty/Reproc/reproc++/reproc++.pc.in

@@ -0,0 +1,13 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=${prefix}
+includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
+libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@
+
+Name: @TARGET@
+Description: @PROJECT_DESCRIPTION@
+URL: @PROJECT_HOMEPAGE_URL@
+Version: @PROJECT_VERSION@
+Cflags: -I${includedir}
+Libs: -L${libdir} -l@TARGET@
+Libs.private: @REPROC_THREAD_LIBRARY@
+Requires.private: reproc = @PROJECT_VERSION@

+ 168 - 0
ThirdParty/Reproc/reproc++/src/reproc.cpp

@@ -0,0 +1,168 @@
+#include <reproc++/reproc.hpp>
+
+#include <reproc/reproc.h>
+
+namespace reproc {
+
+namespace signal {
+
+const int kill = REPROC_SIGKILL;
+const int terminate = REPROC_SIGTERM;
+
+}
+
+const milliseconds infinite = milliseconds(REPROC_INFINITE);
+const milliseconds deadline = milliseconds(REPROC_DEADLINE);
+
+static std::error_code error_code_from(int r)
+{
+  if (r >= 0) {
+    return {};
+  }
+
+  if (r == REPROC_EPIPE) {
+    // https://github.com/microsoft/STL/pull/406
+    return { static_cast<int>(std::errc::broken_pipe),
+             std::generic_category() };
+  }
+
+  return { -r, std::system_category() };
+}
+
+static reproc_stop_actions reproc_stop_actions_from(stop_actions stop)
+{
+  return {
+    { static_cast<REPROC_STOP>(stop.first.action), stop.first.timeout.count() },
+    { static_cast<REPROC_STOP>(stop.second.action),
+      stop.second.timeout.count() },
+    { static_cast<REPROC_STOP>(stop.third.action), stop.third.timeout.count() }
+  };
+}
+
+static reproc_redirect reproc_redirect_from(redirect redirect)
+{
+  return { static_cast<REPROC_REDIRECT>(redirect.type), redirect.handle,
+           redirect.file, redirect.path };
+}
+
+static reproc_options reproc_options_from(const options &options, bool fork)
+{
+  return {
+    options.working_directory,
+    { static_cast<REPROC_ENV>(options.env.behavior), options.env.extra.data() },
+    { reproc_redirect_from(options.redirect.in),
+      reproc_redirect_from(options.redirect.out),
+      reproc_redirect_from(options.redirect.err), options.redirect.parent,
+      options.redirect.discard, options.redirect.file, options.redirect.path },
+    reproc_stop_actions_from(options.stop),
+    options.deadline.count(),
+    { options.input.data(), options.input.size() },
+    options.nonblocking,
+    fork
+  };
+}
+
+process::process() : impl_(reproc_new(), reproc_destroy) {}
+process::~process() noexcept = default;
+
+process::process(process &&other) noexcept = default;
+process &process::operator=(process &&other) noexcept = default;
+
+std::error_code process::start(const arguments &arguments,
+                               const options &options) noexcept
+{
+  reproc_options reproc_options = reproc_options_from(options, false);
+  int r = reproc_start(impl_.get(), arguments.data(), reproc_options);
+  return error_code_from(r);
+}
+
+std::pair<bool, std::error_code> process::fork(const options &options) noexcept
+{
+  reproc_options reproc_options = reproc_options_from(options, true);
+  int r = reproc_start(impl_.get(), nullptr, reproc_options);
+  return { r == 0, error_code_from(r) };
+}
+
+std::pair<int, std::error_code> process::poll(int interests,
+                                              milliseconds timeout)
+{
+  event::source source{ *this, interests, 0 };
+  std::error_code ec = ::reproc::poll(&source, 1, timeout);
+  return { source.events, ec };
+}
+
+std::pair<size_t, std::error_code>
+process::read(stream stream, uint8_t *buffer, size_t size) noexcept
+{
+  int r = reproc_read(impl_.get(), static_cast<REPROC_STREAM>(stream), buffer,
+                      size);
+  return { r, error_code_from(r) };
+}
+
+std::pair<size_t, std::error_code> process::write(const uint8_t *buffer,
+                                                  size_t size) noexcept
+{
+  int r = reproc_write(impl_.get(), buffer, size);
+  return { r, error_code_from(r) };
+}
+
+std::error_code process::close(stream stream) noexcept
+{
+  int r = reproc_close(impl_.get(), static_cast<REPROC_STREAM>(stream));
+  return error_code_from(r);
+}
+
+std::pair<int, std::error_code> process::wait(milliseconds timeout) noexcept
+{
+  int r = reproc_wait(impl_.get(), timeout.count());
+  return { r, error_code_from(r) };
+}
+
+std::error_code process::terminate() noexcept
+{
+  int r = reproc_terminate(impl_.get());
+  return error_code_from(r);
+}
+
+std::error_code process::kill() noexcept
+{
+  int r = reproc_kill(impl_.get());
+  return error_code_from(r);
+}
+
+std::pair<int, std::error_code> process::stop(stop_actions stop) noexcept
+{
+  int r = reproc_stop(impl_.get(), reproc_stop_actions_from(stop));
+  return { r, error_code_from(r) };
+}
+
+std::pair<int, std::error_code> process::pid() noexcept
+{
+  int r = reproc_pid(impl_.get());
+  return { r, error_code_from(r) };
+}
+
+std::error_code
+poll(event::source *sources, size_t num_sources, milliseconds timeout)
+{
+  auto *reproc_sources = new reproc_event_source[num_sources];
+
+  for (size_t i = 0; i < num_sources; i++) {
+    reproc_sources[i] = { sources[i].process.impl_.get(), sources[i].interests,
+                          0 };
+  }
+
+  int r = reproc_poll(reproc_sources, num_sources, timeout.count());
+
+  if (r >= 0) {
+    for (size_t i = 0; i < num_sources; i++) {
+      sources[i].events = reproc_sources[i].events;
+    }
+  }
+
+  delete[] reproc_sources;
+
+  return error_code_from(r);
+}
+
+}

+ 62 - 0
ThirdParty/Reproc/reproc/CMakeLists.txt

@@ -0,0 +1,62 @@
+if(WIN32)
+  set(REPROC_WINSOCK_LIBRARY ws2_32)
+elseif(NOT APPLE)
+  set(REPROC_RT_LIBRARY rt) # clock_gettime
+endif()
+
+reproc_library(reproc C)
+
+if(REPROC_MULTITHREADED)
+  target_compile_definitions(reproc PRIVATE REPROC_MULTITHREADED)
+  target_link_libraries(reproc PRIVATE Threads::Threads)
+endif()
+
+if(WIN32)
+  set(PLATFORM windows)
+  target_compile_definitions(reproc PRIVATE WIN32_LEAN_AND_MEAN)
+  target_link_libraries(reproc PRIVATE ${REPROC_WINSOCK_LIBRARY})
+else()
+  set(PLATFORM posix)
+  if(NOT APPLE)
+    target_link_libraries(reproc PRIVATE ${REPROC_RT_LIBRARY})
+  endif()
+endif()
+
+target_sources(reproc PRIVATE
+  src/clock.${PLATFORM}.c
+  src/drain.c
+  src/error.${PLATFORM}.c
+  src/handle.${PLATFORM}.c
+  src/init.${PLATFORM}.c
+  src/options.c
+  src/pipe.${PLATFORM}.c
+  src/process.${PLATFORM}.c
+  src/redirect.${PLATFORM}.c
+  src/redirect.c
+  src/reproc.c
+  src/run.c
+  src/strv.c
+  src/utf.${PLATFORM}.c
+)
+
+reproc_test(reproc argv C)
+reproc_test(reproc deadline C)
+reproc_test(reproc env C)
+reproc_test(reproc io C)
+reproc_test(reproc overflow C)
+reproc_test(reproc path C)
+reproc_test(reproc stop C)
+reproc_test(reproc working-directory C)
+reproc_test(reproc pid C)
+
+if(UNIX)
+  reproc_test(reproc fork C)
+endif()
+
+reproc_example(reproc drain C)
+reproc_example(reproc env C ARGS PROJECT=REPROC)
+reproc_example(reproc path C)
+reproc_example(reproc poll C)
+reproc_example(reproc read C)
+reproc_example(reproc parent C)
+reproc_example(reproc run C)

+ 62 - 0
ThirdParty/Reproc/reproc/examples/drain.c

@@ -0,0 +1,62 @@
+#include <stdlib.h>
+
+#include <reproc/drain.h>
+#include <reproc/reproc.h>
+
+// Shows the output of the given command using `reproc_drain`.
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  reproc_t *process = NULL;
+  char *output = NULL;
+  int r = REPROC_ENOMEM;
+
+  process = reproc_new();
+  if (process == NULL) {
+    goto finish;
+  }
+
+  r = reproc_start(process, argv + 1, (reproc_options){ 0 });
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = reproc_close(process, REPROC_STREAM_IN);
+  if (r < 0) {
+    goto finish;
+  }
+
+  // `reproc_drain` reads from a child process and passes the output to the
+  // given sinks. A sink consists of a function pointer and a context pointer
+  // which is always passed to the function. reproc provides several built-in
+  // sinks such as `reproc_sink_string` which stores all provided output in the
+  // given string. Passing the same sink to both output streams makes sure the
+  // output from both streams is combined into a single string.
+  reproc_sink sink = reproc_sink_string(&output);
+  // By default, reproc only redirects stdout to a pipe and not stderr so we
+  // pass `REPROC_SINK_NULL` as the sink for stderr here. We could also pass
+  // `sink` but it wouldn't receive any data from stderr.
+  r = reproc_drain(process, sink, REPROC_SINK_NULL);
+  if (r < 0) {
+    goto finish;
+  }
+
+  printf("%s", output);
+
+  r = reproc_wait(process, REPROC_INFINITE);
+  if (r < 0) {
+    goto finish;
+  }
+
+finish:
+  // Memory allocated by `reproc_sink_string` must be freed with `reproc_free`.
+  reproc_free(output);
+  reproc_destroy(process);
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}

+ 21 - 0
ThirdParty/Reproc/reproc/examples/env.c

@@ -0,0 +1,21 @@
+#include <stdlib.h>
+
+#include <reproc/run.h>
+
+// Runs a binary as a child process that prints all its environment variables to
+// stdout and exits. Additional environment variables (in the format A=B) passed
+// via the command line are added to the child process environment variables.
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  const char *args[] = { RESOURCE_DIRECTORY "/env", NULL };
+
+  int r = reproc_run(args, (reproc_options){ .env.extra = argv + 1 });
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}

+ 42 - 0
ThirdParty/Reproc/reproc/examples/parent.c

@@ -0,0 +1,42 @@
+#include <stdlib.h>
+
+#include <reproc/reproc.h>
+
+// Forwards the provided command to `reproc_start` and redirects the standard
+// streams of the child process to the standard streams of the parent process.
+int main(int argc, const char **argv)
+{
+  if (argc <= 1) {
+    fprintf(stderr,
+            "No arguments provided. Example usage: ./inherit cmake --help");
+    return EXIT_FAILURE;
+  }
+
+  reproc_t *process = NULL;
+  int r = REPROC_ENOMEM;
+
+  process = reproc_new();
+  if (process == NULL) {
+    goto finish;
+  }
+
+  r = reproc_start(process, argv + 1,
+                   (reproc_options){ .redirect.parent = true });
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = reproc_wait(process, REPROC_INFINITE);
+  if (r < 0) {
+    goto finish;
+  }
+
+finish:
+  reproc_destroy(process);
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}

+ 18 - 0
ThirdParty/Reproc/reproc/examples/path.c

@@ -0,0 +1,18 @@
+#include <stdlib.h>
+
+#include <reproc/run.h>
+
+// Redirects the output of the given command to the reproc.out file.
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  int r = reproc_run(argv + 1,
+                     (reproc_options){ .redirect.path = "reproc.out" });
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}

+ 107 - 0
ThirdParty/Reproc/reproc/examples/poll.c

@@ -0,0 +1,107 @@
+#ifdef _WIN32
+  #include <windows.h>
+static void millisleep(long ms)
+{
+  Sleep((DWORD) ms);
+}
+static int getpid()
+{
+  return (int) GetCurrentProcessId();
+}
+#else
+  #define _POSIX_C_SOURCE 200809L
+  #include <time.h>
+  #include <unistd.h>
+static inline void millisleep(long ms)
+{
+  nanosleep(&(struct timespec){ .tv_sec = (ms) / 1000,
+                                .tv_nsec = ((ms) % 1000L) * 1000000 },
+            NULL);
+}
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <reproc/reproc.h>
+
+enum { NUM_CHILDREN = 20 };
+
+static int parent(const char *program)
+{
+  reproc_event_source children[NUM_CHILDREN] = { { 0 } };
+  int r = -1;
+
+  for (int i = 0; i < NUM_CHILDREN; i++) {
+    reproc_t *process = reproc_new();
+
+    const char *args[] = { program, "child", NULL };
+
+    r = reproc_start(process, args, (reproc_options){ .nonblocking = true });
+    if (r < 0) {
+      goto finish;
+    }
+
+    children[i].process = process;
+    children[i].interests = REPROC_EVENT_OUT;
+  }
+
+  for (;;) {
+    r = reproc_poll(children, NUM_CHILDREN, REPROC_INFINITE);
+    if (r < 0) {
+      r = r == REPROC_EPIPE ? 0 : r;
+      goto finish;
+    }
+
+    for (int i = 0; i < NUM_CHILDREN; i++) {
+      if (children[i].process == NULL || !children[i].events) {
+        continue;
+      }
+
+      uint8_t output[4096];
+      r = reproc_read(children[i].process, REPROC_STREAM_OUT, output,
+                      sizeof(output));
+      if (r == REPROC_EPIPE) {
+        // `reproc_destroy` returns `NULL`. Event sources with their process set
+        // to `NULL` are ignored by `reproc_poll`.
+        children[i].process = reproc_destroy(children[i].process);
+        continue;
+      }
+
+      if (r < 0) {
+        goto finish;
+      }
+
+      output[r] = '\0';
+      printf("%s\n", output);
+    }
+  }
+
+finish:
+  for (int i = 0; i < NUM_CHILDREN; i++) {
+    reproc_destroy(children[i].process);
+  }
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}
+
+static int child(void)
+{
+  srand(((unsigned int) getpid()));
+  int ms = rand() % NUM_CHILDREN * 4; // NOLINT
+  millisleep(ms);
+  printf("Process %i slept %i milliseconds.", getpid(), ms);
+  return EXIT_SUCCESS;
+}
+
+// Starts a number of child processes that each sleep a random amount of
+// milliseconds before printing a message and exiting. The parent process polls
+// each of the child processes and prints their messages to stdout.
+int main(int argc, const char **argv)
+{
+  return argc > 1 && strcmp(argv[1], "child") == 0 ? child() : parent(argv[0]);
+}

+ 108 - 0
ThirdParty/Reproc/reproc/examples/read.c

@@ -0,0 +1,108 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include <reproc/reproc.h>
+
+// Prints the output of the given command using `reproc_read`. Usually, using
+// `reproc_run` or `reproc_drain` is a better solution when dealing with a
+// single child process.
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  // `reproc_t` stores necessary information between calls to reproc's API.
+  reproc_t *process = NULL;
+  char *output = NULL;
+  size_t size = 0;
+  int r = REPROC_ENOMEM;
+
+  process = reproc_new();
+  if (process == NULL) {
+    goto finish;
+  }
+
+  // `reproc_start` takes a child process instance (`reproc_t`), argv and
+  // a set of options including the working directory and environment of the
+  // child process. If the working directory is `NULL` the working directory of
+  // the parent process is used. If the environment is `NULL`, the environment
+  // of the parent process is used.
+  r = reproc_start(process, argv + 1, (reproc_options){ 0 });
+
+  // On failure, reproc's API functions return a negative `errno` (POSIX) or
+  // `GetLastError` (Windows) style error code. To check against common error
+  // codes, reproc provides cross platform constants such as `REPROC_EPIPE` and
+  // `REPROC_ETIMEDOUT`.
+  if (r < 0) {
+    goto finish;
+  }
+
+  // Close the stdin stream since we're not going to write any input to the
+  // child process.
+  r = reproc_close(process, REPROC_STREAM_IN);
+  if (r < 0) {
+    goto finish;
+  }
+
+  // Read the entire output of the child process. I've found this pattern to be
+  // the most readable when reading the entire output of a child process. The
+  // while loop keeps running until an error occurs in `reproc_read` (the child
+  // process closing its output stream is also reported as an error).
+  for (;;) {
+    uint8_t buffer[4096];
+    r = reproc_read(process, REPROC_STREAM_OUT, buffer, sizeof(buffer));
+    if (r < 0) {
+      break;
+    }
+
+    // On success, `reproc_read` returns the amount of bytes read.
+    size_t bytes_read = (size_t) r;
+
+    // Increase the size of `output` to make sure it can hold the new output.
+    // This is definitely not the most performant way to grow a buffer so keep
+    // that in mind. Add 1 to size to leave space for the NUL terminator which
+    // isn't included in `output_size`.
+    char *result = realloc(output, size + bytes_read + 1);
+    if (result == NULL) {
+      r = REPROC_ENOMEM;
+      goto finish;
+    }
+
+    output = result;
+
+    // Copy new data into `output`.
+    memcpy(output + size, buffer, bytes_read);
+    output[size + bytes_read] = '\0';
+    size += bytes_read;
+  }
+
+  // Check that the while loop stopped because the output stream of the child
+  // process was closed and not because of any other error.
+  if (r != REPROC_EPIPE) {
+    goto finish;
+  }
+
+  printf("%s", output);
+
+  // Wait for the process to exit. This should always be done since some systems
+  // (POSIX) don't clean up system resources allocated to a child process until
+  // the parent process explicitly waits for it after it has exited.
+  r = reproc_wait(process, REPROC_INFINITE);
+  if (r < 0) {
+    goto finish;
+  }
+
+finish:
+  free(output);
+
+  // Clean up all the resources allocated to the child process (including the
+  // memory allocated by `reproc_new`). Unless custom stop actions are passed to
+  // `reproc_start`, `reproc_destroy` will first wait indefinitely for the child
+  // process to exit.
+  reproc_destroy(process);
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}

+ 19 - 0
ThirdParty/Reproc/reproc/examples/run.c

@@ -0,0 +1,19 @@
+#include <stdlib.h>
+
+#include <reproc/run.h>
+
+// Start a process from the arguments given on the command line. Inherit the
+// parent's standard streams and allow the process to run for maximum 5 seconds
+// before terminating it.
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  int r = reproc_run(argv + 1, (reproc_options){ .deadline = 5000 });
+
+  if (r < 0) {
+    fprintf(stderr, "%s\n", reproc_strerror(r));
+  }
+
+  return abs(r);
+}

+ 79 - 0
ThirdParty/Reproc/reproc/include/reproc/drain.h

@@ -0,0 +1,79 @@
+#pragma once
+
+#include <reproc/reproc.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*! Used by `reproc_drain` to provide data to the caller. Each time data is
+read, `function` is called with `context`. If a sink returns a non-zero value,
+`reproc_drain` will return immediately with the same value. */
+typedef struct reproc_sink {
+  int (*function)(REPROC_STREAM stream,
+                  const uint8_t *buffer,
+                  size_t size,
+                  void *context);
+  void *context;
+} reproc_sink;
+
+/*! Pass `REPROC_SINK_NULL` as the sink for output streams that have not been
+redirected to a pipe. */
+REPROC_EXPORT extern const reproc_sink REPROC_SINK_NULL;
+
+/*!
+Reads from the child process stdout and stderr until an error occurs or both
+streams are closed. The `out` and `err` sinks receive the output from stdout and
+stderr respectively. The same sink may be passed to both `out` and `err`.
+
+`reproc_drain` always starts by calling both sinks once with an empty buffer and
+`stream` set to `REPROC_STREAM_IN` to give each sink the chance to process all
+output from the previous call to `reproc_drain` one by one.
+
+When a stream is closed, its corresponding `sink` is called once with `size` set
+to zero.
+
+Note that his function returns 0 instead of `REPROC_EPIPE` when both output
+streams of the child process are closed.
+
+Actionable errors:
+- `REPROC_ETIMEDOUT`
+*/
+REPROC_EXPORT int
+reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err);
+
+/*!
+Appends the output of a process (stdout and stderr) to the value of `output`.
+`output` must point to either `NULL` or a NUL-terminated string.
+
+Calls `realloc` as necessary to make space in `output` to store the output of
+the child process. Make sure to always call `reproc_free` on the value of
+`output` after calling `reproc_drain` (even if it fails).
+
+Because the resulting sink does not store the output size, `strlen` is called
+each time data is read to calculate the current size of the output. This might
+cause performance problems when draining processes that produce a lot of output.
+
+Similarly, this sink will not work on processes that have NUL terminators in
+their output because `strlen` is used to calculate the current output size.
+
+Returns `REPROC_ENOMEM` if a call to `realloc` fails. `output` will contain any
+output read from the child process, preceeded by whatever was stored in it at
+the moment its corresponding sink was passed to `reproc_drain`.
+
+The `drain` example shows how to use `reproc_sink_string`.
+```
+*/
+REPROC_EXPORT reproc_sink reproc_sink_string(char **output);
+
+/*! Discards the output of a process. */
+REPROC_EXPORT reproc_sink reproc_sink_discard(void);
+
+/*! Calls `free` on `ptr` and returns `NULL`. Use this function to free memory
+allocated by `reproc_sink_string`. This avoids issues with allocating across
+module (DLL) boundaries on Windows. */
+REPROC_EXPORT void *reproc_free(void *ptr);
+
+#ifdef __cplusplus
+}
+#endif

+ 21 - 0
ThirdParty/Reproc/reproc/include/reproc/export.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#ifndef REPROC_EXPORT
+  #ifdef _WIN32
+    #ifdef REPROC_SHARED
+      #ifdef REPROC_BUILDING
+        #define REPROC_EXPORT __declspec(dllexport)
+      #else
+        #define REPROC_EXPORT __declspec(dllimport)
+      #endif
+    #else
+      #define REPROC_EXPORT
+    #endif
+  #else
+    #ifdef REPROC_BUILDING
+      #define REPROC_EXPORT __attribute__((visibility("default")))
+    #else
+      #define REPROC_EXPORT
+    #endif
+  #endif
+#endif

+ 530 - 0
ThirdParty/Reproc/reproc/include/reproc/reproc.h

@@ -0,0 +1,530 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <reproc/export.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*! Used to store information about a child process. `reproc_t` is an opaque
+type and can be allocated and released via `reproc_new` and `reproc_destroy`
+respectively. */
+typedef struct reproc_t reproc_t;
+
+/*! reproc error naming follows POSIX errno naming prefixed with `REPROC`. */
+
+/*! An invalid argument was passed to an API function */
+REPROC_EXPORT extern const int REPROC_EINVAL;
+/*! A timeout value passed to an API function expired. */
+REPROC_EXPORT extern const int REPROC_ETIMEDOUT;
+/*! The child process closed one of its streams (and in the case of
+stdout/stderr all of the data remaining in that stream has been read). */
+REPROC_EXPORT extern const int REPROC_EPIPE;
+/*! A memory allocation failed. */
+REPROC_EXPORT extern const int REPROC_ENOMEM;
+/*! A call to `reproc_read` or `reproc_write` would have blocked. */
+REPROC_EXPORT extern const int REPROC_EWOULDBLOCK;
+
+/*! Signal exit status constants. */
+
+REPROC_EXPORT extern const int REPROC_SIGKILL;
+REPROC_EXPORT extern const int REPROC_SIGTERM;
+
+/*! Tells a function that takes a timeout value to wait indefinitely. */
+REPROC_EXPORT extern const int REPROC_INFINITE;
+/*! Tells `reproc_wait` to wait until the deadline passed to `reproc_start`
+expires. */
+REPROC_EXPORT extern const int REPROC_DEADLINE;
+
+/*! Stream identifiers used to indicate which stream to act on. */
+typedef enum {
+  /*! stdin */
+  REPROC_STREAM_IN,
+  /*! stdout */
+  REPROC_STREAM_OUT,
+  /*! stderr */
+  REPROC_STREAM_ERR,
+} REPROC_STREAM;
+
+/*! Used to tell reproc where to redirect the streams of the child process. */
+typedef enum {
+  /*! Use the default redirect behavior, see the documentation for `redirect` in
+  `reproc_options`. */
+  REPROC_REDIRECT_DEFAULT,
+  /*! Redirect to a pipe. */
+  REPROC_REDIRECT_PIPE,
+  /*! Redirect to the corresponding stream from the parent process. */
+  REPROC_REDIRECT_PARENT,
+  /*! Redirect to /dev/null (or NUL on Windows). */
+  REPROC_REDIRECT_DISCARD,
+  /*! Redirect to child process stdout. Only valid for stderr. */
+  REPROC_REDIRECT_STDOUT,
+  /*! Redirect to a handle (fd on Linux, HANDLE/SOCKET on Windows). */
+  REPROC_REDIRECT_HANDLE,
+  /*! Redirect to a `FILE *`. */
+  REPROC_REDIRECT_FILE,
+  /*! Redirect to a specific path. */
+  REPROC_REDIRECT_PATH,
+} REPROC_REDIRECT;
+
+/*! Used to tell `reproc_stop` how to stop a child process. */
+typedef enum {
+  /*! noop (no operation) */
+  REPROC_STOP_NOOP,
+  /*! `reproc_wait` */
+  REPROC_STOP_WAIT,
+  /*! `reproc_terminate` */
+  REPROC_STOP_TERMINATE,
+  /*! `reproc_kill` */
+  REPROC_STOP_KILL,
+} REPROC_STOP;
+
+typedef struct reproc_stop_action {
+  REPROC_STOP action;
+  int timeout;
+} reproc_stop_action;
+
+typedef struct reproc_stop_actions {
+  reproc_stop_action first;
+  reproc_stop_action second;
+  reproc_stop_action third;
+} reproc_stop_actions;
+
+// clang-format off
+
+#define REPROC_STOP_ACTIONS_NULL (reproc_stop_actions) {                       \
+  { REPROC_STOP_NOOP, 0 },                                                     \
+  { REPROC_STOP_NOOP, 0 },                                                     \
+  { REPROC_STOP_NOOP, 0 },                                                     \
+}
+
+// clang-format on
+
+#if defined(_WIN32)
+typedef void *reproc_handle; // `HANDLE`
+#else
+typedef int reproc_handle; // fd
+#endif
+
+typedef struct reproc_redirect {
+  /*! Type of redirection. */
+  REPROC_REDIRECT type;
+  /*!
+  Redirect a stream to an operating system handle. The given handle must be in
+  blocking mode ( `O_NONBLOCK` and `OVERLAPPED` handles are not supported).
+
+  Note that reproc does not take ownership of the handle. The user is
+  responsible for closing the handle after passing it to `reproc_start`. Since
+  the operating system will copy the handle to the child process, the handle
+  can be closed immediately after calling `reproc_start` if the handle is not
+  needed in the parent process anymore.
+
+  If `handle` is set, `type` must be unset or set to `REPROC_REDIRECT_HANDLE`
+  and `file`, `path` must be unset.
+  */
+  reproc_handle handle;
+  /*!
+  Redirect a stream to a file stream.
+
+  Note that reproc does not take ownership of the file. The user is
+  responsible for closing the file after passing it to `reproc_start`. Just
+  like with `handles`, the operating system will copy the file handle to the
+  child process so the file can be closed immediately after calling
+  `reproc_start` if it isn't needed anymore by the parent process.
+
+  Any file passed to `file.in` must have been opened in read mode. Likewise,
+  any files passed to `file.out` or `file.err` must have been opened in write
+  mode.
+
+  If `file` is set, `type` must be unset or set to `REPROC_REDIRECT_FILE` and
+  `handle`, `path` must be unset.
+  */
+  FILE *file;
+  /*!
+  Redirect a stream to a given path.
+
+  reproc will create or open the file at the given path. Depending on the
+  stream, the file is opened in read or write mode.
+
+  If `path` is set, `type` must be unset or set to `REPROC_REDIRECT_PATH` and
+  `handle`, `file` must be unset.
+  */
+  const char *path;
+} reproc_redirect;
+
+typedef enum {
+  REPROC_ENV_EXTEND,
+  REPROC_ENV_EMPTY,
+} REPROC_ENV;
+
+typedef struct reproc_options {
+  /*!
+  `working_directory` specifies the working directory for the child process. If
+  `working_directory` is `NULL`, the child process runs in the working directory
+  of the parent process.
+  */
+  const char *working_directory;
+
+  struct {
+    /*!
+    `behavior` specifies whether the child process should start with a copy of
+    the parent process environment variables or an empty environment. By
+    default, the child process starts with a copy of the parent's environment
+    variables (`REPROC_ENV_EXTEND`). If `behavior` is set to `REPROC_ENV_EMPTY`,
+    the child process starts with an empty environment.
+    */
+    REPROC_ENV behavior;
+    /*!
+    `extra` is an array of UTF-8 encoded, NUL-terminated strings that specifies
+    extra environment variables for the child process. It has the following
+    layout:
+
+    - All elements except the final element must be of the format `NAME=VALUE`.
+    - The final element must be `NULL`.
+
+    Example: ["IP=127.0.0.1", "PORT=8080", `NULL`]
+
+    If `env` is `NULL`, no extra environment variables are added to the
+    environment of the child process.
+    */
+    const char *const *extra;
+  } env;
+  /*!
+  `redirect` specifies where to redirect the streams from the child process.
+
+  By default each stream is redirected to a pipe which can be written to (stdin)
+  or read from (stdout/stderr) using `reproc_write` and `reproc_read`
+  respectively.
+  */
+  struct {
+    /*!
+    `in`, `out` and `err` specify where to redirect the standard I/O streams of
+    the child process. When not set, `in` and `out` default to
+    `REPROC_REDIRECT_PIPE` while `err` defaults to `REPROC_REDIRECT_PARENT`.
+    */
+    reproc_redirect in;
+    reproc_redirect out;
+    reproc_redirect err;
+    /*!
+    Use `REPROC_REDIRECT_PARENT` instead of `REPROC_REDIRECT_PIPE` when `type`
+    is unset.
+
+    When this option is set, `discard`, `file` and `path` must be unset.
+    */
+    bool parent;
+    /*!
+    Use `REPROC_REDIRECT_DISCARD` instead of `REPROC_REDIRECT_PIPE` when `type`
+    is unset.
+
+    When this option is set, `parent`, `file` and `path` must be unset.
+    */
+    bool discard;
+    /*!
+    Shorthand for redirecting stdout and stderr to the same file.
+
+    If this option is set, `out`, `err`, `parent`, `discard` and `path` must be
+    unset.
+    */
+    FILE *file;
+    /*!
+    Shorthand for redirecting stdout and stderr to the same path.
+
+    If this option is set, `out`, `err`, `parent`, `discard` and `file` must be
+    unset.
+    */
+    const char *path;
+  } redirect;
+  /*!
+  Stop actions that are passed to `reproc_stop` in `reproc_destroy` to stop the
+  child process. See `reproc_stop` for more information on how `stop` is
+  interpreted.
+  */
+  reproc_stop_actions stop;
+  /*!
+  Maximum allowed duration in milliseconds the process is allowed to run in
+  milliseconds. If the deadline is exceeded, Any ongoing and future calls to
+  `reproc_poll` return `REPROC_ETIMEDOUT`.
+
+  Note that only `reproc_poll` takes the deadline into account. More
+  specifically, if the `nonblocking` option is not enabled, `reproc_read` and
+  `reproc_write` can deadlock waiting on the child process to perform I/O. If
+  this is a problem, enable the `nonblocking` option and use `reproc_poll`
+  together with a deadline/timeout to avoid any deadlocks.
+
+  If `REPROC_DEADLINE` is passed as the timeout to `reproc_wait`, it waits until
+  the deadline expires.
+
+  When `deadline` is zero, no deadline is set for the process.
+  */
+  int deadline;
+  /*!
+  `input` is written to the stdin pipe before the child process is started.
+
+  Because `input` is written to the stdin pipe before the process starts,
+  `input.size` must be smaller than the system's default pipe size (64KB).
+
+  If `input` is set, the stdin pipe is closed after `input` is written to it.
+
+  If `redirect.in` is set, this option may not be set.
+  */
+  struct {
+    const uint8_t *data;
+    size_t size;
+  } input;
+  /*!
+  This option can only be used on POSIX systems. If enabled on Windows, an error
+  will be returned.
+
+  If `fork` is enabled, `reproc_start` forks a child process and returns 0 in
+  the child process and > 0 in the parent process. In the child process, only
+  `reproc_destroy` may be called on the `reproc_t` instance to free its
+  associated memory.
+
+  When `fork` is enabled. `argv` must be `NULL` when calling `reproc_start`.
+  */
+  bool fork;
+  /*!
+  Put pipes created by reproc in nonblocking mode. This makes `reproc_read` and
+  `reproc_write` nonblocking operations. If needed, use `reproc_poll` to wait
+  until streams becomes readable/writable.
+  */
+  bool nonblocking;
+} reproc_options;
+
+enum {
+  /*! Data can be written to stdin. */
+  REPROC_EVENT_IN = 1 << 0,
+  /*! Data can be read from stdout. */
+  REPROC_EVENT_OUT = 1 << 1,
+  /*! Data can be read from stderr. */
+  REPROC_EVENT_ERR = 1 << 2,
+  /*! The process finished running. */
+  REPROC_EVENT_EXIT = 1 << 3,
+  /*! The deadline of the process expired. This event is added by default to the
+  list of interested events. */
+  REPROC_EVENT_DEADLINE = 1 << 4,
+};
+
+typedef struct reproc_event_source {
+  /*! Process to poll for events. */
+  reproc_t *process;
+  /*! Events of the process that we're interested in. Takes a combo of
+  `REPROC_EVENT` flags. */
+  int interests;
+  /*! Combo of `REPROC_EVENT` flags that indicate the events that occurred. This
+  field is filled in by `reproc_poll`. */
+  int events;
+} reproc_event_source;
+
+/*! Allocate a new `reproc_t` instance on the heap. */
+REPROC_EXPORT reproc_t *reproc_new(void);
+
+/*!
+Starts the process specified by `argv` in the given working directory and
+redirects its input, output and error streams.
+
+If this function does not return an error the child process will have started
+running and can be inspected using the operating system's tools for process
+inspection (e.g. ps on Linux).
+
+Every successful call to this function should be followed by a successful call
+to `reproc_wait` or `reproc_stop` and a call to `reproc_destroy`. If an error
+occurs during `reproc_start` all allocated resources are cleaned up before
+`reproc_start` returns and no further action is required.
+
+`argv` is an array of UTF-8 encoded, NUL-terminated strings that specifies the
+program to execute along with its arguments. It has the following layout:
+
+- The first element indicates the executable to run as a child process. This can
+be an absolute path, a path relative to the working directory of the parent
+process or the name of an executable located in the PATH. It cannot be `NULL`.
+- The following elements indicate the whitespace delimited arguments passed to
+the executable. None of these elements can be `NULL`.
+- The final element must be `NULL`.
+
+Example: ["cmake", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", `NULL`]
+*/
+REPROC_EXPORT int reproc_start(reproc_t *process,
+                               const char *const *argv,
+                               reproc_options options);
+
+/*!
+Returns the process ID of the child or `REPROC_EINVAL` on error.
+
+Note that if `reproc_wait` has been called successfully on this process already,
+the returned pid will be that of the just ended child process. The operating
+system will have cleaned up the resources allocated to the process
+and the operating system is free to reuse the same pid for another process.
+
+Generally, only pass the result of this function to system calls that need a
+valid pid if `reproc_wait` hasn't been called successfully on the process yet.
+*/
+REPROC_EXPORT int reproc_pid(reproc_t *process);
+
+/*!
+Polls each process in `sources` for its corresponding events in `interests` and
+stores events that occurred for each process in `events`. If an event source
+process member is `NULL`, the event source is ignored.
+
+Pass `REPROC_INFINITE` to `timeout` to have `reproc_poll` wait forever for an
+event to occur.
+
+If one or more events occur, returns the number of processes with events. If the
+timeout expires, returns zero. Returns `REPROC_EPIPE` if none of the sources
+have valid pipes remaining that can be polled.
+
+Actionable errors:
+- `REPROC_EPIPE`
+*/
+REPROC_EXPORT int
+reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout);
+
+/*!
+Reads up to `size` bytes into `buffer` from the child process output stream
+indicated by `stream`.
+
+Actionable errors:
+- `REPROC_EPIPE`
+- `REPROC_EWOULDBLOCK`
+*/
+REPROC_EXPORT int reproc_read(reproc_t *process,
+                              REPROC_STREAM stream,
+                              uint8_t *buffer,
+                              size_t size);
+
+/*!
+Writes up to `size` bytes from `buffer` to the standard input (stdin) of the
+child process.
+
+(POSIX) By default, writing to a closed stdin pipe terminates the parent process
+with the `SIGPIPE` signal. `reproc_write` will only return `REPROC_EPIPE` if
+this signal is ignored by the parent process.
+
+Returns the amount of bytes written. If `buffer` is `NULL` and `size` is zero,
+this function returns 0.
+
+If the standard input of the child process wasn't opened with
+`REPROC_REDIRECT_PIPE`, this function returns `REPROC_EPIPE` unless `buffer` is
+`NULL` and `size` is zero.
+
+Actionable errors:
+- `REPROC_EPIPE`
+- `REPROC_EWOULDBLOCK`
+*/
+REPROC_EXPORT int
+reproc_write(reproc_t *process, const uint8_t *buffer, size_t size);
+
+/*!
+Closes the child process standard stream indicated by `stream`.
+
+This function is necessary when a child process reads from stdin until it is
+closed. After writing all the input to the child process using `reproc_write`,
+the standard input stream can be closed using this function.
+*/
+REPROC_EXPORT int reproc_close(reproc_t *process, REPROC_STREAM stream);
+
+/*!
+Waits `timeout` milliseconds for the child process to exit. If the child process
+has already exited or exits within the given timeout, its exit status is
+returned.
+
+If `timeout` is 0, the function will only check if the child process is still
+running without waiting. If `timeout` is `REPROC_INFINITE`, this function will
+wait indefinitely for the child process to exit. If `timeout` is
+`REPROC_DEADLINE`, this function waits until the deadline passed to
+`reproc_start` expires.
+
+Actionable errors:
+- `REPROC_ETIMEDOUT`
+*/
+REPROC_EXPORT int reproc_wait(reproc_t *process, int timeout);
+
+/*!
+Sends the `SIGTERM` signal (POSIX) or the `CTRL-BREAK` signal (Windows) to the
+child process. Remember that successful calls to `reproc_wait` and
+`reproc_destroy` are required to make sure the child process is completely
+cleaned up.
+*/
+REPROC_EXPORT int reproc_terminate(reproc_t *process);
+
+/*!
+Sends the `SIGKILL` signal to the child process (POSIX) or calls
+`TerminateProcess` (Windows) on the child process. Remember that successful
+calls to `reproc_wait` and `reproc_destroy` are required to make sure the child
+process is completely cleaned up.
+*/
+REPROC_EXPORT int reproc_kill(reproc_t *process);
+
+/*!
+Simplifies calling combinations of `reproc_wait`, `reproc_terminate` and
+`reproc_kill`. The function executes each specified step and waits (using
+`reproc_wait`) until the corresponding timeout expires before continuing with
+the next step.
+
+Example:
+
+Wait 10 seconds for the child process to exit on its own before sending
+`SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waiting five more seconds for
+the child process to exit.
+
+```c
+REPROC_ERROR error = reproc_stop(process,
+                                 REPROC_STOP_WAIT, 10000,
+                                 REPROC_STOP_TERMINATE, 5000,
+                                 REPROC_STOP_NOOP, 0);
+```
+
+Call `reproc_wait`, `reproc_terminate` and `reproc_kill` directly if you need
+extra logic such as logging between calls.
+
+`stop` can contain up to three stop actions that instruct this function how the
+child process should be stopped. The first element of each stop action specifies
+which action should be called on the child process. The second element of each
+stop actions specifies how long to wait after executing the operation indicated
+by the first element.
+
+When `stop` is 3x `REPROC_STOP_NOOP`, `reproc_destroy` will wait until the
+deadline expires (or forever if there is no deadline). If the process is still
+running after the deadline expires, `reproc_stop` then calls `reproc_terminate`
+and waits forever for the process to exit.
+
+Note that when a stop action specifies `REPROC_STOP_WAIT`, the function will
+just wait for the specified timeout instead of performing an action to stop the
+child process.
+
+If the child process has already exited or exits during the execution of this
+function, its exit status is returned.
+
+Actionable errors:
+- `REPROC_ETIMEDOUT`
+*/
+REPROC_EXPORT int reproc_stop(reproc_t *process, reproc_stop_actions stop);
+
+/*!
+Release all resources associated with `process` including the memory allocated
+by `reproc_new`. Calling this function before a succesfull call to `reproc_wait`
+can result in resource leaks.
+
+Does nothing if `process` is an invalid `reproc_t` instance and always returns
+an invalid `reproc_t` instance (`NULL`). By assigning the result of
+`reproc_destroy` to the instance being destroyed, it can be safely called
+multiple times on the same instance.
+
+Example: `process = reproc_destroy(process)`.
+*/
+REPROC_EXPORT reproc_t *reproc_destroy(reproc_t *process);
+
+/*!
+Returns a string describing `error`. This string must not be modified by the
+caller.
+*/
+REPROC_EXPORT const char *reproc_strerror(int error);
+
+#ifdef __cplusplus
+}
+#endif

+ 27 - 0
ThirdParty/Reproc/reproc/include/reproc/run.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include <reproc/drain.h>
+#include <reproc/reproc.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*! Sets `options.redirect.parent = true` unless `discard` is set and calls
+`reproc_run_ex` with `REPROC_SINK_NULL` for the `out` and `err` sinks. */
+REPROC_EXPORT int reproc_run(const char *const *argv, reproc_options options);
+
+/*!
+Wrapper function that starts a process with the given arguments, drain its
+output and waits until it exits. Have a look at its (trivial) implementation and
+the documentation of the functions it calls to see exactly what it does:
+https://github.com/DaanDeMeyer/reproc/blob/master/reproc/src/run.c
+*/
+REPROC_EXPORT int reproc_run_ex(const char *const *argv,
+                                reproc_options options,
+                                reproc_sink out,
+                                reproc_sink err);
+
+#ifdef __cplusplus
+}
+#endif

+ 12 - 0
ThirdParty/Reproc/reproc/reproc-config.cmake.in

@@ -0,0 +1,12 @@
+@PACKAGE_INIT@
+
+set(REPROC_MULTITHREADED @REPROC_MULTITHREADED@)
+
+include(CMakeFindDependencyMacro)
+
+if(REPROC_MULTITHREADED)
+  set(THREADS_PREFER_PTHREAD_FLAG ON)
+  find_dependency(Threads)
+endif()
+
+include(${CMAKE_CURRENT_LIST_DIR}/@[email protected])

+ 12 - 0
ThirdParty/Reproc/reproc/reproc.pc.in

@@ -0,0 +1,12 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=${prefix}
+includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
+libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@
+
+Name: @TARGET@
+Description: @PROJECT_DESCRIPTION@
+URL: @PROJECT_HOMEPAGE_URL@
+Version: @PROJECT_VERSION@
+Cflags: -I${includedir}
+Libs: -L${libdir} -l@TARGET@
+Libs.private: @REPROC_THREAD_LIBRARY@ @REPROC_WINSOCK_LIBRARY@ @REPROC_RT_LIBRARY@

+ 10 - 0
ThirdParty/Reproc/reproc/resources/argv.c

@@ -0,0 +1,10 @@
+#include <stdio.h>
+
+int main(int argc, const char **argv)
+{
+  for (int i = 0; i < argc; i++) {
+    printf("%s\n", argv[i]);
+  }
+
+  return 0;
+}

+ 7 - 0
ThirdParty/Reproc/reproc/resources/deadline.c

@@ -0,0 +1,7 @@
+#include "sleep.h"
+
+int main(void)
+{
+  millisleep(25000);
+  return 0;
+}

+ 13 - 0
ThirdParty/Reproc/reproc/resources/env.c

@@ -0,0 +1,13 @@
+#include <stdio.h>
+
+int main(int argc, const char **argv, const char **envp)
+{
+  (void) argc;
+  (void) argv;
+
+  for (size_t i = 0; envp[i] != NULL; i++) {
+    printf("%s\n", envp[i]);
+  }
+
+  return 0;
+}

+ 15 - 0
ThirdParty/Reproc/reproc/resources/io.c

@@ -0,0 +1,15 @@
+#include <stdio.h>
+
+int main(void)
+{
+  char input[8096];
+
+  if (fgets(input, sizeof(input), stdin) == NULL) {
+    return 1;
+  }
+
+  fprintf(stdout, "%s", input);
+  fprintf(stderr, "%s", input);
+
+  return 0;
+}

+ 14 - 0
ThirdParty/Reproc/reproc/resources/overflow.c

@@ -0,0 +1,14 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+int main()
+{
+  char buffer[8192];
+
+  for (int i = 0; i < 200; i++) {
+    FILE *stream = rand() % 2 ? stdout : stderr; // NOLINT
+    fprintf(stream, "%s", buffer);
+  }
+
+  return 0;
+}

+ 10 - 0
ThirdParty/Reproc/reproc/resources/path.c

@@ -0,0 +1,10 @@
+#include <stdio.h>
+
+int main(int argc, const char **argv)
+{
+  (void) argc;
+
+  printf("%s", argv[0]);
+
+  return 0;
+}

+ 15 - 0
ThirdParty/Reproc/reproc/resources/pid.c

@@ -0,0 +1,15 @@
+#include <stdio.h>
+
+#ifdef _WIN32
+  #include <Windows.h>
+  #define getpid (int) GetCurrentProcessId
+#else
+  #include <unistd.h>
+#endif
+
+int main(void)
+{
+  printf("%d", getpid());
+
+  return 0;
+}

+ 18 - 0
ThirdParty/Reproc/reproc/resources/sleep.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#ifdef _WIN32
+  #include <windows.h>
+static inline void millisleep(long ms)
+{
+  Sleep((DWORD) ms);
+}
+#else
+  #define _POSIX_C_SOURCE 200809L
+  #include <time.h>
+static inline void millisleep(long ms)
+{
+  nanosleep(&(struct timespec){ .tv_sec = (ms) / 1000,
+                                .tv_nsec = ((ms) % 1000L) * 1000000 },
+            NULL);
+}
+#endif

+ 7 - 0
ThirdParty/Reproc/reproc/resources/stop.c

@@ -0,0 +1,7 @@
+#include "sleep.h"
+
+int main(void)
+{
+  millisleep(25000);
+  return 0;
+}

+ 21 - 0
ThirdParty/Reproc/reproc/resources/working-directory.c

@@ -0,0 +1,21 @@
+#include <stdio.h>
+
+#if defined(_WIN32)
+  #include <direct.h>
+  #define getcwd _getcwd
+#else
+  #include <unistd.h>
+#endif
+
+int main()
+{
+  char working_directory[8096];
+
+  if (getcwd(working_directory, sizeof(working_directory)) == NULL) {
+    return 1;
+  }
+
+  printf("%s", working_directory);
+
+  return 0;
+}

+ 5 - 0
ThirdParty/Reproc/reproc/src/clock.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include <stdint.h>
+
+int64_t now(void);

+ 17 - 0
ThirdParty/Reproc/reproc/src/clock.posix.c

@@ -0,0 +1,17 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "clock.h"
+
+#include <time.h>
+
+#include "error.h"
+
+int64_t now(void)
+{
+  struct timespec timespec = { 0 };
+
+  int r = clock_gettime(CLOCK_REALTIME, &timespec);
+  ASSERT_UNUSED(r == 0);
+
+  return timespec.tv_sec * 1000 + timespec.tv_nsec / 1000000;
+}

+ 14 - 0
ThirdParty/Reproc/reproc/src/clock.windows.c

@@ -0,0 +1,14 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "clock.h"
+
+#include <windows.h>
+
+int64_t now(void)
+{
+  return (int64_t) GetTickCount64();
+}

+ 121 - 0
ThirdParty/Reproc/reproc/src/drain.c

@@ -0,0 +1,121 @@
+#include <reproc/drain.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "error.h"
+#include "macro.h"
+
+int reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(out.function);
+  ASSERT_EINVAL(err.function);
+
+  const uint8_t initial = 0;
+  int r = -1;
+
+  // A single call to `read` might contain multiple messages. By always calling
+  // both sinks once with no data before reading, we give them the chance to
+  // process all previous output one by one before reading from the child
+  // process again.
+
+  r = out.function(REPROC_STREAM_IN, &initial, 0, out.context);
+  if (r != 0) {
+    return r;
+  }
+
+  r = err.function(REPROC_STREAM_IN, &initial, 0, err.context);
+  if (r != 0) {
+    return r;
+  }
+
+  uint8_t buffer[4096];
+
+  for (;;) {
+    reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR,
+                                   0 };
+
+    r = reproc_poll(&source, 1, REPROC_INFINITE);
+    if (r < 0) {
+      r = r == REPROC_EPIPE ? 0 : r;
+      break;
+    }
+
+    if (source.events & REPROC_EVENT_DEADLINE) {
+      r = REPROC_ETIMEDOUT;
+      break;
+    }
+
+    REPROC_STREAM stream = source.events & REPROC_EVENT_OUT ? REPROC_STREAM_OUT
+                                                            : REPROC_STREAM_ERR;
+
+    r = reproc_read(process, stream, buffer, ARRAY_SIZE(buffer));
+    if (r < 0 && r != REPROC_EPIPE) {
+      break;
+    }
+
+    size_t bytes_read = r == REPROC_EPIPE ? 0 : (size_t) r;
+    reproc_sink sink = stream == REPROC_STREAM_OUT ? out : err;
+
+    r = sink.function(stream, buffer, bytes_read, sink.context);
+    if (r != 0) {
+      break;
+    }
+  }
+
+  return r;
+}
+
+static int sink_string(REPROC_STREAM stream,
+                       const uint8_t *buffer,
+                       size_t size,
+                       void *context)
+{
+  (void) stream;
+
+  char **string = (char **) context;
+  size_t string_size = *string == NULL ? 0 : strlen(*string);
+
+  char *r = (char *) realloc(*string, string_size + size + 1);
+  if (r == NULL) {
+    return REPROC_ENOMEM;
+  }
+
+  *string = r;
+  memcpy(*string + string_size, buffer, size);
+  (*string)[string_size + size] = '\0';
+
+  return 0;
+}
+
+reproc_sink reproc_sink_string(char **output)
+{
+  return (reproc_sink){ sink_string, output };
+}
+
+static int sink_discard(REPROC_STREAM stream,
+                        const uint8_t *buffer,
+                        size_t size,
+                        void *context)
+{
+  (void) stream;
+  (void) buffer;
+  (void) size;
+  (void) context;
+
+  return 0;
+}
+
+reproc_sink reproc_sink_discard(void)
+{
+  return (reproc_sink){ sink_discard, NULL };
+}
+
+const reproc_sink REPROC_SINK_NULL = { sink_discard, NULL };
+
+void *reproc_free(void *ptr)
+{
+  free(ptr);
+  return NULL;
+}

+ 25 - 0
ThirdParty/Reproc/reproc/src/error.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <assert.h>
+
+#define ASSERT(expression) assert(expression)
+
+// Avoid unused assignment warnings in release mode when the result of an
+// assignment is only used in an assert statement.
+#define ASSERT_UNUSED(expression)                                              \
+  do {                                                                         \
+    (void) !(expression);                                                      \
+    ASSERT((expression));                                                      \
+  } while (0)
+
+// Returns `r` if `expression` is false.
+#define ASSERT_RETURN(expression, r)                                           \
+  do {                                                                         \
+    if (!(expression)) {                                                       \
+      return (r);                                                              \
+    }                                                                          \
+  } while (0)
+
+#define ASSERT_EINVAL(expression) ASSERT_RETURN(expression, REPROC_EINVAL)
+
+const char *error_string(int error);

+ 31 - 0
ThirdParty/Reproc/reproc/src/error.posix.c

@@ -0,0 +1,31 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "error.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <reproc/reproc.h>
+
+#include "macro.h"
+
+const int REPROC_EINVAL = -EINVAL;
+const int REPROC_EPIPE = -EPIPE;
+const int REPROC_ETIMEDOUT = -ETIMEDOUT;
+const int REPROC_ENOMEM = -ENOMEM;
+const int REPROC_EWOULDBLOCK = -EWOULDBLOCK;
+
+enum { ERROR_STRING_MAX_SIZE = 512 };
+
+const char *error_string(int error)
+{
+  static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE];
+
+  int r = strerror_r(abs(error), string, ARRAY_SIZE(string));
+  if (r != 0) {
+    return "Failed to retrieve error string";
+  }
+
+  return string;
+}

+ 62 - 0
ThirdParty/Reproc/reproc/src/error.windows.c

@@ -0,0 +1,62 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "error.h"
+
+#include <limits.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <winsock2.h>
+
+#include <reproc/reproc.h>
+
+#include "macro.h"
+
+const int REPROC_EINVAL = -ERROR_INVALID_PARAMETER;
+const int REPROC_EPIPE = -ERROR_BROKEN_PIPE;
+const int REPROC_ETIMEDOUT = -WAIT_TIMEOUT;
+const int REPROC_ENOMEM = -ERROR_NOT_ENOUGH_MEMORY;
+const int REPROC_EWOULDBLOCK = -WSAEWOULDBLOCK;
+
+enum { ERROR_STRING_MAX_SIZE = 512 };
+
+const char *error_string(int error)
+{
+  wchar_t *wstring = NULL;
+  int r = -1;
+
+  wstring = malloc(sizeof(wchar_t) * ERROR_STRING_MAX_SIZE);
+  if (wstring == NULL) {
+    return "Failed to allocate memory for error string";
+  }
+
+  // We don't expect message sizes larger than the maximum possible int.
+  r = (int) FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM |
+                               FORMAT_MESSAGE_IGNORE_INSERTS,
+                           NULL, (DWORD) abs(error),
+                           MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), wstring,
+                           ERROR_STRING_MAX_SIZE, NULL);
+  if (r == 0) {
+    free(wstring);
+    return "Failed to retrieve error string";
+  }
+
+  static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE];
+
+  r = WideCharToMultiByte(CP_UTF8, 0, wstring, -1, string, ARRAY_SIZE(string),
+                          NULL, NULL);
+  free(wstring);
+  if (r == 0) {
+    return "Failed to convert error string to UTF-8";
+  }
+
+  // Remove trailing whitespace and period.
+  if (r >= 4) {
+    string[r - 4] = '\0';
+  }
+
+  return string;
+}

+ 20 - 0
ThirdParty/Reproc/reproc/src/handle.h

@@ -0,0 +1,20 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#if defined(_WIN32)
+typedef void *handle_type; // `HANDLE`
+#else
+typedef int handle_type; // fd
+#endif
+
+extern const handle_type HANDLE_INVALID;
+
+// Sets the `FD_CLOEXEC` flag on the file descriptor. POSIX only.
+int handle_cloexec(handle_type handle, bool enable);
+
+// Closes `handle` if it is not an invalid handle and returns an invalid handle.
+// Does not overwrite the last system error if an error occurs while closing
+// `handle`.
+handle_type handle_destroy(handle_type handle);

+ 42 - 0
ThirdParty/Reproc/reproc/src/handle.posix.c

@@ -0,0 +1,42 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "handle.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "error.h"
+
+const int HANDLE_INVALID = -1;
+
+int handle_cloexec(int handle, bool enable)
+{
+  int r = -1;
+
+  r = fcntl(handle, F_GETFD, 0);
+  if (r < 0) {
+    return -errno;
+  }
+
+  r = enable ? r | FD_CLOEXEC : r & ~FD_CLOEXEC;
+
+  r = fcntl(handle, F_SETFD, r);
+  if (r < 0) {
+    return -errno;
+  }
+
+  return 0;
+}
+
+int handle_destroy(int handle)
+{
+  if (handle == HANDLE_INVALID) {
+    return HANDLE_INVALID;
+  }
+
+  int r = close(handle);
+  ASSERT_UNUSED(r == 0);
+
+  return HANDLE_INVALID;
+}

+ 27 - 0
ThirdParty/Reproc/reproc/src/handle.windows.c

@@ -0,0 +1,27 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "handle.h"
+
+#include <windows.h>
+
+#include "error.h"
+
+const HANDLE HANDLE_INVALID = INVALID_HANDLE_VALUE; // NOLINT
+
+// `handle_cloexec` is POSIX-only.
+
+HANDLE handle_destroy(HANDLE handle)
+{
+  if (handle == NULL || handle == HANDLE_INVALID) {
+    return HANDLE_INVALID;
+  }
+
+  int r = CloseHandle(handle);
+  ASSERT_UNUSED(r != 0);
+
+  return HANDLE_INVALID;
+}

+ 5 - 0
ThirdParty/Reproc/reproc/src/init.h

@@ -0,0 +1,5 @@
+#pragma once
+
+int init(void);
+
+void deinit(void);

+ 10 - 0
ThirdParty/Reproc/reproc/src/init.posix.c

@@ -0,0 +1,10 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "init.h"
+
+int init(void)
+{
+  return 0;
+}
+
+void deinit(void) {}

+ 28 - 0
ThirdParty/Reproc/reproc/src/init.windows.c

@@ -0,0 +1,28 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "init.h"
+
+#include <winsock2.h>
+
+#include "error.h"
+
+int init(void)
+{
+  WSADATA data;
+  int r = WSAStartup(MAKEWORD(2, 2), &data);
+  return -r;
+}
+
+void deinit(void)
+{
+  int saved = WSAGetLastError();
+
+  int r = WSACleanup();
+  ASSERT_UNUSED(r == 0);
+
+  WSASetLastError(saved);
+}

+ 11 - 0
ThirdParty/Reproc/reproc/src/macro.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+#define MIN(a, b) (a) < (b) ? (a) : (b)
+
+#if defined(_WIN32) && !defined(__MINGW32__)
+  #define THREAD_LOCAL __declspec(thread)
+#else
+  #define THREAD_LOCAL __thread
+#endif

+ 137 - 0
ThirdParty/Reproc/reproc/src/options.c

@@ -0,0 +1,137 @@
+#include "options.h"
+
+#include "error.h"
+
+static bool redirect_is_set(reproc_redirect redirect)
+{
+  return redirect.type || redirect.handle || redirect.file || redirect.path;
+}
+
+static int parse_redirect(reproc_redirect *redirect,
+                          REPROC_STREAM stream,
+                          bool parent,
+                          bool discard,
+                          FILE *file,
+                          const char *path)
+{
+  ASSERT(redirect);
+
+  if (file) {
+    ASSERT_EINVAL(!redirect_is_set(*redirect));
+    ASSERT_EINVAL(!parent && !discard && !path);
+    redirect->type = REPROC_REDIRECT_FILE;
+    redirect->file = file;
+  }
+
+  if (path) {
+    ASSERT_EINVAL(!redirect_is_set(*redirect));
+    ASSERT_EINVAL(!parent && !discard && !file);
+    redirect->type = REPROC_REDIRECT_PATH;
+    redirect->path = path;
+  }
+
+  if (redirect->type == REPROC_REDIRECT_HANDLE || redirect->handle) {
+    ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT ||
+                  redirect->type == REPROC_REDIRECT_HANDLE);
+    ASSERT_EINVAL(redirect->handle);
+    ASSERT_EINVAL(!redirect->file && !redirect->path);
+    redirect->type = REPROC_REDIRECT_HANDLE;
+  }
+
+  if (redirect->type == REPROC_REDIRECT_FILE || redirect->file) {
+    ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT ||
+                  redirect->type == REPROC_REDIRECT_FILE);
+    ASSERT_EINVAL(redirect->file);
+    ASSERT_EINVAL(!redirect->handle && !redirect->path);
+    redirect->type = REPROC_REDIRECT_FILE;
+  }
+
+  if (redirect->type == REPROC_REDIRECT_PATH || redirect->path) {
+    ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT ||
+                  redirect->type == REPROC_REDIRECT_PATH);
+    ASSERT_EINVAL(redirect->path);
+    ASSERT_EINVAL(!redirect->handle && !redirect->file);
+    redirect->type = REPROC_REDIRECT_PATH;
+  }
+
+  if (redirect->type == REPROC_REDIRECT_DEFAULT) {
+    if (parent) {
+      ASSERT_EINVAL(!discard);
+      redirect->type = REPROC_REDIRECT_PARENT;
+    } else if (discard) {
+      ASSERT_EINVAL(!parent);
+      redirect->type = REPROC_REDIRECT_DISCARD;
+    } else {
+      redirect->type = stream == REPROC_STREAM_ERR ? REPROC_REDIRECT_PARENT
+                                                   : REPROC_REDIRECT_PIPE;
+    }
+  }
+
+  return 0;
+}
+
+reproc_stop_actions parse_stop_actions(reproc_stop_actions stop)
+{
+  bool is_noop = stop.first.action == REPROC_STOP_NOOP &&
+                 stop.second.action == REPROC_STOP_NOOP &&
+                 stop.third.action == REPROC_STOP_NOOP;
+
+  if (is_noop) {
+    stop.first.action = REPROC_STOP_WAIT;
+    stop.first.timeout = REPROC_DEADLINE;
+    stop.second.action = REPROC_STOP_TERMINATE;
+    stop.second.timeout = REPROC_INFINITE;
+  }
+
+  return stop;
+}
+
+int parse_options(reproc_options *options, const char *const *argv)
+{
+  ASSERT(options);
+
+  int r = -1;
+
+  r = parse_redirect(&options->redirect.in, REPROC_STREAM_IN,
+                     options->redirect.parent, options->redirect.discard, NULL,
+                     NULL);
+  if (r < 0) {
+    return r;
+  }
+
+  r = parse_redirect(&options->redirect.out, REPROC_STREAM_OUT,
+                     options->redirect.parent, options->redirect.discard,
+                     options->redirect.file, options->redirect.path);
+  if (r < 0) {
+    return r;
+  }
+
+  r = parse_redirect(&options->redirect.err, REPROC_STREAM_ERR,
+                     options->redirect.parent, options->redirect.discard,
+                     options->redirect.file, options->redirect.path);
+  if (r < 0) {
+    return r;
+  }
+
+  if (options->input.data != NULL) {
+    ASSERT_EINVAL(options->redirect.in.type == REPROC_REDIRECT_PIPE);
+  }
+
+  if (options->input.size > 0) {
+    ASSERT_EINVAL(options->input.data != NULL);
+  }
+
+  if (options->fork) {
+    ASSERT_EINVAL(argv == NULL);
+  } else {
+    ASSERT_EINVAL(argv != NULL && argv[0] != NULL);
+  }
+
+  if (options->deadline == 0) {
+    options->deadline = REPROC_INFINITE;
+  }
+
+  options->stop = parse_stop_actions(options->stop);
+
+  return 0;
+}

+ 7 - 0
ThirdParty/Reproc/reproc/src/options.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include <reproc/reproc.h>
+
+reproc_stop_actions parse_stop_actions(reproc_stop_actions stop);
+
+int parse_options(reproc_options *options, const char *const *argv);

+ 46 - 0
ThirdParty/Reproc/reproc/src/pipe.h

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef _WIN64
+typedef uint64_t pipe_type; // `SOCKET`
+#elif _WIN32
+typedef uint32_t pipe_type; // `SOCKET`
+#else
+typedef int pipe_type; // fd
+#endif
+
+extern const pipe_type PIPE_INVALID;
+
+extern const short PIPE_EVENT_IN;
+extern const short PIPE_EVENT_OUT;
+
+typedef struct {
+  pipe_type pipe;
+  short interests;
+  short events;
+} pipe_event_source;
+
+// Creates a new anonymous pipe. `parent` and `child` are set to the parent and
+// child endpoint of the pipe respectively.
+int pipe_init(pipe_type *read, pipe_type *write);
+
+// Sets `pipe` to nonblocking mode.
+int pipe_nonblocking(pipe_type pipe, bool enable);
+
+// Reads up to `size` bytes into `buffer` from the pipe indicated by `pipe` and
+// returns the amount of bytes read.
+int pipe_read(pipe_type pipe, uint8_t *buffer, size_t size);
+
+// Writes up to `size` bytes from `buffer` to the pipe indicated by `pipe` and
+// returns the amount of bytes written.
+int pipe_write(pipe_type pipe, const uint8_t *buffer, size_t size);
+
+// Polls the given event sources for events.
+int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout);
+
+int pipe_shutdown(pipe_type pipe);
+
+pipe_type pipe_destroy(pipe_type pipe);

+ 141 - 0
ThirdParty/Reproc/reproc/src/pipe.posix.c

@@ -0,0 +1,141 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "pipe.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "error.h"
+#include "handle.h"
+
+const int PIPE_INVALID = -1;
+
+const short PIPE_EVENT_IN = POLLIN;
+const short PIPE_EVENT_OUT = POLLOUT;
+
+int pipe_init(int *read, int *write)
+{
+  ASSERT(read);
+  ASSERT(write);
+
+  int pair[] = { PIPE_INVALID, PIPE_INVALID };
+  int r = -1;
+
+  r = pipe(pair);
+  if (r < 0) {
+    r = -errno;
+    goto finish;
+  }
+
+  r = handle_cloexec(pair[0], true);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = handle_cloexec(pair[1], true);
+  if (r < 0) {
+    goto finish;
+  }
+
+  *read = pair[0];
+  *write = pair[1];
+
+  pair[0] = PIPE_INVALID;
+  pair[1] = PIPE_INVALID;
+
+finish:
+  pipe_destroy(pair[0]);
+  pipe_destroy(pair[1]);
+
+  return r;
+}
+
+int pipe_nonblocking(int pipe, bool enable)
+{
+  int r = -1;
+
+  r = fcntl(pipe, F_GETFL, 0);
+  if (r < 0) {
+    return -errno;
+  }
+
+  r = enable ? r | O_NONBLOCK : r & ~O_NONBLOCK;
+
+  r = fcntl(pipe, F_SETFL, r);
+
+  return r < 0 ? -errno : 0;
+}
+
+int pipe_read(int pipe, uint8_t *buffer, size_t size)
+{
+  ASSERT(pipe != PIPE_INVALID);
+  ASSERT(buffer);
+
+  int r = (int) read(pipe, buffer, size);
+
+  if (r == 0) {
+    // `read` returns 0 to indicate the other end of the pipe was closed.
+    return -EPIPE;
+  }
+
+  return r < 0 ? -errno : r;
+}
+
+int pipe_write(int pipe, const uint8_t *buffer, size_t size)
+{
+  ASSERT(pipe != PIPE_INVALID);
+  ASSERT(buffer);
+
+  int r = (int) write(pipe, buffer, size);
+
+  return r < 0 ? -errno : r;
+}
+
+int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout)
+{
+  ASSERT(num_sources <= INT_MAX);
+
+  struct pollfd *pollfds = NULL;
+  int r = -1;
+
+  pollfds = calloc(num_sources, sizeof(struct pollfd));
+  if (pollfds == NULL) {
+    r = -errno;
+    goto finish;
+  }
+
+  for (size_t i = 0; i < num_sources; i++) {
+    pollfds[i].fd = sources[i].pipe;
+    pollfds[i].events = sources[i].interests;
+  }
+
+  r = poll(pollfds, (nfds_t) num_sources, timeout);
+  if (r < 0) {
+    r = -errno;
+    goto finish;
+  }
+
+  for (size_t i = 0; i < num_sources; i++) {
+    sources[i].events = pollfds[i].revents;
+  }
+
+finish:
+  free(pollfds);
+
+  return r;
+}
+
+int pipe_shutdown(int pipe)
+{
+  (void) pipe;
+  return 0;
+}
+
+int pipe_destroy(int pipe)
+{
+  return handle_destroy(pipe);
+}

+ 265 - 0
ThirdParty/Reproc/reproc/src/pipe.windows.c

@@ -0,0 +1,265 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "pipe.h"
+
+#include <limits.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <winsock2.h>
+
+#include "error.h"
+#include "handle.h"
+#include "macro.h"
+
+const SOCKET PIPE_INVALID = INVALID_SOCKET;
+
+const short PIPE_EVENT_IN = POLLIN;
+const short PIPE_EVENT_OUT = POLLOUT;
+
+// Inspired by https://gist.github.com/geertj/4325783.
+static int socketpair(int domain, int type, int protocol, SOCKET *out)
+{
+  ASSERT(out);
+
+  SOCKET server = PIPE_INVALID;
+  SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID };
+  int r = -1;
+
+  server = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
+  if (server == INVALID_SOCKET) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  SOCKADDR_IN localhost = { 0 };
+  localhost.sin_family = AF_INET;
+  localhost.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);
+  localhost.sin_port = 0;
+
+  r = bind(server, (SOCKADDR *) &localhost, sizeof(localhost));
+  if (r < 0) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  r = listen(server, 1);
+  if (r < 0) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  SOCKADDR_STORAGE name = { 0 };
+  int size = sizeof(name);
+  r = getsockname(server, (SOCKADDR *) &name, &size);
+  if (r < 0) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  pair[0] = WSASocketW(domain, type, protocol, NULL, 0, 0);
+  if (pair[0] == INVALID_SOCKET) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  struct {
+    WSAPROTOCOL_INFOW data;
+    int size;
+  } info = { { 0 }, sizeof(WSAPROTOCOL_INFOW) };
+
+  r = getsockopt(pair[0], SOL_SOCKET, SO_PROTOCOL_INFOW, (char *) &info.data,
+                 &info.size);
+  if (r < 0) {
+    goto finish;
+  }
+
+  // We require the returned sockets to be usable as Windows file handles. This
+  // might not be the case if extra LSP providers are installed.
+
+  if (!(info.data.dwServiceFlags1 & XP1_IFS_HANDLES)) {
+    r = -ERROR_NOT_SUPPORTED;
+    goto finish;
+  }
+
+  r = pipe_nonblocking(pair[0], true);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = connect(pair[0], (SOCKADDR *) &name, size);
+  if (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  r = pipe_nonblocking(pair[0], false);
+  if (r < 0) {
+    goto finish;
+  }
+
+  pair[1] = accept(server, NULL, NULL);
+  if (pair[1] == INVALID_SOCKET) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  out[0] = pair[0];
+  out[1] = pair[1];
+
+  pair[0] = PIPE_INVALID;
+  pair[1] = PIPE_INVALID;
+
+finish:
+  pipe_destroy(server);
+  pipe_destroy(pair[0]);
+  pipe_destroy(pair[1]);
+
+  return r;
+}
+
+int pipe_init(SOCKET *read, SOCKET *write)
+{
+  ASSERT(read);
+  ASSERT(write);
+
+  SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID };
+  int r = -1;
+
+  // Use sockets instead of pipes so we can use `WSAPoll` which only works with
+  // sockets.
+  r = socketpair(AF_INET, SOCK_STREAM, 0, pair);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = SetHandleInformation((HANDLE) pair[0], HANDLE_FLAG_INHERIT, 0);
+  if (r == 0) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  r = SetHandleInformation((HANDLE) pair[1], HANDLE_FLAG_INHERIT, 0);
+  if (r == 0) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  // Make the connection unidirectional to better emulate a pipe.
+
+  r = shutdown(pair[0], SD_SEND);
+  if (r < 0) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  r = shutdown(pair[1], SD_RECEIVE);
+  if (r < 0) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  *read = pair[0];
+  *write = pair[1];
+
+  pair[0] = PIPE_INVALID;
+  pair[1] = PIPE_INVALID;
+
+finish:
+  pipe_destroy(pair[0]);
+  pipe_destroy(pair[1]);
+
+  return r;
+}
+
+int pipe_nonblocking(SOCKET pipe, bool enable)
+{
+  u_long mode = enable;
+  int r = ioctlsocket(pipe, (long) FIONBIO, &mode);
+  return r < 0 ? -WSAGetLastError() : 0;
+}
+
+int pipe_read(SOCKET pipe, uint8_t *buffer, size_t size)
+{
+  ASSERT(pipe != PIPE_INVALID);
+  ASSERT(buffer);
+  ASSERT(size <= INT_MAX);
+
+  int r = recv(pipe, (char *) buffer, (int) size, 0);
+
+  if (r == 0) {
+    return -ERROR_BROKEN_PIPE;
+  }
+
+  return r < 0 ? -WSAGetLastError() : r;
+}
+
+int pipe_write(SOCKET pipe, const uint8_t *buffer, size_t size)
+{
+  ASSERT(pipe != PIPE_INVALID);
+  ASSERT(buffer);
+  ASSERT(size <= INT_MAX);
+
+  int r = send(pipe, (const char *) buffer, (int) size, 0);
+
+  return r < 0 ? -WSAGetLastError() : r;
+}
+
+int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout)
+{
+  ASSERT(num_sources <= INT_MAX);
+
+  WSAPOLLFD *pollfds = NULL;
+  int r = -1;
+
+  pollfds = calloc(num_sources, sizeof(WSAPOLLFD));
+  if (pollfds == NULL) {
+    r = -ERROR_NOT_ENOUGH_MEMORY;
+    goto finish;
+  }
+
+  for (size_t i = 0; i < num_sources; i++) {
+    pollfds[i].fd = sources[i].pipe;
+    pollfds[i].events = sources[i].interests;
+  }
+
+  r = WSAPoll(pollfds, (ULONG) num_sources, timeout);
+  if (r < 0) {
+    r = -WSAGetLastError();
+    goto finish;
+  }
+
+  for (size_t i = 0; i < num_sources; i++) {
+    sources[i].events = pollfds[i].revents;
+  }
+
+finish:
+  free(pollfds);
+
+  return r;
+}
+
+int pipe_shutdown(SOCKET pipe)
+{
+  if (pipe == PIPE_INVALID) {
+    return 0;
+  }
+
+  int r = shutdown(pipe, SD_SEND);
+  return r < 0 ? -WSAGetLastError() : 0;
+}
+
+SOCKET pipe_destroy(SOCKET pipe)
+{
+  if (pipe == PIPE_INVALID) {
+    return PIPE_INVALID;
+  }
+
+  int r = closesocket(pipe);
+  ASSERT_UNUSED(r == 0);
+
+  return PIPE_INVALID;
+}

+ 66 - 0
ThirdParty/Reproc/reproc/src/process.h

@@ -0,0 +1,66 @@
+#pragma once
+
+#include "handle.h"
+
+#include <stdbool.h>
+
+#include <reproc/reproc.h>
+
+#if defined(_WIN32)
+typedef void *process_type; // `HANDLE`
+#else
+typedef int process_type; // `pid_t`
+#endif
+
+extern const process_type PROCESS_INVALID;
+
+struct process_options {
+  // If `NULL`, the child process inherits the environment of the current
+  // process.
+  struct {
+    REPROC_ENV behavior;
+    const char *const *extra;
+  } env;
+  // If not `NULL`, the working directory of the child process is set to
+  // `working_directory`.
+  const char *working_directory;
+  // The standard streams of the child process are redirected to the `in`, `out`
+  // and `err` handles. If a handle is `HANDLE_INVALID`, the corresponding child
+  // process standard stream is closed. The `exit` handle is simply inherited by
+  // the child process.
+  struct {
+    handle_type in;
+    handle_type out;
+    handle_type err;
+    handle_type exit;
+  } handle;
+};
+
+// Spawns a child process that executes the command stored in `argv`.
+//
+// If `argv` is `NULL` on POSIX, `exec` is not called after fork and this
+// function returns 0 in the child process and > 0 in the parent process. On
+// Windows, if `argv` is `NULL`, an error is returned.
+//
+// The process handle of the new child process is assigned to `process`.
+int process_start(process_type *process,
+                  const char *const *argv,
+                  struct process_options options);
+
+// Returns the process ID associated with the given handle. On posix systems the
+// handle is the process ID and so its returned directly. On WIN32 the process
+// ID is returned from GetProcessId on the pointer.
+int process_pid(process_type process);
+
+// Returns the process's exit status if it has finished running.
+int process_wait(process_type process);
+
+// Sends the `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) signal to the process
+// indicated by `process`.
+int process_terminate(process_type process);
+
+// Sends the `SIGKILL` signal to `process` (POSIX) or calls `TerminateProcess`
+// on `process` (Windows).
+int process_kill(process_type process);
+
+process_type process_destroy(process_type process);

+ 501 - 0
ThirdParty/Reproc/reproc/src/process.posix.c

@@ -0,0 +1,501 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "process.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "error.h"
+#include "macro.h"
+#include "pipe.h"
+#include "strv.h"
+
+#define CWD_BUF_SIZE_INCREMENT 4096
+
+const pid_t PROCESS_INVALID = -1;
+
+static int signal_mask(int how, const sigset_t *newmask, sigset_t *oldmask)
+{
+  int r = -1;
+
+#if defined(REPROC_MULTITHREADED)
+  // `pthread_sigmask` returns positive errno values so we negate them.
+  r = -pthread_sigmask(how, newmask, oldmask);
+#else
+  r = sigprocmask(how, newmask, oldmask);
+  r = r < 0 ? -errno : 0;
+#endif
+
+  return r;
+}
+
+// Returns true if the NUL-terminated string indicated by `path` is a relative
+// path. A path is relative if any character except the first is a forward slash
+// ('/').
+static bool path_is_relative(const char *path)
+{
+  return strlen(path) > 0 && path[0] != '/' && strchr(path + 1, '/') != NULL;
+}
+
+// Prepends the NUL-terminated string indicated by `path` with the current
+// working directory. The caller is responsible for freeing the result of this
+// function. If an error occurs, `NULL` is returned and `errno` is set to
+// indicate the error.
+static char *path_prepend_cwd(const char *path)
+{
+  ASSERT(path);
+
+  size_t path_size = strlen(path);
+  size_t cwd_size = CWD_BUF_SIZE_INCREMENT;
+
+  // We always allocate sufficient space for `path` but do not include this
+  // space in `cwd_size` so we can be sure that when `getcwd` succeeds there is
+  // sufficient space left in `cwd` to append `path`.
+
+  // +2 reserves space to add a NUL terminator and potentially a missing '/'
+  // after the current working directory.
+  char *cwd = calloc(cwd_size + path_size + 2, sizeof(char));
+  if (cwd == NULL) {
+    return cwd;
+  }
+
+  while (getcwd(cwd, cwd_size) == NULL) {
+    if (errno != ERANGE) {
+      free(cwd);
+      return NULL;
+    }
+
+    cwd_size += CWD_BUF_SIZE_INCREMENT;
+
+    char *result = realloc(cwd, cwd_size + path_size + 1);
+    if (result == NULL) {
+      free(cwd);
+      return result;
+    }
+
+    cwd = result;
+  }
+
+  cwd_size = strlen(cwd);
+
+  // Add a forward slash after `cwd` if there is none.
+  if (cwd[cwd_size - 1] != '/') {
+    cwd[cwd_size] = '/';
+    cwd[cwd_size + 1] = '\0';
+    cwd_size++;
+  }
+
+  // We've made sure there's sufficient space left in `cwd` to add `path` and a
+  // NUL terminator.
+  memcpy(cwd + cwd_size, path, path_size);
+  cwd[cwd_size + path_size] = '\0';
+
+  return cwd;
+}
+
+static const int MAX_FD_LIMIT = 1024 * 1024;
+
+static int get_max_fd(void)
+{
+  struct rlimit limit = { 0 };
+
+  int r = getrlimit(RLIMIT_NOFILE, &limit);
+  if (r < 0) {
+    return -errno;
+  }
+
+  rlim_t soft = limit.rlim_cur;
+
+  if (soft == RLIM_INFINITY || soft > INT_MAX) {
+    return INT_MAX;
+  }
+
+  return (int) (soft - 1);
+}
+
+static bool fd_in_set(int fd, const int *fd_set, size_t size)
+{
+  for (size_t i = 0; i < size; i++) {
+    if (fd == fd_set[i]) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+static pid_t process_fork(const int *except, size_t num_except)
+{
+  struct {
+    sigset_t old;
+    sigset_t new;
+  } mask;
+
+  int r = -1;
+
+  // We don't want signal handlers of the parent to run in the child process so
+  // we block all signals before forking.
+
+  r = sigfillset(&mask.new);
+  if (r < 0) {
+    return -errno;
+  }
+
+  r = signal_mask(SIG_SETMASK, &mask.new, &mask.old);
+  if (r < 0) {
+    return r;
+  }
+
+  struct {
+    int read;
+    int write;
+  } pipe = { PIPE_INVALID, PIPE_INVALID };
+
+  r = pipe_init(&pipe.read, &pipe.write);
+  if (r < 0) {
+    return r;
+  }
+
+  r = fork();
+  if (r < 0) {
+    // `fork` error.
+
+    r = -errno; // Save `errno`.
+
+    int q = signal_mask(SIG_SETMASK, &mask.new, &mask.old);
+    ASSERT_UNUSED(q == 0);
+
+    pipe_destroy(pipe.read);
+    pipe_destroy(pipe.write);
+
+    return r;
+  }
+
+  if (r > 0) {
+    // Parent process
+
+    pid_t child = r;
+
+    // From now on, the child process might have started so we don't report
+    // errors from `signal_mask` and `read`. This puts the responsibility
+    // for cleaning up the process in the hands of the caller.
+
+    int q = signal_mask(SIG_SETMASK, &mask.old, &mask.old);
+    ASSERT_UNUSED(q == 0);
+
+    // Close the error pipe write end on the parent's side so `read` will return
+    // when it is closed on the child side as well.
+    pipe_destroy(pipe.write);
+
+    int child_errno = 0;
+    q = (int) read(pipe.read, &child_errno, sizeof(child_errno));
+    ASSERT_UNUSED(q >= 0);
+
+    if (child_errno > 0) {
+      // If the child writes to the error pipe and exits, we're certain the
+      // child process exited on its own and we can report errors as usual.
+      r = waitpid(child, NULL, 0);
+      ASSERT(r < 0 || r == child);
+
+      r = r < 0 ? -errno : -child_errno;
+    }
+
+    pipe_destroy(pipe.read);
+
+    return r < 0 ? r : child;
+  }
+
+  // Child process
+
+  // Reset all signal handlers so they don't run in the child process. By
+  // default, a child process inherits the parent's signal handlers but we
+  // override this as most signal handlers won't be written in a way that they
+  // can deal with being run in a child process.
+
+  struct sigaction action = { .sa_handler = SIG_DFL };
+
+  r = sigemptyset(&action.sa_mask);
+  if (r < 0) {
+    r = -errno;
+    goto finish;
+  }
+
+  // NSIG is not standardized so we use a fixed limit instead.
+  for (int signal = 0; signal < 32; signal++) {
+    r = sigaction(signal, &action, NULL);
+    if (r < 0 && errno != EINVAL) {
+      r = -errno;
+      goto finish;
+    }
+  }
+
+  // Reset the child's signal mask to the default signal mask. By default, a
+  // child process inherits the parent's signal mask (even over an `exec` call)
+  // but we override this as most processes won't be written in a way that they
+  // can deal with starting with a custom signal mask.
+
+  r = sigemptyset(&mask.new);
+  if (r < 0) {
+    r = -errno;
+    goto finish;
+  }
+
+  r = signal_mask(SIG_SETMASK, &mask.new, NULL);
+  if (r < 0) {
+    goto finish;
+  }
+
+  // Not all file descriptors might have been created with the `FD_CLOEXEC`
+  // flag so we manually close all file descriptors to prevent file descriptors
+  // leaking into the child process.
+
+  r = get_max_fd();
+  if (r < 0) {
+    goto finish;
+  }
+
+  int max_fd = r;
+
+  if (max_fd > MAX_FD_LIMIT) {
+    // Refuse to try to close too many file descriptors.
+    r = -EMFILE;
+    goto finish;
+  }
+
+  for (int i = 0; i < max_fd; i++) {
+    // Make sure we don't close the error pipe file descriptors twice.
+    if (i == pipe.read || i == pipe.write) {
+      continue;
+    }
+
+    if (fd_in_set(i, except, num_except)) {
+      continue;
+    }
+
+    // Check if `i` is a valid file descriptor before trying to close it.
+    r = fcntl(i, F_GETFD);
+    if (r >= 0) {
+      handle_destroy(i);
+    }
+  }
+
+  r = 0;
+
+finish:
+  if (r < 0) {
+    (void) !write(pipe.write, &errno, sizeof(errno));
+    _exit(EXIT_FAILURE);
+  }
+
+  pipe_destroy(pipe.write);
+  pipe_destroy(pipe.read);
+
+  return 0;
+}
+
+int process_start(pid_t *process,
+                  const char *const *argv,
+                  struct process_options options)
+{
+  ASSERT(process);
+
+  if (argv != NULL) {
+    ASSERT(argv[0] != NULL);
+  }
+
+  struct {
+    int read;
+    int write;
+  } pipe = { PIPE_INVALID, PIPE_INVALID };
+  char *program = NULL;
+  char **env = NULL;
+  int r = -1;
+
+  // We create an error pipe to receive errors from the child process.
+  r = pipe_init(&pipe.read, &pipe.write);
+  if (r < 0) {
+    goto finish;
+  }
+
+  if (argv != NULL) {
+    // We prepend the parent working directory to `program` if it is a
+    // relative path so that it will always be searched for relative to the
+    // parent working directory even after executing `chdir`.
+    program = options.working_directory && path_is_relative(argv[0])
+                  ? path_prepend_cwd(argv[0])
+                  : strdup(argv[0]);
+    if (program == NULL) {
+      r = -errno;
+      goto finish;
+    }
+  }
+
+  extern char **environ; // NOLINT
+  char *const *parent = options.env.behavior == REPROC_ENV_EMPTY ? NULL
+                                                                 : environ;
+  env = strv_concat(parent, options.env.extra);
+  if (env == NULL) {
+    goto finish;
+  }
+
+  int except[] = { options.handle.in, options.handle.out, options.handle.err,
+                   pipe.read,         pipe.write,         options.handle.exit };
+
+  r = process_fork(except, ARRAY_SIZE(except));
+  if (r < 0) {
+    goto finish;
+  }
+
+  if (r == 0) {
+    // Redirect stdin, stdout and stderr.
+
+    int redirect[] = { options.handle.in, options.handle.out,
+                       options.handle.err };
+
+    for (int i = 0; i < (int) ARRAY_SIZE(redirect); i++) {
+      // `i` corresponds to the standard stream we need to redirect.
+      r = dup2(redirect[i], i);
+      if (r < 0) {
+        r = -errno;
+        goto child;
+      }
+
+      // Make sure we don't accidentally cloexec the standard streams of the
+      // child process when we're inheriting the parent standard streams. If we
+      // don't call `exec`, the caller is responsible for closing the redirect
+      // and exit handles.
+      if (redirect[i] != i) {
+        // Make sure the pipe is closed when we call exec.
+        r = handle_cloexec(redirect[i], true);
+        if (r < 0) {
+          goto child;
+        }
+      }
+    }
+
+    // Make sure the `exit` file descriptor is inherited.
+
+    r = handle_cloexec(options.handle.exit, false);
+    if (r < 0) {
+      goto child;
+    }
+
+    if (options.working_directory != NULL) {
+      r = chdir(options.working_directory);
+      if (r < 0) {
+        r = -errno;
+        goto child;
+      }
+    }
+
+    // `environ` is carried over calls to `exec`.
+    environ = env;
+
+    if (argv != NULL) {
+      ASSERT(program);
+
+      r = execvp(program, (char *const *) argv);
+      if (r < 0) {
+        r = -errno;
+        goto child;
+      }
+    }
+
+    env = NULL;
+
+  child:
+    if (r < 0) {
+      (void) !write(pipe.write, &errno, sizeof(errno));
+      _exit(EXIT_FAILURE);
+    }
+
+    pipe_destroy(pipe.read);
+    pipe_destroy(pipe.write);
+    free(program);
+    strv_free(env);
+
+    return 0;
+  }
+
+  pid_t child = r;
+
+  // Close the error pipe write end on the parent's side so `read` will return
+  // when it is closed on the child side as well.
+  pipe.write = pipe_destroy(pipe.write);
+
+  int child_errno = 0;
+  r = (int) read(pipe.read, &child_errno, sizeof(child_errno));
+  ASSERT_UNUSED(r >= 0);
+
+  if (child_errno > 0) {
+    r = waitpid(child, NULL, 0);
+    r = r < 0 ? -errno : -child_errno;
+    goto finish;
+  }
+
+  *process = child;
+  r = 0;
+
+finish:
+  pipe_destroy(pipe.read);
+  pipe_destroy(pipe.write);
+  free(program);
+  strv_free(env);
+
+  return r < 0 ? r : 1;
+}
+
+static int parse_status(int status)
+{
+  return WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status) + 128;
+}
+
+int process_pid(process_type process)
+{
+  return process;
+}
+
+int process_wait(pid_t process)
+{
+  ASSERT(process != PROCESS_INVALID);
+
+  int status = 0;
+  int r = waitpid(process, &status, 0);
+  if (r < 0) {
+    return -errno;
+  }
+
+  ASSERT(r == process);
+
+  return parse_status(status);
+}
+
+int process_terminate(pid_t process)
+{
+  ASSERT(process != PROCESS_INVALID);
+
+  int r = kill(process, SIGTERM);
+  return r < 0 ? -errno : 0;
+}
+
+int process_kill(pid_t process)
+{
+  ASSERT(process != PROCESS_INVALID);
+
+  int r = kill(process, SIGKILL);
+  return r < 0 ? -errno : 0;
+}
+
+pid_t process_destroy(pid_t process)
+{
+  // `waitpid` already cleans up the process for us.
+  (void) process;
+  return PROCESS_INVALID;
+}

+ 510 - 0
ThirdParty/Reproc/reproc/src/process.windows.c

@@ -0,0 +1,510 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "process.h"
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include "error.h"
+#include "macro.h"
+#include "utf.h"
+
+const HANDLE PROCESS_INVALID = INVALID_HANDLE_VALUE; // NOLINT
+
+static const DWORD CREATION_FLAGS =
+    // Create each child process in a new process group so we don't send
+    // `CTRL-BREAK` signals to more than one child process in
+    // `process_terminate`.
+    CREATE_NEW_PROCESS_GROUP |
+    // Create each child process with a Unicode environment as we accept any
+    // UTF-16 encoded environment (including Unicode characters). Create each
+    CREATE_UNICODE_ENVIRONMENT |
+    // Create each child with an extended STARTUPINFOEXW structure so we can
+    // specify which handles should be inherited.
+    EXTENDED_STARTUPINFO_PRESENT;
+
+// Argument escaping implementation is based on the following blog post:
+// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
+
+static bool argument_should_escape(const char *argument)
+{
+  ASSERT(argument);
+
+  bool should_escape = false;
+
+  for (size_t i = 0; i < strlen(argument); i++) {
+    should_escape = should_escape || argument[i] == ' ' ||
+                    argument[i] == '\t' || argument[i] == '\n' ||
+                    argument[i] == '\v' || argument[i] == '\"';
+  }
+
+  return should_escape;
+}
+
+static size_t argument_escaped_size(const char *argument)
+{
+  ASSERT(argument);
+
+  size_t argument_size = strlen(argument);
+
+  if (!argument_should_escape(argument)) {
+    return argument_size;
+  }
+
+  size_t size = 2; // double quotes
+
+  for (size_t i = 0; i < argument_size; i++) {
+    size_t num_backslashes = 0;
+
+    while (i < argument_size && argument[i] == '\\') {
+      i++;
+      num_backslashes++;
+    }
+
+    if (i == argument_size) {
+      size += num_backslashes * 2;
+    } else if (argument[i] == '"') {
+      size += num_backslashes * 2 + 2;
+    } else {
+      size += num_backslashes + 1;
+    }
+  }
+
+  return size;
+}
+
+static size_t argument_escape(char *dest, const char *argument)
+{
+  ASSERT(dest);
+  ASSERT(argument);
+
+  size_t argument_size = strlen(argument);
+
+  if (!argument_should_escape(argument)) {
+    strcpy(dest, argument); // NOLINT
+    return argument_size;
+  }
+
+  const char *begin = dest;
+
+  *dest++ = '"';
+
+  for (size_t i = 0; i < argument_size; i++) {
+    size_t num_backslashes = 0;
+
+    while (i < argument_size && argument[i] == '\\') {
+      i++;
+      num_backslashes++;
+    }
+
+    if (i == argument_size) {
+      memset(dest, '\\', num_backslashes * 2);
+      dest += num_backslashes * 2;
+    } else if (argument[i] == '"') {
+      memset(dest, '\\', num_backslashes * 2 + 1);
+      dest += num_backslashes * 2 + 1;
+      *dest++ = '"';
+    } else {
+      memset(dest, '\\', num_backslashes);
+      dest += num_backslashes;
+      *dest++ = argument[i];
+    }
+  }
+
+  *dest++ = '"';
+
+  return (size_t)(dest - begin);
+}
+
+static char *argv_join(const char *const *argv)
+{
+  ASSERT(argv);
+
+  // Determine the size of the concatenated string first.
+  size_t joined_size = 1; // Count the NUL terminator.
+  for (int i = 0; argv[i] != NULL; i++) {
+    joined_size += argument_escaped_size(argv[i]);
+
+    if (argv[i + 1] != NULL) {
+      joined_size++; // Count whitespace.
+    }
+  }
+
+  char *joined = calloc(joined_size, sizeof(char));
+  if (joined == NULL) {
+    SetLastError(ERROR_NOT_ENOUGH_MEMORY);
+    return NULL;
+  }
+
+  char *current = joined;
+  for (int i = 0; argv[i] != NULL; i++) {
+    current += argument_escape(current, argv[i]);
+
+    // We add a space after each argument in the joined arguments string except
+    // for the final argument.
+    if (argv[i + 1] != NULL) {
+      *current++ = ' ';
+    }
+  }
+
+  *current = '\0';
+
+  return joined;
+}
+
+static size_t env_join_size(const char *const *env)
+{
+  ASSERT(env);
+
+  size_t joined_size = 1; // Count the NUL terminator.
+  for (int i = 0; env[i] != NULL; i++) {
+    joined_size += strlen(env[i]) + 1; // Count the NUL terminator.
+  }
+
+  return joined_size;
+}
+
+static char *env_join(const char *const *env)
+{
+  ASSERT(env);
+
+  char *joined = calloc(env_join_size(env), sizeof(char));
+  if (joined == NULL) {
+    SetLastError(ERROR_NOT_ENOUGH_MEMORY);
+    return NULL;
+  }
+
+  char *current = joined;
+  for (int i = 0; env[i] != NULL; i++) {
+    size_t to_copy = strlen(env[i]) + 1; // Include NUL terminator.
+    memcpy(current, env[i], to_copy);
+    current += to_copy;
+  }
+
+  *current = '\0';
+
+  return joined;
+}
+
+static const DWORD NUM_ATTRIBUTES = 1;
+
+static LPPROC_THREAD_ATTRIBUTE_LIST setup_attribute_list(HANDLE *handles,
+                                                         size_t num_handles)
+{
+  ASSERT(handles);
+
+  int r = -1;
+
+  // Make sure all the given handles can be inherited.
+  for (size_t i = 0; i < num_handles; i++) {
+    r = SetHandleInformation(handles[i], HANDLE_FLAG_INHERIT,
+                             HANDLE_FLAG_INHERIT);
+    if (r == 0) {
+      return NULL;
+    }
+  }
+
+  // Get the required size for `attribute_list`.
+  SIZE_T attribute_list_size = 0;
+  r = InitializeProcThreadAttributeList(NULL, NUM_ATTRIBUTES, 0,
+                                        &attribute_list_size);
+  if (r == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+    return NULL;
+  }
+
+  LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = malloc(attribute_list_size);
+  if (attribute_list == NULL) {
+    SetLastError(ERROR_NOT_ENOUGH_MEMORY);
+    return NULL;
+  }
+
+  r = InitializeProcThreadAttributeList(attribute_list, NUM_ATTRIBUTES, 0,
+                                        &attribute_list_size);
+  if (r == 0) {
+    free(attribute_list);
+    return NULL;
+  }
+
+  // Add the handles to be inherited to `attribute_list`.
+  r = UpdateProcThreadAttribute(attribute_list, 0,
+                                PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles,
+                                num_handles * sizeof(HANDLE), NULL, NULL);
+  if (r == 0) {
+    DeleteProcThreadAttributeList(attribute_list);
+    free(attribute_list);
+    return NULL;
+  }
+
+  return attribute_list;
+}
+
+#define NULSTR_FOREACH(i, l)                                                   \
+  for ((i) = (l); (i) && *(i) != L'\0'; (i) = wcschr((i), L'\0') + 1)
+
+static wchar_t *env_concat(const wchar_t *a, const wchar_t *b)
+{
+  const wchar_t *i = NULL;
+  size_t size = 1;
+  wchar_t *c = NULL;
+
+  NULSTR_FOREACH(i, a) {
+    size += wcslen(i) + 1;
+  }
+
+  NULSTR_FOREACH(i, b) {
+    size += wcslen(i) + 1;
+  }
+
+  wchar_t *r = calloc(size, sizeof(wchar_t));
+  if (!r) {
+    return NULL;
+  }
+
+  c = r;
+
+  NULSTR_FOREACH(i, a) {
+    wcscpy(c, i);
+    c += wcslen(i) + 1;
+  }
+
+  NULSTR_FOREACH(i, b) {
+    wcscpy(c, i);
+    c += wcslen(i) + 1;
+  }
+
+  *c = L'\0';
+
+  return r;
+}
+
+static wchar_t *env_setup(REPROC_ENV behavior, const char *const *extra)
+{
+  wchar_t *env_parent_wstring = NULL;
+  char *env_extra = NULL;
+  wchar_t *env_extra_wstring = NULL;
+  wchar_t *env_wstring = NULL;
+
+  if (behavior == REPROC_ENV_EXTEND) {
+    env_parent_wstring = GetEnvironmentStringsW();
+  }
+
+  if (extra != NULL) {
+    env_extra = env_join(extra);
+    if (env_extra == NULL) {
+      goto finish;
+    }
+
+    size_t joined_size = env_join_size(extra);
+    ASSERT(joined_size <= INT_MAX);
+
+    env_extra_wstring = utf16_from_utf8(env_extra, (int) joined_size);
+    if (env_extra_wstring == NULL) {
+      goto finish;
+    }
+  }
+
+  env_wstring = env_concat(env_parent_wstring, env_extra_wstring);
+  if (env_wstring == NULL) {
+    goto finish;
+  }
+
+finish:
+  FreeEnvironmentStringsW(env_parent_wstring);
+  free(env_extra);
+  free(env_extra_wstring);
+
+  return env_wstring;
+}
+
+int process_start(HANDLE *process,
+                  const char *const *argv,
+                  struct process_options options)
+{
+  ASSERT(process);
+
+  if (argv == NULL) {
+    return -ERROR_CALL_NOT_IMPLEMENTED;
+  }
+
+  ASSERT(argv[0] != NULL);
+
+  char *command_line = NULL;
+  wchar_t *command_line_wstring = NULL;
+  wchar_t *env_wstring = NULL;
+  wchar_t *working_directory_wstring = NULL;
+  LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = NULL;
+  PROCESS_INFORMATION info = { PROCESS_INVALID, HANDLE_INVALID, 0, 0 };
+  int r = -1;
+
+  // Join `argv` to a whitespace delimited string as required by
+  // `CreateProcessW`.
+  command_line = argv_join(argv);
+  if (command_line == NULL) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  // Convert UTF-8 to UTF-16 as required by `CreateProcessW`.
+  command_line_wstring = utf16_from_utf8(command_line, -1);
+  if (command_line_wstring == NULL) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  // Idem for `working_directory` if it isn't `NULL`.
+  if (options.working_directory != NULL) {
+    working_directory_wstring = utf16_from_utf8(options.working_directory, -1);
+    if (working_directory_wstring == NULL) {
+      r = -(int) GetLastError();
+      goto finish;
+    }
+  }
+
+  env_wstring = env_setup(options.env.behavior, options.env.extra);
+  if (env_wstring == NULL) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  // Windows Vista added the `STARTUPINFOEXW` structure in which we can put a
+  // list of handles that should be inherited. Only these handles are inherited
+  // by the child process. Other code in an application that calls
+  // `CreateProcess` without passing a `STARTUPINFOEXW` struct containing the
+  // handles it should inherit can still unintentionally inherit handles meant
+  // for a reproc child process. See https://stackoverflow.com/a/2345126 for
+  // more information.
+  HANDLE handles[] = { options.handle.exit, options.handle.in,
+                       options.handle.out, options.handle.err };
+  size_t num_handles = ARRAY_SIZE(handles);
+
+  if (options.handle.out == options.handle.err) {
+    // CreateProcess doesn't like the same handle being specified twice in the
+    // `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` attribute.
+    num_handles--;
+  }
+
+  attribute_list = setup_attribute_list(handles, num_handles);
+  if (attribute_list == NULL) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  STARTUPINFOEXW extended_startup_info = {
+    .StartupInfo = { .cb = sizeof(extended_startup_info),
+                     .dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW,
+                     // `STARTF_USESTDHANDLES`
+                     .hStdInput = options.handle.in,
+                     .hStdOutput = options.handle.out,
+                     .hStdError = options.handle.err,
+                     // `STARTF_USESHOWWINDOW`. Make sure the console window of
+                     // the child process isn't visible. See
+                     // https://github.com/DaanDeMeyer/reproc/issues/6 and
+                     // https://github.com/DaanDeMeyer/reproc/pull/7 for more
+                     // information.
+                     .wShowWindow = SW_HIDE },
+    .lpAttributeList = attribute_list
+  };
+
+  LPSTARTUPINFOW startup_info_address = &extended_startup_info.StartupInfo;
+
+  // Child processes inherit the error mode of their parents. To avoid child
+  // processes creating error dialogs we set our error mode to not create error
+  // dialogs temporarily which is inherited by the child process.
+  DWORD previous_error_mode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
+
+  SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES),
+                                         .bInheritHandle = false,
+                                         .lpSecurityDescriptor = NULL };
+
+  r = CreateProcessW(NULL, command_line_wstring, &do_not_inherit,
+                     &do_not_inherit, true, CREATION_FLAGS, env_wstring,
+                     working_directory_wstring, startup_info_address, &info);
+
+  SetErrorMode(previous_error_mode);
+
+  if (r == 0) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  *process = info.hProcess;
+  r = 0;
+
+finish:
+  free(command_line);
+  free(command_line_wstring);
+  free(env_wstring);
+  free(working_directory_wstring);
+  DeleteProcThreadAttributeList(attribute_list);
+  free(attribute_list);
+  handle_destroy(info.hThread);
+
+  return r < 0 ? r : 1;
+}
+
+int process_pid(process_type process)
+{
+  ASSERT(process);
+  return (int) GetProcessId(process);
+}
+
+int process_wait(HANDLE process)
+{
+  ASSERT(process);
+
+  int r = -1;
+
+  r = (int) WaitForSingleObject(process, INFINITE);
+  if ((DWORD) r == WAIT_FAILED) {
+    return -(int) GetLastError();
+  }
+
+  DWORD status = 0;
+  r = GetExitCodeProcess(process, &status);
+  if (r == 0) {
+    return -(int) GetLastError();
+  }
+
+  // `GenerateConsoleCtrlEvent` causes a process to exit with this exit code.
+  // Because `GenerateConsoleCtrlEvent` has roughly the same semantics as
+  // `SIGTERM`, we map its exit code to `SIGTERM`.
+  if (status == 3221225786) {
+    status = (DWORD) REPROC_SIGTERM;
+  }
+
+  return (int) status;
+}
+
+int process_terminate(HANDLE process)
+{
+  ASSERT(process && process != PROCESS_INVALID);
+
+  // `GenerateConsoleCtrlEvent` can only be called on a process group. To call
+  // `GenerateConsoleCtrlEvent` on a single child process it has to be put in
+  // its own process group (which we did when starting the child process).
+  BOOL r = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, GetProcessId(process));
+
+  return r == 0 ? -(int) GetLastError() : 0;
+}
+
+int process_kill(HANDLE process)
+{
+  ASSERT(process && process != PROCESS_INVALID);
+
+  // We use 137 (`SIGKILL`) as the exit status because it is the same exit
+  // status as a process that is stopped with the `SIGKILL` signal on POSIX
+  // systems.
+  BOOL r = TerminateProcess(process, (DWORD) REPROC_SIGKILL);
+
+  return r == 0 ? -(int) GetLastError() : 0;
+}
+
+HANDLE process_destroy(HANDLE process)
+{
+  return handle_destroy(process);
+}

+ 164 - 0
ThirdParty/Reproc/reproc/src/redirect.c

@@ -0,0 +1,164 @@
+#include "redirect.h"
+
+#include "error.h"
+
+static int redirect_pipe(pipe_type *parent,
+                         handle_type *child,
+                         REPROC_STREAM stream,
+                         bool nonblocking)
+{
+  ASSERT(parent);
+  ASSERT(child);
+
+  pipe_type pipe[] = { PIPE_INVALID, PIPE_INVALID };
+  int r = -1;
+
+  r = pipe_init(&pipe[0], &pipe[1]);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = pipe_nonblocking(stream == REPROC_STREAM_IN ? pipe[1] : pipe[0],
+                       nonblocking);
+  if (r < 0) {
+    goto finish;
+  }
+
+  *parent = stream == REPROC_STREAM_IN ? pipe[1] : pipe[0];
+  *child = stream == REPROC_STREAM_IN ? (handle_type) pipe[0]
+                                      : (handle_type) pipe[1];
+
+finish:
+  if (r < 0) {
+    pipe_destroy(pipe[0]);
+    pipe_destroy(pipe[1]);
+  }
+
+  return r;
+}
+
+int redirect_init(pipe_type *parent,
+                  handle_type *child,
+                  REPROC_STREAM stream,
+                  reproc_redirect redirect,
+                  bool nonblocking,
+                  handle_type out)
+{
+  ASSERT(parent);
+  ASSERT(child);
+
+  int r = REPROC_EINVAL;
+
+  switch (redirect.type) {
+
+    case REPROC_REDIRECT_DEFAULT:
+      ASSERT(false);
+      break;
+
+    case REPROC_REDIRECT_PIPE:
+      r = redirect_pipe(parent, child, stream, nonblocking);
+      break;
+
+    case REPROC_REDIRECT_PARENT:
+      r = redirect_parent(child, stream);
+      if (r == REPROC_EPIPE) {
+        // Discard if the corresponding parent stream is closed.
+        r = redirect_discard(child, stream);
+      }
+
+      if (r < 0) {
+        break;
+      }
+
+      *parent = PIPE_INVALID;
+
+      break;
+
+    case REPROC_REDIRECT_DISCARD:
+      r = redirect_discard(child, stream);
+      if (r < 0) {
+        break;
+      }
+
+      *parent = PIPE_INVALID;
+
+      break;
+
+    case REPROC_REDIRECT_HANDLE:
+      ASSERT(redirect.handle);
+
+      r = 0;
+
+      *child = redirect.handle;
+      *parent = PIPE_INVALID;
+
+      break;
+
+    case REPROC_REDIRECT_FILE:
+      ASSERT(redirect.file);
+
+      r = redirect_file(child, redirect.file);
+      if (r < 0) {
+        break;
+      }
+
+      *parent = PIPE_INVALID;
+
+      break;
+
+    case REPROC_REDIRECT_STDOUT:
+      ASSERT(stream == REPROC_STREAM_ERR);
+      ASSERT(out != HANDLE_INVALID);
+
+      r = 0;
+
+      *child = out;
+      *parent = PIPE_INVALID;
+
+      break;
+
+    case REPROC_REDIRECT_PATH:
+      ASSERT(redirect.path);
+
+      r = redirect_path(child, stream, redirect.path);
+      if (r < 0) {
+        break;
+      }
+
+      *parent = PIPE_INVALID;
+
+      break;
+  }
+
+  return r;
+}
+
+handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type)
+{
+  if (child == HANDLE_INVALID) {
+    return HANDLE_INVALID;
+  }
+
+  switch (type) {
+    case REPROC_REDIRECT_DEFAULT:
+      ASSERT(false);
+      break;
+    case REPROC_REDIRECT_PIPE:
+      // We know `handle` is a pipe if `REDIRECT_PIPE` is used so the cast is
+      // safe. This little hack prevents us from having to introduce a generic
+      // handle type.
+      pipe_destroy((pipe_type) child);
+      break;
+    case REPROC_REDIRECT_DISCARD:
+    case REPROC_REDIRECT_PATH:
+      handle_destroy(child);
+      break;
+    case REPROC_REDIRECT_PARENT:
+    case REPROC_REDIRECT_FILE:
+    case REPROC_REDIRECT_HANDLE:
+    case REPROC_REDIRECT_STDOUT:
+      break;
+  }
+
+  return HANDLE_INVALID;
+}

+ 25 - 0
ThirdParty/Reproc/reproc/src/redirect.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <reproc/reproc.h>
+
+#include "handle.h"
+#include "pipe.h"
+
+int redirect_init(pipe_type *parent,
+                  handle_type *child,
+                  REPROC_STREAM stream,
+                  reproc_redirect redirect,
+                  bool nonblocking,
+                  handle_type out);
+
+handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type);
+
+// Internal prototypes
+
+int redirect_parent(handle_type *child, REPROC_STREAM stream);
+
+int redirect_discard(handle_type *child, REPROC_STREAM stream);
+
+int redirect_file(handle_type *child, FILE *file);
+
+int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path);

+ 79 - 0
ThirdParty/Reproc/reproc/src/redirect.posix.c

@@ -0,0 +1,79 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "redirect.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+#include "error.h"
+#include "pipe.h"
+
+static FILE *stream_to_file(REPROC_STREAM stream)
+{
+  switch (stream) {
+    case REPROC_STREAM_IN:
+      return stdin;
+    case REPROC_STREAM_OUT:
+      return stdout;
+    case REPROC_STREAM_ERR:
+      return stderr;
+  }
+
+  return NULL;
+}
+
+int redirect_parent(int *child, REPROC_STREAM stream)
+{
+  ASSERT(child);
+
+  FILE *file = stream_to_file(stream);
+  if (file == NULL) {
+    return -EINVAL;
+  }
+
+  int r = fileno(file);
+  if (r < 0) {
+    return errno == EBADF ? -EPIPE : -errno;
+  }
+
+  *child = r; // `r` contains the duplicated file descriptor.
+
+  return 0;
+}
+
+int redirect_discard(int *child, REPROC_STREAM stream)
+{
+  return redirect_path(child, stream, "/dev/null");
+}
+
+int redirect_file(int *child, FILE *file)
+{
+  ASSERT(child);
+
+  int r = fileno(file);
+  if (r < 0) {
+    return -errno;
+  }
+
+  *child = r;
+
+  return 0;
+}
+
+int redirect_path(int *child, REPROC_STREAM stream, const char *path)
+{
+  ASSERT(child);
+  ASSERT(path);
+
+  int mode = stream == REPROC_STREAM_IN ? O_RDONLY : O_WRONLY;
+
+  int r = open(path, mode | O_CREAT | O_CLOEXEC, 0640);
+  if (r < 0) {
+    return -errno;
+  }
+
+  *child = r;
+
+  return 0;
+}

+ 117 - 0
ThirdParty/Reproc/reproc/src/redirect.windows.c

@@ -0,0 +1,117 @@
+#ifndef _WIN32_WINNT
+  #define _WIN32_WINNT 0x0600 // _WIN32_WINNT_VISTA
+#elif _WIN32_WINNT < 0x0600
+  #error "_WIN32_WINNT must be greater than _WIN32_WINNT_VISTA (0x0600)"
+#endif
+
+#include "redirect.h"
+
+#include <io.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include "error.h"
+#include "pipe.h"
+#include "utf.h"
+
+static DWORD stream_to_id(REPROC_STREAM stream)
+{
+  switch (stream) {
+    case REPROC_STREAM_IN:
+      return STD_INPUT_HANDLE;
+    case REPROC_STREAM_OUT:
+      return STD_OUTPUT_HANDLE;
+    case REPROC_STREAM_ERR:
+      return STD_ERROR_HANDLE;
+  }
+
+  return 0;
+}
+
+int redirect_parent(HANDLE *child, REPROC_STREAM stream)
+{
+  ASSERT(child);
+
+  DWORD id = stream_to_id(stream);
+  if (id == 0) {
+    return -ERROR_INVALID_PARAMETER;
+  }
+
+  HANDLE *handle = GetStdHandle(id);
+  if (handle == INVALID_HANDLE_VALUE) {
+    return -(int) GetLastError();
+  }
+
+  if (handle == NULL) {
+    return -ERROR_BROKEN_PIPE;
+  }
+
+  *child = handle;
+
+  return 0;
+}
+
+enum { FILE_NO_TEMPLATE = 0 };
+
+int redirect_discard(HANDLE *child, REPROC_STREAM stream)
+{
+  return redirect_path(child, stream, "NUL");
+}
+
+int redirect_file(HANDLE *child, FILE *file)
+{
+  ASSERT(child);
+  ASSERT(file);
+
+  int r = _fileno(file);
+  if (r < 0) {
+    return -ERROR_INVALID_HANDLE;
+  }
+
+  intptr_t result = _get_osfhandle(r);
+  if (result == -1) {
+    return -ERROR_INVALID_HANDLE;
+  }
+
+  *child = (HANDLE) result;
+
+  return 0;
+}
+
+int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path)
+{
+  ASSERT(child);
+  ASSERT(path);
+
+  DWORD mode = stream == REPROC_STREAM_IN ? GENERIC_READ : GENERIC_WRITE;
+  HANDLE handle = HANDLE_INVALID;
+  int r = -1;
+
+  wchar_t *wpath = utf16_from_utf8(path, -1);
+  if (wpath == NULL) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES),
+                                         .bInheritHandle = false,
+                                         .lpSecurityDescriptor = NULL };
+
+  handle = CreateFileW(wpath, mode, FILE_SHARE_READ | FILE_SHARE_WRITE,
+                       &do_not_inherit, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL,
+                       (HANDLE) FILE_NO_TEMPLATE);
+  if (handle == INVALID_HANDLE_VALUE) {
+    r = -(int) GetLastError();
+    goto finish;
+  }
+
+  *child = handle;
+  handle = HANDLE_INVALID;
+  r = 0;
+
+finish:
+  free(wpath);
+  handle_destroy(handle);
+
+  return r;
+}

+ 695 - 0
ThirdParty/Reproc/reproc/src/reproc.c

@@ -0,0 +1,695 @@
+#include <reproc/reproc.h>
+
+#include <stdlib.h>
+
+#include "clock.h"
+#include "error.h"
+#include "handle.h"
+#include "init.h"
+#include "macro.h"
+#include "options.h"
+#include "pipe.h"
+#include "process.h"
+#include "redirect.h"
+
+struct reproc_t {
+  process_type handle;
+
+  struct {
+    pipe_type in;
+    pipe_type out;
+    pipe_type err;
+    pipe_type exit;
+  } pipe;
+
+  int status;
+  reproc_stop_actions stop;
+  int64_t deadline;
+  bool nonblocking;
+
+  struct {
+    pipe_type out;
+    pipe_type err;
+  } child;
+};
+
+enum {
+  STATUS_NOT_STARTED = -1,
+  STATUS_IN_PROGRESS = -2,
+  STATUS_IN_CHILD = -3,
+};
+
+#define SIGOFFSET 128
+
+const int REPROC_SIGKILL = SIGOFFSET + 9;
+const int REPROC_SIGTERM = SIGOFFSET + 15;
+
+const int REPROC_INFINITE = -1;
+const int REPROC_DEADLINE = -2;
+
+static int setup_input(pipe_type *pipe, const uint8_t *data, size_t size)
+{
+  if (data == NULL) {
+    ASSERT(size == 0);
+    return 0;
+  }
+
+  ASSERT(pipe && *pipe != PIPE_INVALID);
+
+  // `reproc_write` only needs the child process stdin pipe to be initialized.
+  size_t written = 0;
+  int r = -1;
+
+  // Make sure we don't block indefinitely when `input` is bigger than the
+  // size of the pipe.
+  r = pipe_nonblocking(*pipe, true);
+  if (r < 0) {
+    return r;
+  }
+
+  while (written < size) {
+    r = pipe_write(*pipe, data + written, size - written);
+    if (r < 0) {
+      return r;
+    }
+
+    ASSERT(written + (size_t) r <= size);
+    written += (size_t) r;
+  }
+
+  *pipe = pipe_destroy(*pipe);
+
+  return 0;
+}
+
+static int expiry(int timeout, int64_t deadline)
+{
+  if (timeout == REPROC_INFINITE && deadline == REPROC_INFINITE) {
+    return REPROC_INFINITE;
+  }
+
+  if (deadline == REPROC_INFINITE) {
+    return timeout;
+  }
+
+  int64_t n = now();
+
+  if (n >= deadline) {
+    return REPROC_DEADLINE;
+  }
+
+  // `deadline` exceeds `now` by at most a full `int` so the cast is safe.
+  int remaining = (int) (deadline - n);
+
+  if (timeout == REPROC_INFINITE) {
+    return remaining;
+  }
+
+  return MIN(timeout, remaining);
+}
+
+static size_t find_earliest_deadline(reproc_event_source *sources,
+                                     size_t num_sources)
+{
+  ASSERT(sources);
+  ASSERT(num_sources > 0);
+
+  size_t earliest = 0;
+  int min = REPROC_INFINITE;
+
+  for (size_t i = 0; i < num_sources; i++) {
+    reproc_t *process = sources[i].process;
+
+    if (process == NULL) {
+      continue;
+    }
+
+    int current = expiry(REPROC_INFINITE, process->deadline);
+
+    if (current == REPROC_DEADLINE) {
+      return i;
+    }
+
+    if (min == REPROC_INFINITE || current < min) {
+      earliest = i;
+      min = current;
+    }
+  }
+
+  return earliest;
+}
+
+reproc_t *reproc_new(void)
+{
+  reproc_t *process = malloc(sizeof(reproc_t));
+  if (process == NULL) {
+    return NULL;
+  }
+
+  *process = (reproc_t){ .handle = PROCESS_INVALID,
+                         .pipe = { .in = PIPE_INVALID,
+                                   .out = PIPE_INVALID,
+                                   .err = PIPE_INVALID,
+                                   .exit = PIPE_INVALID },
+                         .child = { .out = PIPE_INVALID, .err = PIPE_INVALID },
+                         .status = STATUS_NOT_STARTED,
+                         .deadline = REPROC_INFINITE };
+
+  return process;
+}
+
+int reproc_start(reproc_t *process,
+                 const char *const *argv,
+                 reproc_options options)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status == STATUS_NOT_STARTED);
+
+  struct {
+    handle_type in;
+    handle_type out;
+    handle_type err;
+    pipe_type exit;
+  } child = { HANDLE_INVALID, HANDLE_INVALID, HANDLE_INVALID, PIPE_INVALID };
+  int r = -1;
+
+  r = init();
+  if (r < 0) {
+    return r; // Make sure we can always call `deinit` in `finish`.
+  }
+
+  r = parse_options(&options, argv);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = redirect_init(&process->pipe.in, &child.in, REPROC_STREAM_IN,
+                    options.redirect.in, options.nonblocking, HANDLE_INVALID);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = redirect_init(&process->pipe.out, &child.out, REPROC_STREAM_OUT,
+                    options.redirect.out, options.nonblocking, HANDLE_INVALID);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = redirect_init(&process->pipe.err, &child.err, REPROC_STREAM_ERR,
+                    options.redirect.err, options.nonblocking, child.out);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = pipe_init(&process->pipe.exit, &child.exit);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = setup_input(&process->pipe.in, options.input.data, options.input.size);
+  if (r < 0) {
+    goto finish;
+  }
+
+  struct process_options process_options = {
+    .env = { .behavior = options.env.behavior, .extra = options.env.extra },
+    .working_directory = options.working_directory,
+    .handle = { .in = child.in,
+                .out = child.out,
+                .err = child.err,
+                .exit = (handle_type) child.exit }
+  };
+
+  r = process_start(&process->handle, argv, process_options);
+  if (r < 0) {
+    goto finish;
+  }
+
+  if (r > 0) {
+    process->stop = options.stop;
+
+    if (options.deadline != REPROC_INFINITE) {
+      process->deadline = now() + options.deadline;
+    }
+
+    process->nonblocking = options.nonblocking;
+  }
+
+finish:
+  // Either an error has ocurred or the child pipe endpoints have been copied to
+  // the stdin/stdout/stderr streams of the child process. Either way, they can
+  // be safely closed.
+  redirect_destroy(child.in, options.redirect.in.type);
+
+  // See `reproc_poll` for why we do this.
+
+#ifdef _WIN32
+  if (r < 0 || options.redirect.out.type != REPROC_REDIRECT_PIPE) {
+    child.out = redirect_destroy(child.out, options.redirect.out.type);
+  }
+
+  if (r < 0 || options.redirect.err.type != REPROC_REDIRECT_PIPE) {
+    child.err = redirect_destroy(child.err, options.redirect.err.type);
+  }
+#else
+  child.out = redirect_destroy(child.out, options.redirect.out.type);
+  child.err = redirect_destroy(child.err, options.redirect.err.type);
+#endif
+
+  pipe_destroy(child.exit);
+
+  if (r < 0) {
+    process->handle = process_destroy(process->handle);
+    process->pipe.in = pipe_destroy(process->pipe.in);
+    process->pipe.out = pipe_destroy(process->pipe.out);
+    process->pipe.err = pipe_destroy(process->pipe.err);
+    process->pipe.exit = pipe_destroy(process->pipe.exit);
+    deinit();
+  } else if (r == 0) {
+    process->handle = PROCESS_INVALID;
+    // `process_start` has already taken care of closing the handles for us.
+    process->pipe.in = PIPE_INVALID;
+    process->pipe.out = PIPE_INVALID;
+    process->pipe.err = PIPE_INVALID;
+    process->pipe.exit = PIPE_INVALID;
+    process->status = STATUS_IN_CHILD;
+  } else {
+    process->child.out = (pipe_type) child.out;
+    process->child.err = (pipe_type) child.err;
+    process->status = STATUS_IN_PROGRESS;
+  }
+
+  return r;
+}
+
+enum { PIPES_PER_SOURCE = 4 };
+
+static bool contains_valid_pipe(pipe_event_source *sources, size_t num_sources)
+{
+  for (size_t i = 0; i < num_sources; i++) {
+    if (sources[i].pipe != PIPE_INVALID) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+int reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout)
+{
+  ASSERT_EINVAL(sources);
+  ASSERT_EINVAL(num_sources > 0);
+
+  size_t earliest = find_earliest_deadline(sources, num_sources);
+  int64_t deadline = sources[earliest].process == NULL
+                         ? REPROC_INFINITE
+                         : sources[earliest].process->deadline;
+
+  int first = expiry(timeout, deadline);
+  size_t num_pipes = num_sources * PIPES_PER_SOURCE;
+  int r = REPROC_ENOMEM;
+
+  if (first == REPROC_DEADLINE) {
+    for (size_t i = 0; i < num_sources; i++) {
+      sources[i].events = 0;
+    }
+
+    sources[earliest].events = REPROC_EVENT_DEADLINE;
+    return 1;
+  }
+
+  pipe_event_source *pipes = calloc(num_pipes, sizeof(pipe_event_source));
+  if (pipes == NULL) {
+    return r;
+  }
+
+  for (size_t i = 0; i < num_pipes; i++) {
+    pipes[i].pipe = PIPE_INVALID;
+  }
+
+  for (size_t i = 0; i < num_sources; i++) {
+    size_t j = i * PIPES_PER_SOURCE;
+    reproc_t *process = sources[i].process;
+    int interests = sources[i].interests;
+
+    if (process == NULL) {
+      continue;
+    }
+
+    bool in = interests & REPROC_EVENT_IN;
+    pipes[j + 0].pipe = in ? process->pipe.in : PIPE_INVALID;
+    pipes[j + 0].interests = PIPE_EVENT_OUT;
+
+    bool out = interests & REPROC_EVENT_OUT;
+    pipes[j + 1].pipe = out ? process->pipe.out : PIPE_INVALID;
+    pipes[j + 1].interests = PIPE_EVENT_IN;
+
+    bool err = interests & REPROC_EVENT_ERR;
+    pipes[j + 2].pipe = err ? process->pipe.err : PIPE_INVALID;
+    pipes[j + 2].interests = PIPE_EVENT_IN;
+
+    bool exit = (interests & REPROC_EVENT_EXIT) ||
+                (interests & REPROC_EVENT_OUT &&
+                 process->child.out != PIPE_INVALID) ||
+                (interests & REPROC_EVENT_ERR &&
+                 process->child.err != PIPE_INVALID);
+    pipes[j + 3].pipe = exit ? process->pipe.exit : PIPE_INVALID;
+    pipes[j + 3].interests = PIPE_EVENT_IN;
+  }
+
+  if (!contains_valid_pipe(pipes, num_pipes)) {
+    r = REPROC_EPIPE;
+    goto finish;
+  }
+
+  r = pipe_poll(pipes, num_pipes, first);
+  if (r < 0) {
+    goto finish;
+  }
+
+  for (size_t i = 0; i < num_sources; i++) {
+    sources[i].events = 0;
+  }
+
+  if (r == 0 && first != timeout) {
+    // Differentiate between timeout and deadline expiry. Deadline expiry is an
+    // event, timeouts are not.
+    sources[earliest].events = REPROC_EVENT_DEADLINE;
+    r = 1;
+  } else if (r > 0) {
+    // Convert pipe events to process events.
+    for (size_t i = 0; i < num_pipes; i++) {
+      if (pipes[i].pipe == PIPE_INVALID) {
+        continue;
+      }
+
+      if (pipes[i].events > 0) {
+        // Index in a set of pipes determines the process pipe and thus the
+        // process event.
+        // 0 = stdin pipe => REPROC_EVENT_IN
+        // 1 = stdout pipe => REPROC_EVENT_OUT
+        // ...
+        int event = 1 << (i % PIPES_PER_SOURCE);
+        sources[i / PIPES_PER_SOURCE].events |= event;
+      }
+    }
+
+    r = 0;
+
+    // Count the number of processes with events.
+    for (size_t i = 0; i < num_sources; i++) {
+      r += sources[i].events > 0;
+    }
+
+    // On Windows, when redirecting to sockets, we keep the child handles alive
+    // in the parent process (see `reproc_start`). We do this because Windows
+    // doesn't correctly flush redirected socket handles when a child process
+    // exits. This can lead to data loss where the parent process doesn't
+    // receive all output of the child process. To get around this, we keep an
+    // extra handle open in the parent process which we close correctly when we
+    // detect the child process has exited. Detecting whether a child process
+    // has exited happens via another inherited socket, but here there's no
+    // danger of data loss because no data is received over this socket.
+
+    bool again = false;
+
+    for (size_t i = 0; i < num_sources; i++) {
+      if (!(sources[i].events & REPROC_EVENT_EXIT)) {
+        continue;
+      }
+
+      reproc_t *process = sources[i].process;
+
+      if (process->child.out == PIPE_INVALID &&
+          process->child.err == PIPE_INVALID) {
+        continue;
+      }
+
+      r = pipe_shutdown(process->child.out);
+      if (r < 0) {
+        goto finish;
+      }
+
+      r = pipe_shutdown(process->child.err);
+      if (r < 0) {
+        goto finish;
+      }
+
+      process->child.out = pipe_destroy(process->child.out);
+      process->child.err = pipe_destroy(process->child.err);
+      again = true;
+    }
+
+    // If we've closed handles, we poll again so we can include any new close
+    // events that occurred because we closed handles.
+
+    if (again) {
+      r = reproc_poll(sources, num_sources, timeout);
+      if (r < 0) {
+        goto finish;
+      }
+    }
+  }
+
+finish:
+  free(pipes);
+
+  return r;
+}
+
+int reproc_read(reproc_t *process,
+                REPROC_STREAM stream,
+                uint8_t *buffer,
+                size_t size)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+  ASSERT_EINVAL(stream == REPROC_STREAM_OUT || stream == REPROC_STREAM_ERR);
+  ASSERT_EINVAL(buffer);
+
+  pipe_type *pipe = stream == REPROC_STREAM_OUT ? &process->pipe.out
+                                                : &process->pipe.err;
+  pipe_type child = stream == REPROC_STREAM_OUT ? process->child.out
+                                                : process->child.err;
+  int r = -1;
+
+  if (*pipe == PIPE_INVALID) {
+    return REPROC_EPIPE;
+  }
+
+  // If we've kept extra handles open in the parent, make sure we use
+  // `reproc_poll` which closes the extra handles we keep open when the child
+  // process exits. If we don't, `pipe_read` will block forever because the
+  // extra handles we keep open in the parent would never be closed.
+  if (child != PIPE_INVALID) {
+    int event = stream == REPROC_STREAM_OUT ? REPROC_EVENT_OUT
+                                            : REPROC_EVENT_ERR;
+    reproc_event_source source = { process, event, 0 };
+    r = reproc_poll(&source, 1, process->nonblocking ? 0 : REPROC_INFINITE);
+    if (r <= 0) {
+      return r == 0 ? REPROC_EWOULDBLOCK : r;
+    }
+  }
+
+  r = pipe_read(*pipe, buffer, size);
+
+  if (r == REPROC_EPIPE) {
+    *pipe = pipe_destroy(*pipe);
+  }
+
+  return r;
+}
+
+int reproc_write(reproc_t *process, const uint8_t *buffer, size_t size)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+
+  if (buffer == NULL) {
+    // Allow `NULL` buffers but only if `size == 0`.
+    ASSERT_EINVAL(size == 0);
+    return 0;
+  }
+
+  if (process->pipe.in == PIPE_INVALID) {
+    return REPROC_EPIPE;
+  }
+
+  int r = pipe_write(process->pipe.in, buffer, size);
+
+  if (r == REPROC_EPIPE) {
+    process->pipe.in = pipe_destroy(process->pipe.in);
+  }
+
+  return r;
+}
+
+int reproc_close(reproc_t *process, REPROC_STREAM stream)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+
+  switch (stream) {
+    case REPROC_STREAM_IN:
+      process->pipe.in = pipe_destroy(process->pipe.in);
+      return 0;
+    case REPROC_STREAM_OUT:
+      process->pipe.out = pipe_destroy(process->pipe.out);
+      return 0;
+    case REPROC_STREAM_ERR:
+      process->pipe.err = pipe_destroy(process->pipe.err);
+      return 0;
+  }
+
+  return REPROC_EINVAL;
+}
+
+int reproc_wait(reproc_t *process, int timeout)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+  ASSERT_EINVAL(process->status != STATUS_NOT_STARTED);
+
+  int r = -1;
+
+  if (process->status >= 0) {
+    return process->status;
+  }
+
+  if (timeout == REPROC_DEADLINE) {
+    timeout = expiry(REPROC_INFINITE, process->deadline);
+    // If the deadline has expired, `expiry` returns `REPROC_DEADLINE` which
+    // means we'll only check if the process is still running.
+    if (timeout == REPROC_DEADLINE) {
+      timeout = 0;
+    }
+  }
+
+  ASSERT(process->pipe.exit != PIPE_INVALID);
+
+  pipe_event_source source = { .pipe = process->pipe.exit,
+                               .interests = PIPE_EVENT_IN };
+
+  r = pipe_poll(&source, 1, timeout);
+  if (r <= 0) {
+    return r == 0 ? REPROC_ETIMEDOUT : r;
+  }
+
+  r = process_wait(process->handle);
+  if (r < 0) {
+    return r;
+  }
+
+  process->pipe.exit = pipe_destroy(process->pipe.exit);
+
+  return process->status = r;
+}
+
+int reproc_terminate(reproc_t *process)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+  ASSERT_EINVAL(process->status != STATUS_NOT_STARTED);
+
+  if (process->status >= 0) {
+    return 0;
+  }
+
+  return process_terminate(process->handle);
+}
+
+int reproc_kill(reproc_t *process)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+  ASSERT_EINVAL(process->status != STATUS_NOT_STARTED);
+
+  if (process->status >= 0) {
+    return 0;
+  }
+
+  return process_kill(process->handle);
+}
+
+int reproc_stop(reproc_t *process, reproc_stop_actions stop)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+  ASSERT_EINVAL(process->status != STATUS_NOT_STARTED);
+
+  stop = parse_stop_actions(stop);
+
+  reproc_stop_action actions[] = { stop.first, stop.second, stop.third };
+  int r = -1;
+
+  for (size_t i = 0; i < ARRAY_SIZE(actions); i++) {
+    r = REPROC_EINVAL; // NOLINT
+
+    switch (actions[i].action) {
+      case REPROC_STOP_NOOP:
+        r = 0;
+        continue;
+      case REPROC_STOP_WAIT:
+        r = 0;
+        break;
+      case REPROC_STOP_TERMINATE:
+        r = reproc_terminate(process);
+        break;
+      case REPROC_STOP_KILL:
+        r = reproc_kill(process);
+        break;
+    }
+
+    // Stop if `reproc_terminate` or `reproc_kill` fail.
+    if (r < 0) {
+      break;
+    }
+
+    r = reproc_wait(process, actions[i].timeout);
+    if (r != REPROC_ETIMEDOUT) {
+      break;
+    }
+  }
+
+  return r;
+}
+
+int reproc_pid(reproc_t *process)
+{
+  ASSERT_EINVAL(process);
+  ASSERT_EINVAL(process->status != STATUS_IN_CHILD);
+  ASSERT_EINVAL(process->status != STATUS_NOT_STARTED);
+
+  return process_pid(process->handle);
+}
+
+reproc_t *reproc_destroy(reproc_t *process)
+{
+  ASSERT_RETURN(process, NULL);
+
+  if (process->status == STATUS_IN_PROGRESS) {
+    reproc_stop(process, process->stop);
+  }
+
+  process_destroy(process->handle);
+  pipe_destroy(process->pipe.in);
+  pipe_destroy(process->pipe.out);
+  pipe_destroy(process->pipe.err);
+  pipe_destroy(process->pipe.exit);
+
+  pipe_destroy(process->child.out);
+  pipe_destroy(process->child.err);
+
+  if (process->status != STATUS_NOT_STARTED) {
+    deinit();
+  }
+
+  free(process);
+
+  return NULL;
+}
+
+const char *reproc_strerror(int error)
+{
+  return error_string(error);
+}

+ 54 - 0
ThirdParty/Reproc/reproc/src/run.c

@@ -0,0 +1,54 @@
+#include <reproc/run.h>
+
+#include <reproc/drain.h>
+
+#include "error.h"
+
+int reproc_run(const char *const *argv, reproc_options options)
+{
+  if (!options.redirect.discard && !options.redirect.file &&
+      !options.redirect.path) {
+    options.redirect.parent = true;
+  }
+
+  return reproc_run_ex(argv, options, REPROC_SINK_NULL, REPROC_SINK_NULL);
+}
+
+int reproc_run_ex(const char *const *argv,
+                  reproc_options options, // lgtm [cpp/large-parameter]
+                  reproc_sink out,
+                  reproc_sink err)
+{
+  reproc_t *process = NULL;
+  int r = REPROC_ENOMEM;
+
+  // There's no way for `reproc_run_ex` to inform the caller whether we're in
+  // the forked process or the parent process so let's not allow forking when
+  // using `reproc_run_ex`.
+  ASSERT_EINVAL(!options.fork);
+
+  process = reproc_new();
+  if (process == NULL) {
+    goto finish;
+  }
+
+  r = reproc_start(process, argv, options);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = reproc_drain(process, out, err);
+  if (r < 0) {
+    goto finish;
+  }
+
+  r = reproc_stop(process, options.stop);
+  if (r < 0) {
+    goto finish;
+  }
+
+finish:
+  reproc_destroy(process);
+
+  return r;
+}

+ 88 - 0
ThirdParty/Reproc/reproc/src/strv.c

@@ -0,0 +1,88 @@
+#include "strv.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "error.h"
+
+static char *str_dup(const char *s)
+{
+  ASSERT_RETURN(s, NULL);
+
+  char *r = malloc(strlen(s) + 1);
+  if (!r) {
+    return NULL;
+  }
+
+  strcpy(r, s); // NOLINT
+
+  return r;
+}
+
+char **strv_concat(char *const *a, const char *const *b)
+{
+  char *const *i = NULL;
+  const char *const *j = NULL;
+  size_t size = 1;
+  size_t c = 0;
+
+  STRV_FOREACH(i, a) {
+    size++;
+  }
+
+  STRV_FOREACH(j, b) {
+    size++;
+  }
+
+  char **r = calloc(size, sizeof(char *));
+  if (!r) {
+    goto finish;
+  }
+
+  STRV_FOREACH(i, a) {
+    r[c] = str_dup(*i);
+    if (!r[c]) {
+      goto finish;
+    }
+
+    c++;
+  }
+
+  STRV_FOREACH(j, b) {
+    r[c] = str_dup(*j);
+    if (!r[c]) {
+      goto finish;
+    }
+
+    c++;
+  }
+
+  r[c++] = NULL;
+
+finish:
+  if (c < size) {
+    STRV_FOREACH(i, r) {
+      free(*i);
+    }
+
+    free(r);
+
+    return NULL;
+  }
+
+  return r;
+}
+
+char **strv_free(char **l)
+{
+  char **s = NULL;
+
+  STRV_FOREACH(s, l) {
+    free(*s);
+  }
+
+  free(l);
+
+  return NULL;
+}

+ 7 - 0
ThirdParty/Reproc/reproc/src/strv.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#define STRV_FOREACH(s, l) for ((s) = (l); (s) && *(s); (s)++)
+
+char **strv_concat(char *const *a, const char *const *b);
+
+char **strv_free(char **l);

+ 13 - 0
ThirdParty/Reproc/reproc/src/utf.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <wchar.h>
+
+// `size` represents the entire size of `string`, including NUL-terminators. We
+// take the entire size because strings like the environment string passed to
+// CreateProcessW includes multiple NUL-terminators so we can't always rely on
+// `strlen` to calculate the string length for us. See the lpEnvironment
+// documentation of CreateProcessW:
+// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
+// Pass -1 as the size to have `utf16_from_utf8` calculate the size until (and
+// including) the first NUL terminator.
+wchar_t *utf16_from_utf8(const char *string, int size);

+ 3 - 0
ThirdParty/Reproc/reproc/src/utf.posix.c

@@ -0,0 +1,3 @@
+#include "utf.h"
+
+// `utf16_from_utf8` is Windows-only.

+ 39 - 0
ThirdParty/Reproc/reproc/src/utf.windows.c

@@ -0,0 +1,39 @@
+#include "utf.h"
+
+#include <limits.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include "error.h"
+
+wchar_t *utf16_from_utf8(const char *string, int size)
+{
+  ASSERT(string);
+
+  // Determine wstring size (`MultiByteToWideChar` returns the required size if
+  // its last two arguments are `NULL` and 0).
+  int r = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string, size, NULL,
+                              0);
+  if (r == 0) {
+    return NULL;
+  }
+
+  // `MultiByteToWideChar` does not return negative values so the cast to
+  // `size_t` is safe.
+  wchar_t *wstring = calloc((size_t) r, sizeof(wchar_t));
+  if (wstring == NULL) {
+    SetLastError(ERROR_NOT_ENOUGH_MEMORY);
+    return NULL;
+  }
+
+  // Now we pass our allocated string and its size as the last two arguments
+  // instead of `NULL` and 0 which makes `MultiByteToWideChar` actually perform
+  // the conversion.
+  r = MultiByteToWideChar(CP_UTF8, 0, string, size, wstring, r);
+  if (r == 0) {
+    free(wstring);
+    return NULL;
+  }
+
+  return wstring;
+}

+ 33 - 0
ThirdParty/Reproc/reproc/test/argv.c

@@ -0,0 +1,33 @@
+#include <reproc/run.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  const char *argv[] = { RESOURCE_DIRECTORY "/argv", "\"argument 1\"",
+                         "\"argument 2\"", NULL };
+  char *output = NULL;
+  reproc_sink sink = reproc_sink_string(&output);
+  int r = -1;
+
+  r = reproc_run_ex(argv, (reproc_options){ 0 }, sink, sink);
+  ASSERT_OK(r);
+  ASSERT(output != NULL);
+
+  const char *current = output;
+
+  for (size_t i = 0; i < 3; i++) {
+    size_t size = strlen(argv[i]);
+
+    ASSERT_GE_SIZE(strlen(current), size);
+    ASSERT_EQ_MEM(current, argv[i], size);
+
+    current += size;
+    current += *current == '\r';
+    current += *current == '\n';
+  }
+
+  ASSERT_EQ_SIZE(strlen(current), (size_t) 0);
+
+  reproc_free(output);
+}

+ 43 - 0
ThirdParty/Reproc/reproc/test/assert.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define ASSERT(expression) ASSERT_MSG(expression, "%s", "")
+#define ASSERT_OK(r) ASSERT_MSG(r >= 0, "%s", reproc_strerror(r))
+
+#define ASSERT_EQ_MEM(left, right, size)                                       \
+  ASSERT_MSG(memcmp(left, right, size) == 0, "\"%.*s\" == \"%.*s\"",           \
+             (int) size, left, (int) size, right)
+
+#define ASSERT_EQ_STR(left, right)                                             \
+  ASSERT_MSG(strcmp(left, right) == 0, "%s == %s", left, right)
+
+#define ASSERT_GE_SIZE(left, right)                                            \
+  ASSERT_MSG(left >= right, "%zu >= %zu", left, right)
+
+#define ASSERT_EQ_SIZE(left, right)                                            \
+  ASSERT_MSG(left == right, "%zu == %zu", left, right)
+
+#define ASSERT_EQ_INT(left, right)                                             \
+  ASSERT_MSG(left == right, "%i == %i", left, right)
+
+#ifdef _WIN32
+  #define ABORT() exit(EXIT_FAILURE)
+#else
+  // Use `abort` so we get a coredump.
+  #define ABORT() abort()
+#endif
+
+#define ASSERT_MSG(expression, format, ...)                                    \
+  do {                                                                         \
+    if (!(expression)) {                                                       \
+      fprintf(stderr, "%s:%u: Assertion '%s' (" format ") failed", __FILE__,   \
+              __LINE__, #expression, __VA_ARGS__);                             \
+                                                                               \
+      fflush(stderr);                                                          \
+                                                                               \
+      ABORT();                                                                 \
+    }                                                                          \
+  } while (0)

+ 10 - 0
ThirdParty/Reproc/reproc/test/deadline.c

@@ -0,0 +1,10 @@
+#include <reproc/run.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  const char *argv[] = { RESOURCE_DIRECTORY "/deadline", NULL };
+  int r = reproc_run(argv, (reproc_options){ .deadline = 100 });
+  ASSERT(r == REPROC_SIGTERM);
+}

+ 36 - 0
ThirdParty/Reproc/reproc/test/env.c

@@ -0,0 +1,36 @@
+#include <reproc/run.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  const char *argv[] = { RESOURCE_DIRECTORY "/env", NULL };
+  const char *envp[] = { "IP=127.0.0.1", "PORT=8080", NULL };
+  char *output = NULL;
+  reproc_sink sink = reproc_sink_string(&output);
+  int r = -1;
+
+  r = reproc_run_ex(argv,
+                    (reproc_options){ .env.behavior = REPROC_ENV_EMPTY,
+                                      .env.extra = envp },
+                    sink, sink);
+  ASSERT_OK(r);
+  ASSERT(output != NULL);
+
+  const char *current = output;
+
+  for (size_t i = 0; i < 2; i++) {
+    size_t size = strlen(envp[i]);
+
+    ASSERT_GE_SIZE(strlen(current), size);
+    ASSERT_EQ_MEM(current, envp[i], size);
+
+    current += size;
+    current += *current == '\r';
+    current += *current == '\n';
+  }
+
+  ASSERT_EQ_SIZE(strlen(current), (size_t) 0);
+
+  reproc_free(output);
+}

+ 39 - 0
ThirdParty/Reproc/reproc/test/fork.c

@@ -0,0 +1,39 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <reproc/run.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  reproc_t *process = reproc_new();
+  const char *MESSAGE = "reproc stands for REdirected PROCess!";
+  char *output = NULL;
+  reproc_sink sink = reproc_sink_string(&output);
+  int r = -1;
+
+  r = reproc_start(process, NULL, (reproc_options){ .fork = true });
+
+  if (r == 0) {
+    printf("%s", MESSAGE);
+    fclose(stdout); // `_exit` doesn't flush stdout.
+    _exit(EXIT_SUCCESS);
+  }
+
+  ASSERT_OK(r);
+
+  r = reproc_drain(process, sink, sink);
+  ASSERT_OK(r);
+
+  ASSERT(output != NULL);
+  ASSERT_EQ_STR(output, MESSAGE);
+
+  r = reproc_wait(process, REPROC_INFINITE);
+  ASSERT_OK(r);
+
+  reproc_destroy(process);
+
+  reproc_free(output);
+}

+ 74 - 0
ThirdParty/Reproc/reproc/test/io.c

@@ -0,0 +1,74 @@
+#include <reproc/drain.h>
+#include <reproc/reproc.h>
+
+#include "assert.h"
+
+#define MESSAGE "reproc stands for REdirected PROCess"
+
+static void io()
+{
+  int r = -1;
+
+  reproc_t *process = reproc_new();
+  ASSERT(process);
+
+  const char *argv[] = { RESOURCE_DIRECTORY "/io", NULL };
+
+  r = reproc_start(process, argv,
+                   (reproc_options){
+                       .redirect.err.type = REPROC_REDIRECT_STDOUT });
+  ASSERT_OK(r);
+
+  r = reproc_write(process, (uint8_t *) MESSAGE, strlen(MESSAGE));
+  ASSERT_OK(r);
+  ASSERT_EQ_INT(r, (int) strlen(MESSAGE));
+
+  r = reproc_close(process, REPROC_STREAM_IN);
+  ASSERT_OK(r);
+
+  char *out = NULL;
+  r = reproc_drain(process, reproc_sink_string(&out), REPROC_SINK_NULL);
+  ASSERT_OK(r);
+
+  ASSERT(out != NULL);
+  ASSERT_EQ_STR(out, MESSAGE MESSAGE);
+
+  r = reproc_wait(process, REPROC_INFINITE);
+  ASSERT_OK(r);
+
+  reproc_destroy(process);
+
+  reproc_free(out);
+}
+
+static void timeout(void)
+{
+  int r = -1;
+
+  reproc_t *process = reproc_new();
+  ASSERT(process);
+
+  const char *argv[] = { RESOURCE_DIRECTORY "/io", NULL };
+
+  r = reproc_start(process, argv, (reproc_options){ 0 });
+  ASSERT_OK(r);
+
+  reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR,
+                                 0 };
+  r = reproc_poll(&source, 1, 200);
+  ASSERT(r == 0);
+
+  r = reproc_close(process, REPROC_STREAM_IN);
+  ASSERT_OK(r);
+
+  r = reproc_poll(&source, 1, 200);
+  ASSERT_OK(r);
+
+  reproc_destroy(process);
+}
+
+int main(void)
+{
+  io();
+  timeout();
+}

+ 17 - 0
ThirdParty/Reproc/reproc/test/overflow.c

@@ -0,0 +1,17 @@
+#include <reproc/run.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  const char *argv[] = { RESOURCE_DIRECTORY "/overflow", NULL };
+  char *output = NULL;
+  reproc_sink sink = reproc_sink_string(&output);
+  int r = -1;
+
+  r = reproc_run_ex(argv, (reproc_options){ 0 }, sink, sink);
+  ASSERT_OK(r);
+  ASSERT(output != NULL);
+
+  reproc_free(output);
+}

+ 41 - 0
ThirdParty/Reproc/reproc/test/path.c

@@ -0,0 +1,41 @@
+#include <reproc/run.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  const char *argv[] = { RESOURCE_DIRECTORY "/path", NULL };
+  int r = -1;
+
+  r = reproc_run(argv, (reproc_options){ .redirect.path = "path.txt" });
+  ASSERT_OK(r);
+
+  FILE *file = fopen("path.txt", "rb");
+  ASSERT(file != NULL);
+
+  r = fseek(file, 0, SEEK_END);
+  ASSERT_OK(r);
+
+  r = (int) ftell(file);
+  ASSERT_OK(r);
+
+  size_t size = (size_t) r;
+  char *string = malloc(size + 1);
+  ASSERT(string != NULL);
+
+  rewind(file);
+  r = (int) fread(string, sizeof(char), size, file);
+  ASSERT_EQ_INT(r, (int) size);
+
+  string[r] = '\0';
+
+  r = fclose(file);
+  ASSERT_OK(r);
+
+  r = remove("path.txt");
+  ASSERT_OK(r);
+
+  ASSERT_EQ_STR(string, argv[0]);
+
+  free(string);
+}

+ 37 - 0
ThirdParty/Reproc/reproc/test/pid.c

@@ -0,0 +1,37 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <reproc/drain.h>
+#include <reproc/reproc.h>
+
+#include "assert.h"
+
+int main(void)
+{
+  const char *argv[] = { RESOURCE_DIRECTORY "/pid", NULL };
+  char *output = NULL;
+  reproc_sink sink = reproc_sink_string(&output);
+  int r = -1;
+
+  reproc_t *process = reproc_new();
+  ASSERT(process);
+
+  ASSERT(reproc_pid(process) == REPROC_EINVAL);
+
+  r = reproc_start(process, argv, (reproc_options){ 0 });
+  ASSERT_OK(r);
+
+  r = reproc_drain(process, sink, sink);
+  ASSERT_OK(r);
+  ASSERT(output != NULL);
+
+  ASSERT(reproc_pid(process) == strtol(output, NULL, 10));
+
+  r = reproc_wait(process, REPROC_INFINITE);
+  ASSERT_OK(r);
+
+  ASSERT(reproc_pid(process) == strtol(output, NULL, 10));
+
+  reproc_destroy(process);
+  reproc_free(output);
+}

+ 32 - 0
ThirdParty/Reproc/reproc/test/stop.c

@@ -0,0 +1,32 @@
+#include <reproc/reproc.h>
+
+#include "assert.h"
+
+static void stop(REPROC_STOP action, int status)
+{
+  int r = -1;
+
+  reproc_t *process = reproc_new();
+  ASSERT(process);
+
+  const char *argv[] = { RESOURCE_DIRECTORY "/stop", NULL };
+
+  r = reproc_start(process, argv, (reproc_options){ 0 });
+  ASSERT_OK(r);
+
+  r = reproc_wait(process, 50);
+  ASSERT(r == REPROC_ETIMEDOUT);
+
+  reproc_stop_actions stop = { .first = { action, 500 } };
+
+  r = reproc_stop(process, stop);
+  ASSERT_EQ_INT(r, status);
+
+  reproc_destroy(process);
+}
+
+int main(void)
+{
+  stop(REPROC_STOP_TERMINATE, REPROC_SIGTERM);
+  stop(REPROC_STOP_KILL, REPROC_SIGKILL);
+}

Some files were not shown because too many files changed in this diff