#define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include #include #include #include #include "Config.h" #include "PluginInstaller.h" #include "TestLauncher.h" #include #include #include #include #include #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") namespace { constexpr wchar_t kWindowClass[] = L"PlandentMstscScriptHookConfigurator"; constexpr wchar_t kTitle[] = L"Plandent MSTSC Script Hook"; enum ControlId : int { IDC_ENABLED = 1001, IDC_LOGGING, IDC_CONN_PATH, IDC_CONN_BROWSE, IDC_CONN_ARGS, IDC_CONN_WORKDIR, IDC_CONN_WORK_BROWSE, IDC_CONN_START, IDC_CONN_STOP, IDC_CONN_HIDDEN, IDC_CONN_DUP, IDC_CONN_TEST, IDC_CONN_TEST_STOP, IDC_TRIGGER_PATH, IDC_TRIGGER_BROWSE, IDC_TRIGGER_ARGS, IDC_TRIGGER_WORKDIR, IDC_TRIGGER_WORK_BROWSE, IDC_TRIGGER_ENABLED, IDC_TRIGGER_HIDDEN, IDC_TRIGGER_DUP, IDC_TRIGGER_TEST, IDC_TRIGGER_TEST_STOP, IDC_SAVE, IDC_INSTALL, IDC_REMOVE, IDC_LOG_OPEN, IDC_TIMER = 2001 }; struct AppState { HWND window = nullptr; HFONT normalFont = nullptr; HFONT titleFont = nullptr; HWND title = nullptr; HWND description = nullptr; HWND generalGroup = nullptr; HWND enabled = nullptr; HWND logging = nullptr; HWND connectGroup = nullptr; HWND connectDescription = nullptr; HWND connectPathLabel = nullptr; HWND connectPath = nullptr; HWND connectBrowse = nullptr; HWND connectArgsLabel = nullptr; HWND connectArgs = nullptr; HWND connectWorkLabel = nullptr; HWND connectWork = nullptr; HWND connectWorkBrowse = nullptr; HWND connectStart = nullptr; HWND connectStop = nullptr; HWND connectHidden = nullptr; HWND connectDup = nullptr; HWND connectTest = nullptr; HWND connectTestStop = nullptr; HWND triggerGroup = nullptr; HWND triggerDescription = nullptr; HWND triggerPathLabel = nullptr; HWND triggerPath = nullptr; HWND triggerBrowse = nullptr; HWND triggerArgsLabel = nullptr; HWND triggerArgs = nullptr; HWND triggerWorkLabel = nullptr; HWND triggerWork = nullptr; HWND triggerWorkBrowse = nullptr; HWND triggerEnabled = nullptr; HWND triggerHidden = nullptr; HWND triggerDup = nullptr; HWND triggerTest = nullptr; HWND triggerTestStop = nullptr; HWND status = nullptr; HWND hint = nullptr; HWND save = nullptr; HWND install = nullptr; HWND remove = nullptr; HWND logOpen = nullptr; testlaunch::TestProcess connectTestProcess; testlaunch::TestProcess triggerTestProcess; }; AppState g; void SetFont(HWND control, HFONT font) { SendMessageW(control, WM_SETFONT, reinterpret_cast(font), TRUE); } HWND MakeControl( const wchar_t* className, const wchar_t* text, DWORD style, DWORD exStyle = 0, int id = 0) { HWND handle = CreateWindowExW( exStyle, className, text, WS_CHILD | WS_VISIBLE | style, 0, 0, 10, 10, g.window, reinterpret_cast(static_cast(id)), GetModuleHandleW(nullptr), nullptr); if (handle != nullptr) { SetFont(handle, g.normalFont); } return handle; } HWND MakeStatic(const wchar_t* text, DWORD style = SS_LEFT) { return MakeControl(L"STATIC", text, style); } HWND MakeButton(const wchar_t* text, int id) { return MakeControl(L"BUTTON", text, BS_PUSHBUTTON | WS_TABSTOP, 0, id); } HWND MakeCheck(const wchar_t* text, int id) { return MakeControl(L"BUTTON", text, BS_AUTOCHECKBOX | WS_TABSTOP, 0, id); } HWND MakeEdit(int id) { return MakeControl(L"EDIT", L"", ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE, id); } std::wstring Text(HWND control) { int len = GetWindowTextLengthW(control); std::vector buffer(static_cast(len) + 1, L'\0'); GetWindowTextW(control, buffer.data(), static_cast(buffer.size())); return std::wstring(buffer.data()); } void SetText(HWND control, const std::wstring& value) { SetWindowTextW(control, value.c_str()); } bool Checked(HWND control) { return SendMessageW(control, BM_GETCHECK, 0, 0) == BST_CHECKED; } void SetChecked(HWND control, bool checked) { SendMessageW(control, BM_SETCHECK, checked ? BST_CHECKED : BST_UNCHECKED, 0); } void Move(HWND control, int x, int y, int width, int height) { MoveWindow(control, x, y, std::max(width, 1), height, TRUE); } std::wstring ParentDirectory(const std::wstring& path) { 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 FileName(const std::wstring& path) { size_t slash = path.find_last_of(L"\\/"); return slash == std::wstring::npos ? path : path.substr(slash + 1); } std::wstring Lower(std::wstring value) { std::transform(value.begin(), value.end(), value.begin(), [](wchar_t c) { return static_cast(std::towlower(c)); }); return value; } std::wstring Trim(const std::wstring& value) { size_t first = 0; while (first < value.size() && std::iswspace(value[first])) { ++first; } size_t last = value.size(); while (last > first && std::iswspace(value[last - 1])) { --last; } return value.substr(first, last - first); } bool FileExists(const std::wstring& path) { DWORD attr = GetFileAttributesW(path.c_str()); return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) == 0; } bool DirectoryExists(const std::wstring& path) { DWORD attr = GetFileAttributesW(path.c_str()); return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) != 0; } bool SupportedExtension(const std::wstring& path) { size_t dot = path.find_last_of(L'.'); if (dot == std::wstring::npos) { return false; } std::wstring ext = Lower(path.substr(dot)); return ext == L".exe" || ext == L".bat" || ext == L".cmd" || ext == L".ps1"; } void ErrorBox(const std::wstring& text, const wchar_t* caption = L"MSTSC Script Hook") { MessageBoxW(g.window, text.c_str(), caption, MB_OK | MB_ICONERROR); } void InfoBox(const std::wstring& text, const wchar_t* caption = L"MSTSC Script Hook") { MessageBoxW(g.window, text.c_str(), caption, MB_OK | MB_ICONINFORMATION); } void WarningBox(const std::wstring& text, const wchar_t* caption = L"MSTSC Script Hook") { MessageBoxW(g.window, text.c_str(), caption, MB_OK | MB_ICONWARNING); } void CreateControls() { g.title = MakeStatic(L"MSTSC Script Hook"); SetFont(g.title, g.titleFont); g.description = MakeStatic( L"Lokale Programme können automatisch beim RDP-Verbindungsaufbau oder gezielt durch eine Anwendung innerhalb der Terminalserver-Sitzung gestartet werden."); g.generalGroup = MakeControl(L"BUTTON", L"Allgemein", BS_GROUPBOX); g.enabled = MakeCheck(L"Plugin-Funktion aktiviert", IDC_ENABLED); g.logging = MakeCheck(L"Plugin-Logging aktivieren", IDC_LOGGING); g.connectGroup = MakeControl(L"BUTTON", L"Automatischer Start bei RDP-Verbindung", BS_GROUPBOX); g.connectDescription = MakeStatic( L"Optionales lokales Programm/Script, das direkt nach erfolgreichem Aufbau der klassischen MSTSC-Verbindung gestartet wird."); g.connectPathLabel = MakeStatic(L"Programm/Script:"); g.connectPath = MakeEdit(IDC_CONN_PATH); g.connectBrowse = MakeButton(L"...", IDC_CONN_BROWSE); g.connectArgsLabel = MakeStatic(L"Argumente:"); g.connectArgs = MakeEdit(IDC_CONN_ARGS); g.connectWorkLabel = MakeStatic(L"Arbeitsverzeichnis:"); g.connectWork = MakeEdit(IDC_CONN_WORKDIR); g.connectWorkBrowse = MakeButton(L"...", IDC_CONN_WORK_BROWSE); g.connectStart = MakeCheck(L"Bei erfolgreicher RDP-Verbindung starten", IDC_CONN_START); g.connectStop = MakeCheck(L"Bei RDP-Trennung beenden", IDC_CONN_STOP); g.connectHidden = MakeCheck(L"Unsichtbar starten", IDC_CONN_HIDDEN); g.connectDup = MakeCheck(L"Mehrfachstart verhindern", IDC_CONN_DUP); g.connectTest = MakeButton(L"Verbindungs-Programm testen", IDC_CONN_TEST); g.connectTestStop = MakeButton(L"Test beenden", IDC_CONN_TEST_STOP); g.triggerGroup = MakeControl(L"BUTTON", L"Programmstart durch Terminalserver", BS_GROUPBOX); g.triggerDescription = MakeStatic( L"Optionales lokales Programm/Script mit eigenen Parametern. Es wird nur gestartet, wenn PlandentRdpClientTrigger.exe innerhalb derselben RDP-Sitzung auf dem Terminalserver ausgeführt wird."); g.triggerPathLabel = MakeStatic(L"Programm/Script:"); g.triggerPath = MakeEdit(IDC_TRIGGER_PATH); g.triggerBrowse = MakeButton(L"...", IDC_TRIGGER_BROWSE); g.triggerArgsLabel = MakeStatic(L"Argumente:"); g.triggerArgs = MakeEdit(IDC_TRIGGER_ARGS); g.triggerWorkLabel = MakeStatic(L"Arbeitsverzeichnis:"); g.triggerWork = MakeEdit(IDC_TRIGGER_WORKDIR); g.triggerWorkBrowse = MakeButton(L"...", IDC_TRIGGER_WORK_BROWSE); g.triggerEnabled = MakeCheck(L"Start durch Terminalserver-Trigger erlauben", IDC_TRIGGER_ENABLED); g.triggerHidden = MakeCheck(L"Unsichtbar starten", IDC_TRIGGER_HIDDEN); g.triggerDup = MakeCheck(L"Mehrfachstart verhindern", IDC_TRIGGER_DUP); g.triggerTest = MakeButton(L"Trigger-Programm testen", IDC_TRIGGER_TEST); g.triggerTestStop = MakeButton(L"Test beenden", IDC_TRIGGER_TEST_STOP); g.status = MakeControl(L"STATIC", L"", SS_LEFT | SS_CENTERIMAGE, WS_EX_CLIENTEDGE); g.hint = MakeStatic( L"Hinweis: PlandentRdpClientTrigger.exe auf dem Terminalserver übermittelt ausschließlich einen festen START-Befehl. Programmpfad und Parameter werden niemals vom Server übertragen."); g.save = MakeButton(L"Konfiguration speichern", IDC_SAVE); g.install = MakeButton(L"Plugin installieren", IDC_INSTALL); g.remove = MakeButton(L"Plugin entfernen", IDC_REMOVE); g.logOpen = MakeButton(L"Log öffnen", IDC_LOG_OPEN); EnableWindow(g.connectTestStop, FALSE); EnableWindow(g.triggerTestStop, FALSE); } void LayoutControls(int clientWidth) { const int margin = 18; const int contentWidth = std::max(clientWidth - margin * 2, 760); const int innerX = margin + 14; const int labelWidth = 132; const int browseWidth = 42; const int editX = innerX + labelWidth; const int editWidth = contentWidth - 28 - labelWidth - browseWidth - 6; const int browseX = editX + editWidth + 6; Move(g.title, margin, 14, contentWidth, 30); Move(g.description, margin, 48, contentWidth, 30); Move(g.generalGroup, margin, 82, contentWidth, 54); Move(g.enabled, innerX, 103, 240, 22); Move(g.logging, innerX + 260, 103, 240, 22); Move(g.connectGroup, margin, 142, contentWidth, 198); Move(g.connectDescription, innerX, 162, contentWidth - 28, 26); Move(g.connectPathLabel, innerX, 191, labelWidth - 4, 22); Move(g.connectPath, editX, 188, editWidth, 24); Move(g.connectBrowse, browseX, 188, browseWidth, 25); Move(g.connectArgsLabel, innerX, 220, labelWidth - 4, 22); Move(g.connectArgs, editX, 217, editWidth + browseWidth + 6, 24); Move(g.connectWorkLabel, innerX, 249, labelWidth - 4, 22); Move(g.connectWork, editX, 246, editWidth, 24); Move(g.connectWorkBrowse, browseX, 246, browseWidth, 25); Move(g.connectStart, innerX, 278, 285, 22); Move(g.connectStop, innerX + 292, 278, 210, 22); Move(g.connectHidden, innerX + 505, 278, 150, 22); Move(g.connectDup, innerX + 658, 278, 190, 22); Move(g.connectTest, innerX, 307, 220, 26); Move(g.connectTestStop, innerX + 228, 307, 120, 26); Move(g.triggerGroup, margin, 346, contentWidth, 190); Move(g.triggerDescription, innerX, 366, contentWidth - 28, 31); Move(g.triggerPathLabel, innerX, 400, labelWidth - 4, 22); Move(g.triggerPath, editX, 397, editWidth, 24); Move(g.triggerBrowse, browseX, 397, browseWidth, 25); Move(g.triggerArgsLabel, innerX, 429, labelWidth - 4, 22); Move(g.triggerArgs, editX, 426, editWidth + browseWidth + 6, 24); Move(g.triggerWorkLabel, innerX, 458, labelWidth - 4, 22); Move(g.triggerWork, editX, 455, editWidth, 24); Move(g.triggerWorkBrowse, browseX, 455, browseWidth, 25); Move(g.triggerEnabled, innerX, 487, 315, 22); Move(g.triggerHidden, innerX + 322, 487, 150, 22); Move(g.triggerDup, innerX + 475, 487, 190, 22); Move(g.triggerTest, innerX, 510, 190, 26); Move(g.triggerTestStop, innerX + 198, 510, 120, 26); Move(g.status, margin, 544, contentWidth, 44); Move(g.hint, margin, 596, contentWidth, 38); int x = margin; Move(g.save, x, 642, 178, 28); x += 186; Move(g.install, x, 642, 150, 28); x += 158; Move(g.remove, x, 642, 145, 28); x += 153; Move(g.logOpen, x, 642, 100, 28); } config::HookConfig ReadForm() { config::HookConfig value; value.enabled = Checked(g.enabled); value.enableLogging = Checked(g.logging); value.scriptPath = Trim(Text(g.connectPath)); value.arguments = Trim(Text(g.connectArgs)); value.workingDirectory = Trim(Text(g.connectWork)); value.startOnConnect = Checked(g.connectStart); value.stopOnDisconnect = Checked(g.connectStop); value.hidden = Checked(g.connectHidden); value.preventDuplicates = Checked(g.connectDup); value.triggerEnabled = Checked(g.triggerEnabled); value.triggerProgramPath = Trim(Text(g.triggerPath)); value.triggerArguments = Trim(Text(g.triggerArgs)); value.triggerWorkingDirectory = Trim(Text(g.triggerWork)); value.triggerHidden = Checked(g.triggerHidden); value.triggerPreventDuplicates = Checked(g.triggerDup); return value; } void RefreshStatus() { installer::Status plugin = installer::GetStatus(); std::wstring connectStatus = Checked(g.connectStart) ? (Text(g.connectPath).empty() ? L" RDP-Start: nicht konfiguriert." : L" RDP-Start: konfiguriert.") : L" RDP-Start: aus."; std::wstring triggerStatus = Checked(g.triggerEnabled) ? (Text(g.triggerPath).empty() ? L" Server-Trigger: nicht konfiguriert." : L" Server-Trigger: konfiguriert.") : L" Server-Trigger: aus."; SetText(g.status, std::wstring(plugin.installed ? L"✓ " : L"○ ") + plugin.text + connectStatus + triggerStatus); } void LoadConfiguration() { config::HookConfig value = config::Load(); SetChecked(g.enabled, value.enabled); SetChecked(g.logging, value.enableLogging); SetText(g.connectPath, value.scriptPath); SetText(g.connectArgs, value.arguments); SetText(g.connectWork, value.workingDirectory); SetChecked(g.connectStart, value.startOnConnect); SetChecked(g.connectStop, value.stopOnDisconnect); SetChecked(g.connectHidden, value.hidden); SetChecked(g.connectDup, value.preventDuplicates); SetText(g.triggerPath, value.triggerProgramPath); SetText(g.triggerArgs, value.triggerArguments); SetText(g.triggerWork, value.triggerWorkingDirectory); SetChecked(g.triggerEnabled, value.triggerEnabled); SetChecked(g.triggerHidden, value.triggerHidden); SetChecked(g.triggerDup, value.triggerPreventDuplicates); RefreshStatus(); } bool ValidateLaunchTarget( const std::wstring& configuredPath, const std::wstring& configuredWorkingDirectory, const std::wstring& displayName, bool required, std::wstring& error) { std::wstring path = config::ExpandEnvironment(configuredPath); if (path.empty()) { if (required) { error = L"Bitte für „" + displayName + L"“ ein Programm oder Script auswählen."; return false; } return true; } if (!FileExists(path)) { error = displayName + L" wurde nicht gefunden:\n" + path; return false; } if (!SupportedExtension(path)) { error = displayName + L": Unterstützt werden .bat, .cmd, .ps1 und .exe."; return false; } if (!configuredWorkingDirectory.empty()) { std::wstring work = config::ExpandEnvironment(configuredWorkingDirectory); if (!DirectoryExists(work)) { error = L"Arbeitsverzeichnis für „" + displayName + L"“ wurde nicht gefunden:\n" + work; return false; } } return true; } bool ValidateConfiguration(const config::HookConfig& value, bool installation, std::wstring& error) { if (!ValidateLaunchTarget(value.scriptPath, value.workingDirectory, L"RDP-Verbindungsprogramm", false, error) || !ValidateLaunchTarget(value.triggerProgramPath, value.triggerWorkingDirectory, L"Trigger-Programm", false, error)) { return false; } if (!installation || !value.enabled) { return true; } if (value.startOnConnect && !ValidateLaunchTarget(value.scriptPath, value.workingDirectory, L"RDP-Verbindungsprogramm", true, error)) { return false; } if (value.triggerEnabled && !ValidateLaunchTarget(value.triggerProgramPath, value.triggerWorkingDirectory, L"Trigger-Programm", true, error)) { return false; } return true; } bool SaveConfiguration(bool showConfirmation, bool installationValidation) { config::HookConfig value = ReadForm(); std::wstring error; if (!ValidateConfiguration(value, installationValidation, error)) { ErrorBox(error, L"Konfiguration ist ungültig"); return false; } if (!config::Save(value, error)) { ErrorBox(error, L"Konfiguration konnte nicht gespeichert werden"); return false; } RefreshStatus(); if (showConfirmation) { InfoBox(L"Konfiguration wurde gespeichert."); } return true; } void BrowseProgram(HWND pathControl, HWND workControl, HWND argsControl, bool remoteVddsPreset) { wchar_t fileBuffer[32768]{}; std::wstring current = config::ExpandEnvironment(Text(pathControl)); if (!current.empty()) { wcsncpy_s(fileBuffer, current.c_str(), _TRUNCATE); } std::wstring initialDir = ParentDirectory(current); const wchar_t filter[] = L"Unterstützte Dateien (*.exe;*.bat;*.cmd;*.ps1)\0*.exe;*.bat;*.cmd;*.ps1\0" L"Alle Dateien (*.*)\0*.*\0\0"; OPENFILENAMEW ofn{}; ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = g.window; ofn.lpstrFile = fileBuffer; ofn.nMaxFile = ARRAYSIZE(fileBuffer); ofn.lpstrFilter = filter; ofn.lpstrTitle = L"Programm oder Script auswählen"; ofn.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str(); ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR; if (!GetOpenFileNameW(&ofn)) { return; } SetText(pathControl, fileBuffer); if (Text(workControl).empty()) { SetText(workControl, ParentDirectory(fileBuffer)); } if (remoteVddsPreset && Text(argsControl).empty()) { std::wstring fileName = Lower(FileName(fileBuffer)); if (fileName.find(L"remotevdds_receiver_ts") != std::wstring::npos) { SetText(argsControl, L"MIN"); } } RefreshStatus(); } void BrowseDirectory(HWND target) { IFileDialog* dialog = nullptr; HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog)); if (FAILED(hr) || dialog == nullptr) { ErrorBox(L"Ordnerauswahl konnte nicht geöffnet werden."); return; } DWORD options = 0; dialog->GetOptions(&options); dialog->SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST); dialog->SetTitle(L"Arbeitsverzeichnis auswählen"); std::wstring current = config::ExpandEnvironment(Text(target)); if (DirectoryExists(current)) { IShellItem* folder = nullptr; if (SUCCEEDED(SHCreateItemFromParsingName(current.c_str(), nullptr, IID_PPV_ARGS(&folder))) && folder != nullptr) { dialog->SetFolder(folder); folder->Release(); } } if (SUCCEEDED(dialog->Show(g.window))) { IShellItem* item = nullptr; if (SUCCEEDED(dialog->GetResult(&item)) && item != nullptr) { PWSTR path = nullptr; if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path != nullptr) { SetText(target, path); CoTaskMemFree(path); } item->Release(); } } dialog->Release(); } testlaunch::LaunchSettings ConnectionLaunchSettings(const config::HookConfig& value) { return { value.scriptPath, value.arguments, value.workingDirectory, value.hidden }; } testlaunch::LaunchSettings TriggerLaunchSettings(const config::HookConfig& value) { return { value.triggerProgramPath, value.triggerArguments, value.triggerWorkingDirectory, value.triggerHidden }; } void StartConnectionTest() { config::HookConfig value = ReadForm(); std::wstring error; if (!ValidateLaunchTarget(value.scriptPath, value.workingDirectory, L"RDP-Verbindungsprogramm", true, error)) { ErrorBox(error, L"Test fehlgeschlagen"); return; } if (g.connectTestProcess.IsRunning()) { g.connectTestProcess.Stop(error); } if (!g.connectTestProcess.Start(ConnectionLaunchSettings(value), error)) { ErrorBox(error, L"Test fehlgeschlagen"); return; } EnableWindow(g.connectTestStop, TRUE); SetText(g.status, L"Verbindungs-Test gestartet: PID " + std::to_wstring(g.connectTestProcess.ProcessId()) + L". Der Test läuft unabhängig von MSTSC."); } void StartTriggerTest() { config::HookConfig value = ReadForm(); std::wstring error; if (!ValidateLaunchTarget(value.triggerProgramPath, value.triggerWorkingDirectory, L"Trigger-Programm", true, error)) { ErrorBox(error, L"Trigger-Programm-Test fehlgeschlagen"); return; } if (g.triggerTestProcess.IsRunning()) { g.triggerTestProcess.Stop(error); } if (!g.triggerTestProcess.Start(TriggerLaunchSettings(value), error)) { ErrorBox(error, L"Trigger-Programm-Test fehlgeschlagen"); return; } EnableWindow(g.triggerTestStop, TRUE); SetText(g.status, L"Trigger-Programm-Test gestartet: PID " + std::to_wstring(g.triggerTestProcess.ProcessId()) + L". Der Test läuft ohne Terminalserver-Trigger."); } void StopTest(testlaunch::TestProcess& process, HWND button, const wchar_t* description) { std::wstring error; if (!process.Stop(error)) { WarningBox(error, description); return; } EnableWindow(button, FALSE); SetText(g.status, std::wstring(description) + L" wurde beendet."); } void InstallPlugin() { if (!SaveConfiguration(false, true)) { return; } std::wstring error; if (!installer::Install(error)) { ErrorBox(error, L"Installation fehlgeschlagen"); return; } RefreshStatus(); InfoBox( L"Plugin wurde für den aktuellen Benutzer installiert und die Konfiguration gespeichert.\n\n" L"Bereits geöffnete mstsc.exe-Fenster vollständig schließen und anschließend neu starten, damit die neue DLL geladen wird.", L"Installation abgeschlossen"); } void RemovePlugin() { std::wstring message; bool fullyRemoved = false; if (!installer::Uninstall(message, fullyRemoved)) { ErrorBox(message, L"Plugin entfernen"); return; } RefreshStatus(); if (fullyRemoved) { InfoBox(message, L"Plugin entfernen"); } else { WarningBox(message, L"Plugin entfernen"); } } void OpenLog() { std::wstring path = installer::LogPath(); if (!FileExists(path)) { InfoBox(L"Es existiert noch kein Plugin-Log."); return; } HINSTANCE result = ShellExecuteW(g.window, L"open", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL); if (reinterpret_cast(result) <= 32) { ErrorBox(L"Plugin-Log konnte nicht geöffnet werden."); } } void HandleTimer() { bool connectionRunning = g.connectTestProcess.IsRunning(); bool triggerRunning = g.triggerTestProcess.IsRunning(); EnableWindow(g.connectTestStop, connectionRunning ? TRUE : FALSE); EnableWindow(g.triggerTestStop, triggerRunning ? TRUE : FALSE); } LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CREATE: g.window = hwnd; g.normalFont = static_cast(GetStockObject(DEFAULT_GUI_FONT)); g.titleFont = CreateFontW( -24, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); CreateControls(); LoadConfiguration(); SetTimer(hwnd, IDC_TIMER, 1000, nullptr); return 0; case WM_SIZE: LayoutControls(LOWORD(lParam)); return 0; case WM_GETMINMAXINFO: { auto* info = reinterpret_cast(lParam); info->ptMinTrackSize.x = 920; info->ptMinTrackSize.y = 720; return 0; } case WM_TIMER: if (wParam == IDC_TIMER) { HandleTimer(); } return 0; case WM_COMMAND: switch (LOWORD(wParam)) { case IDC_CONN_BROWSE: BrowseProgram(g.connectPath, g.connectWork, g.connectArgs, true); break; case IDC_CONN_WORK_BROWSE: BrowseDirectory(g.connectWork); break; case IDC_TRIGGER_BROWSE: BrowseProgram(g.triggerPath, g.triggerWork, g.triggerArgs, false); break; case IDC_TRIGGER_WORK_BROWSE: BrowseDirectory(g.triggerWork); break; case IDC_CONN_TEST: StartConnectionTest(); break; case IDC_CONN_TEST_STOP: StopTest(g.connectTestProcess, g.connectTestStop, L"Verbindungs-Test"); break; case IDC_TRIGGER_TEST: StartTriggerTest(); break; case IDC_TRIGGER_TEST_STOP: StopTest(g.triggerTestProcess, g.triggerTestStop, L"Trigger-Programm-Test"); break; case IDC_SAVE: SaveConfiguration(true, false); break; case IDC_INSTALL: InstallPlugin(); break; case IDC_REMOVE: RemovePlugin(); break; case IDC_LOG_OPEN: OpenLog(); break; case IDC_ENABLED: case IDC_CONN_START: case IDC_TRIGGER_ENABLED: if (HIWORD(wParam) == BN_CLICKED) { RefreshStatus(); } break; default: break; } return 0; case WM_DESTROY: KillTimer(hwnd, IDC_TIMER); // Wie beim bisherigen .NET-Konfigurator werden Testprozesse beim Schließen // der GUI nicht zwangsweise beendet; nur die lokalen Handles werden freigegeben. g.connectTestProcess.Detach(); g.triggerTestProcess.Detach(); if (g.titleFont != nullptr) { DeleteObject(g.titleFont); g.titleFont = nullptr; } PostQuitMessage(0); return 0; } return DefWindowProcW(hwnd, message, wParam, lParam); } } int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int showCommand) { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); HRESULT com = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); INITCOMMONCONTROLSEX controls{}; controls.dwSize = sizeof(controls); controls.dwICC = ICC_STANDARD_CLASSES; InitCommonControlsEx(&controls); WNDCLASSEXW wc{}; wc.cbSize = sizeof(wc); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WindowProc; wc.hInstance = instance; wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION); wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); wc.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); wc.lpszClassName = kWindowClass; wc.hIconSm = LoadIconW(nullptr, IDI_APPLICATION); if (!RegisterClassExW(&wc)) { if (SUCCEEDED(com)) CoUninitialize(); return 1; } HWND window = CreateWindowExW( 0, kWindowClass, kTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 940, 730, nullptr, nullptr, instance, nullptr); if (window == nullptr) { if (SUCCEEDED(com)) CoUninitialize(); return 2; } ShowWindow(window, showCommand); UpdateWindow(window); MSG message{}; while (GetMessageW(&message, nullptr, 0, 0) > 0) { TranslateMessage(&message); DispatchMessageW(&message); } if (SUCCEEDED(com)) CoUninitialize(); return static_cast(message.wParam); }