Files
T
patrick 4c51989600 feat: initiale Version 0.1.0 des MSTSC Script Hooks
- Native x64 MSTSC-DVC-Erweiterung für lokalen Programmstart
- Automatischer Script-/Programmstart bei RDP-Verbindungsaufbau
- Separater Programmstart über Trigger innerhalb der RDP-Sitzung
- Bidirektionaler DVC zur Ermittlung des lokalen Client-Hostnamens
- SIOMIN-Trigger zur Anbindung lokaler Sidexis-Installationen
- SIOMIN-Datensatzlängen und Hostname-Felder bytegenau angepasst
- Temporäre SIOMIN-Einträge nach erfolgreicher Übernahme bereinigt
- Native Win32-Konfigurationsoberfläche ohne .NET-Abhängigkeit
- Mehrfachstartschutz und optionaler unsichtbarer Programmstart
- Debug- und Plugin-Logging ergänzt
- RemoteVDDS-Anwendungsfall integriert
- Installer/Registrierung des MSTSC-Plugins über Client-Konfigurator
- Techniker-, Entwickler- und Projektdokumentation ergänzt
- Proprietäre Lizenz für Copyright Patrick Gniza hinzugefügt
2026-08-11 16:32:33 +02:00

926 lines
32 KiB
C++

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
#include <shobjidl.h>
#include <shellapi.h>
#include "Config.h"
#include "PluginInstaller.h"
#include "TestLauncher.h"
#include <algorithm>
#include <cwchar>
#include <cwctype>
#include <string>
#include <vector>
#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<WPARAM>(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<HMENU>(static_cast<INT_PTR>(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<wchar_t> buffer(static_cast<size_t>(len) + 1, L'\0');
GetWindowTextW(control, buffer.data(), static_cast<int>(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<wchar_t>(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<INT_PTR>(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<HFONT>(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<MINMAXINFO*>(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<HBRUSH>(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<int>(message.wParam);
}