# 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: 1. automatischer lokaler Start eines Programms oder Scripts beim erfolgreichen RDP-Verbindungsaufbau, 2. expliziter lokaler Programmstart durch einen festen Trigger innerhalb der RDP-Sitzung, 3. 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 ```text 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 ```powershell .\build-release.ps1 -Clean ``` Erwartete Ausgabe: ```text 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: 1. INI lesen oder erzeugen, 2. Client-Hostname über DVC abfragen, 3. temporäre SDX-Datei binär lesen, 4. Hostname im SIOMIN-Aktionsdatensatz ersetzen, 5. interne Datensatzlänge korrigieren, 6. Ergebnis an finale `siomin.sdx` anhängen, 7. Daten mit `FlushFileBuffers()` bestätigen, 8. temporäre SDX auf 0 Bytes kürzen, 9. START über einen neuen DVC senden, 10. START-Quittung abwarten. ## 5. Registry-Konfiguration ### MSTSC-Registrierung ```text HKCU\Software\Microsoft\Terminal Server Client\Default\AddIns\PlandentMstscScriptHook Name = %LOCALAPPDATA%\Plandent\MstscScriptHook\PlandentMstscScriptHook.dll ``` ### Anwendungskonfiguration ```text HKCU\Software\Plandent\MstscScriptHook ``` Relevante Werte: ```text 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: ```text .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 ```text plandent::mstsc-script-hook ``` Der Name ist in Client-Plugin und Server-Komponenten identisch zu halten. ### Protokoll Aktuell definierte Nachrichten: ```text 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 ``` ### 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: ```text START_OK ``` oder: ```text 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: ```text 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: ```text 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: ```text [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: ```text 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: ```text 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: ```text TS-FR ``` wird zu: ```text FR-Empfang-3R ``` Wichtig ist die Größenänderung: ```text TS-FR = 5 Byte FR-Empfang-3R = 13 Byte Differenz = +8 Byte ``` Ein ursprünglicher A-Datensatz mit: ```text 0x4C = 76 Byte ``` muss anschließend: ```text 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äß: ```text 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 + LEN` muss innerhalb der Quelldatei liegen. - Datensatzende muss `0D 0A` enthalten. - Nur Datensätze mit Typ `A` werden 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: ```text PlandentSiominClientTrigger.exe PlandentSiominClientTrigger.ini ``` Inhalt: ```ini [SIOMIN] SIOMIN_TEMP_PATH=C:\PDATA\temp_siomin.sdx SIOMIN_PATH=C:\PDATA\siomin.sdx DEBUG=0 ``` Typische Terminalserver-Konfiguration: ```ini [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: ```ini DEBUG=1 ``` Logpfad: ```text \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 ```text 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 ```text 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: ```text EnableLogging=1 ``` Log: ```text %LOCALAPPDATA%\Plandent\MstscScriptHook\plugin.log ``` Typische Einträge: - `IWTSPlugin::Initialize` - Registrierung des DVC-Listeners - `Connected` - `Disconnected` - angenommener DVC - `GET_CLIENT_HOSTNAME` - `START` - 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: ```text 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.sdx` verschiebt ## 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.