/* * 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/windows_utils.h" #include "mpv_talloc.h" #include "common/common.h" #include "stream/stream.h" #include "misc/bstr.h" #include "misc/thread_tools.h" // Internal CRT FD flags #define FOPEN (0x01) #define FPIPE (0x08) #define FDEV (0x40) 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 *argv0, char **args) { // argv0 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\"", argv0); if (args) { for (int i = 0; args[i]; i++) { bstr_xappend(NULL, &cmdline, bstr0(" ")); write_arg(&cmdline, args[i]); } } wchar_t *wcmdline = mp_from_utf8(ctx, cmdline.start); talloc_free(cmdline.start); return wcmdline; } 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[MP_SUBPROCESS_MAX_FDS + 2]; int map[MP_SUBPROCESS_MAX_FDS + 2]; 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 bool is_valid_handle(HANDLE h) { // _get_osfhandle can return -2 "when the file descriptor is not associated // with a stream" return h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2; } static wchar_t *convert_environ(void *ctx, char **env) { // Environment size in wchar_ts, including the trailing NUL size_t env_size = 1; for (int i = 0; env[i]; i++) { int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1, NULL, 0); if (count <= 0) abort(); env_size += count; } wchar_t *ret = talloc_array(ctx, wchar_t, env_size); size_t pos = 0; for (int i = 0; env[i]; i++) { int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1, ret + pos, env_size - pos); if (count <= 0) abort(); pos += count; } return ret; } void mp_subprocess2(struct mp_subprocess_opts *opts, struct mp_subprocess_result *res) { wchar_t *tmp = talloc_new(NULL); DWORD r; HANDLE share_hndls[MP_SUBPROCESS_MAX_FDS] = {0}; int share_hndl_count = 0; HANDLE wait_hndls[MP_SUBPROCESS_MAX_FDS + 2] = {0}; int wait_hndl_count = 0; struct { HANDLE handle; bool handle_close; char crt_flags; HANDLE read; OVERLAPPED read_ol; char *read_buf; } fd_data[MP_SUBPROCESS_MAX_FDS] = {0}; // The maximum target FD is limited because FDs have to fit in two sparse // arrays in STARTUPINFO.lpReserved2, which has a maximum size of 65535 // bytes. The first four bytes are the handle count, followed by one byte // per handle for flags, and an intptr_t per handle for the HANDLE itself. static const int crt_fd_max = (65535 - sizeof(int)) / (1 + sizeof(intptr_t)); int crt_fd_count = 0; // If the function exits before CreateProcess, there was an init error *res = (struct mp_subprocess_result){ .error = MP_SUBPROCESS_EINIT }; STARTUPINFOEXW si = { .StartupInfo = { .cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES | STARTF_FORCEOFFFEEDBACK, }, }; PROCESS_INFORMATION pi = {0}; for (int n = 0; n < opts->num_fds; n++) { if (opts->fds[n].fd >= crt_fd_max) { // Target FD is too big to fit in the CRT FD array res->error = MP_SUBPROCESS_EUNSUPPORTED; goto done; } if (opts->fds[n].fd >= crt_fd_count) crt_fd_count = opts->fds[n].fd + 1; if (opts->fds[n].src_fd >= 0) { HANDLE src_handle = (HANDLE)_get_osfhandle(opts->fds[n].src_fd); // Invalid handles are just ignored. This is because sometimes the // standard handles are invalid in Windows, like in GUI processes. // In this case mp_subprocess2 callers should still be able to // blindly forward the standard FDs. if (!is_valid_handle(src_handle)) continue; DWORD type = GetFileType(src_handle); bool is_console_handle = false; switch (type & 0xff) { case FILE_TYPE_DISK: fd_data[n].crt_flags = FOPEN; break; case FILE_TYPE_CHAR: fd_data[n].crt_flags = FOPEN | FDEV; is_console_handle = GetConsoleMode(src_handle, &(DWORD){0}); break; case FILE_TYPE_PIPE: fd_data[n].crt_flags = FOPEN | FPIPE; break; case FILE_TYPE_UNKNOWN: continue; } if (is_console_handle) { // Some Windows versions have bugs when duplicating console // handles, or when adding console handles to the CreateProcess // handle list, so just use the handle directly for now. Console // handles treat inheritance weirdly, so this should still work. fd_data[n].handle = src_handle; } else { // Instead of making the source handle inheritable, just // duplicate it to an inheritable handle if (!DuplicateHandle(GetCurrentProcess(), src_handle, GetCurrentProcess(), &fd_data[n].handle, 0, TRUE, DUPLICATE_SAME_ACCESS)) goto done; fd_data[n].handle_close = true; share_hndls[share_hndl_count++] = fd_data[n].handle; } } else if (opts->fds[n].on_read && !opts->detach) { fd_data[n].read_ol.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); if (!fd_data[n].read_ol.hEvent) goto done; struct w32_create_anon_pipe_opts o = { .server_flags = PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, .client_inheritable = true, }; if (!mp_w32_create_anon_pipe(&fd_data[n].read, &fd_data[n].handle, &o)) goto done; fd_data[n].handle_close = true; wait_hndls[n] = fd_data[n].read_ol.hEvent; wait_hndl_count++; fd_data[n].crt_flags = FOPEN | FPIPE; fd_data[n].read_buf = talloc_size(tmp, 4096); share_hndls[share_hndl_count++] = fd_data[n].handle; } else { DWORD access; if (opts->fds[n].fd == 0) { access = FILE_GENERIC_READ; } else if (opts->fds[n].fd <= 2) { access = FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES; } else { access = FILE_GENERIC_READ | FILE_GENERIC_WRITE; } SECURITY_ATTRIBUTES sa = { .nLength = sizeof sa, .bInheritHandle = TRUE, }; fd_data[n].crt_flags = FOPEN | FDEV; fd_data[n].handle = CreateFileW(L"NUL", access, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL); fd_data[n].handle_close = true; } switch (opts->fds[n].fd) { case 0: si.StartupInfo.hStdInput = fd_data[n].handle; break; case 1: si.StartupInfo.hStdOutput = fd_data[n].handle; break; case 2: si.StartupInfo.hStdError = fd_data[n].handle; break; } } // Convert the UTF-8 environment into a UTF-16 Windows environment block wchar_t *env = NULL; if (opts->env) env = convert_environ(tmp, opts->env); // Convert the args array to a UTF-16 Windows command-line string char **args = opts->args && opts->args[0] ? &opts->args[1] : 0; wchar_t *cmdline = write_cmdline(tmp, opts->exe, args); // Get pointers to the arrays in lpReserved2. This is an undocumented data // structure used by MSVCRT (and other frameworks and runtimes) to emulate // FD inheritance. The format is unofficially documented here: // si.StartupInfo.cbReserved2 = sizeof(int) + crt_fd_count * (1 + sizeof(intptr_t)); si.StartupInfo.lpReserved2 = talloc_size(tmp, si.StartupInfo.cbReserved2); char *crt_buf_flags = si.StartupInfo.lpReserved2 + sizeof(int); char *crt_buf_hndls = crt_buf_flags + crt_fd_count; memcpy(si.StartupInfo.lpReserved2, &crt_fd_count, sizeof(int)); // Fill the handle array with INVALID_HANDLE_VALUE, for unassigned handles for (int n = 0; n < crt_fd_count; n++) { HANDLE h = INVALID_HANDLE_VALUE; memcpy(crt_buf_hndls + n * sizeof(intptr_t), &h, sizeof(intptr_t)); } for (int n = 0; n < opts->num_fds; n++) { crt_buf_flags[opts->fds[n].fd] = fd_data[n].crt_flags; memcpy(crt_buf_hndls + opts->fds[n].fd * sizeof(intptr_t), &fd_data[n].handle, sizeof(intptr_t)); } DWORD flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT; // 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: // si.lpAttributeList = create_handle_list(tmp, share_hndls, share_hndl_count); // 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, env, NULL, &si.StartupInfo, &pi)) goto done; talloc_free(cmdline); talloc_free(env); talloc_free(si.StartupInfo.lpReserved2); talloc_free(si.lpAttributeList); CloseHandle(pi.hThread); for (int n = 0; n < opts->num_fds; n++) { if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle)) CloseHandle(fd_data[n].handle); fd_data[n].handle = NULL; if (fd_data[n].read) { // Do the first read operation on each pipe if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096, &fd_data[n].read_ol)) { CloseHandle(fd_data[n].read); wait_hndls[n] = fd_data[n].read = NULL; wait_hndl_count--; } } } if (opts->detach) { res->error = MP_SUBPROCESS_OK; goto done; } res->error = MP_SUBPROCESS_EGENERIC; wait_hndls[MP_SUBPROCESS_MAX_FDS] = pi.hProcess; wait_hndl_count++; if (opts->cancel) wait_hndls[MP_SUBPROCESS_MAX_FDS + 1] = mp_cancel_get_event(opts->cancel); DWORD exit_code; while (wait_hndl_count) { int n = sparse_wait(wait_hndls, MP_ARRAY_SIZE(wait_hndls)); if (n >= 0 && n < MP_SUBPROCESS_MAX_FDS) { // Complete the read operation on the pipe if (!GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE)) { CloseHandle(fd_data[n].read); wait_hndls[n] = fd_data[n].read = NULL; wait_hndl_count--; } else { opts->fds[n].on_read(opts->fds[n].on_read_ctx, fd_data[n].read_buf, r); // Begin the next read operation on the pipe if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096, &fd_data[n].read_ol)) { CloseHandle(fd_data[n].read); wait_hndls[n] = fd_data[n].read = NULL; wait_hndl_count--; } } } else if (n == MP_SUBPROCESS_MAX_FDS) { // pi.hProcess GetExitCodeProcess(pi.hProcess, &exit_code); res->exit_status = exit_code; CloseHandle(pi.hProcess); wait_hndls[n] = pi.hProcess = NULL; wait_hndl_count--; } else if (n == MP_SUBPROCESS_MAX_FDS + 1) { // opts.cancel if (pi.hProcess) { TerminateProcess(pi.hProcess, 1); res->error = MP_SUBPROCESS_EKILLED_BY_US; goto done; } } else { goto done; } } res->error = MP_SUBPROCESS_OK; done: for (int n = 0; n < opts->num_fds; n++) { if (is_valid_handle(fd_data[n].read)) { // Cancel any pending I/O (if the process was killed) CancelIo(fd_data[n].read); GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE); CloseHandle(fd_data[n].read); } if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle)) CloseHandle(fd_data[n].handle); if (fd_data[n].read_ol.hEvent) CloseHandle(fd_data[n].read_ol.hEvent); } if (pi.hProcess) CloseHandle(pi.hProcess); talloc_free(tmp); }