diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a141f7d5..9298ce63 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -388,7 +388,8 @@ jobs: libboost-filesystem1.71-dev \ libboost-log1.71-dev \ libboost-regex1.71-dev \ - libboost-thread1.71-dev + libboost-thread1.71-dev \ + libboost-program-options1.71-dev # Install cmake wget https://cmake.org/files/v3.22/cmake-3.22.2-linux-x86_64.sh @@ -412,7 +413,8 @@ jobs: cmake \ libboost-filesystem-dev \ libboost-log-dev \ - libboost-thread-dev + libboost-thread-dev \ + libboost-program-options-dev fi sudo apt-get install -y \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 299d2374..c7ec75fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,7 +68,7 @@ pkg_check_modules (CURL REQUIRED libcurl) if(NOT APPLE) set(Boost_USE_STATIC_LIBS ON) endif() -find_package(Boost COMPONENTS log filesystem REQUIRED) +find_package(Boost COMPONENTS log filesystem program_options REQUIRED) list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare) @@ -122,6 +122,7 @@ if(WIN32) d3d11 dxgi D3DCompiler setupapi dwmapi + userenv synchronization.lib ${CURL_STATIC_LIBRARIES} ) @@ -658,8 +659,8 @@ elseif(UNIX) # Dependencies set(CPACK_DEB_COMPONENT_INSTALL ON) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "openssl, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libcurl4, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2") - set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1, boost-thread >= 1.67.0, boost-filesystem >= 1.67.0, boost-log >= 1.67.0, libcurl >= 7.0, pulseaudio-libs >= 10.0, libopusenc >= 0.2.1, libxcb >= 1.13, libXtst >= 1.2.3, libevdev >= 1.5.6, libdrm >= 2.4.97, libcap >= 2.22") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "openssl, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libboost-program-options1.67.0 | libboost-program-options1.71.0 | libboost-program-options1.74.0, libcurl4, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2") + set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1, boost-thread >= 1.67.0, boost-filesystem >= 1.67.0, boost-log >= 1.67.0, boost-program-options >= 1.67.0, libcurl >= 7.0, pulseaudio-libs >= 10.0, libopusenc >= 0.2.1, libxcb >= 1.13, libXtst >= 1.2.3, libevdev >= 1.5.6, libdrm >= 2.4.97, libcap >= 2.22") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) # This should automatically figure out dependencies, doesn't work with the current config endif() endif() diff --git a/Dockerfile b/Dockerfile index 94d24ba1..9ff9d0d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN apt-get update -y \ libboost-filesystem-dev=1.74.0* \ libboost-log-dev=1.74.0* \ libboost-thread-dev=1.74.0* \ + libboost-program-options-dev=1.74.0* \ libcap-dev=1:2.44* \ libcurl4-openssl-dev=7.81.0* \ libdrm-dev=2.4.110* \ diff --git a/docs/source/building/linux.rst b/docs/source/building/linux.rst index 943f1027..0b1c6ffe 100644 --- a/docs/source/building/linux.rst +++ b/docs/source/building/linux.rst @@ -18,6 +18,7 @@ Install Requirements libboost-filesystem-dev \ libboost-log-dev \ libboost-thread-dev \ + libboost-program-options-dev \ libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ @@ -104,6 +105,7 @@ Install Requirements libboost-log1.71-dev \ libboost-regex1.71-dev \ libboost-thread1.71-dev \ + libboost-program-options1.71-dev \ libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ @@ -160,6 +162,7 @@ Install Requirements libboost-filesystem-dev \ libboost-log-dev \ libboost-thread-dev \ + libboost-program-options-dev \ libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ @@ -206,6 +209,7 @@ Install Requirements libboost-filesystem-dev \ libboost-log-dev \ libboost-thread-dev \ + libboost-program-options-dev \ libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ diff --git a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml index 2dde26bf..a27e4e7a 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml @@ -33,7 +33,7 @@ modules: buildsystem: simple build-commands: - cd tools/build && bison -y -d -o src/engine/jamgram.cpp src/engine/jamgram.y - - ./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=system,thread,log || cat bootstrap.log + - ./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=system,thread,log,program_options || cat bootstrap.log - ./b2 install variant=release link=static,shared runtime-link=shared cxxflags="$CXXFLAGS" linkflags="$LDFLAGS" -j $FLATPAK_BUILDER_N_JOBS sources: diff --git a/src/platform/common.h b/src/platform/common.h index 7c8828a8..e01a9595 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -17,6 +17,20 @@ struct sockaddr; struct AVFrame; +// Forward declarations of boost classes to avoid having to include boost headers +// here, which results in issues with Windows.h and WinSock2.h include order. +namespace boost { +namespace filesystem { +class path; +} +namespace process { +class child; +template +class basic_environment; +typedef basic_environment environment; +} // namespace process +} // namespace boost + namespace platf { constexpr auto MAX_GAMEPADS = 32; @@ -289,6 +303,8 @@ std::shared_ptr display(mem_type_e hwdevice_type, const std::string & // A list of names of displays accepted as display_name with the mem_type_e std::vector display_names(mem_type_e hwdevice_type); +boost::process::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec); + input_t input(); void move_mouse(input_t &input, int deltaX, int deltaY); void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 36896dd9..1fbf9c5b 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -14,6 +14,8 @@ #include "src/main.h" #include "src/platform/common.h" +#include + #ifdef __GNUC__ #define SUNSHINE_GNUC_EXTENSION __extension__ #else @@ -22,6 +24,7 @@ using namespace std::literals; namespace fs = std::filesystem; +namespace bp = boost::process; window_system_e window_system; @@ -140,6 +143,16 @@ std::string get_mac_address(const std::string_view &address) { return "00:00:00:00:00:00"s; } +bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) { + BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + 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); + } +} + namespace source { enum source_e : std::size_t { #ifdef SUNSHINE_BUILD_CUDA diff --git a/src/platform/macos/misc.cpp b/src/platform/macos/misc.cpp index a7b0a89f..64a7ef42 100644 --- a/src/platform/macos/misc.cpp +++ b/src/platform/macos/misc.cpp @@ -9,10 +9,14 @@ #include "src/main.h" #include "src/platform/common.h" +#include + using namespace std::literals; namespace fs = std::filesystem; +namespace bp = boost::process; namespace platf { + std::unique_ptr init() { if(!CGPreflightScreenCaptureAccess()) { BOOST_LOG(error) << "No screen capture permission!"sv; @@ -116,6 +120,17 @@ std::string get_mac_address(const std::string_view &address) { BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; return "00:00:00:00:00:00"s; } + +bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) { + BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + 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); + } +} + } // namespace platf namespace dyn { diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index b9d08543..4967d815 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include // prevent clang format from "optimizing" the header include order // clang-format off @@ -10,11 +12,14 @@ #include #include #include +#include // clang-format on #include "src/main.h" #include "src/utility.h" +namespace bp = boost::process; + using namespace std::literals; namespace platf { using adapteraddrs_t = util::c_ptr; @@ -120,4 +125,277 @@ void print_status(const std::string_view &prefix, HRESULT status) { BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes }; } + +std::wstring utf8_to_wide_string(const std::string &str) { + // Determine the size required for the destination string + int chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), NULL, 0); + + // Allocate it + wchar_t buffer[chars] = {}; + + // Do the conversion for real + chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), buffer, chars); + return std::wstring(buffer, chars); +} + +std::string wide_to_utf8_string(const std::wstring &str) { + // Determine the size required for the destination string + int bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), NULL, 0, NULL, NULL); + + // Allocate it + char buffer[bytes] = {}; + + // Do the conversion for real + bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), buffer, bytes, NULL, NULL); + return std::string(buffer, bytes); +} + +HANDLE duplicate_shell_token() { + // Get the shell window (will usually be owned by explorer.exe) + HWND shell_window = GetShellWindow(); + if(!shell_window) { + BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv; + return NULL; + } + + // Open a handle to the explorer.exe process + DWORD shell_pid; + GetWindowThreadProcessId(shell_window, &shell_pid); + HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid); + if(!shell_process) { + BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError(); + return NULL; + } + + // Open explorer's token to clone for process creation + HANDLE shell_token; + BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token); + CloseHandle(shell_process); + if(!ret) { + BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError(); + return NULL; + } + + // Duplicate the token to make it usable for process creation + HANDLE new_token; + ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token); + CloseHandle(shell_token); + if(!ret) { + BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError(); + return NULL; + } + + return new_token; +} + +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 = wide_to_utf8_string(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; +} + +// Note: This does NOT append a null terminator +void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { + std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t)); + 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 += utf8_to_wide_string(name).length() + 1 /* L'=' */ + utf8_to_wide_string(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, utf8_to_wide_string(name)); + env_block[offset++] = L'='; + append_string_to_environment_block(env_block, offset, utf8_to_wide_string(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) { + SIZE_T size; + InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size); + + auto list = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, size); + if(list == NULL) { + return NULL; + } + + if(!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) { + HeapFree(GetProcessHeap(), 0, list); + return NULL; + } + + return list; +} + +void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { + DeleteProcThreadAttributeList(list); + HeapFree(GetProcessHeap(), 0, list); +} + +bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) { + HANDLE shell_token = duplicate_shell_token(); + if(!shell_token) { + // This can happen if the shell has crashed. Fail the launch rather than risking launching with + // Sunshine's permissions unmodified. + ec = std::make_error_code(std::errc::no_such_process); + return bp::child(); + } + + auto token_close = util::fail_guard([shell_token]() { + CloseHandle(shell_token); + }); + + // Populate env with user-specific environment variables + if(!merge_user_environment_block(env, shell_token)) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); + } + + // Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16 + std::wstring wcmd = utf8_to_wide_string(cmd); + std::wstring env_block = create_environment_block(env); + std::wstring start_dir = utf8_to_wide_string(working_dir.string()); + + STARTUPINFOEXW startup_info = {}; + startup_info.StartupInfo.cb = sizeof(startup_info); + + // Allocate a process attribute list with space for 1 element + startup_info.lpAttributeList = allocate_proc_thread_attr_list(1); + if(startup_info.lpAttributeList == NULL) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); + } + + auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { + free_proc_thread_attr_list(list); + }); + + if(file) { + 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). + UpdateProcThreadAttribute(startup_info.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + &log_file_handle, + sizeof(log_file_handle), + NULL, + NULL); + } + + // Impersonate the user when launching the process. This will ensure that appropriate access + // checks are done against the user token, not our SYSTEM token. It will also allow network + // shares and mapped network drives to be used as launch targets, since those credentials + // are stored per-user. + if(!ImpersonateLoggedOnUser(shell_token)) { + auto winerror = GetLastError(); + BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; + ec = std::make_error_code(std::errc::permission_denied); + return bp::child(); + } + + // Launch the process with the duplicated shell token. + // Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated. + // Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified. + PROCESS_INFORMATION process_info; + BOOL ret = CreateProcessAsUserW(shell_token, + NULL, + (LPWSTR)wcmd.c_str(), + NULL, + NULL, + !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, + env_block.data(), + start_dir.empty() ? NULL : start_dir.c_str(), + (LPSTARTUPINFOW)&startup_info, + &process_info); + + // End impersonation of the logged on user. If this fails (which is extremely unlikely), + // we will be running with an unknown user token. The only safe thing to do in that case + // is terminate ourselves. + if(!RevertToSelf()) { + auto winerror = GetLastError(); + BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; + std::abort(); + } + + if(ret) { + // Since we are always spawning a process with a less privileged token than ourselves, + // bp::child() should have no problem opening it with any access rights it wants. + auto child = bp::child((bp::pid_t)process_info.dwProcessId); + + // Only close handles after bp::child() has opened the process. If the process terminates + // quickly, the PID could be reused if we close the process handle. + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + + BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); + return child; + } + else { + // We must NOT try bp::child() here, since this case can potentially be induced by ACL + // manipulation (denying yourself execute permission) to cause an escalation of privilege. + auto winerror = GetLastError(); + BOOST_LOG(error) << "Failed to launch process: "sv << winerror; + ec = std::make_error_code(std::errc::invalid_argument); + return bp::child(); + } +} + } // namespace platf \ No newline at end of file diff --git a/src/process.cpp b/src/process.cpp index 8783db8c..44be2e83 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -10,12 +10,19 @@ #include #include +#include #include #include #include "main.h" +#include "platform/common.h" #include "utility.h" +#ifdef _WIN32 +// _SH constants for _wfsopen() +#include +#endif + namespace proc { using namespace std::literals; namespace bp = boost::process; @@ -35,7 +42,7 @@ void process_end(bp::child &proc, bp::group &proc_handle) { proc.wait(); } -int exe(const std::string &cmd, bp::environment &env, file_t &file, std::error_code &ec) { +int exe_with_full_privs(const std::string &cmd, bp::environment &env, file_t &file, std::error_code &ec) { if(!file) { return bp::system(cmd, env, bp::std_out > bp::null, bp::std_err > bp::null, ec); } @@ -43,23 +50,46 @@ int exe(const std::string &cmd, bp::environment &env, file_t &file, std::error_c return bp::system(cmd, env, bp::std_out > file.get(), bp::std_err > file.get(), ec); } -int proc_t::execute(int app_id) { - if(!running() && _app_id != -1) { - // previous process exited on its own, reset _process_handle - _process_handle = bp::group(); - - _app_id = -1; +boost::filesystem::path find_working_directory(const std::string &cmd, bp::environment &env) { + // Parse the raw command string into parts to get the actual command portion +#ifdef _WIN32 + auto parts = boost::program_options::split_winmain(cmd); +#else + auto parts = boost::program_options::split_unix(cmd); +#endif + if(parts.empty()) { + BOOST_LOG(error) << "Unable to parse command: "sv << cmd; + return boost::filesystem::path(); } + BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + + // If the cmd path is not a complete path, resolve it using our PATH variable + boost::filesystem::path cmd_path(parts.at(0)); + if(!cmd_path.is_complete()) { + cmd_path = boost::process::search_path(parts.at(0)); + if(cmd_path.empty()) { + BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; + return boost::filesystem::path(); + } + } + + BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + + // Now that we have a complete path, we can just use parent_path() + return cmd_path.parent_path(); +} + +int proc_t::execute(int app_id) { + // Ensure starting from a clean slate + terminate(); + if(app_id < 0 || app_id >= _apps.size()) { BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; return 404; } - // Ensure starting from a clean slate - terminate(); - _app_id = app_id; auto &proc = _apps[app_id]; @@ -67,7 +97,19 @@ int proc_t::execute(int app_id) { _undo_it = _undo_begin; if(!proc.output.empty() && proc.output != "null"sv) { +#ifdef _WIN32 + // fopen() interprets the filename as an ANSI string on Windows, so we must convert it + // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. + std::wstring_convert, wchar_t> converter; + auto woutput = converter.from_bytes(proc.output); + + // Use _SH_DENYNO to allow us to open this log file again for writing even if it is + // still open from a previous execution. This is required to handle the case of a + // detached process executing again while the previous process is still running. + _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); +#else _pipe.reset(fopen(proc.output.c_str(), "a")); +#endif } std::error_code ec; @@ -80,7 +122,7 @@ int proc_t::execute(int app_id) { auto &cmd = _undo_it->do_cmd; BOOST_LOG(info) << "Executing: ["sv << cmd << ']'; - auto ret = exe(cmd, _env, _pipe, ec); + auto ret = exe_with_full_privs(cmd, _env, _pipe, ec); if(ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message(); @@ -94,17 +136,17 @@ int proc_t::execute(int app_id) { } for(auto &cmd : proc.detached) { - BOOST_LOG(info) << "Spawning ["sv << cmd << ']'; - if(proc.output.empty() || proc.output == "null"sv) { - bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec); - } - else { - bp::spawn(cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec); - } - + boost::filesystem::path working_dir = proc.working_dir.empty() ? + find_working_directory(cmd, _env) : + boost::filesystem::path(proc.working_dir); + BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; + auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec); if(ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } + else { + child.detach(); + } } if(proc.cmd.empty()) { @@ -113,21 +155,16 @@ int proc_t::execute(int app_id) { } else { boost::filesystem::path working_dir = proc.working_dir.empty() ? - boost::filesystem::path(proc.cmd).parent_path() : + find_working_directory(proc.cmd, _env) : boost::filesystem::path(proc.working_dir); - if(proc.output.empty() || proc.output == "null"sv) { - BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']'; - _process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); + BOOST_LOG(info) << "Executing: ["sv << proc.cmd << "] in ["sv << working_dir << ']'; + _process = platf::run_unprivileged(proc.cmd, working_dir, _env, _pipe.get(), ec); + if(ec) { + BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message(); + return -1; } - else { - BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']'; - _process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec); - } - } - if(ec) { - BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message(); - return -1; + _process_handle.add(_process); } fg.disable(); @@ -140,6 +177,11 @@ int proc_t::running() { return _app_id; } + // Perform cleanup actions now if needed + if(_process) { + terminate(); + } + return -1; } @@ -149,7 +191,9 @@ void proc_t::terminate() { // Ensure child process is terminated placebo = false; process_end(_process, _process_handle); - _app_id = -1; + _process = bp::child(); + _process_handle = bp::group(); + _app_id = -1; if(ec) { BOOST_LOG(fatal) << "System: "sv << ec.message(); @@ -166,7 +210,7 @@ void proc_t::terminate() { BOOST_LOG(debug) << "Executing: ["sv << cmd << ']'; - auto ret = exe(cmd, _env, _pipe, ec); + auto ret = exe_with_full_privs(cmd, _env, _pipe, ec); if(ec) { BOOST_LOG(fatal) << "System: "sv << ec.message();