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:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "resource.h"
|
||||
|
||||
IDR_PLUGIN_DLL RCDATA "Generated\\PlandentMstscScriptHook.dll"
|
||||
@@ -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. & exit /b 1)
|
||||
copy /Y "$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\PlandentMstscScriptHook.dll" "$(ProjectDir)Generated\PlandentMstscScriptHook.dll" >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. & exit /b 1)
|
||||
copy /Y "$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\PlandentMstscScriptHook.dll" "$(ProjectDir)Generated\PlandentMstscScriptHook.dll" >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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define IDR_PLUGIN_DLL 101
|
||||
Reference in New Issue
Block a user