- 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
926 lines
32 KiB
C++
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);
|
|
}
|