Files
patrick 4c51989600 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
2026-08-11 16:32:33 +02:00

16 KiB
Raw Permalink Blame History

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

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:

  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

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 + 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:

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
  • 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:

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.