Hinzufügen der Möglichkeit ein Programmstart auf dem Client durch die RDP Sitzung zu triggern

This commit is contained in:
2026-08-11 09:52:49 +02:00
parent eec4f52b5a
commit ee88f1ff30
22 changed files with 1162 additions and 263 deletions
+1
View File
@@ -94,6 +94,7 @@
<ClInclude Include="ProcessLauncher.h" />
<ClInclude Include="RegistryConfig.h" />
<ClInclude Include="ScriptHookPlugin.h" />
<ClInclude Include="..\Shared\TriggerProtocol.h" />
</ItemGroup>
<ItemGroup>
<None Include="exports.def" />
@@ -16,5 +16,6 @@
<ClInclude Include="ProcessLauncher.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="RegistryConfig.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="ScriptHookPlugin.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="..\Shared\TriggerProtocol.h"><Filter>Header Files</Filter></ClInclude>
</ItemGroup>
</Project>
+28 -28
View File
@@ -122,9 +122,9 @@ namespace
std::wstring commandLine;
};
bool BuildLaunchCommand(const hook::HookConfig& config, LaunchCommand& command)
bool BuildLaunchCommand(const hook::LaunchConfig& config, LaunchCommand& command)
{
const std::wstring extension = GetExtensionLower(config.scriptPath);
const std::wstring extension = GetExtensionLower(config.path);
if (extension == L".bat" || extension == L".cmd")
{
@@ -139,7 +139,7 @@ namespace
comspec = systemRoot + L"\\System32\\cmd.exe";
}
std::wstring inner = QuoteWindowsArgument(config.scriptPath);
std::wstring inner = QuoteWindowsArgument(config.path);
if (!config.arguments.empty())
{
inner += L" " + config.arguments;
@@ -162,7 +162,7 @@ namespace
command.application = powershell;
command.commandLine = QuoteWindowsArgument(powershell) +
L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
QuoteWindowsArgument(config.scriptPath);
QuoteWindowsArgument(config.path);
if (!config.arguments.empty())
{
command.commandLine += L" " + config.arguments;
@@ -172,8 +172,8 @@ namespace
if (extension == L".exe")
{
command.application = config.scriptPath;
command.commandLine = QuoteWindowsArgument(config.scriptPath);
command.application = config.path;
command.commandLine = QuoteWindowsArgument(config.path);
if (!config.arguments.empty())
{
command.commandLine += L" " + config.arguments;
@@ -217,37 +217,37 @@ namespace hook
return process_ != nullptr;
}
bool ProcessLauncher::Start(const HookConfig& config)
bool ProcessLauncher::Start(const LaunchConfig& config, bool loggingEnabled, const std::wstring& sourceName)
{
CleanupExitedProcess();
if (process_ != nullptr)
{
Log(config.enableLogging, L"Script ist für diese MSTSC-Instanz bereits aktiv (PID " + std::to_wstring(processId_) + L").");
Log(loggingEnabled, sourceName + L" ist für diese MSTSC-Instanz bereits aktiv (PID " + std::to_wstring(processId_) + L").");
return true;
}
if (config.scriptPath.empty())
if (config.path.empty())
{
Log(config.enableLogging, L"Kein ScriptPath konfiguriert; Start übersprungen.");
Log(loggingEnabled, L"Kein Pfad für " + sourceName + L" konfiguriert; Start übersprungen.");
return false;
}
DWORD attributes = GetFileAttributesW(config.scriptPath.c_str());
DWORD attributes = GetFileAttributesW(config.path.c_str());
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
Log(config.enableLogging, L"Script wurde nicht gefunden: " + config.scriptPath);
Log(loggingEnabled, sourceName + L" wurde nicht gefunden: " + config.path);
return false;
}
LaunchCommand command;
if (!BuildLaunchCommand(config, command))
{
Log(config.enableLogging, L"Nicht unterstützter Script-/Dateityp: " + config.scriptPath);
Log(loggingEnabled, L"Nicht unterstützter Datei-/Programmtyp für " + sourceName + L": " + config.path);
return false;
}
std::wstring workingDirectory = config.workingDirectory.empty()
? ParentDirectory(config.scriptPath)
? ParentDirectory(config.path)
: config.workingDirectory;
HANDLE duplicateMutex = nullptr;
@@ -257,19 +257,19 @@ namespace hook
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
std::wstring mutexName = MutexNameForScript(config.scriptPath);
std::wstring mutexName = MutexNameForScript(config.path);
duplicateMutex = CreateMutexW(&sa, FALSE, mutexName.c_str());
if (duplicateMutex == nullptr)
{
DWORD error = GetLastError();
Log(config.enableLogging, L"Mehrfachstart-Schutz konnte nicht erstellt werden: " + FormatWin32Error(error));
Log(loggingEnabled, L"Mehrfachstart-Schutz konnte nicht erstellt werden: " + FormatWin32Error(error));
return false;
}
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
CloseHandle(duplicateMutex);
Log(config.enableLogging, L"Ein Receiver mit demselben ScriptPath läuft bereits; kein weiterer Start.");
Log(loggingEnabled, sourceName + L" läuft laut Mehrfachstart-Schutz bereits; kein weiterer Start.");
return true;
}
}
@@ -305,7 +305,7 @@ namespace hook
{
DWORD error = GetLastError();
CloseHandle(duplicateMutex);
Log(config.enableLogging, L"Prozessattributliste konnte nicht initialisiert werden: " + FormatWin32Error(error));
Log(loggingEnabled, L"Prozessattributliste konnte nicht initialisiert werden: " + FormatWin32Error(error));
return false;
}
@@ -322,7 +322,7 @@ namespace hook
DWORD error = GetLastError();
DeleteProcThreadAttributeList(startup.lpAttributeList);
CloseHandle(duplicateMutex);
Log(config.enableLogging, L"Handle-Vererbung konnte nicht vorbereitet werden: " + FormatWin32Error(error));
Log(loggingEnabled, L"Handle-Vererbung konnte nicht vorbereitet werden: " + FormatWin32Error(error));
return false;
}
@@ -371,7 +371,7 @@ namespace hook
if (!created)
{
DWORD error = GetLastError();
Log(config.enableLogging, L"Scriptstart fehlgeschlagen: " + FormatWin32Error(error));
Log(loggingEnabled, sourceName + L" konnte nicht gestartet werden: " + FormatWin32Error(error));
return false;
}
@@ -379,14 +379,14 @@ namespace hook
process_ = pi.hProcess;
processId_ = pi.dwProcessId;
Log(config.enableLogging,
L"Script gestartet. PID=" + std::to_wstring(processId_) +
L", Script=" + config.scriptPath +
Log(loggingEnabled,
sourceName + L" gestartet. PID=" + std::to_wstring(processId_) +
L", Pfad=" + config.path +
(config.arguments.empty() ? L"" : L", Argumente=" + config.arguments));
return true;
}
void ProcessLauncher::Stop(bool loggingEnabled)
void ProcessLauncher::Stop(bool loggingEnabled, const std::wstring& sourceName)
{
CleanupExitedProcess();
if (process_ == nullptr)
@@ -398,11 +398,11 @@ namespace hook
if (!TerminateProcess(process_, 0))
{
DWORD error = GetLastError();
Log(loggingEnabled, L"Scriptprozess PID " + std::to_wstring(pid) + L" konnte nicht beendet werden: " + FormatWin32Error(error));
Log(loggingEnabled, sourceName + L" PID " + std::to_wstring(pid) + L" konnte nicht beendet werden: " + FormatWin32Error(error));
}
else
{
Log(loggingEnabled, L"Beenden des Scriptprozesses PID " + std::to_wstring(pid) + L" wurde bei RDP-Trennung angefordert.");
Log(loggingEnabled, L"Beenden von " + sourceName + L" PID " + std::to_wstring(pid) + L" wurde angefordert.");
}
CloseHandle(process_);
@@ -410,7 +410,7 @@ namespace hook
processId_ = 0;
}
void ProcessLauncher::Detach(bool loggingEnabled)
void ProcessLauncher::Detach(bool loggingEnabled, const std::wstring& sourceName)
{
CleanupExitedProcess();
if (process_ == nullptr)
@@ -418,7 +418,7 @@ namespace hook
return;
}
Log(loggingEnabled, L"MSTSC beendet Plugin; Script PID " + std::to_wstring(processId_) + L" läuft weiter.");
Log(loggingEnabled, L"MSTSC beendet Plugin; " + sourceName + L" PID " + std::to_wstring(processId_) + L" läuft weiter.");
CloseHandle(process_);
process_ = nullptr;
processId_ = 0;
+3 -3
View File
@@ -15,9 +15,9 @@ namespace hook
ProcessLauncher(const ProcessLauncher&) = delete;
ProcessLauncher& operator=(const ProcessLauncher&) = delete;
bool Start(const HookConfig& config);
void Stop(bool loggingEnabled);
void Detach(bool loggingEnabled);
bool Start(const LaunchConfig& config, bool loggingEnabled, const std::wstring& sourceName);
void Stop(bool loggingEnabled, const std::wstring& sourceName = L"Programm");
void Detach(bool loggingEnabled, const std::wstring& sourceName = L"Programm");
bool IsRunning();
private:
+16 -6
View File
@@ -76,14 +76,24 @@ namespace hook
}
config.enabled = ReadDword(key, L"Enabled", 1) != 0;
config.scriptPath = ReadString(key, L"ScriptPath");
config.arguments = ExpandEnvironment(ReadString(key, L"Arguments"));
config.workingDirectory = ReadString(key, L"WorkingDirectory");
config.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0;
// Bestehende Registry-Werte bleiben vollständig kompatibel.
config.connectProgram.path = ReadString(key, L"ScriptPath");
config.connectProgram.arguments = ExpandEnvironment(ReadString(key, L"Arguments"));
config.connectProgram.workingDirectory = ReadString(key, L"WorkingDirectory");
config.connectProgram.hidden = ReadDword(key, L"Hidden", 1) != 0;
config.connectProgram.preventDuplicates = ReadDword(key, L"PreventDuplicates", 1) != 0;
config.startOnConnect = ReadDword(key, L"StartOnConnect", 1) != 0;
config.stopOnDisconnect = ReadDword(key, L"StopOnDisconnect", 0) != 0;
config.hidden = ReadDword(key, L"Hidden", 1) != 0;
config.preventDuplicates = ReadDword(key, L"PreventDuplicates", 1) != 0;
config.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0;
// Neue, ausschließlich lokal definierte Konfiguration für Server-Trigger.
config.triggerEnabled = ReadDword(key, L"TriggerEnabled", 0) != 0;
config.triggerProgram.path = ReadString(key, L"TriggerProgramPath");
config.triggerProgram.arguments = ExpandEnvironment(ReadString(key, L"TriggerArguments"));
config.triggerProgram.workingDirectory = ReadString(key, L"TriggerWorkingDirectory");
config.triggerProgram.hidden = ReadDword(key, L"TriggerHidden", 1) != 0;
config.triggerProgram.preventDuplicates = ReadDword(key, L"TriggerPreventDuplicates", 1) != 0;
RegCloseKey(key);
return config;
+18 -6
View File
@@ -7,17 +7,29 @@ namespace hook
{
inline constexpr wchar_t kConfigKey[] = L"Software\\Plandent\\MstscScriptHook";
struct LaunchConfig
{
std::wstring path;
std::wstring arguments;
std::wstring workingDirectory;
bool hidden = true;
bool preventDuplicates = true;
};
struct HookConfig
{
bool enabled = true;
std::wstring scriptPath;
std::wstring arguments;
std::wstring workingDirectory;
bool enableLogging = true;
// Optionaler automatischer Start bei erfolgreicher RDP-Verbindung.
LaunchConfig connectProgram;
bool startOnConnect = true;
bool stopOnDisconnect = false;
bool hidden = true;
bool preventDuplicates = true;
bool enableLogging = true;
// Optionaler Start, der durch PlandentRdpClientTrigger.exe auf dem
// Terminalserver über den Dynamic Virtual Channel ausgelöst wird.
bool triggerEnabled = false;
LaunchConfig triggerProgram;
};
HookConfig LoadConfig();
+203 -11
View File
@@ -2,9 +2,109 @@
#include "Logging.h"
#include "RegistryConfig.h"
#include "../Shared/TriggerProtocol.h"
#include <cstring>
#include <new>
#include <string>
namespace
{
class TriggerChannelCallback final : public IWTSVirtualChannelCallback
{
public:
TriggerChannelCallback() = default;
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override
{
if (ppvObject == nullptr)
{
return E_POINTER;
}
*ppvObject = nullptr;
if (riid == __uuidof(IUnknown) || riid == __uuidof(IWTSVirtualChannelCallback))
{
*ppvObject = static_cast<IWTSVirtualChannelCallback*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override
{
return static_cast<ULONG>(InterlockedIncrement(&refCount_));
}
ULONG STDMETHODCALLTYPE Release() override
{
ULONG remaining = static_cast<ULONG>(InterlockedDecrement(&refCount_));
if (remaining == 0)
{
delete this;
}
return remaining;
}
HRESULT STDMETHODCALLTYPE OnDataReceived(ULONG cbSize, BYTE* buffer) override
{
const hook::HookConfig config = hook::LoadConfig();
if (buffer == nullptr || cbSize == 0)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Leere Nachricht empfangen; ignoriert.");
return S_OK;
}
if (cbSize != hook::protocol::kStartMessageLength ||
std::memcmp(buffer, hook::protocol::kStartMessage, hook::protocol::kStartMessageLength) != 0)
{
hook::Log(config.enableLogging,
L"DVC-Trigger: Unbekannte Nachricht empfangen; aus Sicherheitsgründen ignoriert.");
return S_OK;
}
hook::Log(config.enableLogging, L"DVC-Trigger: START vom Terminalserver empfangen.");
if (!config.enabled)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Plugin-Funktion ist deaktiviert; Start ignoriert.");
return S_OK;
}
if (!config.triggerEnabled)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Server-Trigger ist in der Client-Konfiguration deaktiviert.");
return S_OK;
}
// Der Terminalserver liefert absichtlich weder Pfad noch Argumente.
// Beides wird ausschließlich aus HKCU auf dem lokalen Client gelesen.
hook::ProcessLauncher launcher;
launcher.Start(config.triggerProgram, config.enableLogging, L"Server-Trigger-Programm");
return S_OK;
}
HRESULT STDMETHODCALLTYPE OnClose() override
{
const hook::HookConfig config = hook::LoadConfig();
hook::Log(config.enableLogging, L"DVC-Trigger: Kanal geschlossen.");
return S_OK;
}
private:
~TriggerChannelCallback() = default;
LONG refCount_ = 1;
};
}
ScriptHookPlugin::ScriptHookPlugin() = default;
ScriptHookPlugin::~ScriptHookPlugin() = default;
ScriptHookPlugin::~ScriptHookPlugin()
{
ReleaseDvcObjects();
}
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** ppvObject)
{
@@ -14,14 +114,22 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** p
}
*ppvObject = nullptr;
if (riid == __uuidof(IUnknown) || riid == __uuidof(IWTSPlugin))
{
*ppvObject = static_cast<IWTSPlugin*>(this);
AddRef();
return S_OK;
}
else if (riid == __uuidof(IWTSListenerCallback))
{
*ppvObject = static_cast<IWTSListenerCallback*>(this);
}
else
{
return E_NOINTERFACE;
}
return E_NOINTERFACE;
AddRef();
return S_OK;
}
ULONG STDMETHODCALLTYPE ScriptHookPlugin::AddRef()
@@ -41,9 +149,40 @@ ULONG STDMETHODCALLTYPE ScriptHookPlugin::Release()
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Initialize(IWTSVirtualChannelManager* channelManager)
{
UNREFERENCED_PARAMETER(channelManager);
const hook::HookConfig config = hook::LoadConfig();
hook::Log(config.enableLogging, L"IWTSPlugin::Initialize aufgerufen.");
if (channelManager == nullptr)
{
hook::Log(config.enableLogging, L"DVC-Initialisierung fehlgeschlagen: channelManager ist NULL.");
return E_POINTER;
}
ReleaseDvcObjects();
channelManager->AddRef();
channelManager_ = channelManager;
HRESULT hr = channelManager_->CreateListener(
const_cast<LPSTR>(hook::protocol::kChannelName),
0,
static_cast<IWTSListenerCallback*>(this),
&listener_);
if (FAILED(hr))
{
hook::Log(config.enableLogging,
L"DVC-Listener konnte nicht erstellt werden. HRESULT=" + std::to_wstring(static_cast<unsigned long>(hr)) +
L". Der automatische Start bei RDP-Verbindung bleibt trotzdem aktiv.");
ReleaseDvcObjects();
// Der DVC-Trigger ist eine optionale Zusatzfunktion. Ein nicht verfügbarer
// DVC darf den bestehenden Connected()/Disconnected()-Hook nicht deaktivieren.
return S_OK;
}
hook::Log(config.enableLogging,
L"DVC-Listener für 'plandent::mstsc-script-hook' wurde registriert.");
return S_OK;
}
@@ -54,12 +193,12 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Connected()
if (!config.enabled || !config.startOnConnect)
{
hook::Log(config.enableLogging, L"Start ist in der Konfiguration deaktiviert.");
hook::Log(config.enableLogging, L"Automatischer Start bei RDP-Verbindung ist deaktiviert.");
return S_OK;
}
// Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Script gewartet.
launcher_.Start(config);
// Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Programm gewartet.
connectLauncher_.Start(config.connectProgram, config.enableLogging, L"RDP-Verbindungsprogramm");
return S_OK;
}
@@ -71,7 +210,7 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Disconnected(DWORD disconnectCode)
if (config.stopOnDisconnect)
{
launcher_.Stop(config.enableLogging);
connectLauncher_.Stop(config.enableLogging, L"RDP-Verbindungsprogramm");
}
return S_OK;
}
@@ -83,11 +222,64 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Terminated()
if (config.stopOnDisconnect)
{
launcher_.Stop(config.enableLogging);
connectLauncher_.Stop(config.enableLogging, L"RDP-Verbindungsprogramm");
}
else
{
launcher_.Detach(config.enableLogging);
connectLauncher_.Detach(config.enableLogging, L"RDP-Verbindungsprogramm");
}
ReleaseDvcObjects();
return S_OK;
}
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::OnNewChannelConnection(
IWTSVirtualChannel* channel,
BSTR data,
BOOL* accept,
IWTSVirtualChannelCallback** callback)
{
UNREFERENCED_PARAMETER(data);
const hook::HookConfig config = hook::LoadConfig();
if (accept == nullptr || callback == nullptr)
{
return E_POINTER;
}
*accept = FALSE;
*callback = nullptr;
if (channel == nullptr)
{
return E_POINTER;
}
auto* channelCallback = new (std::nothrow) TriggerChannelCallback();
if (channelCallback == nullptr)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Callback konnte nicht angelegt werden.");
return E_OUTOFMEMORY;
}
*callback = static_cast<IWTSVirtualChannelCallback*>(channelCallback);
*accept = TRUE;
hook::Log(config.enableLogging, L"DVC-Trigger: Verbindung vom Terminalserver akzeptiert.");
return S_OK;
}
void ScriptHookPlugin::ReleaseDvcObjects()
{
if (listener_ != nullptr)
{
listener_->Release();
listener_ = nullptr;
}
if (channelManager_ != nullptr)
{
channelManager_->Release();
channelManager_ = nullptr;
}
}
+13 -2
View File
@@ -4,7 +4,7 @@
#include <windows.h>
#include <tsvirtualchannels.h>
class ScriptHookPlugin final : public IWTSPlugin
class ScriptHookPlugin final : public IWTSPlugin, public IWTSListenerCallback
{
public:
ScriptHookPlugin();
@@ -13,14 +13,25 @@ public:
ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override;
// IWTSPlugin
HRESULT STDMETHODCALLTYPE Initialize(IWTSVirtualChannelManager* channelManager) override;
HRESULT STDMETHODCALLTYPE Connected() override;
HRESULT STDMETHODCALLTYPE Disconnected(DWORD disconnectCode) override;
HRESULT STDMETHODCALLTYPE Terminated() override;
// IWTSListenerCallback
HRESULT STDMETHODCALLTYPE OnNewChannelConnection(
IWTSVirtualChannel* channel,
BSTR data,
BOOL* accept,
IWTSVirtualChannelCallback** callback) override;
private:
~ScriptHookPlugin();
void ReleaseDvcObjects();
LONG refCount_ = 1;
hook::ProcessLauncher launcher_;
hook::ProcessLauncher connectLauncher_;
IWTSVirtualChannelManager* channelManager_ = nullptr;
IWTSListener* listener_ = nullptr;
};