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. --- osdep/subprocess-posix.c | 146 +++++++++++++++++++ osdep/subprocess-win.c | 371 +++++++++++++++++++++++++++++++++++++++++++++++ osdep/subprocess.h | 30 ++++ 3 files changed, 547 insertions(+) create mode 100644 osdep/subprocess-posix.c create mode 100644 osdep/subprocess-win.c create mode 100644 osdep/subprocess.h (limited to 'osdep') 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 -- cgit v1.2.3