- 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
16 KiB
Entwicklerdokumentation – Plandent MSTSC Script Hook
1. Ziel des Projekts
Der Plandent MSTSC Script Hook verbindet Programme innerhalb einer Microsoft-RDP-Sitzung mit lokal auf dem RDP-Client ausgeführten Programmen.
Der aktuelle Projektumfang ist bewusst auf drei konkrete Workflows ausgerichtet:
- automatischer lokaler Start eines Programms oder Scripts beim erfolgreichen RDP-Verbindungsaufbau,
- expliziter lokaler Programmstart durch einen festen Trigger innerhalb der RDP-Sitzung,
- SIOMIN-/SLIDA-Bridge zwischen einem Terminalprogramm und einem lokal installierten Sidexis.
Der Terminalserver darf dabei keinen beliebigen lokalen Programmpfad oder beliebige Startparameter vorgeben. Programmpfad und Parameter werden ausschließlich lokal auf dem Client konfiguriert.
2. Projektstruktur
PlandentMstscScriptHook/
├── Configurator/
│ ├── Main.cpp
│ ├── Config.h
│ ├── RegistryConfig.cpp
│ ├── PluginInstaller.cpp
│ ├── TestLauncher.cpp
│ └── Configurator.vcxproj
│
├── Plugin/
│ ├── DllMain.cpp
│ ├── ScriptHookPlugin.cpp
│ ├── RegistryConfig.cpp
│ ├── ProcessLauncher.cpp
│ ├── Logging.cpp
│ ├── exports.def
│ └── PlandentMstscScriptHook.vcxproj
│
├── ServerTrigger/
│ ├── Main.cpp
│ └── PlandentRdpClientTrigger.vcxproj
│
├── SiominTrigger/
│ ├── Main.cpp
│ └── PlandentSiominClientTrigger.vcxproj
│
├── Shared/
│ ├── TriggerProtocol.h
│ └── DvcServerIo.h
│
├── examples/
├── build-release.ps1
└── PlandentMstscScriptHook.sln
Alle Projekte sind native Windows-x64-C++-Projekte. Der frühere .NET-Konfigurator wurde vollständig entfernt.
3. Build
Voraussetzungen
- Visual Studio 2022 oder entsprechende Build Tools
- Workload Desktopentwicklung mit C++
- Windows 10/11 SDK
- x64 Toolchain
Die Release-Projekte verwenden die statische MSVC-Laufzeit.
Vollständiges Release
.\build-release.ps1 -Clean
Erwartete Ausgabe:
dist\
├── PlandentMstscScriptHook.exe
├── PlandentRdpClientTrigger.exe
└── PlandentSiominClientTrigger.exe
Die Plugin-DLL wird beim Build in die Configurator-EXE eingebettet und muss nicht separat verteilt werden.
4. Komponenten
4.1 Configurator
PlandentMstscScriptHook.exe ist ein nativer Win32-Konfigurator.
Aufgaben:
- Lesen und Schreiben der HKCU-Konfiguration,
- Teststart der konfigurierten Programme,
- Extraktion der eingebetteten Plugin-DLL,
- Registrierung des Plugins für
mstsc.exe, - Entfernen der MSTSC-Registrierung,
- Zugriff auf das Plugin-Log.
Die Installation ist benutzerbezogen.
4.2 MSTSC-Plugin
PlandentMstscScriptHook.dll wird vom klassischen Microsoft-RDP-Client geladen.
Das Plugin übernimmt:
- RDP-Lifecycle,
- optionalen Start bei erfolgreichem Connect,
- DVC-Listener,
- Hostname-Abfrage,
- festen START-Trigger,
- Start des lokal vorkonfigurierten Programms,
- Logging und Mehrfachstartschutz.
4.3 Einfacher Server-Trigger
PlandentRdpClientTrigger.exe läuft innerhalb der aktuellen RDP-Sitzung und sendet ausschließlich den festen START-Befehl.
Es gibt absichtlich keine Kommandozeilenparameter für Zielpfad oder Argumente.
4.4 SIOMIN-Trigger
PlandentSiominClientTrigger.exe läuft ebenfalls innerhalb der RDP-Sitzung.
Der Ablauf besteht aus:
- INI lesen oder erzeugen,
- Client-Hostname über DVC abfragen,
- temporäre SDX-Datei binär lesen,
- Hostname im SIOMIN-Aktionsdatensatz ersetzen,
- interne Datensatzlänge korrigieren,
- Ergebnis an finale
siomin.sdxanhängen, - Daten mit
FlushFileBuffers()bestätigen, - temporäre SDX auf 0 Bytes kürzen,
- START über einen neuen DVC senden,
- START-Quittung abwarten.
5. Registry-Konfiguration
MSTSC-Registrierung
HKCU\Software\Microsoft\Terminal Server Client\Default\AddIns\PlandentMstscScriptHook
Name = %LOCALAPPDATA%\Plandent\MstscScriptHook\PlandentMstscScriptHook.dll
Anwendungskonfiguration
HKCU\Software\Plandent\MstscScriptHook
Relevante Werte:
Enabled REG_DWORD
EnableLogging REG_DWORD
ScriptPath REG_EXPAND_SZ
Arguments REG_SZ
WorkingDirectory REG_EXPAND_SZ
StartOnConnect REG_DWORD
StopOnDisconnect REG_DWORD
Hidden REG_DWORD
PreventDuplicates REG_DWORD
TriggerEnabled REG_DWORD
TriggerProgramPath REG_EXPAND_SZ
TriggerArguments REG_SZ
TriggerWorkingDirectory REG_EXPAND_SZ
TriggerHidden REG_DWORD
TriggerPreventDuplicates REG_DWORD
Der automatische RDP-Start und der DVC-Trigger besitzen bewusst getrennte Konfigurationen.
6. Lokaler Prozessstart
Unterstützte Dateitypen:
.exe
.bat
.cmd
.ps1
Für Batchdateien wird cmd.exe, für PowerShell-Scripte Windows PowerShell und für EXE-Dateien ein direkter Prozessstart verwendet.
Bei verborgenem Start werden die entsprechenden Windows-Prozessflags verwendet, damit kein Konsolenfenster sichtbar wird.
Mehrfachstartschutz
Für den automatischen Start und den Triggerstart kann separat ein Mehrfachstartschutz aktiviert werden.
Aus dem expandierten Programmpfad wird ein Named Mutex abgeleitet. Das Mutex-Handle wird an den gestarteten Root-Prozess vererbt. Solange der Prozess lebt, kann derselbe konfigurierte Pfad nicht erneut gestartet werden.
7. Dynamic Virtual Channel
Kanalname
plandent::mstsc-script-hook
Der Name ist in Client-Plugin und Server-Komponenten identisch zu halten.
Protokoll
Aktuell definierte Nachrichten:
PLANDENT_MSTSC_HOOK/1 START
PLANDENT_MSTSC_HOOK/1 START_OK
PLANDENT_MSTSC_HOOK/1 START_FAILED
PLANDENT_MSTSC_HOOK/1 GET_CLIENT_HOSTNAME
PLANDENT_MSTSC_HOOK/1 CLIENT_HOSTNAME <hostname>
START
Der Server öffnet einen kurzlebigen DVC für die aktuelle RDP-Sitzung und sendet START.
Das Plugin prüft die exakte Nachricht und startet ausschließlich das lokal konfigurierte Trigger-Programm.
Danach antwortet das Plugin mit:
START_OK
oder:
START_FAILED
Der Server schließt den DVC erst nach Empfang dieser Quittung. Diese Quittierung verhindert die zuvor beobachtete Race Condition, bei der ein unmittelbar nach WTSVirtualChannelWrite() geschlossener Kanal dazu führen konnte, dass spätere Trigger nicht mehr beim Client ankamen.
Client-Hostname
Der SIOMIN-Trigger sendet zunächst:
PLANDENT_MSTSC_HOOK/1 GET_CLIENT_HOSTNAME
Der Rechnername wird direkt auf dem lokalen Windows-Client ermittelt. Damit ist die Funktion nicht von CLIENTNAME oder COMPUTERNAME innerhalb der Remotesitzung abhängig.
Antwort:
PLANDENT_MSTSC_HOOK/1 CLIENT_HOSTNAME FR-Empfang-3R
Für den anschließenden START wird ein neuer DVC geöffnet.
DVC-Payload auf der Serverseite
Bei Antworten des Client-Plugins kann WTSVirtualChannelRead() einen CHANNEL_PDU_HEADER vor den eigentlichen Payload stellen.
Shared/DvcServerIo.h behandelt daher:
- DVC-Header,
- Payload-Länge,
- gegebenenfalls fragmentierte Nachrichten,
- Timeout.
Diese Behandlung muss für alle zukünftigen bidirektionalen DVC-Nachrichten wiederverwendet werden.
8. SIOMIN-/SLIDA-Dateiformat
Wichtig: binäres bzw. längencodiertes Format
Die untersuchten SIOMIN-Dateien dürfen nicht wie gewöhnliche Textdateien neu serialisiert werden.
Das beobachtete Format verwendet:
- ein Byte für die Datensatzlänge,
- NUL-Trenner,
- einbyteige Textfelder,
- CRLF am Ende des Datensatzes.
Beobachtete Grundstruktur:
[LEN] 00 [TYPE] 00 [Feld] 00 [Feld] 00 ... 0D 0A
LEN ist die Gesamtlänge des jeweiligen Datensatzes in Bytes, einschließlich Längenbyte und abschließendem CRLF.
Beispiele:
3A 00 4E 00 ... -> 0x3A = 58 Byte, Typ N
40 00 4E 00 ... -> 0x40 = 64 Byte, Typ N
46 00 41 00 ... -> 0x46 = 70 Byte, Typ A
4C 00 41 00 ... -> 0x4C = 76 Byte, Typ A
Dadurch darf das erste Byte nicht als ASCII-Datensatzkennung interpretiert werden. Je nach Länge kann es im Texteditor zufällig als sichtbarer Buchstabe erscheinen.
A-Datensatz
Für den relevanten Aktionsdatensatz wurde folgende Feldfolge beobachtet:
LEN
00
A
00
Nachname
00
Vorname
00
Geburtsdatum
00
Patienten-ID
00
Hostname
00
Datum
00
Uhrzeit
...
0D 0A
Der Hostname beginnt nach dem sechsten NUL-Trenner ab Datensatzbeginn.
Ersetzen des Hostnamens
Der Hostname wird als druckbares ASCII in das vorhandene Feld geschrieben.
Beispiel:
TS-FR
wird zu:
FR-Empfang-3R
Wichtig ist die Größenänderung:
TS-FR = 5 Byte
FR-Empfang-3R = 13 Byte
Differenz = +8 Byte
Ein ursprünglicher A-Datensatz mit:
0x4C = 76 Byte
muss anschließend:
0x54 = 84 Byte
als Längenbyte enthalten.
Wird nur der Hostname geändert und das Längenbyte nicht angepasst, verwirft Sidexis den Datensatz. Ein real beobachteter Fehler lautete sinngemäß:
specified length = 76
real length = 84
und führte zur Ablage in unprocessable.sdx.
Parser-Regeln
Aktuelle Regeln:
- Datensatzbeginn ist die Position des Längenbytes.
recordStart + LENmuss innerhalb der Quelldatei liegen.- Datensatzende muss
0D 0Aenthalten. - Nur Datensätze mit Typ
Awerden hinsichtlich Hostname verändert. - Der Hostname darf nicht leer sein.
- Nach Änderung muss die neue Datensatzlänge zwischen 1 und 255 liegen.
- Alle nicht veränderten Bytes werden unverändert übernommen.
- Mehrere passende A-Datensätze können in einem Temp-Block verarbeitet werden.
9. SIOMIN-INI
Der SIOMIN-Trigger verwendet eine INI mit demselben Basisnamen wie die EXE.
Beispiel:
PlandentSiominClientTrigger.exe
PlandentSiominClientTrigger.ini
Inhalt:
[SIOMIN]
SIOMIN_TEMP_PATH=C:\PDATA\temp_siomin.sdx
SIOMIN_PATH=C:\PDATA\siomin.sdx
DEBUG=0
Typische Terminalserver-Konfiguration:
[SIOMIN]
SIOMIN_TEMP_PATH=C:\PDATA\temp_siomin.sdx
SIOMIN_PATH=\\SERVER\PDATA\siomin.sdx
DEBUG=0
Fehlt die INI, wird sie als UTF-16-LE-Datei mit BOM erzeugt.
Fehlt bei einer bestehenden INI nur DEBUG, wird DEBUG=0 ergänzt.
10. SIOMIN-Dateioperationen
Temporäre Datei
SIOMIN_TEMP_PATH wird vollständig binär eingelesen.
Die Datei wird erst geleert, wenn:
- ein gültiger Hostname vorliegt,
- die Transformation erfolgreich war,
- mindestens ein A-Datensatz geändert wurde,
- die finale Datei erfolgreich geschrieben wurde,
FlushFileBuffers()erfolgreich war.
Danach wird die Temp-Datei auf 0 Bytes gekürzt, nicht gelöscht.
Finale Datei
Existiert SIOMIN_PATH noch nicht, wird sie angelegt.
Bei einer bereits vorhandenen, nichtleeren Datei wird geprüft, ob die Datei auf CRLF endet. Ist die Dateigrenze nicht sicher, wird nicht angehängt.
Rollback
Kann die Temp-Datei nach erfolgreichem Append nicht geleert werden, versucht der Trigger das Append zurückzurollen:
- bei vorher nicht vorhandener finaler Datei: Datei löschen,
- bei vorhandener finaler Datei: auf ursprüngliche Größe zurückkürzen.
Damit soll verhindert werden, dass beim nächsten Aufruf derselbe Temp-Eintrag erneut angehängt wird.
11. SIOMIN-Logging
INI:
DEBUG=1
Logpfad:
<EXE-Verzeichnis>\PlandentSiominClientTrigger.log
Das Log enthält unter anderem:
- EXE- und INI-Pfad,
- konfigurierte SIOMIN-Pfade,
- Hostname-Abfrage,
- DVC-Payload-Größen,
- ermittelten Client-Hostname,
- Größe der Temp-Datei,
- Anzahl der geänderten Hostfelder,
- Größe des transformierten Blocks,
- Append,
- Clear,
- START und START-Quittung.
Bei Parserfehlern kann zusätzlich eine Hex-Vorschau der Temp-SDX ausgegeben werden.
DEBUG=0 deaktiviert die Logdatei, nicht aber OutputDebugStringW.
12. Exitcodes
PlandentRdpClientTrigger.exe
0 START_OK empfangen
10 DVC konnte nicht geöffnet werden
11 Schreiben fehlgeschlagen
12 Nachricht nicht vollständig geschrieben
13 START-Quittung konnte nicht gelesen werden
14 START_FAILED empfangen
15 ungültige START-Quittung
PlandentSiominClientTrigger.exe
0 erfolgreich
20 EXE-/INI-Pfad konnte nicht ermittelt werden
21 INI konnte nicht angelegt werden
22 DVC für Hostname konnte nicht geöffnet werden
23 Hostname-Anfrage konnte nicht geschrieben werden
24 Hostname-Antwort konnte nicht gelesen werden
25 ungültige Hostname-Antwort
26 Client-Hostname für SIOMIN ungeeignet
27 Temp-SDX konnte nicht gelesen werden
28 kein gültiges A-Hostname-Feld gefunden
29 SIOMIN-Pfade ungültig oder identisch
30 finale Datei konnte nicht geöffnet werden
31 unsichere Dateigrenze der finalen SIOMIN-Datei
32 finale Datei konnte nicht geschrieben werden
33 START konnte nicht geschrieben werden
34 Temp-SDX konnte nicht geleert werden
35 START-Quittung konnte nicht gelesen werden
36 START_FAILED vom Client
37 ungültige START-Quittung
38 Rollback nach Clear-Fehler fehlgeschlagen
13. Logging des Client-Plugins
Registry-Option:
EnableLogging=1
Log:
%LOCALAPPDATA%\Plandent\MstscScriptHook\plugin.log
Typische Einträge:
IWTSPlugin::Initialize- Registrierung des DVC-Listeners
ConnectedDisconnected- angenommener DVC
GET_CLIENT_HOSTNAMESTART- gestartete PID
- Kanal geschlossen
Bei DVC-Problemen sollten immer sowohl das Client-Plugin-Log als auch das Log der jeweiligen Server-Trigger-EXE betrachtet werden.
14. RemoteVDDS-Anwendungsfall
Der automatische RDP-Start ist insbesondere für die RemoteVDDS-Scripte von Tobias Bauer vorgesehen.
Typische lokale Konfiguration:
ScriptPath=C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
Arguments=MIN
WorkingDirectory=C:\RemoteVDDS
StartOnConnect=1
Hidden=1
PreventDuplicates=1
Die RemoteVDDS-Scripte selbst gehören nicht zum Projekt und sollten nicht ohne Prüfung ihrer eigenen Lizenzbedingungen in Releases aufgenommen werden.
15. Sidexis-Anwendungsfall
Auf dem Client wird Sidexis als Trigger-Programm konfiguriert.
Auf dem Terminalserver wird im aufrufenden Fachprogramm PlandentSiominClientTrigger.exe als Sidexis-EXE eingetragen.
Das Fachprogramm schreibt zunächst in eine lokale Temp-SDX. Der Trigger schreibt den korrigierten Eintrag anschließend in die zentrale PDATA-siomin.sdx und startet erst danach Sidexis auf dem tatsächlichen RDP-Client.
Wichtig ist, dass die Temp-Datei nicht direkt dieselbe Datei wie die finale siomin.sdx ist.
16. Sicherheit und Grenzen
Das DVC-Protokoll ist absichtlich keine allgemeine Remote-Execution-Schnittstelle.
Der Server kann nur:
- den Client-Hostname anfragen,
- den lokal vorkonfigurierten START auslösen.
Der Server überträgt keinen lokalen Programmpfad und keine Startparameter.
Weitere Grenzen:
- nur klassischer Microsoft-RDC-/MSTSC-Pluginmechanismus,
- x64-Build,
- Trigger muss innerhalb der betreffenden RDP-Sitzung laufen,
- lokaler Prozess läuft im Sicherheitskontext des MSTSC-Benutzers,
- AppLocker/WDAC können DLL-Laden oder Prozessstarts blockieren,
- SIOMIN-Parser basiert auf dem praktisch beobachteten, längencodierten Format und sollte bei anderen Sidexis-/SLIDA-Versionen mit echten Beispieldateien validiert werden.
17. Testempfehlung
Vor einem Release sollten mindestens folgende Fälle getestet werden:
Client
- Plugin installieren/aktualisieren/entfernen
- neue MSTSC-Verbindung lädt Plugin
- automatischer Scriptstart
- unsichtbarer Batchstart
- Mehrfachstartschutz
- Disconnect-Verhalten
Einfacher Trigger
- erster Start
- mehrere Starts innerhalb derselben RDP-Sitzung
- Start bei bereits laufendem Zielprogramm
- fehlendes Plugin
START_FAILED
SIOMIN
- INI-Neuanlage
- lokaler und UNC-Zielpfad
- Hostname kürzer/länger als vorhandener Hostname
- korrekte Anpassung des Längenbytes
- mehrere Datensätze in einer Temp-Datei
- finale Datei nicht vorhanden
- finale Datei bereits vorhanden
- Temp-Datei wird nach Erfolg geleert
- Rollback bei Clear-Fehler
DEBUG=1- Kontrolle, dass Sidexis den Eintrag verarbeitet und nicht nach
unprocessable.sdxverschiebt
18. Lizenz und Drittkomponenten
Copyright (c) 2026 Patrick Gniza.
Der Projektcode ist proprietär. Siehe LICENSE.
Drittkomponenten und separat bezogene Scripte, insbesondere die RemoteVDDS-Scripte von Tobias Bauer, sind nicht automatisch Bestandteil dieser Lizenz.