diff --git a/src/platform/common.h b/src/platform/common.h index 5430c0ac..9b82487a 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -623,9 +623,6 @@ namespace platf { bool needs_encoder_reenumeration(); - boost::process::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); - enum class thread_priority_e : int { low, normal, diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 6a7f25e9..195222c7 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -238,47 +238,8 @@ namespace platf { return "00:00:00:00:00:00"s; } - bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - if (!group) { - if (!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); - } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); - } - } - else { - if (!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); - } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); - } - } - } - /** - * @brief Open a url in the default web browser. - * @param url The url to open. - */ - void - open_url(const std::string &url) { - // set working dir to user home directory - auto working_dir = boost::filesystem::path(std::getenv("HOME")); - std::string cmd = R"(xdg-open ")" + url + R"(")"; - boost::process::environment _env = boost::this_process::environment(); - std::error_code ec; - auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); - if (ec) { - BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { - BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; - child.detach(); - } - } void adjust_thread_priority(thread_priority_e priority) { diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index d6028781..26cb4476 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -167,46 +167,6 @@ namespace platf { return "00:00:00:00:00:00"s; } - bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - if (!group) { - if (!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); - } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); - } - } - else { - if (!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); - } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); - } - } - } - - /** - * @brief Open a url in the default web browser. - * @param url The url to open. - */ - void - open_url(const std::string &url) { - boost::filesystem::path working_dir; - std::string cmd = R"(open ")" + url + R"(")"; - - boost::process::environment _env = boost::this_process::environment(); - std::error_code ec; - auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); - if (ec) { - BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { - BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; - child.detach(); - } - } void adjust_thread_priority(thread_priority_e priority) { diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 84d8e671..158fdc7a 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -6,7 +6,6 @@ #include #include -#include // We have to include boost/process.hpp before display.h due to WinSock.h, // but that prevents the definition of NTSTATUS so we must define it ourself. @@ -24,7 +23,6 @@ namespace platf { using namespace std::literals; } namespace platf::dxgi { - namespace bp = boost::process; /** * DDAPI-specific initialization goes here. @@ -283,67 +281,7 @@ namespace platf::dxgi { return true; } - // On hybrid graphics systems, Windows will change the order of GPUs reported by - // DXGI in accordance with the user's GPU preference. If the selected GPU is a - // render-only device with no displays, DXGI will add virtual outputs to the - // that device to avoid confusing applications. While this works properly for most - // applications, it breaks the Desktop Duplication API because DXGI doesn't proxy - // the virtual DXGIOutput to the real GPU it is attached to. When trying to call - // DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED - // (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the - // virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process, - // we spawn a helper tool to probe for us before we set our own GPU preference. - bool - probe_for_gpu_preference(const std::string &display_name) { - // If we've already been through here, there's nothing to do this time. - static bool set_gpu_preference = false; - if (set_gpu_preference) { - return true; - } - std::string cmd = "ddprobe.exe"; - - // We start at 1 because 0 is automatic selection which can be overridden by - // the GPU driver control panel options. Since ddprobe.exe can have different - // GPU driver overrides than Sunshine.exe, we want to avoid a scenario where - // autoselection might work for ddprobe.exe but not for us. - for (int i = 1; i < 5; i++) { - // Run the probe tool. It returns the status of DuplicateOutput(). - // - // Arg format: [GPU preference] [Display name] - HRESULT result; - try { - result = bp::system(cmd, std::to_string(i), display_name, bp::std_out > bp::null, bp::std_err > bp::null); - } - catch (bp::process_error &e) { - BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what(); - return false; - } - - BOOST_LOG(debug) << "ddprobe.exe ["sv << i << "] ["sv << display_name << "] returned: 0x"sv << util::hex(result).to_string_view(); - - // E_ACCESSDENIED can happen at the login screen. If we get this error, - // we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED - // would have been raised first if it wasn't. - if (result == S_OK || result == E_ACCESSDENIED) { - // We found a working GPU preference, so set ourselves to use that. - if (set_gpu_preference_on_self(i)) { - set_gpu_preference = true; - return true; - } - else { - return false; - } - } - else { - // This configuration didn't work, so continue testing others - continue; - } - } - - // If none of the manual options worked, leave the GPU preference alone - return false; - } /** * @brief Tests to determine if the Desktop Duplication API can capture the given output. @@ -441,11 +379,6 @@ namespace platf::dxgi { HRESULT status; - // We must set the GPU preference before calling any DXGI APIs! - if (!probe_for_gpu_preference(display_name)) { - BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; - } - status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; @@ -1004,11 +937,6 @@ namespace platf { BOOST_LOG(debug) << "Detecting monitors..."sv; - // We must set the GPU preference before calling any DXGI APIs! - if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { - BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; - } - // We sync the thread desktop once before we start the enumeration process // to ensure test_dxgi_duplication() returns consistent results for all GPUs // even if the current desktop changes during our enumeration process. diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index e1341ab8..ff7277ac 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -10,7 +10,6 @@ #include #include -#include #include // prevent clang format from "optimizing" the header include order @@ -60,7 +59,6 @@ #define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD) (_minor)) << 16 | (_major)) #endif -namespace bp = boost::process; using namespace std::literals; namespace platf { @@ -299,77 +297,6 @@ namespace platf { return userToken; } - bool - merge_user_environment_block(bp::environment &env, HANDLE shell_token) { - // Get the target user's environment block - PVOID env_block; - if (!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) { - return false; - } - - // Parse the environment block and populate env - for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { - // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = to_utf8(std::wstring { c }); - std::string env_name = env_tuple.substr(0, env_tuple.find('=')); - std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); - - // Perform a case-insensitive search to see if this variable name already exists - auto itr = std::find_if(env.cbegin(), env.cend(), - [&](const auto &e) { return boost::iequals(e.get_name(), env_name); }); - if (itr != env.cend()) { - // Use this existing name if it is already present to ensure we merge properly - env_name = itr->get_name(); - } - - // For the PATH variable, we will merge the values together - if (boost::iequals(env_name, "PATH")) { - env[env_name] = env_val + ";" + env[env_name].to_string(); - } - else { - // Other variables will be superseded by those in the user's environment block - env[env_name] = env_val; - } - } - - DestroyEnvironmentBlock(env_block); - return true; - } - - /** - * @brief Check if the current process is running with system-level privileges. - * @return `true` if the current process has system-level privileges, `false` otherwise. - */ - bool - is_running_as_system() { - BOOL ret; - PSID SystemSid; - DWORD dwSize = SECURITY_MAX_SID_SIZE; - - // Allocate memory for the SID structure - SystemSid = LocalAlloc(LMEM_FIXED, dwSize); - if (SystemSid == nullptr) { - BOOST_LOG(error) << "Failed to allocate memory for the SID structure: " << GetLastError(); - return false; - } - - // Create a SID for the local system account - ret = CreateWellKnownSid(WinLocalSystemSid, nullptr, SystemSid, &dwSize); - if (ret) { - // Check if the current process token contains this SID - if (!CheckTokenMembership(nullptr, SystemSid, &ret)) { - BOOST_LOG(error) << "Failed to check token membership: " << GetLastError(); - ret = false; - } - } - else { - BOOST_LOG(error) << "Failed to create a SID for the local system account. This may happen if the system is out of memory or if the SID buffer is too small: " << GetLastError(); - } - - // Free the memory allocated for the SID structure - LocalFree(SystemSid); - return ret; - } // Note: This does NOT append a null terminator void @@ -378,35 +305,6 @@ namespace platf { offset += wstr.length(); } - std::wstring - create_environment_block(bp::environment &env) { - int size = 0; - for (const auto &entry : env) { - auto name = entry.get_name(); - auto value = entry.to_string(); - size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\0' */; - } - - size += 1 /* L'\0' */; - - wchar_t env_block[size]; - int offset = 0; - for (const auto &entry : env) { - auto name = entry.get_name(); - auto value = entry.to_string(); - - // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, from_utf8(name)); - env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, from_utf8(value)); - env_block[offset++] = L'\0'; - } - - // Append a final null terminator - env_block[offset++] = L'\0'; - - return std::wstring(env_block, offset); - } LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { @@ -432,45 +330,7 @@ namespace platf { HeapFree(GetProcessHeap(), 0, list); } - /** - * @brief Create a `bp::child` object from the results of launching a process. - * @param process_launched A boolean indicating if the launch was successful. - * @param cmd The command that was used to launch the process. - * @param ec A reference to an `std::error_code` object that will store any error that occurred during the launch. - * @param process_info A reference to a `PROCESS_INFORMATION` structure that contains information about the new process. - * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch failed. - */ - bp::child - create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info) { - // Use RAII to ensure the process is closed when we're done with it, even if there was an error. - auto close_process_handles = util::fail_guard([process_launched, process_info]() { - if (process_launched) { - CloseHandle(process_info.hThread); - CloseHandle(process_info.hProcess); - } - }); - if (ec) { - // If there was an error, return an empty bp::child object - return bp::child(); - } - - if (process_launched) { - // If the launch was successful, create a new bp::child object representing the new process - auto child = bp::child((bp::pid_t) process_info.dwProcessId); - BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); - return child; - } - else { - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to launch process: "sv << winerror; - ec = std::make_error_code(std::errc::invalid_argument); - // We must NOT attach the failed process here, since this case can potentially be induced by ACL - // manipulation (denying yourself execute permission) to cause an escalation of privilege. - // So to protect ourselves against that, we'll return an empty child process instead. - return bp::child(); - } - } /** * @brief Impersonate the current user and invoke the callback function. @@ -509,563 +369,6 @@ namespace platf { return ec; } - /** - * @brief A function to create a `STARTUPINFOEXW` structure for launching a process. - * @param file A pointer to a `FILE` object that will be used as the standard output and error for the new process, or null if not needed. - * @param job A job object handle to insert the new process into. This pointer must remain valid for the life of this startup info! - * @param ec A reference to a `std::error_code` object that will store any error that occurred during the creation of the structure. - * @return A `STARTUPINFOEXW` structure that contains information about how to launch the new process. - */ - STARTUPINFOEXW - create_startup_info(FILE *file, HANDLE *job, std::error_code &ec) { - // Initialize a zeroed-out STARTUPINFOEXW structure and set its size - STARTUPINFOEXW startup_info = {}; - startup_info.StartupInfo.cb = sizeof(startup_info); - - // Allocate a process attribute list with space for 2 elements - startup_info.lpAttributeList = allocate_proc_thread_attr_list(2); - if (startup_info.lpAttributeList == NULL) { - // If the allocation failed, set ec to an appropriate error code and return the structure - ec = std::make_error_code(std::errc::not_enough_memory); - return startup_info; - } - - if (file) { - // If a file was provided, get its handle and use it as the standard output and error for the new process - HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file)); - - // Populate std handles if the caller gave us a log file to use - startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; - startup_info.StartupInfo.hStdInput = NULL; - startup_info.StartupInfo.hStdOutput = log_file_handle; - startup_info.StartupInfo.hStdError = log_file_handle; - - // Allow the log file handle to be inherited by the child process (without inheriting all of - // our inheritable handles, such as our own log file handle created by SunshineSvc). - // - // Note: The value we point to here must be valid for the lifetime of the attribute list, - // so we need to point into the STARTUPINFO instead of our log_file_variable on the stack. - UpdateProcThreadAttribute(startup_info.lpAttributeList, - 0, - PROC_THREAD_ATTRIBUTE_HANDLE_LIST, - &startup_info.StartupInfo.hStdOutput, - sizeof(startup_info.StartupInfo.hStdOutput), - NULL, - NULL); - } - - if (job) { - // Atomically insert the new process into the specified job. - // - // Note: The value we point to here must be valid for the lifetime of the attribute list, - // so we take a HANDLE* instead of just a HANDLE to use the caller's stack storage. - UpdateProcThreadAttribute(startup_info.lpAttributeList, - 0, - PROC_THREAD_ATTRIBUTE_JOB_LIST, - job, - sizeof(*job), - NULL, - NULL); - } - - return startup_info; - } - - /** - * @brief This function overrides HKEY_CURRENT_USER and HKEY_CLASSES_ROOT using the provided token. - * @param token The primary token identifying the user to use, or `NULL` to restore original keys. - * @return `true` if the override or restore operation was successful. - */ - bool - override_per_user_predefined_keys(HANDLE token) { - HKEY user_classes_root = NULL; - if (token) { - auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root); - if (err != ERROR_SUCCESS) { - BOOST_LOG(error) << "Failed to open classes root for target user: "sv << err; - return false; - } - } - auto close_classes_root = util::fail_guard([user_classes_root]() { - if (user_classes_root) { - RegCloseKey(user_classes_root); - } - }); - - HKEY user_key = NULL; - if (token) { - impersonate_current_user(token, [&]() { - // RegOpenCurrentUser() doesn't take a token. It assumes we're impersonating the desired user. - auto err = RegOpenCurrentUser(GENERIC_ALL, &user_key); - if (err != ERROR_SUCCESS) { - BOOST_LOG(error) << "Failed to open user key for target user: "sv << err; - user_key = NULL; - } - }); - if (!user_key) { - return false; - } - } - auto close_user = util::fail_guard([user_key]() { - if (user_key) { - RegCloseKey(user_key); - } - }); - - auto err = RegOverridePredefKey(HKEY_CLASSES_ROOT, user_classes_root); - if (err != ERROR_SUCCESS) { - BOOST_LOG(error) << "Failed to override HKEY_CLASSES_ROOT: "sv << err; - return false; - } - - err = RegOverridePredefKey(HKEY_CURRENT_USER, user_key); - if (err != ERROR_SUCCESS) { - BOOST_LOG(error) << "Failed to override HKEY_CURRENT_USER: "sv << err; - RegOverridePredefKey(HKEY_CLASSES_ROOT, NULL); - return false; - } - - return true; - } - - /** - * @brief This function quotes/escapes an argument according to the Windows parsing convention. - * @param argument The raw argument to process. - * @return An argument string suitable for use by CreateProcess(). - */ - std::wstring - escape_argument(const std::wstring &argument) { - // If there are no characters requiring quoting/escaping, we're done - if (argument.find_first_of(L" \t\n\v\"") == argument.npos) { - return argument; - } - - // The algorithm implemented here comes from a MSDN blog post: - // https://web.archive.org/web/20120201194949/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx - std::wstring escaped_arg; - escaped_arg.push_back(L'"'); - for (auto it = argument.begin();; it++) { - auto backslash_count = 0U; - while (it != argument.end() && *it == L'\\') { - it++; - backslash_count++; - } - - if (it == argument.end()) { - escaped_arg.append(backslash_count * 2, L'\\'); - break; - } - else if (*it == L'"') { - escaped_arg.append(backslash_count * 2 + 1, L'\\'); - } - else { - escaped_arg.append(backslash_count, L'\\'); - } - - escaped_arg.push_back(*it); - } - escaped_arg.push_back(L'"'); - return escaped_arg; - } - - /** - * @brief This function escapes an argument according to cmd's parsing convention. - * @param argument An argument already escaped by `escape_argument()`. - * @return An argument string suitable for use by cmd.exe. - */ - std::wstring - escape_argument_for_cmd(const std::wstring &argument) { - // Start with the original string and modify from there - std::wstring escaped_arg = argument; - - // Look for the next cmd metacharacter - size_t match_pos = 0; - while ((match_pos = escaped_arg.find_first_of(L"()%!^\"<>&|", match_pos)) != std::wstring::npos) { - // Insert an escape character and skip past the match - escaped_arg.insert(match_pos, 1, L'^'); - match_pos += 2; - } - - return escaped_arg; - } - - /** - * @brief This function resolves the given raw command into a proper command string for CreateProcess(). - * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute(). - * @param raw_cmd The raw command provided by the user. - * @param working_dir The working directory for the new process. - * @param token The user token currently being impersonated or `NULL` if running as ourselves. - * @param creation_flags The creation flags for CreateProcess(), which may be modified by this function. - * @return A command string suitable for use by CreateProcess(). - */ - std::wstring - resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { - std::wstring raw_cmd_w = from_utf8(raw_cmd); - - // First, convert the given command into parts so we can get the executable/file/URL without parameters - auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); - if (raw_cmd_parts.empty()) { - // This is highly unexpected, but we'll just return the raw string and hope for the best. - BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; - return from_utf8(raw_cmd); - } - - auto raw_target = raw_cmd_parts.at(0); - std::wstring lookup_string; - HRESULT res; - - if (PathIsURLW(raw_target.c_str())) { - std::array scheme; - - DWORD out_len = scheme.size(); - res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0); - if (res != S_OK) { - BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']'; - return from_utf8(raw_cmd); - } - - // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') - lookup_string = scheme.data(); - } - else { - // If the target is not a URL, assume it's a regular file path - auto extension = PathFindExtensionW(raw_target.c_str()); - if (extension == nullptr || *extension == 0) { - // If the file has no extension, assume it's a command and allow CreateProcess() - // to try to find it via PATH - return from_utf8(raw_cmd); - } - else if (boost::iequals(extension, L".exe")) { - // If the file has an .exe extension, we will bypass the resolution here and - // directly pass the unmodified command string to CreateProcess(). The argument - // escaping rules are subtly different between CreateProcess() and ShellExecute(), - // and we want to preserve backwards compatibility with older configs. - return from_utf8(raw_cmd); - } - - // For regular files, the class is found using the file extension (including the dot) - lookup_string = extension; - } - - std::array shell_command_string; - bool needs_cmd_escaping = false; - { - // Overriding these predefined keys affects process-wide state, so serialize all calls - // to ensure the handle state is consistent while we perform the command query. - static std::mutex per_user_key_mutex; - auto lg = std::lock_guard(per_user_key_mutex); - - // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info - if (!override_per_user_predefined_keys(token)) { - return from_utf8(raw_cmd); - } - - // Find the command string for the specified class - DWORD out_len = shell_command_string.size(); - res = AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, lookup_string.c_str(), L"open", shell_command_string.data(), &out_len); - - // In some cases (UWP apps), we might not have a command for this target. If that happens, - // we'll have to launch via cmd.exe. This prevents proper job tracking, but that was already - // broken for UWP apps anyway due to how they are started by Windows. Even 'start /wait' - // doesn't work properly for UWP, so really no termination tracking seems to work at all. - // - // FIXME: Maybe we can improve this in the future. - if (res == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { - BOOST_LOG(warning) << "Using trampoline to handle target: "sv << raw_cmd; - std::wcscpy(shell_command_string.data(), L"cmd.exe /c start \"\" /wait \"%1\" %*"); - needs_cmd_escaping = true; - - // We must suppress the console window that would otherwise appear when starting cmd.exe. - creation_flags &= ~CREATE_NEW_CONSOLE; - creation_flags |= CREATE_NO_WINDOW; - - res = S_OK; - } - - // Reset per-user keys back to the original value - override_per_user_predefined_keys(NULL); - } - - if (res != S_OK) { - BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; - return from_utf8(raw_cmd); - } - - // Finally, construct the real command string that will be passed into CreateProcess(). - // We support common substitutions (%*, %1, %2, %L, %W, %V, etc), but there are other - // uncommon ones that are unsupported here. - // - // https://web.archive.org/web/20111002101214/http://msdn.microsoft.com/en-us/library/windows/desktop/cc144101(v=vs.85).aspx - std::wstring cmd_string { shell_command_string.data() }; - size_t match_pos = 0; - while ((match_pos = cmd_string.find_first_of(L'%', match_pos)) != std::wstring::npos) { - std::wstring match_replacement; - - // If no additional character exists after the match, the dangling '%' is stripped - if (match_pos + 1 == cmd_string.size()) { - cmd_string.erase(match_pos, 1); - break; - } - - // Shell command replacements are strictly '%' followed by a single non-'%' character - auto next_char = std::tolower(cmd_string.at(match_pos + 1)); - switch (next_char) { - // Escape character - case L'%': - match_replacement = L'%'; - break; - - // Argument replacements - case L'0': - case L'1': - case L'2': - case L'3': - case L'4': - case L'5': - case L'6': - case L'7': - case L'8': - case L'9': { - // Arguments numbers are 1-based, except for %0 which is equivalent to %1 - int index = next_char - L'0'; - if (next_char != L'0') { - index--; - } - - // Replace with the matching argument, or nothing if the index is invalid - if (index < raw_cmd_parts.size()) { - match_replacement = raw_cmd_parts.at(index); - } - break; - } - - // All arguments following the target - case L'*': - for (int i = 1; i < raw_cmd_parts.size(); i++) { - // Insert a space before arguments after the first one - if (i > 1) { - match_replacement += L' '; - } - - // Argument escaping applies only to %*, not the single substitutions like %2 - auto escaped_argument = escape_argument(raw_cmd_parts.at(i)); - if (needs_cmd_escaping) { - // If we're using the cmd.exe trampoline, we'll need to add additional escaping - escaped_argument = escape_argument_for_cmd(escaped_argument); - } - match_replacement += escaped_argument; - } - break; - - // Long file path of target - case L'l': - case L'd': - case L'v': { - std::array path; - std::array other_dirs { working_dir.c_str(), nullptr }; - - // PathFindOnPath() is a little gross because it uses the same - // buffer for input and output, so we need to copy our input - // into the path array. - std::wcsncpy(path.data(), raw_target.c_str(), path.size()); - if (path[path.size() - 1] != 0) { - // The path was so long it was truncated by this copy. We'll - // assume it was an absolute path (likely) and use it unmodified. - match_replacement = raw_target; - } - // See if we can find the path on our search path or working directory - else if (PathFindOnPathW(path.data(), other_dirs.data())) { - match_replacement = std::wstring { path.data() }; - } - else { - // We couldn't find the target, so we'll just hope for the best - match_replacement = raw_target; - } - break; - } - - // Working directory - case L'w': - match_replacement = working_dir; - break; - - default: - BOOST_LOG(warning) << "Unsupported argument replacement: %%" << next_char; - break; - } - - // Replace the % and following character with the match replacement - cmd_string.replace(match_pos, 2, match_replacement); - - // Skip beyond the match replacement itself to prevent recursive replacement - match_pos += match_replacement.size(); - } - - BOOST_LOG(info) << "Resolved user-provided command '"sv << raw_cmd << "' to '"sv << cmd_string << '\''; - return cmd_string; - } - - /** - * @brief Run a command on the users profile. - * - * Launches a child process as the user, using the current user's environment and a specific working directory. - * - * @param elevated Specify whether to elevate the process. - * @param interactive Specify whether this will run in a window or hidden. - * @param cmd The command to run. - * @param working_dir The working directory for the new process. - * @param env The environment variables to use for the new process. - * @param file A file object to redirect the child process's output to (may be `nullptr`). - * @param ec An error code, set to indicate any errors that occur during the launch process. - * @param group A pointer to a `bp::group` object to which the new process should belong (may be `nullptr`). - * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. - */ - bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - std::wstring start_dir = from_utf8(working_dir.string()); - HANDLE job = group ? group->native_handle() : nullptr; - STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); - PROCESS_INFORMATION process_info; - - // Clone the environment to create a local copy. Boost.Process (bp) shares the environment with all spawned processes. - // Since we're going to modify the 'env' variable by merging user-specific environment variables into it, - // we make a clone to prevent side effects to the shared environment. - bp::environment cloned_env = env; - - if (ec) { - // In the event that startup_info failed, return a blank child process. - return bp::child(); - } - - // Use RAII to ensure the attribute list is freed when we're done with it - auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { - free_proc_thread_attr_list(list); - }); - - DWORD creation_flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB; - - // Create a new console for interactive processes and use no console for non-interactive processes - creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; - - // Find the PATH variable in our environment block using a case-insensitive search - auto sunshine_wenv = boost::this_process::wenvironment(); - std::wstring path_var_name { L"PATH" }; - std::wstring old_path_val; - auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), path_var_name); }); - if (itr != sunshine_wenv.cend()) { - // Use the existing variable if it exists, since Boost treats these as case-sensitive. - path_var_name = itr->get_name(); - old_path_val = sunshine_wenv[path_var_name].to_string(); - } - - // Temporarily prepend the specified working directory to PATH to ensure CreateProcess() - // will (preferentially) find binaries that reside in the working directory. - sunshine_wenv[path_var_name].assign(start_dir + L";" + old_path_val); - - // Restore the old PATH value for our process when we're done here - auto restore_path = util::fail_guard([&]() { - if (old_path_val.empty()) { - sunshine_wenv[path_var_name].clear(); - } - else { - sunshine_wenv[path_var_name].assign(old_path_val); - } - }); - - BOOL ret; - if (is_running_as_system()) { - // Duplicate the current user's token - HANDLE user_token = retrieve_users_token(elevated); - if (!user_token) { - // Fail the launch rather than risking launching with Sunshine's permissions unmodified. - ec = std::make_error_code(std::errc::permission_denied); - return bp::child(); - } - - // Use RAII to ensure the shell token is closed when we're done with it - auto token_close = util::fail_guard([user_token]() { - CloseHandle(user_token); - }); - - // Populate env with user-specific environment variables - if (!merge_user_environment_block(cloned_env, user_token)) { - ec = std::make_error_code(std::errc::not_enough_memory); - return bp::child(); - } - - // Open the process as the current user account, elevation is handled in the token itself. - ec = impersonate_current_user(user_token, [&]() { - std::wstring env_block = create_environment_block(cloned_env); - std::wstring wcmd = resolve_command_string(cmd, start_dir, user_token, creation_flags); - ret = CreateProcessAsUserW(user_token, - NULL, - (LPWSTR) wcmd.c_str(), - NULL, - NULL, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - creation_flags, - env_block.data(), - start_dir.empty() ? NULL : start_dir.c_str(), - (LPSTARTUPINFOW) &startup_info, - &process_info); - }); - } - // Otherwise, launch the process using CreateProcessW() - // This will inherit the elevation of whatever the user launched Sunshine with. - else { - // Open our current token to resolve environment variables - HANDLE process_token; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &process_token)) { - ec = std::make_error_code(std::errc::permission_denied); - return bp::child(); - } - auto token_close = util::fail_guard([process_token]() { - CloseHandle(process_token); - }); - - // Populate env with user-specific environment variables - if (!merge_user_environment_block(cloned_env, process_token)) { - ec = std::make_error_code(std::errc::not_enough_memory); - return bp::child(); - } - - std::wstring env_block = create_environment_block(cloned_env); - std::wstring wcmd = resolve_command_string(cmd, start_dir, NULL, creation_flags); - ret = CreateProcessW(NULL, - (LPWSTR) wcmd.c_str(), - NULL, - NULL, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - creation_flags, - env_block.data(), - start_dir.empty() ? NULL : start_dir.c_str(), - (LPSTARTUPINFOW) &startup_info, - &process_info); - } - - // Use the results of the launch to create a bp::child object - return create_boost_child_from_results(ret, cmd, ec, process_info); - } - - /** - * @brief Open a url in the default web browser. - * @param url The url to open. - */ - void - open_url(const std::string &url) { - boost::process::environment _env = boost::this_process::environment(); - auto working_dir = boost::filesystem::path(); - std::error_code ec; - - auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr); - if (ec) { - BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { - BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; - child.detach(); - } - } void adjust_thread_priority(thread_priority_e priority) {