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
This commit is contained in:
2026-08-11 16:32:33 +02:00
parent ee88f1ff30
commit 4c51989600
37 changed files with 4885 additions and 1475 deletions
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <string>
namespace config
{
inline constexpr wchar_t kConfigKey[] = L"Software\\Plandent\\MstscScriptHook";
inline constexpr wchar_t kAddInKey[] = L"Software\\Microsoft\\Terminal Server Client\\Default\\AddIns\\PlandentMstscScriptHook";
struct HookConfig
{
bool enabled = true;
bool enableLogging = true;
std::wstring scriptPath;
std::wstring arguments;
std::wstring workingDirectory;
bool startOnConnect = true;
bool stopOnDisconnect = false;
bool hidden = true;
bool preventDuplicates = true;
bool triggerEnabled = false;
std::wstring triggerProgramPath;
std::wstring triggerArguments;
std::wstring triggerWorkingDirectory;
bool triggerHidden = true;
bool triggerPreventDuplicates = true;
};
HookConfig Load();
bool Save(const HookConfig& value, std::wstring& error);
std::wstring GetRegisteredPluginPath();
bool RegisterPlugin(const std::wstring& dllPath, std::wstring& error);
bool UnregisterPlugin(std::wstring& error);
std::wstring ExpandEnvironment(const std::wstring& value);
}
-39
View File
@@ -1,39 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>PlandentMstscScriptHook</AssemblyName>
<RootNamespace>Plandent.MstscScriptHook.Configurator</RootNamespace>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<PluginPayloadPath>$(MSBuildProjectDirectory)\..\artifacts\x64\$(Configuration)\PlandentMstscScriptHook.dll</PluginPayloadPath>
</PropertyGroup>
<!-- Beim normalen Visual-Studio-Debugging wird die lokal installierte .NET-8-Runtime benutzt.
Dadurch werden keine win-x64 Runtime-Packs von NuGet benötigt. -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<SelfContained>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<DebugType>portable</DebugType>
</PropertyGroup>
<!-- Nur das Release-Publish erzeugt die verteilbare self-contained Single-File-EXE. -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="$(PluginPayloadPath)" LogicalName="PlandentMstscScriptHook.Native.PlandentMstscScriptHook.dll" />
</ItemGroup>
<Target Name="CheckNativePlugin" BeforeTargets="CreateManifestResourceNames">
<Error Condition="!Exists('$(PluginPayloadPath)')" Text="Native Plugin-DLL fehlt: $(PluginPayloadPath). Zuerst Plugin bauen oder build-release.ps1 verwenden." />
</Target>
</Project>
+3
View File
@@ -0,0 +1,3 @@
#include "resource.h"
IDR_PLUGIN_DLL RCDATA "Generated\\PlandentMstscScriptHook.dll"
+134
View File
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}</ProjectGuid>
<RootNamespace>PlandentMstscScriptHookConfigurator</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)..\artifacts\obj\$(Platform)\$(Configuration)\Configurator\</IntDir>
<TargetName>PlandentMstscScriptHook</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Advapi32.lib;Shell32.lib;Comdlg32.lib;Ole32.lib;Comctl32.lib;User32.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PreBuildEvent>
<Command>if not exist "$(ProjectDir)Generated" mkdir "$(ProjectDir)Generated"
if not exist "$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\PlandentMstscScriptHook.dll" (echo Native Plugin-DLL fehlt. Projektabhaengigkeit pruefen. &amp; exit /b 1)
copy /Y "$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\PlandentMstscScriptHook.dll" "$(ProjectDir)Generated\PlandentMstscScriptHook.dll" &gt;nul</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>false</GenerateDebugInformation>
<AdditionalDependencies>Advapi32.lib;Shell32.lib;Comdlg32.lib;Ole32.lib;Comctl32.lib;User32.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PreBuildEvent>
<Command>if not exist "$(ProjectDir)Generated" mkdir "$(ProjectDir)Generated"
if not exist "$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\PlandentMstscScriptHook.dll" (echo Native Plugin-DLL fehlt. Projektabhaengigkeit pruefen. &amp; exit /b 1)
copy /Y "$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\PlandentMstscScriptHook.dll" "$(ProjectDir)Generated\PlandentMstscScriptHook.dll" &gt;nul</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="Main.cpp" />
<ClCompile Include="PluginInstaller.cpp" />
<ClCompile Include="RegistryConfig.cpp" />
<ClCompile Include="TestLauncher.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Config.h" />
<ClInclude Include="PluginInstaller.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="TestLauncher.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Configurator.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Plugin\PlandentMstscScriptHook.vcxproj">
<Project>{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<LinkLibraryDependencies>false</LinkLibraryDependencies>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{D7D3B9B1-4B80-4A4B-888D-E7CA10A76FC5}</UniqueIdentifier>
<Extensions>cpp;c;cxx;cc</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{6C56EC9E-3B39-44B2-B913-74A798B7EA8F}</UniqueIdentifier>
<Extensions>h;hpp;hxx</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{37447730-171A-4380-A30C-CA33A0A2FF38}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Main.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="PluginInstaller.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="RegistryConfig.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="TestLauncher.cpp"><Filter>Source Files</Filter></ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Config.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="PluginInstaller.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="resource.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="TestLauncher.h"><Filter>Header Files</Filter></ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Configurator.rc"><Filter>Resource Files</Filter></ResourceCompile>
</ItemGroup>
</Project>
-25
View File
@@ -1,25 +0,0 @@
namespace Plandent.MstscScriptHook.Configurator;
internal sealed class HookConfig
{
public bool Enabled { get; set; } = true;
public bool EnableLogging { get; set; } = true;
// Automatischer Start bei erfolgreicher RDP-Verbindung.
public string ScriptPath { get; set; } = string.Empty;
public string Arguments { get; set; } = string.Empty;
public string WorkingDirectory { get; set; } = string.Empty;
public bool StartOnConnect { get; set; } = true;
public bool StopOnDisconnect { get; set; }
public bool Hidden { get; set; } = true;
public bool PreventDuplicates { get; set; } = true;
// Separates lokales Programm, das ausschließlich durch den festen
// START-Trigger vom Terminalserver gestartet werden kann.
public bool TriggerEnabled { get; set; }
public string TriggerProgramPath { get; set; } = string.Empty;
public string TriggerArguments { get; set; } = string.Empty;
public string TriggerWorkingDirectory { get; set; } = string.Empty;
public bool TriggerHidden { get; set; } = true;
public bool TriggerPreventDuplicates { get; set; } = true;
}
+925
View File
@@ -0,0 +1,925 @@
#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);
}
-699
View File
@@ -1,699 +0,0 @@
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
namespace Plandent.MstscScriptHook.Configurator;
internal sealed class MainForm : Form
{
// Global
private readonly CheckBox enabledCheckBox = new() { Text = "Plugin-Funktion aktiviert", AutoSize = true };
private readonly CheckBox loggingCheckBox = new() { Text = "Plugin-Logging aktivieren", AutoSize = true };
// Automatischer Start bei RDP-Verbindung
private readonly TextBox scriptPathTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox argumentsTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox workingDirectoryTextBox = new() { Dock = DockStyle.Fill };
private readonly CheckBox startOnConnectCheckBox = new() { Text = "Bei erfolgreicher RDP-Verbindung starten", AutoSize = true };
private readonly CheckBox stopOnDisconnectCheckBox = new() { Text = "Bei RDP-Trennung beenden", AutoSize = true };
private readonly CheckBox hiddenCheckBox = new() { Text = "Unsichtbar starten", AutoSize = true };
private readonly CheckBox preventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", AutoSize = true };
private readonly Button stopConnectionTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
private Process? connectionTestProcess;
// Separater Start durch Terminalserver-Trigger
private readonly TextBox triggerProgramPathTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox triggerArgumentsTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox triggerWorkingDirectoryTextBox = new() { Dock = DockStyle.Fill };
private readonly CheckBox triggerEnabledCheckBox = new() { Text = "Start durch Terminalserver-Trigger erlauben", AutoSize = true };
private readonly CheckBox triggerHiddenCheckBox = new() { Text = "Unsichtbar starten", AutoSize = true };
private readonly CheckBox triggerPreventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", AutoSize = true };
private readonly Button stopTriggerTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
private Process? triggerTestProcess;
private readonly Label statusLabel = new()
{
AutoSize = false,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(8)
};
public MainForm()
{
Text = "Plandent MSTSC Script Hook";
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(820, 720);
Size = new Size(900, 800);
Font = new Font("Segoe UI", 9F);
Controls.Add(BuildMainLayout());
Load += (_, _) => LoadConfiguration();
FormClosed += (_, _) => DisposeAllTestProcesses();
}
private Control BuildMainLayout()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
Padding = new Padding(18),
ColumnCount = 1,
RowCount = 6
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var title = new Label
{
Text = "MSTSC Script Hook",
Font = new Font(Font.FontFamily, 16F, FontStyle.Bold),
AutoSize = true,
Margin = new Padding(0, 0, 0, 4)
};
root.Controls.Add(title, 0, 0);
var description = new Label
{
Text = "Lokale Programme können automatisch beim RDP-Verbindungsaufbau oder gezielt durch eine Anwendung innerhalb der Terminalserver-Sitzung gestartet werden.",
AutoSize = true,
MaximumSize = new Size(820, 0),
Margin = new Padding(0, 0, 0, 12)
};
root.Controls.Add(description, 0, 1);
var scroll = new Panel
{
Dock = DockStyle.Fill,
AutoScroll = true,
Margin = new Padding(0, 0, 0, 12)
};
var content = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 1,
RowCount = 3
};
content.Controls.Add(BuildGlobalOptionsGroup(), 0, 0);
content.Controls.Add(BuildConnectionProgramGroup(), 0, 1);
content.Controls.Add(BuildTriggerProgramGroup(), 0, 2);
scroll.Controls.Add(content);
root.Controls.Add(scroll, 0, 2);
root.Controls.Add(BuildStatusPanel(), 0, 3);
var hint = new Label
{
Text = "Hinweis: PlandentRdpClientTrigger.exe auf dem Terminalserver übermittelt ausschließlich einen festen START-Befehl. Programmpfad und Parameter werden niemals vom Server übertragen.",
AutoSize = true,
MaximumSize = new Size(820, 0),
Margin = new Padding(0, 0, 0, 10)
};
root.Controls.Add(hint, 0, 4);
root.Controls.Add(BuildMainButtons(), 0, 5);
return root;
}
private Control BuildGlobalOptionsGroup()
{
var group = CreateGroup("Allgemein");
var options = new FlowLayoutPanel
{
Dock = DockStyle.Top,
FlowDirection = FlowDirection.TopDown,
WrapContents = false,
AutoSize = true
};
options.Controls.Add(enabledCheckBox);
options.Controls.Add(loggingCheckBox);
group.Controls.Add(options);
return group;
}
private Control BuildConnectionProgramGroup()
{
var group = CreateGroup("Automatischer Start bei RDP-Verbindung");
var layout = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 1,
RowCount = 4
};
layout.Controls.Add(new Label
{
Text = "Optionales lokales Programm/Script, das direkt nach erfolgreichem Aufbau der klassischen MSTSC-Verbindung gestartet wird.",
AutoSize = true,
MaximumSize = new Size(780, 0),
Margin = new Padding(0, 0, 0, 8)
}, 0, 0);
layout.Controls.Add(BuildFileGrid(
scriptPathTextBox,
argumentsTextBox,
workingDirectoryTextBox,
() => BrowseProgram(scriptPathTextBox, workingDirectoryTextBox, useRemoteVddsPreset: true),
() => BrowseWorkingDirectory(workingDirectoryTextBox)), 0, 1);
var options = new FlowLayoutPanel
{
Dock = DockStyle.Top,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
AutoSize = true,
Margin = new Padding(0, 4, 0, 6)
};
options.Controls.Add(startOnConnectCheckBox);
options.Controls.Add(stopOnDisconnectCheckBox);
options.Controls.Add(hiddenCheckBox);
options.Controls.Add(preventDuplicatesCheckBox);
layout.Controls.Add(options, 0, 2);
var testButtons = new FlowLayoutPanel
{
Dock = DockStyle.Top,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
AutoSize = true,
Margin = new Padding(0)
};
var testButton = new Button { Text = "Verbindungs-Programm testen", AutoSize = true };
testButton.Click += (_, _) => StartConnectionTest();
stopConnectionTestButton.Click += (_, _) => StopConnectionTest(silent: false);
testButtons.Controls.Add(testButton);
testButtons.Controls.Add(stopConnectionTestButton);
layout.Controls.Add(testButtons, 0, 3);
group.Controls.Add(layout);
return group;
}
private Control BuildTriggerProgramGroup()
{
var group = CreateGroup("Programmstart durch Terminalserver");
var layout = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 1,
RowCount = 4
};
layout.Controls.Add(new Label
{
Text = "Optionales lokales Programm/Script mit eigenen Parametern. Es wird nur gestartet, wenn PlandentRdpClientTrigger.exe innerhalb derselben RDP-Sitzung auf dem Terminalserver ausgeführt wird.",
AutoSize = true,
MaximumSize = new Size(780, 0),
Margin = new Padding(0, 0, 0, 8)
}, 0, 0);
layout.Controls.Add(BuildFileGrid(
triggerProgramPathTextBox,
triggerArgumentsTextBox,
triggerWorkingDirectoryTextBox,
() => BrowseProgram(triggerProgramPathTextBox, triggerWorkingDirectoryTextBox, useRemoteVddsPreset: false),
() => BrowseWorkingDirectory(triggerWorkingDirectoryTextBox)), 0, 1);
var options = new FlowLayoutPanel
{
Dock = DockStyle.Top,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
AutoSize = true,
Margin = new Padding(0, 4, 0, 6)
};
options.Controls.Add(triggerEnabledCheckBox);
options.Controls.Add(triggerHiddenCheckBox);
options.Controls.Add(triggerPreventDuplicatesCheckBox);
layout.Controls.Add(options, 0, 2);
var testButtons = new FlowLayoutPanel
{
Dock = DockStyle.Top,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
AutoSize = true,
Margin = new Padding(0)
};
var testButton = new Button { Text = "Trigger-Programm testen", AutoSize = true };
testButton.Click += (_, _) => StartTriggerTest();
stopTriggerTestButton.Click += (_, _) => StopTriggerTest(silent: false);
testButtons.Controls.Add(testButton);
testButtons.Controls.Add(stopTriggerTestButton);
layout.Controls.Add(testButtons, 0, 3);
group.Controls.Add(layout);
return group;
}
private static GroupBox CreateGroup(string title)
{
return new GroupBox
{
Text = title,
Dock = DockStyle.Top,
AutoSize = true,
Padding = new Padding(12),
Margin = new Padding(0, 0, 0, 12)
};
}
private static Control BuildFileGrid(
TextBox pathTextBox,
TextBox argumentsTextBox,
TextBox workingDirectoryTextBox,
Action browseProgram,
Action browseWorkingDirectory)
{
var grid = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 3,
RowCount = 3,
Margin = new Padding(0)
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.Controls.Add(CreateFieldLabel("Programm/Script:"), 0, 0);
grid.Controls.Add(pathTextBox, 1, 0);
var browseProgramButton = new Button { Text = "…", Width = 42, Height = pathTextBox.PreferredHeight + 4 };
browseProgramButton.Click += (_, _) => browseProgram();
grid.Controls.Add(browseProgramButton, 2, 0);
grid.Controls.Add(CreateFieldLabel("Argumente:"), 0, 1);
grid.Controls.Add(argumentsTextBox, 1, 1);
grid.SetColumnSpan(argumentsTextBox, 2);
grid.Controls.Add(CreateFieldLabel("Arbeitsverzeichnis:"), 0, 2);
grid.Controls.Add(workingDirectoryTextBox, 1, 2);
var browseWorkingButton = new Button { Text = "…", Width = 42, Height = workingDirectoryTextBox.PreferredHeight + 4 };
browseWorkingButton.Click += (_, _) => browseWorkingDirectory();
grid.Controls.Add(browseWorkingButton, 2, 2);
return grid;
}
private static Label CreateFieldLabel(string text)
{
return new Label
{
Text = text,
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 6, 12, 6)
};
}
private Control BuildStatusPanel()
{
var panel = new Panel
{
Dock = DockStyle.Top,
Height = 70,
BorderStyle = BorderStyle.FixedSingle,
Margin = new Padding(0, 0, 0, 10)
};
panel.Controls.Add(statusLabel);
return panel;
}
private Control BuildMainButtons()
{
var buttons = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
Margin = new Padding(0)
};
var saveButton = new Button { Text = "Konfiguration speichern", AutoSize = true };
saveButton.Click += (_, _) => SaveConfiguration(showConfirmation: true);
var installButton = new Button { Text = "Plugin installieren", AutoSize = true };
installButton.Click += (_, _) => InstallPlugin();
var removeButton = new Button { Text = "Plugin entfernen", AutoSize = true };
removeButton.Click += (_, _) => RemovePlugin();
var logButton = new Button { Text = "Log öffnen", AutoSize = true };
logButton.Click += (_, _) => OpenLog();
buttons.Controls.Add(saveButton);
buttons.Controls.Add(installButton);
buttons.Controls.Add(removeButton);
buttons.Controls.Add(logButton);
return buttons;
}
private void BrowseProgram(TextBox pathTextBox, TextBox workingDirectoryTextBox, bool useRemoteVddsPreset)
{
using var dialog = new OpenFileDialog
{
Title = "Programm oder Script auswählen",
Filter = "Unterstützte Dateien (*.exe;*.bat;*.cmd;*.ps1)|*.exe;*.bat;*.cmd;*.ps1|Alle Dateien (*.*)|*.*",
CheckFileExists = true
};
if (!string.IsNullOrWhiteSpace(pathTextBox.Text))
{
string expanded = Environment.ExpandEnvironmentVariables(pathTextBox.Text);
if (File.Exists(expanded))
{
dialog.InitialDirectory = Path.GetDirectoryName(expanded);
dialog.FileName = Path.GetFileName(expanded);
}
}
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
pathTextBox.Text = dialog.FileName;
if (string.IsNullOrWhiteSpace(workingDirectoryTextBox.Text))
{
workingDirectoryTextBox.Text = Path.GetDirectoryName(dialog.FileName) ?? string.Empty;
}
if (useRemoteVddsPreset &&
string.IsNullOrWhiteSpace(argumentsTextBox.Text) &&
Path.GetFileName(dialog.FileName).Contains("RemoteVDDS_Receiver_TS", StringComparison.OrdinalIgnoreCase))
{
argumentsTextBox.Text = "MIN";
}
}
private void BrowseWorkingDirectory(TextBox target)
{
using var dialog = new FolderBrowserDialog
{
Description = "Arbeitsverzeichnis auswählen",
UseDescriptionForTitle = true,
ShowNewFolderButton = true
};
string expanded = Environment.ExpandEnvironmentVariables(target.Text);
if (Directory.Exists(expanded))
{
dialog.SelectedPath = expanded;
}
if (dialog.ShowDialog(this) == DialogResult.OK)
{
target.Text = dialog.SelectedPath;
}
}
private HookConfig ReadForm()
{
return new HookConfig
{
Enabled = enabledCheckBox.Checked,
EnableLogging = loggingCheckBox.Checked,
ScriptPath = scriptPathTextBox.Text.Trim(),
Arguments = argumentsTextBox.Text.Trim(),
WorkingDirectory = workingDirectoryTextBox.Text.Trim(),
StartOnConnect = startOnConnectCheckBox.Checked,
StopOnDisconnect = stopOnDisconnectCheckBox.Checked,
Hidden = hiddenCheckBox.Checked,
PreventDuplicates = preventDuplicatesCheckBox.Checked,
TriggerEnabled = triggerEnabledCheckBox.Checked,
TriggerProgramPath = triggerProgramPathTextBox.Text.Trim(),
TriggerArguments = triggerArgumentsTextBox.Text.Trim(),
TriggerWorkingDirectory = triggerWorkingDirectoryTextBox.Text.Trim(),
TriggerHidden = triggerHiddenCheckBox.Checked,
TriggerPreventDuplicates = triggerPreventDuplicatesCheckBox.Checked
};
}
private void LoadConfiguration()
{
HookConfig config = RegistryConfig.Load();
enabledCheckBox.Checked = config.Enabled;
loggingCheckBox.Checked = config.EnableLogging;
scriptPathTextBox.Text = config.ScriptPath;
argumentsTextBox.Text = config.Arguments;
workingDirectoryTextBox.Text = config.WorkingDirectory;
startOnConnectCheckBox.Checked = config.StartOnConnect;
stopOnDisconnectCheckBox.Checked = config.StopOnDisconnect;
hiddenCheckBox.Checked = config.Hidden;
preventDuplicatesCheckBox.Checked = config.PreventDuplicates;
triggerEnabledCheckBox.Checked = config.TriggerEnabled;
triggerProgramPathTextBox.Text = config.TriggerProgramPath;
triggerArgumentsTextBox.Text = config.TriggerArguments;
triggerWorkingDirectoryTextBox.Text = config.TriggerWorkingDirectory;
triggerHiddenCheckBox.Checked = config.TriggerHidden;
triggerPreventDuplicatesCheckBox.Checked = config.TriggerPreventDuplicates;
RefreshStatus();
}
private bool SaveConfiguration(bool showConfirmation)
{
try
{
HookConfig config = ReadForm();
ValidateConfiguredTargets(config, installationValidation: false);
RegistryConfig.Save(config);
if (showConfirmation)
{
MessageBox.Show(this, "Konfiguration wurde gespeichert.", "MSTSC Script Hook", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
RefreshStatus();
return true;
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Konfiguration konnte nicht gespeichert werden", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
private void InstallPlugin()
{
try
{
HookConfig config = ReadForm();
ValidateConfiguredTargets(config, installationValidation: true);
RegistryConfig.Save(config);
PluginInstaller.Install();
RefreshStatus();
MessageBox.Show(
this,
"Plugin wurde für den aktuellen Benutzer installiert und die Konfiguration gespeichert.\n\n" +
"Bereits geöffnete mstsc.exe-Fenster vollständig schließen und anschließend neu starten, damit die neue DLL geladen wird.",
"Installation abgeschlossen",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void RemovePlugin()
{
UninstallResult result = PluginInstaller.Uninstall();
RefreshStatus();
MessageBox.Show(
this,
result.Message,
"Plugin entfernen",
MessageBoxButtons.OK,
result.FullyRemoved ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
}
private void StartConnectionTest()
{
try
{
StopConnectionTest(silent: true);
HookConfig config = ReadForm();
ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true);
connectionTestProcess = ScriptTestLauncher.StartConnectionProgram(config);
stopConnectionTestButton.Enabled = true;
statusLabel.Text = $"Verbindungs-Test gestartet: PID {connectionTestProcess.Id}. Der Test läuft unabhängig von MSTSC.";
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void StartTriggerTest()
{
try
{
StopTriggerTest(silent: true);
HookConfig config = ReadForm();
ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: true);
triggerTestProcess = ScriptTestLauncher.StartTriggerProgram(config);
stopTriggerTestButton.Enabled = true;
statusLabel.Text = $"Trigger-Programm-Test gestartet: PID {triggerTestProcess.Id}. Der Test läuft ohne Terminalserver-Trigger.";
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Trigger-Programm-Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void StopConnectionTest(bool silent)
{
StopTestProcess(ref connectionTestProcess, stopConnectionTestButton, "Verbindungs-Test", silent);
}
private void StopTriggerTest(bool silent)
{
StopTestProcess(ref triggerTestProcess, stopTriggerTestButton, "Trigger-Programm-Test", silent);
}
private void StopTestProcess(ref Process? process, Button button, string description, bool silent)
{
if (process is null)
{
button.Enabled = false;
return;
}
try
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
process.WaitForExit(3000);
}
if (!silent)
{
statusLabel.Text = description + " wurde beendet.";
}
}
catch (Exception ex)
{
if (!silent)
{
MessageBox.Show(this, ex.Message, description + " konnte nicht beendet werden", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
finally
{
process?.Dispose();
process = null;
button.Enabled = false;
}
}
private void DisposeAllTestProcesses()
{
connectionTestProcess?.Dispose();
triggerTestProcess?.Dispose();
connectionTestProcess = null;
triggerTestProcess = null;
}
private void OpenLog()
{
string logPath = Path.Combine(PluginInstaller.InstallDirectory, "plugin.log");
if (!File.Exists(logPath))
{
MessageBox.Show(this, "Es existiert noch kein Plugin-Log.", "MSTSC Script Hook", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
Process.Start(new ProcessStartInfo
{
FileName = logPath,
UseShellExecute = true
});
}
private void RefreshStatus()
{
bool installed = PluginInstaller.IsInstalled(out string pluginStatus);
string connectStatus = startOnConnectCheckBox.Checked
? (string.IsNullOrWhiteSpace(scriptPathTextBox.Text) ? " RDP-Start: nicht konfiguriert." : " RDP-Start: konfiguriert.")
: " RDP-Start: aus.";
string triggerStatus = triggerEnabledCheckBox.Checked
? (string.IsNullOrWhiteSpace(triggerProgramPathTextBox.Text) ? " Server-Trigger: nicht konfiguriert." : " Server-Trigger: konfiguriert.")
: " Server-Trigger: aus.";
statusLabel.Text = (installed ? "✓ " : "○ ") + pluginStatus + connectStatus + triggerStatus;
}
private static void ValidateConfiguredTargets(HookConfig config, bool installationValidation)
{
// Bereits ausgefüllte Pfade immer prüfen, damit Tippfehler nicht gespeichert werden.
ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: false);
ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: false);
if (!installationValidation || !config.Enabled)
{
return;
}
if (config.StartOnConnect)
{
ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true);
}
if (config.TriggerEnabled)
{
ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: true);
}
}
private static void ValidateLaunchTarget(string configuredPath, string configuredWorkingDirectory, string displayName, bool required)
{
string path = Environment.ExpandEnvironmentVariables(configuredPath ?? string.Empty);
if (string.IsNullOrWhiteSpace(path))
{
if (required)
{
throw new InvalidOperationException($"Bitte für „{displayName}“ ein Programm oder Script auswählen.");
}
return;
}
if (!File.Exists(path))
{
throw new FileNotFoundException($"{displayName} wurde nicht gefunden.", path);
}
string extension = Path.GetExtension(path).ToLowerInvariant();
if (extension is not (".bat" or ".cmd" or ".ps1" or ".exe"))
{
throw new NotSupportedException($"{displayName}: Unterstützt werden .bat, .cmd, .ps1 und .exe.");
}
if (!string.IsNullOrWhiteSpace(configuredWorkingDirectory))
{
string workDir = Environment.ExpandEnvironmentVariables(configuredWorkingDirectory);
if (!Directory.Exists(workDir))
{
throw new DirectoryNotFoundException($"Arbeitsverzeichnis für „{displayName}“ wurde nicht gefunden: {workDir}");
}
}
}
}
+227
View File
@@ -0,0 +1,227 @@
#include "PluginInstaller.h"
#include "Config.h"
#include "resource.h"
#include <windows.h>
#include <shlobj.h>
#include <vector>
namespace
{
constexpr wchar_t kDllFileName[] = L"PlandentMstscScriptHook.dll";
std::wstring JoinPath(const std::wstring& left, const std::wstring& right)
{
if (left.empty())
{
return right;
}
if (left.back() == L'\\' || left.back() == L'/')
{
return left + right;
}
return left + L"\\" + right;
}
std::wstring ModuleDirectory()
{
std::vector<wchar_t> buffer(32768, L'\0');
DWORD len = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (len == 0 || len >= buffer.size())
{
return {};
}
std::wstring path(buffer.data(), len);
size_t slash = path.find_last_of(L"\\/");
return slash == std::wstring::npos ? std::wstring{} : path.substr(0, slash);
}
bool EnsureDirectory(const std::wstring& path, std::wstring& error)
{
if (CreateDirectoryW(path.c_str(), nullptr) || GetLastError() == ERROR_ALREADY_EXISTS)
{
return true;
}
error = L"Verzeichnis konnte nicht erstellt werden: " + path + L" (Fehler " + std::to_wstring(GetLastError()) + L")";
return false;
}
bool ExtractResourceToFile(const std::wstring& destination, std::wstring& error)
{
HMODULE module = GetModuleHandleW(nullptr);
HRSRC resource = FindResourceW(module, MAKEINTRESOURCEW(IDR_PLUGIN_DLL), RT_RCDATA);
if (resource == nullptr)
{
// Fallback für Entwicklung: Plugin-DLL direkt neben der EXE akzeptieren.
std::wstring adjacent = JoinPath(ModuleDirectory(), kDllFileName);
DWORD attr = GetFileAttributesW(adjacent.c_str());
if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) == 0)
{
if (CopyFileW(adjacent.c_str(), destination.c_str(), FALSE))
{
return true;
}
error = L"Plugin-DLL konnte nicht kopiert werden (Fehler " + std::to_wstring(GetLastError()) + L").";
return false;
}
error = L"Die native Plugin-DLL ist weder eingebettet noch neben der Konfigurations-EXE vorhanden.";
return false;
}
HGLOBAL loaded = LoadResource(module, resource);
DWORD size = SizeofResource(module, resource);
const void* data = loaded != nullptr ? LockResource(loaded) : nullptr;
if (data == nullptr || size == 0)
{
error = L"Eingebettete Plugin-DLL konnte nicht gelesen werden.";
return false;
}
HANDLE file = CreateFileW(
destination.c_str(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (file == INVALID_HANDLE_VALUE)
{
error = L"Plugin-DLL konnte nicht geschrieben werden: " + destination + L" (Fehler " + std::to_wstring(GetLastError()) + L")";
return false;
}
DWORD written = 0;
BOOL ok = WriteFile(file, data, size, &written, nullptr);
DWORD writeError = ok ? ERROR_SUCCESS : GetLastError();
CloseHandle(file);
if (!ok || written != size)
{
DeleteFileW(destination.c_str());
error = L"Plugin-DLL konnte nicht vollständig geschrieben werden (Fehler " + std::to_wstring(writeError) + L").";
return false;
}
return true;
}
}
namespace installer
{
std::wstring InstallDirectory()
{
PWSTR localAppData = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &localAppData);
if (FAILED(hr) || localAppData == nullptr)
{
return {};
}
std::wstring result = std::wstring(localAppData) + L"\\Plandent\\MstscScriptHook";
CoTaskMemFree(localAppData);
return result;
}
std::wstring InstalledDllPath()
{
return JoinPath(InstallDirectory(), kDllFileName);
}
std::wstring LogPath()
{
return JoinPath(InstallDirectory(), L"plugin.log");
}
bool Install(std::wstring& error)
{
PWSTR localAppData = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &localAppData);
if (FAILED(hr) || localAppData == nullptr)
{
error = L"%LOCALAPPDATA% konnte nicht ermittelt werden.";
return false;
}
std::wstring base(localAppData);
CoTaskMemFree(localAppData);
std::wstring plandent = JoinPath(base, L"Plandent");
std::wstring targetDir = JoinPath(plandent, L"MstscScriptHook");
if (!EnsureDirectory(plandent, error) || !EnsureDirectory(targetDir, error))
{
return false;
}
const std::wstring targetDll = JoinPath(targetDir, kDllFileName);
if (!ExtractResourceToFile(targetDll, error))
{
return false;
}
if (!config::RegisterPlugin(targetDll, error))
{
return false;
}
return true;
}
bool Uninstall(std::wstring& message, bool& fullyRemoved)
{
fullyRemoved = false;
std::wstring error;
if (!config::UnregisterPlugin(error))
{
message = error;
return false;
}
std::wstring dll = InstalledDllPath();
DWORD attr = GetFileAttributesW(dll.c_str());
if (attr == INVALID_FILE_ATTRIBUTES)
{
fullyRemoved = true;
message = L"Plugin-Registrierung wurde entfernt.";
return true;
}
if (DeleteFileW(dll.c_str()))
{
RemoveDirectoryW(InstallDirectory().c_str()); // Darf fehlschlagen, z. B. wenn plugin.log existiert.
fullyRemoved = true;
message = L"Plugin-Registrierung und DLL wurden entfernt.";
return true;
}
DWORD lastError = GetLastError();
if (lastError == ERROR_SHARING_VIOLATION || lastError == ERROR_ACCESS_DENIED)
{
message = L"Die Registrierung wurde entfernt. Die DLL wird wahrscheinlich noch von mstsc.exe verwendet. "
L"Alle Remote-Desktop-Clients schließen und anschließend erneut auf „Plugin entfernen“ klicken.";
return true;
}
message = L"Registrierung wurde entfernt, die DLL konnte aber nicht gelöscht werden (Fehler " + std::to_wstring(lastError) + L").";
return true;
}
Status GetStatus()
{
Status status;
std::wstring registered = config::GetRegisteredPluginPath();
if (registered.empty())
{
status.text = L"Plugin ist nicht registriert.";
return status;
}
std::wstring expanded = config::ExpandEnvironment(registered);
DWORD attr = GetFileAttributesW(expanded.c_str());
if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
status.text = L"Plugin ist registriert, DLL fehlt jedoch: " + expanded;
return status;
}
status.installed = true;
status.text = L"Plugin ist registriert: " + expanded;
return status;
}
}
-113
View File
@@ -1,113 +0,0 @@
using System.Reflection;
namespace Plandent.MstscScriptHook.Configurator;
internal static class PluginInstaller
{
private const string ResourceName = "PlandentMstscScriptHook.Native.PlandentMstscScriptHook.dll";
public const string DllFileName = "PlandentMstscScriptHook.dll";
public static string InstallDirectory => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Plandent",
"MstscScriptHook");
public static string InstalledDllPath => Path.Combine(InstallDirectory, DllFileName);
public static void Install()
{
Directory.CreateDirectory(InstallDirectory);
ExtractPluginBinary(InstalledDllPath);
RegistryConfig.RegisterPlugin(InstalledDllPath);
}
public static UninstallResult Uninstall()
{
RegistryConfig.UnregisterPlugin();
if (!File.Exists(InstalledDllPath))
{
return new UninstallResult(true, "Plugin-Registrierung entfernt.");
}
try
{
File.Delete(InstalledDllPath);
TryDeleteEmptyInstallDirectory();
return new UninstallResult(true, "Plugin-Registrierung und DLL wurden entfernt.");
}
catch (IOException)
{
return new UninstallResult(
false,
"Die Registrierung wurde entfernt. Die DLL wird noch von mstsc.exe verwendet. " +
"Alle Remote-Desktop-Clients schließen und die Datei anschließend löschen bzw. erneut auf „Plugin entfernen“ klicken.");
}
catch (UnauthorizedAccessException ex)
{
return new UninstallResult(false, "Registrierung entfernt, DLL konnte aber nicht gelöscht werden: " + ex.Message);
}
}
public static bool IsInstalled(out string status)
{
string? registered = RegistryConfig.GetRegisteredPluginPath();
if (string.IsNullOrWhiteSpace(registered))
{
status = "Plugin ist nicht registriert.";
return false;
}
string expanded = Environment.ExpandEnvironmentVariables(registered);
if (!File.Exists(expanded))
{
status = $"Plugin ist registriert, DLL fehlt jedoch: {expanded}";
return false;
}
status = $"Plugin ist registriert: {expanded}";
return true;
}
private static void ExtractPluginBinary(string destination)
{
Assembly assembly = Assembly.GetExecutingAssembly();
using Stream? resource = assembly.GetManifestResourceStream(ResourceName);
if (resource is not null)
{
using FileStream output = new(destination, FileMode.Create, FileAccess.Write, FileShare.None);
resource.CopyTo(output);
return;
}
// Praktischer Fallback für Debugging/Entwicklung: DLL direkt neben der EXE akzeptieren.
string adjacent = Path.Combine(AppContext.BaseDirectory, DllFileName);
if (File.Exists(adjacent))
{
File.Copy(adjacent, destination, overwrite: true);
return;
}
throw new FileNotFoundException(
"Die native Plugin-DLL ist weder als Resource eingebettet noch neben der Konfigurations-EXE vorhanden. " +
"Bitte zuerst build-release.ps1 ausführen.",
DllFileName);
}
private static void TryDeleteEmptyInstallDirectory()
{
try
{
if (Directory.Exists(InstallDirectory) && !Directory.EnumerateFileSystemEntries(InstallDirectory).Any())
{
Directory.Delete(InstallDirectory);
}
}
catch
{
// Unkritisch: ein leeres Verzeichnis darf bestehen bleiben.
}
}
}
internal sealed record UninstallResult(bool FullyRemoved, string Message);
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <string>
namespace installer
{
struct Status
{
bool installed = false;
std::wstring text;
};
std::wstring InstallDirectory();
std::wstring InstalledDllPath();
std::wstring LogPath();
bool Install(std::wstring& error);
bool Uninstall(std::wstring& message, bool& fullyRemoved);
Status GetStatus();
}
-13
View File
@@ -1,13 +0,0 @@
using System.Windows.Forms;
namespace Plandent.MstscScriptHook.Configurator;
internal static class Program
{
[STAThread]
private static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
+212
View File
@@ -0,0 +1,212 @@
#include "Config.h"
#include <windows.h>
#include <vector>
namespace
{
DWORD ReadDword(HKEY key, const wchar_t* name, DWORD defaultValue)
{
DWORD value = defaultValue;
DWORD size = sizeof(value);
DWORD type = 0;
if (RegQueryValueExW(key, name, nullptr, &type, reinterpret_cast<BYTE*>(&value), &size) != ERROR_SUCCESS ||
type != REG_DWORD)
{
return defaultValue;
}
return value;
}
std::wstring ReadStringRaw(HKEY key, const wchar_t* name)
{
DWORD type = 0;
DWORD size = 0;
if (RegQueryValueExW(key, name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS ||
(type != REG_SZ && type != REG_EXPAND_SZ) || size == 0)
{
return {};
}
std::vector<wchar_t> buffer((size / sizeof(wchar_t)) + 1, L'\0');
if (RegQueryValueExW(key, name, nullptr, &type, reinterpret_cast<BYTE*>(buffer.data()), &size) != ERROR_SUCCESS)
{
return {};
}
return std::wstring(buffer.data());
}
bool WriteDword(HKEY key, const wchar_t* name, bool value, std::wstring& error)
{
DWORD data = value ? 1u : 0u;
LONG result = RegSetValueExW(key, name, 0, REG_DWORD, reinterpret_cast<const BYTE*>(&data), sizeof(data));
if (result != ERROR_SUCCESS)
{
error = L"Registry-Wert konnte nicht geschrieben werden: " + std::wstring(name) + L" (Fehler " + std::to_wstring(result) + L")";
return false;
}
return true;
}
bool WriteString(HKEY key, const wchar_t* name, const std::wstring& value, DWORD type, std::wstring& error)
{
const DWORD bytes = static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t));
LONG result = RegSetValueExW(
key,
name,
0,
type,
reinterpret_cast<const BYTE*>(value.c_str()),
bytes);
if (result != ERROR_SUCCESS)
{
error = L"Registry-Wert konnte nicht geschrieben werden: " + std::wstring(name) + L" (Fehler " + std::to_wstring(result) + L")";
return false;
}
return true;
}
}
namespace config
{
std::wstring ExpandEnvironment(const std::wstring& value)
{
if (value.empty())
{
return value;
}
DWORD needed = ExpandEnvironmentStringsW(value.c_str(), nullptr, 0);
if (needed == 0)
{
return value;
}
std::vector<wchar_t> buffer(needed, L'\0');
if (ExpandEnvironmentStringsW(value.c_str(), buffer.data(), needed) == 0)
{
return value;
}
return std::wstring(buffer.data());
}
HookConfig Load()
{
HookConfig value;
HKEY key = nullptr;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kConfigKey, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS)
{
return value;
}
value.enabled = ReadDword(key, L"Enabled", 1) != 0;
value.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0;
value.scriptPath = ReadStringRaw(key, L"ScriptPath");
value.arguments = ReadStringRaw(key, L"Arguments");
value.workingDirectory = ReadStringRaw(key, L"WorkingDirectory");
value.startOnConnect = ReadDword(key, L"StartOnConnect", 1) != 0;
value.stopOnDisconnect = ReadDword(key, L"StopOnDisconnect", 0) != 0;
value.hidden = ReadDword(key, L"Hidden", 1) != 0;
value.preventDuplicates = ReadDword(key, L"PreventDuplicates", 1) != 0;
value.triggerEnabled = ReadDword(key, L"TriggerEnabled", 0) != 0;
value.triggerProgramPath = ReadStringRaw(key, L"TriggerProgramPath");
value.triggerArguments = ReadStringRaw(key, L"TriggerArguments");
value.triggerWorkingDirectory = ReadStringRaw(key, L"TriggerWorkingDirectory");
value.triggerHidden = ReadDword(key, L"TriggerHidden", 1) != 0;
value.triggerPreventDuplicates = ReadDword(key, L"TriggerPreventDuplicates", 1) != 0;
RegCloseKey(key);
return value;
}
bool Save(const HookConfig& value, std::wstring& error)
{
HKEY key = nullptr;
DWORD disposition = 0;
LONG result = RegCreateKeyExW(
HKEY_CURRENT_USER,
kConfigKey,
0,
nullptr,
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,
nullptr,
&key,
&disposition);
if (result != ERROR_SUCCESS)
{
error = L"Konfigurationsschlüssel konnte nicht erstellt werden (Fehler " + std::to_wstring(result) + L").";
return false;
}
bool ok =
WriteDword(key, L"Enabled", value.enabled, error) &&
WriteDword(key, L"EnableLogging", value.enableLogging, error) &&
WriteString(key, L"ScriptPath", value.scriptPath, REG_EXPAND_SZ, error) &&
WriteString(key, L"Arguments", value.arguments, REG_SZ, error) &&
WriteString(key, L"WorkingDirectory", value.workingDirectory, REG_EXPAND_SZ, error) &&
WriteDword(key, L"StartOnConnect", value.startOnConnect, error) &&
WriteDword(key, L"StopOnDisconnect", value.stopOnDisconnect, error) &&
WriteDword(key, L"Hidden", value.hidden, error) &&
WriteDword(key, L"PreventDuplicates", value.preventDuplicates, error) &&
WriteDword(key, L"TriggerEnabled", value.triggerEnabled, error) &&
WriteString(key, L"TriggerProgramPath", value.triggerProgramPath, REG_EXPAND_SZ, error) &&
WriteString(key, L"TriggerArguments", value.triggerArguments, REG_SZ, error) &&
WriteString(key, L"TriggerWorkingDirectory", value.triggerWorkingDirectory, REG_EXPAND_SZ, error) &&
WriteDword(key, L"TriggerHidden", value.triggerHidden, error) &&
WriteDword(key, L"TriggerPreventDuplicates", value.triggerPreventDuplicates, error);
RegCloseKey(key);
return ok;
}
std::wstring GetRegisteredPluginPath()
{
HKEY key = nullptr;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kAddInKey, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS)
{
return {};
}
std::wstring value = ReadStringRaw(key, L"Name");
RegCloseKey(key);
return value;
}
bool RegisterPlugin(const std::wstring& dllPath, std::wstring& error)
{
HKEY key = nullptr;
DWORD disposition = 0;
LONG result = RegCreateKeyExW(
HKEY_CURRENT_USER,
kAddInKey,
0,
nullptr,
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,
nullptr,
&key,
&disposition);
if (result != ERROR_SUCCESS)
{
error = L"MSTSC AddIn-Schlüssel konnte nicht erstellt werden (Fehler " + std::to_wstring(result) + L").";
return false;
}
bool ok = WriteString(key, L"Name", dllPath, REG_SZ, error);
RegCloseKey(key);
return ok;
}
bool UnregisterPlugin(std::wstring& error)
{
LONG result = RegDeleteTreeW(HKEY_CURRENT_USER, kAddInKey);
if (result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND || result == ERROR_PATH_NOT_FOUND)
{
return true;
}
error = L"MSTSC-Plugin-Registrierung konnte nicht entfernt werden (Fehler " + std::to_wstring(result) + L").";
return false;
}
}
-97
View File
@@ -1,97 +0,0 @@
using Microsoft.Win32;
namespace Plandent.MstscScriptHook.Configurator;
internal static class RegistryConfig
{
public const string ConfigKeyPath = @"Software\Plandent\MstscScriptHook";
public const string AddInKeyPath = @"Software\Microsoft\Terminal Server Client\Default\AddIns\PlandentMstscScriptHook";
public static HookConfig Load()
{
var config = new HookConfig();
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(ConfigKeyPath, writable: false);
if (key is null)
{
return config;
}
config.Enabled = ReadBool(key, "Enabled", true);
config.EnableLogging = ReadBool(key, "EnableLogging", true);
config.ScriptPath = ReadString(key, "ScriptPath");
config.Arguments = ReadString(key, "Arguments");
config.WorkingDirectory = ReadString(key, "WorkingDirectory");
config.StartOnConnect = ReadBool(key, "StartOnConnect", true);
config.StopOnDisconnect = ReadBool(key, "StopOnDisconnect", false);
config.Hidden = ReadBool(key, "Hidden", true);
config.PreventDuplicates = ReadBool(key, "PreventDuplicates", true);
config.TriggerEnabled = ReadBool(key, "TriggerEnabled", false);
config.TriggerProgramPath = ReadString(key, "TriggerProgramPath");
config.TriggerArguments = ReadString(key, "TriggerArguments");
config.TriggerWorkingDirectory = ReadString(key, "TriggerWorkingDirectory");
config.TriggerHidden = ReadBool(key, "TriggerHidden", true);
config.TriggerPreventDuplicates = ReadBool(key, "TriggerPreventDuplicates", true);
return config;
}
public static void Save(HookConfig config)
{
using RegistryKey key = Registry.CurrentUser.CreateSubKey(ConfigKeyPath, writable: true)
?? throw new InvalidOperationException("Registry-Schlüssel konnte nicht erstellt werden.");
key.SetValue("Enabled", config.Enabled ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("EnableLogging", config.EnableLogging ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("ScriptPath", config.ScriptPath ?? string.Empty, RegistryValueKind.ExpandString);
key.SetValue("Arguments", config.Arguments ?? string.Empty, RegistryValueKind.String);
key.SetValue("WorkingDirectory", config.WorkingDirectory ?? string.Empty, RegistryValueKind.ExpandString);
key.SetValue("StartOnConnect", config.StartOnConnect ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("StopOnDisconnect", config.StopOnDisconnect ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("Hidden", config.Hidden ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("PreventDuplicates", config.PreventDuplicates ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("TriggerEnabled", config.TriggerEnabled ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("TriggerProgramPath", config.TriggerProgramPath ?? string.Empty, RegistryValueKind.ExpandString);
key.SetValue("TriggerArguments", config.TriggerArguments ?? string.Empty, RegistryValueKind.String);
key.SetValue("TriggerWorkingDirectory", config.TriggerWorkingDirectory ?? string.Empty, RegistryValueKind.ExpandString);
key.SetValue("TriggerHidden", config.TriggerHidden ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("TriggerPreventDuplicates", config.TriggerPreventDuplicates ? 1 : 0, RegistryValueKind.DWord);
}
public static string? GetRegisteredPluginPath()
{
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(AddInKeyPath, writable: false);
return key?.GetValue("Name", null, RegistryValueOptions.DoNotExpandEnvironmentNames) as string;
}
public static void RegisterPlugin(string dllPath)
{
using RegistryKey key = Registry.CurrentUser.CreateSubKey(AddInKeyPath, writable: true)
?? throw new InvalidOperationException("MSTSC AddIn-Registry-Schlüssel konnte nicht erstellt werden.");
key.SetValue("Name", dllPath, RegistryValueKind.String);
}
public static void UnregisterPlugin()
{
Registry.CurrentUser.DeleteSubKeyTree(AddInKeyPath, throwOnMissingSubKey: false);
}
private static bool ReadBool(RegistryKey key, string name, bool defaultValue)
{
object? value = key.GetValue(name);
return value switch
{
int i => i != 0,
long l => l != 0,
_ => defaultValue
};
}
private static string ReadString(RegistryKey key, string name, string defaultValue = "")
{
return key.GetValue(name, defaultValue, RegistryValueOptions.DoNotExpandEnvironmentNames) as string
?? defaultValue;
}
}
-169
View File
@@ -1,169 +0,0 @@
using System.Diagnostics;
namespace Plandent.MstscScriptHook.Configurator;
internal static class ScriptTestLauncher
{
public static Process StartConnectionProgram(HookConfig config) => Start(
config.ScriptPath,
config.Arguments,
config.WorkingDirectory,
config.Hidden);
public static Process StartTriggerProgram(HookConfig config) => Start(
config.TriggerProgramPath,
config.TriggerArguments,
config.TriggerWorkingDirectory,
config.TriggerHidden);
private static Process Start(string configuredPath, string configuredArguments, string configuredWorkingDirectory, bool hidden)
{
string programPath = Environment.ExpandEnvironmentVariables(configuredPath ?? string.Empty);
if (string.IsNullOrWhiteSpace(programPath) || !File.Exists(programPath))
{
throw new FileNotFoundException("Das ausgewählte Programm/Script wurde nicht gefunden.", programPath);
}
string extension = Path.GetExtension(programPath).ToLowerInvariant();
string workingDirectory = string.IsNullOrWhiteSpace(configuredWorkingDirectory)
? Path.GetDirectoryName(programPath) ?? Environment.CurrentDirectory
: Environment.ExpandEnvironmentVariables(configuredWorkingDirectory);
var startInfo = new ProcessStartInfo
{
UseShellExecute = false,
CreateNoWindow = hidden,
WindowStyle = hidden ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal,
WorkingDirectory = workingDirectory
};
switch (extension)
{
case ".bat":
case ".cmd":
startInfo.FileName = Environment.GetEnvironmentVariable("ComSpec")
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
startInfo.ArgumentList.Add("/D");
startInfo.ArgumentList.Add("/S");
startInfo.ArgumentList.Add("/C");
startInfo.ArgumentList.Add(BuildCmdCommand(programPath, configuredArguments));
break;
case ".ps1":
startInfo.FileName = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.System),
"WindowsPowerShell",
"v1.0",
"powershell.exe");
startInfo.ArgumentList.Add("-NoLogo");
startInfo.ArgumentList.Add("-NoProfile");
startInfo.ArgumentList.Add("-NonInteractive");
startInfo.ArgumentList.Add("-ExecutionPolicy");
startInfo.ArgumentList.Add("Bypass");
startInfo.ArgumentList.Add("-File");
startInfo.ArgumentList.Add(programPath);
AddRawArguments(startInfo, configuredArguments);
break;
case ".exe":
startInfo.FileName = programPath;
AddRawArguments(startInfo, configuredArguments);
break;
default:
throw new NotSupportedException("Unterstützt werden .bat, .cmd, .ps1 und .exe.");
}
return Process.Start(startInfo)
?? throw new InvalidOperationException("Der Testprozess konnte nicht gestartet werden.");
}
private static string BuildCmdCommand(string programPath, string? arguments)
{
string command = QuoteForCmd(programPath);
if (!string.IsNullOrWhiteSpace(arguments))
{
command += " " + arguments;
}
return command;
}
private static string QuoteForCmd(string value)
{
return "\"" + value.Replace("\"", "\\\"") + "\"";
}
private static void AddRawArguments(ProcessStartInfo startInfo, string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return;
}
foreach (string argument in CommandLineSplitter.Split(raw))
{
startInfo.ArgumentList.Add(argument);
}
}
}
internal static class CommandLineSplitter
{
public static IReadOnlyList<string> Split(string commandLine)
{
var result = new List<string>();
var current = new System.Text.StringBuilder();
bool inQuotes = false;
int backslashes = 0;
for (int i = 0; i < commandLine.Length; i++)
{
char ch = commandLine[i];
if (ch == '\\')
{
backslashes++;
continue;
}
if (ch == '"')
{
current.Append('\\', backslashes / 2);
if (backslashes % 2 == 0)
{
inQuotes = !inQuotes;
}
else
{
current.Append('"');
}
backslashes = 0;
continue;
}
current.Append('\\', backslashes);
backslashes = 0;
if (char.IsWhiteSpace(ch) && !inQuotes)
{
if (current.Length > 0)
{
result.Add(current.ToString());
current.Clear();
}
}
else
{
current.Append(ch);
}
}
current.Append('\\', backslashes);
if (current.Length > 0)
{
result.Add(current.ToString());
}
return result;
}
}
+299
View File
@@ -0,0 +1,299 @@
#include "TestLauncher.h"
#include "Config.h"
#include <algorithm>
#include <cwctype>
#include <vector>
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 ExtensionLower(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<wchar_t>(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 EnvironmentValue(const wchar_t* name)
{
DWORD size = GetEnvironmentVariableW(name, nullptr, 0);
if (size == 0)
{
return {};
}
std::vector<wchar_t> buffer(size, L'\0');
if (GetEnvironmentVariableW(name, buffer.data(), size) == 0)
{
return {};
}
return std::wstring(buffer.data());
}
struct LaunchCommand
{
std::wstring application;
std::wstring commandLine;
};
bool BuildCommand(const testlaunch::LaunchSettings& settings, LaunchCommand& command, std::wstring& error)
{
const std::wstring extension = ExtensionLower(settings.path);
if (extension == L".bat" || extension == L".cmd")
{
std::wstring comspec = EnvironmentValue(L"ComSpec");
if (comspec.empty())
{
std::wstring windows = EnvironmentValue(L"SystemRoot");
if (windows.empty())
{
error = L"ComSpec/SystemRoot konnte nicht ermittelt werden.";
return false;
}
comspec = windows + L"\\System32\\cmd.exe";
}
std::wstring inner = QuoteWindowsArgument(settings.path);
if (!settings.arguments.empty())
{
inner += L" " + settings.arguments;
}
command.application = comspec;
command.commandLine = QuoteWindowsArgument(comspec) + L" /D /S /C \"" + inner + L"\"";
return true;
}
if (extension == L".ps1")
{
std::wstring windows = EnvironmentValue(L"SystemRoot");
if (windows.empty())
{
error = L"SystemRoot konnte nicht ermittelt werden.";
return false;
}
std::wstring powershell = windows + L"\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
command.application = powershell;
command.commandLine = QuoteWindowsArgument(powershell) +
L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
QuoteWindowsArgument(settings.path);
if (!settings.arguments.empty())
{
command.commandLine += L" " + settings.arguments;
}
return true;
}
if (extension == L".exe")
{
command.application = settings.path;
command.commandLine = QuoteWindowsArgument(settings.path);
if (!settings.arguments.empty())
{
command.commandLine += L" " + settings.arguments;
}
return true;
}
error = L"Unterstützt werden .bat, .cmd, .ps1 und .exe.";
return false;
}
std::wstring Win32ErrorMessage(DWORD error)
{
wchar_t* text = nullptr;
DWORD size = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
error,
0,
reinterpret_cast<wchar_t*>(&text),
0,
nullptr);
if (size == 0 || text == nullptr)
{
return L"Win32-Fehler " + std::to_wstring(error);
}
std::wstring result(text, size);
LocalFree(text);
while (!result.empty() && (result.back() == L'\r' || result.back() == L'\n' || result.back() == L' '))
{
result.pop_back();
}
return result + L" (" + std::to_wstring(error) + L")";
}
}
namespace testlaunch
{
TestProcess::~TestProcess()
{
Detach();
}
void TestProcess::CleanupExited()
{
if (process_ != nullptr && WaitForSingleObject(process_, 0) != WAIT_TIMEOUT)
{
CloseHandle(process_);
process_ = nullptr;
processId_ = 0;
}
}
bool TestProcess::IsRunning()
{
CleanupExited();
return process_ != nullptr;
}
void TestProcess::Detach()
{
if (process_ != nullptr)
{
CloseHandle(process_);
process_ = nullptr;
processId_ = 0;
}
}
bool TestProcess::Start(const LaunchSettings& rawSettings, std::wstring& error)
{
CleanupExited();
if (process_ != nullptr)
{
error = L"Der Testprozess läuft bereits.";
return false;
}
LaunchSettings settings = rawSettings;
settings.path = config::ExpandEnvironment(settings.path);
settings.workingDirectory = config::ExpandEnvironment(settings.workingDirectory);
DWORD attr = GetFileAttributesW(settings.path.c_str());
if (settings.path.empty() || attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
error = L"Das ausgewählte Programm/Script wurde nicht gefunden: " + settings.path;
return false;
}
LaunchCommand command;
if (!BuildCommand(settings, command, error))
{
return false;
}
std::wstring workingDirectory = settings.workingDirectory.empty()
? ParentDirectory(settings.path)
: settings.workingDirectory;
std::vector<wchar_t> mutableCommand(command.commandLine.begin(), command.commandLine.end());
mutableCommand.push_back(L'\0');
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
if (settings.hidden)
{
startup.dwFlags = STARTF_USESHOWWINDOW;
startup.wShowWindow = SW_HIDE;
}
PROCESS_INFORMATION pi{};
DWORD flags = settings.hidden ? CREATE_NO_WINDOW : 0;
BOOL created = CreateProcessW(
command.application.c_str(),
mutableCommand.data(),
nullptr,
nullptr,
FALSE,
flags,
nullptr,
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
&startup,
&pi);
if (!created)
{
error = L"Testprozess konnte nicht gestartet werden: " + Win32ErrorMessage(GetLastError());
return false;
}
CloseHandle(pi.hThread);
process_ = pi.hProcess;
processId_ = pi.dwProcessId;
return true;
}
bool TestProcess::Stop(std::wstring& error)
{
CleanupExited();
if (process_ == nullptr)
{
return true;
}
if (!TerminateProcess(process_, 0))
{
error = L"Testprozess konnte nicht beendet werden: " + Win32ErrorMessage(GetLastError());
return false;
}
WaitForSingleObject(process_, 3000);
CloseHandle(process_);
process_ = nullptr;
processId_ = 0;
return true;
}
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <windows.h>
#include <string>
namespace testlaunch
{
struct LaunchSettings
{
std::wstring path;
std::wstring arguments;
std::wstring workingDirectory;
bool hidden = true;
};
class TestProcess
{
public:
~TestProcess();
TestProcess(const TestProcess&) = delete;
TestProcess& operator=(const TestProcess&) = delete;
TestProcess() = default;
bool Start(const LaunchSettings& settings, std::wstring& error);
bool Stop(std::wstring& error);
bool IsRunning();
DWORD ProcessId() const { return processId_; }
void Detach();
private:
HANDLE process_ = nullptr;
DWORD processId_ = 0;
void CleanupExited();
};
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#define IDR_PLUGIN_DLL 101