From b1eab3abdfaad7fc486da86b2dcb3bfdaecdaf02 Mon Sep 17 00:00:00 2001 From: doylet Date: Mon, 25 Mar 2024 13:14:05 +1100 Subject: [PATCH] Add capturing of stdout/stderr output in exec API --- dqn_cppbuild.h | 7 +- dqn_os.cpp | 44 +++++++---- dqn_os.h | 26 ++++++- dqn_os_win32.cpp | 197 +++++++++++++++++++++++++++++++++++++++++------ dqn_win32.h | 16 +++- 5 files changed, 242 insertions(+), 48 deletions(-) diff --git a/dqn_cppbuild.h b/dqn_cppbuild.h index 96ca1c7..2070d51 100644 --- a/dqn_cppbuild.h +++ b/dqn_cppbuild.h @@ -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 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 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 diff --git a/dqn_os.cpp b/dqn_os.cpp index c866549..64db290 100644 --- a/dqn_os.cpp +++ b/dqn_os.cpp @@ -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 cmd_line, Dqn_Str8 working_dir) +DQN_API Dqn_OSExecResult Dqn_OS_Exec(Dqn_Slice 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 cmd_line, Dqn_Str8 working_dir) +DQN_API Dqn_OSExecResult Dqn_OS_ExecOrAbort(Dqn_Slice 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 //////////////////////////////////////////////////////////////////////// diff --git a/dqn_os.h b/dqn_os.h index 5af51ec..220b3de 100644 --- a/dqn_os.h +++ b/dqn_os.h @@ -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 cmd_line, Dqn_Str8 working_dir); -DQN_API Dqn_OSExecResult Dqn_OS_Exec (Dqn_Slice cmd_line, Dqn_Str8 working_dir); -DQN_API void Dqn_OS_ExecOrAbort(Dqn_Slice 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 cmd_line, Dqn_Str8 working_dir, uint8_t exec_flag, Dqn_ErrorSink *error); +DQN_API Dqn_OSExecResult Dqn_OS_Exec (Dqn_Slice 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 cmd_line, Dqn_Str8 working_dir, uint8_t exec_flag, Dqn_Arena *arena); // NOTE: [$SEMA] Dqn_OSSemaphore /////////////////////////////////////////////////////////////////// #if !defined(DQN_NO_SEMAPHORE) diff --git a/dqn_os_win32.cpp b/dqn_os_win32.cpp index 9c45997..e88d108 100644 --- a/dqn_os_win32.cpp +++ b/dqn_os_win32.cpp @@ -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 cmd_line, Dqn_Str8 working_dir) +DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync(Dqn_Slice 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 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; } diff --git a/dqn_win32.h b/dqn_win32.h index ad94ad8..f7ca625 100644 --- a/dqn_win32.h +++ b/dqn_win32.h @@ -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)