| /* |
| Simple DirectMedia Layer |
| Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
| |
| This software is provided 'as-is', without any express or implied |
| warranty. In no event will the authors be held liable for any damages |
| arising from the use of this software. |
| |
| Permission is granted to anyone to use this software for any purpose, |
| including commercial applications, and to alter it and redistribute it |
| freely, subject to the following restrictions: |
| |
| 1. The origin of this software must not be misrepresented; you must not |
| claim that you wrote the original software. If you use this software |
| in a product, an acknowledgment in the product documentation would be |
| appreciated but is not required. |
| 2. Altered source versions must be plainly marked as such, and must not be |
| misrepresented as being the original software. |
| 3. This notice may not be removed or altered from any source distribution. |
| */ |
| #include "SDL_internal.h" |
| |
| #ifdef SDL_PROCESS_WINDOWS |
| |
| #include "../../core/windows/SDL_windows.h" |
| #include "../SDL_sysprocess.h" |
| #include "../../io/SDL_iostream_c.h" |
| |
| #define READ_END 0 |
| #define WRITE_END 1 |
| |
| struct SDL_ProcessData { |
| PROCESS_INFORMATION process_information; |
| }; |
| |
| static void CleanupStream(void *userdata, void *value) |
| { |
| SDL_Process *process = (SDL_Process *)value; |
| const char *property = (const char *)userdata; |
| |
| SDL_ClearProperty(process->props, property); |
| } |
| |
| static bool SetupStream(SDL_Process *process, HANDLE handle, const char *mode, const char *property) |
| { |
| SDL_IOStream *io = SDL_IOFromHandle(handle, mode, true); |
| if (!io) { |
| return false; |
| } |
| |
| SDL_SetPointerPropertyWithCleanup(SDL_GetIOProperties(io), "SDL.internal.process", process, CleanupStream, (void *)property); |
| SDL_SetPointerProperty(process->props, property, io); |
| return true; |
| } |
| |
| static bool SetupRedirect(SDL_PropertiesID props, const char *property, HANDLE *result) |
| { |
| SDL_IOStream *io = (SDL_IOStream *)SDL_GetPointerProperty(props, property, NULL); |
| if (!io) { |
| SDL_SetError("%s is not set", property); |
| return false; |
| } |
| |
| HANDLE handle = (HANDLE)SDL_GetPointerProperty(SDL_GetIOProperties(io), SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER, INVALID_HANDLE_VALUE); |
| if (handle == INVALID_HANDLE_VALUE) { |
| SDL_SetError("%s doesn't have SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER available", property); |
| return false; |
| } |
| |
| if (!DuplicateHandle(GetCurrentProcess(), handle, |
| GetCurrentProcess(), result, |
| 0, TRUE, DUPLICATE_SAME_ACCESS)) { |
| WIN_SetError("DuplicateHandle()"); |
| return false; |
| } |
| |
| if (GetFileType(*result) == FILE_TYPE_PIPE) { |
| DWORD wait_mode = PIPE_WAIT; |
| if (!SetNamedPipeHandleState(*result, &wait_mode, NULL, NULL)) { |
| WIN_SetError("SetNamedPipeHandleState()"); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| static bool is_batch_file_path(const char *path) { |
| size_t len_path = SDL_strlen(path); |
| if (len_path < 4) { |
| return false; |
| } |
| if (SDL_strcasecmp(path + len_path - 4, ".bat") == 0 || SDL_strcasecmp(path + len_path - 4, ".cmd") == 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| static bool join_arguments(const char * const *args, LPWSTR *args_out) |
| { |
| size_t len; |
| int i; |
| size_t i_out; |
| char *result; |
| bool batch_file = is_batch_file_path(args[0]); |
| |
| len = 0; |
| for (i = 0; args[i]; i++) { |
| const char *a = args[i]; |
| bool quotes = *a == '\0' || SDL_strpbrk(a, " \r\n\t\v") != NULL; |
| |
| if (quotes) { |
| /* surround the argument with double quote if it is empty or contains whitespaces */ |
| len += 2; |
| } |
| |
| for (; *a; a++) { |
| switch (*a) { |
| case '"': |
| len += 2; |
| break; |
| case '\\': |
| /* only escape backslashes that precede a double quote (including the enclosing double quote) */ |
| len += (a[1] == '"' || (quotes && a[1] == '\0')) ? 2 : 1; |
| break; |
| case ' ': |
| case '^': |
| case '&': |
| case '|': |
| case '<': |
| case '>': |
| if (batch_file) { |
| len += 2; |
| } else { |
| len += 1; |
| } |
| break; |
| default: |
| len += 1; |
| break; |
| } |
| } |
| /* space separator or final '\0' */ |
| len += 1; |
| } |
| |
| result = SDL_malloc(len); |
| if (!result) { |
| *args_out = NULL; |
| return false; |
| } |
| |
| i_out = 0; |
| for (i = 0; args[i]; i++) { |
| const char *a = args[i]; |
| bool quotes = *a == '\0' || SDL_strpbrk(a, " \r\n\t\v") != NULL; |
| |
| if (quotes) { |
| result[i_out++] = '"'; |
| } |
| for (; *a; a++) { |
| switch (*a) { |
| case '"': |
| if (batch_file) { |
| result[i_out++] = '"'; |
| } else { |
| result[i_out++] = '\\'; |
| } |
| result[i_out++] = *a; |
| break; |
| case '\\': |
| result[i_out++] = *a; |
| if (a[1] == '"' || (quotes && a[1] == '\0')) { |
| result[i_out++] = *a; |
| } |
| break; |
| case ' ': |
| if (batch_file) { |
| result[i_out++] = '^'; |
| } |
| result[i_out++] = *a; |
| break; |
| case '^': |
| case '&': |
| case '|': |
| case '<': |
| case '>': |
| if (batch_file) { |
| result[i_out++] = '^'; |
| } |
| result[i_out++] = *a; |
| break; |
| default: |
| result[i_out++] = *a; |
| break; |
| } |
| } |
| if (quotes) { |
| result[i_out++] = '"'; |
| } |
| result[i_out++] = ' '; |
| } |
| SDL_assert(i_out == len); |
| result[len - 1] = '\0'; |
| |
| *args_out = (LPWSTR)SDL_iconv_string("UTF-16LE", "UTF-8", (const char *)result, len); |
| SDL_free(result); |
| if (!args_out) { |
| return false; |
| } |
| return true; |
| } |
| |
| static bool join_env(char **env, LPWSTR *env_out) |
| { |
| size_t len; |
| char **var; |
| char *result; |
| |
| len = 0; |
| for (var = env; *var; var++) { |
| len += SDL_strlen(*var) + 1; |
| } |
| result = SDL_malloc(len + 1); |
| if (!result) { |
| return false; |
| } |
| |
| len = 0; |
| for (var = env; *var; var++) { |
| size_t l = SDL_strlen(*var); |
| SDL_memcpy(result + len, *var, l); |
| result[len + l] = '\0'; |
| len += l + 1; |
| } |
| result[len] = '\0'; |
| |
| *env_out = (LPWSTR)SDL_iconv_string("UTF-16LE", "UTF-8", (const char *)result, len); |
| SDL_free(result); |
| if (!*env_out) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID props) |
| { |
| const char * const *args = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, NULL); |
| const char *cmdline = SDL_GetStringProperty(props, SDL_PROP_PROCESS_CREATE_CMDLINE_STRING, NULL); |
| SDL_Environment *env = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, SDL_GetEnvironment()); |
| char **envp = NULL; |
| const char *working_directory = SDL_GetStringProperty(props, SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING, NULL); |
| SDL_ProcessIO stdin_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL); |
| SDL_ProcessIO stdout_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_INHERITED); |
| SDL_ProcessIO stderr_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_INHERITED); |
| bool redirect_stderr = SDL_GetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_TO_STDOUT_BOOLEAN, false) && |
| !SDL_HasProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER); |
| LPWSTR createprocess_cmdline = NULL; |
| LPWSTR createprocess_env = NULL; |
| LPWSTR createprocess_cwd = NULL; |
| STARTUPINFOW startup_info; |
| DWORD creation_flags; |
| SECURITY_ATTRIBUTES security_attributes; |
| HANDLE stdin_pipe[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE }; |
| HANDLE stdout_pipe[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE }; |
| HANDLE stderr_pipe[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE }; |
| DWORD pipe_mode = PIPE_NOWAIT; |
| bool result = false; |
| |
| // Keep the malloc() before exec() so that an OOM won't run a process at all |
| envp = SDL_GetEnvironmentVariables(env); |
| if (!envp) { |
| return false; |
| } |
| |
| SDL_ProcessData *data = SDL_calloc(1, sizeof(*data)); |
| if (!data) { |
| SDL_free(envp); |
| return false; |
| } |
| process->internal = data; |
| data->process_information.hProcess = INVALID_HANDLE_VALUE; |
| data->process_information.hThread = INVALID_HANDLE_VALUE; |
| |
| creation_flags = CREATE_UNICODE_ENVIRONMENT; |
| |
| SDL_zero(startup_info); |
| startup_info.cb = sizeof(startup_info); |
| startup_info.dwFlags |= STARTF_USESTDHANDLES; |
| startup_info.hStdInput = INVALID_HANDLE_VALUE; |
| startup_info.hStdOutput = INVALID_HANDLE_VALUE; |
| startup_info.hStdError = INVALID_HANDLE_VALUE; |
| |
| SDL_zero(security_attributes); |
| security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES); |
| security_attributes.bInheritHandle = TRUE; |
| security_attributes.lpSecurityDescriptor = NULL; |
| |
| if (cmdline) { |
| createprocess_cmdline = WIN_UTF8ToString(cmdline); |
| if (!createprocess_cmdline) { |
| goto done; |
| } |
| } else if (!join_arguments(args, &createprocess_cmdline)) { |
| goto done; |
| } |
| |
| if (!join_env(envp, &createprocess_env)) { |
| goto done; |
| } |
| |
| if (working_directory) { |
| createprocess_cwd = WIN_UTF8ToStringW(working_directory); |
| if (!createprocess_cwd) { |
| goto done; |
| } |
| } |
| |
| // Background processes don't have access to the terminal |
| // This isn't necessary on Windows, but we keep the same behavior as the POSIX implementation. |
| if (process->background) { |
| if (stdin_option == SDL_PROCESS_STDIO_INHERITED) { |
| stdin_option = SDL_PROCESS_STDIO_NULL; |
| } |
| if (stdout_option == SDL_PROCESS_STDIO_INHERITED) { |
| stdout_option = SDL_PROCESS_STDIO_NULL; |
| } |
| if (stderr_option == SDL_PROCESS_STDIO_INHERITED) { |
| stderr_option = SDL_PROCESS_STDIO_NULL; |
| } |
| creation_flags |= CREATE_NO_WINDOW; |
| } |
| |
| switch (stdin_option) { |
| case SDL_PROCESS_STDIO_REDIRECT: |
| if (!SetupRedirect(props, SDL_PROP_PROCESS_CREATE_STDIN_POINTER, &startup_info.hStdInput)) { |
| goto done; |
| } |
| break; |
| case SDL_PROCESS_STDIO_APP: |
| if (!CreatePipe(&stdin_pipe[READ_END], &stdin_pipe[WRITE_END], &security_attributes, 0)) { |
| stdin_pipe[READ_END] = INVALID_HANDLE_VALUE; |
| stdin_pipe[WRITE_END] = INVALID_HANDLE_VALUE; |
| goto done; |
| } |
| if (!SetNamedPipeHandleState(stdin_pipe[WRITE_END], &pipe_mode, NULL, NULL)) { |
| WIN_SetError("SetNamedPipeHandleState()"); |
| goto done; |
| } |
| if (!SetHandleInformation(stdin_pipe[WRITE_END], HANDLE_FLAG_INHERIT, 0) ) { |
| WIN_SetError("SetHandleInformation()"); |
| goto done; |
| } |
| startup_info.hStdInput = stdin_pipe[READ_END]; |
| break; |
| case SDL_PROCESS_STDIO_NULL: |
| startup_info.hStdInput = CreateFile(TEXT("\\\\.\\NUL"), (GENERIC_READ | GENERIC_WRITE), 0, &security_attributes, OPEN_EXISTING, 0, NULL); |
| break; |
| case SDL_PROCESS_STDIO_INHERITED: |
| default: |
| if (!DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_INPUT_HANDLE), |
| GetCurrentProcess(), &startup_info.hStdInput, |
| 0, TRUE, DUPLICATE_SAME_ACCESS)) { |
| startup_info.hStdInput = INVALID_HANDLE_VALUE; |
| WIN_SetError("DuplicateHandle()"); |
| goto done; |
| } |
| break; |
| } |
| |
| switch (stdout_option) { |
| case SDL_PROCESS_STDIO_REDIRECT: |
| if (!SetupRedirect(props, SDL_PROP_PROCESS_CREATE_STDOUT_POINTER, &startup_info.hStdOutput)) { |
| goto done; |
| } |
| break; |
| case SDL_PROCESS_STDIO_APP: |
| if (!CreatePipe(&stdout_pipe[READ_END], &stdout_pipe[WRITE_END], &security_attributes, 0)) { |
| stdout_pipe[READ_END] = INVALID_HANDLE_VALUE; |
| stdout_pipe[WRITE_END] = INVALID_HANDLE_VALUE; |
| goto done; |
| } |
| if (!SetNamedPipeHandleState(stdout_pipe[READ_END], &pipe_mode, NULL, NULL)) { |
| WIN_SetError("SetNamedPipeHandleState()"); |
| goto done; |
| } |
| if (!SetHandleInformation(stdout_pipe[READ_END], HANDLE_FLAG_INHERIT, 0) ) { |
| WIN_SetError("SetHandleInformation()"); |
| goto done; |
| } |
| startup_info.hStdOutput = stdout_pipe[WRITE_END]; |
| break; |
| case SDL_PROCESS_STDIO_NULL: |
| startup_info.hStdOutput = CreateFile(TEXT("\\\\.\\NUL"), (GENERIC_READ | GENERIC_WRITE), 0, &security_attributes, OPEN_EXISTING, 0, NULL); |
| break; |
| case SDL_PROCESS_STDIO_INHERITED: |
| default: |
| if (!DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_OUTPUT_HANDLE), |
| GetCurrentProcess(), &startup_info.hStdOutput, |
| 0, TRUE, DUPLICATE_SAME_ACCESS)) { |
| startup_info.hStdOutput = INVALID_HANDLE_VALUE; |
| WIN_SetError("DuplicateHandle()"); |
| goto done; |
| } |
| break; |
| } |
| |
| if (redirect_stderr) { |
| if (!DuplicateHandle(GetCurrentProcess(), startup_info.hStdOutput, |
| GetCurrentProcess(), &startup_info.hStdError, |
| 0, TRUE, DUPLICATE_SAME_ACCESS)) { |
| startup_info.hStdError = INVALID_HANDLE_VALUE; |
| WIN_SetError("DuplicateHandle()"); |
| goto done; |
| } |
| } else { |
| switch (stderr_option) { |
| case SDL_PROCESS_STDIO_REDIRECT: |
| if (!SetupRedirect(props, SDL_PROP_PROCESS_CREATE_STDERR_POINTER, &startup_info.hStdError)) { |
| goto done; |
| } |
| break; |
| case SDL_PROCESS_STDIO_APP: |
| if (!CreatePipe(&stderr_pipe[READ_END], &stderr_pipe[WRITE_END], &security_attributes, 0)) { |
| stderr_pipe[READ_END] = INVALID_HANDLE_VALUE; |
| stderr_pipe[WRITE_END] = INVALID_HANDLE_VALUE; |
| goto done; |
| } |
| if (!SetNamedPipeHandleState(stderr_pipe[READ_END], &pipe_mode, NULL, NULL)) { |
| WIN_SetError("SetNamedPipeHandleState()"); |
| goto done; |
| } |
| if (!SetHandleInformation(stderr_pipe[READ_END], HANDLE_FLAG_INHERIT, 0) ) { |
| WIN_SetError("SetHandleInformation()"); |
| goto done; |
| } |
| startup_info.hStdError = stderr_pipe[WRITE_END]; |
| break; |
| case SDL_PROCESS_STDIO_NULL: |
| startup_info.hStdError = CreateFile(TEXT("\\\\.\\NUL"), (GENERIC_READ | GENERIC_WRITE), 0, &security_attributes, OPEN_EXISTING, 0, NULL); |
| break; |
| case SDL_PROCESS_STDIO_INHERITED: |
| default: |
| if (!DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), |
| GetCurrentProcess(), &startup_info.hStdError, |
| 0, TRUE, DUPLICATE_SAME_ACCESS)) { |
| startup_info.hStdError = INVALID_HANDLE_VALUE; |
| WIN_SetError("DuplicateHandle()"); |
| goto done; |
| } |
| break; |
| } |
| } |
| |
| if (!CreateProcessW(NULL, createprocess_cmdline, NULL, NULL, TRUE, creation_flags, createprocess_env, createprocess_cwd, &startup_info, &data->process_information)) { |
| WIN_SetError("CreateProcess"); |
| goto done; |
| } |
| |
| SDL_SetNumberProperty(process->props, SDL_PROP_PROCESS_PID_NUMBER, data->process_information.dwProcessId); |
| |
| if (stdin_option == SDL_PROCESS_STDIO_APP) { |
| if (!SetupStream(process, stdin_pipe[WRITE_END], "wb", SDL_PROP_PROCESS_STDIN_POINTER)) { |
| CloseHandle(stdin_pipe[WRITE_END]); |
| stdin_pipe[WRITE_END] = INVALID_HANDLE_VALUE; |
| } |
| } |
| if (stdout_option == SDL_PROCESS_STDIO_APP) { |
| if (!SetupStream(process, stdout_pipe[READ_END], "rb", SDL_PROP_PROCESS_STDOUT_POINTER)) { |
| CloseHandle(stdout_pipe[READ_END]); |
| stdout_pipe[READ_END] = INVALID_HANDLE_VALUE; |
| } |
| } |
| if (stderr_option == SDL_PROCESS_STDIO_APP) { |
| if (!SetupStream(process, stderr_pipe[READ_END], "rb", SDL_PROP_PROCESS_STDERR_POINTER)) { |
| CloseHandle(stderr_pipe[READ_END]); |
| stderr_pipe[READ_END] = INVALID_HANDLE_VALUE; |
| } |
| } |
| |
| result = true; |
| |
| done: |
| if (startup_info.hStdInput != INVALID_HANDLE_VALUE && |
| startup_info.hStdInput != stdin_pipe[READ_END]) { |
| CloseHandle(startup_info.hStdInput); |
| } |
| if (startup_info.hStdOutput != INVALID_HANDLE_VALUE && |
| startup_info.hStdOutput != stdout_pipe[WRITE_END]) { |
| CloseHandle(startup_info.hStdOutput); |
| } |
| if (startup_info.hStdError != INVALID_HANDLE_VALUE && |
| startup_info.hStdError != stderr_pipe[WRITE_END]) { |
| CloseHandle(startup_info.hStdError); |
| } |
| if (stdin_pipe[READ_END] != INVALID_HANDLE_VALUE) { |
| CloseHandle(stdin_pipe[READ_END]); |
| } |
| if (stdout_pipe[WRITE_END] != INVALID_HANDLE_VALUE) { |
| CloseHandle(stdout_pipe[WRITE_END]); |
| } |
| if (stderr_pipe[WRITE_END] != INVALID_HANDLE_VALUE) { |
| CloseHandle(stderr_pipe[WRITE_END]); |
| } |
| SDL_free(createprocess_cmdline); |
| SDL_free(createprocess_env); |
| SDL_free(createprocess_cwd); |
| SDL_free(envp); |
| |
| if (!result) { |
| if (stdin_pipe[WRITE_END] != INVALID_HANDLE_VALUE) { |
| CloseHandle(stdin_pipe[WRITE_END]); |
| } |
| if (stdout_pipe[READ_END] != INVALID_HANDLE_VALUE) { |
| CloseHandle(stdout_pipe[READ_END]); |
| } |
| if (stderr_pipe[READ_END] != INVALID_HANDLE_VALUE) { |
| CloseHandle(stderr_pipe[READ_END]); |
| } |
| } |
| return result; |
| } |
| |
| static BOOL CALLBACK terminate_app(HWND hwnd, LPARAM lparam) |
| { |
| DWORD current_proc_id = 0, *term_info = (DWORD *) lparam; |
| GetWindowThreadProcessId(hwnd, ¤t_proc_id); |
| if (current_proc_id == term_info[0] && PostMessage(hwnd, WM_CLOSE, 0, 0)) { |
| term_info[1]++; |
| } |
| return TRUE; |
| } |
| |
| bool SDL_SYS_KillProcess(SDL_Process *process, bool force) |
| { |
| if (!force) { |
| // term_info[0] is the process ID, term_info[1] is number of successful tries |
| DWORD term_info[2]; |
| term_info[0] = process->internal->process_information.dwProcessId; |
| term_info[1] = 0; |
| EnumWindows(terminate_app, (LPARAM) &term_info); |
| if (term_info[1] || PostThreadMessage(process->internal->process_information.dwThreadId, WM_CLOSE, 0, 0)) { |
| return true; |
| } |
| if (GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, term_info[0])) { |
| return true; |
| } |
| } |
| if (!TerminateProcess(process->internal->process_information.hProcess, 1)) { |
| return WIN_SetError("TerminateProcess failed"); |
| } |
| return true; |
| } |
| |
| bool SDL_SYS_WaitProcess(SDL_Process *process, bool block, int *exitcode) |
| { |
| DWORD result; |
| |
| result = WaitForSingleObject(process->internal->process_information.hProcess, block ? INFINITE : 0); |
| |
| if (result == WAIT_OBJECT_0) { |
| DWORD rc; |
| if (!GetExitCodeProcess(process->internal->process_information.hProcess, &rc)) { |
| return WIN_SetError("GetExitCodeProcess"); |
| } |
| if (exitcode) { |
| *exitcode = (int)rc; |
| } |
| return true; |
| } else if (result == WAIT_FAILED) { |
| return WIN_SetError("WaitForSingleObject(hProcess) returned WAIT_FAILED"); |
| } else { |
| SDL_ClearError(); |
| return false; |
| } |
| } |
| |
| void SDL_SYS_DestroyProcess(SDL_Process *process) |
| { |
| SDL_ProcessData *data = process->internal; |
| SDL_IOStream *io; |
| |
| io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDIN_POINTER, NULL); |
| if (io) { |
| SDL_CloseIO(io); |
| } |
| io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDERR_POINTER, NULL); |
| if (io) { |
| SDL_CloseIO(io); |
| } |
| io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDOUT_POINTER, NULL); |
| if (io) { |
| SDL_CloseIO(io); |
| } |
| if (data) { |
| if (data->process_information.hThread != INVALID_HANDLE_VALUE) { |
| CloseHandle(data->process_information.hThread); |
| } |
| if (data->process_information.hProcess != INVALID_HANDLE_VALUE) { |
| CloseHandle(data->process_information.hProcess); |
| } |
| } |
| SDL_free(data); |
| } |
| |
| #endif // SDL_PROCESS_WINDOWS |