using System.Diagnostics; using System.Drawing; using System.Windows.Forms; namespace Plandent.MstscScriptHook.Configurator; internal sealed class MainForm : Form { // Global private readonly CheckBox enabledCheckBox = new() { Text = "Plugin-Funktion aktiviert", AutoSize = true }; private readonly CheckBox loggingCheckBox = new() { Text = "Plugin-Logging aktivieren", AutoSize = true }; // Automatischer Start bei RDP-Verbindung private readonly TextBox scriptPathTextBox = new() { Dock = DockStyle.Fill }; private readonly TextBox argumentsTextBox = new() { Dock = DockStyle.Fill }; private readonly TextBox workingDirectoryTextBox = new() { Dock = DockStyle.Fill }; private readonly CheckBox startOnConnectCheckBox = new() { Text = "Bei erfolgreicher RDP-Verbindung starten", AutoSize = true }; private readonly CheckBox stopOnDisconnectCheckBox = new() { Text = "Bei RDP-Trennung beenden", AutoSize = true }; private readonly CheckBox hiddenCheckBox = new() { Text = "Unsichtbar starten", AutoSize = true }; private readonly CheckBox preventDuplicatesCheckBox = new() { Text = "Mehrfachstart verhindern", AutoSize = true }; private readonly 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() { AutoSize = false, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(8) }; public MainForm() { Text = "Plandent MSTSC Script Hook"; StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(820, 720); Size = new Size(900, 800); Font = new Font("Segoe UI", 9F); Controls.Add(BuildMainLayout()); Load += (_, _) => LoadConfiguration(); FormClosed += (_, _) => DisposeAllTestProcesses(); } 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.Percent, 100)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); var title = new Label { 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 = "Lokale Programme können automatisch beim RDP-Verbindungsaufbau oder gezielt durch eine Anwendung innerhalb der Terminalserver-Sitzung gestartet werden.", AutoSize = true, MaximumSize = new Size(820, 0), Margin = new Padding(0, 0, 0, 12) }; root.Controls.Add(description, 0, 1); var scroll = new Panel { Dock = DockStyle.Fill, AutoScroll = true, Margin = new Padding(0, 0, 0, 12) }; var content = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 1, RowCount = 3 }; content.Controls.Add(BuildGlobalOptionsGroup(), 0, 0); content.Controls.Add(BuildConnectionProgramGroup(), 0, 1); content.Controls.Add(BuildTriggerProgramGroup(), 0, 2); scroll.Controls.Add(content); root.Controls.Add(scroll, 0, 2); root.Controls.Add(BuildStatusPanel(), 0, 3); var hint = new Label { Text = "Hinweis: PlandentRdpClientTrigger.exe auf dem Terminalserver übermittelt ausschließlich einen festen START-Befehl. Programmpfad und Parameter werden niemals vom Server übertragen.", AutoSize = true, MaximumSize = new Size(820, 0), Margin = new Padding(0, 0, 0, 10) }; root.Controls.Add(hint, 0, 4); root.Controls.Add(BuildMainButtons(), 0, 5); return root; } private Control 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 { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 3, RowCount = 3, Margin = new Padding(0) }; grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); grid.Controls.Add(CreateFieldLabel("Programm/Script:"), 0, 0); grid.Controls.Add(pathTextBox, 1, 0); var browseProgramButton = new Button { Text = "…", Width = 42, Height = pathTextBox.PreferredHeight + 4 }; browseProgramButton.Click += (_, _) => browseProgram(); grid.Controls.Add(browseProgramButton, 2, 0); grid.Controls.Add(CreateFieldLabel("Argumente:"), 0, 1); grid.Controls.Add(argumentsTextBox, 1, 1); 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 BuildStatusPanel() { var panel = new Panel { Dock = DockStyle.Top, Height = 70, BorderStyle = BorderStyle.FixedSingle, Margin = new Padding(0, 0, 0, 10) }; panel.Controls.Add(statusLabel); return panel; } private Control BuildMainButtons() { var buttons = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true, FlowDirection = FlowDirection.LeftToRight, WrapContents = true, Margin = new Padding(0) }; 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(saveButton); buttons.Controls.Add(installButton); buttons.Controls.Add(removeButton); buttons.Controls.Add(logButton); return buttons; } private void BrowseProgram(TextBox pathTextBox, TextBox workingDirectoryTextBox, bool useRemoteVddsPreset) { using var dialog = new OpenFileDialog { Title = "Programm oder Script auswählen", Filter = "Unterstützte Dateien (*.exe;*.bat;*.cmd;*.ps1)|*.exe;*.bat;*.cmd;*.ps1|Alle Dateien (*.*)|*.*", CheckFileExists = true }; if (!string.IsNullOrWhiteSpace(pathTextBox.Text)) { string expanded = Environment.ExpandEnvironmentVariables(pathTextBox.Text); if (File.Exists(expanded)) { dialog.InitialDirectory = Path.GetDirectoryName(expanded); dialog.FileName = Path.GetFileName(expanded); } } if (dialog.ShowDialog(this) != DialogResult.OK) { return; } pathTextBox.Text = dialog.FileName; if (string.IsNullOrWhiteSpace(workingDirectoryTextBox.Text)) { workingDirectoryTextBox.Text = Path.GetDirectoryName(dialog.FileName) ?? string.Empty; } if (useRemoteVddsPreset && string.IsNullOrWhiteSpace(argumentsTextBox.Text) && Path.GetFileName(dialog.FileName).Contains("RemoteVDDS_Receiver_TS", StringComparison.OrdinalIgnoreCase)) { argumentsTextBox.Text = "MIN"; } } private void BrowseWorkingDirectory(TextBox target) { using var dialog = new FolderBrowserDialog { Description = "Arbeitsverzeichnis auswählen", UseDescriptionForTitle = true, ShowNewFolderButton = true }; string expanded = Environment.ExpandEnvironmentVariables(target.Text); if (Directory.Exists(expanded)) { dialog.SelectedPath = expanded; } if (dialog.ShowDialog(this) == DialogResult.OK) { target.Text = dialog.SelectedPath; } } private HookConfig ReadForm() { return new HookConfig { Enabled = enabledCheckBox.Checked, EnableLogging = loggingCheckBox.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, TriggerEnabled = triggerEnabledCheckBox.Checked, TriggerProgramPath = triggerProgramPathTextBox.Text.Trim(), TriggerArguments = triggerArgumentsTextBox.Text.Trim(), TriggerWorkingDirectory = triggerWorkingDirectoryTextBox.Text.Trim(), TriggerHidden = triggerHiddenCheckBox.Checked, TriggerPreventDuplicates = triggerPreventDuplicatesCheckBox.Checked }; } private void LoadConfiguration() { HookConfig config = RegistryConfig.Load(); enabledCheckBox.Checked = config.Enabled; loggingCheckBox.Checked = config.EnableLogging; scriptPathTextBox.Text = config.ScriptPath; argumentsTextBox.Text = config.Arguments; workingDirectoryTextBox.Text = config.WorkingDirectory; startOnConnectCheckBox.Checked = config.StartOnConnect; stopOnDisconnectCheckBox.Checked = config.StopOnDisconnect; hiddenCheckBox.Checked = config.Hidden; preventDuplicatesCheckBox.Checked = config.PreventDuplicates; 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(); } private bool SaveConfiguration(bool showConfirmation) { try { HookConfig config = ReadForm(); ValidateConfiguredTargets(config, installationValidation: 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(); ValidateConfiguredTargets(config, installationValidation: true); RegistryConfig.Save(config); PluginInstaller.Install(); RefreshStatus(); MessageBox.Show( this, "Plugin wurde für den aktuellen Benutzer installiert und die Konfiguration gespeichert.\n\n" + "Bereits geöffnete mstsc.exe-Fenster vollständig schließen und anschließend neu starten, damit die neue 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 StartConnectionTest() { try { StopConnectionTest(silent: true); HookConfig config = ReadForm(); ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true); connectionTestProcess = ScriptTestLauncher.StartConnectionProgram(config); stopConnectionTestButton.Enabled = true; statusLabel.Text = $"Verbindungs-Test gestartet: PID {connectionTestProcess.Id}. Der Test läuft unabhängig von MSTSC."; } catch (Exception ex) { MessageBox.Show(this, ex.Message, "Test fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); } } 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 StopConnectionTest(bool silent) { StopTestProcess(ref connectionTestProcess, stopConnectionTestButton, "Verbindungs-Test", silent); } private void StopTriggerTest(bool silent) { StopTestProcess(ref triggerTestProcess, stopTriggerTestButton, "Trigger-Programm-Test", silent); } private void StopTestProcess(ref Process? process, Button button, string description, bool silent) { if (process is null) { button.Enabled = false; return; } try { if (!process.HasExited) { process.Kill(entireProcessTree: true); process.WaitForExit(3000); } if (!silent) { statusLabel.Text = description + " wurde beendet."; } } catch (Exception ex) { if (!silent) { MessageBox.Show(this, ex.Message, description + " konnte nicht beendet werden", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } finally { process?.Dispose(); process = null; button.Enabled = false; } } private void DisposeAllTestProcesses() { connectionTestProcess?.Dispose(); triggerTestProcess?.Dispose(); connectionTestProcess = null; triggerTestProcess = null; } 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 connectStatus = startOnConnectCheckBox.Checked ? (string.IsNullOrWhiteSpace(scriptPathTextBox.Text) ? " RDP-Start: nicht konfiguriert." : " RDP-Start: konfiguriert.") : " RDP-Start: aus."; string triggerStatus = triggerEnabledCheckBox.Checked ? (string.IsNullOrWhiteSpace(triggerProgramPathTextBox.Text) ? " Server-Trigger: nicht konfiguriert." : " Server-Trigger: konfiguriert.") : " Server-Trigger: aus."; statusLabel.Text = (installed ? "✓ " : "○ ") + pluginStatus + connectStatus + triggerStatus; } private static void ValidateConfiguredTargets(HookConfig config, bool installationValidation) { // 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; } if (config.StartOnConnect) { ValidateLaunchTarget(config.ScriptPath, config.WorkingDirectory, "RDP-Verbindungsprogramm", required: true); } if (config.TriggerEnabled) { ValidateLaunchTarget(config.TriggerProgramPath, config.TriggerWorkingDirectory, "Trigger-Programm", required: true); } } private static void ValidateLaunchTarget(string configuredPath, string configuredWorkingDirectory, string displayName, bool required) { string path = Environment.ExpandEnvironmentVariables(configuredPath ?? string.Empty); if (string.IsNullOrWhiteSpace(path)) { if (required) { throw new InvalidOperationException($"Bitte für „{displayName}“ ein Programm oder Script auswählen."); } return; } if (!File.Exists(path)) { throw new FileNotFoundException($"{displayName} wurde nicht gefunden.", path); } string extension = Path.GetExtension(path).ToLowerInvariant(); if (extension is not (".bat" or ".cmd" or ".ps1" or ".exe")) { throw new NotSupportedException($"{displayName}: Unterstützt werden .bat, .cmd, .ps1 und .exe."); } if (!string.IsNullOrWhiteSpace(configuredWorkingDirectory)) { string workDir = Environment.ExpandEnvironmentVariables(configuredWorkingDirectory); if (!Directory.Exists(workDir)) { throw new DirectoryNotFoundException($"Arbeitsverzeichnis für „{displayName}“ wurde nicht gefunden: {workDir}"); } } } }