#include "process_control.h" #include "../../global/base64.h" #include "../../global/exec_dir_helper.h" #include #include #ifdef __GNUC__ #include #endif #ifdef _WIN32 // Resolve conflict #pragma push_macro("interface") #undef interface #ifndef NOMINMAX #define NOMINMAX #endif #include #include #include #include // Resolve conflict #pragma pop_macro("interface") #ifdef GetClassInfo #undef GetClassInfo #endif #elif defined __unix__ //#include #include #include #include #else #error OS is not supported! #endif #include "../../global/trace.h" CProcessControl::~CProcessControl() { Shutdown(); } void CProcessControl::Initialize(const sdv::u8string& /*ssObjectConfig*/) { if (m_eObjectStatus != sdv::EObjectStatus::initialization_pending) return; // Without monitor no trigger... m_threadMonitor = std::thread(&CProcessControl::MonitorThread, this); m_eObjectStatus = sdv::EObjectStatus::initialized; } sdv::EObjectStatus CProcessControl::GetStatus() const { return m_eObjectStatus; } void CProcessControl::SetOperationMode(sdv::EOperationMode eMode) { switch (eMode) { case sdv::EOperationMode::configuring: if (m_eObjectStatus == sdv::EObjectStatus::running || m_eObjectStatus == sdv::EObjectStatus::initialized) m_eObjectStatus = sdv::EObjectStatus::configuring; break; case sdv::EOperationMode::running: if (m_eObjectStatus == sdv::EObjectStatus::configuring || m_eObjectStatus == sdv::EObjectStatus::initialized) m_eObjectStatus = sdv::EObjectStatus::running; break; default: break; } } void CProcessControl::Shutdown() { // TODO: Close process handles if (m_eObjectStatus == sdv::EObjectStatus::destruction_pending) return; m_eObjectStatus = sdv::EObjectStatus::shutdown_in_progress; // Shutdown the monitor m_bShutdown = true; if (m_threadMonitor.joinable()) m_threadMonitor.join(); m_eObjectStatus = sdv::EObjectStatus::destruction_pending; } bool CProcessControl::AllowProcessControl() const { const sdv::app::IAppContext* pAppContext = sdv::core::GetCore(); return pAppContext && (pAppContext->GetContextType() == sdv::app::EAppContext::main || pAppContext->GetContextType() == sdv::app::EAppContext::maintenance || pAppContext->GetContextType() == sdv::app::EAppContext::essential); } sdv::process::TProcessID CProcessControl::GetProcessID() const { #ifdef _WIN32 return static_cast(GetCurrentProcessId()); #elif defined __unix__ return static_cast(getpid()); #else #error OS is not supported! #endif } uint32_t CProcessControl::RegisterMonitor(/*in*/ sdv::process::TProcessID tProcessID, /*in*/ sdv::IInterfaceAccess* pMonitor) { if (!tProcessID || !pMonitor) return 0; sdv::process::IProcessLifetimeCallback* pCallback = sdv::TInterfaceAccessPtr(pMonitor).GetInterface(); if (!pCallback) return 0; std::unique_lock lock(m_mtxProcesses); SDV_LOG_TRACE(GetTimestamp(), "Registering... (PID#", tProcessID, ")"); // Find the process auto itProcess = m_mapProcesses.find(tProcessID); if (itProcess == m_mapProcesses.end()) { #if _WIN32 // Get the process handle. HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast(tProcessID)); if (!hProcess) return 0; // Cannot monitor #elif defined __unix__ // Use "kill" to detect the existence of the process // Notice: this doesn't kill the process. if (kill(tProcessID, 0) != 0) return 0; // Cannot monitor #else #error OS is not supported! #endif // Create a new process structure auto ptrNewProcess = std::make_shared(); // Ignore cppcheck warning; normally the returned pointer should always have a value at this stage (otherwise an // exception was triggered). // cppcheck-suppress knownConditionTrueFalse if (!ptrNewProcess) { #ifdef _WIN32 CloseHandle(hProcess); #endif SDV_LOG_TRACE(GetTimestamp(), "Could not add monitor... (PID#", tProcessID, ")"); return 0; } ptrNewProcess->tProcessID = tProcessID; #ifdef _WIN32 ptrNewProcess->hProcess = hProcess; #endif auto prInsert = m_mapProcesses.insert(std::make_pair(tProcessID, ptrNewProcess)); if (!prInsert.second) { #ifdef _WIN32 CloseHandle(hProcess); #endif SDV_LOG_TRACE(GetTimestamp(), "Could not add monitor... (PID#", tProcessID, ")"); return 0; // Could not insert } SDV_LOG_TRACE(GetTimestamp(), "Monitor now added... (PID#", tProcessID, ")"); itProcess = prInsert.first; } auto ptrProcess = itProcess->second; // Create a new cookie uint32_t uiCookie = m_uiNextMonCookie++; // Register the new monitor m_mapMonitors[uiCookie] = ptrProcess; lock.unlock(); // Add the monitor to the process as well std::unique_lock lockProcess(ptrProcess->mtxProcess); ptrProcess->mapAssociatedMonitors[uiCookie] = pCallback; // Monitor thread running? // Already terminated? if (!ptrProcess->bRunning) pCallback->ProcessTerminated(tProcessID, ptrProcess->iRetVal); return uiCookie; } void CProcessControl::UnregisterMonitor(/*in*/ uint32_t uiCookie) { if (!uiCookie) return; std::unique_lock lock(m_mtxProcesses); // Find the monitor and remove the monitor auto itMonitor = m_mapMonitors.find(uiCookie); if (itMonitor == m_mapMonitors.end()) return; auto ptrProcess = itMonitor->second; m_mapMonitors.erase(itMonitor); lock.unlock(); // Removing the monitor from the map might be problematic when the call is done in a callback. Simply update the pointer // instead (this will not affect the map order). std::unique_lock lockProcess(ptrProcess->mtxProcess); ptrProcess->mapAssociatedMonitors.erase(uiCookie); } bool CProcessControl::WaitForTerminate(/*in*/ sdv::process::TProcessID tProcessID, /*in*/ uint32_t uiWaitMs) { if (!tProcessID) return true; // Non-existent processes are terminated :-) std::unique_lock lock(m_mtxProcesses); // Find the process auto itProcess = m_mapProcesses.find(tProcessID); if (itProcess == m_mapProcesses.end()) return true; // Non-existent processes are terminated :-) auto ptrProcess = itProcess->second; lock.unlock(); // Already terminated? std::unique_lock lockProcess(ptrProcess->mtxProcess); if (!ptrProcess->bRunning) return true; // Wait for termination std::chrono::high_resolution_clock::time_point tpStart = std::chrono::high_resolution_clock::now(); bool bTimeout = false; // False warning from cppcheck. bRunning is set by the monitor thread. Suppress warning. // cppcheck-suppress knownConditionTrueFalse while (ptrProcess->bRunning) { ptrProcess->cvWaitForProcess.wait_for(lockProcess, std::chrono::milliseconds(10)); if (std::chrono::duration(std::chrono::high_resolution_clock::now() - tpStart).count() > static_cast(uiWaitMs)) { bTimeout = true; break; } } return !bTimeout; } sdv::process::TProcessID CProcessControl::Execute(/*in*/ const sdv::u8string& ssModule, /*in*/ const sdv::sequence& seqArgs, /*in*/ sdv::process::EProcessRights eRights) { if (!AllowProcessControl()) return 0; if (ssModule.empty()) return 0; sdv::process::TProcessID tProcessID = 0; // Update rights bool bReduceRights = false; const sdv::app::IAppContext* pAppContext = sdv::core::GetCore(); if (!pAppContext) return 0; switch (eRights) { case sdv::process::EProcessRights::default_rights: // Default implementation would be reduced rights bReduceRights = pAppContext->GetContextType() == sdv::app::EAppContext::main; break; case sdv::process::EProcessRights::reduced_rights: bReduceRights = true; break; case sdv::process::EProcessRights::parent_rights: break; default: break; } // Check for the existence of the module std::filesystem::path pathModule(static_cast(ssModule)); #ifdef _WIN32 if (!pathModule.has_extension()) pathModule.replace_extension(".exe"); #endif if (pathModule.is_relative()) { // Get the module serch dirs sdv::sequence seqSearchDirs; const sdv::core::IModuleControlConfig* pModuleControlConfig = sdv::core::GetCore(); if (pModuleControlConfig) seqSearchDirs = pModuleControlConfig->GetModuleSearchDirs(); // Add the current directory as well. seqSearchDirs.insert(seqSearchDirs.begin(), "."); // Now find the module bool bFound = false; for (const sdv::u8string& rssPath : seqSearchDirs) { std::filesystem::path pathModuleTemp = std::filesystem::path(static_cast(rssPath)) / pathModule; if (std::filesystem::exists(pathModuleTemp)) { pathModule = pathModuleTemp; bFound = true; break; } } // Found? if (!bFound) return 0; } #ifdef _WIN32 // The command line is one string. If containing spaces, include quotes std::wstringstream sstreamCommandline; sstreamCommandline << pathModule.native(); for (auto& rssArg : seqArgs) { sstreamCommandline << L" "; bool bQuotes = rssArg.find(" ") != std::string::npos; if (bQuotes) sstreamCommandline << L'\"'; sstreamCommandline << sdv::MakeWString(rssArg); if (bQuotes) sstreamCommandline << L'\"'; } // Command line string std::wstring ssCommandLine = sstreamCommandline.str(); if (ssCommandLine.size() > 32768) return 0; ssCommandLine.reserve(32768); /* First get a handle to the current process's primary token */ HANDLE hProcessToken = 0; OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, &hProcessToken); // Update rights if (bReduceRights) { // Create a restricted token with all privileges removed. // NOTE: This doesn't restrict file system access. To do so, use a separate user (e.g. guest user). HANDLE hRestrictedToken = 0; CreateRestrictedToken(hProcessToken, DISABLE_MAX_PRIVILEGE, 0, 0, 0, 0, 0, 0, &hRestrictedToken); CloseHandle(hProcessToken); hProcessToken = hRestrictedToken; } // Start the process STARTUPINFOW sStartInfo{}; PROCESS_INFORMATION sProcessInfo{}; bool bRes = CreateProcessAsUserW(hProcessToken, // Process token nullptr, // No module name (use command line) &ssCommandLine.front(), // Command line nullptr, // Process handle not inheritable nullptr, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE 0, // No creation flags nullptr, // Use parent's environment block GetExecDirectory().native().c_str(), // Use parent's starting directory &sStartInfo, // Pointer to STARTUPINFO structure &sProcessInfo); // Pointer to PROCESS_INFORMATION structure if (!bRes) return 0; tProcessID = sProcessInfo.dwProcessId; CloseHandle(sProcessInfo.hThread); CloseHandle(hProcessToken); #elif defined __unix__ // Create the argument list std::vector vecArgs; auto seqTempArgs = seqArgs; std::string ssModuleTemp = pathModule.native(); seqTempArgs.insert(seqTempArgs.begin(), &ssModuleTemp.front()); for (auto& rssArg : seqTempArgs) vecArgs.push_back(&rssArg.front()); vecArgs.push_back(nullptr); // Create environment variable list std::vector vecEnv; vecEnv.push_back(nullptr); // TODO: Update rights and environment vars // Fork the process int pid = vfork(); switch (pid) { case -1: // Error called in parent process; cannot continue return 0; case 0: // Child process is executing with pid == 0 { // Reduce rights if (bReduceRights) { #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-result" #endif setgid(getgid()); setuid(getuid()); #ifdef __GNUC__ #pragma GCC diagnostic pop #endif } execve(pathModule.native().c_str(), &vecArgs.front(), &vecEnv.front()); std::abort(); // Child process only comes here on error } default: // Parent process is executing with pid != 0 tProcessID = static_cast(pid); break; } #else #error OS is not supported! #endif // Create a new process structure auto ptrNewProcess = std::make_shared(); // Ignore cppcheck warning; normally the returned pointer should always have a value at this stage (otherwise an // exception was triggered). // cppcheck-suppress knownConditionTrueFalse if (!ptrNewProcess) { // TODO Terminate process and close handles... return 0; } ptrNewProcess->tProcessID = tProcessID; #ifdef _WIN32 ptrNewProcess->hProcess = sProcessInfo.hProcess; #endif m_mapProcesses[tProcessID] = ptrNewProcess; return tProcessID; } bool CProcessControl::Terminate(/*in*/ sdv::process::TProcessID tProcessID) { if (!AllowProcessControl()) return false; if (!tProcessID) return false; #ifdef _WIN32 HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, static_cast(tProcessID)); if (!hProcess) return 0; // Cannot terminate TerminateProcess(hProcess, static_cast(-100)); CloseHandle(hProcess); #elif defined __unix__ // Send a signal (SIGUSR1) to the child process. if (kill(tProcessID, SIGUSR1) != 0) return false; #else #error OS is not supported! #endif return true; } void CProcessControl::MonitorThread() { while (!m_bShutdown) { // Run through the list of processes and determine which process has been terminated. std::unique_lock lock(m_mtxProcesses); std::vector> m_vecTerminatedProcesses; for (auto& vtProcess : m_mapProcesses) { // Update only once if (!vtProcess.second->bRunning) continue; #ifdef _WIN32 DWORD dwExitCode = 0; if (GetExitCodeProcess(vtProcess.second->hProcess, &dwExitCode) && dwExitCode != STILL_ACTIVE) // if (WaitForSingleObject(vtProcess.second->hProcess, 0) == WAIT_OBJECT_0) { // Get the exit code //DWORD dwExitCode = 0; //GetExitCodeProcess(vtProcess.second->hProcess, &dwExitCode); vtProcess.second->iRetVal = static_cast(dwExitCode); // Do not convert to 64-bit! // Close the process handle CloseHandle(vtProcess.second->hProcess); vtProcess.second->hProcess = 0; m_vecTerminatedProcesses.push_back(vtProcess.second); TRACE(GetTimestamp(), "Adding process PID#", vtProcess.second->tProcessID, " to termination map with exit code ", vtProcess.second->iRetVal); } #elif defined __unix__ // There are two ways of checking whether a process is still running. If the monitor process is the parent process, the // waitpid function should be used to request the process state. If the process was not created by the parent process, // the kill function returns information about the process. // After termination a process will stay dorment until the parent process has received the exit value of the process. // This is also the reason why the kill function is not working with the parent process, since it is only returning // a result after the process is not available at all any more (also not dorment). // Use a flag to indicate that the process is not a child process of the monitor process. if (!vtProcess.second->bNotAChild) { // Check with waitpid first int iStatus = 0; #ifdef WCONTINUED pid_t pid = waitpid(vtProcess.second->tProcessID, &iStatus, WCONTINUED | WNOHANG); #else pid_t pid = waitpid(vtProcess.second->tProcessID, &iStatus, WNOHANG); #endif switch (pid) { case -1: // Not a child process of the monitor process. Or a signal is returned to the calling process. Use kill instead. vtProcess.second->bNotAChild = true; SDV_LOG_TRACE(getpid(), " Process ", vtProcess.second->tProcessID, " is not child process..."); break; case 0: // Process still running, no status available. break; default: if (WIFEXITED(iStatus)) { m_vecTerminatedProcesses.push_back(vtProcess.second); vtProcess.second->iRetVal = static_cast(WEXITSTATUS(iStatus)); SDV_LOG_TRACE(getpid(), " Normal exit detected for process ", vtProcess.second->tProcessID); } else if (WIFSIGNALED(iStatus)) { m_vecTerminatedProcesses.push_back(vtProcess.second); // Note: The status is not reported by the process, since it was terminated without return value. //vtProcess.second->iRetVal = static_cast(WTERMSIG(iStatus)); vtProcess.second->iRetVal = -100; SDV_LOG_TRACE(getpid(), " Signalled stop detected for process ", vtProcess.second->tProcessID); } else if (WIFSTOPPED(iStatus)) { m_vecTerminatedProcesses.push_back(vtProcess.second); vtProcess.second->iRetVal = static_cast(WSTOPSIG(iStatus)); SDV_LOG_TRACE(getpid(), " Terminate exit detected for process ", vtProcess.second->tProcessID); } SDV_LOG_TRACE(GetTimestamp(), "Exit detected with exit code ", vtProcess.second->iRetVal); } } // For a process not being the child of the monitor process, check with the kill function. // Note: check for the bNotAChild flag once more, since it might be set by the waitpid analysis. if (vtProcess.second->bNotAChild) { // Use "kill" to detect the existence of the process // Notice: this doesn't kill the process. kill(vtProcess.first, 0); if (errno == ESRCH) { m_vecTerminatedProcesses.push_back(vtProcess.second); SDV_LOG_TRACE(getpid(), " Non-child exit detected for process ", vtProcess.second->tProcessID); // No exit code available... } } #else #error OS is not supported! #endif } // Allow changes lock.unlock(); // Is there a vector of terminated process IDs? for (auto ptrTerminatedProcess : m_vecTerminatedProcesses) { std::unique_lock lockProcess(ptrTerminatedProcess->mtxProcess); // Reset the running flag ptrTerminatedProcess->bRunning = false; // Call the callback functions // Note: The unregister function might change the pointers in the map. Make a copy. auto mapAssociatedMonitorsCopy = ptrTerminatedProcess->mapAssociatedMonitors; lockProcess.unlock(); for (auto& rvtMonitor : mapAssociatedMonitorsCopy) { // Check whether the monitor still exists lockProcess.lock(); auto itMonitor = ptrTerminatedProcess->mapAssociatedMonitors.find(rvtMonitor.first); if (itMonitor == ptrTerminatedProcess->mapAssociatedMonitors.end() || !itMonitor->second) { // Monitor not available... lockProcess.unlock(); continue; } // Allow updates to take place lockProcess.unlock(); // Call callback rvtMonitor.second->ProcessTerminated(ptrTerminatedProcess->tProcessID, ptrTerminatedProcess->iRetVal); } // Inform all waiting processes ptrTerminatedProcess->cvWaitForProcess.notify_all(); } // Wait for 100ms until next check std::this_thread::sleep_for(std::chrono::milliseconds(100)); } }