Files
Mstsc-Script-Hook/DEVELOPMENT.md
T
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

644 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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
<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
```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.