diff --git a/test/subprocess.h b/test/subprocess.h index f3ea161..5e80902 100644 --- a/test/subprocess.h +++ b/test/subprocess.h @@ -49,9 +49,9 @@ #endif #if defined(__TINYC__) -#define subprocess_attribute(a) __attribute((a)) +#define SUBPROCESS_ATTRIBUTE(a) __attribute((a)) #else -#define subprocess_attribute(a) __attribute__((a)) +#define SUBPROCESS_ATTRIBUTE(a) __attribute__((a)) #endif #if defined(_MSC_VER) @@ -59,15 +59,15 @@ #define subprocess_weak __inline #define subprocess_tls __declspec(thread) #elif defined(__MINGW32__) -#define subprocess_pure subprocess_attribute(pure) -#define subprocess_weak static subprocess_attribute(used) +#define subprocess_pure SUBPROCESS_ATTRIBUTE(pure) +#define subprocess_weak static SUBPROCESS_ATTRIBUTE(used) #define subprocess_tls __thread #elif defined(__clang__) || defined(__GNUC__) || defined(__TINYC__) -#define subprocess_pure subprocess_attribute(pure) -#define subprocess_weak subprocess_attribute(weak) +#define subprocess_pure SUBPROCESS_ATTRIBUTE(pure) +#define subprocess_weak SUBPROCESS_ATTRIBUTE(weak) #define subprocess_tls __thread #else -#error Non clang, non gcc, non MSVC, non tcc compiler found! +#error Non clang, non gcc, non MSVC compiler found! #endif struct subprocess_s; @@ -89,7 +89,25 @@ enum subprocess_option_e { // Search for program names in the PATH variable. Always enabled on Windows. // Note: this will **not** search for paths in any provided custom environment // and instead uses the PATH of the spawning process. - subprocess_option_search_user_path = 0x10 + subprocess_option_search_user_path = 0x10, + + // Make subprocess_read_stdout and subprocess_read_stderr return immediately + // with 0 if no data is available. Requires subprocess_option_enable_async. + subprocess_option_enable_async_no_wait = 0x20 +}; + +// Error codes returned by subprocess_create and subprocess_create_ex. +// subprocess_error_success is always zero; all errors are non-zero. +enum subprocess_error_e { + subprocess_error_success = 0, + subprocess_error_unknown = -1, + subprocess_error_invalid_options = -2, + subprocess_error_invalid_environment = -3, + subprocess_error_not_found = -4, + subprocess_error_permission_denied = -5, + subprocess_error_no_memory = -6, + subprocess_error_pipe = -7, + subprocess_error_spawn = -8 }; #if defined(__cplusplus) @@ -99,11 +117,14 @@ extern "C" { /// @brief Create a process. /// @param command_line An array of strings for the command line to execute for /// this process. The last element must be NULL to signify the end of the array. -/// The memory backing this parameter only needs to persist until this function -/// returns. +/// On Windows these strings are interpreted as UTF-8 and passed to the Unicode +/// process creation APIs. The memory backing this parameter only needs to +/// persist until this function returns. /// @param options A bit field of subprocess_option_e's to pass. /// @param out_process The newly created process. -/// @return On success zero is returned. +/// @return On success zero is returned. On failure a non-zero +/// `subprocess_error_e` value is returned; inspect `errno` on POSIX platforms +/// or `GetLastError()` on Windows for the platform-specific failure reason. subprocess_weak int subprocess_create(const char *const command_line[], int options, struct subprocess_s *const out_process); @@ -111,20 +132,28 @@ subprocess_weak int subprocess_create(const char *const command_line[], /// @brief Create a process (extended create). /// @param command_line An array of strings for the command line to execute for /// this process. The last element must be NULL to signify the end of the array. -/// The memory backing this parameter only needs to persist until this function -/// returns. +/// On Windows these strings are interpreted as UTF-8 and passed to the Unicode +/// process creation APIs. The memory backing this parameter only needs to +/// persist until this function returns. /// @param options A bit field of subprocess_option_e's to pass. /// @param environment An optional array of strings for the environment to use /// for a child process (each element of the form FOO=BAR). The last element -/// must be NULL to signify the end of the array. +/// must be NULL to signify the end of the array. On Windows these strings are +/// interpreted as UTF-8. +/// @param process_cwd The current working directory of the newly created +/// process. If NULL, will be the same as the parent process. On Windows this +/// string is interpreted as UTF-8. /// @param out_process The newly created process. -/// @return On success zero is returned. +/// @return On success zero is returned. On failure a non-zero +/// `subprocess_error_e` value is returned; inspect `errno` on POSIX platforms +/// or `GetLastError()` on Windows for the platform-specific failure reason. /// /// If `options` contains `subprocess_option_inherit_environment`, then /// `environment` must be NULL. subprocess_weak int subprocess_create_ex(const char *const command_line[], int options, const char *const environment[], + const char *const process_cwd, struct subprocess_s *const out_process); /// @brief Get the standard input file for a process. @@ -189,11 +218,12 @@ subprocess_weak int subprocess_terminate(struct subprocess_s *const process); /// @param buffer The buffer to read into. /// @param size The maximum number of bytes to read. /// @return The number of bytes actually read into buffer. Can only be 0 if the -/// process has complete. +/// process has complete, or if the process was created with +/// `subprocess_option_enable_async_no_wait` and no data is currently available. /// /// The only safe way to read from the standard output of a process during it's /// execution is to use the `subprocess_option_enable_async` option in -/// conjuction with this method. +/// conjunction with this method. subprocess_weak unsigned subprocess_read_stdout(struct subprocess_s *const process, char *const buffer, unsigned size); @@ -203,11 +233,12 @@ subprocess_read_stdout(struct subprocess_s *const process, char *const buffer, /// @param buffer The buffer to read into. /// @param size The maximum number of bytes to read. /// @return The number of bytes actually read into buffer. Can only be 0 if the -/// process has complete. +/// process has complete, or if the process was created with +/// `subprocess_option_enable_async_no_wait` and no data is currently available. /// /// The only safe way to read from the standard error of a process during it's /// execution is to use the `subprocess_option_enable_async` option in -/// conjuction with this method. +/// conjunction with this method. subprocess_weak unsigned subprocess_read_stderr(struct subprocess_s *const process, char *const buffer, unsigned size); @@ -230,9 +261,14 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #endif #if !defined(_WIN32) +#include +#include #include #include #include +#if defined(__APPLE__) +#include +#endif #include #include #include @@ -240,6 +276,23 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #if defined(_WIN32) +#include + +#if defined(__clang__) +#if __has_warning("-Wc++-keyword") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++-keyword" +#endif +#endif + +typedef wchar_t subprocess_wchar_t; + +#if defined(__clang__) +#if __has_warning("-Wc++-keyword") +#pragma clang diagnostic pop +#endif +#endif + #if (_MSC_VER < 1920) #ifdef _WIN64 typedef __int64 subprocess_intptr_t; @@ -262,7 +315,7 @@ typedef size_t subprocess_size_t; typedef struct _PROCESS_INFORMATION *LPPROCESS_INFORMATION; typedef struct _SECURITY_ATTRIBUTES *LPSECURITY_ATTRIBUTES; -typedef struct _STARTUPINFOA *LPSTARTUPINFOA; +typedef struct _STARTUPINFOW *LPSTARTUPINFOW; typedef struct _OVERLAPPED *LPOVERLAPPED; #ifdef __clang__ @@ -276,6 +329,10 @@ typedef struct _OVERLAPPED *LPOVERLAPPED; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #endif +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif struct subprocess_subprocess_information_s { void *hProcess; @@ -292,9 +349,9 @@ struct subprocess_security_attributes_s { struct subprocess_startup_info_s { unsigned long cb; - char *lpReserved; - char *lpDesktop; - char *lpTitle; + subprocess_wchar_t *lpReserved; + subprocess_wchar_t *lpDesktop; + subprocess_wchar_t *lpTitle; unsigned long dwX; unsigned long dwY; unsigned long dwXSize; @@ -325,6 +382,9 @@ struct subprocess_overlapped_s { void *hEvent; }; +#ifdef __clang__ +#pragma clang diagnostic pop +#endif #ifdef __MINGW32__ #pragma GCC diagnostic pop #endif @@ -333,6 +393,7 @@ struct subprocess_overlapped_s { #endif __declspec(dllimport) unsigned long __stdcall GetLastError(void); +__declspec(dllimport) void __stdcall SetLastError(unsigned long); __declspec(dllimport) int __stdcall SetHandleInformation(void *, unsigned long, unsigned long); __declspec(dllimport) int __stdcall CreatePipe(void **, void **, @@ -352,9 +413,12 @@ __declspec(dllimport) void *__stdcall CreateFileA(const char *, unsigned long, void *); __declspec(dllimport) void *__stdcall CreateEventA(LPSECURITY_ATTRIBUTES, int, int, const char *); -__declspec(dllimport) int __stdcall CreateProcessA( - const char *, char *, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, int, - unsigned long, void *, const char *, LPSTARTUPINFOA, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall CreateProcessW( + const subprocess_wchar_t *, subprocess_wchar_t *, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, int, unsigned long, void *, + const subprocess_wchar_t *, LPSTARTUPINFOW, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall MultiByteToWideChar( + unsigned int, unsigned long, const char *, int, subprocess_wchar_t *, int); __declspec(dllimport) int __stdcall CloseHandle(void *); __declspec(dllimport) unsigned long __stdcall WaitForSingleObject( void *, unsigned long); @@ -365,6 +429,10 @@ __declspec(dllimport) unsigned long __stdcall WaitForMultipleObjects( unsigned long, void *const *, int, unsigned long); __declspec(dllimport) int __stdcall GetOverlappedResult(void *, LPOVERLAPPED, unsigned long *, int); +__declspec(dllimport) int __stdcall PeekNamedPipe(void *, void *, unsigned long, + unsigned long *, + unsigned long *, + unsigned long *); #if defined(_DLL) #define SUBPROCESS_DLLIMPORT __declspec(dllimport) @@ -380,6 +448,7 @@ __declspec(dllimport) int __stdcall GetOverlappedResult(void *, LPOVERLAPPED, SUBPROCESS_DLLIMPORT int __cdecl _fileno(FILE *); SUBPROCESS_DLLIMPORT int __cdecl _open_osfhandle(subprocess_intptr_t, int); SUBPROCESS_DLLIMPORT subprocess_intptr_t __cdecl _get_osfhandle(int); +SUBPROCESS_DLLIMPORT int __cdecl _close(int); #ifndef __MINGW32__ void *__cdecl _alloca(subprocess_size_t); @@ -414,14 +483,89 @@ struct subprocess_s { int return_status; #endif - subprocess_size_t alive; + int alive; + int no_wait; }; #ifdef __clang__ #pragma clang diagnostic pop #endif +#if defined(_WIN32) +subprocess_weak int subprocess_error_from_windows_error(unsigned long error); +int subprocess_error_from_windows_error(unsigned long error) { + enum { + errorFileNotFound = 2, + errorPathNotFound = 3, + errorTooManyOpenFiles = 4, + errorAccessDenied = 5, + errorInvalidHandle = 6, + errorNotEnoughMemory = 8, + errorOutOfMemory = 14, + errorInvalidDrive = 15, + errorBadPathname = 161, + errorDirectory = 267 + }; + + switch (error) { + case errorFileNotFound: + case errorPathNotFound: + case errorInvalidDrive: + case errorBadPathname: + case errorDirectory: + return subprocess_error_not_found; + case errorAccessDenied: + return subprocess_error_permission_denied; + case errorTooManyOpenFiles: + case errorNotEnoughMemory: + case errorOutOfMemory: + return subprocess_error_no_memory; + case errorInvalidHandle: + return subprocess_error_pipe; + default: + return subprocess_error_unknown; + } +} +#else +subprocess_weak int subprocess_error_from_errno(int error); +int subprocess_error_from_errno(int error) { + switch (error) { + case ENOENT: + case ENOTDIR: + return subprocess_error_not_found; + case EACCES: + case EPERM: + return subprocess_error_permission_denied; + case EMFILE: + case ENFILE: + case ENOMEM: + return subprocess_error_no_memory; + default: + return subprocess_error_unknown; + } +} +#endif + +#if defined(__clang__) +#if __has_warning("-Wunsafe-buffer-usage") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" +#endif +#endif + #if defined(_WIN32) subprocess_weak int subprocess_create_named_pipe_helper(void **rd, void **wr); +subprocess_weak void subprocess_close_handle(void **handle); +void subprocess_close_handle(void **handle) { + const void *const invalidHandleValue = + SUBPROCESS_PTR_CAST(void *, ~(SUBPROCESS_CAST(subprocess_intptr_t, 0))); + + if (*handle && (invalidHandleValue != *handle)) { + CloseHandle(*handle); + } + + *handle = SUBPROCESS_NULL; +} + int subprocess_create_named_pipe_helper(void **rd, void **wr) { const unsigned long pipeAccessInbound = 0x00000001; const unsigned long fileFlagOverlapped = 0x40000000; @@ -435,9 +579,23 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { struct subprocess_security_attributes_s saAttr = {sizeof(saAttr), SUBPROCESS_NULL, 1}; char name[256] = {0}; +#if defined(__clang__) +#if __has_warning("-Wunique-object-duplication") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunique-object-duplication" +#endif +#endif static subprocess_tls long index = 0; +#if defined(__clang__) +#if __has_warning("-Wunique-object-duplication") +#pragma clang diagnostic pop +#endif +#endif const long unique = index++; + *rd = SUBPROCESS_NULL; + *wr = SUBPROCESS_NULL; + #if defined(_MSC_VER) && _MSC_VER < 1900 #pragma warning(push, 1) #pragma warning(disable : 4996) @@ -465,6 +623,7 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { openExisting, fileAttributeNormal, SUBPROCESS_NULL); if (invalidHandleValue == *wr) { + subprocess_close_handle(rd); return -1; } @@ -475,27 +634,41 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { int subprocess_create(const char *const commandLine[], int options, struct subprocess_s *const out_process) { return subprocess_create_ex(commandLine, options, SUBPROCESS_NULL, - out_process); + SUBPROCESS_NULL, out_process); } int subprocess_create_ex(const char *const commandLine[], int options, const char *const environment[], + const char *const process_cwd, struct subprocess_s *const out_process) { #if defined(_WIN32) int fd; - void *rd, *wr; + int async_no_wait; + void *rd = SUBPROCESS_NULL; + void *wr = SUBPROCESS_NULL; char *commandLineCombined; + subprocess_wchar_t *commandLineCombinedWide = SUBPROCESS_NULL; + subprocess_wchar_t *process_cwd_wide = SUBPROCESS_NULL; subprocess_size_t len; + int wide_len; int i, j; int need_quoting; unsigned long flags = 0; + unsigned long last_error = 0; + int result = subprocess_error_unknown; + const unsigned int codePageUtf8 = 65001; + const unsigned long mbErrInvalidChars = 0x00000008; const unsigned long startFUseStdHandles = 0x00000100; const unsigned long handleFlagInherit = 0x00000001; const unsigned long createNoWindow = 0x08000000; - struct subprocess_subprocess_information_s processInfo; + const unsigned long createUnicodeEnvironment = 0x00000400; + struct subprocess_subprocess_information_s processInfo = {SUBPROCESS_NULL, + SUBPROCESS_NULL, 0, + 0}; struct subprocess_security_attributes_s saAttr = {sizeof(saAttr), SUBPROCESS_NULL, 1}; - char *used_environment = SUBPROCESS_NULL; + subprocess_wchar_t empty_environment[2] = {0, 0}; + subprocess_wchar_t *used_environment = SUBPROCESS_NULL; struct subprocess_startup_info_s startInfo = {0, SUBPROCESS_NULL, SUBPROCESS_NULL, @@ -515,6 +688,14 @@ int subprocess_create_ex(const char *const commandLine[], int options, SUBPROCESS_NULL, SUBPROCESS_NULL}; + async_no_wait = subprocess_option_enable_async_no_wait == + (options & subprocess_option_enable_async_no_wait); + + if (async_no_wait && (subprocess_option_enable_async != + (options & subprocess_option_enable_async))) { + return subprocess_error_invalid_options; + } + startInfo.cb = sizeof(startInfo); startInfo.dwFlags = startFUseStdHandles; @@ -522,93 +703,142 @@ int subprocess_create_ex(const char *const commandLine[], int options, flags |= createNoWindow; } + memset(out_process, 0, sizeof(*out_process)); + if (subprocess_option_inherit_environment != (options & subprocess_option_inherit_environment)) { + flags |= createUnicodeEnvironment; + if (SUBPROCESS_NULL == environment) { - used_environment = SUBPROCESS_CONST_CAST(char *, "\0\0"); + used_environment = empty_environment; } else { - // We always end with two null terminators. - len = 2; + // We always end with two null terminators. MultiByteToWideChar includes + // each environment string's null terminator, so start with one extra. + len = 1; for (i = 0; environment[i]; i++) { - for (j = 0; '\0' != environment[i][j]; j++) { - len++; + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + environment[i], -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } - // For the null terminator too. - len++; + len += SUBPROCESS_CAST(subprocess_size_t, wide_len); + } + + if (((SUBPROCESS_CAST(subprocess_size_t, -1)) / + sizeof(subprocess_wchar_t)) < len) { + result = subprocess_error_no_memory; + goto cleanup; } - used_environment = SUBPROCESS_CAST(char *, _alloca(len)); + used_environment = SUBPROCESS_CAST( + subprocess_wchar_t *, _alloca(len * sizeof(subprocess_wchar_t))); + if (!used_environment) { + result = subprocess_error_no_memory; + goto cleanup; + } - // Re-use len for the insertion position + // Re-use len for the insertion position. len = 0; for (i = 0; environment[i]; i++) { - for (j = 0; '\0' != environment[i][j]; j++) { - used_environment[len++] = environment[i][j]; + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + environment[i], -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } - used_environment[len++] = '\0'; + if (0 == MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + environment[i], -1, + &used_environment[len], wide_len)) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + len += SUBPROCESS_CAST(subprocess_size_t, wide_len); } - // End with the two null terminators. - used_environment[len++] = '\0'; - used_environment[len++] = '\0'; + // End with the second null terminator. + used_environment[len++] = 0; } } else { if (SUBPROCESS_NULL != environment) { - return -1; + return subprocess_error_invalid_environment; } } if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } if (!SetHandleInformation(wr, handleFlagInherit, 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, wr), 0); + if (-1 == fd) { + result = subprocess_error_pipe; + goto cleanup; + } + wr = SUBPROCESS_NULL; - if (-1 != fd) { - out_process->stdin_file = _fdopen(fd, "wb"); - - if (SUBPROCESS_NULL == out_process->stdin_file) { - return -1; - } + out_process->stdin_file = _fdopen(fd, "wb"); + if (SUBPROCESS_NULL == out_process->stdin_file) { + _close(fd); + goto cleanup; } startInfo.hStdInput = rd; + rd = SUBPROCESS_NULL; if (options & subprocess_option_enable_async) { if (subprocess_create_named_pipe_helper(&rd, &wr)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } else { if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } if (!SetHandleInformation(rd, handleFlagInherit, 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, rd), 0); + if (-1 == fd) { + result = subprocess_error_pipe; + goto cleanup; + } + rd = SUBPROCESS_NULL; - if (-1 != fd) { - out_process->stdout_file = _fdopen(fd, "rb"); - - if (SUBPROCESS_NULL == out_process->stdout_file) { - return -1; - } + out_process->stdout_file = _fdopen(fd, "rb"); + if (SUBPROCESS_NULL == out_process->stdout_file) { + _close(fd); + goto cleanup; } startInfo.hStdOutput = wr; + wr = SUBPROCESS_NULL; if (subprocess_option_combined_stdout_stderr == (options & subprocess_option_combined_stdout_stderr)) { @@ -617,30 +847,37 @@ int subprocess_create_ex(const char *const commandLine[], int options, } else { if (options & subprocess_option_enable_async) { if (subprocess_create_named_pipe_helper(&rd, &wr)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } else { if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } if (!SetHandleInformation(rd, handleFlagInherit, 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, rd), 0); + if (-1 == fd) { + result = subprocess_error_pipe; + goto cleanup; + } + rd = SUBPROCESS_NULL; - if (-1 != fd) { - out_process->stderr_file = _fdopen(fd, "rb"); - - if (SUBPROCESS_NULL == out_process->stderr_file) { - return -1; - } + out_process->stderr_file = _fdopen(fd, "rb"); + if (SUBPROCESS_NULL == out_process->stderr_file) { + _close(fd); + goto cleanup; } startInfo.hStdError = wr; + wr = SUBPROCESS_NULL; } if (options & subprocess_option_enable_async) { @@ -650,6 +887,13 @@ int subprocess_create_ex(const char *const commandLine[], int options, out_process->hEventError = CreateEventA(SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 1, 1, SUBPROCESS_NULL); + if (!out_process->hEventOutput || !out_process->hEventError) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_no_memory; + } + goto cleanup; + } } else { out_process->hEventOutput = SUBPROCESS_NULL; out_process->hEventError = SUBPROCESS_NULL; @@ -662,7 +906,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, len++; // Quote the argument if it has a space in it - if (strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL) + if (strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL || + commandLine[i][0] == SUBPROCESS_NULL) len += 2; for (j = 0; '\0' != commandLine[i][j]; j++) { @@ -686,7 +931,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined = SUBPROCESS_CAST(char *, _alloca(len)); if (!commandLineCombined) { - return -1; + result = subprocess_error_no_memory; + goto cleanup; } // Gonna re-use len to store the write index into commandLineCombined @@ -697,7 +943,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined[len++] = ' '; } - need_quoting = strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL; + need_quoting = strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL || + commandLine[i][0] == SUBPROCESS_NULL; if (need_quoting) { commandLineCombined[len++] = '"'; } @@ -726,27 +973,93 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined[len] = '\0'; - if (!CreateProcessA( + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + commandLineCombined, -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + commandLineCombinedWide = SUBPROCESS_CAST( + subprocess_wchar_t *, + _alloca(SUBPROCESS_CAST(subprocess_size_t, wide_len) * + sizeof(subprocess_wchar_t))); + if (!commandLineCombinedWide) { + result = subprocess_error_no_memory; + goto cleanup; + } + + if (0 == MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + commandLineCombined, -1, + commandLineCombinedWide, wide_len)) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + if (process_cwd) { + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, process_cwd, + -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + process_cwd_wide = SUBPROCESS_CAST( + subprocess_wchar_t *, + _alloca(SUBPROCESS_CAST(subprocess_size_t, wide_len) * + sizeof(subprocess_wchar_t))); + if (!process_cwd_wide) { + result = subprocess_error_no_memory; + goto cleanup; + } + + if (0 == MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, process_cwd, + -1, process_cwd_wide, wide_len)) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + } + + if (!CreateProcessW( SUBPROCESS_NULL, - commandLineCombined, // command line - SUBPROCESS_NULL, // process security attributes - SUBPROCESS_NULL, // primary thread security attributes - 1, // handles are inherited - flags, // creation flags - used_environment, // used environment - SUBPROCESS_NULL, // use parent's current directory - SUBPROCESS_PTR_CAST(LPSTARTUPINFOA, + commandLineCombinedWide, // command line + SUBPROCESS_NULL, // process security attributes + SUBPROCESS_NULL, // primary thread security attributes + 1, // handles are inherited + flags, // creation flags + used_environment, // used environment + process_cwd_wide, // use specified current directory + SUBPROCESS_PTR_CAST(LPSTARTUPINFOW, &startInfo), // STARTUPINFO pointer SUBPROCESS_PTR_CAST(LPPROCESS_INFORMATION, &processInfo))) { - return -1; + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } out_process->hProcess = processInfo.hProcess; + processInfo.hProcess = SUBPROCESS_NULL; out_process->hStdInput = startInfo.hStdInput; + startInfo.hStdInput = SUBPROCESS_NULL; // We don't need the handle of the primary thread in the called process. CloseHandle(processInfo.hThread); + processInfo.hThread = SUBPROCESS_NULL; if (SUBPROCESS_NULL != startInfo.hStdOutput) { CloseHandle(startInfo.hStdOutput); @@ -754,40 +1067,111 @@ int subprocess_create_ex(const char *const commandLine[], int options, if (startInfo.hStdError != startInfo.hStdOutput) { CloseHandle(startInfo.hStdError); } + + startInfo.hStdOutput = SUBPROCESS_NULL; + startInfo.hStdError = SUBPROCESS_NULL; } out_process->alive = 1; + out_process->no_wait = async_no_wait; return 0; + +cleanup: + last_error = GetLastError(); + + if (subprocess_error_unknown == result) { + result = subprocess_error_from_windows_error(last_error); + } + + if (out_process->stdin_file) { + fclose(out_process->stdin_file); + out_process->stdin_file = SUBPROCESS_NULL; + } + + if (out_process->stdout_file) { + fclose(out_process->stdout_file); + if (out_process->stderr_file && + (out_process->stdout_file != out_process->stderr_file)) { + fclose(out_process->stderr_file); + } + out_process->stdout_file = SUBPROCESS_NULL; + out_process->stderr_file = SUBPROCESS_NULL; + } + + subprocess_close_handle(&rd); + subprocess_close_handle(&wr); + subprocess_close_handle(&startInfo.hStdInput); + + if (startInfo.hStdOutput == startInfo.hStdError) { + subprocess_close_handle(&startInfo.hStdOutput); + startInfo.hStdError = SUBPROCESS_NULL; + } else { + subprocess_close_handle(&startInfo.hStdOutput); + subprocess_close_handle(&startInfo.hStdError); + } + + subprocess_close_handle(&out_process->hEventOutput); + subprocess_close_handle(&out_process->hEventError); + subprocess_close_handle(&processInfo.hThread); + subprocess_close_handle(&processInfo.hProcess); + + SetLastError(last_error); + + return result; #else - int stdinfd[2]; - int stdoutfd[2]; - int stderrfd[2]; - pid_t child; + int stdinfd[2] = {-1, -1}; + int stdoutfd[2] = {-1, -1}; + int stderrfd[2] = {-1, -1}; + int fd, fd_flags; + int async_no_wait; + int actions_created = 0; + int result = subprocess_error_unknown; + int saved_errno = 0; + int posix_error; + pid_t child = 0; extern char **environ; char *const empty_environment[1] = {SUBPROCESS_NULL}; posix_spawn_file_actions_t actions; char *const *used_environment; + async_no_wait = subprocess_option_enable_async_no_wait == + (options & subprocess_option_enable_async_no_wait); + + if (async_no_wait && (subprocess_option_enable_async != + (options & subprocess_option_enable_async))) { + errno = EINVAL; + return subprocess_error_invalid_options; + } + if (subprocess_option_inherit_environment == (options & subprocess_option_inherit_environment)) { if (SUBPROCESS_NULL != environment) { - return -1; + errno = EINVAL; + return subprocess_error_invalid_environment; } } + memset(out_process, 0, sizeof(*out_process)); + if (0 != pipe(stdinfd)) { - return -1; + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; } if (0 != pipe(stdoutfd)) { - return -1; + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; } if (subprocess_option_combined_stdout_stderr != (options & subprocess_option_combined_stdout_stderr)) { if (0 != pipe(stderrfd)) { - return -1; + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; } } @@ -797,7 +1181,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, #pragma clang diagnostic ignored "-Wcast-qual" #pragma clang diagnostic ignored "-Wold-style-cast" #endif - used_environment = (char *const *)environment; + used_environment = SUBPROCESS_CONST_CAST(char *const *, environment); #ifdef __clang__ #pragma clang diagnostic pop #endif @@ -808,54 +1192,120 @@ int subprocess_create_ex(const char *const commandLine[], int options, used_environment = empty_environment; } - if (0 != posix_spawn_file_actions_init(&actions)) { - return -1; + posix_error = posix_spawn_file_actions_init(&actions); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + actions_created = 1; + + // Set working directory + if (process_cwd) { +#if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000 + posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd); +#else +#if defined(__APPLE__) && defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + posix_error = posix_spawn_file_actions_addchdir_np(&actions, process_cwd); +#if defined(__APPLE__) && defined(__clang__) +#pragma clang diagnostic pop +#endif +#endif + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } } // Close the stdin write end - if (0 != posix_spawn_file_actions_addclose(&actions, stdinfd[1])) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_addclose(&actions, stdinfd[1]); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Map the read end to stdin - if (0 != - posix_spawn_file_actions_adddup2(&actions, stdinfd[0], STDIN_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = + posix_spawn_file_actions_adddup2(&actions, stdinfd[0], STDIN_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Close the stdout read end - if (0 != posix_spawn_file_actions_addclose(&actions, stdoutfd[0])) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_addclose(&actions, stdoutfd[0]); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Map the write end to stdout - if (0 != - posix_spawn_file_actions_adddup2(&actions, stdoutfd[1], STDOUT_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = + posix_spawn_file_actions_adddup2(&actions, stdoutfd[1], STDOUT_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } if (subprocess_option_combined_stdout_stderr == (options & subprocess_option_combined_stdout_stderr)) { - if (0 != posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, - STDERR_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, + STDERR_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } else { // Close the stderr read end - if (0 != posix_spawn_file_actions_addclose(&actions, stderrfd[0])) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_addclose(&actions, stderrfd[0]); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Map the write end to stdout - if (0 != posix_spawn_file_actions_adddup2(&actions, stderrfd[1], - STDERR_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_adddup2(&actions, stderrfd[1], + STDERR_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } @@ -866,16 +1316,30 @@ int subprocess_create_ex(const char *const commandLine[], int options, #endif if (subprocess_option_search_user_path == (options & subprocess_option_search_user_path)) { - if (0 != posix_spawnp(&child, commandLine[0], &actions, SUBPROCESS_NULL, - (char *const *)commandLine, used_environment)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawnp(&child, commandLine[0], &actions, + SUBPROCESS_NULL, + SUBPROCESS_CONST_CAST(char *const *, commandLine), + used_environment); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } else { - if (0 != posix_spawn(&child, commandLine[0], &actions, SUBPROCESS_NULL, - (char *const *)commandLine, used_environment)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn(&child, commandLine[0], &actions, + SUBPROCESS_NULL, + SUBPROCESS_CONST_CAST(char *const *, commandLine), + used_environment); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } #ifdef __clang__ @@ -884,13 +1348,34 @@ int subprocess_create_ex(const char *const commandLine[], int options, // Close the stdin read end close(stdinfd[0]); + stdinfd[0] = -1; // Store the stdin write end out_process->stdin_file = fdopen(stdinfd[1], "wb"); + if (SUBPROCESS_NULL == out_process->stdin_file) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + goto cleanup; + } + stdinfd[1] = -1; // Close the stdout write end close(stdoutfd[1]); + stdoutfd[1] = -1; // Store the stdout read end out_process->stdout_file = fdopen(stdoutfd[0], "rb"); + if (SUBPROCESS_NULL == out_process->stdout_file) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + goto cleanup; + } + stdoutfd[0] = -1; + + // Set non blocking if we are async and asked not to wait. + if (async_no_wait) { + fd = fileno(out_process->stdout_file); + fd_flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK); + } if (subprocess_option_combined_stdout_stderr == (options & subprocess_option_combined_stdout_stderr)) { @@ -898,17 +1383,92 @@ int subprocess_create_ex(const char *const commandLine[], int options, } else { // Close the stderr write end close(stderrfd[1]); + stderrfd[1] = -1; // Store the stderr read end out_process->stderr_file = fdopen(stderrfd[0], "rb"); + if (SUBPROCESS_NULL == out_process->stderr_file) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + goto cleanup; + } + stderrfd[0] = -1; + + // Set non blocking if we are async and asked not to wait. + if (async_no_wait) { + fd = fileno(out_process->stderr_file); + fd_flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK); + } } // Store the child's pid out_process->child = child; + child = 0; out_process->alive = 1; + out_process->no_wait = async_no_wait; - posix_spawn_file_actions_destroy(&actions); - return 0; + result = 0; + +cleanup: + if ((0 != result) && (0 == saved_errno)) { + saved_errno = errno; + } + + if ((subprocess_error_unknown == result) && (0 != saved_errno)) { + result = subprocess_error_from_errno(saved_errno); + } + + if (actions_created) { + posix_spawn_file_actions_destroy(&actions); + } + + if (0 != result) { + if (child) { + kill(child, 9); + waitpid(child, SUBPROCESS_NULL, 0); + } + + if (out_process->stdin_file) { + fclose(out_process->stdin_file); + out_process->stdin_file = SUBPROCESS_NULL; + } + + if (out_process->stdout_file) { + fclose(out_process->stdout_file); + if (out_process->stderr_file && + (out_process->stdout_file != out_process->stderr_file)) { + fclose(out_process->stderr_file); + } + out_process->stdout_file = SUBPROCESS_NULL; + out_process->stderr_file = SUBPROCESS_NULL; + } + } + + if (-1 != stdinfd[0]) { + close(stdinfd[0]); + } + if (-1 != stdinfd[1]) { + close(stdinfd[1]); + } + if (-1 != stdoutfd[0]) { + close(stdoutfd[0]); + } + if (-1 != stdoutfd[1]) { + close(stdoutfd[1]); + } + if (-1 != stderrfd[0]) { + close(stderrfd[0]); + } + if (-1 != stderrfd[1]) { + close(stderrfd[1]); + } + + if ((0 != result) && (0 != saved_errno)) { + errno = saved_errno; + } + + return result; #endif } @@ -1049,6 +1609,7 @@ unsigned subprocess_read_stdout(struct subprocess_s *const process, char *const buffer, unsigned size) { #if defined(_WIN32) void *handle; + unsigned long bytes_available = 0; unsigned long bytes_read = 0; struct subprocess_overlapped_s overlapped = {0, 0, {{0, 0}}, SUBPROCESS_NULL}; overlapped.hEvent = process->hEventOutput; @@ -1056,6 +1617,17 @@ unsigned subprocess_read_stdout(struct subprocess_s *const process, handle = SUBPROCESS_PTR_CAST(void *, _get_osfhandle(_fileno(process->stdout_file))); + if (process->no_wait) { + if (!PeekNamedPipe(handle, SUBPROCESS_NULL, 0, SUBPROCESS_NULL, + &bytes_available, SUBPROCESS_NULL)) { + return 0; + } + + if (0 == bytes_available) { + return 0; + } + } + if (!ReadFile(handle, buffer, size, &bytes_read, SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped))) { const unsigned long errorIoPending = 997; @@ -1063,14 +1635,20 @@ unsigned subprocess_read_stdout(struct subprocess_s *const process, // Means we've got an async read! if (error == errorIoPending) { + const uintptr_t statusPending = 0x00000103; + + const int wait = statusPending == overlapped.Internal; + if (!GetOverlappedResult(handle, SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped), - &bytes_read, 1)) { - const unsigned long errorIoIncomplete = 996; + &bytes_read, wait)) { const unsigned long errorHandleEOF = 38; + const unsigned long errorBrokenPipe = 109; + const unsigned long errorIoIncomplete = 996; error = GetLastError(); - if ((error != errorIoIncomplete) && (error != errorHandleEOF)) { + if ((errorHandleEOF != error) && (errorBrokenPipe != error) && + (errorIoIncomplete != error)) { return 0; } } @@ -1094,6 +1672,7 @@ unsigned subprocess_read_stderr(struct subprocess_s *const process, char *const buffer, unsigned size) { #if defined(_WIN32) void *handle; + unsigned long bytes_available = 0; unsigned long bytes_read = 0; struct subprocess_overlapped_s overlapped = {0, 0, {{0, 0}}, SUBPROCESS_NULL}; overlapped.hEvent = process->hEventError; @@ -1101,6 +1680,17 @@ unsigned subprocess_read_stderr(struct subprocess_s *const process, handle = SUBPROCESS_PTR_CAST(void *, _get_osfhandle(_fileno(process->stderr_file))); + if (process->no_wait) { + if (!PeekNamedPipe(handle, SUBPROCESS_NULL, 0, SUBPROCESS_NULL, + &bytes_available, SUBPROCESS_NULL)) { + return 0; + } + + if (0 == bytes_available) { + return 0; + } + } + if (!ReadFile(handle, buffer, size, &bytes_read, SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped))) { const unsigned long errorIoPending = 997; @@ -1179,6 +1769,12 @@ int subprocess_alive(struct subprocess_s *const process) { return is_alive; } +#if defined(__clang__) +#if __has_warning("-Wunsafe-buffer-usage") +#pragma clang diagnostic pop +#endif +#endif + #if defined(__cplusplus) } // extern "C" #endif diff --git a/utest.h b/utest.h index f1c7575..5341a59 100644 --- a/utest.h +++ b/utest.h @@ -1217,20 +1217,28 @@ utest_strncpy_gcc(char *const dst, const char *const src, const size_t size) { #if defined(__clang__) #if __has_warning("-Wunsafe-buffer-usage-in-libc-call") -#define UTEST_SURPRESS_WARNINGS_BEGIN \ - _Pragma("clang diagnostic push") \ - _Pragma("clang diagnostic ignored \"-Wunsafe-buffer-usage\"") \ - _Pragma("clang diagnostic ignored \"-Wunsafe-buffer-usage-in-libc-call\"") -#define UTEST_SURPRESS_WARNINGS_END _Pragma("clang diagnostic pop") +#define UTEST_SURPRESS_UNSAFE_BUFFER_USAGE \ + _Pragma("clang diagnostic ignored \"-Wunsafe-buffer-usage\"") \ + _Pragma("clang diagnostic ignored \"-Wunsafe-buffer-usage-in-libc-call\"") #elif __has_warning("-Wunsafe-buffer-usage") +#define UTEST_SURPRESS_UNSAFE_BUFFER_USAGE \ + _Pragma("clang diagnostic ignored \"-Wunsafe-buffer-usage\"") +#else +#define UTEST_SURPRESS_UNSAFE_BUFFER_USAGE +#endif + +#if __has_warning("-Wglobal-constructors") +#define UTEST_SURPRESS_GLOBAL_CONSTRUCTORS \ + _Pragma("clang diagnostic ignored \"-Wglobal-constructors\"") +#else +#define UTEST_SURPRESS_GLOBAL_CONSTRUCTORS +#endif + #define UTEST_SURPRESS_WARNINGS_BEGIN \ _Pragma("clang diagnostic push") \ - _Pragma("clang diagnostic ignored \"-Wunsafe-buffer-usage\"") + UTEST_SURPRESS_UNSAFE_BUFFER_USAGE \ + UTEST_SURPRESS_GLOBAL_CONSTRUCTORS #define UTEST_SURPRESS_WARNINGS_END _Pragma("clang diagnostic pop") -#else -#define UTEST_SURPRESS_WARNINGS_BEGIN -#define UTEST_SURPRESS_WARNINGS_END -#endif #elif defined(__GNUC__) && __GNUC__ >= 8 && defined(__cplusplus) #define UTEST_SURPRESS_WARNINGS_BEGIN \ _Pragma("GCC diagnostic push") \