Add capturing of stdout/stderr output in exec API

This commit is contained in:
doylet 2024-03-25 13:14:05 +11:00
parent af7ae2ae2a
commit b1eab3abdf
5 changed files with 242 additions and 48 deletions

View File

@ -173,7 +173,7 @@ DQN_API Dqn_Str8 Dqn_CPPBuild_ToCommandLineStr8(Dqn_CPPBuildContext build_contex
return result;
}
DQN_API Dqn_CPPBuildAsyncResult Dqn_CPPBuild_Async(Dqn_CPPBuildContext build_context, Dqn_CPPBuildMode mode)
DQN_API Dqn_CPPBuildAsyncResult Dqn_CPPBuild_Async(Dqn_CPPBuildContext build_context, Dqn_CPPBuildMode mode, Dqn_ErrorSink *error)
{
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
Dqn_Slice<Dqn_Str8> cmd_line = Dqn_CPPBuild_ToCommandLine(build_context, mode, scratch.arena);
@ -183,10 +183,11 @@ DQN_API Dqn_CPPBuildAsyncResult Dqn_CPPBuild_Async(Dqn_CPPBuildContext build_con
if (!Dqn_OS_MakeDir(build_context.build_dir)) {
result.status = Dqn_CPPBuildStatus_BuildDirectoryFailedToBeMade;
Dqn_ErrorSink_MakeF(error, result.status, "Failed to make build directory '%.*s'", DQN_STR_FMT(build_context.build_dir));
return result;
}
result.async_handle = Dqn_OS_ExecAsync(cmd_line, build_context.build_dir);
result.async_handle = Dqn_OS_ExecAsync(cmd_line, build_context.build_dir, Dqn_OSExecFlag_Nil, error);
return result;
}
@ -198,6 +199,6 @@ void Dqn_CPPBuild_ExecOrAbort(Dqn_CPPBuildContext build_context, Dqn_CPPBuildMod
}
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
Dqn_Slice<Dqn_Str8> cmd_line = Dqn_CPPBuild_ToCommandLine(build_context, mode, scratch.arena);
Dqn_OS_ExecOrAbort(cmd_line, build_context.build_dir);
Dqn_OS_ExecOrAbort(cmd_line, build_context.build_dir, Dqn_OSExecFlag_Nil, scratch.arena);
}
#endif // DQN_CPP_BUILD_IMPLEMENTATION

View File

@ -440,29 +440,39 @@ DQN_API Dqn_Str8 Dqn_OS_PathBuildWithSeparator(Dqn_Arena *arena, Dqn_OSPath cons
// NOTE: [$EXEC] Dqn_OSExec ////////////////////////////////////////////////////////////////////////
DQN_API Dqn_OSExecResult Dqn_OS_Exec(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir)
DQN_API Dqn_OSExecResult Dqn_OS_Exec(Dqn_Slice<Dqn_Str8> cmd_line,
Dqn_Str8 working_dir,
uint8_t exec_flag,
Dqn_Arena *arena,
Dqn_ErrorSink *error)
{
Dqn_OSExecAsyncHandle async_handle = Dqn_OS_ExecAsync(cmd_line, working_dir);
Dqn_OSExecResult result = Dqn_OS_ExecWait(async_handle);
Dqn_OSExecAsyncHandle async_handle = Dqn_OS_ExecAsync(cmd_line, working_dir, exec_flag, error);
Dqn_OSExecResult result = Dqn_OS_ExecWait(async_handle, arena, error);
return result;
}
DQN_API void Dqn_OS_ExecOrAbort(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir)
DQN_API Dqn_OSExecResult Dqn_OS_ExecOrAbort(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir, uint8_t exec_flag, Dqn_Arena *arena)
{
Dqn_OSExecResult result = Dqn_OS_Exec(cmd_line, working_dir);
if (result.os_error_code || result.exit_code) {
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
Dqn_Str8 cmd_combined = Dqn_Slice_Str8Render(scratch.arena, cmd_line, DQN_STR8(" ") /*separator*/);
if (result.os_error_code) {
Dqn_Log_ErrorF("OS failed to execute the requested command returning the error code %u. The command was\n\n%.*s", result.os_error_code, DQN_STR_FMT(cmd_combined));
Dqn_OS_Exit(result.os_error_code);
}
if (result.exit_code) {
Dqn_Log_ErrorF("OS executed command and returned a non-zero status: %u. The command was\n\n%.*s", result.exit_code, DQN_STR_FMT(cmd_combined));
Dqn_OS_Exit(result.exit_code);
}
Dqn_ErrorSink *error = Dqn_ErrorSink_Begin(Dqn_ErrorSinkMode_Nil);
Dqn_OSExecResult result = Dqn_OS_Exec(cmd_line, working_dir, exec_flag, arena, error);
if (result.os_error_code) {
Dqn_ErrorSink_EndAndExitIfErrorF(
error,
result.os_error_code,
"OS failed to execute the requested command returning the error code %u",
result.os_error_code);
}
if (result.exit_code) {
Dqn_ErrorSink_EndAndExitIfErrorF(
error,
result.exit_code,
"OS executed command and returned non-zero exit code %u",
result.exit_code);
}
Dqn_ErrorSink_EndAndIgnore(error);
return result;
}
// NOTE: [$HTTP] Dqn_OSHttp ////////////////////////////////////////////////////////////////////////

View File

@ -169,15 +169,33 @@ struct Dqn_OSPath
};
// NOTE: [$EXEC] Dqn_OSExec ////////////////////////////////////////////////////////////////////////
enum Dqn_OSExecFlag
{
Dqn_OSExecFlag_Nil = 0,
Dqn_OSExecFlag_SaveStdout = 1 << 0,
Dqn_OSExecFlag_SaveStderr = 1 << 1,
Dqn_OSExecFlag_SaveOutput = Dqn_OSExecFlag_SaveStdout | Dqn_OSExecFlag_SaveStderr,
Dqn_OSExecFlag_MergeStderrToStdout = 1 << 2 | Dqn_OSExecFlag_SaveOutput,
};
struct Dqn_OSExecAsyncHandle
{
uint8_t exec_flags;
uint32_t os_error_code;
uint32_t exit_code;
void *process;
#if defined(DQN_OS_WIN32)
void *stdout_read;
void *stdout_write;
void *stderr_read;
void *stderr_write;
#endif
};
struct Dqn_OSExecResult
{
Dqn_Str8 stdout_text;
Dqn_Str8 stderr_text;
uint32_t os_error_code;
uint32_t exit_code;
};
@ -345,10 +363,10 @@ DQN_API Dqn_Str8 Dqn_OS_PathConvertF (Dqn_Arena *arena
// NOTE: [$EXEC] Dqn_OSExec ////////////////////////////////////////////////////////////////////////
DQN_API void Dqn_OS_Exit (int32_t exit_code);
DQN_API Dqn_OSExecResult Dqn_OS_ExecWait (Dqn_OSExecAsyncHandle handle);
DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync (Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir);
DQN_API Dqn_OSExecResult Dqn_OS_Exec (Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir);
DQN_API void Dqn_OS_ExecOrAbort(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir);
DQN_API Dqn_OSExecResult Dqn_OS_ExecWait (Dqn_OSExecAsyncHandle handle, Dqn_Arena *arena, Dqn_ErrorSink *error);
DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync (Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir, uint8_t exec_flag, Dqn_ErrorSink *error);
DQN_API Dqn_OSExecResult Dqn_OS_Exec (Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir, uint8_t exec_flag, Dqn_Arena *arena, Dqn_ErrorSink *error);
DQN_API Dqn_OSExecResult Dqn_OS_ExecOrAbort(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir, uint8_t exec_flag, Dqn_Arena *arena);
// NOTE: [$SEMA] Dqn_OSSemaphore ///////////////////////////////////////////////////////////////////
#if !defined(DQN_NO_SEMAPHORE)

View File

@ -550,38 +550,92 @@ DQN_API void Dqn_OS_Exit(int32_t exit_code)
ExitProcess(DQN_CAST(UINT)exit_code);
}
DQN_API Dqn_OSExecResult Dqn_OS_ExecWait(Dqn_OSExecAsyncHandle handle)
DQN_API Dqn_OSExecResult Dqn_OS_ExecWait(Dqn_OSExecAsyncHandle handle, Dqn_Arena *arena, Dqn_ErrorSink *error)
{
Dqn_OSExecResult result = {};
if (!handle.process || handle.os_error_code) {
result.os_error_code = handle.os_error_code;
if (!handle.process || handle.os_error_code || handle.exit_code) {
if (handle.os_error_code)
result.os_error_code = handle.os_error_code;
else
result.exit_code = handle.exit_code;
DQN_ASSERT(!handle.stdout_read);
DQN_ASSERT(!handle.stdout_write);
DQN_ASSERT(!handle.stderr_read);
DQN_ASSERT(!handle.stderr_write);
DQN_ASSERT(!handle.process);
return result;
}
if (handle.exit_code) {
result.exit_code = handle.exit_code;
return result;
}
Dqn_Scratch scratch = Dqn_Scratch_Get(arena);
DWORD exec_result = WaitForSingleObject(handle.process, INFINITE);
CloseHandle(handle.stdout_write);
CloseHandle(handle.stderr_write);
DWORD exec_result = WaitForSingleObject(handle.process, INFINITE);
if (exec_result == WAIT_FAILED) {
result.os_error_code = GetLastError();
return result;
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(error, result.os_error_code, "Executed command failed to terminate: %.*s", DQN_STR_FMT(win_error.msg));
CloseHandle(handle.process);
} else {
// NOTE: Get exit code /////////////////////////////////////////////////////////////////////
DWORD exit_status;
if (GetExitCodeProcess(handle.process, &exit_status)) {
result.exit_code = exit_status;
} else {
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(error,
result.os_error_code,
"Failed to retrieve command exit code: %.*s",
DQN_STR_FMT(win_error.msg));
}
CloseHandle(handle.process);
// NOTE: Read stdout from process //////////////////////////////////////////////////////////
if (arena && handle.stdout_write) {
char stdout_buffer[4096];
Dqn_Str8Builder builder = {};
builder.arena = scratch.arena;
for (;;) {
DWORD bytes_read = 0;
BOOL success = ReadFile(handle.stdout_read, stdout_buffer, sizeof(stdout_buffer), &bytes_read, NULL);
if (!success || bytes_read == 0)
break;
Dqn_Str8Builder_AppendF(&builder, "%.*s", bytes_read, stdout_buffer);
}
result.stdout_text = Dqn_Str8Builder_Build(&builder, arena);
}
// NOTE: Read stderr from process //////////////////////////////////////////////////////////
if (arena && handle.stderr_read) {
char stderr_buffer[4096];
Dqn_Str8Builder builder = {};
builder.arena = scratch.arena;
for (;;) {
DWORD bytes_read = 0;
BOOL success = ReadFile(handle.stderr_read, stderr_buffer, sizeof(stderr_buffer), &bytes_read, NULL);
if (!success || bytes_read == 0)
break;
Dqn_Str8Builder_AppendF(&builder, "%.*s", bytes_read, stderr_buffer);
}
result.stderr_text = Dqn_Str8Builder_Build(&builder, arena);
}
}
DWORD exit_status;
if (!GetExitCodeProcess(handle.process, &exit_status)) {
result.os_error_code = GetLastError();
return result;
}
result.exit_code = exit_status;
CloseHandle(handle.process);
CloseHandle(handle.stdout_read);
CloseHandle(handle.stderr_read);
return result;
}
DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir)
DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir, uint8_t flags, Dqn_ErrorSink *error)
{
// NOTE: Pre-amble /////////////////////////////////////////////////////////////////////////////
Dqn_OSExecAsyncHandle result = {};
if (cmd_line.size == 0)
return result;
@ -591,21 +645,118 @@ DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync(Dqn_Slice<Dqn_Str8> cmd_line, Dqn
Dqn_Str16 cmd16 = Dqn_Win_Str8ToStr16(scratch.arena, cmd_rendered);
Dqn_Str16 working_dir16 = Dqn_Win_Str8ToStr16(scratch.arena, working_dir);
// NOTE: Stdout/err security attributes ////////////////////////////////////////////////////////
SECURITY_ATTRIBUTES save_std_security_attribs = {};
save_std_security_attribs.nLength = sizeof(save_std_security_attribs);
save_std_security_attribs.bInheritHandle = true;
// NOTE: Redirect stdout ///////////////////////////////////////////////////////////////////////
HANDLE stdout_read = {};
HANDLE stdout_write = {};
if (flags & Dqn_OSExecFlag_SaveStdout) {
if (!CreatePipe(&stdout_read, &stdout_write, &save_std_security_attribs, /*nSize*/ 0)) {
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(
error,
result.os_error_code,
"Failed to create stdout pipe to redirect the output of the command '%.*s': %.*s",
DQN_STR_FMT(cmd_rendered),
DQN_STR_FMT(win_error.msg));
return result;
}
DQN_DEFER {
if (result.os_error_code) {
CloseHandle(stdout_read);
CloseHandle(stdout_write);
}
};
if (!SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0)) {
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(error,
result.os_error_code,
"Failed to make stdout 'read' pipe non-inheritable when trying to "
"execute command '%.*s': %.*s",
DQN_STR_FMT(cmd_rendered),
DQN_STR_FMT(win_error.msg));
return result;
}
}
// NOTE: Redirect stderr ///////////////////////////////////////////////////////////////////////
HANDLE stderr_read = {};
HANDLE stderr_write = {};
if (flags & Dqn_OSExecFlag_SaveStderr) {
if (flags & Dqn_OSExecFlag_MergeStderrToStdout) {
stderr_read = stdout_read;
stderr_write = stdout_write;
} else {
if (!CreatePipe(&stderr_read, &stderr_write, &save_std_security_attribs, /*nSize*/ 0)) {
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(
error,
result.os_error_code,
"Failed to create stderr pipe to redirect the output of the command '%.*s': %.*s",
DQN_STR_FMT(cmd_rendered),
DQN_STR_FMT(win_error.msg));
return result;
}
DQN_DEFER {
if (result.os_error_code) {
CloseHandle(stderr_read);
CloseHandle(stderr_write);
}
};
if (!SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0)) {
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(error,
result.os_error_code,
"Failed to make stderr 'read' pipe non-inheritable when trying to "
"execute command '%.*s': %.*s",
DQN_STR_FMT(cmd_rendered),
DQN_STR_FMT(win_error.msg));
return result;
}
}
}
// NOTE: Execute command ///////////////////////////////////////////////////////////////////////
PROCESS_INFORMATION proc_info = {};
STARTUPINFOW startup_info = {};
startup_info.cb = sizeof(STARTUPINFOW);
startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);
startup_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
startup_info.hStdError = stderr_write ? stderr_write : GetStdHandle(STD_ERROR_HANDLE);
startup_info.hStdOutput = stdout_write ? stdout_write : GetStdHandle(STD_OUTPUT_HANDLE);
startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startup_info.dwFlags |= STARTF_USESTDHANDLES;
BOOL create_result = CreateProcessW(nullptr, cmd16.data, nullptr, nullptr, true, 0, nullptr, working_dir16.data, &startup_info, &proc_info);
if (!create_result) {
result.os_error_code = GetLastError();
Dqn_WinError win_error = Dqn_Win_LastError(scratch.arena);
result.os_error_code = win_error.code;
Dqn_ErrorSink_MakeF(error,
result.os_error_code,
"Failed to execute command '%.*s': %.*s",
DQN_STR_FMT(cmd_rendered),
DQN_STR_FMT(win_error.msg));
return result;
}
// NOTE: Post-amble ////////////////////////////////////////////////////////////////////////////
CloseHandle(proc_info.hThread);
result.process = proc_info.hProcess;
result.process = proc_info.hProcess;
result.stdout_read = stdout_read;
result.stdout_write = stdout_write;
if ((flags & Dqn_OSExecFlag_MergeStderrToStdout) == 0) {
result.stderr_read = stderr_read;
result.stderr_write = stderr_write;
}
result.exec_flags = flags;
return result;
}

View File

@ -417,7 +417,6 @@
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
// NOTE: um/winbase.h //////////////////////////////////////////////////////////////////////////
#define WAIT_FAILED ((DWORD)0xFFFFFFFF)
#define WAIT_OBJECT_0 ((STATUS_WAIT_0 ) + 0 )
@ -427,6 +426,9 @@
#define STD_OUTPUT_HANDLE ((DWORD)-11)
#define STD_ERROR_HANDLE ((DWORD)-12)
#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
// NOTE: MoveFile
#define MOVEFILE_REPLACE_EXISTING 0x00000001
#define MOVEFILE_COPY_ALLOWED 0x00000002
@ -1080,5 +1082,17 @@
__declspec(dllimport) BOOL __stdcall IsDebuggerPresent();
}
// NOTE: um/namedpipeapi.h /////////////////////////////////////////////////////////////////////
extern "C"
{
__declspec(dllimport) BOOL __stdcall CreatePipe (HANDLE *hReadPipe, HANDLE *hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize);
}
// NOTE: um/handleapi.h /////////////////////////////////////////////////////////////////////
extern "C"
{
__declspec(dllimport) BOOL __stdcall SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags);
}
DQN_MSVC_WARNING_POP
#endif // !defined(_INC_WINDOWS)