#include "ProcessLauncher.h" #include "Logging.h" #include #include #include #include #include #include namespace { std::wstring QuoteWindowsArgument(const std::wstring& arg) { if (arg.empty()) { return L"\"\""; } if (arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) { return arg; } std::wstring result = L"\""; size_t backslashes = 0; for (wchar_t ch : arg) { if (ch == L'\\') { ++backslashes; continue; } if (ch == L'\"') { result.append(backslashes * 2 + 1, L'\\'); result.push_back(L'\"'); backslashes = 0; continue; } result.append(backslashes, L'\\'); backslashes = 0; result.push_back(ch); } result.append(backslashes * 2, L'\\'); result.push_back(L'\"'); return result; } std::wstring GetExtensionLower(const std::wstring& path) { const size_t dot = path.find_last_of(L'.'); const size_t slash = path.find_last_of(L"\\/"); if (dot == std::wstring::npos || (slash != std::wstring::npos && dot < slash)) { return {}; } std::wstring ext = path.substr(dot); std::transform(ext.begin(), ext.end(), ext.begin(), [](wchar_t c) { return static_cast(std::towlower(c)); }); return ext; } std::wstring ParentDirectory(const std::wstring& path) { const size_t slash = path.find_last_of(L"\\/"); if (slash == std::wstring::npos) { return {}; } if (slash == 2 && path.size() >= 3 && path[1] == L':') { return path.substr(0, 3); } return path.substr(0, slash); } std::wstring GetEnvironmentValue(const wchar_t* name) { DWORD size = GetEnvironmentVariableW(name, nullptr, 0); if (size == 0) { return {}; } std::vector buffer(size, L'\0'); if (GetEnvironmentVariableW(name, buffer.data(), size) == 0) { return {}; } return std::wstring(buffer.data()); } uint64_t HashPath(const std::wstring& path) { constexpr uint64_t fnvOffset = 14695981039346656037ull; constexpr uint64_t fnvPrime = 1099511628211ull; uint64_t hash = fnvOffset; for (wchar_t ch : path) { wchar_t lower = static_cast(std::towlower(ch)); hash ^= static_cast(lower); hash *= fnvPrime; } return hash; } std::wstring MutexNameForScript(const std::wstring& scriptPath) { std::wstringstream stream; stream << L"Local\\Plandent.MstscScriptHook." << std::uppercase << std::hex << std::setw(16) << std::setfill(L'0') << HashPath(scriptPath); return stream.str(); } struct LaunchCommand { std::wstring application; std::wstring commandLine; }; bool BuildLaunchCommand(const hook::LaunchConfig& config, LaunchCommand& command) { const std::wstring extension = GetExtensionLower(config.path); if (extension == L".bat" || extension == L".cmd") { std::wstring comspec = GetEnvironmentValue(L"ComSpec"); if (comspec.empty()) { std::wstring systemRoot = GetEnvironmentValue(L"SystemRoot"); if (systemRoot.empty()) { return false; } comspec = systemRoot + L"\\System32\\cmd.exe"; } std::wstring inner = QuoteWindowsArgument(config.path); if (!config.arguments.empty()) { inner += L" " + config.arguments; } command.application = comspec; command.commandLine = QuoteWindowsArgument(comspec) + L" /D /S /C \"" + inner + L"\""; return true; } if (extension == L".ps1") { std::wstring systemRoot = GetEnvironmentValue(L"SystemRoot"); if (systemRoot.empty()) { return false; } std::wstring powershell = systemRoot + L"\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; command.application = powershell; command.commandLine = QuoteWindowsArgument(powershell) + L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " + QuoteWindowsArgument(config.path); if (!config.arguments.empty()) { command.commandLine += L" " + config.arguments; } return true; } if (extension == L".exe") { command.application = config.path; command.commandLine = QuoteWindowsArgument(config.path); if (!config.arguments.empty()) { command.commandLine += L" " + config.arguments; } return true; } return false; } } namespace hook { ProcessLauncher::~ProcessLauncher() { if (process_ != nullptr) { CloseHandle(process_); process_ = nullptr; } } void ProcessLauncher::CleanupExitedProcess() { if (process_ == nullptr) { return; } if (WaitForSingleObject(process_, 0) != WAIT_TIMEOUT) { CloseHandle(process_); process_ = nullptr; processId_ = 0; } } bool ProcessLauncher::IsRunning() { CleanupExitedProcess(); return process_ != nullptr; } bool ProcessLauncher::Start(const LaunchConfig& config, bool loggingEnabled, const std::wstring& sourceName) { CleanupExitedProcess(); if (process_ != nullptr) { Log(loggingEnabled, sourceName + L" ist für diese MSTSC-Instanz bereits aktiv (PID " + std::to_wstring(processId_) + L")."); return true; } if (config.path.empty()) { Log(loggingEnabled, L"Kein Pfad für " + sourceName + L" konfiguriert; Start übersprungen."); return false; } DWORD attributes = GetFileAttributesW(config.path.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { Log(loggingEnabled, sourceName + L" wurde nicht gefunden: " + config.path); return false; } LaunchCommand command; if (!BuildLaunchCommand(config, command)) { Log(loggingEnabled, L"Nicht unterstützter Datei-/Programmtyp für " + sourceName + L": " + config.path); return false; } std::wstring workingDirectory = config.workingDirectory.empty() ? ParentDirectory(config.path) : config.workingDirectory; HANDLE duplicateMutex = nullptr; if (config.preventDuplicates) { SECURITY_ATTRIBUTES sa{}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; std::wstring mutexName = MutexNameForScript(config.path); duplicateMutex = CreateMutexW(&sa, FALSE, mutexName.c_str()); if (duplicateMutex == nullptr) { DWORD error = GetLastError(); Log(loggingEnabled, L"Mehrfachstart-Schutz konnte nicht erstellt werden: " + FormatWin32Error(error)); return false; } if (GetLastError() == ERROR_ALREADY_EXISTS) { CloseHandle(duplicateMutex); Log(loggingEnabled, sourceName + L" läuft laut Mehrfachstart-Schutz bereits; kein weiterer Start."); return true; } } PROCESS_INFORMATION pi{}; DWORD creationFlags = 0; if (config.hidden) { creationFlags |= CREATE_NO_WINDOW; } std::vector mutableCommand(command.commandLine.begin(), command.commandLine.end()); mutableCommand.push_back(L'\0'); BOOL created = FALSE; if (duplicateMutex != nullptr) { STARTUPINFOEXW startup{}; startup.StartupInfo.cb = sizeof(startup); if (config.hidden) { startup.StartupInfo.dwFlags |= STARTF_USESHOWWINDOW; startup.StartupInfo.wShowWindow = SW_HIDE; } SIZE_T attributeSize = 0; InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeSize); std::vector attributeBuffer(attributeSize); startup.lpAttributeList = reinterpret_cast(attributeBuffer.data()); if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &attributeSize)) { DWORD error = GetLastError(); CloseHandle(duplicateMutex); Log(loggingEnabled, L"Prozessattributliste konnte nicht initialisiert werden: " + FormatWin32Error(error)); return false; } HANDLE handles[] = { duplicateMutex }; if (!UpdateProcThreadAttribute( startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr)) { DWORD error = GetLastError(); DeleteProcThreadAttributeList(startup.lpAttributeList); CloseHandle(duplicateMutex); Log(loggingEnabled, L"Handle-Vererbung konnte nicht vorbereitet werden: " + FormatWin32Error(error)); return false; } creationFlags |= EXTENDED_STARTUPINFO_PRESENT; created = CreateProcessW( command.application.c_str(), mutableCommand.data(), nullptr, nullptr, TRUE, creationFlags, nullptr, workingDirectory.empty() ? nullptr : workingDirectory.c_str(), &startup.StartupInfo, &pi); DeleteProcThreadAttributeList(startup.lpAttributeList); // Der Child-Prozess hat bei erfolgreichem Start eine eigene Referenz auf den Mutex. // Dadurch bleibt der Mehrfachstart-Schutz auch bestehen, wenn mstsc.exe später endet. CloseHandle(duplicateMutex); } else { STARTUPINFOW startup{}; startup.cb = sizeof(startup); if (config.hidden) { startup.dwFlags |= STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE; } created = CreateProcessW( command.application.c_str(), mutableCommand.data(), nullptr, nullptr, FALSE, creationFlags, nullptr, workingDirectory.empty() ? nullptr : workingDirectory.c_str(), &startup, &pi); } if (!created) { DWORD error = GetLastError(); Log(loggingEnabled, sourceName + L" konnte nicht gestartet werden: " + FormatWin32Error(error)); return false; } CloseHandle(pi.hThread); process_ = pi.hProcess; processId_ = pi.dwProcessId; Log(loggingEnabled, sourceName + L" gestartet. PID=" + std::to_wstring(processId_) + L", Pfad=" + config.path + (config.arguments.empty() ? L"" : L", Argumente=" + config.arguments)); return true; } void ProcessLauncher::Stop(bool loggingEnabled, const std::wstring& sourceName) { CleanupExitedProcess(); if (process_ == nullptr) { return; } const DWORD pid = processId_; if (!TerminateProcess(process_, 0)) { DWORD error = GetLastError(); Log(loggingEnabled, sourceName + L" PID " + std::to_wstring(pid) + L" konnte nicht beendet werden: " + FormatWin32Error(error)); } else { Log(loggingEnabled, L"Beenden von " + sourceName + L" PID " + std::to_wstring(pid) + L" wurde angefordert."); } CloseHandle(process_); process_ = nullptr; processId_ = 0; } void ProcessLauncher::Detach(bool loggingEnabled, const std::wstring& sourceName) { CleanupExitedProcess(); if (process_ == nullptr) { return; } Log(loggingEnabled, L"MSTSC beendet Plugin; " + sourceName + L" PID " + std::to_wstring(processId_) + L" läuft weiter."); CloseHandle(process_); process_ = nullptr; processId_ = 0; } }