Files
Mstsc-Script-Hook/Configurator/MainForm.cs
T

700 lines
26 KiB
C#

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}");
}
}
}
}