Hinzufügen der Möglichkeit ein Programmstart auf dem Client durch die RDP Sitzung zu triggern
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+350
-119
@@ -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()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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 StopTest(bool silent)
|
private void StopConnectionTest(bool silent)
|
||||||
{
|
{
|
||||||
if (testProcess is null)
|
StopTestProcess(ref connectionTestProcess, stopConnectionTestButton, "Verbindungs-Test", silent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopTriggerTest(bool silent)
|
||||||
{
|
{
|
||||||
stopTestButton.Enabled = false;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string extension = Path.GetExtension(scriptPath).ToLowerInvariant();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -32,6 +74,9 @@ Konfiguration:
|
|||||||
```text
|
```text
|
||||||
HKCU\Software\Plandent\MstscScriptHook
|
HKCU\Software\Plandent\MstscScriptHook
|
||||||
Enabled REG_DWORD 1
|
Enabled REG_DWORD 1
|
||||||
|
EnableLogging REG_DWORD 1
|
||||||
|
|
||||||
|
; Automatischer Start bei RDP-Verbindung
|
||||||
ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
|
ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
|
||||||
Arguments REG_SZ MIN
|
Arguments REG_SZ MIN
|
||||||
WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS
|
WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS
|
||||||
@@ -39,22 +84,99 @@ HKCU\Software\Plandent\MstscScriptHook
|
|||||||
StopOnDisconnect REG_DWORD 0
|
StopOnDisconnect REG_DWORD 0
|
||||||
Hidden REG_DWORD 1
|
Hidden REG_DWORD 1
|
||||||
PreventDuplicates REG_DWORD 1
|
PreventDuplicates REG_DWORD 1
|
||||||
EnableLogging 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.
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user