mirror of
https://github.com/thinkonmay/sunshine-sdk.git
synced 2025-12-28 08:01:04 +00:00
fix build by removing boost process
This commit is contained in:
parent
51fa4acb22
commit
623c6b7a77
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
#include <initguid.h>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
// 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.
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/process.hpp>
|
||||
#include <boost/program_options/parsers.hpp>
|
||||
|
||||
// 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<WCHAR, 128> 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<WCHAR, MAX_PATH> 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<WCHAR, MAX_PATH> path;
|
||||
std::array<PCWCHAR, 2> 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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user