diff --git a/Configurator/Configurator.csproj b/Configurator/Configurator.csproj
index a2c2273..97557a4 100644
--- a/Configurator/Configurator.csproj
+++ b/Configurator/Configurator.csproj
@@ -9,12 +9,24 @@
Plandent.MstscScriptHook.Configurator
x64
x64
+ $(MSBuildProjectDirectory)\..\artifacts\x64\$(Configuration)\PlandentMstscScriptHook.dll
+
+
+
+
+ false
+ false
+ portable
+
+
+
+
win-x64
true
true
false
embedded
- $(MSBuildProjectDirectory)\..\artifacts\x64\$(Configuration)\PlandentMstscScriptHook.dll
diff --git a/Configurator/HookConfig.cs b/Configurator/HookConfig.cs
index 0f347ab..1713e93 100644
--- a/Configurator/HookConfig.cs
+++ b/Configurator/HookConfig.cs
@@ -3,6 +3,9 @@ namespace Plandent.MstscScriptHook.Configurator;
internal sealed class HookConfig
{
public bool Enabled { get; set; } = true;
+ public bool EnableLogging { get; set; } = true;
+
+ // Automatischer Start bei erfolgreicher RDP-Verbindung.
public string ScriptPath { get; set; } = string.Empty;
public string Arguments { get; set; } = string.Empty;
public string WorkingDirectory { get; set; } = string.Empty;
@@ -10,5 +13,13 @@ internal sealed class HookConfig
public bool StopOnDisconnect { get; set; }
public bool Hidden { get; set; } = true;
public bool PreventDuplicates { get; set; } = true;
- public bool EnableLogging { get; set; } = true;
+
+ // Separates lokales Programm, das ausschließlich durch den festen
+ // START-Trigger vom Terminalserver gestartet werden kann.
+ public bool TriggerEnabled { get; set; }
+ public string TriggerProgramPath { get; set; } = string.Empty;
+ public string TriggerArguments { get; set; } = string.Empty;
+ public string TriggerWorkingDirectory { get; set; } = string.Empty;
+ public bool TriggerHidden { get; set; } = true;
+ public bool TriggerPreventDuplicates { get; set; } = true;
}
diff --git a/Configurator/MainForm.cs b/Configurator/MainForm.cs
index aea4a2d..3980cb5 100644
--- a/Configurator/MainForm.cs
+++ b/Configurator/MainForm.cs
@@ -1,21 +1,35 @@
+using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
-using System.Diagnostics;
namespace Plandent.MstscScriptHook.Configurator;
internal sealed class MainForm : Form
{
+ // Global
+ private readonly CheckBox enabledCheckBox = new() { Text = "Plugin-Funktion aktiviert", AutoSize = true };
+ private readonly CheckBox loggingCheckBox = new() { Text = "Plugin-Logging aktivieren", AutoSize = true };
+
+ // Automatischer Start bei RDP-Verbindung
private readonly TextBox scriptPathTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox argumentsTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox workingDirectoryTextBox = new() { Dock = DockStyle.Fill };
-
- private readonly CheckBox enabledCheckBox = new() { Text = "Plugin-Funktion aktiviert", AutoSize = true };
- private readonly CheckBox startOnConnectCheckBox = new() { Text = "Script bei erfolgreicher RDP-Verbindung starten", AutoSize = true };
- private readonly CheckBox stopOnDisconnectCheckBox = new() { Text = "Script bei RDP-Trennung beenden", AutoSize = true };
- private readonly CheckBox hiddenCheckBox = new() { Text = "Script unsichtbar starten", AutoSize = true };
+ private readonly CheckBox startOnConnectCheckBox = new() { Text = "Bei erfolgreicher RDP-Verbindung starten", AutoSize = true };
+ private readonly CheckBox stopOnDisconnectCheckBox = new() { Text = "Bei RDP-Trennung beenden", AutoSize = true };
+ private readonly CheckBox hiddenCheckBox = new() { Text = "Unsichtbar starten", AutoSize = true };
private readonly CheckBox preventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", AutoSize = true };
- private readonly CheckBox loggingCheckBox = new() { Text = "Plugin-Logging aktivieren", AutoSize = true };
+ private readonly Button stopConnectionTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
+ private Process? connectionTestProcess;
+
+ // Separater Start durch Terminalserver-Trigger
+ private readonly TextBox triggerProgramPathTextBox = new() { Dock = DockStyle.Fill };
+ private readonly TextBox triggerArgumentsTextBox = new() { Dock = DockStyle.Fill };
+ private readonly TextBox triggerWorkingDirectoryTextBox = new() { Dock = DockStyle.Fill };
+ private readonly CheckBox triggerEnabledCheckBox = new() { Text = "Start durch Terminalserver-Trigger erlauben", AutoSize = true };
+ private readonly CheckBox triggerHiddenCheckBox = new() { Text = "Unsichtbar starten", AutoSize = true };
+ private readonly CheckBox triggerPreventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", AutoSize = true };
+ private readonly Button stopTriggerTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
+ private Process? triggerTestProcess;
private readonly Label statusLabel = new()
{
@@ -25,20 +39,17 @@ internal sealed class MainForm : Form
Padding = new Padding(8)
};
- private readonly Button stopTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
- private Process? testProcess;
-
public MainForm()
{
Text = "Plandent MSTSC Script Hook";
StartPosition = FormStartPosition.CenterScreen;
- MinimumSize = new Size(760, 590);
- Size = new Size(820, 640);
+ MinimumSize = new Size(820, 720);
+ Size = new Size(900, 800);
Font = new Font("Segoe UI", 9F);
Controls.Add(BuildMainLayout());
Load += (_, _) => LoadConfiguration();
- FormClosed += (_, _) => DisposeTestProcessHandle();
+ FormClosed += (_, _) => DisposeAllTestProcesses();
}
private Control BuildMainLayout()
@@ -52,10 +63,10 @@ internal sealed class MainForm : Form
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
- root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var title = new Label
{
@@ -68,21 +79,201 @@ internal sealed class MainForm : Form
var description = new Label
{
- Text = "Startet bei einer erfolgreichen klassischen Remotedesktop-Verbindung lokal ein konfiguriertes Script im Hintergrund.",
+ Text = "Lokale Programme können automatisch beim RDP-Verbindungsaufbau oder gezielt durch eine Anwendung innerhalb der Terminalserver-Sitzung gestartet werden.",
AutoSize = true,
- MaximumSize = new Size(740, 0),
- Margin = new Padding(0, 0, 0, 16)
+ MaximumSize = new Size(820, 0),
+ Margin = new Padding(0, 0, 0, 12)
};
root.Controls.Add(description, 0, 1);
- root.Controls.Add(BuildFileGrid(), 0, 2);
- root.Controls.Add(BuildOptionsGroup(), 0, 3);
- root.Controls.Add(BuildStatusPanel(), 0, 4);
- root.Controls.Add(BuildButtons(), 0, 5);
+ var scroll = new Panel
+ {
+ Dock = DockStyle.Fill,
+ AutoScroll = true,
+ Margin = new Padding(0, 0, 0, 12)
+ };
+
+ var content = new TableLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ ColumnCount = 1,
+ RowCount = 3
+ };
+ content.Controls.Add(BuildGlobalOptionsGroup(), 0, 0);
+ content.Controls.Add(BuildConnectionProgramGroup(), 0, 1);
+ content.Controls.Add(BuildTriggerProgramGroup(), 0, 2);
+ scroll.Controls.Add(content);
+ root.Controls.Add(scroll, 0, 2);
+
+ root.Controls.Add(BuildStatusPanel(), 0, 3);
+
+ var hint = new Label
+ {
+ Text = "Hinweis: PlandentRdpClientTrigger.exe auf dem Terminalserver übermittelt ausschließlich einen festen START-Befehl. Programmpfad und Parameter werden niemals vom Server übertragen.",
+ AutoSize = true,
+ MaximumSize = new Size(820, 0),
+ Margin = new Padding(0, 0, 0, 10)
+ };
+ root.Controls.Add(hint, 0, 4);
+
+ root.Controls.Add(BuildMainButtons(), 0, 5);
return root;
}
- private Control BuildFileGrid()
+ private Control BuildGlobalOptionsGroup()
+ {
+ var group = CreateGroup("Allgemein");
+ var options = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ FlowDirection = FlowDirection.TopDown,
+ WrapContents = false,
+ AutoSize = true
+ };
+ options.Controls.Add(enabledCheckBox);
+ options.Controls.Add(loggingCheckBox);
+ group.Controls.Add(options);
+ return group;
+ }
+
+ private Control BuildConnectionProgramGroup()
+ {
+ var group = CreateGroup("Automatischer Start bei RDP-Verbindung");
+
+ var layout = new TableLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ ColumnCount = 1,
+ RowCount = 4
+ };
+
+ layout.Controls.Add(new Label
+ {
+ Text = "Optionales lokales Programm/Script, das direkt nach erfolgreichem Aufbau der klassischen MSTSC-Verbindung gestartet wird.",
+ AutoSize = true,
+ MaximumSize = new Size(780, 0),
+ Margin = new Padding(0, 0, 0, 8)
+ }, 0, 0);
+
+ layout.Controls.Add(BuildFileGrid(
+ scriptPathTextBox,
+ argumentsTextBox,
+ workingDirectoryTextBox,
+ () => BrowseProgram(scriptPathTextBox, workingDirectoryTextBox, useRemoteVddsPreset: true),
+ () => BrowseWorkingDirectory(workingDirectoryTextBox)), 0, 1);
+
+ var options = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ FlowDirection = FlowDirection.LeftToRight,
+ WrapContents = true,
+ AutoSize = true,
+ Margin = new Padding(0, 4, 0, 6)
+ };
+ options.Controls.Add(startOnConnectCheckBox);
+ options.Controls.Add(stopOnDisconnectCheckBox);
+ options.Controls.Add(hiddenCheckBox);
+ options.Controls.Add(preventDuplicatesCheckBox);
+ layout.Controls.Add(options, 0, 2);
+
+ var testButtons = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ FlowDirection = FlowDirection.LeftToRight,
+ WrapContents = true,
+ AutoSize = true,
+ Margin = new Padding(0)
+ };
+ var testButton = new Button { Text = "Verbindungs-Programm testen", AutoSize = true };
+ testButton.Click += (_, _) => StartConnectionTest();
+ stopConnectionTestButton.Click += (_, _) => StopConnectionTest(silent: false);
+ testButtons.Controls.Add(testButton);
+ testButtons.Controls.Add(stopConnectionTestButton);
+ layout.Controls.Add(testButtons, 0, 3);
+
+ group.Controls.Add(layout);
+ return group;
+ }
+
+ private Control BuildTriggerProgramGroup()
+ {
+ var group = CreateGroup("Programmstart durch Terminalserver");
+
+ var layout = new TableLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ ColumnCount = 1,
+ RowCount = 4
+ };
+
+ layout.Controls.Add(new Label
+ {
+ Text = "Optionales lokales Programm/Script mit eigenen Parametern. Es wird nur gestartet, wenn PlandentRdpClientTrigger.exe innerhalb derselben RDP-Sitzung auf dem Terminalserver ausgeführt wird.",
+ AutoSize = true,
+ MaximumSize = new Size(780, 0),
+ Margin = new Padding(0, 0, 0, 8)
+ }, 0, 0);
+
+ layout.Controls.Add(BuildFileGrid(
+ triggerProgramPathTextBox,
+ triggerArgumentsTextBox,
+ triggerWorkingDirectoryTextBox,
+ () => BrowseProgram(triggerProgramPathTextBox, triggerWorkingDirectoryTextBox, useRemoteVddsPreset: false),
+ () => BrowseWorkingDirectory(triggerWorkingDirectoryTextBox)), 0, 1);
+
+ var options = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ FlowDirection = FlowDirection.LeftToRight,
+ WrapContents = true,
+ AutoSize = true,
+ Margin = new Padding(0, 4, 0, 6)
+ };
+ options.Controls.Add(triggerEnabledCheckBox);
+ options.Controls.Add(triggerHiddenCheckBox);
+ options.Controls.Add(triggerPreventDuplicatesCheckBox);
+ layout.Controls.Add(options, 0, 2);
+
+ var testButtons = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ FlowDirection = FlowDirection.LeftToRight,
+ WrapContents = true,
+ AutoSize = true,
+ Margin = new Padding(0)
+ };
+ var testButton = new Button { Text = "Trigger-Programm testen", AutoSize = true };
+ testButton.Click += (_, _) => StartTriggerTest();
+ stopTriggerTestButton.Click += (_, _) => StopTriggerTest(silent: false);
+ testButtons.Controls.Add(testButton);
+ testButtons.Controls.Add(stopTriggerTestButton);
+ layout.Controls.Add(testButtons, 0, 3);
+
+ group.Controls.Add(layout);
+ return group;
+ }
+
+ private static GroupBox CreateGroup(string title)
+ {
+ return new GroupBox
+ {
+ Text = title,
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ Padding = new Padding(12),
+ Margin = new Padding(0, 0, 0, 12)
+ };
+ }
+
+ private static Control BuildFileGrid(
+ TextBox pathTextBox,
+ TextBox argumentsTextBox,
+ TextBox workingDirectoryTextBox,
+ Action browseProgram,
+ Action browseWorkingDirectory)
{
var grid = new TableLayoutPanel
{
@@ -90,17 +281,17 @@ internal sealed class MainForm : Form
AutoSize = true,
ColumnCount = 3,
RowCount = 3,
- Margin = new Padding(0, 0, 0, 14)
+ Margin = new Padding(0)
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
- grid.Controls.Add(CreateFieldLabel("Script:"), 0, 0);
- grid.Controls.Add(scriptPathTextBox, 1, 0);
- var browseScriptButton = new Button { Text = "…", Width = 42, Height = scriptPathTextBox.PreferredHeight + 4 };
- browseScriptButton.Click += (_, _) => BrowseScript();
- grid.Controls.Add(browseScriptButton, 2, 0);
+ grid.Controls.Add(CreateFieldLabel("Programm/Script:"), 0, 0);
+ grid.Controls.Add(pathTextBox, 1, 0);
+ var browseProgramButton = new Button { Text = "…", Width = 42, Height = pathTextBox.PreferredHeight + 4 };
+ browseProgramButton.Click += (_, _) => browseProgram();
+ grid.Controls.Add(browseProgramButton, 2, 0);
grid.Controls.Add(CreateFieldLabel("Argumente:"), 0, 1);
grid.Controls.Add(argumentsTextBox, 1, 1);
@@ -109,7 +300,7 @@ internal sealed class MainForm : Form
grid.Controls.Add(CreateFieldLabel("Arbeitsverzeichnis:"), 0, 2);
grid.Controls.Add(workingDirectoryTextBox, 1, 2);
var browseWorkingButton = new Button { Text = "…", Width = 42, Height = workingDirectoryTextBox.PreferredHeight + 4 };
- browseWorkingButton.Click += (_, _) => BrowseWorkingDirectory();
+ browseWorkingButton.Click += (_, _) => browseWorkingDirectory();
grid.Controls.Add(browseWorkingButton, 2, 2);
return grid;
@@ -126,48 +317,20 @@ internal sealed class MainForm : Form
};
}
- private Control BuildOptionsGroup()
- {
- var group = new GroupBox
- {
- Text = "Verhalten",
- Dock = DockStyle.Top,
- AutoSize = true,
- Padding = new Padding(12),
- Margin = new Padding(0, 0, 0, 14)
- };
-
- var options = new FlowLayoutPanel
- {
- Dock = DockStyle.Top,
- FlowDirection = FlowDirection.TopDown,
- WrapContents = false,
- AutoSize = true
- };
- options.Controls.Add(enabledCheckBox);
- options.Controls.Add(startOnConnectCheckBox);
- options.Controls.Add(stopOnDisconnectCheckBox);
- options.Controls.Add(hiddenCheckBox);
- options.Controls.Add(preventDuplicatesCheckBox);
- options.Controls.Add(loggingCheckBox);
- group.Controls.Add(options);
- return group;
- }
-
private Control BuildStatusPanel()
{
var panel = new Panel
{
Dock = DockStyle.Top,
- Height = 58,
+ Height = 70,
BorderStyle = BorderStyle.FixedSingle,
- Margin = new Padding(0, 0, 0, 14)
+ Margin = new Padding(0, 0, 0, 10)
};
panel.Controls.Add(statusLabel);
return panel;
}
- private Control BuildButtons()
+ private Control BuildMainButtons()
{
var buttons = new FlowLayoutPanel
{
@@ -178,10 +341,6 @@ internal sealed class MainForm : Form
Margin = new Padding(0)
};
- var testButton = new Button { Text = "Script testen", AutoSize = true };
- testButton.Click += (_, _) => StartTest();
- stopTestButton.Click += (_, _) => StopTest();
-
var saveButton = new Button { Text = "Konfiguration speichern", AutoSize = true };
saveButton.Click += (_, _) => SaveConfiguration(showConfirmation: true);
@@ -194,8 +353,6 @@ internal sealed class MainForm : Form
var logButton = new Button { Text = "Log öffnen", AutoSize = true };
logButton.Click += (_, _) => OpenLog();
- buttons.Controls.Add(testButton);
- buttons.Controls.Add(stopTestButton);
buttons.Controls.Add(saveButton);
buttons.Controls.Add(installButton);
buttons.Controls.Add(removeButton);
@@ -203,18 +360,18 @@ internal sealed class MainForm : Form
return buttons;
}
- private void BrowseScript()
+ private void BrowseProgram(TextBox pathTextBox, TextBox workingDirectoryTextBox, bool useRemoteVddsPreset)
{
using var dialog = new OpenFileDialog
{
- Title = "Script oder Programm auswählen",
- Filter = "Unterstützte Dateien (*.bat;*.cmd;*.ps1;*.exe)|*.bat;*.cmd;*.ps1;*.exe|Alle Dateien (*.*)|*.*",
+ Title = "Programm oder Script auswählen",
+ Filter = "Unterstützte Dateien (*.exe;*.bat;*.cmd;*.ps1)|*.exe;*.bat;*.cmd;*.ps1|Alle Dateien (*.*)|*.*",
CheckFileExists = true
};
- if (!string.IsNullOrWhiteSpace(scriptPathTextBox.Text))
+ if (!string.IsNullOrWhiteSpace(pathTextBox.Text))
{
- string expanded = Environment.ExpandEnvironmentVariables(scriptPathTextBox.Text);
+ string expanded = Environment.ExpandEnvironmentVariables(pathTextBox.Text);
if (File.Exists(expanded))
{
dialog.InitialDirectory = Path.GetDirectoryName(expanded);
@@ -227,20 +384,21 @@ internal sealed class MainForm : Form
return;
}
- scriptPathTextBox.Text = dialog.FileName;
+ pathTextBox.Text = dialog.FileName;
if (string.IsNullOrWhiteSpace(workingDirectoryTextBox.Text))
{
workingDirectoryTextBox.Text = Path.GetDirectoryName(dialog.FileName) ?? string.Empty;
}
- if (string.IsNullOrWhiteSpace(argumentsTextBox.Text) &&
+ if (useRemoteVddsPreset &&
+ string.IsNullOrWhiteSpace(argumentsTextBox.Text) &&
Path.GetFileName(dialog.FileName).Contains("RemoteVDDS_Receiver_TS", StringComparison.OrdinalIgnoreCase))
{
argumentsTextBox.Text = "MIN";
}
}
- private void BrowseWorkingDirectory()
+ private void BrowseWorkingDirectory(TextBox target)
{
using var dialog = new FolderBrowserDialog
{
@@ -249,7 +407,7 @@ internal sealed class MainForm : Form
ShowNewFolderButton = true
};
- string expanded = Environment.ExpandEnvironmentVariables(workingDirectoryTextBox.Text);
+ string expanded = Environment.ExpandEnvironmentVariables(target.Text);
if (Directory.Exists(expanded))
{
dialog.SelectedPath = expanded;
@@ -257,7 +415,7 @@ internal sealed class MainForm : Form
if (dialog.ShowDialog(this) == DialogResult.OK)
{
- workingDirectoryTextBox.Text = dialog.SelectedPath;
+ target.Text = dialog.SelectedPath;
}
}
@@ -266,6 +424,8 @@ internal sealed class MainForm : Form
return new HookConfig
{
Enabled = enabledCheckBox.Checked,
+ EnableLogging = loggingCheckBox.Checked,
+
ScriptPath = scriptPathTextBox.Text.Trim(),
Arguments = argumentsTextBox.Text.Trim(),
WorkingDirectory = workingDirectoryTextBox.Text.Trim(),
@@ -273,22 +433,37 @@ internal sealed class MainForm : Form
StopOnDisconnect = stopOnDisconnectCheckBox.Checked,
Hidden = hiddenCheckBox.Checked,
PreventDuplicates = preventDuplicatesCheckBox.Checked,
- EnableLogging = loggingCheckBox.Checked
+
+ TriggerEnabled = triggerEnabledCheckBox.Checked,
+ TriggerProgramPath = triggerProgramPathTextBox.Text.Trim(),
+ TriggerArguments = triggerArgumentsTextBox.Text.Trim(),
+ TriggerWorkingDirectory = triggerWorkingDirectoryTextBox.Text.Trim(),
+ TriggerHidden = triggerHiddenCheckBox.Checked,
+ TriggerPreventDuplicates = triggerPreventDuplicatesCheckBox.Checked
};
}
private void LoadConfiguration()
{
HookConfig config = RegistryConfig.Load();
+ enabledCheckBox.Checked = config.Enabled;
+ loggingCheckBox.Checked = config.EnableLogging;
+
scriptPathTextBox.Text = config.ScriptPath;
argumentsTextBox.Text = config.Arguments;
workingDirectoryTextBox.Text = config.WorkingDirectory;
- enabledCheckBox.Checked = config.Enabled;
startOnConnectCheckBox.Checked = config.StartOnConnect;
stopOnDisconnectCheckBox.Checked = config.StopOnDisconnect;
hiddenCheckBox.Checked = config.Hidden;
preventDuplicatesCheckBox.Checked = config.PreventDuplicates;
- loggingCheckBox.Checked = config.EnableLogging;
+
+ triggerEnabledCheckBox.Checked = config.TriggerEnabled;
+ triggerProgramPathTextBox.Text = config.TriggerProgramPath;
+ triggerArgumentsTextBox.Text = config.TriggerArguments;
+ triggerWorkingDirectoryTextBox.Text = config.TriggerWorkingDirectory;
+ triggerHiddenCheckBox.Checked = config.TriggerHidden;
+ triggerPreventDuplicatesCheckBox.Checked = config.TriggerPreventDuplicates;
+
RefreshStatus();
}
@@ -297,7 +472,7 @@ internal sealed class MainForm : Form
try
{
HookConfig config = ReadForm();
- ValidateConfig(config, requireScript: false);
+ ValidateConfiguredTargets(config, installationValidation: false);
RegistryConfig.Save(config);
if (showConfirmation)
{
@@ -318,14 +493,14 @@ internal sealed class MainForm : Form
try
{
HookConfig config = ReadForm();
- ValidateConfig(config, requireScript: true);
+ ValidateConfiguredTargets(config, installationValidation: true);
RegistryConfig.Save(config);
PluginInstaller.Install();
RefreshStatus();
MessageBox.Show(
this,
"Plugin wurde für den aktuellen Benutzer installiert und die Konfiguration gespeichert.\n\n" +
- "Bereits geöffnete mstsc.exe-Fenster vollständig schließen und anschließend neu starten, damit die DLL geladen wird.",
+ "Bereits geöffnete mstsc.exe-Fenster vollständig schließen und anschließend neu starten, damit die neue DLL geladen wird.",
"Installation abgeschlossen",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
@@ -348,63 +523,92 @@ internal sealed class MainForm : Form
result.FullyRemoved ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
}
- private void StartTest()
+ private void StartConnectionTest()
{
try
{
- StopTest(silent: true);
+ StopConnectionTest(silent: true);
HookConfig config = ReadForm();
- ValidateConfig(config, requireScript: true);
- testProcess = ScriptTestLauncher.Start(config);
- stopTestButton.Enabled = true;
- statusLabel.Text = $"Testprozess gestartet: PID {testProcess.Id}. Der Test läuft unabhängig von MSTSC.";
+ ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true);
+ connectionTestProcess = ScriptTestLauncher.StartConnectionProgram(config);
+ stopConnectionTestButton.Enabled = true;
+ statusLabel.Text = $"Verbindungs-Test gestartet: PID {connectionTestProcess.Id}. Der Test läuft unabhängig von MSTSC.";
}
catch (Exception ex)
{
- MessageBox.Show(this, ex.Message, "Script-Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ MessageBox.Show(this, ex.Message, "Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
- private void StopTest() => StopTest(silent: false);
-
- private void StopTest(bool silent)
+ private void StartTriggerTest()
{
- if (testProcess is null)
+ try
{
- stopTestButton.Enabled = false;
+ StopTriggerTest(silent: true);
+ HookConfig config = ReadForm();
+ ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: true);
+ triggerTestProcess = ScriptTestLauncher.StartTriggerProgram(config);
+ stopTriggerTestButton.Enabled = true;
+ statusLabel.Text = $"Trigger-Programm-Test gestartet: PID {triggerTestProcess.Id}. Der Test läuft ohne Terminalserver-Trigger.";
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, ex.Message, "Trigger-Programm-Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ private void StopConnectionTest(bool silent)
+ {
+ StopTestProcess(ref connectionTestProcess, stopConnectionTestButton, "Verbindungs-Test", silent);
+ }
+
+ private void StopTriggerTest(bool silent)
+ {
+ StopTestProcess(ref triggerTestProcess, stopTriggerTestButton, "Trigger-Programm-Test", silent);
+ }
+
+ private void StopTestProcess(ref Process? process, Button button, string description, bool silent)
+ {
+ if (process is null)
+ {
+ button.Enabled = false;
return;
}
try
{
- if (!testProcess.HasExited)
+ if (!process.HasExited)
{
- testProcess.Kill(entireProcessTree: true);
- testProcess.WaitForExit(3000);
+ process.Kill(entireProcessTree: true);
+ process.WaitForExit(3000);
}
+
if (!silent)
{
- statusLabel.Text = "Testprozess wurde beendet.";
+ statusLabel.Text = description + " wurde beendet.";
}
}
catch (Exception ex)
{
if (!silent)
{
- MessageBox.Show(this, ex.Message, "Testprozess konnte nicht beendet werden", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ MessageBox.Show(this, ex.Message, description + " konnte nicht beendet werden", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
finally
{
- DisposeTestProcessHandle();
+ process?.Dispose();
+ process = null;
+ button.Enabled = false;
}
}
- private void DisposeTestProcessHandle()
+ private void DisposeAllTestProcesses()
{
- testProcess?.Dispose();
- testProcess = null;
- stopTestButton.Enabled = false;
+ connectionTestProcess?.Dispose();
+ triggerTestProcess?.Dispose();
+ connectionTestProcess = null;
+ triggerTestProcess = null;
}
private void OpenLog()
@@ -426,42 +630,69 @@ internal sealed class MainForm : Form
private void RefreshStatus()
{
bool installed = PluginInstaller.IsInstalled(out string pluginStatus);
- string configStatus = string.IsNullOrWhiteSpace(scriptPathTextBox.Text)
- ? " Kein Script konfiguriert."
- : $" Script: {scriptPathTextBox.Text}";
- statusLabel.Text = (installed ? "✓ " : "○ ") + pluginStatus + configStatus;
+
+ string connectStatus = startOnConnectCheckBox.Checked
+ ? (string.IsNullOrWhiteSpace(scriptPathTextBox.Text) ? " RDP-Start: nicht konfiguriert." : " RDP-Start: konfiguriert.")
+ : " RDP-Start: aus.";
+
+ string triggerStatus = triggerEnabledCheckBox.Checked
+ ? (string.IsNullOrWhiteSpace(triggerProgramPathTextBox.Text) ? " Server-Trigger: nicht konfiguriert." : " Server-Trigger: konfiguriert.")
+ : " Server-Trigger: aus.";
+
+ statusLabel.Text = (installed ? "✓ " : "○ ") + pluginStatus + connectStatus + triggerStatus;
}
- private static void ValidateConfig(HookConfig config, bool requireScript)
+ private static void ValidateConfiguredTargets(HookConfig config, bool installationValidation)
{
- if (!requireScript && string.IsNullOrWhiteSpace(config.ScriptPath))
+ // Bereits ausgefüllte Pfade immer prüfen, damit Tippfehler nicht gespeichert werden.
+ ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: false);
+ ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: false);
+
+ if (!installationValidation || !config.Enabled)
{
return;
}
- string scriptPath = Environment.ExpandEnvironmentVariables(config.ScriptPath);
- if (string.IsNullOrWhiteSpace(scriptPath))
+ if (config.StartOnConnect)
{
- throw new InvalidOperationException("Bitte ein Script auswählen.");
+ ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true);
}
- if (!File.Exists(scriptPath))
+ if (config.TriggerEnabled)
{
- throw new FileNotFoundException("Das ausgewählte Script wurde nicht gefunden.", scriptPath);
+ ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: true);
+ }
+ }
+
+ private static void ValidateLaunchTarget(string configuredPath, string configuredWorkingDirectory, string displayName, bool required)
+ {
+ string path = Environment.ExpandEnvironmentVariables(configuredPath ?? string.Empty);
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ if (required)
+ {
+ throw new InvalidOperationException($"Bitte für „{displayName}“ ein Programm oder Script auswählen.");
+ }
+ return;
}
- string extension = Path.GetExtension(scriptPath).ToLowerInvariant();
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"{displayName} wurde nicht gefunden.", path);
+ }
+
+ string extension = Path.GetExtension(path).ToLowerInvariant();
if (extension is not (".bat" or ".cmd" or ".ps1" or ".exe"))
{
- throw new NotSupportedException("Unterstützt werden .bat, .cmd, .ps1 und .exe.");
+ throw new NotSupportedException($"{displayName}: Unterstützt werden .bat, .cmd, .ps1 und .exe.");
}
- if (!string.IsNullOrWhiteSpace(config.WorkingDirectory))
+ if (!string.IsNullOrWhiteSpace(configuredWorkingDirectory))
{
- string workDir = Environment.ExpandEnvironmentVariables(config.WorkingDirectory);
+ string workDir = Environment.ExpandEnvironmentVariables(configuredWorkingDirectory);
if (!Directory.Exists(workDir))
{
- throw new DirectoryNotFoundException("Das Arbeitsverzeichnis wurde nicht gefunden: " + workDir);
+ throw new DirectoryNotFoundException($"Arbeitsverzeichnis für „{displayName}“ wurde nicht gefunden: {workDir}");
}
}
}
diff --git a/Configurator/RegistryConfig.cs b/Configurator/RegistryConfig.cs
index 9acfd0c..393f13e 100644
--- a/Configurator/RegistryConfig.cs
+++ b/Configurator/RegistryConfig.cs
@@ -17,6 +17,8 @@ internal static class RegistryConfig
}
config.Enabled = ReadBool(key, "Enabled", true);
+ config.EnableLogging = ReadBool(key, "EnableLogging", true);
+
config.ScriptPath = ReadString(key, "ScriptPath");
config.Arguments = ReadString(key, "Arguments");
config.WorkingDirectory = ReadString(key, "WorkingDirectory");
@@ -24,7 +26,13 @@ internal static class RegistryConfig
config.StopOnDisconnect = ReadBool(key, "StopOnDisconnect", false);
config.Hidden = ReadBool(key, "Hidden", true);
config.PreventDuplicates = ReadBool(key, "PreventDuplicates", true);
- config.EnableLogging = ReadBool(key, "EnableLogging", true);
+
+ config.TriggerEnabled = ReadBool(key, "TriggerEnabled", false);
+ config.TriggerProgramPath = ReadString(key, "TriggerProgramPath");
+ config.TriggerArguments = ReadString(key, "TriggerArguments");
+ config.TriggerWorkingDirectory = ReadString(key, "TriggerWorkingDirectory");
+ config.TriggerHidden = ReadBool(key, "TriggerHidden", true);
+ config.TriggerPreventDuplicates = ReadBool(key, "TriggerPreventDuplicates", true);
return config;
}
@@ -34,6 +42,8 @@ internal static class RegistryConfig
?? throw new InvalidOperationException("Registry-Schlüssel konnte nicht erstellt werden.");
key.SetValue("Enabled", config.Enabled ? 1 : 0, RegistryValueKind.DWord);
+ key.SetValue("EnableLogging", config.EnableLogging ? 1 : 0, RegistryValueKind.DWord);
+
key.SetValue("ScriptPath", config.ScriptPath ?? string.Empty, RegistryValueKind.ExpandString);
key.SetValue("Arguments", config.Arguments ?? string.Empty, RegistryValueKind.String);
key.SetValue("WorkingDirectory", config.WorkingDirectory ?? string.Empty, RegistryValueKind.ExpandString);
@@ -41,7 +51,13 @@ internal static class RegistryConfig
key.SetValue("StopOnDisconnect", config.StopOnDisconnect ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("Hidden", config.Hidden ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("PreventDuplicates", config.PreventDuplicates ? 1 : 0, RegistryValueKind.DWord);
- key.SetValue("EnableLogging", config.EnableLogging ? 1 : 0, RegistryValueKind.DWord);
+
+ key.SetValue("TriggerEnabled", config.TriggerEnabled ? 1 : 0, RegistryValueKind.DWord);
+ key.SetValue("TriggerProgramPath", config.TriggerProgramPath ?? string.Empty, RegistryValueKind.ExpandString);
+ key.SetValue("TriggerArguments", config.TriggerArguments ?? string.Empty, RegistryValueKind.String);
+ key.SetValue("TriggerWorkingDirectory", config.TriggerWorkingDirectory ?? string.Empty, RegistryValueKind.ExpandString);
+ key.SetValue("TriggerHidden", config.TriggerHidden ? 1 : 0, RegistryValueKind.DWord);
+ key.SetValue("TriggerPreventDuplicates", config.TriggerPreventDuplicates ? 1 : 0, RegistryValueKind.DWord);
}
public static string? GetRegisteredPluginPath()
diff --git a/Configurator/ScriptTestLauncher.cs b/Configurator/ScriptTestLauncher.cs
index f65d7c1..759c232 100644
--- a/Configurator/ScriptTestLauncher.cs
+++ b/Configurator/ScriptTestLauncher.cs
@@ -4,24 +4,36 @@ namespace Plandent.MstscScriptHook.Configurator;
internal static class ScriptTestLauncher
{
- public static Process Start(HookConfig config)
+ public static Process StartConnectionProgram(HookConfig config) => Start(
+ config.ScriptPath,
+ config.Arguments,
+ config.WorkingDirectory,
+ config.Hidden);
+
+ public static Process StartTriggerProgram(HookConfig config) => Start(
+ config.TriggerProgramPath,
+ config.TriggerArguments,
+ config.TriggerWorkingDirectory,
+ config.TriggerHidden);
+
+ private static Process Start(string configuredPath, string configuredArguments, string configuredWorkingDirectory, bool hidden)
{
- string scriptPath = Environment.ExpandEnvironmentVariables(config.ScriptPath ?? string.Empty);
- if (string.IsNullOrWhiteSpace(scriptPath) || !File.Exists(scriptPath))
+ string programPath = Environment.ExpandEnvironmentVariables(configuredPath ?? string.Empty);
+ if (string.IsNullOrWhiteSpace(programPath) || !File.Exists(programPath))
{
- throw new FileNotFoundException("Das ausgewählte Script wurde nicht gefunden.", scriptPath);
+ throw new FileNotFoundException("Das ausgewählte Programm/Script wurde nicht gefunden.", programPath);
}
- string extension = Path.GetExtension(scriptPath).ToLowerInvariant();
- string workingDirectory = string.IsNullOrWhiteSpace(config.WorkingDirectory)
- ? Path.GetDirectoryName(scriptPath) ?? Environment.CurrentDirectory
- : Environment.ExpandEnvironmentVariables(config.WorkingDirectory);
+ string extension = Path.GetExtension(programPath).ToLowerInvariant();
+ string workingDirectory = string.IsNullOrWhiteSpace(configuredWorkingDirectory)
+ ? Path.GetDirectoryName(programPath) ?? Environment.CurrentDirectory
+ : Environment.ExpandEnvironmentVariables(configuredWorkingDirectory);
var startInfo = new ProcessStartInfo
{
UseShellExecute = false,
- CreateNoWindow = config.Hidden,
- WindowStyle = config.Hidden ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal,
+ CreateNoWindow = hidden,
+ WindowStyle = hidden ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal,
WorkingDirectory = workingDirectory
};
@@ -34,7 +46,7 @@ internal static class ScriptTestLauncher
startInfo.ArgumentList.Add("/D");
startInfo.ArgumentList.Add("/S");
startInfo.ArgumentList.Add("/C");
- startInfo.ArgumentList.Add(BuildCmdCommand(scriptPath, config.Arguments));
+ startInfo.ArgumentList.Add(BuildCmdCommand(programPath, configuredArguments));
break;
case ".ps1":
@@ -49,13 +61,13 @@ internal static class ScriptTestLauncher
startInfo.ArgumentList.Add("-ExecutionPolicy");
startInfo.ArgumentList.Add("Bypass");
startInfo.ArgumentList.Add("-File");
- startInfo.ArgumentList.Add(scriptPath);
- AddRawArguments(startInfo, config.Arguments);
+ startInfo.ArgumentList.Add(programPath);
+ AddRawArguments(startInfo, configuredArguments);
break;
case ".exe":
- startInfo.FileName = scriptPath;
- AddRawArguments(startInfo, config.Arguments);
+ startInfo.FileName = programPath;
+ AddRawArguments(startInfo, configuredArguments);
break;
default:
@@ -66,9 +78,9 @@ internal static class ScriptTestLauncher
?? throw new InvalidOperationException("Der Testprozess konnte nicht gestartet werden.");
}
- private static string BuildCmdCommand(string scriptPath, string? arguments)
+ private static string BuildCmdCommand(string programPath, string? arguments)
{
- string command = QuoteForCmd(scriptPath);
+ string command = QuoteForCmd(programPath);
if (!string.IsNullOrWhiteSpace(arguments))
{
command += " " + arguments;
@@ -81,9 +93,6 @@ internal static class ScriptTestLauncher
return "\"" + value.Replace("\"", "\\\"") + "\"";
}
- // Die GUI behandelt das Argumentfeld bewusst als Kommandozeilen-Argumentstring.
- // Für den Test wird eine einfache Windows-Argumentzerlegung verwendet. Das Plugin selbst
- // übergibt den String unverändert an cmd.exe/PowerShell bzw. an die EXE-Kommandozeile.
private static void AddRawArguments(ProcessStartInfo startInfo, string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
diff --git a/PlandentMstscScriptHook.sln b/PlandentMstscScriptHook.sln
index d018b54..4b6d3f6 100644
--- a/PlandentMstscScriptHook.sln
+++ b/PlandentMstscScriptHook.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlandentMstscScriptHook.Con
{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA} = {D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}
EndProjectSection
EndProject
+Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "PlandentRdpClientTrigger", "ServerTrigger\PlandentRdpClientTrigger.vcxproj", "{A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -23,6 +25,10 @@ Global
{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Debug|x64.Build.0 = Debug|x64
{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Release|x64.ActiveCfg = Release|x64
{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Release|x64.Build.0 = Release|x64
+ {A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}.Debug|x64.ActiveCfg = Debug|x64
+ {A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}.Debug|x64.Build.0 = Debug|x64
+ {A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}.Release|x64.ActiveCfg = Release|x64
+ {A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Plugin/PlandentMstscScriptHook.vcxproj b/Plugin/PlandentMstscScriptHook.vcxproj
index 89850ec..ec915af 100644
--- a/Plugin/PlandentMstscScriptHook.vcxproj
+++ b/Plugin/PlandentMstscScriptHook.vcxproj
@@ -94,6 +94,7 @@
+
diff --git a/Plugin/PlandentMstscScriptHook.vcxproj.filters b/Plugin/PlandentMstscScriptHook.vcxproj.filters
index 037f4ff..3abf9ad 100644
--- a/Plugin/PlandentMstscScriptHook.vcxproj.filters
+++ b/Plugin/PlandentMstscScriptHook.vcxproj.filters
@@ -16,5 +16,6 @@
Header Files
Header Files
Header Files
+ Header Files
diff --git a/Plugin/ProcessLauncher.cpp b/Plugin/ProcessLauncher.cpp
index 2fc9506..764a8b0 100644
--- a/Plugin/ProcessLauncher.cpp
+++ b/Plugin/ProcessLauncher.cpp
@@ -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;
diff --git a/Plugin/ProcessLauncher.h b/Plugin/ProcessLauncher.h
index 7755be5..3ea14bf 100644
--- a/Plugin/ProcessLauncher.h
+++ b/Plugin/ProcessLauncher.h
@@ -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:
diff --git a/Plugin/RegistryConfig.cpp b/Plugin/RegistryConfig.cpp
index 23aa062..6f0db9c 100644
--- a/Plugin/RegistryConfig.cpp
+++ b/Plugin/RegistryConfig.cpp
@@ -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;
diff --git a/Plugin/RegistryConfig.h b/Plugin/RegistryConfig.h
index 78e152b..00b1766 100644
--- a/Plugin/RegistryConfig.h
+++ b/Plugin/RegistryConfig.h
@@ -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();
diff --git a/Plugin/ScriptHookPlugin.cpp b/Plugin/ScriptHookPlugin.cpp
index eaa5f7d..1cff37d 100644
--- a/Plugin/ScriptHookPlugin.cpp
+++ b/Plugin/ScriptHookPlugin.cpp
@@ -2,9 +2,109 @@
#include "Logging.h"
#include "RegistryConfig.h"
+#include "../Shared/TriggerProtocol.h"
+
+#include
+#include
+#include
+
+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(this);
+ AddRef();
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+ }
+
+ ULONG STDMETHODCALLTYPE AddRef() override
+ {
+ return static_cast(InterlockedIncrement(&refCount_));
+ }
+
+ ULONG STDMETHODCALLTYPE Release() override
+ {
+ ULONG remaining = static_cast(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(this);
- AddRef();
- return S_OK;
+ }
+ else if (riid == __uuidof(IWTSListenerCallback))
+ {
+ *ppvObject = static_cast(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(hook::protocol::kChannelName),
+ 0,
+ static_cast(this),
+ &listener_);
+
+ if (FAILED(hr))
+ {
+ hook::Log(config.enableLogging,
+ L"DVC-Listener konnte nicht erstellt werden. HRESULT=" + std::to_wstring(static_cast(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(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;
+ }
+}
diff --git a/Plugin/ScriptHookPlugin.h b/Plugin/ScriptHookPlugin.h
index 450797d..4bce05f 100644
--- a/Plugin/ScriptHookPlugin.h
+++ b/Plugin/ScriptHookPlugin.h
@@ -4,7 +4,7 @@
#include
#include
-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;
};
diff --git a/README.md b/README.md
index f5653a3..af2b31d 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,64 @@
# Plandent MSTSC Script Hook
-Kleines Windows-Projekt, das beim erfolgreichen Aufbau einer klassischen RDP-Verbindung mit `mstsc.exe` lokal ein konfiguriertes Script startet.
+Windows-Projekt für den klassischen Microsoft Remote Desktop Connection Client (`mstsc.exe`). Das Client-Plugin kann lokale Programme auf zwei voneinander getrennten Wegen starten:
+
+1. **Automatisch beim erfolgreichen RDP-Verbindungsaufbau**.
+2. **Gezielt durch `PlandentRdpClientTrigger.exe` innerhalb der Terminalserver-Sitzung**.
+
+Der Terminalserver überträgt dabei **keinen Programmpfad und keine Argumente**. Die Server-EXE sendet ausschließlich einen festen `START`-Befehl über einen RDP Dynamic Virtual Channel (DVC). Welches lokale Programm mit welchen Parametern gestartet wird, wird ausschließlich auf dem Client in der Registry konfiguriert.
## Komponenten
-- **PlandentMstscScriptHook.dll** – native x64 DVC-Plugin-DLL für den Remote Desktop Connection Client.
+### Client
+
+- **PlandentMstscScriptHook.dll** – native x64 DVC-Plugin-DLL, die von `mstsc.exe` geladen wird.
- **PlandentMstscScriptHook.exe** – WinForms-Konfigurator/Installer. Die Release-EXE enthält die DLL als Resource und extrahiert sie bei der Installation.
-## Verhalten
+### Terminalserver
-Die DLL implementiert `IWTSPlugin` und exportiert `VirtualChannelGetInstance`.
+- **PlandentRdpClientTrigger.exe** – kleine native x64 Windows-EXE ohne GUI und ohne Parameter. Sie wird **innerhalb der betreffenden RDP-Benutzersitzung** ausgeführt und sendet den festen Start-Trigger an den Client dieser Sitzung.
-- `Initialize()` – Plugin wird von MSTSC initialisiert.
-- `Connected()` – liest die Konfiguration und startet das lokale Script.
-- `Disconnected()` – beendet das von dieser Plugin-Instanz gestartete Script, wenn `StopOnDisconnect=1` gesetzt ist.
-- `Terminated()` – bereinigt Prozesshandles; bei `StopOnDisconnect=0` läuft das Script weiter.
+## Architektur
-Der Plugin-Callback wartet **nicht** auf das Script. Es wird nur mit `CreateProcessW` gestartet und der Callback kehrt direkt zurück.
+```text
+LOKALER CLIENT TERMINALSERVER
+================ ==============
+
+mstsc.exe
+ │
+ └── PlandentMstscScriptHook.dll
+ │
+ ├── Connected()
+ │ └── optional: lokales RDP-Verbindungsprogramm
+ │
+ └── DVC Listener
+ plandent::mstsc-script-hook
+ ▲
+ │ fester Befehl:
+ │ PLANDENT_MSTSC_HOOK/1 START
+ │
+ └──────────────────── PlandentRdpClientTrigger.exe
+ │
+ └── läuft in der RDP-Sitzung
+
+Nach START liest die DLL ausschließlich lokal:
+HKCU\Software\Plandent\MstscScriptHook
+ TriggerProgramPath
+ TriggerArguments
+ TriggerWorkingDirectory
+```
+
+## Sicherheit des Triggers
+
+Die Server-Komponente ist absichtlich minimal gehalten:
+
+- keine Kommandozeilenparameter für das Client-Programm,
+- kein Pfad wird vom Terminalserver übertragen,
+- keine beliebigen Befehle werden akzeptiert,
+- ausschließlich die exakte Protokollnachricht `PLANDENT_MSTSC_HOOK/1 START` wird verarbeitet,
+- Programmpfad und Argumente stammen nur aus der lokalen HKCU-Konfiguration des angemeldeten Client-Benutzers.
+
+Damit kann `PlandentRdpClientTrigger.exe` nur den vorher auf dem Client festgelegten Start auslösen.
## Registry
@@ -31,30 +73,110 @@ Konfiguration:
```text
HKCU\Software\Plandent\MstscScriptHook
- Enabled REG_DWORD 1
- ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
- Arguments REG_SZ MIN
- WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS
- StartOnConnect REG_DWORD 1
- StopOnDisconnect REG_DWORD 0
- Hidden REG_DWORD 1
- PreventDuplicates REG_DWORD 1
- EnableLogging REG_DWORD 1
+ Enabled REG_DWORD 1
+ EnableLogging REG_DWORD 1
+
+ ; Automatischer Start bei RDP-Verbindung
+ ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
+ Arguments REG_SZ MIN
+ WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS
+ StartOnConnect REG_DWORD 1
+ StopOnDisconnect REG_DWORD 0
+ Hidden REG_DWORD 1
+ PreventDuplicates REG_DWORD 1
+
+ ; Separates Programm für den Terminalserver-Trigger
+ TriggerEnabled REG_DWORD 1
+ TriggerProgramPath REG_EXPAND_SZ C:\Program Files\Plandent\ClientTool.exe
+ TriggerArguments REG_SZ --receive --silent
+ TriggerWorkingDirectory REG_EXPAND_SZ C:\Program Files\Plandent
+ TriggerHidden REG_DWORD 1
+ TriggerPreventDuplicates REG_DWORD 1
```
-## Unterstützte Starttypen
+Die bisherigen Registry-Werte für den automatischen RDP-Start bleiben kompatibel.
+
+## Unterstützte lokale Starttypen
+
+Für beide Startarten werden unterstützt:
- `.bat` / `.cmd` → `cmd.exe /D /S /C ...`
- `.ps1` → Windows PowerShell mit `-NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`
- `.exe` → direkter Start
-Bei `Hidden=1` wird `CREATE_NO_WINDOW` verwendet und `SW_HIDE` gesetzt.
+Bei aktivierter Option **Unsichtbar starten** werden Konsolenprogramme ohne sichtbares Konsolenfenster gestartet.
## Mehrfachstartschutz
-Bei `PreventDuplicates=1` wird aus dem expandierten Scriptpfad ein Named Mutex erzeugt. Das Mutex-Handle wird gezielt an den gestarteten Prozess vererbt. Damit bleibt der Schutz bestehen, solange der Receiver läuft – auch wenn die ursprüngliche `mstsc.exe`-Instanz bereits beendet wurde.
+Für beide Programme existiert eine getrennt konfigurierbare Option **Mehrfachstart verhindern**.
-Der Schutz ist bewusst `Local\\...`, also auf die lokale Windows-Sitzung begrenzt.
+Der Schutz wird aus dem expandierten Programmpfad abgeleitet und verwendet einen Named Mutex unter:
+
+```text
+Local\Plandent.MstscScriptHook.
+```
+
+Das Mutex-Handle wird an den gestarteten Root-Prozess vererbt. Solange dieser Prozess läuft, wird ein erneuter Start desselben Pfades verhindert.
+
+## Dynamic Virtual Channel
+
+Das Client-Plugin registriert in `IWTSPlugin::Initialize()` den DVC-Listener:
+
+```text
+plandent::mstsc-script-hook
+```
+
+Wenn `PlandentRdpClientTrigger.exe` innerhalb der RDP-Sitzung läuft, öffnet sie mit `WTSVirtualChannelOpenEx(WTS_CURRENT_SESSION, ..., WTS_CHANNEL_OPTION_DYNAMIC)` genau diesen Kanal und sendet den festen Startbefehl.
+
+Auf dem Client nimmt `IWTSListenerCallback::OnNewChannelConnection()` den Kanal an. `IWTSVirtualChannelCallback::OnDataReceived()` akzeptiert ausschließlich die definierte `START`-Nachricht und startet anschließend das lokal konfigurierte Trigger-Programm.
+
+## Terminalserver-Trigger verwenden
+
+`PlandentRdpClientTrigger.exe` benötigt keine Argumente:
+
+```powershell
+.\PlandentRdpClientTrigger.exe
+```
+
+Sie sollte von einem Prozess innerhalb der Benutzer-RDP-Sitzung gestartet werden, deren Client angesprochen werden soll.
+
+Die EXE öffnet **keinen Netzwerkport**, benötigt keine zusätzliche Firewallfreigabe und kommuniziert ausschließlich über den bestehenden RDP-DVC.
+
+### Exitcodes
+
+```text
+0 Trigger wurde an den DVC geschrieben
+10 DVC konnte nicht geöffnet werden
+11 Schreiben auf den DVC fehlgeschlagen
+12 Trigger-Nachricht wurde nicht vollständig geschrieben
+```
+
+Ein Exitcode `10` tritt beispielsweise auf, wenn die EXE nicht innerhalb einer passenden RDP-Sitzung läuft, das Client-Plugin nicht geladen wurde oder der DVC nicht verfügbar ist.
+
+## Client-Konfigurator
+
+Die GUI enthält jetzt zwei getrennte Bereiche:
+
+### Automatischer Start bei RDP-Verbindung
+
+- Programm/Script
+- Argumente
+- Arbeitsverzeichnis
+- Bei erfolgreicher RDP-Verbindung starten
+- Bei RDP-Trennung beenden
+- Unsichtbar starten
+- Mehrfachstart verhindern
+- lokaler Test
+
+### Programmstart durch Terminalserver
+
+- Trigger aktivieren/deaktivieren
+- separates Programm/Script
+- separate Argumente
+- separates Arbeitsverzeichnis
+- Unsichtbar starten
+- Mehrfachstart verhindern
+- lokaler Test
## Logging
@@ -64,16 +186,28 @@ Bei aktiviertem Logging:
%LOCALAPPDATA%\Plandent\MstscScriptHook\plugin.log
```
-Die GUI enthält einen Button **Log öffnen**.
+Dort werden sowohl der normale RDP-Lifecycle als auch DVC-Verbindungen und Triggerstarts protokolliert.
## Voraussetzungen zum Bauen
-- Windows 10/11 x64
-- Visual Studio 2022 oder Visual Studio Build Tools 2022
+- Windows 10/11 x64 als Entwicklungsrechner
+- Visual Studio 2022 oder entsprechende Build Tools
- Workload **Desktopentwicklung mit C++**
- Windows 10/11 SDK
- .NET 8 SDK
+## Debug-Build in Visual Studio
+
+`Configurator.csproj` ist für **Debug** jetzt absichtlich framework-dependent konfiguriert. Dadurch werden beim normalen F5-Build keine `Microsoft.*.Runtime.win-x64`-Runtime-Pakete benötigt.
+
+Als Startprojekt in Visual Studio:
+
+```text
+PlandentMstscScriptHook.Configurator
+```
+
+Die Plugin-DLL wird über die Projektabhängigkeit zuerst gebaut und anschließend als Resource in den Configurator eingebettet.
+
## Release bauen
PowerShell im Projektverzeichnis:
@@ -85,33 +219,65 @@ PowerShell im Projektverzeichnis:
Ergebnis:
```text
-dist\PlandentMstscScriptHook.exe
+dist\
+├── PlandentMstscScriptHook.exe
+└── PlandentRdpClientTrigger.exe
```
-Die veröffentlichte EXE ist x64, self-contained und enthält die native Plugin-DLL als Resource.
+- `PlandentMstscScriptHook.exe` → auf dem **lokalen RDP-Client** verwenden.
+- `PlandentRdpClientTrigger.exe` → auf den **Terminalserver** kopieren.
-## Installation
+Die Client-EXE ist im Release x64, self-contained und Single-File. Die native Plugin-DLL ist darin eingebettet.
+
+## Installation auf dem Client
1. `PlandentMstscScriptHook.exe` starten.
-2. Script über `...` auswählen.
-3. Für `RemoteVDDS_Receiver_TS.bat` als Argument `MIN` setzen.
-4. Optional Arbeitsverzeichnis auswählen; standardmäßig sollte es dem Scriptverzeichnis entsprechen.
-5. **Script testen** und anschließend **Test beenden**.
+2. Gewünschten automatischen RDP-Start konfigurieren oder deaktivieren.
+3. Optional unter **Programmstart durch Terminalserver** das lokale Trigger-Programm einschließlich Argumenten konfigurieren.
+4. **Start durch Terminalserver-Trigger erlauben** aktivieren.
+5. Optional beide Programme mit den Testbuttons lokal testen.
6. **Plugin installieren** anklicken.
-7. Alle bereits laufenden `mstsc.exe`-Instanzen schließen.
-8. MSTSC neu starten und eine RDP-Verbindung herstellen.
+7. Alle bereits laufenden `mstsc.exe`-Instanzen vollständig schließen.
+8. MSTSC neu starten und die RDP-Verbindung herstellen.
-Die Installation erfolgt nur unter `HKCU` und `%LOCALAPPDATA%`; dafür sind normalerweise keine Administratorrechte nötig.
+Die Client-Installation erfolgt unter `HKCU` und `%LOCALAPPDATA%`; dafür sind normalerweise keine Administratorrechte nötig.
-## Deinstallation
+## Installation auf dem Terminalserver
-**Plugin entfernen** löscht den MSTSC-Registryeintrag und die installierte DLL. Ist die DLL noch in einer laufenden `mstsc.exe` geladen, wird zumindest die Registrierung entfernt; nach dem Schließen aller MSTSC-Instanzen kann die DLL erneut über den Button entfernt werden.
+Es ist keine Registrierung erforderlich. Lediglich:
-Die Script-Konfiguration unter `HKCU\Software\Plandent\MstscScriptHook` bleibt absichtlich erhalten.
+```text
+PlandentRdpClientTrigger.exe
+```
+
+an einen geeigneten Ort kopieren und von der Anwendung ausführen, die den lokalen Start anfordern soll.
+
+Beispiel aus einer Batchdatei innerhalb der RDP-Sitzung:
+
+```bat
+C:\Tools\PlandentRdpClientTrigger.exe
+```
+
+Beispiel PowerShell mit Exitcode-Prüfung:
+
+```powershell
+& 'C:\Tools\PlandentRdpClientTrigger.exe'
+if ($LASTEXITCODE -ne 0) {
+ Write-Warning "Client-Trigger fehlgeschlagen. ExitCode: $LASTEXITCODE"
+}
+```
+
+## Deinstallation auf dem Client
+
+**Plugin entfernen** löscht den MSTSC-Registryeintrag und die installierte DLL. Ist die DLL noch in einer laufenden `mstsc.exe` geladen, wird die Registrierung entfernt und die DLL kann nach dem Schließen aller MSTSC-Instanzen erneut entfernt werden.
+
+Die eigentliche Programmkonfiguration unter `HKCU\Software\Plandent\MstscScriptHook` bleibt absichtlich erhalten.
## Wichtige Grenzen
-- Das Plugin ist für den klassischen Microsoft RDC-Pluginmechanismus ausgelegt. Andere RDP-Clients müssen diesen Mechanismus ebenfalls unterstützen, sonst wird die DLL nicht geladen.
-- `StopOnDisconnect` beendet nur den vom Plugin selbst gestarteten Root-Prozess. Beim RemoteVDDS-BAT ist das der dauerhaft laufende `cmd.exe`-Receiver.
-- Das Script läuft mit den Rechten des lokalen Benutzers, der `mstsc.exe` gestartet hat.
-- AppLocker/WDAC oder andere Application-Control-Richtlinien können das Laden einer nicht signierten DLL bzw. das Starten des Scripts blockieren.
+- Das Plugin ist für den klassischen Microsoft-RDC-Pluginmechanismus ausgelegt. Andere RDP-Clients müssen diesen Mechanismus unterstützen.
+- Der Server-Trigger muss innerhalb einer aktiven RDP-Sitzung ausgeführt werden, wenn `WTS_CURRENT_SESSION` verwendet wird.
+- Das lokale Programm läuft mit den Rechten des Benutzers, der `mstsc.exe` auf dem Client gestartet hat.
+- Der Server kann über dieses Protokoll **keine** beliebigen Programme oder Parameter vorgeben.
+- `StopOnDisconnect` gilt ausschließlich für das automatische RDP-Verbindungsprogramm, nicht für das per Server ausgelöste Programm.
+- AppLocker/WDAC oder andere Application-Control-Richtlinien können das Laden einer nicht signierten DLL bzw. den Programmstart blockieren.
diff --git a/ServerTrigger/Main.cpp b/ServerTrigger/Main.cpp
new file mode 100644
index 0000000..90eb9d8
--- /dev/null
+++ b/ServerTrigger/Main.cpp
@@ -0,0 +1,65 @@
+#include "../Shared/TriggerProtocol.h"
+
+#include
+#include
+
+#include
+#include
+
+namespace
+{
+ enum class ExitCode : int
+ {
+ Success = 0,
+ ChannelOpenFailed = 10,
+ WriteFailed = 11,
+ PartialWrite = 12
+ };
+
+ void DebugError(const wchar_t* step, DWORD error)
+ {
+ wchar_t buffer[256]{};
+ swprintf_s(buffer, _countof(buffer), L"PlandentRdpClientTrigger: %s fehlgeschlagen, Win32=%lu\n", step, error);
+ OutputDebugStringW(buffer);
+ }
+}
+
+int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
+{
+ // WTS_CURRENT_SESSION stellt sicher, dass ausschließlich der Client der
+ // RDP-Sitzung angesprochen wird, in der diese EXE ausgeführt wird.
+ HANDLE channel = WTSVirtualChannelOpenEx(
+ WTS_CURRENT_SESSION,
+ const_cast(hook::protocol::kChannelName),
+ WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_MED);
+
+ if (channel == nullptr)
+ {
+ DebugError(L"WTSVirtualChannelOpenEx", GetLastError());
+ return static_cast(ExitCode::ChannelOpenFailed);
+ }
+
+ ULONG bytesWritten = 0;
+ BOOL written = WTSVirtualChannelWrite(
+ channel,
+ const_cast(hook::protocol::kStartMessage),
+ static_cast(hook::protocol::kStartMessageLength),
+ &bytesWritten);
+
+ DWORD writeError = written ? ERROR_SUCCESS : GetLastError();
+ WTSVirtualChannelClose(channel);
+
+ if (!written)
+ {
+ DebugError(L"WTSVirtualChannelWrite", writeError);
+ return static_cast(ExitCode::WriteFailed);
+ }
+
+ if (bytesWritten != static_cast(hook::protocol::kStartMessageLength))
+ {
+ OutputDebugStringW(L"PlandentRdpClientTrigger: Trigger-Nachricht wurde nur teilweise geschrieben.\n");
+ return static_cast(ExitCode::PartialWrite);
+ }
+
+ return static_cast(ExitCode::Success);
+}
diff --git a/ServerTrigger/PlandentRdpClientTrigger.vcxproj b/ServerTrigger/PlandentRdpClientTrigger.vcxproj
new file mode 100644
index 0000000..b79b2f3
--- /dev/null
+++ b/ServerTrigger/PlandentRdpClientTrigger.vcxproj
@@ -0,0 +1,91 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 17.0
+ Win32Proj
+ {A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}
+ PlandentRdpClientTrigger
+ 10.0
+
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+ $(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\
+ $(ProjectDir)..\artifacts\obj\$(Platform)\$(Configuration)\ServerTrigger\
+ PlandentRdpClientTrigger
+
+
+
+ Level4
+ true
+ WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;_DEBUG;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+ MultiThreadedDebug
+
+
+ Windows
+ true
+ Wtsapi32.lib;%(AdditionalDependencies)
+
+
+
+
+ Level4
+ true
+ true
+ true
+ WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;NDEBUG;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+ MultiThreaded
+
+
+ Windows
+ true
+ true
+ false
+ Wtsapi32.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ServerTrigger/PlandentRdpClientTrigger.vcxproj.filters b/ServerTrigger/PlandentRdpClientTrigger.vcxproj.filters
new file mode 100644
index 0000000..5d0cf2d
--- /dev/null
+++ b/ServerTrigger/PlandentRdpClientTrigger.vcxproj.filters
@@ -0,0 +1,13 @@
+
+
+
+ {D6213E4F-B945-4B58-A331-B988E3EC325B}cpp;c
+ {81A97E24-D824-427B-B039-F2CBE560B587}h;hpp
+
+
+ Source Files
+
+
+ Header Files
+
+
diff --git a/Shared/TriggerProtocol.h b/Shared/TriggerProtocol.h
new file mode 100644
index 0000000..cb2acd8
--- /dev/null
+++ b/Shared/TriggerProtocol.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include
+
+namespace hook::protocol
+{
+ // Der Dynamic-Virtual-Channel-Name muss auf Client und Terminalserver identisch sein.
+ // DVC-Namen dürfen laut WTSVirtualChannelOpenEx bis MAX_PATH lang sein.
+ inline constexpr char kChannelName[] = "plandent::mstsc-script-hook";
+
+ // Absichtlich ein festes Protokollkommando ohne Pfad/Argumente.
+ // Der Terminalserver kann damit nur den lokal vorkonfigurierten Start anfordern.
+ inline constexpr char kStartMessage[] = "PLANDENT_MSTSC_HOOK/1 START";
+ inline constexpr std::size_t kStartMessageLength = sizeof(kStartMessage) - 1;
+}
diff --git a/build-release.ps1 b/build-release.ps1
index d494cbb..0b5dc48 100644
--- a/build-release.ps1
+++ b/build-release.ps1
@@ -8,6 +8,7 @@ param(
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSCommandPath
$PluginProject = Join-Path $Root 'Plugin\PlandentMstscScriptHook.vcxproj'
+$ServerTriggerProject = Join-Path $Root 'ServerTrigger\PlandentRdpClientTrigger.vcxproj'
$ConfiguratorProject = Join-Path $Root 'Configurator\Configurator.csproj'
$Dist = Join-Path $Root 'dist'
$Artifacts = Join-Path $Root 'artifacts'
@@ -35,7 +36,8 @@ if ($Clean) {
New-Item -ItemType Directory -Path $Dist -Force | Out-Null
$MSBuild = Find-MSBuild
-Write-Host "[1/2] Native MSTSC Plugin-DLL bauen ($Configuration|x64)..." -ForegroundColor Cyan
+
+Write-Host "[1/3] Native MSTSC Plugin-DLL bauen ($Configuration|x64)..." -ForegroundColor Cyan
& $MSBuild $PluginProject /m /nologo "/p:Configuration=$Configuration" '/p:Platform=x64'
if ($LASTEXITCODE -ne 0) {
throw "Native DLL Build fehlgeschlagen (ExitCode $LASTEXITCODE)."
@@ -46,7 +48,19 @@ if (-not (Test-Path $PluginDll)) {
throw "Erwartete DLL wurde nicht erzeugt: $PluginDll"
}
-Write-Host "[2/2] Konfigurations-EXE als x64 Single-File veröffentlichen..." -ForegroundColor Cyan
+Write-Host "[2/3] Terminalserver-Trigger bauen ($Configuration|x64)..." -ForegroundColor Cyan
+& $MSBuild $ServerTriggerProject /m /nologo "/p:Configuration=$Configuration" '/p:Platform=x64'
+if ($LASTEXITCODE -ne 0) {
+ throw "Server-Trigger Build fehlgeschlagen (ExitCode $LASTEXITCODE)."
+}
+
+$ServerTriggerExe = Join-Path $Artifacts "x64\$Configuration\PlandentRdpClientTrigger.exe"
+if (-not (Test-Path $ServerTriggerExe)) {
+ throw "Erwartete Server-Trigger-EXE wurde nicht erzeugt: $ServerTriggerExe"
+}
+Copy-Item $ServerTriggerExe (Join-Path $Dist 'PlandentRdpClientTrigger.exe') -Force
+
+Write-Host "[3/3] Client-Konfigurations-EXE als x64 Single-File veröffentlichen..." -ForegroundColor Cyan
& dotnet publish $ConfiguratorProject `
-c $Configuration `
-r win-x64 `
@@ -60,13 +74,15 @@ if ($LASTEXITCODE -ne 0) {
throw "Configurator Publish fehlgeschlagen (ExitCode $LASTEXITCODE)."
}
-$Exe = Join-Path $Dist 'PlandentMstscScriptHook.exe'
-if (-not (Test-Path $Exe)) {
- throw "Erwartete EXE wurde nicht erzeugt: $Exe"
+$ConfiguratorExe = Join-Path $Dist 'PlandentMstscScriptHook.exe'
+if (-not (Test-Path $ConfiguratorExe)) {
+ throw "Erwartete Konfigurations-EXE wurde nicht erzeugt: $ConfiguratorExe"
}
Write-Host ''
Write-Host 'Build erfolgreich:' -ForegroundColor Green
-Write-Host " $Exe"
+Write-Host " Client: $ConfiguratorExe"
+Write-Host " Server: $(Join-Path $Dist 'PlandentRdpClientTrigger.exe')"
Write-Host ''
-Write-Host 'Diese EXE enthält die native DLL als eingebettete Resource.'
+Write-Host 'Die Client-EXE enthält die native MSTSC-Plugin-DLL als eingebettete Resource.'
+Write-Host 'Die Server-EXE sendet ausschließlich den festen START-Trigger über den RDP-DVC.'
diff --git a/examples/README.md b/examples/README.md
index f6171f6..be556fd 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,13 +1,24 @@
-# RemoteVDDS-Beispiel
+# Beispiele
-Für `RemoteVDDS_Receiver_TS.bat` empfiehlt sich:
+## RemoteVDDS-Preset.reg
-- Script: Pfad zu `RemoteVDDS_Receiver_TS.bat`
-- Argumente: `MIN`
-- Arbeitsverzeichnis: Verzeichnis des BAT-Scripts
-- Start bei RDP-Verbindung: an
-- Bei Trennung beenden: aus (bei Bedarf einschalten)
-- Unsichtbar starten: an
-- Mehrfachstart verhindern: an
+Beispielwerte für den bisherigen automatischen RemoteVDDS-Start:
-Das Argument `MIN` ist wichtig, weil das vorhandene BAT-Script ohne diesen Parameter selbst noch einmal per `start /MIN` gestartet wird. Der MSTSC Script Hook startet den Receiver bereits unsichtbar, daher wird der zweite Start nicht benötigt.
+```text
+ScriptPath = C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
+Arguments = MIN
+StartOnConnect = 1
+Hidden = 1
+```
+
+Der neue Terminalserver-Trigger ist in diesem Preset absichtlich deaktiviert. Das Trigger-Programm sollte über den Client-Konfigurator ausgewählt werden, damit Pfad, Argumente und Arbeitsverzeichnis zum jeweiligen Rechner passen.
+
+## Terminalserver
+
+`PlandentRdpClientTrigger.exe` benötigt keine Argumente. Ausführen innerhalb der gewünschten RDP-Benutzersitzung:
+
+```powershell
+.\PlandentRdpClientTrigger.exe
+```
+
+Die EXE sendet nur das feste `START`-Kommando. Das tatsächlich zu startende Programm wird ausschließlich auf dem Client konfiguriert.
diff --git a/examples/RemoteVDDS-Preset.reg b/examples/RemoteVDDS-Preset.reg
index 13f17ca..3f3f803 100644
--- a/examples/RemoteVDDS-Preset.reg
+++ b/examples/RemoteVDDS-Preset.reg
@@ -2,11 +2,21 @@ Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Plandent\MstscScriptHook]
"Enabled"=dword:00000001
-"ScriptPath"="C:\\RemoteVDDS\\RemoteVDDS_Receiver_TS.bat"
+"EnableLogging"=dword:00000001
+
+; Automatischer RemoteVDDS-Receiver bei RDP-Verbindung
+"ScriptPath"=hex(2):43,00,3a,00,5c,00,52,00,65,00,6d,00,6f,00,74,00,65,00,56,00,44,00,44,00,53,00,5c,00,52,00,65,00,6d,00,6f,00,74,00,65,00,56,00,44,00,44,00,53,00,5f,00,52,00,65,00,63,00,65,00,69,00,76,00,65,00,72,00,5f,00,54,00,53,00,2e,00,62,00,61,00,74,00,00,00
"Arguments"="MIN"
-"WorkingDirectory"="C:\\RemoteVDDS"
+"WorkingDirectory"=hex(2):43,00,3a,00,5c,00,52,00,65,00,6d,00,6f,00,74,00,65,00,56,00,44,00,44,00,53,00,00,00
"StartOnConnect"=dword:00000001
"StopOnDisconnect"=dword:00000000
"Hidden"=dword:00000001
"PreventDuplicates"=dword:00000001
-"EnableLogging"=dword:00000001
+
+; Server-Trigger standardmäßig deaktiviert. Über die GUI aktivieren und Pfad/Argumente setzen.
+"TriggerEnabled"=dword:00000000
+"TriggerProgramPath"=""
+"TriggerArguments"=""
+"TriggerWorkingDirectory"=""
+"TriggerHidden"=dword:00000001
+"TriggerPreventDuplicates"=dword:00000001