Initialversion
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.vs/
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
artifacts/
|
||||||
|
dist/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>PlandentMstscScriptHook</AssemblyName>
|
||||||
|
<RootNamespace>Plandent.MstscScriptHook.Configurator</RootNamespace>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<PublishReadyToRun>false</PublishReadyToRun>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
<PluginPayloadPath>$(MSBuildProjectDirectory)\..\artifacts\x64\$(Configuration)\PlandentMstscScriptHook.dll</PluginPayloadPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="$(PluginPayloadPath)" LogicalName="PlandentMstscScriptHook.Native.PlandentMstscScriptHook.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="CheckNativePlugin" BeforeTargets="CreateManifestResourceNames">
|
||||||
|
<Error Condition="!Exists('$(PluginPayloadPath)')" Text="Native Plugin-DLL fehlt: $(PluginPayloadPath). Zuerst Plugin bauen oder build-release.ps1 verwenden." />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Plandent.MstscScriptHook.Configurator;
|
||||||
|
|
||||||
|
internal sealed class HookConfig
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public string ScriptPath { get; set; } = string.Empty;
|
||||||
|
public string Arguments { get; set; } = string.Empty;
|
||||||
|
public string WorkingDirectory { get; set; } = string.Empty;
|
||||||
|
public bool StartOnConnect { get; set; } = true;
|
||||||
|
public bool StopOnDisconnect { get; set; }
|
||||||
|
public bool Hidden { get; set; } = true;
|
||||||
|
public bool PreventDuplicates { get; set; } = true;
|
||||||
|
public bool EnableLogging { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Plandent.MstscScriptHook.Configurator;
|
||||||
|
|
||||||
|
internal sealed class MainForm : Form
|
||||||
|
{
|
||||||
|
private readonly TextBox scriptPathTextBox = new() { Dock = DockStyle.Fill };
|
||||||
|
private readonly TextBox argumentsTextBox = new() { Dock = DockStyle.Fill };
|
||||||
|
private readonly TextBox workingDirectoryTextBox = new() { Dock = DockStyle.Fill };
|
||||||
|
|
||||||
|
private readonly CheckBox enabledCheckBox = new() { Text = "Plugin-Funktion aktiviert", AutoSize = true };
|
||||||
|
private readonly CheckBox startOnConnectCheckBox = new() { Text = "Script bei erfolgreicher RDP-Verbindung starten", AutoSize = true };
|
||||||
|
private readonly CheckBox stopOnDisconnectCheckBox = new() { Text = "Script bei RDP-Trennung beenden", AutoSize = true };
|
||||||
|
private readonly CheckBox hiddenCheckBox = new() { Text = "Script unsichtbar starten", AutoSize = true };
|
||||||
|
private readonly CheckBox preventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", AutoSize = true };
|
||||||
|
private readonly CheckBox loggingCheckBox = new() { Text = "Plugin-Logging aktivieren", AutoSize = true };
|
||||||
|
|
||||||
|
private readonly Label statusLabel = new()
|
||||||
|
{
|
||||||
|
AutoSize = false,
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
TextAlign = ContentAlignment.MiddleLeft,
|
||||||
|
Padding = new Padding(8)
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly Button stopTestButton = new() { Text = "Test beenden", AutoSize = true, Enabled = false };
|
||||||
|
private Process? testProcess;
|
||||||
|
|
||||||
|
public MainForm()
|
||||||
|
{
|
||||||
|
Text = "Plandent MSTSC Script Hook";
|
||||||
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
MinimumSize = new Size(760, 590);
|
||||||
|
Size = new Size(820, 640);
|
||||||
|
Font = new Font("Segoe UI", 9F);
|
||||||
|
|
||||||
|
Controls.Add(BuildMainLayout());
|
||||||
|
Load += (_, _) => LoadConfiguration();
|
||||||
|
FormClosed += (_, _) => DisposeTestProcessHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control BuildMainLayout()
|
||||||
|
{
|
||||||
|
var root = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
Padding = new Padding(18),
|
||||||
|
ColumnCount = 1,
|
||||||
|
RowCount = 6
|
||||||
|
};
|
||||||
|
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||||
|
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
|
|
||||||
|
var title = new Label
|
||||||
|
{
|
||||||
|
Text = "MSTSC Script Hook",
|
||||||
|
Font = new Font(Font.FontFamily, 16F, FontStyle.Bold),
|
||||||
|
AutoSize = true,
|
||||||
|
Margin = new Padding(0, 0, 0, 4)
|
||||||
|
};
|
||||||
|
root.Controls.Add(title, 0, 0);
|
||||||
|
|
||||||
|
var description = new Label
|
||||||
|
{
|
||||||
|
Text = "Startet bei einer erfolgreichen klassischen Remotedesktop-Verbindung lokal ein konfiguriertes Script im Hintergrund.",
|
||||||
|
AutoSize = true,
|
||||||
|
MaximumSize = new Size(740, 0),
|
||||||
|
Margin = new Padding(0, 0, 0, 16)
|
||||||
|
};
|
||||||
|
root.Controls.Add(description, 0, 1);
|
||||||
|
|
||||||
|
root.Controls.Add(BuildFileGrid(), 0, 2);
|
||||||
|
root.Controls.Add(BuildOptionsGroup(), 0, 3);
|
||||||
|
root.Controls.Add(BuildStatusPanel(), 0, 4);
|
||||||
|
root.Controls.Add(BuildButtons(), 0, 5);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control BuildFileGrid()
|
||||||
|
{
|
||||||
|
var grid = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
ColumnCount = 3,
|
||||||
|
RowCount = 3,
|
||||||
|
Margin = new Padding(0, 0, 0, 14)
|
||||||
|
};
|
||||||
|
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||||
|
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
|
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||||
|
|
||||||
|
grid.Controls.Add(CreateFieldLabel("Script:"), 0, 0);
|
||||||
|
grid.Controls.Add(scriptPathTextBox, 1, 0);
|
||||||
|
var browseScriptButton = new Button { Text = "…", Width = 42, Height = scriptPathTextBox.PreferredHeight + 4 };
|
||||||
|
browseScriptButton.Click += (_, _) => BrowseScript();
|
||||||
|
grid.Controls.Add(browseScriptButton, 2, 0);
|
||||||
|
|
||||||
|
grid.Controls.Add(CreateFieldLabel("Argumente:"), 0, 1);
|
||||||
|
grid.Controls.Add(argumentsTextBox, 1, 1);
|
||||||
|
grid.SetColumnSpan(argumentsTextBox, 2);
|
||||||
|
|
||||||
|
grid.Controls.Add(CreateFieldLabel("Arbeitsverzeichnis:"), 0, 2);
|
||||||
|
grid.Controls.Add(workingDirectoryTextBox, 1, 2);
|
||||||
|
var browseWorkingButton = new Button { Text = "…", Width = 42, Height = workingDirectoryTextBox.PreferredHeight + 4 };
|
||||||
|
browseWorkingButton.Click += (_, _) => BrowseWorkingDirectory();
|
||||||
|
grid.Controls.Add(browseWorkingButton, 2, 2);
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label CreateFieldLabel(string text)
|
||||||
|
{
|
||||||
|
return new Label
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
AutoSize = true,
|
||||||
|
Anchor = AnchorStyles.Left,
|
||||||
|
Margin = new Padding(0, 6, 12, 6)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control BuildOptionsGroup()
|
||||||
|
{
|
||||||
|
var group = new GroupBox
|
||||||
|
{
|
||||||
|
Text = "Verhalten",
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
Padding = new Padding(12),
|
||||||
|
Margin = new Padding(0, 0, 0, 14)
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
FlowDirection = FlowDirection.TopDown,
|
||||||
|
WrapContents = false,
|
||||||
|
AutoSize = true
|
||||||
|
};
|
||||||
|
options.Controls.Add(enabledCheckBox);
|
||||||
|
options.Controls.Add(startOnConnectCheckBox);
|
||||||
|
options.Controls.Add(stopOnDisconnectCheckBox);
|
||||||
|
options.Controls.Add(hiddenCheckBox);
|
||||||
|
options.Controls.Add(preventDuplicatesCheckBox);
|
||||||
|
options.Controls.Add(loggingCheckBox);
|
||||||
|
group.Controls.Add(options);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control BuildStatusPanel()
|
||||||
|
{
|
||||||
|
var panel = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 58,
|
||||||
|
BorderStyle = BorderStyle.FixedSingle,
|
||||||
|
Margin = new Padding(0, 0, 0, 14)
|
||||||
|
};
|
||||||
|
panel.Controls.Add(statusLabel);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control BuildButtons()
|
||||||
|
{
|
||||||
|
var buttons = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
|
WrapContents = true,
|
||||||
|
Margin = new Padding(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
var testButton = new Button { Text = "Script testen", AutoSize = true };
|
||||||
|
testButton.Click += (_, _) => StartTest();
|
||||||
|
stopTestButton.Click += (_, _) => StopTest();
|
||||||
|
|
||||||
|
var saveButton = new Button { Text = "Konfiguration speichern", AutoSize = true };
|
||||||
|
saveButton.Click += (_, _) => SaveConfiguration(showConfirmation: true);
|
||||||
|
|
||||||
|
var installButton = new Button { Text = "Plugin installieren", AutoSize = true };
|
||||||
|
installButton.Click += (_, _) => InstallPlugin();
|
||||||
|
|
||||||
|
var removeButton = new Button { Text = "Plugin entfernen", AutoSize = true };
|
||||||
|
removeButton.Click += (_, _) => RemovePlugin();
|
||||||
|
|
||||||
|
var logButton = new Button { Text = "Log öffnen", AutoSize = true };
|
||||||
|
logButton.Click += (_, _) => OpenLog();
|
||||||
|
|
||||||
|
buttons.Controls.Add(testButton);
|
||||||
|
buttons.Controls.Add(stopTestButton);
|
||||||
|
buttons.Controls.Add(saveButton);
|
||||||
|
buttons.Controls.Add(installButton);
|
||||||
|
buttons.Controls.Add(removeButton);
|
||||||
|
buttons.Controls.Add(logButton);
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BrowseScript()
|
||||||
|
{
|
||||||
|
using var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Script oder Programm auswählen",
|
||||||
|
Filter = "Unterstützte Dateien (*.bat;*.cmd;*.ps1;*.exe)|*.bat;*.cmd;*.ps1;*.exe|Alle Dateien (*.*)|*.*",
|
||||||
|
CheckFileExists = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(scriptPathTextBox.Text))
|
||||||
|
{
|
||||||
|
string expanded = Environment.ExpandEnvironmentVariables(scriptPathTextBox.Text);
|
||||||
|
if (File.Exists(expanded))
|
||||||
|
{
|
||||||
|
dialog.InitialDirectory = Path.GetDirectoryName(expanded);
|
||||||
|
dialog.FileName = Path.GetFileName(expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPathTextBox.Text = dialog.FileName;
|
||||||
|
if (string.IsNullOrWhiteSpace(workingDirectoryTextBox.Text))
|
||||||
|
{
|
||||||
|
workingDirectoryTextBox.Text = Path.GetDirectoryName(dialog.FileName) ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(argumentsTextBox.Text) &&
|
||||||
|
Path.GetFileName(dialog.FileName).Contains("RemoteVDDS_Receiver_TS", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
argumentsTextBox.Text = "MIN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BrowseWorkingDirectory()
|
||||||
|
{
|
||||||
|
using var dialog = new FolderBrowserDialog
|
||||||
|
{
|
||||||
|
Description = "Arbeitsverzeichnis auswählen",
|
||||||
|
UseDescriptionForTitle = true,
|
||||||
|
ShowNewFolderButton = true
|
||||||
|
};
|
||||||
|
|
||||||
|
string expanded = Environment.ExpandEnvironmentVariables(workingDirectoryTextBox.Text);
|
||||||
|
if (Directory.Exists(expanded))
|
||||||
|
{
|
||||||
|
dialog.SelectedPath = expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||||
|
{
|
||||||
|
workingDirectoryTextBox.Text = dialog.SelectedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HookConfig ReadForm()
|
||||||
|
{
|
||||||
|
return new HookConfig
|
||||||
|
{
|
||||||
|
Enabled = enabledCheckBox.Checked,
|
||||||
|
ScriptPath = scriptPathTextBox.Text.Trim(),
|
||||||
|
Arguments = argumentsTextBox.Text.Trim(),
|
||||||
|
WorkingDirectory = workingDirectoryTextBox.Text.Trim(),
|
||||||
|
StartOnConnect = startOnConnectCheckBox.Checked,
|
||||||
|
StopOnDisconnect = stopOnDisconnectCheckBox.Checked,
|
||||||
|
Hidden = hiddenCheckBox.Checked,
|
||||||
|
PreventDuplicates = preventDuplicatesCheckBox.Checked,
|
||||||
|
EnableLogging = loggingCheckBox.Checked
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadConfiguration()
|
||||||
|
{
|
||||||
|
HookConfig config = RegistryConfig.Load();
|
||||||
|
scriptPathTextBox.Text = config.ScriptPath;
|
||||||
|
argumentsTextBox.Text = config.Arguments;
|
||||||
|
workingDirectoryTextBox.Text = config.WorkingDirectory;
|
||||||
|
enabledCheckBox.Checked = config.Enabled;
|
||||||
|
startOnConnectCheckBox.Checked = config.StartOnConnect;
|
||||||
|
stopOnDisconnectCheckBox.Checked = config.StopOnDisconnect;
|
||||||
|
hiddenCheckBox.Checked = config.Hidden;
|
||||||
|
preventDuplicatesCheckBox.Checked = config.PreventDuplicates;
|
||||||
|
loggingCheckBox.Checked = config.EnableLogging;
|
||||||
|
RefreshStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SaveConfiguration(bool showConfirmation)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HookConfig config = ReadForm();
|
||||||
|
ValidateConfig(config, requireScript: false);
|
||||||
|
RegistryConfig.Save(config);
|
||||||
|
if (showConfirmation)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Konfiguration wurde gespeichert.", "MSTSC Script Hook", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
}
|
||||||
|
RefreshStatus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, ex.Message, "Konfiguration konnte nicht gespeichert werden", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InstallPlugin()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HookConfig config = ReadForm();
|
||||||
|
ValidateConfig(config, requireScript: true);
|
||||||
|
RegistryConfig.Save(config);
|
||||||
|
PluginInstaller.Install();
|
||||||
|
RefreshStatus();
|
||||||
|
MessageBox.Show(
|
||||||
|
this,
|
||||||
|
"Plugin wurde für den aktuellen Benutzer installiert und die Konfiguration gespeichert.\n\n" +
|
||||||
|
"Bereits geöffnete mstsc.exe-Fenster vollständig schließen und anschließend neu starten, damit die DLL geladen wird.",
|
||||||
|
"Installation abgeschlossen",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Information);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemovePlugin()
|
||||||
|
{
|
||||||
|
UninstallResult result = PluginInstaller.Uninstall();
|
||||||
|
RefreshStatus();
|
||||||
|
MessageBox.Show(
|
||||||
|
this,
|
||||||
|
result.Message,
|
||||||
|
"Plugin entfernen",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
result.FullyRemoved ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartTest()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StopTest(silent: true);
|
||||||
|
HookConfig config = ReadForm();
|
||||||
|
ValidateConfig(config, requireScript: true);
|
||||||
|
testProcess = ScriptTestLauncher.Start(config);
|
||||||
|
stopTestButton.Enabled = true;
|
||||||
|
statusLabel.Text = $"Testprozess gestartet: PID {testProcess.Id}. Der Test läuft unabhängig von MSTSC.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, ex.Message, "Script-Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopTest() => StopTest(silent: false);
|
||||||
|
|
||||||
|
private void StopTest(bool silent)
|
||||||
|
{
|
||||||
|
if (testProcess is null)
|
||||||
|
{
|
||||||
|
stopTestButton.Enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!testProcess.HasExited)
|
||||||
|
{
|
||||||
|
testProcess.Kill(entireProcessTree: true);
|
||||||
|
testProcess.WaitForExit(3000);
|
||||||
|
}
|
||||||
|
if (!silent)
|
||||||
|
{
|
||||||
|
statusLabel.Text = "Testprozess wurde beendet.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!silent)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, ex.Message, "Testprozess konnte nicht beendet werden", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DisposeTestProcessHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeTestProcessHandle()
|
||||||
|
{
|
||||||
|
testProcess?.Dispose();
|
||||||
|
testProcess = null;
|
||||||
|
stopTestButton.Enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenLog()
|
||||||
|
{
|
||||||
|
string logPath = Path.Combine(PluginInstaller.InstallDirectory, "plugin.log");
|
||||||
|
if (!File.Exists(logPath))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Es existiert noch kein Plugin-Log.", "MSTSC Script Hook", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = logPath,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshStatus()
|
||||||
|
{
|
||||||
|
bool installed = PluginInstaller.IsInstalled(out string pluginStatus);
|
||||||
|
string configStatus = string.IsNullOrWhiteSpace(scriptPathTextBox.Text)
|
||||||
|
? " Kein Script konfiguriert."
|
||||||
|
: $" Script: {scriptPathTextBox.Text}";
|
||||||
|
statusLabel.Text = (installed ? "✓ " : "○ ") + pluginStatus + configStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateConfig(HookConfig config, bool requireScript)
|
||||||
|
{
|
||||||
|
if (!requireScript && string.IsNullOrWhiteSpace(config.ScriptPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string scriptPath = Environment.ExpandEnvironmentVariables(config.ScriptPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(scriptPath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Bitte ein Script auswählen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(scriptPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Das ausgewählte Script wurde nicht gefunden.", scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
string extension = Path.GetExtension(scriptPath).ToLowerInvariant();
|
||||||
|
if (extension is not (".bat" or ".cmd" or ".ps1" or ".exe"))
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unterstützt werden .bat, .cmd, .ps1 und .exe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(config.WorkingDirectory))
|
||||||
|
{
|
||||||
|
string workDir = Environment.ExpandEnvironmentVariables(config.WorkingDirectory);
|
||||||
|
if (!Directory.Exists(workDir))
|
||||||
|
{
|
||||||
|
throw new DirectoryNotFoundException("Das Arbeitsverzeichnis wurde nicht gefunden: " + workDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Plandent.MstscScriptHook.Configurator;
|
||||||
|
|
||||||
|
internal static class PluginInstaller
|
||||||
|
{
|
||||||
|
private const string ResourceName = "PlandentMstscScriptHook.Native.PlandentMstscScriptHook.dll";
|
||||||
|
public const string DllFileName = "PlandentMstscScriptHook.dll";
|
||||||
|
|
||||||
|
public static string InstallDirectory => Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Plandent",
|
||||||
|
"MstscScriptHook");
|
||||||
|
|
||||||
|
public static string InstalledDllPath => Path.Combine(InstallDirectory, DllFileName);
|
||||||
|
|
||||||
|
public static void Install()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(InstallDirectory);
|
||||||
|
ExtractPluginBinary(InstalledDllPath);
|
||||||
|
RegistryConfig.RegisterPlugin(InstalledDllPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UninstallResult Uninstall()
|
||||||
|
{
|
||||||
|
RegistryConfig.UnregisterPlugin();
|
||||||
|
|
||||||
|
if (!File.Exists(InstalledDllPath))
|
||||||
|
{
|
||||||
|
return new UninstallResult(true, "Plugin-Registrierung entfernt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(InstalledDllPath);
|
||||||
|
TryDeleteEmptyInstallDirectory();
|
||||||
|
return new UninstallResult(true, "Plugin-Registrierung und DLL wurden entfernt.");
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return new UninstallResult(
|
||||||
|
false,
|
||||||
|
"Die Registrierung wurde entfernt. Die DLL wird noch von mstsc.exe verwendet. " +
|
||||||
|
"Alle Remote-Desktop-Clients schließen und die Datei anschließend löschen bzw. erneut auf „Plugin entfernen“ klicken.");
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
return new UninstallResult(false, "Registrierung entfernt, DLL konnte aber nicht gelöscht werden: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsInstalled(out string status)
|
||||||
|
{
|
||||||
|
string? registered = RegistryConfig.GetRegisteredPluginPath();
|
||||||
|
if (string.IsNullOrWhiteSpace(registered))
|
||||||
|
{
|
||||||
|
status = "Plugin ist nicht registriert.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string expanded = Environment.ExpandEnvironmentVariables(registered);
|
||||||
|
if (!File.Exists(expanded))
|
||||||
|
{
|
||||||
|
status = $"Plugin ist registriert, DLL fehlt jedoch: {expanded}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = $"Plugin ist registriert: {expanded}";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExtractPluginBinary(string destination)
|
||||||
|
{
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
using Stream? resource = assembly.GetManifestResourceStream(ResourceName);
|
||||||
|
if (resource is not null)
|
||||||
|
{
|
||||||
|
using FileStream output = new(destination, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
resource.CopyTo(output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Praktischer Fallback für Debugging/Entwicklung: DLL direkt neben der EXE akzeptieren.
|
||||||
|
string adjacent = Path.Combine(AppContext.BaseDirectory, DllFileName);
|
||||||
|
if (File.Exists(adjacent))
|
||||||
|
{
|
||||||
|
File.Copy(adjacent, destination, overwrite: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
"Die native Plugin-DLL ist weder als Resource eingebettet noch neben der Konfigurations-EXE vorhanden. " +
|
||||||
|
"Bitte zuerst build-release.ps1 ausführen.",
|
||||||
|
DllFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteEmptyInstallDirectory()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(InstallDirectory) && !Directory.EnumerateFileSystemEntries(InstallDirectory).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(InstallDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Unkritisch: ein leeres Verzeichnis darf bestehen bleiben.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record UninstallResult(bool FullyRemoved, string Message);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace Plandent.MstscScriptHook.Configurator;
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
private static void Main()
|
||||||
|
{
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
Application.Run(new MainForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace Plandent.MstscScriptHook.Configurator;
|
||||||
|
|
||||||
|
internal static class RegistryConfig
|
||||||
|
{
|
||||||
|
public const string ConfigKeyPath = @"Software\Plandent\MstscScriptHook";
|
||||||
|
public const string AddInKeyPath = @"Software\Microsoft\Terminal Server Client\Default\AddIns\PlandentMstscScriptHook";
|
||||||
|
|
||||||
|
public static HookConfig Load()
|
||||||
|
{
|
||||||
|
var config = new HookConfig();
|
||||||
|
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(ConfigKeyPath, writable: false);
|
||||||
|
if (key is null)
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Enabled = ReadBool(key, "Enabled", true);
|
||||||
|
config.ScriptPath = ReadString(key, "ScriptPath");
|
||||||
|
config.Arguments = ReadString(key, "Arguments");
|
||||||
|
config.WorkingDirectory = ReadString(key, "WorkingDirectory");
|
||||||
|
config.StartOnConnect = ReadBool(key, "StartOnConnect", true);
|
||||||
|
config.StopOnDisconnect = ReadBool(key, "StopOnDisconnect", false);
|
||||||
|
config.Hidden = ReadBool(key, "Hidden", true);
|
||||||
|
config.PreventDuplicates = ReadBool(key, "PreventDuplicates", true);
|
||||||
|
config.EnableLogging = ReadBool(key, "EnableLogging", true);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save(HookConfig config)
|
||||||
|
{
|
||||||
|
using RegistryKey key = Registry.CurrentUser.CreateSubKey(ConfigKeyPath, writable: true)
|
||||||
|
?? throw new InvalidOperationException("Registry-Schlüssel konnte nicht erstellt werden.");
|
||||||
|
|
||||||
|
key.SetValue("Enabled", config.Enabled ? 1 : 0, RegistryValueKind.DWord);
|
||||||
|
key.SetValue("ScriptPath", config.ScriptPath ?? string.Empty, RegistryValueKind.ExpandString);
|
||||||
|
key.SetValue("Arguments", config.Arguments ?? string.Empty, RegistryValueKind.String);
|
||||||
|
key.SetValue("WorkingDirectory", config.WorkingDirectory ?? string.Empty, RegistryValueKind.ExpandString);
|
||||||
|
key.SetValue("StartOnConnect", config.StartOnConnect ? 1 : 0, RegistryValueKind.DWord);
|
||||||
|
key.SetValue("StopOnDisconnect", config.StopOnDisconnect ? 1 : 0, RegistryValueKind.DWord);
|
||||||
|
key.SetValue("Hidden", config.Hidden ? 1 : 0, RegistryValueKind.DWord);
|
||||||
|
key.SetValue("PreventDuplicates", config.PreventDuplicates ? 1 : 0, RegistryValueKind.DWord);
|
||||||
|
key.SetValue("EnableLogging", config.EnableLogging ? 1 : 0, RegistryValueKind.DWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetRegisteredPluginPath()
|
||||||
|
{
|
||||||
|
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(AddInKeyPath, writable: false);
|
||||||
|
return key?.GetValue("Name", null, RegistryValueOptions.DoNotExpandEnvironmentNames) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterPlugin(string dllPath)
|
||||||
|
{
|
||||||
|
using RegistryKey key = Registry.CurrentUser.CreateSubKey(AddInKeyPath, writable: true)
|
||||||
|
?? throw new InvalidOperationException("MSTSC AddIn-Registry-Schlüssel konnte nicht erstellt werden.");
|
||||||
|
key.SetValue("Name", dllPath, RegistryValueKind.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UnregisterPlugin()
|
||||||
|
{
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(AddInKeyPath, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReadBool(RegistryKey key, string name, bool defaultValue)
|
||||||
|
{
|
||||||
|
object? value = key.GetValue(name);
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
int i => i != 0,
|
||||||
|
long l => l != 0,
|
||||||
|
_ => defaultValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadString(RegistryKey key, string name, string defaultValue = "")
|
||||||
|
{
|
||||||
|
return key.GetValue(name, defaultValue, RegistryValueOptions.DoNotExpandEnvironmentNames) as string
|
||||||
|
?? defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Plandent.MstscScriptHook.Configurator;
|
||||||
|
|
||||||
|
internal static class ScriptTestLauncher
|
||||||
|
{
|
||||||
|
public static Process Start(HookConfig config)
|
||||||
|
{
|
||||||
|
string scriptPath = Environment.ExpandEnvironmentVariables(config.ScriptPath ?? string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(scriptPath) || !File.Exists(scriptPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Das ausgewählte Script wurde nicht gefunden.", scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
string extension = Path.GetExtension(scriptPath).ToLowerInvariant();
|
||||||
|
string workingDirectory = string.IsNullOrWhiteSpace(config.WorkingDirectory)
|
||||||
|
? Path.GetDirectoryName(scriptPath) ?? Environment.CurrentDirectory
|
||||||
|
: Environment.ExpandEnvironmentVariables(config.WorkingDirectory);
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = config.Hidden,
|
||||||
|
WindowStyle = config.Hidden ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal,
|
||||||
|
WorkingDirectory = workingDirectory
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (extension)
|
||||||
|
{
|
||||||
|
case ".bat":
|
||||||
|
case ".cmd":
|
||||||
|
startInfo.FileName = Environment.GetEnvironmentVariable("ComSpec")
|
||||||
|
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
|
||||||
|
startInfo.ArgumentList.Add("/D");
|
||||||
|
startInfo.ArgumentList.Add("/S");
|
||||||
|
startInfo.ArgumentList.Add("/C");
|
||||||
|
startInfo.ArgumentList.Add(BuildCmdCommand(scriptPath, config.Arguments));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ".ps1":
|
||||||
|
startInfo.FileName = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.System),
|
||||||
|
"WindowsPowerShell",
|
||||||
|
"v1.0",
|
||||||
|
"powershell.exe");
|
||||||
|
startInfo.ArgumentList.Add("-NoLogo");
|
||||||
|
startInfo.ArgumentList.Add("-NoProfile");
|
||||||
|
startInfo.ArgumentList.Add("-NonInteractive");
|
||||||
|
startInfo.ArgumentList.Add("-ExecutionPolicy");
|
||||||
|
startInfo.ArgumentList.Add("Bypass");
|
||||||
|
startInfo.ArgumentList.Add("-File");
|
||||||
|
startInfo.ArgumentList.Add(scriptPath);
|
||||||
|
AddRawArguments(startInfo, config.Arguments);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ".exe":
|
||||||
|
startInfo.FileName = scriptPath;
|
||||||
|
AddRawArguments(startInfo, config.Arguments);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException("Unterstützt werden .bat, .cmd, .ps1 und .exe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Process.Start(startInfo)
|
||||||
|
?? throw new InvalidOperationException("Der Testprozess konnte nicht gestartet werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCmdCommand(string scriptPath, string? arguments)
|
||||||
|
{
|
||||||
|
string command = QuoteForCmd(scriptPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(arguments))
|
||||||
|
{
|
||||||
|
command += " " + arguments;
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string QuoteForCmd(string value)
|
||||||
|
{
|
||||||
|
return "\"" + value.Replace("\"", "\\\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Die GUI behandelt das Argumentfeld bewusst als Kommandozeilen-Argumentstring.
|
||||||
|
// Für den Test wird eine einfache Windows-Argumentzerlegung verwendet. Das Plugin selbst
|
||||||
|
// übergibt den String unverändert an cmd.exe/PowerShell bzw. an die EXE-Kommandozeile.
|
||||||
|
private static void AddRawArguments(ProcessStartInfo startInfo, string? raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string argument in CommandLineSplitter.Split(raw))
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add(argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class CommandLineSplitter
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<string> Split(string commandLine)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
var current = new System.Text.StringBuilder();
|
||||||
|
bool inQuotes = false;
|
||||||
|
int backslashes = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < commandLine.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = commandLine[i];
|
||||||
|
|
||||||
|
if (ch == '\\')
|
||||||
|
{
|
||||||
|
backslashes++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '"')
|
||||||
|
{
|
||||||
|
current.Append('\\', backslashes / 2);
|
||||||
|
if (backslashes % 2 == 0)
|
||||||
|
{
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current.Append('"');
|
||||||
|
}
|
||||||
|
backslashes = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.Append('\\', backslashes);
|
||||||
|
backslashes = 0;
|
||||||
|
|
||||||
|
if (char.IsWhiteSpace(ch) && !inQuotes)
|
||||||
|
{
|
||||||
|
if (current.Length > 0)
|
||||||
|
{
|
||||||
|
result.Add(current.ToString());
|
||||||
|
current.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current.Append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current.Append('\\', backslashes);
|
||||||
|
if (current.Length > 0)
|
||||||
|
{
|
||||||
|
result.Add(current.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "PlandentMstscScriptHook.Plugin", "Plugin\PlandentMstscScriptHook.vcxproj", "{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlandentMstscScriptHook.Configurator", "Configurator\Configurator.csproj", "{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA} = {D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}.Release|x64.Build.0 = Release|x64
|
||||||
|
{935FA3CC-4D72-4AE5-A23B-A74AE54A873A}.Debug|x64.ActiveCfg = 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.Build.0 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "ScriptHookPlugin.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <cchannel.h>
|
||||||
|
#include <tsvirtualchannels.h>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
|
||||||
|
{
|
||||||
|
UNREFERENCED_PARAMETER(reserved);
|
||||||
|
if (reason == DLL_PROCESS_ATTACH)
|
||||||
|
{
|
||||||
|
DisableThreadLibraryCalls(module);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Der Prototyp ist laut Microsoft nicht in einem öffentlichen Header enthalten
|
||||||
|
// und muss exakt in dieser Form exportiert werden.
|
||||||
|
extern "C" __declspec(dllexport) HRESULT VCAPITYPE VirtualChannelGetInstance(
|
||||||
|
_In_ REFIID refiid,
|
||||||
|
_Inout_ ULONG* pNumObjs,
|
||||||
|
_Out_opt_ VOID** ppObjArray)
|
||||||
|
{
|
||||||
|
if (pNumObjs == nullptr)
|
||||||
|
{
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refiid != __uuidof(IWTSPlugin))
|
||||||
|
{
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr ULONG requiredObjects = 1;
|
||||||
|
if (ppObjArray == nullptr)
|
||||||
|
{
|
||||||
|
*pNumObjs = requiredObjects;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*pNumObjs < requiredObjects)
|
||||||
|
{
|
||||||
|
*pNumObjs = requiredObjects;
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* plugin = new (std::nothrow) ScriptHookPlugin();
|
||||||
|
if (plugin == nullptr)
|
||||||
|
{
|
||||||
|
return E_OUTOFMEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
ppObjArray[0] = static_cast<IWTSPlugin*>(plugin);
|
||||||
|
*pNumObjs = requiredObjects;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#include "Logging.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <iterator>
|
||||||
|
#include <cwchar>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::wstring GetLogDirectory()
|
||||||
|
{
|
||||||
|
wchar_t localAppData[32768]{};
|
||||||
|
DWORD len = GetEnvironmentVariableW(L"LOCALAPPDATA", localAppData, static_cast<DWORD>(std::size(localAppData)));
|
||||||
|
if (len == 0 || len >= std::size(localAppData))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring base(localAppData);
|
||||||
|
std::wstring plandent = base + L"\\Plandent";
|
||||||
|
std::wstring hookDir = plandent + L"\\MstscScriptHook";
|
||||||
|
CreateDirectoryW(plandent.c_str(), nullptr);
|
||||||
|
CreateDirectoryW(hookDir.c_str(), nullptr);
|
||||||
|
return hookDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ToUtf8(const std::wstring& value)
|
||||||
|
{
|
||||||
|
if (value.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
int needed = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
||||||
|
if (needed <= 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::string result(static_cast<size_t>(needed), '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), needed, nullptr, nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace hook
|
||||||
|
{
|
||||||
|
std::wstring FormatWin32Error(unsigned long errorCode)
|
||||||
|
{
|
||||||
|
wchar_t* buffer = nullptr;
|
||||||
|
DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||||
|
DWORD length = FormatMessageW(flags, nullptr, errorCode, 0, reinterpret_cast<wchar_t*>(&buffer), 0, nullptr);
|
||||||
|
if (length == 0 || buffer == nullptr)
|
||||||
|
{
|
||||||
|
return L"Win32-Fehler " + std::to_wstring(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring result(buffer, length);
|
||||||
|
LocalFree(buffer);
|
||||||
|
while (!result.empty() && (result.back() == L'\r' || result.back() == L'\n' || result.back() == L' '))
|
||||||
|
{
|
||||||
|
result.pop_back();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Log(bool enabled, const std::wstring& message)
|
||||||
|
{
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring directory = GetLogDirectory();
|
||||||
|
if (directory.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SYSTEMTIME st{};
|
||||||
|
GetLocalTime(&st);
|
||||||
|
|
||||||
|
wchar_t prefix[128]{};
|
||||||
|
swprintf_s(prefix, std::size(prefix), L"%04u-%02u-%02u %02u:%02u:%02u.%03u [PID:%lu TID:%lu] ",
|
||||||
|
st.wYear, st.wMonth, st.wDay,
|
||||||
|
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
|
||||||
|
GetCurrentProcessId(), GetCurrentThreadId());
|
||||||
|
|
||||||
|
std::wstring fullLine = std::wstring(prefix) + message + L"\r\n";
|
||||||
|
std::string utf8 = ToUtf8(fullLine);
|
||||||
|
if (utf8.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring logPath = directory + L"\\plugin.log";
|
||||||
|
HANDLE file = CreateFileW(
|
||||||
|
logPath.c_str(),
|
||||||
|
FILE_APPEND_DATA,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
nullptr,
|
||||||
|
OPEN_ALWAYS,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
if (file == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD written = 0;
|
||||||
|
WriteFile(file, utf8.data(), static_cast<DWORD>(utf8.size()), &written, nullptr);
|
||||||
|
CloseHandle(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace hook
|
||||||
|
{
|
||||||
|
void Log(bool enabled, const std::wstring& message);
|
||||||
|
std::wstring FormatWin32Error(unsigned long errorCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?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>{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}</ProjectGuid>
|
||||||
|
<RootNamespace>PlandentMstscScriptHook</RootNamespace>
|
||||||
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||||
|
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||||
|
<ConfigurationType>DynamicLibrary</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)\Plugin\</IntDir>
|
||||||
|
<TargetName>PlandentMstscScriptHook</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>
|
||||||
|
<ModuleDefinitionFile>exports.def</ModuleDefinitionFile>
|
||||||
|
<AdditionalDependencies>Advapi32.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>
|
||||||
|
<ModuleDefinitionFile>exports.def</ModuleDefinitionFile>
|
||||||
|
<AdditionalDependencies>Advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="DllMain.cpp" />
|
||||||
|
<ClCompile Include="Logging.cpp" />
|
||||||
|
<ClCompile Include="ProcessLauncher.cpp" />
|
||||||
|
<ClCompile Include="RegistryConfig.cpp" />
|
||||||
|
<ClCompile Include="ScriptHookPlugin.cpp" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="Logging.h" />
|
||||||
|
<ClInclude Include="ProcessLauncher.h" />
|
||||||
|
<ClInclude Include="RegistryConfig.h" />
|
||||||
|
<ClInclude Include="ScriptHookPlugin.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="exports.def" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
<ImportGroup Label="ExtensionTargets" />
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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>{F4A82F73-9E41-44E6-AB6B-69D29AC5C3CC}</UniqueIdentifier><Extensions>cpp;c</Extensions></Filter>
|
||||||
|
<Filter Include="Header Files"><UniqueIdentifier>{D93CE101-7264-440E-A04E-D633530285A9}</UniqueIdentifier><Extensions>h;hpp</Extensions></Filter>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="DllMain.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||||
|
<ClCompile Include="Logging.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||||
|
<ClCompile Include="ProcessLauncher.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||||
|
<ClCompile Include="RegistryConfig.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||||
|
<ClCompile Include="ScriptHookPlugin.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="Logging.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="ScriptHookPlugin.h"><Filter>Header Files</Filter></ClInclude>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
#include "ProcessLauncher.h"
|
||||||
|
#include "Logging.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cwctype>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::wstring QuoteWindowsArgument(const std::wstring& arg)
|
||||||
|
{
|
||||||
|
if (arg.empty())
|
||||||
|
{
|
||||||
|
return L"\"\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.find_first_of(L" \t\n\v\"") == std::wstring::npos)
|
||||||
|
{
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring result = L"\"";
|
||||||
|
size_t backslashes = 0;
|
||||||
|
for (wchar_t ch : arg)
|
||||||
|
{
|
||||||
|
if (ch == L'\\')
|
||||||
|
{
|
||||||
|
++backslashes;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == L'\"')
|
||||||
|
{
|
||||||
|
result.append(backslashes * 2 + 1, L'\\');
|
||||||
|
result.push_back(L'\"');
|
||||||
|
backslashes = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(backslashes, L'\\');
|
||||||
|
backslashes = 0;
|
||||||
|
result.push_back(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(backslashes * 2, L'\\');
|
||||||
|
result.push_back(L'\"');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring GetExtensionLower(const std::wstring& path)
|
||||||
|
{
|
||||||
|
const size_t dot = path.find_last_of(L'.');
|
||||||
|
const size_t slash = path.find_last_of(L"\\/");
|
||||||
|
if (dot == std::wstring::npos || (slash != std::wstring::npos && dot < slash))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ext = path.substr(dot);
|
||||||
|
std::transform(ext.begin(), ext.end(), ext.begin(), [](wchar_t c) { return static_cast<wchar_t>(std::towlower(c)); });
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ParentDirectory(const std::wstring& path)
|
||||||
|
{
|
||||||
|
const size_t slash = path.find_last_of(L"\\/");
|
||||||
|
if (slash == std::wstring::npos)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (slash == 2 && path.size() >= 3 && path[1] == L':')
|
||||||
|
{
|
||||||
|
return path.substr(0, 3);
|
||||||
|
}
|
||||||
|
return path.substr(0, slash);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring GetEnvironmentValue(const wchar_t* name)
|
||||||
|
{
|
||||||
|
DWORD size = GetEnvironmentVariableW(name, nullptr, 0);
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::vector<wchar_t> buffer(size, L'\0');
|
||||||
|
if (GetEnvironmentVariableW(name, buffer.data(), size) == 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::wstring(buffer.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t HashPath(const std::wstring& path)
|
||||||
|
{
|
||||||
|
constexpr uint64_t fnvOffset = 14695981039346656037ull;
|
||||||
|
constexpr uint64_t fnvPrime = 1099511628211ull;
|
||||||
|
uint64_t hash = fnvOffset;
|
||||||
|
for (wchar_t ch : path)
|
||||||
|
{
|
||||||
|
wchar_t lower = static_cast<wchar_t>(std::towlower(ch));
|
||||||
|
hash ^= static_cast<uint16_t>(lower);
|
||||||
|
hash *= fnvPrime;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring MutexNameForScript(const std::wstring& scriptPath)
|
||||||
|
{
|
||||||
|
std::wstringstream stream;
|
||||||
|
stream << L"Local\\Plandent.MstscScriptHook."
|
||||||
|
<< std::uppercase << std::hex << std::setw(16) << std::setfill(L'0')
|
||||||
|
<< HashPath(scriptPath);
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LaunchCommand
|
||||||
|
{
|
||||||
|
std::wstring application;
|
||||||
|
std::wstring commandLine;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool BuildLaunchCommand(const hook::HookConfig& config, LaunchCommand& command)
|
||||||
|
{
|
||||||
|
const std::wstring extension = GetExtensionLower(config.scriptPath);
|
||||||
|
|
||||||
|
if (extension == L".bat" || extension == L".cmd")
|
||||||
|
{
|
||||||
|
std::wstring comspec = GetEnvironmentValue(L"ComSpec");
|
||||||
|
if (comspec.empty())
|
||||||
|
{
|
||||||
|
std::wstring systemRoot = GetEnvironmentValue(L"SystemRoot");
|
||||||
|
if (systemRoot.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
comspec = systemRoot + L"\\System32\\cmd.exe";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring inner = QuoteWindowsArgument(config.scriptPath);
|
||||||
|
if (!config.arguments.empty())
|
||||||
|
{
|
||||||
|
inner += L" " + config.arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
command.application = comspec;
|
||||||
|
command.commandLine = QuoteWindowsArgument(comspec) + L" /D /S /C \"" + inner + L"\"";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension == L".ps1")
|
||||||
|
{
|
||||||
|
std::wstring systemRoot = GetEnvironmentValue(L"SystemRoot");
|
||||||
|
if (systemRoot.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring powershell = systemRoot + L"\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
||||||
|
command.application = powershell;
|
||||||
|
command.commandLine = QuoteWindowsArgument(powershell) +
|
||||||
|
L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
|
||||||
|
QuoteWindowsArgument(config.scriptPath);
|
||||||
|
if (!config.arguments.empty())
|
||||||
|
{
|
||||||
|
command.commandLine += L" " + config.arguments;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension == L".exe")
|
||||||
|
{
|
||||||
|
command.application = config.scriptPath;
|
||||||
|
command.commandLine = QuoteWindowsArgument(config.scriptPath);
|
||||||
|
if (!config.arguments.empty())
|
||||||
|
{
|
||||||
|
command.commandLine += L" " + config.arguments;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace hook
|
||||||
|
{
|
||||||
|
ProcessLauncher::~ProcessLauncher()
|
||||||
|
{
|
||||||
|
if (process_ != nullptr)
|
||||||
|
{
|
||||||
|
CloseHandle(process_);
|
||||||
|
process_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessLauncher::CleanupExitedProcess()
|
||||||
|
{
|
||||||
|
if (process_ == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WaitForSingleObject(process_, 0) != WAIT_TIMEOUT)
|
||||||
|
{
|
||||||
|
CloseHandle(process_);
|
||||||
|
process_ = nullptr;
|
||||||
|
processId_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProcessLauncher::IsRunning()
|
||||||
|
{
|
||||||
|
CleanupExitedProcess();
|
||||||
|
return process_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProcessLauncher::Start(const HookConfig& config)
|
||||||
|
{
|
||||||
|
CleanupExitedProcess();
|
||||||
|
if (process_ != nullptr)
|
||||||
|
{
|
||||||
|
Log(config.enableLogging, L"Script ist für diese MSTSC-Instanz bereits aktiv (PID " + std::to_wstring(processId_) + L").");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.scriptPath.empty())
|
||||||
|
{
|
||||||
|
Log(config.enableLogging, L"Kein ScriptPath konfiguriert; Start übersprungen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD attributes = GetFileAttributesW(config.scriptPath.c_str());
|
||||||
|
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||||
|
{
|
||||||
|
Log(config.enableLogging, L"Script wurde nicht gefunden: " + config.scriptPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchCommand command;
|
||||||
|
if (!BuildLaunchCommand(config, command))
|
||||||
|
{
|
||||||
|
Log(config.enableLogging, L"Nicht unterstützter Script-/Dateityp: " + config.scriptPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring workingDirectory = config.workingDirectory.empty()
|
||||||
|
? ParentDirectory(config.scriptPath)
|
||||||
|
: config.workingDirectory;
|
||||||
|
|
||||||
|
HANDLE duplicateMutex = nullptr;
|
||||||
|
if (config.preventDuplicates)
|
||||||
|
{
|
||||||
|
SECURITY_ATTRIBUTES sa{};
|
||||||
|
sa.nLength = sizeof(sa);
|
||||||
|
sa.bInheritHandle = TRUE;
|
||||||
|
|
||||||
|
std::wstring mutexName = MutexNameForScript(config.scriptPath);
|
||||||
|
duplicateMutex = CreateMutexW(&sa, FALSE, mutexName.c_str());
|
||||||
|
if (duplicateMutex == nullptr)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
Log(config.enableLogging, L"Mehrfachstart-Schutz konnte nicht erstellt werden: " + FormatWin32Error(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetLastError() == ERROR_ALREADY_EXISTS)
|
||||||
|
{
|
||||||
|
CloseHandle(duplicateMutex);
|
||||||
|
Log(config.enableLogging, L"Ein Receiver mit demselben ScriptPath läuft bereits; kein weiterer Start.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PROCESS_INFORMATION pi{};
|
||||||
|
DWORD creationFlags = 0;
|
||||||
|
if (config.hidden)
|
||||||
|
{
|
||||||
|
creationFlags |= CREATE_NO_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<wchar_t> mutableCommand(command.commandLine.begin(), command.commandLine.end());
|
||||||
|
mutableCommand.push_back(L'\0');
|
||||||
|
|
||||||
|
BOOL created = FALSE;
|
||||||
|
|
||||||
|
if (duplicateMutex != nullptr)
|
||||||
|
{
|
||||||
|
STARTUPINFOEXW startup{};
|
||||||
|
startup.StartupInfo.cb = sizeof(startup);
|
||||||
|
if (config.hidden)
|
||||||
|
{
|
||||||
|
startup.StartupInfo.dwFlags |= STARTF_USESHOWWINDOW;
|
||||||
|
startup.StartupInfo.wShowWindow = SW_HIDE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SIZE_T attributeSize = 0;
|
||||||
|
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeSize);
|
||||||
|
std::vector<BYTE> attributeBuffer(attributeSize);
|
||||||
|
startup.lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributeBuffer.data());
|
||||||
|
|
||||||
|
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &attributeSize))
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
CloseHandle(duplicateMutex);
|
||||||
|
Log(config.enableLogging, L"Prozessattributliste konnte nicht initialisiert werden: " + FormatWin32Error(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE handles[] = { duplicateMutex };
|
||||||
|
if (!UpdateProcThreadAttribute(
|
||||||
|
startup.lpAttributeList,
|
||||||
|
0,
|
||||||
|
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||||
|
handles,
|
||||||
|
sizeof(handles),
|
||||||
|
nullptr,
|
||||||
|
nullptr))
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
||||||
|
CloseHandle(duplicateMutex);
|
||||||
|
Log(config.enableLogging, L"Handle-Vererbung konnte nicht vorbereitet werden: " + FormatWin32Error(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
creationFlags |= EXTENDED_STARTUPINFO_PRESENT;
|
||||||
|
created = CreateProcessW(
|
||||||
|
command.application.c_str(),
|
||||||
|
mutableCommand.data(),
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
TRUE,
|
||||||
|
creationFlags,
|
||||||
|
nullptr,
|
||||||
|
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||||
|
&startup.StartupInfo,
|
||||||
|
&pi);
|
||||||
|
|
||||||
|
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
||||||
|
|
||||||
|
// Der Child-Prozess hat bei erfolgreichem Start eine eigene Referenz auf den Mutex.
|
||||||
|
// Dadurch bleibt der Mehrfachstart-Schutz auch bestehen, wenn mstsc.exe später endet.
|
||||||
|
CloseHandle(duplicateMutex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
STARTUPINFOW startup{};
|
||||||
|
startup.cb = sizeof(startup);
|
||||||
|
if (config.hidden)
|
||||||
|
{
|
||||||
|
startup.dwFlags |= STARTF_USESHOWWINDOW;
|
||||||
|
startup.wShowWindow = SW_HIDE;
|
||||||
|
}
|
||||||
|
|
||||||
|
created = CreateProcessW(
|
||||||
|
command.application.c_str(),
|
||||||
|
mutableCommand.data(),
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
FALSE,
|
||||||
|
creationFlags,
|
||||||
|
nullptr,
|
||||||
|
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||||
|
&startup,
|
||||||
|
&pi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!created)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
Log(config.enableLogging, L"Scriptstart fehlgeschlagen: " + FormatWin32Error(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
process_ = pi.hProcess;
|
||||||
|
processId_ = pi.dwProcessId;
|
||||||
|
|
||||||
|
Log(config.enableLogging,
|
||||||
|
L"Script gestartet. PID=" + std::to_wstring(processId_) +
|
||||||
|
L", Script=" + config.scriptPath +
|
||||||
|
(config.arguments.empty() ? L"" : L", Argumente=" + config.arguments));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessLauncher::Stop(bool loggingEnabled)
|
||||||
|
{
|
||||||
|
CleanupExitedProcess();
|
||||||
|
if (process_ == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DWORD pid = processId_;
|
||||||
|
if (!TerminateProcess(process_, 0))
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
Log(loggingEnabled, L"Scriptprozess PID " + std::to_wstring(pid) + L" konnte nicht beendet werden: " + FormatWin32Error(error));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log(loggingEnabled, L"Beenden des Scriptprozesses PID " + std::to_wstring(pid) + L" wurde bei RDP-Trennung angefordert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(process_);
|
||||||
|
process_ = nullptr;
|
||||||
|
processId_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessLauncher::Detach(bool loggingEnabled)
|
||||||
|
{
|
||||||
|
CleanupExitedProcess();
|
||||||
|
if (process_ == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log(loggingEnabled, L"MSTSC beendet Plugin; Script PID " + std::to_wstring(processId_) + L" läuft weiter.");
|
||||||
|
CloseHandle(process_);
|
||||||
|
process_ = nullptr;
|
||||||
|
processId_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RegistryConfig.h"
|
||||||
|
#include <windows.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace hook
|
||||||
|
{
|
||||||
|
class ProcessLauncher
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ProcessLauncher() = default;
|
||||||
|
~ProcessLauncher();
|
||||||
|
|
||||||
|
ProcessLauncher(const ProcessLauncher&) = delete;
|
||||||
|
ProcessLauncher& operator=(const ProcessLauncher&) = delete;
|
||||||
|
|
||||||
|
bool Start(const HookConfig& config);
|
||||||
|
void Stop(bool loggingEnabled);
|
||||||
|
void Detach(bool loggingEnabled);
|
||||||
|
bool IsRunning();
|
||||||
|
|
||||||
|
private:
|
||||||
|
HANDLE process_ = nullptr;
|
||||||
|
DWORD processId_ = 0;
|
||||||
|
|
||||||
|
void CleanupExitedProcess();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#include "RegistryConfig.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
DWORD ReadDword(HKEY key, const wchar_t* name, DWORD defaultValue)
|
||||||
|
{
|
||||||
|
DWORD value = defaultValue;
|
||||||
|
DWORD size = sizeof(value);
|
||||||
|
DWORD type = 0;
|
||||||
|
if (RegQueryValueExW(key, name, nullptr, &type, reinterpret_cast<BYTE*>(&value), &size) != ERROR_SUCCESS ||
|
||||||
|
type != REG_DWORD)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ReadString(HKEY key, const wchar_t* name)
|
||||||
|
{
|
||||||
|
DWORD type = 0;
|
||||||
|
DWORD size = 0;
|
||||||
|
if (RegQueryValueExW(key, name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS ||
|
||||||
|
(type != REG_SZ && type != REG_EXPAND_SZ) || size == 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<wchar_t> buffer((size / sizeof(wchar_t)) + 1, L'\0');
|
||||||
|
if (RegQueryValueExW(key, name, nullptr, &type, reinterpret_cast<BYTE*>(buffer.data()), &size) != ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring value(buffer.data());
|
||||||
|
if (type == REG_EXPAND_SZ)
|
||||||
|
{
|
||||||
|
return hook::ExpandEnvironment(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace hook
|
||||||
|
{
|
||||||
|
std::wstring ExpandEnvironment(const std::wstring& value)
|
||||||
|
{
|
||||||
|
if (value.empty())
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD needed = ExpandEnvironmentStringsW(value.c_str(), nullptr, 0);
|
||||||
|
if (needed == 0)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<wchar_t> buffer(needed, L'\0');
|
||||||
|
if (ExpandEnvironmentStringsW(value.c_str(), buffer.data(), needed) == 0)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return std::wstring(buffer.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
HookConfig LoadConfig()
|
||||||
|
{
|
||||||
|
HookConfig config;
|
||||||
|
|
||||||
|
HKEY key = nullptr;
|
||||||
|
if (RegOpenKeyExW(HKEY_CURRENT_USER, kConfigKey, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.enabled = ReadDword(key, L"Enabled", 1) != 0;
|
||||||
|
config.scriptPath = ReadString(key, L"ScriptPath");
|
||||||
|
config.arguments = ExpandEnvironment(ReadString(key, L"Arguments"));
|
||||||
|
config.workingDirectory = ReadString(key, L"WorkingDirectory");
|
||||||
|
config.startOnConnect = ReadDword(key, L"StartOnConnect", 1) != 0;
|
||||||
|
config.stopOnDisconnect = ReadDword(key, L"StopOnDisconnect", 0) != 0;
|
||||||
|
config.hidden = ReadDword(key, L"Hidden", 1) != 0;
|
||||||
|
config.preventDuplicates = ReadDword(key, L"PreventDuplicates", 1) != 0;
|
||||||
|
config.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0;
|
||||||
|
|
||||||
|
RegCloseKey(key);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace hook
|
||||||
|
{
|
||||||
|
inline constexpr wchar_t kConfigKey[] = L"Software\\Plandent\\MstscScriptHook";
|
||||||
|
|
||||||
|
struct HookConfig
|
||||||
|
{
|
||||||
|
bool enabled = true;
|
||||||
|
std::wstring scriptPath;
|
||||||
|
std::wstring arguments;
|
||||||
|
std::wstring workingDirectory;
|
||||||
|
bool startOnConnect = true;
|
||||||
|
bool stopOnDisconnect = false;
|
||||||
|
bool hidden = true;
|
||||||
|
bool preventDuplicates = true;
|
||||||
|
bool enableLogging = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
HookConfig LoadConfig();
|
||||||
|
std::wstring ExpandEnvironment(const std::wstring& value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "ScriptHookPlugin.h"
|
||||||
|
|
||||||
|
#include "Logging.h"
|
||||||
|
#include "RegistryConfig.h"
|
||||||
|
|
||||||
|
ScriptHookPlugin::ScriptHookPlugin() = default;
|
||||||
|
ScriptHookPlugin::~ScriptHookPlugin() = default;
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** ppvObject)
|
||||||
|
{
|
||||||
|
if (ppvObject == nullptr)
|
||||||
|
{
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppvObject = nullptr;
|
||||||
|
if (riid == __uuidof(IUnknown) || riid == __uuidof(IWTSPlugin))
|
||||||
|
{
|
||||||
|
*ppvObject = static_cast<IWTSPlugin*>(this);
|
||||||
|
AddRef();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE ScriptHookPlugin::AddRef()
|
||||||
|
{
|
||||||
|
return static_cast<ULONG>(InterlockedIncrement(&refCount_));
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE ScriptHookPlugin::Release()
|
||||||
|
{
|
||||||
|
ULONG remaining = static_cast<ULONG>(InterlockedDecrement(&refCount_));
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Initialize(IWTSVirtualChannelManager* channelManager)
|
||||||
|
{
|
||||||
|
UNREFERENCED_PARAMETER(channelManager);
|
||||||
|
const hook::HookConfig config = hook::LoadConfig();
|
||||||
|
hook::Log(config.enableLogging, L"IWTSPlugin::Initialize aufgerufen.");
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Connected()
|
||||||
|
{
|
||||||
|
const hook::HookConfig config = hook::LoadConfig();
|
||||||
|
hook::Log(config.enableLogging, L"RDP-Verbindung hergestellt (IWTSPlugin::Connected).");
|
||||||
|
|
||||||
|
if (!config.enabled || !config.startOnConnect)
|
||||||
|
{
|
||||||
|
hook::Log(config.enableLogging, L"Start ist in der Konfiguration deaktiviert.");
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Script gewartet.
|
||||||
|
launcher_.Start(config);
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Disconnected(DWORD disconnectCode)
|
||||||
|
{
|
||||||
|
const hook::HookConfig config = hook::LoadConfig();
|
||||||
|
hook::Log(config.enableLogging,
|
||||||
|
L"RDP-Verbindung getrennt (IWTSPlugin::Disconnected), Code=" + std::to_wstring(disconnectCode) + L".");
|
||||||
|
|
||||||
|
if (config.stopOnDisconnect)
|
||||||
|
{
|
||||||
|
launcher_.Stop(config.enableLogging);
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Terminated()
|
||||||
|
{
|
||||||
|
const hook::HookConfig config = hook::LoadConfig();
|
||||||
|
hook::Log(config.enableLogging, L"IWTSPlugin::Terminated aufgerufen.");
|
||||||
|
|
||||||
|
if (config.stopOnDisconnect)
|
||||||
|
{
|
||||||
|
launcher_.Stop(config.enableLogging);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
launcher_.Detach(config.enableLogging);
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ProcessLauncher.h"
|
||||||
|
#include <windows.h>
|
||||||
|
#include <tsvirtualchannels.h>
|
||||||
|
|
||||||
|
class ScriptHookPlugin final : public IWTSPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScriptHookPlugin();
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override;
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE Initialize(IWTSVirtualChannelManager* channelManager) override;
|
||||||
|
HRESULT STDMETHODCALLTYPE Connected() override;
|
||||||
|
HRESULT STDMETHODCALLTYPE Disconnected(DWORD disconnectCode) override;
|
||||||
|
HRESULT STDMETHODCALLTYPE Terminated() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
~ScriptHookPlugin();
|
||||||
|
|
||||||
|
LONG refCount_ = 1;
|
||||||
|
hook::ProcessLauncher launcher_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
LIBRARY "PlandentMstscScriptHook"
|
||||||
|
EXPORTS
|
||||||
|
VirtualChannelGetInstance
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Plandent MSTSC Script Hook
|
||||||
|
|
||||||
|
Kleines Windows-Projekt, das beim erfolgreichen Aufbau einer klassischen RDP-Verbindung mit `mstsc.exe` lokal ein konfiguriertes Script startet.
|
||||||
|
|
||||||
|
## Komponenten
|
||||||
|
|
||||||
|
- **PlandentMstscScriptHook.dll** – native x64 DVC-Plugin-DLL für den Remote Desktop Connection Client.
|
||||||
|
- **PlandentMstscScriptHook.exe** – WinForms-Konfigurator/Installer. Die Release-EXE enthält die DLL als Resource und extrahiert sie bei der Installation.
|
||||||
|
|
||||||
|
## Verhalten
|
||||||
|
|
||||||
|
Die DLL implementiert `IWTSPlugin` und exportiert `VirtualChannelGetInstance`.
|
||||||
|
|
||||||
|
- `Initialize()` – Plugin wird von MSTSC initialisiert.
|
||||||
|
- `Connected()` – liest die Konfiguration und startet das lokale Script.
|
||||||
|
- `Disconnected()` – beendet das von dieser Plugin-Instanz gestartete Script, wenn `StopOnDisconnect=1` gesetzt ist.
|
||||||
|
- `Terminated()` – bereinigt Prozesshandles; bei `StopOnDisconnect=0` läuft das Script weiter.
|
||||||
|
|
||||||
|
Der Plugin-Callback wartet **nicht** auf das Script. Es wird nur mit `CreateProcessW` gestartet und der Callback kehrt direkt zurück.
|
||||||
|
|
||||||
|
## Registry
|
||||||
|
|
||||||
|
MSTSC-Registrierung pro Benutzer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HKCU\Software\Microsoft\Terminal Server Client\Default\AddIns\PlandentMstscScriptHook
|
||||||
|
Name = C:\Users\<Benutzer>\AppData\Local\Plandent\MstscScriptHook\PlandentMstscScriptHook.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
Konfiguration:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HKCU\Software\Plandent\MstscScriptHook
|
||||||
|
Enabled REG_DWORD 1
|
||||||
|
ScriptPath REG_EXPAND_SZ C:\RemoteVDDS\RemoteVDDS_Receiver_TS.bat
|
||||||
|
Arguments REG_SZ MIN
|
||||||
|
WorkingDirectory REG_EXPAND_SZ C:\RemoteVDDS
|
||||||
|
StartOnConnect REG_DWORD 1
|
||||||
|
StopOnDisconnect REG_DWORD 0
|
||||||
|
Hidden REG_DWORD 1
|
||||||
|
PreventDuplicates REG_DWORD 1
|
||||||
|
EnableLogging REG_DWORD 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unterstützte Starttypen
|
||||||
|
|
||||||
|
- `.bat` / `.cmd` → `cmd.exe /D /S /C ...`
|
||||||
|
- `.ps1` → Windows PowerShell mit `-NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`
|
||||||
|
- `.exe` → direkter Start
|
||||||
|
|
||||||
|
Bei `Hidden=1` wird `CREATE_NO_WINDOW` verwendet und `SW_HIDE` gesetzt.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Der Schutz ist bewusst `Local\\...`, also auf die lokale Windows-Sitzung begrenzt.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Bei aktiviertem Logging:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%LOCALAPPDATA%\Plandent\MstscScriptHook\plugin.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Die GUI enthält einen Button **Log öffnen**.
|
||||||
|
|
||||||
|
## Voraussetzungen zum Bauen
|
||||||
|
|
||||||
|
- Windows 10/11 x64
|
||||||
|
- Visual Studio 2022 oder Visual Studio Build Tools 2022
|
||||||
|
- Workload **Desktopentwicklung mit C++**
|
||||||
|
- Windows 10/11 SDK
|
||||||
|
- .NET 8 SDK
|
||||||
|
|
||||||
|
## Release bauen
|
||||||
|
|
||||||
|
PowerShell im Projektverzeichnis:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\build-release.ps1 -Clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist\PlandentMstscScriptHook.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Die veröffentlichte EXE ist x64, self-contained und enthält die native Plugin-DLL als Resource.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. `PlandentMstscScriptHook.exe` starten.
|
||||||
|
2. Script über `...` auswählen.
|
||||||
|
3. Für `RemoteVDDS_Receiver_TS.bat` als Argument `MIN` setzen.
|
||||||
|
4. Optional Arbeitsverzeichnis auswählen; standardmäßig sollte es dem Scriptverzeichnis entsprechen.
|
||||||
|
5. **Script testen** und anschließend **Test beenden**.
|
||||||
|
6. **Plugin installieren** anklicken.
|
||||||
|
7. Alle bereits laufenden `mstsc.exe`-Instanzen schließen.
|
||||||
|
8. MSTSC neu starten und eine RDP-Verbindung herstellen.
|
||||||
|
|
||||||
|
Die Installation erfolgt nur unter `HKCU` und `%LOCALAPPDATA%`; dafür sind normalerweise keine Administratorrechte nötig.
|
||||||
|
|
||||||
|
## Deinstallation
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
Die Script-Konfiguration unter `HKCU\Software\Plandent\MstscScriptHook` bleibt absichtlich erhalten.
|
||||||
|
|
||||||
|
## Wichtige Grenzen
|
||||||
|
|
||||||
|
- Das Plugin ist für den klassischen Microsoft RDC-Pluginmechanismus ausgelegt. Andere RDP-Clients müssen diesen Mechanismus ebenfalls unterstützen, sonst wird die DLL nicht geladen.
|
||||||
|
- `StopOnDisconnect` beendet nur den vom Plugin selbst gestarteten Root-Prozess. Beim RemoteVDDS-BAT ist das der dauerhaft laufende `cmd.exe`-Receiver.
|
||||||
|
- Das Script läuft mit den Rechten des lokalen Benutzers, der `mstsc.exe` gestartet hat.
|
||||||
|
- AppLocker/WDAC oder andere Application-Control-Richtlinien können das Laden einer nicht signierten DLL bzw. das Starten des Scripts blockieren.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateSet('Release','Debug')]
|
||||||
|
[string]$Configuration = 'Release',
|
||||||
|
[switch]$Clean
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$Root = Split-Path -Parent $PSCommandPath
|
||||||
|
$PluginProject = Join-Path $Root 'Plugin\PlandentMstscScriptHook.vcxproj'
|
||||||
|
$ConfiguratorProject = Join-Path $Root 'Configurator\Configurator.csproj'
|
||||||
|
$Dist = Join-Path $Root 'dist'
|
||||||
|
$Artifacts = Join-Path $Root 'artifacts'
|
||||||
|
|
||||||
|
function Find-MSBuild {
|
||||||
|
$cmd = Get-Command msbuild.exe -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { return $cmd.Source }
|
||||||
|
|
||||||
|
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||||
|
if (Test-Path $vswhere) {
|
||||||
|
$found = & $vswhere -latest -products * `
|
||||||
|
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
|
||||||
|
-find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1
|
||||||
|
if ($found) { return $found }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'MSBuild wurde nicht gefunden. Visual Studio 2022 bzw. Build Tools mit Desktopentwicklung mit C++ installieren.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Clean) {
|
||||||
|
Remove-Item $Artifacts -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item $Dist -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $Dist -Force | Out-Null
|
||||||
|
|
||||||
|
$MSBuild = Find-MSBuild
|
||||||
|
Write-Host "[1/2] Native MSTSC Plugin-DLL bauen ($Configuration|x64)..." -ForegroundColor Cyan
|
||||||
|
& $MSBuild $PluginProject /m /nologo "/p:Configuration=$Configuration" '/p:Platform=x64'
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Native DLL Build fehlgeschlagen (ExitCode $LASTEXITCODE)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$PluginDll = Join-Path $Artifacts "x64\$Configuration\PlandentMstscScriptHook.dll"
|
||||||
|
if (-not (Test-Path $PluginDll)) {
|
||||||
|
throw "Erwartete DLL wurde nicht erzeugt: $PluginDll"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[2/2] Konfigurations-EXE als x64 Single-File veröffentlichen..." -ForegroundColor Cyan
|
||||||
|
& dotnet publish $ConfiguratorProject `
|
||||||
|
-c $Configuration `
|
||||||
|
-r win-x64 `
|
||||||
|
--self-contained true `
|
||||||
|
-p:Platform=x64 `
|
||||||
|
-p:PublishSingleFile=true `
|
||||||
|
-p:DebugType=embedded `
|
||||||
|
-p:DebugSymbols=false `
|
||||||
|
-o $Dist
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Configurator Publish fehlgeschlagen (ExitCode $LASTEXITCODE)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$Exe = Join-Path $Dist 'PlandentMstscScriptHook.exe'
|
||||||
|
if (-not (Test-Path $Exe)) {
|
||||||
|
throw "Erwartete EXE wurde nicht erzeugt: $Exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Build erfolgreich:' -ForegroundColor Green
|
||||||
|
Write-Host " $Exe"
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Diese EXE enthält die native DLL als eingebettete Resource.'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# RemoteVDDS-Beispiel
|
||||||
|
|
||||||
|
Für `RemoteVDDS_Receiver_TS.bat` empfiehlt sich:
|
||||||
|
|
||||||
|
- Script: Pfad zu `RemoteVDDS_Receiver_TS.bat`
|
||||||
|
- Argumente: `MIN`
|
||||||
|
- Arbeitsverzeichnis: Verzeichnis des BAT-Scripts
|
||||||
|
- Start bei RDP-Verbindung: an
|
||||||
|
- Bei Trennung beenden: aus (bei Bedarf einschalten)
|
||||||
|
- Unsichtbar starten: an
|
||||||
|
- Mehrfachstart verhindern: an
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Plandent\MstscScriptHook]
|
||||||
|
"Enabled"=dword:00000001
|
||||||
|
"ScriptPath"="C:\\RemoteVDDS\\RemoteVDDS_Receiver_TS.bat"
|
||||||
|
"Arguments"="MIN"
|
||||||
|
"WorkingDirectory"="C:\\RemoteVDDS"
|
||||||
|
"StartOnConnect"=dword:00000001
|
||||||
|
"StopOnDisconnect"=dword:00000000
|
||||||
|
"Hidden"=dword:00000001
|
||||||
|
"PreventDuplicates"=dword:00000001
|
||||||
|
"EnableLogging"=dword:00000001
|
||||||
Reference in New Issue
Block a user