From ef0d1cddb6d1719b60933fd1dc00f938c095c00c Mon Sep 17 00:00:00 2001 From: James Ross-Gowan Date: Sat, 22 Nov 2014 17:21:33 +1100 Subject: lua: subprocess: move to osdep/subprocess-{win,posix}.c The subprocess code was already split into fairly general functions, separate from the Lua code. It's getting pretty big though, especially the Windows-specific parts, so move it into its own files. --- old-makefile | 1 + osdep/subprocess-posix.c | 146 ++++++++++++++ osdep/subprocess-win.c | 371 ++++++++++++++++++++++++++++++++++++ osdep/subprocess.h | 30 +++ player/lua.c | 481 +---------------------------------------------- wscript_build.py | 2 + 6 files changed, 553 insertions(+), 478 deletions(-) create mode 100644 osdep/subprocess-posix.c create mode 100644 osdep/subprocess-win.c create mode 100644 osdep/subprocess.h diff --git a/old-makefile b/old-makefile index aa73c41050..7408b3c089 100644 --- a/old-makefile +++ b/old-makefile @@ -193,6 +193,7 @@ SOURCES = audio/audio.c \ osdep/io.c \ osdep/numcores.c \ osdep/semaphore_osx.c \ + osdep/subprocess-posix.c \ osdep/terminal-unix.c \ osdep/timer.c \ osdep/timer-linux.c \ diff --git a/osdep/subprocess-posix.c b/osdep/subprocess-posix.c new file mode 100644 index 0000000000..8dcc5f6304 --- /dev/null +++ b/osdep/subprocess-posix.c @@ -0,0 +1,146 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see . + */ + +#include "osdep/subprocess.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "osdep/io.h" +#include "common/common.h" + +// Normally, this must be declared manually, but glibc is retarded. +#ifndef __GLIBC__ +extern char **environ; +#endif + +// A silly helper: automatically skips entries with negative FDs +static int sparse_poll(struct pollfd *fds, int num_fds, int timeout) +{ + struct pollfd p_fds[10]; + int map[10]; + if (num_fds > MP_ARRAY_SIZE(p_fds)) + return -1; + int p_num_fds = 0; + for (int n = 0; n < num_fds; n++) { + map[n] = -1; + if (fds[n].fd < 0) + continue; + map[n] = p_num_fds; + p_fds[p_num_fds++] = fds[n]; + } + int r = poll(p_fds, p_num_fds, timeout); + for (int n = 0; n < num_fds; n++) + fds[n].revents = (map[n] < 0 && r >= 0) ? 0 : p_fds[map[n]].revents; + return r; +} + +int mp_subprocess(char **args, struct mp_cancel *cancel, void *ctx, + subprocess_read_cb on_stdout, subprocess_read_cb on_stderr, + char **error) +{ + posix_spawn_file_actions_t fa; + bool fa_destroy = false; + int status = -1; + int p_stdout[2] = {-1, -1}; + int p_stderr[2] = {-1, -1}; + pid_t pid = -1; + + if (mp_make_cloexec_pipe(p_stdout) < 0) + goto done; + if (mp_make_cloexec_pipe(p_stderr) < 0) + goto done; + + if (posix_spawn_file_actions_init(&fa)) + goto done; + fa_destroy = true; + // redirect stdout and stderr + if (posix_spawn_file_actions_adddup2(&fa, p_stdout[1], 1)) + goto done; + if (posix_spawn_file_actions_adddup2(&fa, p_stderr[1], 2)) + goto done; + + if (posix_spawnp(&pid, args[0], &fa, NULL, args, environ)) { + pid = -1; + goto done; + } + + close(p_stdout[1]); + p_stdout[1] = -1; + close(p_stderr[1]); + p_stderr[1] = -1; + + int *read_fds[2] = {&p_stdout[0], &p_stderr[0]}; + subprocess_read_cb read_cbs[2] = {on_stdout, on_stderr}; + + while (p_stdout[0] >= 0 || p_stderr[0] >= 0) { + struct pollfd fds[] = { + {.events = POLLIN, .fd = *read_fds[0]}, + {.events = POLLIN, .fd = *read_fds[1]}, + {.events = POLLIN, .fd = cancel ? mp_cancel_get_fd(cancel) : -1}, + }; + if (sparse_poll(fds, MP_ARRAY_SIZE(fds), -1) < 0 && errno != EINTR) + break; + for (int n = 0; n < 2; n++) { + if (fds[n].revents) { + char buf[4096]; + ssize_t r = read(*read_fds[n], buf, sizeof(buf)); + if (r < 0 && errno == EINTR) + continue; + if (r > 0 && read_cbs[n]) + read_cbs[n](ctx, buf, r); + if (r <= 0) { + close(*read_fds[n]); + *read_fds[n] = -1; + } + } + } + if (fds[2].revents) { + kill(pid, SIGKILL); + break; + } + } + + // Note: it can happen that a child process closes the pipe, but does not + // terminate yet. In this case, we would have to run waitpid() in + // a separate thread and use pthread_cancel(), or use other weird + // and laborious tricks. So this isn't handled yet. + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {} + +done: + if (fa_destroy) + posix_spawn_file_actions_destroy(&fa); + close(p_stdout[0]); + close(p_stdout[1]); + close(p_stderr[0]); + close(p_stderr[1]); + + if (WIFEXITED(status) && WEXITSTATUS(status) != 127) { + *error = NULL; + status = WEXITSTATUS(status); + } else { + *error = WEXITSTATUS(status) == 127 ? "init" : "killed"; + status = -1; + } + + return status; +} diff --git a/osdep/subprocess-win.c b/osdep/subprocess-win.c new file mode 100644 index 0000000000..04ea4ec3e4 --- /dev/null +++ b/osdep/subprocess-win.c @@ -0,0 +1,371 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see . + */ + +#define _WIN32_WINNT 0x0600 +#include "osdep/subprocess.h" + +#include +#include + +#include "osdep/io.h" +#include "osdep/atomics.h" + +#include "talloc.h" +#include "common/common.h" +#include "misc/bstr.h" + +static void write_arg(bstr *cmdline, char *arg) +{ + // If the string doesn't have characters that need to be escaped, it's best + // to leave it alone for the sake of Windows programs that don't process + // quoted args correctly. + if (!strpbrk(arg, " \t\"")) { + bstr_xappend(NULL, cmdline, bstr0(arg)); + return; + } + + // If there are characters that need to be escaped, write a quoted string + bstr_xappend(NULL, cmdline, bstr0("\"")); + + // Escape the argument. To match the behavior of CommandLineToArgvW, + // backslashes are only escaped if they appear before a quote or the end of + // the string. + int num_slashes = 0; + for (int pos = 0; arg[pos]; pos++) { + switch (arg[pos]) { + case '\\': + // Count backslashes that appear in a row + num_slashes++; + break; + case '"': + bstr_xappend(NULL, cmdline, (struct bstr){arg, pos}); + + // Double preceding slashes + for (int i = 0; i < num_slashes; i++) + bstr_xappend(NULL, cmdline, bstr0("\\")); + + // Escape the following quote + bstr_xappend(NULL, cmdline, bstr0("\\")); + + arg += pos; + pos = 0; + num_slashes = 0; + break; + default: + num_slashes = 0; + } + } + + // Write the rest of the argument + bstr_xappend(NULL, cmdline, bstr0(arg)); + + // Double slashes that appear at the end of the string + for (int i = 0; i < num_slashes; i++) + bstr_xappend(NULL, cmdline, bstr0("\\")); + + bstr_xappend(NULL, cmdline, bstr0("\"")); +} + +// Convert an array of arguments to a properly escaped command-line string +static wchar_t *write_cmdline(void *ctx, char **argv) +{ + bstr cmdline = {0}; + + for (int i = 0; argv[i]; i++) { + write_arg(&cmdline, argv[i]); + if (argv[i + 1]) + bstr_xappend(NULL, &cmdline, bstr0(" ")); + } + + wchar_t *wcmdline = mp_from_utf8(ctx, cmdline.start); + talloc_free(cmdline.start); + return wcmdline; +} + +static int create_overlapped_pipe(HANDLE *read, HANDLE *write) +{ + static atomic_ulong counter = ATOMIC_VAR_INIT(0); + + // Generate pipe name + unsigned long id = atomic_fetch_add(&counter, 1); + unsigned pid = GetCurrentProcessId(); + wchar_t buf[36]; + swprintf(buf, sizeof(buf), L"\\\\?\\pipe\\mpv-anon-%08x-%08lx", pid, id); + + // The function for creating anonymous pipes (CreatePipe) can't create + // overlapped pipes, so instead, use a named pipe with a unique name + *read = CreateNamedPipeW(buf, PIPE_ACCESS_INBOUND | + FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, + 1, 0, 4096, 0, NULL); + if (!*read) + goto error; + + // Open the write end of the pipe as a synchronous handle + *write = CreateFileW(buf, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (*write == INVALID_HANDLE_VALUE) + goto error; + + return 0; +error: + *read = *write = INVALID_HANDLE_VALUE; + return -1; +} + +static void delete_handle_list(void *p) +{ + LPPROC_THREAD_ATTRIBUTE_LIST list = p; + VOID (WINAPI *pDeleteProcThreadAttributeList)(LPPROC_THREAD_ATTRIBUTE_LIST); + + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + pDeleteProcThreadAttributeList = + (VOID (WINAPI*)(LPPROC_THREAD_ATTRIBUTE_LIST)) + GetProcAddress(kernel32, "DeleteProcThreadAttributeList"); + + if (pDeleteProcThreadAttributeList) + pDeleteProcThreadAttributeList(list); +} + +// Create a PROC_THREAD_ATTRIBUTE_LIST that specifies exactly which handles are +// inherited by the subprocess +static LPPROC_THREAD_ATTRIBUTE_LIST create_handle_list(void *ctx, + HANDLE *handles, int num) +{ + WINBOOL (WINAPI *pInitializeProcThreadAttributeList)( + LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, PSIZE_T); + WINBOOL (WINAPI *pUpdateProcThreadAttribute)(LPPROC_THREAD_ATTRIBUTE_LIST, + DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T); + + // Load Windows Vista functions, if available + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + pInitializeProcThreadAttributeList = + (WINBOOL (WINAPI*)(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, PSIZE_T)) + GetProcAddress(kernel32, "InitializeProcThreadAttributeList"); + pUpdateProcThreadAttribute = + (WINBOOL (WINAPI*)(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD_PTR, + PVOID, SIZE_T, PVOID, PSIZE_T)) + GetProcAddress(kernel32, "UpdateProcThreadAttribute"); + if (!pInitializeProcThreadAttributeList || !pUpdateProcThreadAttribute) + return NULL; + + // Get required attribute list size + SIZE_T size = 0; + if (!pInitializeProcThreadAttributeList(NULL, 1, 0, &size)) { + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) + return NULL; + } + + // Allocate attribute list + LPPROC_THREAD_ATTRIBUTE_LIST list = talloc_size(ctx, size); + if (!pInitializeProcThreadAttributeList(list, 1, 0, &size)) + goto error; + talloc_set_destructor(list, delete_handle_list); + + if (!pUpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + handles, num * sizeof(HANDLE), NULL, NULL)) + goto error; + + return list; +error: + talloc_free(list); + return NULL; +} + +// Helper method similar to sparse_poll, skips NULL handles +static int sparse_wait(HANDLE *handles, unsigned num_handles) +{ + unsigned w_num_handles = 0; + HANDLE w_handles[num_handles]; + int map[num_handles]; + + for (unsigned i = 0; i < num_handles; i++) { + if (!handles[i]) + continue; + + w_handles[w_num_handles] = handles[i]; + map[w_num_handles] = i; + w_num_handles++; + } + + if (w_num_handles == 0) + return -1; + DWORD i = WaitForMultipleObjects(w_num_handles, w_handles, FALSE, INFINITE); + i -= WAIT_OBJECT_0; + + if (i >= w_num_handles) + return -1; + return map[i]; +} + +// Wrapper for ReadFile that treats ERROR_IO_PENDING as success +static int async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol) +{ + if (!ReadFile(file, buf, size, NULL, ol)) + return (GetLastError() == ERROR_IO_PENDING) ? 0 : -1; + return 0; +} + +int mp_subprocess(char **args, struct mp_cancel *cancel, void *ctx, + subprocess_read_cb on_stdout, subprocess_read_cb on_stderr, + char **error) +{ + wchar_t *tmp = talloc_new(NULL); + int status = -1; + struct { + HANDLE read; + HANDLE write; + OVERLAPPED ol; + char buf[4096]; + subprocess_read_cb read_cb; + } pipes[2] = { + { .read_cb = on_stdout }, + { .read_cb = on_stderr }, + }; + + // If the function exits before CreateProcess, there was an init error + *error = "init"; + + for (int i = 0; i < 2; i++) { + pipes[i].ol.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!pipes[i].ol.hEvent) + goto done; + if (create_overlapped_pipe(&pipes[i].read, &pipes[i].write)) + goto done; + if (!SetHandleInformation(pipes[i].write, HANDLE_FLAG_INHERIT, + HANDLE_FLAG_INHERIT)) + goto done; + } + + // Convert the args array to a UTF-16 Windows command-line string + wchar_t *cmdline = write_cmdline(tmp, args); + + DWORD flags = CREATE_UNICODE_ENVIRONMENT; + PROCESS_INFORMATION pi = {0}; + STARTUPINFOEXW si = { + .StartupInfo = { + .cb = sizeof(si), + .dwFlags = STARTF_USESTDHANDLES, + .hStdInput = NULL, + .hStdOutput = pipes[0].write, + .hStdError = pipes[1].write, + }, + + // Specify which handles are inherited by the subprocess. If this isn't + // specified, the subprocess inherits all inheritable handles, which + // could include handles created by other threads. See: + // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/16/10248328.aspx + .lpAttributeList = create_handle_list(tmp, + (HANDLE[]){ pipes[0].write, pipes[1].write }, 2), + }; + + // PROC_THREAD_ATTRIBUTE_LISTs are only supported in Vista and up. If not + // supported, create_handle_list will return NULL. + if (si.lpAttributeList) + flags |= EXTENDED_STARTUPINFO_PRESENT; + + // If we have a console, the subprocess will automatically attach to it so + // it can receive Ctrl+C events. If we don't have a console, prevent the + // subprocess from creating its own console window by specifying + // CREATE_NO_WINDOW. GetConsoleCP() can be used to reliably determine if we + // have a console or not (Cygwin uses it too.) + if (!GetConsoleCP()) + flags |= CREATE_NO_WINDOW; + + if (!CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, flags, NULL, NULL, + &si.StartupInfo, &pi)) + goto done; + talloc_free(cmdline); + talloc_free(si.lpAttributeList); + CloseHandle(pi.hThread); + + // Init is finished + *error = NULL; + + // List of handles to watch with sparse_wait + HANDLE handles[] = { pipes[0].ol.hEvent, pipes[1].ol.hEvent, pi.hProcess, + cancel ? mp_cancel_get_event(cancel) : NULL }; + + for (int i = 0; i < 2; i++) { + // Close our copy of the write end of the pipes + CloseHandle(pipes[i].write); + pipes[i].write = NULL; + + // Do the first read operation on each pipe + if (async_read(pipes[i].read, pipes[i].buf, 4096, &pipes[i].ol)) { + CloseHandle(pipes[i].read); + handles[i] = pipes[i].read = NULL; + } + } + + DWORD r; + DWORD exit_code; + while (pipes[0].read || pipes[1].read || pi.hProcess) { + int i = sparse_wait(handles, MP_ARRAY_SIZE(handles)); + switch (i) { + case 0: + case 1: + // Complete the read operation on the pipe + if (!GetOverlappedResult(pipes[i].read, &pipes[i].ol, &r, TRUE)) { + CloseHandle(pipes[i].read); + handles[i] = pipes[i].read = NULL; + break; + } + + pipes[i].read_cb(ctx, pipes[i].buf, r); + + // Begin the next read operation on the pipe + if (async_read(pipes[i].read, pipes[i].buf, 4096, &pipes[i].ol)) { + CloseHandle(pipes[i].read); + handles[i] = pipes[i].read = NULL; + } + + break; + case 2: + GetExitCodeProcess(pi.hProcess, &exit_code); + status = exit_code; + + CloseHandle(pi.hProcess); + handles[i] = pi.hProcess = NULL; + break; + case 3: + if (pi.hProcess) { + TerminateProcess(pi.hProcess, 1); + *error = "killed"; + goto done; + } + break; + default: + goto done; + } + } + +done: + for (int i = 0; i < 2; i++) { + if (pipes[i].read) { + // Cancel any pending I/O (if the process was killed) + CancelIo(pipes[i].read); + GetOverlappedResult(pipes[i].read, &pipes[i].ol, &r, TRUE); + CloseHandle(pipes[i].read); + } + if (pipes[i].write) CloseHandle(pipes[i].write); + if (pipes[i].ol.hEvent) CloseHandle(pipes[i].ol.hEvent); + } + if (pi.hProcess) CloseHandle(pi.hProcess); + talloc_free(tmp); + return status; +} diff --git a/osdep/subprocess.h b/osdep/subprocess.h new file mode 100644 index 0000000000..1ab4ddbdd7 --- /dev/null +++ b/osdep/subprocess.h @@ -0,0 +1,30 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see . + */ + +#ifndef MP_SUBPROCESS_H_ +#define MP_SUBPROCESS_H_ + +#include "stream/stream.h" + +typedef void (*subprocess_read_cb)(void *ctx, char *data, size_t size); + +// Start a subprocess. Uses callbacks to read from stdout and stderr. +int mp_subprocess(char **args, struct mp_cancel *cancel, void *ctx, + subprocess_read_cb on_stdout, subprocess_read_cb on_stderr, + char **error); + +#endif diff --git a/player/lua.c b/player/lua.c index ba60826778..cdcd2a2443 100644 --- a/player/lua.c +++ b/player/lua.c @@ -15,11 +15,6 @@ * with mpv. If not, see . */ -#ifdef __MINGW32__ -#define _WIN32_WINNT 0x0600 -#include -#endif - #include #include #include @@ -47,6 +42,7 @@ #include "options/path.h" #include "misc/bstr.h" #include "misc/json.h" +#include "osdep/subprocess.h" #include "osdep/timer.h" #include "osdep/threads.h" #include "stream/stream.h" @@ -1177,477 +1173,6 @@ static int script_join_path(lua_State *L) return 1; } -typedef void (*read_cb)(void *ctx, char *data, size_t size); - -#ifdef __MINGW32__ -static void write_arg(bstr *cmdline, char *arg) -{ - // If the string doesn't have characters that need to be escaped, it's best - // to leave it alone for the sake of Windows programs that don't process - // quoted args correctly. - if (!strpbrk(arg, " \t\"")) { - bstr_xappend(NULL, cmdline, bstr0(arg)); - return; - } - - // If there are characters that need to be escaped, write a quoted string - bstr_xappend(NULL, cmdline, bstr0("\"")); - - // Escape the argument. To match the behavior of CommandLineToArgvW, - // backslashes are only escaped if they appear before a quote or the end of - // the string. - int num_slashes = 0; - for (int pos = 0; arg[pos]; pos++) { - switch (arg[pos]) { - case '\\': - // Count backslashes that appear in a row - num_slashes++; - break; - case '"': - bstr_xappend(NULL, cmdline, (struct bstr){arg, pos}); - - // Double preceding slashes - for (int i = 0; i < num_slashes; i++) - bstr_xappend(NULL, cmdline, bstr0("\\")); - - // Escape the following quote - bstr_xappend(NULL, cmdline, bstr0("\\")); - - arg += pos; - pos = 0; - num_slashes = 0; - break; - default: - num_slashes = 0; - } - } - - // Write the rest of the argument - bstr_xappend(NULL, cmdline, bstr0(arg)); - - // Double slashes that appear at the end of the string - for (int i = 0; i < num_slashes; i++) - bstr_xappend(NULL, cmdline, bstr0("\\")); - - bstr_xappend(NULL, cmdline, bstr0("\"")); -} - -// Convert an array of arguments to a properly escaped command-line string -static wchar_t *write_cmdline(void *ctx, char **argv) -{ - bstr cmdline = {0}; - - for (int i = 0; argv[i]; i++) { - write_arg(&cmdline, argv[i]); - if (argv[i + 1]) - bstr_xappend(NULL, &cmdline, bstr0(" ")); - } - - wchar_t *wcmdline = mp_from_utf8(ctx, cmdline.start); - talloc_free(cmdline.start); - return wcmdline; -} - -static int create_overlapped_pipe(HANDLE *read, HANDLE *write) -{ - static atomic_ulong counter = ATOMIC_VAR_INIT(0); - - // Generate pipe name - unsigned long id = atomic_fetch_add(&counter, 1); - unsigned pid = GetCurrentProcessId(); - wchar_t buf[36]; - swprintf(buf, sizeof(buf), L"\\\\?\\pipe\\mpv-anon-%08x-%08lx", pid, id); - - // The function for creating anonymous pipes (CreatePipe) can't create - // overlapped pipes, so instead, use a named pipe with a unique name - *read = CreateNamedPipeW(buf, PIPE_ACCESS_INBOUND | - FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, - PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, - 1, 0, 4096, 0, NULL); - if (!*read) - goto error; - - // Open the write end of the pipe as a synchronous handle - *write = CreateFileW(buf, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, NULL); - if (*write == INVALID_HANDLE_VALUE) - goto error; - - return 0; -error: - *read = *write = INVALID_HANDLE_VALUE; - return -1; -} - -static void delete_handle_list(void *p) -{ - LPPROC_THREAD_ATTRIBUTE_LIST list = p; - VOID (WINAPI *pDeleteProcThreadAttributeList)(LPPROC_THREAD_ATTRIBUTE_LIST); - - HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); - pDeleteProcThreadAttributeList = - (VOID (WINAPI*)(LPPROC_THREAD_ATTRIBUTE_LIST)) - GetProcAddress(kernel32, "DeleteProcThreadAttributeList"); - - if (pDeleteProcThreadAttributeList) - pDeleteProcThreadAttributeList(list); -} - -// Create a PROC_THREAD_ATTRIBUTE_LIST that specifies exactly which handles are -// inherited by the subprocess -static LPPROC_THREAD_ATTRIBUTE_LIST create_handle_list(void *ctx, - HANDLE *handles, int num) -{ - WINBOOL (WINAPI *pInitializeProcThreadAttributeList)( - LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, PSIZE_T); - WINBOOL (WINAPI *pUpdateProcThreadAttribute)(LPPROC_THREAD_ATTRIBUTE_LIST, - DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T); - - // Load Windows Vista functions, if available - HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); - pInitializeProcThreadAttributeList = - (WINBOOL (WINAPI*)(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, PSIZE_T)) - GetProcAddress(kernel32, "InitializeProcThreadAttributeList"); - pUpdateProcThreadAttribute = - (WINBOOL (WINAPI*)(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD_PTR, - PVOID, SIZE_T, PVOID, PSIZE_T)) - GetProcAddress(kernel32, "UpdateProcThreadAttribute"); - if (!pInitializeProcThreadAttributeList || !pUpdateProcThreadAttribute) - return NULL; - - // Get required attribute list size - SIZE_T size = 0; - if (!pInitializeProcThreadAttributeList(NULL, 1, 0, &size)) { - if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) - return NULL; - } - - // Allocate attribute list - LPPROC_THREAD_ATTRIBUTE_LIST list = talloc_size(ctx, size); - if (!pInitializeProcThreadAttributeList(list, 1, 0, &size)) - goto error; - talloc_set_destructor(list, delete_handle_list); - - if (!pUpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, - handles, num * sizeof(HANDLE), NULL, NULL)) - goto error; - - return list; -error: - talloc_free(list); - return NULL; -} - -// Helper method similar to sparse_poll, skips NULL handles -static int sparse_wait(HANDLE *handles, unsigned num_handles) -{ - unsigned w_num_handles = 0; - HANDLE w_handles[num_handles]; - int map[num_handles]; - - for (unsigned i = 0; i < num_handles; i++) { - if (!handles[i]) - continue; - - w_handles[w_num_handles] = handles[i]; - map[w_num_handles] = i; - w_num_handles++; - } - - if (w_num_handles == 0) - return -1; - DWORD i = WaitForMultipleObjects(w_num_handles, w_handles, FALSE, INFINITE); - i -= WAIT_OBJECT_0; - - if (i >= w_num_handles) - return -1; - return map[i]; -} - -// Wrapper for ReadFile that treats ERROR_IO_PENDING as success -static int async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol) -{ - if (!ReadFile(file, buf, size, NULL, ol)) - return (GetLastError() == ERROR_IO_PENDING) ? 0 : -1; - return 0; -} - -static int subprocess(char **args, struct mp_cancel *cancel, void *ctx, - read_cb on_stdout, read_cb on_stderr, char **error) -{ - wchar_t *tmp = talloc_new(NULL); - int status = -1; - struct { - HANDLE read; - HANDLE write; - OVERLAPPED ol; - char buf[4096]; - read_cb read_cb; - } pipes[2] = { - { .read_cb = on_stdout }, - { .read_cb = on_stderr }, - }; - - // If the function exits before CreateProcess, there was an init error - *error = "init"; - - for (int i = 0; i < 2; i++) { - pipes[i].ol.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); - if (!pipes[i].ol.hEvent) - goto done; - if (create_overlapped_pipe(&pipes[i].read, &pipes[i].write)) - goto done; - if (!SetHandleInformation(pipes[i].write, HANDLE_FLAG_INHERIT, - HANDLE_FLAG_INHERIT)) - goto done; - } - - // Convert the args array to a UTF-16 Windows command-line string - wchar_t *cmdline = write_cmdline(tmp, args); - - DWORD flags = CREATE_UNICODE_ENVIRONMENT; - PROCESS_INFORMATION pi = {0}; - STARTUPINFOEXW si = { - .StartupInfo = { - .cb = sizeof(si), - .dwFlags = STARTF_USESTDHANDLES, - .hStdInput = NULL, - .hStdOutput = pipes[0].write, - .hStdError = pipes[1].write, - }, - - // Specify which handles are inherited by the subprocess. If this isn't - // specified, the subprocess inherits all inheritable handles, which - // could include handles created by other threads. See: - // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/16/10248328.aspx - .lpAttributeList = create_handle_list(tmp, - (HANDLE[]){ pipes[0].write, pipes[1].write }, 2), - }; - - // PROC_THREAD_ATTRIBUTE_LISTs are only supported in Vista and up. If not - // supported, create_handle_list will return NULL. - if (si.lpAttributeList) - flags |= EXTENDED_STARTUPINFO_PRESENT; - - // If we have a console, the subprocess will automatically attach to it so - // it can receive Ctrl+C events. If we don't have a console, prevent the - // subprocess from creating its own console window by specifying - // CREATE_NO_WINDOW. GetConsoleCP() can be used to reliably determine if we - // have a console or not (Cygwin uses it too.) - if (!GetConsoleCP()) - flags |= CREATE_NO_WINDOW; - - if (!CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, flags, NULL, NULL, - &si.StartupInfo, &pi)) - goto done; - talloc_free(cmdline); - talloc_free(si.lpAttributeList); - CloseHandle(pi.hThread); - - // Init is finished - *error = NULL; - - // List of handles to watch with sparse_wait - HANDLE handles[] = { pipes[0].ol.hEvent, pipes[1].ol.hEvent, pi.hProcess, - cancel ? mp_cancel_get_event(cancel) : NULL }; - - for (int i = 0; i < 2; i++) { - // Close our copy of the write end of the pipes - CloseHandle(pipes[i].write); - pipes[i].write = NULL; - - // Do the first read operation on each pipe - if (async_read(pipes[i].read, pipes[i].buf, 4096, &pipes[i].ol)) { - CloseHandle(pipes[i].read); - handles[i] = pipes[i].read = NULL; - } - } - - DWORD r; - DWORD exit_code; - while (pipes[0].read || pipes[1].read || pi.hProcess) { - int i = sparse_wait(handles, MP_ARRAY_SIZE(handles)); - switch (i) { - case 0: - case 1: - // Complete the read operation on the pipe - if (!GetOverlappedResult(pipes[i].read, &pipes[i].ol, &r, TRUE)) { - CloseHandle(pipes[i].read); - handles[i] = pipes[i].read = NULL; - break; - } - - pipes[i].read_cb(ctx, pipes[i].buf, r); - - // Begin the next read operation on the pipe - if (async_read(pipes[i].read, pipes[i].buf, 4096, &pipes[i].ol)) { - CloseHandle(pipes[i].read); - handles[i] = pipes[i].read = NULL; - } - - break; - case 2: - GetExitCodeProcess(pi.hProcess, &exit_code); - status = exit_code; - - CloseHandle(pi.hProcess); - handles[i] = pi.hProcess = NULL; - break; - case 3: - if (pi.hProcess) { - TerminateProcess(pi.hProcess, 1); - *error = "killed"; - goto done; - } - break; - default: - goto done; - } - } - -done: - for (int i = 0; i < 2; i++) { - if (pipes[i].read) { - // Cancel any pending I/O (if the process was killed) - CancelIo(pipes[i].read); - GetOverlappedResult(pipes[i].read, &pipes[i].ol, &r, TRUE); - CloseHandle(pipes[i].read); - } - if (pipes[i].write) CloseHandle(pipes[i].write); - if (pipes[i].ol.hEvent) CloseHandle(pipes[i].ol.hEvent); - } - if (pi.hProcess) CloseHandle(pi.hProcess); - talloc_free(tmp); - return status; -} -#endif - -#if HAVE_POSIX_SPAWN -#include -#include -#include -#include -#include -#include -#include - -// Normally, this must be declared manually, but glibc is retarded. -#ifndef __GLIBC__ -extern char **environ; -#endif - -// A silly helper: automatically skips entries with negative FDs -static int sparse_poll(struct pollfd *fds, int num_fds, int timeout) -{ - struct pollfd p_fds[10]; - int map[10]; - if (num_fds > MP_ARRAY_SIZE(p_fds)) - return -1; - int p_num_fds = 0; - for (int n = 0; n < num_fds; n++) { - map[n] = -1; - if (fds[n].fd < 0) - continue; - map[n] = p_num_fds; - p_fds[p_num_fds++] = fds[n]; - } - int r = poll(p_fds, p_num_fds, timeout); - for (int n = 0; n < num_fds; n++) - fds[n].revents = (map[n] < 0 && r >= 0) ? 0 : p_fds[map[n]].revents; - return r; -} - -static int subprocess(char **args, struct mp_cancel *cancel, void *ctx, - read_cb on_stdout, read_cb on_stderr, char **error) -{ - posix_spawn_file_actions_t fa; - bool fa_destroy = false; - int status = -1; - int p_stdout[2] = {-1, -1}; - int p_stderr[2] = {-1, -1}; - pid_t pid = -1; - - if (mp_make_cloexec_pipe(p_stdout) < 0) - goto done; - if (mp_make_cloexec_pipe(p_stderr) < 0) - goto done; - - if (posix_spawn_file_actions_init(&fa)) - goto done; - fa_destroy = true; - // redirect stdout and stderr - if (posix_spawn_file_actions_adddup2(&fa, p_stdout[1], 1)) - goto done; - if (posix_spawn_file_actions_adddup2(&fa, p_stderr[1], 2)) - goto done; - - if (posix_spawnp(&pid, args[0], &fa, NULL, args, environ)) { - pid = -1; - goto done; - } - - close(p_stdout[1]); - p_stdout[1] = -1; - close(p_stderr[1]); - p_stderr[1] = -1; - - int *read_fds[2] = {&p_stdout[0], &p_stderr[0]}; - read_cb read_cbs[2] = {on_stdout, on_stderr}; - - while (p_stdout[0] >= 0 || p_stderr[0] >= 0) { - struct pollfd fds[] = { - {.events = POLLIN, .fd = *read_fds[0]}, - {.events = POLLIN, .fd = *read_fds[1]}, - {.events = POLLIN, .fd = cancel ? mp_cancel_get_fd(cancel) : -1}, - }; - if (sparse_poll(fds, MP_ARRAY_SIZE(fds), -1) < 0 && errno != EINTR) - break; - for (int n = 0; n < 2; n++) { - if (fds[n].revents) { - char buf[4096]; - ssize_t r = read(*read_fds[n], buf, sizeof(buf)); - if (r < 0 && errno == EINTR) - continue; - if (r > 0 && read_cbs[n]) - read_cbs[n](ctx, buf, r); - if (r <= 0) { - close(*read_fds[n]); - *read_fds[n] = -1; - } - } - } - if (fds[2].revents) { - kill(pid, SIGKILL); - break; - } - } - - // Note: it can happen that a child process closes the pipe, but does not - // terminate yet. In this case, we would have to run waitpid() in - // a separate thread and use pthread_cancel(), or use other weird - // and laborious tricks. So this isn't handled yet. - while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {} - -done: - if (fa_destroy) - posix_spawn_file_actions_destroy(&fa); - close(p_stdout[0]); - close(p_stdout[1]); - close(p_stderr[0]); - close(p_stderr[1]); - - if (WIFEXITED(status) && WEXITSTATUS(status) != 127) { - *error = NULL; - status = WEXITSTATUS(status); - } else { - *error = WEXITSTATUS(status) == 127 ? "init" : "killed"; - status = -1; - } - - return status; -} -#endif - #if HAVE_POSIX_SPAWN || defined(__MINGW32__) struct subprocess_cb_ctx { struct mp_log *log; @@ -1711,8 +1236,8 @@ static int script_subprocess(lua_State *L) }; char *error = NULL; - int status = subprocess(args, cancel, &cb_ctx, subprocess_stdout, - subprocess_stderr, &error); + int status = mp_subprocess(args, cancel, &cb_ctx, subprocess_stdout, + subprocess_stderr, &error); lua_newtable(L); // res if (error) { diff --git a/wscript_build.py b/wscript_build.py index be6b783b86..6120c53813 100644 --- a/wscript_build.py +++ b/wscript_build.py @@ -380,6 +380,8 @@ def build(ctx): ( "osdep/macosx_application.m", "cocoa-application" ), ( "osdep/macosx_events.m", "cocoa" ), ( "osdep/semaphore_osx.c" ), + ( "osdep/subprocess-posix.c", "posix-spawn" ), + ( "osdep/subprocess-win.c", "os-win32" ), ( "osdep/path-macosx.m", "cocoa" ), ( "osdep/path-win.c", "os-win32" ), ( "osdep/path-win.c", "os-cygwin" ), -- cgit v1.2.3