/* * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv. If not, see . */ #include #include #include "osdep/subprocess.h" #include "osdep/io.h" #include "osdep/atomic.h" #include "mpv_talloc.h" #include "common/common.h" #include "stream/stream.h" #include "misc/bstr.h" static void write_arg(bstr *cmdline, char *arg) { // Empty args must be represented as an empty quoted string if (!arg[0]) { bstr_xappend(NULL, cmdline, bstr0("\"\"")); return; } // 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 consecutive backslashes num_slashes++; break; case '"': // Write the argument up to the point before the quote bstr_xappend(NULL, cmdline, (struct bstr){arg, pos}); arg += pos; pos = 0; // Double backslashes preceding the quote for (int i = 0; i < num_slashes; i++) bstr_xappend(NULL, cmdline, bstr0("\\")); num_slashes = 0; // Escape the quote itself bstr_xappend(NULL, cmdline, bstr0("\\")); break; default: num_slashes = 0; } } // Write the rest of the argument bstr_xappend(NULL, cmdline, bstr0(arg)); // Double backslashes at the end of the argument 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) { // argv[0] should always be quoted. Otherwise, arguments may be interpreted // as part of the program name. Also, it can't contain escape sequences. bstr cmdline = {0}; bstr_xappend_asprintf(NULL, &cmdline, "\"%s\"", argv[0]); for (int i = 1; argv[i]; i++) { bstr_xappend(NULL, &cmdline, bstr0(" ")); write_arg(&cmdline, argv[i]); } 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 == INVALID_HANDLE_VALUE) 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; DeleteProcThreadAttributeList(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) { // Get required attribute list size SIZE_T size = 0; if (!InitializeProcThreadAttributeList(NULL, 1, 0, &size)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return NULL; } // Allocate attribute list LPPROC_THREAD_ATTRIBUTE_LIST list = talloc_size(ctx, size); if (!InitializeProcThreadAttributeList(list, 1, 0, &size)) goto error; talloc_set_destructor(list, delete_handle_list); if (!UpdateProcThreadAttribute(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[10]; int map[10]; if (num_handles > MP_ARRAY_SIZE(w_handles)) return -1; 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 void write_none(void *ctx, char *data, size_t size) { } 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 ? on_stdout : write_none }, { .read_cb = on_stderr ? on_stderr : write_none }, }; // 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 | EXTENDED_STARTUPINFO_PRESENT; 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), }; // 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"; status = MP_SUBPROCESS_EKILLED_BY_US; 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; }