Hinzufügen der Möglichkeit ein Programmstart auf dem Client durch die RDP Sitzung zu triggern

This commit is contained in:
2026-08-11 09:52:49 +02:00
parent eec4f52b5a
commit ee88f1ff30
22 changed files with 1162 additions and 263 deletions
+13 -1
View File
@@ -9,12 +9,24 @@
<RootNamespace>Plandent.MstscScriptHook.Configurator</RootNamespace> <RootNamespace>Plandent.MstscScriptHook.Configurator</RootNamespace>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
<PluginPayloadPath>$(MSBuildProjectDirectory)\..\artifacts\x64\$(Configuration)\PlandentMstscScriptHook.dll</PluginPayloadPath>
</PropertyGroup>
<!-- Beim normalen Visual-Studio-Debugging wird die lokal installierte .NET-8-Runtime benutzt.
Dadurch werden keine win-x64 Runtime-Packs von NuGet benötigt. -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<SelfContained>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<DebugType>portable</DebugType>
</PropertyGroup>
<!-- Nur das Release-Publish erzeugt die verteilbare self-contained Single-File-EXE. -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile> <PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun> <PublishReadyToRun>false</PublishReadyToRun>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<PluginPayloadPath>$(MSBuildProjectDirectory)\..\artifacts\x64\$(Configuration)\PlandentMstscScriptHook.dll</PluginPayloadPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+12 -1
View File
@@ -3,6 +3,9 @@ namespace Plandent.MstscScriptHook.Configurator;
internal sealed class HookConfig internal sealed class HookConfig
{ {
public bool Enabled { get; set; } = true; 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 ScriptPath { get; set; } = string.Empty;
public string Arguments { get; set; } = string.Empty; public string Arguments { get; set; } = string.Empty;
public string WorkingDirectory { 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 StopOnDisconnect { get; set; }
public bool Hidden { get; set; } = true; public bool Hidden { get; set; } = true;
public bool PreventDuplicates { 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;
} }
+351 -120
View File
@@ -1,21 +1,35 @@
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using System.Diagnostics;
namespace Plandent.MstscScriptHook.Configurator; namespace Plandent.MstscScriptHook.Configurator;
internal sealed class MainForm : Form 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 scriptPathTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox argumentsTextBox = new() { Dock = DockStyle.Fill }; private readonly TextBox argumentsTextBox = new() { Dock = DockStyle.Fill };
private readonly TextBox workingDirectoryTextBox = new() { Dock = DockStyle.Fill }; private readonly TextBox workingDirectoryTextBox = new() { Dock = DockStyle.Fill };
private readonly CheckBox startOnConnectCheckBox = new() { Text = "Bei erfolgreicher RDP-Verbindung starten", AutoSize = true };
private readonly CheckBox enabledCheckBox = new() { Text = "Plugin-Funktion aktiviert", AutoSize = true }; private readonly CheckBox stopOnDisconnectCheckBox = new() { Text = "Bei RDP-Trennung beenden", AutoSize = true };
private readonly CheckBox startOnConnectCheckBox = new() { Text = "Script bei erfolgreicher RDP-Verbindung starten", AutoSize = true }; private readonly CheckBox hiddenCheckBox = new() { Text = "Unsichtbar 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 preventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", 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() private readonly Label statusLabel = new()
{ {
@@ -25,20 +39,17 @@ internal sealed class MainForm : Form
Padding = new Padding(8) Padding = new Padding(8)
}; };
private readonly Button stopTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
private Process? testProcess;
public MainForm() public MainForm()
{ {
Text = "Plandent MSTSC Script Hook"; Text = "Plandent MSTSC Script Hook";
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(760, 590); MinimumSize = new Size(820, 720);
Size = new Size(820, 640); Size = new Size(900, 800);
Font = new Font("Segoe UI", 9F); Font = new Font("Segoe UI", 9F);
Controls.Add(BuildMainLayout()); Controls.Add(BuildMainLayout());
Load += (_, _) => LoadConfiguration(); Load += (_, _) => LoadConfiguration();
FormClosed += (_, _) => DisposeTestProcessHandle(); FormClosed += (_, _) => DisposeAllTestProcesses();
} }
private Control BuildMainLayout() 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.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); 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)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var title = new Label var title = new Label
{ {
@@ -68,21 +79,201 @@ internal sealed class MainForm : Form
var description = new Label 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, AutoSize = true,
MaximumSize = new Size(740, 0), MaximumSize = new Size(820, 0),
Margin = new Padding(0, 0, 0, 16) Margin = new Padding(0, 0, 0, 12)
}; };
root.Controls.Add(description, 0, 1); root.Controls.Add(description, 0, 1);
root.Controls.Add(BuildFileGrid(), 0, 2); var scroll = new Panel
root.Controls.Add(BuildOptionsGroup(), 0, 3); {
root.Controls.Add(BuildStatusPanel(), 0, 4); Dock = DockStyle.Fill,
root.Controls.Add(BuildButtons(), 0, 5); 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; 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 var grid = new TableLayoutPanel
{ {
@@ -90,17 +281,17 @@ internal sealed class MainForm : Form
AutoSize = true, AutoSize = true,
ColumnCount = 3, ColumnCount = 3,
RowCount = 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.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.Controls.Add(CreateFieldLabel("Script:"), 0, 0); grid.Controls.Add(CreateFieldLabel("Programm/Script:"), 0, 0);
grid.Controls.Add(scriptPathTextBox, 1, 0); grid.Controls.Add(pathTextBox, 1, 0);
var browseScriptButton = new Button { Text = "…", Width = 42, Height = scriptPathTextBox.PreferredHeight + 4 }; var browseProgramButton = new Button { Text = "…", Width = 42, Height = pathTextBox.PreferredHeight + 4 };
browseScriptButton.Click += (_, _) => BrowseScript(); browseProgramButton.Click += (_, _) => browseProgram();
grid.Controls.Add(browseScriptButton, 2, 0); grid.Controls.Add(browseProgramButton, 2, 0);
grid.Controls.Add(CreateFieldLabel("Argumente:"), 0, 1); grid.Controls.Add(CreateFieldLabel("Argumente:"), 0, 1);
grid.Controls.Add(argumentsTextBox, 1, 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(CreateFieldLabel("Arbeitsverzeichnis:"), 0, 2);
grid.Controls.Add(workingDirectoryTextBox, 1, 2); grid.Controls.Add(workingDirectoryTextBox, 1, 2);
var browseWorkingButton = new Button { Text = "…", Width = 42, Height = workingDirectoryTextBox.PreferredHeight + 4 }; var browseWorkingButton = new Button { Text = "…", Width = 42, Height = workingDirectoryTextBox.PreferredHeight + 4 };
browseWorkingButton.Click += (_, _) => BrowseWorkingDirectory(); browseWorkingButton.Click += (_, _) => browseWorkingDirectory();
grid.Controls.Add(browseWorkingButton, 2, 2); grid.Controls.Add(browseWorkingButton, 2, 2);
return grid; 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() private Control BuildStatusPanel()
{ {
var panel = new Panel var panel = new Panel
{ {
Dock = DockStyle.Top, Dock = DockStyle.Top,
Height = 58, Height = 70,
BorderStyle = BorderStyle.FixedSingle, BorderStyle = BorderStyle.FixedSingle,
Margin = new Padding(0, 0, 0, 14) Margin = new Padding(0, 0, 0, 10)
}; };
panel.Controls.Add(statusLabel); panel.Controls.Add(statusLabel);
return panel; return panel;
} }
private Control BuildButtons() private Control BuildMainButtons()
{ {
var buttons = new FlowLayoutPanel var buttons = new FlowLayoutPanel
{ {
@@ -178,10 +341,6 @@ internal sealed class MainForm : Form
Margin = new Padding(0) 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 }; var saveButton = new Button { Text = "Konfiguration speichern", AutoSize = true };
saveButton.Click += (_, _) => SaveConfiguration(showConfirmation: true); saveButton.Click += (_, _) => SaveConfiguration(showConfirmation: true);
@@ -194,8 +353,6 @@ internal sealed class MainForm : Form
var logButton = new Button { Text = "Log öffnen", AutoSize = true }; var logButton = new Button { Text = "Log öffnen", AutoSize = true };
logButton.Click += (_, _) => OpenLog(); logButton.Click += (_, _) => OpenLog();
buttons.Controls.Add(testButton);
buttons.Controls.Add(stopTestButton);
buttons.Controls.Add(saveButton); buttons.Controls.Add(saveButton);
buttons.Controls.Add(installButton); buttons.Controls.Add(installButton);
buttons.Controls.Add(removeButton); buttons.Controls.Add(removeButton);
@@ -203,18 +360,18 @@ internal sealed class MainForm : Form
return buttons; return buttons;
} }
private void BrowseScript() private void BrowseProgram(TextBox pathTextBox, TextBox workingDirectoryTextBox, bool useRemoteVddsPreset)
{ {
using var dialog = new OpenFileDialog using var dialog = new OpenFileDialog
{ {
Title = "Script oder Programm auswählen", Title = "Programm oder Script auswählen",
Filter = "Unterstützte Dateien (*.bat;*.cmd;*.ps1;*.exe)|*.bat;*.cmd;*.ps1;*.exe|Alle Dateien (*.*)|*.*", Filter = "Unterstützte Dateien (*.exe;*.bat;*.cmd;*.ps1)|*.exe;*.bat;*.cmd;*.ps1|Alle Dateien (*.*)|*.*",
CheckFileExists = true 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)) if (File.Exists(expanded))
{ {
dialog.InitialDirectory = Path.GetDirectoryName(expanded); dialog.InitialDirectory = Path.GetDirectoryName(expanded);
@@ -227,20 +384,21 @@ internal sealed class MainForm : Form
return; return;
} }
scriptPathTextBox.Text = dialog.FileName; pathTextBox.Text = dialog.FileName;
if (string.IsNullOrWhiteSpace(workingDirectoryTextBox.Text)) if (string.IsNullOrWhiteSpace(workingDirectoryTextBox.Text))
{ {
workingDirectoryTextBox.Text = Path.GetDirectoryName(dialog.FileName) ?? string.Empty; 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)) Path.GetFileName(dialog.FileName).Contains("RemoteVDDS_Receiver_TS", StringComparison.OrdinalIgnoreCase))
{ {
argumentsTextBox.Text = "MIN"; argumentsTextBox.Text = "MIN";
} }
} }
private void BrowseWorkingDirectory() private void BrowseWorkingDirectory(TextBox target)
{ {
using var dialog = new FolderBrowserDialog using var dialog = new FolderBrowserDialog
{ {
@@ -249,7 +407,7 @@ internal sealed class MainForm : Form
ShowNewFolderButton = true ShowNewFolderButton = true
}; };
string expanded = Environment.ExpandEnvironmentVariables(workingDirectoryTextBox.Text); string expanded = Environment.ExpandEnvironmentVariables(target.Text);
if (Directory.Exists(expanded)) if (Directory.Exists(expanded))
{ {
dialog.SelectedPath = expanded; dialog.SelectedPath = expanded;
@@ -257,7 +415,7 @@ internal sealed class MainForm : Form
if (dialog.ShowDialog(this) == DialogResult.OK) 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 return new HookConfig
{ {
Enabled = enabledCheckBox.Checked, Enabled = enabledCheckBox.Checked,
EnableLogging = loggingCheckBox.Checked,
ScriptPath = scriptPathTextBox.Text.Trim(), ScriptPath = scriptPathTextBox.Text.Trim(),
Arguments = argumentsTextBox.Text.Trim(), Arguments = argumentsTextBox.Text.Trim(),
WorkingDirectory = workingDirectoryTextBox.Text.Trim(), WorkingDirectory = workingDirectoryTextBox.Text.Trim(),
@@ -273,22 +433,37 @@ internal sealed class MainForm : Form
StopOnDisconnect = stopOnDisconnectCheckBox.Checked, StopOnDisconnect = stopOnDisconnectCheckBox.Checked,
Hidden = hiddenCheckBox.Checked, Hidden = hiddenCheckBox.Checked,
PreventDuplicates = preventDuplicatesCheckBox.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() private void LoadConfiguration()
{ {
HookConfig config = RegistryConfig.Load(); HookConfig config = RegistryConfig.Load();
enabledCheckBox.Checked = config.Enabled;
loggingCheckBox.Checked = config.EnableLogging;
scriptPathTextBox.Text = config.ScriptPath; scriptPathTextBox.Text = config.ScriptPath;
argumentsTextBox.Text = config.Arguments; argumentsTextBox.Text = config.Arguments;
workingDirectoryTextBox.Text = config.WorkingDirectory; workingDirectoryTextBox.Text = config.WorkingDirectory;
enabledCheckBox.Checked = config.Enabled;
startOnConnectCheckBox.Checked = config.StartOnConnect; startOnConnectCheckBox.Checked = config.StartOnConnect;
stopOnDisconnectCheckBox.Checked = config.StopOnDisconnect; stopOnDisconnectCheckBox.Checked = config.StopOnDisconnect;
hiddenCheckBox.Checked = config.Hidden; hiddenCheckBox.Checked = config.Hidden;
preventDuplicatesCheckBox.Checked = config.PreventDuplicates; 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(); RefreshStatus();
} }
@@ -297,7 +472,7 @@ internal sealed class MainForm : Form
try try
{ {
HookConfig config = ReadForm(); HookConfig config = ReadForm();
ValidateConfig(config, requireScript: false); ValidateConfiguredTargets(config, installationValidation: false);
RegistryConfig.Save(config); RegistryConfig.Save(config);
if (showConfirmation) if (showConfirmation)
{ {
@@ -318,14 +493,14 @@ internal sealed class MainForm : Form
try try
{ {
HookConfig config = ReadForm(); HookConfig config = ReadForm();
ValidateConfig(config, requireScript: true); ValidateConfiguredTargets(config, installationValidation: true);
RegistryConfig.Save(config); RegistryConfig.Save(config);
PluginInstaller.Install(); PluginInstaller.Install();
RefreshStatus(); RefreshStatus();
MessageBox.Show( MessageBox.Show(
this, this,
"Plugin wurde für den aktuellen Benutzer installiert und die Konfiguration gespeichert.\n\n" + "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", "Installation abgeschlossen",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Information); MessageBoxIcon.Information);
@@ -348,63 +523,92 @@ internal sealed class MainForm : Form
result.FullyRemoved ? MessageBoxIcon.Information : MessageBoxIcon.Warning); result.FullyRemoved ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
} }
private void StartTest() private void StartConnectionTest()
{ {
try try
{ {
StopTest(silent: true); StopConnectionTest(silent: true);
HookConfig config = ReadForm(); HookConfig config = ReadForm();
ValidateConfig(config, requireScript: true); ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true);
testProcess = ScriptTestLauncher.Start(config); connectionTestProcess = ScriptTestLauncher.StartConnectionProgram(config);
stopTestButton.Enabled = true; stopConnectionTestButton.Enabled = true;
statusLabel.Text = $"Testprozess gestartet: PID {testProcess.Id}. Der Test läuft unabhängig von MSTSC."; statusLabel.Text = $"Verbindungs-Test gestartet: PID {connectionTestProcess.Id}. Der Test läuft unabhängig von MSTSC.";
} }
catch (Exception ex) 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 StartTriggerTest()
private void StopTest(bool silent)
{ {
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; return;
} }
try try
{ {
if (!testProcess.HasExited) if (!process.HasExited)
{ {
testProcess.Kill(entireProcessTree: true); process.Kill(entireProcessTree: true);
testProcess.WaitForExit(3000); process.WaitForExit(3000);
} }
if (!silent) if (!silent)
{ {
statusLabel.Text = "Testprozess wurde beendet."; statusLabel.Text = description + " wurde beendet.";
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
if (!silent) 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 finally
{ {
DisposeTestProcessHandle(); process?.Dispose();
process = null;
button.Enabled = false;
} }
} }
private void DisposeTestProcessHandle() private void DisposeAllTestProcesses()
{ {
testProcess?.Dispose(); connectionTestProcess?.Dispose();
testProcess = null; triggerTestProcess?.Dispose();
stopTestButton.Enabled = false; connectionTestProcess = null;
triggerTestProcess = null;
} }
private void OpenLog() private void OpenLog()
@@ -426,42 +630,69 @@ internal sealed class MainForm : Form
private void RefreshStatus() private void RefreshStatus()
{ {
bool installed = PluginInstaller.IsInstalled(out string pluginStatus); bool installed = PluginInstaller.IsInstalled(out string pluginStatus);
string configStatus = string.IsNullOrWhiteSpace(scriptPathTextBox.Text)
? " Kein Script konfiguriert." string connectStatus = startOnConnectCheckBox.Checked
: $" Script: {scriptPathTextBox.Text}"; ? (string.IsNullOrWhiteSpace(scriptPathTextBox.Text) ? " RDP-Start: nicht konfiguriert." : " RDP-Start: konfiguriert.")
statusLabel.Text = (installed ? "✓ " : "○ ") + pluginStatus + configStatus; : " 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; return;
} }
string scriptPath = Environment.ExpandEnvironmentVariables(config.ScriptPath); if (config.StartOnConnect)
if (string.IsNullOrWhiteSpace(scriptPath))
{ {
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")) 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)) if (!Directory.Exists(workDir))
{ {
throw new DirectoryNotFoundException("Das Arbeitsverzeichnis wurde nicht gefunden: " + workDir); throw new DirectoryNotFoundException($"Arbeitsverzeichnis für „{displayName}“ wurde nicht gefunden: {workDir}");
} }
} }
} }
+18 -2
View File
@@ -17,6 +17,8 @@ internal static class RegistryConfig
} }
config.Enabled = ReadBool(key, "Enabled", true); config.Enabled = ReadBool(key, "Enabled", true);
config.EnableLogging = ReadBool(key, "EnableLogging", true);
config.ScriptPath = ReadString(key, "ScriptPath"); config.ScriptPath = ReadString(key, "ScriptPath");
config.Arguments = ReadString(key, "Arguments"); config.Arguments = ReadString(key, "Arguments");
config.WorkingDirectory = ReadString(key, "WorkingDirectory"); config.WorkingDirectory = ReadString(key, "WorkingDirectory");
@@ -24,7 +26,13 @@ internal static class RegistryConfig
config.StopOnDisconnect = ReadBool(key, "StopOnDisconnect", false); config.StopOnDisconnect = ReadBool(key, "StopOnDisconnect", false);
config.Hidden = ReadBool(key, "Hidden", true); config.Hidden = ReadBool(key, "Hidden", true);
config.PreventDuplicates = ReadBool(key, "PreventDuplicates", 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; return config;
} }
@@ -34,6 +42,8 @@ internal static class RegistryConfig
?? throw new InvalidOperationException("Registry-Schlüssel konnte nicht erstellt werden."); ?? throw new InvalidOperationException("Registry-Schlüssel konnte nicht erstellt werden.");
key.SetValue("Enabled", config.Enabled ? 1 : 0, RegistryValueKind.DWord); 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("ScriptPath", config.ScriptPath ?? string.Empty, RegistryValueKind.ExpandString);
key.SetValue("Arguments", config.Arguments ?? string.Empty, RegistryValueKind.String); key.SetValue("Arguments", config.Arguments ?? string.Empty, RegistryValueKind.String);
key.SetValue("WorkingDirectory", config.WorkingDirectory ?? string.Empty, RegistryValueKind.ExpandString); 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("StopOnDisconnect", config.StopOnDisconnect ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("Hidden", config.Hidden ? 1 : 0, RegistryValueKind.DWord); key.SetValue("Hidden", config.Hidden ? 1 : 0, RegistryValueKind.DWord);
key.SetValue("PreventDuplicates", config.PreventDuplicates ? 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() public static string? GetRegisteredPluginPath()
+29 -20
View File
@@ -4,24 +4,36 @@ namespace Plandent.MstscScriptHook.Configurator;
internal static class ScriptTestLauncher 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); string programPath = Environment.ExpandEnvironmentVariables(configuredPath ?? string.Empty);
if (string.IsNullOrWhiteSpace(scriptPath) || !File.Exists(scriptPath)) 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 extension = Path.GetExtension(programPath).ToLowerInvariant();
string workingDirectory = string.IsNullOrWhiteSpace(config.WorkingDirectory) string workingDirectory = string.IsNullOrWhiteSpace(configuredWorkingDirectory)
? Path.GetDirectoryName(scriptPath) ?? Environment.CurrentDirectory ? Path.GetDirectoryName(programPath) ?? Environment.CurrentDirectory
: Environment.ExpandEnvironmentVariables(config.WorkingDirectory); : Environment.ExpandEnvironmentVariables(configuredWorkingDirectory);
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = config.Hidden, CreateNoWindow = hidden,
WindowStyle = config.Hidden ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal, WindowStyle = hidden ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal,
WorkingDirectory = workingDirectory WorkingDirectory = workingDirectory
}; };
@@ -34,7 +46,7 @@ internal static class ScriptTestLauncher
startInfo.ArgumentList.Add("/D"); startInfo.ArgumentList.Add("/D");
startInfo.ArgumentList.Add("/S"); startInfo.ArgumentList.Add("/S");
startInfo.ArgumentList.Add("/C"); startInfo.ArgumentList.Add("/C");
startInfo.ArgumentList.Add(BuildCmdCommand(scriptPath, config.Arguments)); startInfo.ArgumentList.Add(BuildCmdCommand(programPath, configuredArguments));
break; break;
case ".ps1": case ".ps1":
@@ -49,13 +61,13 @@ internal static class ScriptTestLauncher
startInfo.ArgumentList.Add("-ExecutionPolicy"); startInfo.ArgumentList.Add("-ExecutionPolicy");
startInfo.ArgumentList.Add("Bypass"); startInfo.ArgumentList.Add("Bypass");
startInfo.ArgumentList.Add("-File"); startInfo.ArgumentList.Add("-File");
startInfo.ArgumentList.Add(scriptPath); startInfo.ArgumentList.Add(programPath);
AddRawArguments(startInfo, config.Arguments); AddRawArguments(startInfo, configuredArguments);
break; break;
case ".exe": case ".exe":
startInfo.FileName = scriptPath; startInfo.FileName = programPath;
AddRawArguments(startInfo, config.Arguments); AddRawArguments(startInfo, configuredArguments);
break; break;
default: default:
@@ -66,9 +78,9 @@ internal static class ScriptTestLauncher
?? throw new InvalidOperationException("Der Testprozess konnte nicht gestartet werden."); ?? 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)) if (!string.IsNullOrWhiteSpace(arguments))
{ {
command += " " + arguments; command += " " + arguments;
@@ -81,9 +93,6 @@ internal static class ScriptTestLauncher
return "\"" + value.Replace("\"", "\\\"") + "\""; 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) private static void AddRawArguments(ProcessStartInfo startInfo, string? raw)
{ {
if (string.IsNullOrWhiteSpace(raw)) if (string.IsNullOrWhiteSpace(raw))
+7 -1
View File
@@ -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 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 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} {D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA} = {D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}
EndProjectSection EndProjectSection
EndProject EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "PlandentRdpClientTrigger", "ServerTrigger\PlandentRdpClientTrigger.vcxproj", "{A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64 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}.Debug|x64.Build.0 = Debug|x64
{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Release|x64.ActiveCfg = Release|x64 {935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Release|x64.ActiveCfg = Release|x64
{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Release|x64.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+1
View File
@@ -94,6 +94,7 @@
<ClInclude Include="ProcessLauncher.h" /> <ClInclude Include="ProcessLauncher.h" />
<ClInclude Include="RegistryConfig.h" /> <ClInclude Include="RegistryConfig.h" />
<ClInclude Include="ScriptHookPlugin.h" /> <ClInclude Include="ScriptHookPlugin.h" />
<ClInclude Include="..\Shared\TriggerProtocol.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="exports.def" /> <None Include="exports.def" />
@@ -16,5 +16,6 @@
<ClInclude Include="ProcessLauncher.h"><Filter>Header Files</Filter></ClInclude> <ClInclude Include="ProcessLauncher.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="RegistryConfig.h"><Filter>Header Files</Filter></ClInclude> <ClInclude Include="RegistryConfig.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="ScriptHookPlugin.h"><Filter>Header Files</Filter></ClInclude> <ClInclude Include="ScriptHookPlugin.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="..\Shared\TriggerProtocol.h"><Filter>Header Files</Filter></ClInclude>
</ItemGroup> </ItemGroup>
</Project> </Project>
+28 -28
View File
@@ -122,9 +122,9 @@ namespace
std::wstring commandLine; 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") if (extension == L".bat" || extension == L".cmd")
{ {
@@ -139,7 +139,7 @@ namespace
comspec = systemRoot + L"\\System32\\cmd.exe"; comspec = systemRoot + L"\\System32\\cmd.exe";
} }
std::wstring inner = QuoteWindowsArgument(config.scriptPath); std::wstring inner = QuoteWindowsArgument(config.path);
if (!config.arguments.empty()) if (!config.arguments.empty())
{ {
inner += L" " + config.arguments; inner += L" " + config.arguments;
@@ -162,7 +162,7 @@ namespace
command.application = powershell; command.application = powershell;
command.commandLine = QuoteWindowsArgument(powershell) + command.commandLine = QuoteWindowsArgument(powershell) +
L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " + L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
QuoteWindowsArgument(config.scriptPath); QuoteWindowsArgument(config.path);
if (!config.arguments.empty()) if (!config.arguments.empty())
{ {
command.commandLine += L" " + config.arguments; command.commandLine += L" " + config.arguments;
@@ -172,8 +172,8 @@ namespace
if (extension == L".exe") if (extension == L".exe")
{ {
command.application = config.scriptPath; command.application = config.path;
command.commandLine = QuoteWindowsArgument(config.scriptPath); command.commandLine = QuoteWindowsArgument(config.path);
if (!config.arguments.empty()) if (!config.arguments.empty())
{ {
command.commandLine += L" " + config.arguments; command.commandLine += L" " + config.arguments;
@@ -217,37 +217,37 @@ namespace hook
return process_ != nullptr; return process_ != nullptr;
} }
bool ProcessLauncher::Start(const HookConfig& config) bool ProcessLauncher::Start(const LaunchConfig& config, bool loggingEnabled, const std::wstring& sourceName)
{ {
CleanupExitedProcess(); CleanupExitedProcess();
if (process_ != nullptr) 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; 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; 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) 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; return false;
} }
LaunchCommand command; LaunchCommand command;
if (!BuildLaunchCommand(config, 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; return false;
} }
std::wstring workingDirectory = config.workingDirectory.empty() std::wstring workingDirectory = config.workingDirectory.empty()
? ParentDirectory(config.scriptPath) ? ParentDirectory(config.path)
: config.workingDirectory; : config.workingDirectory;
HANDLE duplicateMutex = nullptr; HANDLE duplicateMutex = nullptr;
@@ -257,19 +257,19 @@ namespace hook
sa.nLength = sizeof(sa); sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE; sa.bInheritHandle = TRUE;
std::wstring mutexName = MutexNameForScript(config.scriptPath); std::wstring mutexName = MutexNameForScript(config.path);
duplicateMutex = CreateMutexW(&sa, FALSE, mutexName.c_str()); duplicateMutex = CreateMutexW(&sa, FALSE, mutexName.c_str());
if (duplicateMutex == nullptr) if (duplicateMutex == nullptr)
{ {
DWORD error = GetLastError(); 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; return false;
} }
if (GetLastError() == ERROR_ALREADY_EXISTS) if (GetLastError() == ERROR_ALREADY_EXISTS)
{ {
CloseHandle(duplicateMutex); 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; return true;
} }
} }
@@ -305,7 +305,7 @@ namespace hook
{ {
DWORD error = GetLastError(); DWORD error = GetLastError();
CloseHandle(duplicateMutex); 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; return false;
} }
@@ -322,7 +322,7 @@ namespace hook
DWORD error = GetLastError(); DWORD error = GetLastError();
DeleteProcThreadAttributeList(startup.lpAttributeList); DeleteProcThreadAttributeList(startup.lpAttributeList);
CloseHandle(duplicateMutex); 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; return false;
} }
@@ -371,7 +371,7 @@ namespace hook
if (!created) if (!created)
{ {
DWORD error = GetLastError(); DWORD error = GetLastError();
Log(config.enableLogging, L"Scriptstart fehlgeschlagen: " + FormatWin32Error(error)); Log(loggingEnabled, sourceName + L" konnte nicht gestartet werden: " + FormatWin32Error(error));
return false; return false;
} }
@@ -379,14 +379,14 @@ namespace hook
process_ = pi.hProcess; process_ = pi.hProcess;
processId_ = pi.dwProcessId; processId_ = pi.dwProcessId;
Log(config.enableLogging, Log(loggingEnabled,
L"Script gestartet. PID=" + std::to_wstring(processId_) + sourceName + L" gestartet. PID=" + std::to_wstring(processId_) +
L", Script=" + config.scriptPath + L", Pfad=" + config.path +
(config.arguments.empty() ? L"" : L", Argumente=" + config.arguments)); (config.arguments.empty() ? L"" : L", Argumente=" + config.arguments));
return true; return true;
} }
void ProcessLauncher::Stop(bool loggingEnabled) void ProcessLauncher::Stop(bool loggingEnabled, const std::wstring& sourceName)
{ {
CleanupExitedProcess(); CleanupExitedProcess();
if (process_ == nullptr) if (process_ == nullptr)
@@ -398,11 +398,11 @@ namespace hook
if (!TerminateProcess(process_, 0)) if (!TerminateProcess(process_, 0))
{ {
DWORD error = GetLastError(); 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 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_); CloseHandle(process_);
@@ -410,7 +410,7 @@ namespace hook
processId_ = 0; processId_ = 0;
} }
void ProcessLauncher::Detach(bool loggingEnabled) void ProcessLauncher::Detach(bool loggingEnabled, const std::wstring& sourceName)
{ {
CleanupExitedProcess(); CleanupExitedProcess();
if (process_ == nullptr) if (process_ == nullptr)
@@ -418,7 +418,7 @@ namespace hook
return; 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_); CloseHandle(process_);
process_ = nullptr; process_ = nullptr;
processId_ = 0; processId_ = 0;
+3 -3
View File
@@ -15,9 +15,9 @@ namespace hook
ProcessLauncher(const ProcessLauncher&) = delete; ProcessLauncher(const ProcessLauncher&) = delete;
ProcessLauncher& operator=(const ProcessLauncher&) = delete; ProcessLauncher& operator=(const ProcessLauncher&) = delete;
bool Start(const HookConfig& config); bool Start(const LaunchConfig& config, bool loggingEnabled, const std::wstring& sourceName);
void Stop(bool loggingEnabled); void Stop(bool loggingEnabled, const std::wstring& sourceName = L"Programm");
void Detach(bool loggingEnabled); void Detach(bool loggingEnabled, const std::wstring& sourceName = L"Programm");
bool IsRunning(); bool IsRunning();
private: private:
+16 -6
View File
@@ -76,14 +76,24 @@ namespace hook
} }
config.enabled = ReadDword(key, L"Enabled", 1) != 0; config.enabled = ReadDword(key, L"Enabled", 1) != 0;
config.scriptPath = ReadString(key, L"ScriptPath"); config.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0;
config.arguments = ExpandEnvironment(ReadString(key, L"Arguments"));
config.workingDirectory = ReadString(key, L"WorkingDirectory"); // 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.startOnConnect = ReadDword(key, L"StartOnConnect", 1) != 0;
config.stopOnDisconnect = ReadDword(key, L"StopOnDisconnect", 0) != 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; // Neue, ausschließlich lokal definierte Konfiguration für Server-Trigger.
config.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0; 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); RegCloseKey(key);
return config; return config;
+18 -6
View File
@@ -7,17 +7,29 @@ namespace hook
{ {
inline constexpr wchar_t kConfigKey[] = L"Software\\Plandent\\MstscScriptHook"; 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 struct HookConfig
{ {
bool enabled = true; bool enabled = true;
std::wstring scriptPath; bool enableLogging = true;
std::wstring arguments;
std::wstring workingDirectory; // Optionaler automatischer Start bei erfolgreicher RDP-Verbindung.
LaunchConfig connectProgram;
bool startOnConnect = true; bool startOnConnect = true;
bool stopOnDisconnect = false; bool stopOnDisconnect = false;
bool hidden = true;
bool preventDuplicates = true; // Optionaler Start, der durch PlandentRdpClientTrigger.exe auf dem
bool enableLogging = true; // Terminalserver über den Dynamic Virtual Channel ausgelöst wird.
bool triggerEnabled = false;
LaunchConfig triggerProgram;
}; };
HookConfig LoadConfig(); HookConfig LoadConfig();
+203 -11
View File
@@ -2,9 +2,109 @@
#include "Logging.h" #include "Logging.h"
#include "RegistryConfig.h" #include "RegistryConfig.h"
#include "../Shared/TriggerProtocol.h"
#include <cstring>
#include <new>
#include <string>
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<IWTSVirtualChannelCallback*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override
{
return static_cast<ULONG>(InterlockedIncrement(&refCount_));
}
ULONG STDMETHODCALLTYPE Release() override
{
ULONG remaining = static_cast<ULONG>(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() = default;
ScriptHookPlugin::~ScriptHookPlugin()
{
ReleaseDvcObjects();
}
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** ppvObject) HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** ppvObject)
{ {
@@ -14,14 +114,22 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** p
} }
*ppvObject = nullptr; *ppvObject = nullptr;
if (riid == __uuidof(IUnknown) || riid == __uuidof(IWTSPlugin)) if (riid == __uuidof(IUnknown) || riid == __uuidof(IWTSPlugin))
{ {
*ppvObject = static_cast<IWTSPlugin*>(this); *ppvObject = static_cast<IWTSPlugin*>(this);
AddRef(); }
return S_OK; else if (riid == __uuidof(IWTSListenerCallback))
{
*ppvObject = static_cast<IWTSListenerCallback*>(this);
}
else
{
return E_NOINTERFACE;
} }
return E_NOINTERFACE; AddRef();
return S_OK;
} }
ULONG STDMETHODCALLTYPE ScriptHookPlugin::AddRef() ULONG STDMETHODCALLTYPE ScriptHookPlugin::AddRef()
@@ -41,9 +149,40 @@ ULONG STDMETHODCALLTYPE ScriptHookPlugin::Release()
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Initialize(IWTSVirtualChannelManager* channelManager) HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Initialize(IWTSVirtualChannelManager* channelManager)
{ {
UNREFERENCED_PARAMETER(channelManager);
const hook::HookConfig config = hook::LoadConfig(); const hook::HookConfig config = hook::LoadConfig();
hook::Log(config.enableLogging, L"IWTSPlugin::Initialize aufgerufen."); 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<LPSTR>(hook::protocol::kChannelName),
0,
static_cast<IWTSListenerCallback*>(this),
&listener_);
if (FAILED(hr))
{
hook::Log(config.enableLogging,
L"DVC-Listener konnte nicht erstellt werden. HRESULT=" + std::to_wstring(static_cast<unsigned long>(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; return S_OK;
} }
@@ -54,12 +193,12 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Connected()
if (!config.enabled || !config.startOnConnect) 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; return S_OK;
} }
// Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Script gewartet. // Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Programm gewartet.
launcher_.Start(config); connectLauncher_.Start(config.connectProgram, config.enableLogging, L"RDP-Verbindungsprogramm");
return S_OK; return S_OK;
} }
@@ -71,7 +210,7 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Disconnected(DWORD disconnectCode)
if (config.stopOnDisconnect) if (config.stopOnDisconnect)
{ {
launcher_.Stop(config.enableLogging); connectLauncher_.Stop(config.enableLogging, L"RDP-Verbindungsprogramm");
} }
return S_OK; return S_OK;
} }
@@ -83,11 +222,64 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Terminated()
if (config.stopOnDisconnect) if (config.stopOnDisconnect)
{ {
launcher_.Stop(config.enableLogging); connectLauncher_.Stop(config.enableLogging, L"RDP-Verbindungsprogramm");
} }
else else
{ {
launcher_.Detach(config.enableLogging); connectLauncher_.Detach(config.enableLogging, L"RDP-Verbindungsprogramm");
} }
ReleaseDvcObjects();
return S_OK; 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<IWTSVirtualChannelCallback*>(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;
}
}
+13 -2
View File
@@ -4,7 +4,7 @@
#include <windows.h> #include <windows.h>
#include <tsvirtualchannels.h> #include <tsvirtualchannels.h>
class ScriptHookPlugin final : public IWTSPlugin class ScriptHookPlugin final : public IWTSPlugin, public IWTSListenerCallback
{ {
public: public:
ScriptHookPlugin(); ScriptHookPlugin();
@@ -13,14 +13,25 @@ public:
ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override; ULONG STDMETHODCALLTYPE Release() override;
// IWTSPlugin
HRESULT STDMETHODCALLTYPE Initialize(IWTSVirtualChannelManager* channelManager) override; HRESULT STDMETHODCALLTYPE Initialize(IWTSVirtualChannelManager* channelManager) override;
HRESULT STDMETHODCALLTYPE Connected() override; HRESULT STDMETHODCALLTYPE Connected() override;
HRESULT STDMETHODCALLTYPE Disconnected(DWORD disconnectCode) override; HRESULT STDMETHODCALLTYPE Disconnected(DWORD disconnectCode) override;
HRESULT STDMETHODCALLTYPE Terminated() override; HRESULT STDMETHODCALLTYPE Terminated() override;
// IWTSListenerCallback
HRESULT STDMETHODCALLTYPE OnNewChannelConnection(
IWTSVirtualChannel* channel,
BSTR data,
BOOL* accept,
IWTSVirtualChannelCallback** callback) override;
private: private:
~ScriptHookPlugin(); ~ScriptHookPlugin();
void ReleaseDvcObjects();
LONG refCount_ = 1; LONG refCount_ = 1;
hook::ProcessLauncher launcher_; hook::ProcessLauncher connectLauncher_;
IWTSVirtualChannelManager* channelManager_ = nullptr;
IWTSListener* listener_ = nullptr;
}; };
+208 -42
View File
@@ -1,22 +1,64 @@
# Plandent MSTSC Script Hook # 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 ## 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. - **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. ## Architektur
- `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.
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 ## Registry
@@ -31,30 +73,110 @@ Konfiguration:
```text ```text
HKCU\Software\Plandent\MstscScriptHook HKCU\Software\Plandent\MstscScriptHook
Enabled REG_DWORD 1 Enabled REG_DWORD 1
ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat EnableLogging REG_DWORD 1
Arguments REG_SZ MIN
WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS ; Automatischer Start bei RDP-Verbindung
StartOnConnect REG_DWORD 1 ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
StopOnDisconnect REG_DWORD 0 Arguments REG_SZ MIN
Hidden REG_DWORD 1 WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS
PreventDuplicates REG_DWORD 1 StartOnConnect REG_DWORD 1
EnableLogging 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 ...` - `.bat` / `.cmd` → `cmd.exe /D /S /C ...`
- `.ps1` → Windows PowerShell mit `-NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...` - `.ps1` → Windows PowerShell mit `-NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`
- `.exe` → direkter Start - `.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 ## 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.<Hash>
```
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 ## Logging
@@ -64,16 +186,28 @@ Bei aktiviertem Logging:
%LOCALAPPDATA%\Plandent\MstscScriptHook\plugin.log %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 ## Voraussetzungen zum Bauen
- Windows 10/11 x64 - Windows 10/11 x64 als Entwicklungsrechner
- Visual Studio 2022 oder Visual Studio Build Tools 2022 - Visual Studio 2022 oder entsprechende Build Tools
- Workload **Desktopentwicklung mit C++** - Workload **Desktopentwicklung mit C++**
- Windows 10/11 SDK - Windows 10/11 SDK
- .NET 8 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 ## Release bauen
PowerShell im Projektverzeichnis: PowerShell im Projektverzeichnis:
@@ -85,33 +219,65 @@ PowerShell im Projektverzeichnis:
Ergebnis: Ergebnis:
```text ```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. 1. `PlandentMstscScriptHook.exe` starten.
2. Script über `...` auswählen. 2. Gewünschten automatischen RDP-Start konfigurieren oder deaktivieren.
3. Für `RemoteVDDS_Receiver_TS.bat` als Argument `MIN` setzen. 3. Optional unter **Programmstart durch Terminalserver** das lokale Trigger-Programm einschließlich Argumenten konfigurieren.
4. Optional Arbeitsverzeichnis auswählen; standardmäßig sollte es dem Scriptverzeichnis entsprechen. 4. **Start durch Terminalserver-Trigger erlauben** aktivieren.
5. **Script testen** und anschließend **Test beenden**. 5. Optional beide Programme mit den Testbuttons lokal testen.
6. **Plugin installieren** anklicken. 6. **Plugin installieren** anklicken.
7. Alle bereits laufenden `mstsc.exe`-Instanzen schließen. 7. Alle bereits laufenden `mstsc.exe`-Instanzen vollständig schließen.
8. MSTSC neu starten und eine RDP-Verbindung herstellen. 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 ## 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. - Das Plugin ist für den klassischen Microsoft-RDC-Pluginmechanismus ausgelegt. Andere RDP-Clients müssen diesen Mechanismus unterstützen.
- `StopOnDisconnect` beendet nur den vom Plugin selbst gestarteten Root-Prozess. Beim RemoteVDDS-BAT ist das der dauerhaft laufende `cmd.exe`-Receiver. - Der Server-Trigger muss innerhalb einer aktiven RDP-Sitzung ausgeführt werden, wenn `WTS_CURRENT_SESSION` verwendet wird.
- Das Script läuft mit den Rechten des lokalen Benutzers, der `mstsc.exe` gestartet hat. - Das lokale Programm läuft mit den Rechten des Benutzers, der `mstsc.exe` auf dem Client gestartet hat.
- AppLocker/WDAC oder andere Application-Control-Richtlinien können das Laden einer nicht signierten DLL bzw. das Starten des Scripts blockieren. - 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.
+65
View File
@@ -0,0 +1,65 @@
#include "../Shared/TriggerProtocol.h"
#include <windows.h>
#include <wtsapi32.h>
#include <cwchar>
#include <stdlib.h>
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<LPSTR>(hook::protocol::kChannelName),
WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_MED);
if (channel == nullptr)
{
DebugError(L"WTSVirtualChannelOpenEx", GetLastError());
return static_cast<int>(ExitCode::ChannelOpenFailed);
}
ULONG bytesWritten = 0;
BOOL written = WTSVirtualChannelWrite(
channel,
const_cast<PCHAR>(hook::protocol::kStartMessage),
static_cast<ULONG>(hook::protocol::kStartMessageLength),
&bytesWritten);
DWORD writeError = written ? ERROR_SUCCESS : GetLastError();
WTSVirtualChannelClose(channel);
if (!written)
{
DebugError(L"WTSVirtualChannelWrite", writeError);
return static_cast<int>(ExitCode::WriteFailed);
}
if (bytesWritten != static_cast<ULONG>(hook::protocol::kStartMessageLength))
{
OutputDebugStringW(L"PlandentRdpClientTrigger: Trigger-Nachricht wurde nur teilweise geschrieben.\n");
return static_cast<int>(ExitCode::PartialWrite);
}
return static_cast<int>(ExitCode::Success);
}
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{A71B1703-2FD5-4777-A50C-6D6C21D1F4AE}</ProjectGuid>
<RootNamespace>PlandentRdpClientTrigger</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)..\artifacts\obj\$(Platform)\$(Configuration)\ServerTrigger\</IntDir>
<TargetName>PlandentRdpClientTrigger</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>false</GenerateDebugInformation>
<AdditionalDependencies>Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="Main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Shared\TriggerProtocol.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files"><UniqueIdentifier>{D6213E4F-B945-4B58-A331-B988E3EC325B}</UniqueIdentifier><Extensions>cpp;c</Extensions></Filter>
<Filter Include="Header Files"><UniqueIdentifier>{81A97E24-D824-427B-B039-F2CBE560B587}</UniqueIdentifier><Extensions>h;hpp</Extensions></Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Main.cpp"><Filter>Source Files</Filter></ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Shared\TriggerProtocol.h"><Filter>Header Files</Filter></ClInclude>
</ItemGroup>
</Project>
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstddef>
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;
}
+23 -7
View File
@@ -8,6 +8,7 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSCommandPath $Root = Split-Path -Parent $PSCommandPath
$PluginProject = Join-Path $Root 'Plugin\PlandentMstscScriptHook.vcxproj' $PluginProject = Join-Path $Root 'Plugin\PlandentMstscScriptHook.vcxproj'
$ServerTriggerProject = Join-Path $Root 'ServerTrigger\PlandentRdpClientTrigger.vcxproj'
$ConfiguratorProject = Join-Path $Root 'Configurator\Configurator.csproj' $ConfiguratorProject = Join-Path $Root 'Configurator\Configurator.csproj'
$Dist = Join-Path $Root 'dist' $Dist = Join-Path $Root 'dist'
$Artifacts = Join-Path $Root 'artifacts' $Artifacts = Join-Path $Root 'artifacts'
@@ -35,7 +36,8 @@ if ($Clean) {
New-Item -ItemType Directory -Path $Dist -Force | Out-Null New-Item -ItemType Directory -Path $Dist -Force | Out-Null
$MSBuild = Find-MSBuild $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' & $MSBuild $PluginProject /m /nologo "/p:Configuration=$Configuration" '/p:Platform=x64'
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "Native DLL Build fehlgeschlagen (ExitCode $LASTEXITCODE)." throw "Native DLL Build fehlgeschlagen (ExitCode $LASTEXITCODE)."
@@ -46,7 +48,19 @@ if (-not (Test-Path $PluginDll)) {
throw "Erwartete DLL wurde nicht erzeugt: $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 ` & dotnet publish $ConfiguratorProject `
-c $Configuration ` -c $Configuration `
-r win-x64 ` -r win-x64 `
@@ -60,13 +74,15 @@ if ($LASTEXITCODE -ne 0) {
throw "Configurator Publish fehlgeschlagen (ExitCode $LASTEXITCODE)." throw "Configurator Publish fehlgeschlagen (ExitCode $LASTEXITCODE)."
} }
$Exe = Join-Path $Dist 'PlandentMstscScriptHook.exe' $ConfiguratorExe = Join-Path $Dist 'PlandentMstscScriptHook.exe'
if (-not (Test-Path $Exe)) { if (-not (Test-Path $ConfiguratorExe)) {
throw "Erwartete EXE wurde nicht erzeugt: $Exe" throw "Erwartete Konfigurations-EXE wurde nicht erzeugt: $ConfiguratorExe"
} }
Write-Host '' Write-Host ''
Write-Host 'Build erfolgreich:' -ForegroundColor Green 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 ''
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.'
+21 -10
View File
@@ -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` Beispielwerte für den bisherigen automatischen RemoteVDDS-Start:
- 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
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.
+13 -3
View File
@@ -2,11 +2,21 @@ Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Plandent\MstscScriptHook] [HKEY_CURRENT_USER\Software\Plandent\MstscScriptHook]
"Enabled"=dword:00000001 "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" "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 "StartOnConnect"=dword:00000001
"StopOnDisconnect"=dword:00000000 "StopOnDisconnect"=dword:00000000
"Hidden"=dword:00000001 "Hidden"=dword:00000001
"PreventDuplicates"=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