/* * Win32System.cpp * * Copyright (C) 2022 by Posit Software, PBC * * Unless you have received this program directly from Posit Software pursuant * to the terms of a commercial license agreement with Posit Software, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE #define JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 0x2000 #endif #ifndef JOB_OBJECT_LIMIT_BREAKAWAY_OK #define JOB_OBJECT_LIMIT_BREAKAWAY_OK 0x00000800 #endif using namespace boost::placeholders; namespace rstudio { namespace core { namespace system { namespace { Error initJobObject(bool* detachFromJob) { /* * Create a Job object and assign this process to it. This will * cause all child processes to be assigned to the same job. * With JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE set, all the child * processes will be killed when this process terminates (since * it is the only one holding a handle to the job). With * JOB_OBJECT_LIMIT_BREAKAWAY_OK set it is possible to pass * CREATE_BREAKAWAY_FROM_JOB to CreateProcess (this is required * by Chrome for creating its sub-processes) */ // If detachFromJob is true, it means we need to relaunch this // executable with CREATE_BREAKAWAY_FROM_JOB *detachFromJob = false; HANDLE hJob = ::CreateJobObject(nullptr, nullptr); if (!hJob) { return LAST_SYSTEM_ERROR(); } JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = { 0 }; jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION; ::SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli)); if (::AssignProcessToJobObject(hJob, ::GetCurrentProcess())) { auto lastErr = ::GetLastError(); if (lastErr == ERROR_ACCESS_DENIED) { // Use an environment variable to prevent us from somehow // getting into an infinite loop of detaching (which would // otherwise occur if ERROR_ACCESS_DENIED is being returned // for some reason other than an existing job object being // attached). This works because environment variables are // inherited by our job-detached child process. if (getenv("_RSTUDIO_LEVEL").empty()) { setenv("_RSTUDIO_LEVEL", "1"); *detachFromJob = true; } } return systemError(lastErr, ERROR_LOCATION); } return Success(); } bool isHiddenFile(const std::string& path) { DWORD attribs = ::GetFileAttributesA(path.c_str()); if (attribs == INVALID_FILE_ATTRIBUTES) return false; else if (attribs & FILE_ATTRIBUTE_HIDDEN) return true; else return false; } } // anonymous namespace void initHook() { // Logging will NOT work in this function!! bool detachFromJob; Error error = initJobObject(&detachFromJob); if (!detachFromJob) return; TCHAR path[MAX_PATH]; if (!::GetModuleFileName(nullptr, path, MAX_PATH)) return; // Couldn't get the path of the current .exe STARTUPINFO startupInfo; memset(&startupInfo, 0, sizeof(startupInfo)); startupInfo.cb = sizeof(startupInfo); PROCESS_INFORMATION procInfo; memset(&procInfo, 0, sizeof(procInfo)); if (!::CreateProcess(nullptr, ::GetCommandLine(), nullptr, nullptr, TRUE, CREATE_BREAKAWAY_FROM_JOB | ::GetPriorityClass(::GetCurrentProcess()), nullptr, nullptr, &startupInfo, &procInfo)) { return; // Couldn't execute } ::AllowSetForegroundWindow(procInfo.dwProcessId); ::WaitForSingleObject(procInfo.hProcess, INFINITE); DWORD exitCode; if (!::GetExitCodeProcess(procInfo.hProcess, &exitCode)) exitCode = ::GetLastError(); ::CloseHandle(procInfo.hProcess); ::CloseHandle(procInfo.hThread); ::ExitProcess(exitCode); } Error initializeSystemLog(const std::string& programIdentity, log::LogLevel logLevel, bool enableConfigReload) { return Success(); } void initializeLogConfigReload() { } Error findProgramOnPath(const std::string& program, core::FilePath* pProgramPath) { std::string path = core::system::getenv("PATH"); auto paths = core::algorithm::split(path, ";"); // if the program supplied already has an extension, // then we'll skip searching any custom extensions auto programExt = core::string_utils::getExtension(program); auto defaultExts = { ".exe", ".com", ".bat", ".cmd" }; auto noExts = { "" }; auto exts = programExt.empty() ? defaultExts : noExts; for (auto&& path : paths) { if (path.empty()) continue; for (auto&& ext : exts) { FilePath programPath = FilePath(path).completeChildPath(program + ext); if (!programPath.exists()) continue; *pProgramPath = programPath; return Success(); } } return fileNotFoundError(program, ERROR_LOCATION); } bool isWin64() { return !getenv("PROCESSOR_ARCHITEW6432").empty() || getenv("PROCESSOR_ARCHITECTURE") == "AMD64"; } bool isCurrentProcessWin64() { return getenv("PROCESSOR_ARCHITECTURE") == "AMD64"; } bool isWin7OrLater() { return IsWindows7OrGreater(); } std::string username() { return system::getenv("USERNAME"); } unsigned int effectiveUserId() { return 0; // no concept of this on Win32 } bool effectiveUserIsRoot() { // on Windows, treat built-in administrator account, or elevation to it, to be the // equivalent of Posix "root" HANDLE hProcessToken = nullptr; if (!OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &hProcessToken)) { auto lastErr = ::GetLastError(); LOG_ERROR(systemError(lastErr, ERROR_LOCATION)); return false; } core::system::CloseHandleOnExitScope processTokenScope(&hProcessToken, ERROR_LOCATION); bool isAdmin = false; DWORD bytesUsed = 0; TOKEN_ELEVATION_TYPE tokenElevationType; if (!::GetTokenInformation(hProcessToken, TokenElevationType, &tokenElevationType, sizeof(tokenElevationType), &bytesUsed)) { auto lastErr = ::GetLastError(); LOG_ERROR(systemError(lastErr, ERROR_LOCATION)); return false; } if (tokenElevationType == TokenElevationTypeLimited) { HANDLE hUnfiltered; if (!::GetTokenInformation(hProcessToken, TokenLinkedToken, &hUnfiltered, sizeof(HANDLE), &bytesUsed)) { auto lastErr = ::GetLastError(); LOG_ERROR(systemError(lastErr, ERROR_LOCATION)); return false; } core::system::CloseHandleOnExitScope unfilteredHandle(&hUnfiltered, ERROR_LOCATION); BYTE adminSID[SECURITY_MAX_SID_SIZE]; DWORD sidSize = sizeof(adminSID); if (!::CreateWellKnownSid(WinBuiltinAdministratorsSid, 0, &adminSID, &sidSize)) { auto lastErr = ::GetLastError(); LOG_ERROR(systemError(lastErr, ERROR_LOCATION)); return false; } BOOL isMember = FALSE; if (::CheckTokenMembership(hUnfiltered, &adminSID, &isMember)) { auto lastErr = ::GetLastError(); LOG_ERROR(systemError(lastErr, ERROR_LOCATION)); return false; } isAdmin = (isMember != FALSE); } else { isAdmin = ::IsUserAnAdmin(); } return isAdmin; } FilePath userHomePath(std::string envOverride) { return User::getUserHomePath(envOverride); } FilePath userSettingsPath(const FilePath& userHomeDirectory, const std::string& appName, bool ensureDirectory) { wchar_t* path = nullptr; HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path); if (result != S_OK || path == nullptr) { Error error = systemError( boost::system::errc::no_such_file_or_directory, "unable to compute user settings (LocalAppData) path", ERROR_LOCATION); error.addProperty("error-code", core::safe_convert::numberToHexString(result)); LOG_ERROR(error); return FilePath(); } FilePath localAppData = FilePath(std::wstring(path)); FilePath settingsPath = localAppData.completeChildPath(appName); if (ensureDirectory) { Error error = settingsPath.ensureDirectory(); if (error) LOG_ERROR(error); } return settingsPath; } FilePath systemSettingsPath(const std::string& appName, bool create) { int nFolder = CSIDL_COMMON_APPDATA; if (create) nFolder |= CSIDL_FLAG_CREATE; wchar_t path[MAX_PATH + 1]; HRESULT hr = ::SHGetFolderPathW(nullptr, nFolder, nullptr, SHGFP_TYPE_CURRENT, path); if (hr != S_OK) { LOG_ERROR_MESSAGE("Unable to retrieve per machine configuration path. HRESULT: " + safe_convert::numberToHexString(hr)); return FilePath(); } FilePath settingsPath = FilePath(std::wstring(path)); FilePath completePath = settingsPath.completePath(appName); if (create) { std::wstring appNameWide = core::string_utils::utf8ToWide(appName); hr = ::SHGetFolderPathAndSubDirW(nullptr, CSIDL_COMMON_APPDATA|CSIDL_FLAG_CREATE, nullptr, SHGFP_TYPE_CURRENT, appNameWide.c_str(), path); if (hr != S_OK) { LOG_ERROR_MESSAGE("Cannot create folder under per machine configuration path. HRESULT: " + safe_convert::numberToHexString(hr)); return FilePath(); } } return completePath; } bool currentUserIsPrivilleged(unsigned int minimumUserId) { return false; } Error captureCommand(const std::string& command, std::string* pOutput) { // WIN32 popen docs: // http://msdn.microsoft.com/en-us/library/96ayss4b(VS.80).aspx // NOTE: note that popen only works from win32 console applications! // start process FILE* fp = ::_popen(command.c_str(), "r"); if (fp == nullptr) return systemError(errno, ERROR_LOCATION); // collect output const int kBuffSize = 1024; char buffer[kBuffSize]; while (::fgets(buffer, kBuffSize, fp) != nullptr) *pOutput += buffer; // check if an error terminated our output Error error; if (::ferror(fp)) error = systemError(boost::system::errc::io_error, ERROR_LOCATION); // close file if (::_pclose(fp) == -1) { // log existing error before overwriting it if (error) LOG_ERROR(error); error = systemError(errno, ERROR_LOCATION); } // return status return error; } Error realPath(const FilePath& filePath, FilePath* pRealPath) { std::wstring wPath = filePath.getAbsolutePathW(); std::vector buffer(512); DWORD res = ::GetFullPathNameW(wPath.c_str(), static_cast(buffer.size()), &(buffer[0]), nullptr); if (res == 0) { Error error = LAST_SYSTEM_ERROR(); error.addProperty("path", filePath); return error; } else if (res > buffer.size()) { buffer.resize(res); res = ::GetFullPathNameW(wPath.c_str(), static_cast(buffer.size()), &(buffer[0]), nullptr); if (res == 0) { return LAST_SYSTEM_ERROR(); } else if (res > buffer.size()) return systemError(boost::system::windows_error::bad_length, ERROR_LOCATION); } wPath = std::wstring(&(buffer[0]), res); *pRealPath = FilePath(wPath); return Success(); } Error realPath(const std::string& path, FilePath* pRealPath) { return realPath(FilePath(path), pRealPath); } bool isHiddenFile(const FilePath& filePath) { return isHiddenFile(filePath.getAbsolutePath()); } bool isHiddenFile(const FileInfo& fileInfo) { return isHiddenFile(fileInfo.absolutePath()); } bool isReadOnly(const FilePath& filePath) { // TODO: readonly detection for windows return false; } Error makeFileHidden(const FilePath& path) { std::wstring filePath = path.getAbsolutePathW(); LPCWSTR lpszPath = filePath.c_str(); DWORD attribs = ::GetFileAttributesW(lpszPath); if (attribs == INVALID_FILE_ATTRIBUTES) { return LAST_SYSTEM_ERROR(); } if (!::SetFileAttributesW(lpszPath, attribs | FILE_ATTRIBUTE_HIDDEN)) { return LAST_SYSTEM_ERROR(); } return Success(); } bool stderrIsTerminal() { return _isatty(_fileno(stderr)); } bool stdoutIsTerminal() { return _isatty(_fileno(stdout)); } // uuid std::string generateUuid(bool includeDashes) { // create the uuid UUID uuid = {0}; ::UuidCreate(&uuid); PUCHAR pChar = nullptr; ::UuidToStringA(&uuid, &pChar); std::string uuidStr((char*)pChar); ::RpcStringFreeA(&pChar); // remove dashes if requested if (!includeDashes) boost::algorithm::replace_all(uuidStr, "-", ""); // return return uuidStr; } PidType currentProcessId() { return ::GetCurrentProcessId(); } Error executablePath(const char *argv0, FilePath* pExecutablePath) { wchar_t exePath[MAX_PATH]; if (!GetModuleFileNameW(nullptr, exePath, MAX_PATH)) { auto lastErr = ::GetLastError(); return systemError(lastErr, ERROR_LOCATION); } std::wstring wzPath(exePath); *pExecutablePath = FilePath(wzPath); return Success(); } // installation path Error installPath(const std::string& relativeToExecutable, const char * argv0, FilePath* pInstallationPath) { // get full executable path FilePath exePath; Error error = executablePath(argv0, &exePath); if (error) return error; // resolve to install path using given relative path if (relativeToExecutable == "..") // common case *pInstallationPath = exePath.getParent().getParent(); else *pInstallationPath = exePath.getParent().completePath(relativeToExecutable); return Success(); } void fixupExecutablePath(FilePath* pExePath) { if (pExePath->getExtension().empty()) *pExePath = pExePath->getParent().completePath(pExePath->getFilename() + ".exe"); } void abort() { ::exit(1); } //////////////////////////////////////////////////////////////////////////// // // No signals on Win32 so all of these are no-ops // // Error ignoreTerminalSignals() { return Success(); } Error ignoreChildExits() { return Success(); } Error reapChildren() { return Success(); } struct SignalBlocker::Impl { }; SignalBlocker::SignalBlocker() : pImpl_(new Impl()) { } Error SignalBlocker::block(SignalType signal) { return Success(); } Error SignalBlocker::blockAll() { return Success(); } SignalBlocker::~SignalBlocker() { try { } catch(...) { } } Error clearSignalMask() { return Success(); } namespace signal_safe { int clearSignalMask() { return 0; } } Error handleSignal(SignalType signal, void (*handler)(int)) { return Success(); } core::Error ignoreSignal(SignalType signal) { return Success(); } Error useDefaultSignalHandler(SignalType signal) { return Success(); } void sendSignalToSelf(SignalType signal) { } class ClipboardScope : boost::noncopyable { public: ClipboardScope() : opened_(false) {} Error open() { if (!::OpenClipboard(nullptr)) { return LAST_SYSTEM_ERROR(); } else { opened_ = true; return Success(); } } ~ClipboardScope() { try { if (opened_) { if (!::CloseClipboard()) { LOG_ERROR(LAST_SYSTEM_ERROR()); } } } catch(...) { } } private: bool opened_; }; class EnhMetaFile : boost::noncopyable { public: EnhMetaFile() : hMF_(nullptr) {} Error open(const FilePath& path) { hMF_ = ::GetEnhMetaFileW(path.getAbsolutePathW().c_str()); if (hMF_ == nullptr) { return LAST_SYSTEM_ERROR(); } else return Success(); } ~EnhMetaFile() { try { if (hMF_ != nullptr) { if (!::DeleteEnhMetaFile(hMF_)) { LOG_ERROR(LAST_SYSTEM_ERROR()); } } } catch(...) { } } HENHMETAFILE handle() const { return hMF_; } void release() { hMF_ = nullptr; } private: HENHMETAFILE hMF_; }; Error copyMetafileToClipboard(const FilePath& path) { // open metafile EnhMetaFile enhMetaFile; Error error = enhMetaFile.open(path); if (error) return error; // open the clipboard ClipboardScope clipboardScope; error = clipboardScope.open(); if (error) return error; // empty the clipboard if (!::EmptyClipboard()) { return LAST_SYSTEM_ERROR(); } // set the clipboard data if (!::SetClipboardData(CF_ENHMETAFILE, enhMetaFile.handle())) { return LAST_SYSTEM_ERROR(); } // release the handle (because the clipboard now owns it) enhMetaFile.release(); // return success return Success(); } void ensureLongPath(FilePath* pFilePath) { const std::size_t kBuffSize = (MAX_PATH*2) + 1; char buffer[kBuffSize]; std::string path = string_utils::utf8ToSystem(pFilePath->getAbsolutePath()); if (::GetLongPathName(path.c_str(), buffer, kBuffSize) > 0) { *pFilePath = FilePath(string_utils::systemToUtf8(buffer)); } } Error expandEnvironmentVariables(std::string value, std::string* pResult) { if (value.empty()) { *pResult = value; return Success(); } DWORD sizeRequired = ::ExpandEnvironmentStrings(value.c_str(), nullptr, 0); if (!sizeRequired) { return LAST_SYSTEM_ERROR(); } std::vector buffer(sizeRequired); auto result = ::ExpandEnvironmentStrings(value.c_str(), &buffer[0], static_cast(buffer.capacity())); if (!result) { return LAST_SYSTEM_ERROR(); } else if (result > buffer.capacity()) return systemError(ERROR_MORE_DATA, ERROR_LOCATION); // not expected *pResult = std::string(&buffer[0]); return Success(); } FilePath expandComSpec() { std::string result; Error err = expandEnvironmentVariables("%COMSPEC%", &result); if (err) return FilePath(); return FilePath(result); } Error terminateProcess(PidType pid) { HANDLE hProc = ::OpenProcess(PROCESS_TERMINATE, false, pid); if (!hProc) { return LAST_SYSTEM_ERROR(); } if (!::TerminateProcess(hProc, 1)) { return LAST_SYSTEM_ERROR(); } return Success(); } std::vector getSubprocesses(PidType pid) { std::vector subprocs; HANDLE hSnapShot; CloseHandleOnExitScope closeSnapShot(&hSnapShot, ERROR_LOCATION); hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapShot == INVALID_HANDLE_VALUE) { // err on the side of assuming child processes, so we don't kill // a job unintentionally LOG_ERROR(LAST_SYSTEM_ERROR()); return subprocs; } PROCESSENTRY32 pe32; pe32.dwSize = sizeof(pe32); if (!Process32First(hSnapShot, &pe32)) { LOG_ERROR(LAST_SYSTEM_ERROR()); return subprocs; } do { if (pe32.th32ParentProcessID == pid) { // Found a child process SubprocInfo info; info.pid = pe32.th32ProcessID; info.exe = pe32.szExeFile; subprocs.push_back(info); } } while (Process32Next(hSnapShot, &pe32)); return subprocs; } FilePath currentWorkingDir(PidType pid) { // NYI for Win32; commonly accepted technique for this is to use // CreateRemoteThread to inject code to run GetCurrentDirectory in the // context of the target program. That is ugly and we aren't // likely to ever do it. return FilePath(); } Error closeHandle(HANDLE* pHandle, const ErrorLocation& location) { if (*pHandle != nullptr) { BOOL result = ::CloseHandle(*pHandle); *pHandle = nullptr; if (!result) { return LAST_SYSTEM_ERROR(); } else return Success(); } else { return Success(); } } CloseHandleOnExitScope::~CloseHandleOnExitScope() { try { // A "null" handle can contain INVALID_HANDLE or NULL, depending // on the context. This is a painful inconsistency in Windows, see: // https://blogs.msdn.microsoft.com/oldnewthing/20040302-00/?p=40443 if (!pHandle_ || *pHandle_ == INVALID_HANDLE_VALUE || *pHandle_ == nullptr) return; Error error = closeHandle(pHandle_, location_); if (error) LOG_ERROR(error); } catch(...) { } } Error getProcesses(std::vector *pOutProcesses) { PROCESSENTRY32 processEntry; memset(&processEntry, 0, sizeof(PROCESSENTRY32)); processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE hSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnap == INVALID_HANDLE_VALUE) { return LAST_SYSTEM_ERROR(); } if (Process32First(hSnap, &processEntry)) { BOOL moreProcesses = TRUE; while (moreProcesses) { ProcessInfo process; process.pid = processEntry.th32ProcessID; process.ppid = processEntry.th32ParentProcessID; pOutProcesses->push_back(process); moreProcesses = ::Process32Next(hSnap, &processEntry); } } return Success(); } Error getChildProcesses(std::vector *pOutProcesses) { if (!pOutProcesses) return systemError(EINVAL, ERROR_LOCATION); // get all processes std::vector processes; Error error = getProcesses(&processes); if (error) return error; // build a process tree of the processes ProcessTreeT tree; createProcessTree(processes, &tree); // return just the children of this process ProcessTreeT::const_iterator iter = tree.find(::GetCurrentProcessId()); if (iter == tree.end()) return Success(); const boost::shared_ptr& rootNode = iter->second; getChildren(rootNode, pOutProcesses); return Success(); } Error terminateChildProcesses() { std::vector childProcesses; Error error = getChildProcesses(&childProcesses); if (error) return error; for (const ProcessInfo& process : childProcesses) { HANDLE hChildProc = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, process.pid); if (hChildProc) { if (!::TerminateProcess(hChildProc, 1)) { LOG_ERROR(LAST_SYSTEM_ERROR()); } if (!::CloseHandle(hChildProc)) { LOG_ERROR(LAST_SYSTEM_ERROR()); } } else { LOG_ERROR(LAST_SYSTEM_ERROR()); } } // the actual kill is best effort // so return success regardless return Success(); } void setHomeToUserProfile(core::system::Options* pChildEnv) { std::string userProfile = core::system::getenv(*pChildEnv, "USERPROFILE"); core::system::setenv(pChildEnv, "HOME", userProfile); } } // namespace system } // namespace core } // namespace rstudio