From f7e9bd43e24f15a2b22364ec6195b1df8342d5aa Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 29 Jul 2026 09:03:01 +0200 Subject: [PATCH] Add Sonic container scan with manual multi-select after system setup. Co-authored-by: Cursor --- .../Services/SonicContainerScanner.cs | 303 +++++++ .../Setup/SelectContainersForm.cs | 432 ++++++++++ .../Setup/SetupWizardForm.cs | 90 ++- .../Tools/SonicMfContainerTool$Args.class | Bin 1419 -> 1876 bytes .../Tools/SonicMfContainerTool.class | Bin 7250 -> 9577 bytes .../Tools/SonicMfContainerTool.jar | Bin 8988 -> 6550 bytes .../Tools/SonicMfContainerTool.java | 741 ++++++++---------- 7 files changed, 1160 insertions(+), 406 deletions(-) create mode 100644 ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs create mode 100644 ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs new file mode 100644 index 0000000..3f10221 --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerScanner.cs @@ -0,0 +1,303 @@ +using System.Diagnostics; +using System.Text; +using ZA.CoreService.ESBCertificateManager.Models; + +namespace ZA.CoreService.ESBCertificateManager.Services; + +public sealed class SonicContainerScanner +{ + private readonly RuntimeSettings _runtimeSettings; + + public SonicContainerScanner(RuntimeSettings runtimeSettings) + { + _runtimeSettings = runtimeSettings + ?? throw new ArgumentNullException(nameof(runtimeSettings)); + } + + public async Task ScanAsync( + SonicSystemOption system, + string userName, + string secret, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(system); + + if (string.IsNullOrWhiteSpace(userName)) + { + return ContainerScanResult.Failed( + "Der Sonic-Benutzername fehlt."); + } + + if (string.IsNullOrEmpty(secret)) + { + return ContainerScanResult.Failed( + "Das Sonic-Kennwort fehlt."); + } + + string javaPath = + PathResolver.ResolvePath( + _runtimeSettings.JavaExecutablePath); + + string libraryPath = + PathResolver.ResolvePath( + _runtimeSettings.SonicClientLibraryPath); + + string? toolsPath = ResolveToolsDirectory(); + + if (!File.Exists(javaPath)) + { + return ContainerScanResult.Failed( + $"Java wurde nicht gefunden: {javaPath}"); + } + + if (!Directory.Exists(libraryPath)) + { + return ContainerScanResult.Failed( + $"Der Sonic-JAR-Ordner wurde nicht gefunden: {libraryPath}"); + } + + if (toolsPath is null) + { + return ContainerScanResult.Failed( + "SonicMfContainerTool.class wurde nicht gefunden."); + } + + string[] jars = + Directory.GetFiles( + libraryPath, + "*.jar", + SearchOption.TopDirectoryOnly); + + if (jars.Length == 0) + { + return ContainerScanResult.Failed( + "Im Sonic-Client-Ordner wurden keine JAR-Dateien gefunden."); + } + + string classpath = + toolsPath + + Path.PathSeparator + + string.Join( + Path.PathSeparator, + jars.OrderBy( + Path.GetFileName, + StringComparer.OrdinalIgnoreCase)); + + ProcessStartInfo startInfo = new() + { + FileName = javaPath, + WorkingDirectory = toolsPath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + CreateNoWindow = true + }; + + startInfo.ArgumentList.Add("-cp"); + startInfo.ArgumentList.Add(classpath); + startInfo.ArgumentList.Add("SonicMfContainerTool"); + startInfo.ArgumentList.Add("list"); + startInfo.ArgumentList.Add("--domain"); + startInfo.ArgumentList.Add(system.DomainName); + startInfo.ArgumentList.Add("--url"); + startInfo.ArgumentList.Add(system.ConnectionUrl); + startInfo.ArgumentList.Add("--user"); + startInfo.ArgumentList.Add(userName.Trim()); + startInfo.ArgumentList.Add("--timeout"); + startInfo.ArgumentList.Add("45"); + + startInfo.Environment["ESB_SONIC_PASSWORD"] = secret; + + using Process process = new() + { + StartInfo = startInfo + }; + + try + { + if (!process.Start()) + { + return ContainerScanResult.Failed( + "Der Java-Prozess konnte nicht gestartet werden."); + } + } + catch (Exception ex) + { + return ContainerScanResult.Failed( + $"Java konnte nicht gestartet werden: {ex.Message}"); + } + + Task standardOutputTask = + process.StandardOutput.ReadToEndAsync(); + + Task standardErrorTask = + process.StandardError.ReadToEndAsync(); + + using CancellationTokenSource timeoutSource = + CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken); + + timeoutSource.CancelAfter(TimeSpan.FromSeconds(50)); + + try + { + await process.WaitForExitAsync(timeoutSource.Token); + } + catch (OperationCanceledException) + when (!cancellationToken.IsCancellationRequested) + { + TryKill(process); + + return ContainerScanResult.Failed( + "Der Container-Scan hat das Zeitlimit überschritten."); + } + catch (OperationCanceledException) + { + TryKill(process); + throw; + } + + string standardOutput = + (await standardOutputTask).Trim(); + + string standardError = + (await standardErrorTask).Trim(); + + string combined = + string.Join( + Environment.NewLine, + new[] { standardOutput, standardError } + .Where(value => !string.IsNullOrWhiteSpace(value))); + + bool success = + process.ExitCode == 0 + && combined.Contains( + "OK:ContainerListEnd", + StringComparison.OrdinalIgnoreCase) + && !combined.Contains( + "ERROR:", + StringComparison.OrdinalIgnoreCase); + + if (!success) + { + return ContainerScanResult.Failed( + ExtractError(combined) + ?? $"Container-Scan fehlgeschlagen, ExitCode={process.ExitCode}."); + } + + List containers = []; + + foreach (string line in standardOutput.Split( + ['\r', '\n'], + StringSplitOptions.RemoveEmptyEntries)) + { + const string prefix = "CONTAINER="; + + if (!line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string name = line[prefix.Length..].Trim(); + + if (name.Length > 0 + && !containers.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + containers.Add(name); + } + } + + containers.Sort(StringComparer.OrdinalIgnoreCase); + + return ContainerScanResult.Successful(containers); + } + + private static string? ResolveToolsDirectory() + { + string baseDirectory = AppContext.BaseDirectory; + + string[] candidates = + [ + Path.Combine(baseDirectory, "Tools"), + Path.Combine(baseDirectory, "..", "..", "..", "Tools"), + Path.Combine( + Directory.GetCurrentDirectory(), + "Tools") + ]; + + foreach (string candidate in candidates) + { + string fullPath = Path.GetFullPath(candidate); + string classFile = Path.Combine( + fullPath, + "SonicMfContainerTool.class"); + + if (File.Exists(classFile)) + { + return fullPath; + } + } + + return null; + } + + private static string? ExtractError(string output) + { + foreach (string line in output.Split( + ['\r', '\n'], + StringSplitOptions.RemoveEmptyEntries)) + { + if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) + { + return line["ERROR:".Length..].Trim(); + } + } + + return string.IsNullOrWhiteSpace(output) ? null : output; + } + + private static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + } + } +} + +public sealed class ContainerScanResult +{ + public bool Success { get; init; } + + public string Message { get; init; } = string.Empty; + + public IReadOnlyList Containers { get; init; } = []; + + public static ContainerScanResult Successful( + IReadOnlyList containers) + { + return new ContainerScanResult + { + Success = true, + Message = $"{containers.Count} Container gefunden.", + Containers = containers + }; + } + + public static ContainerScanResult Failed(string message) + { + return new ContainerScanResult + { + Success = false, + Message = message + }; + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs new file mode 100644 index 0000000..0c03cea --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Setup/SelectContainersForm.cs @@ -0,0 +1,432 @@ +using ZA.CoreService.ESBCertificateManager.Models; +using ZA.CoreService.ESBCertificateManager.Services; + +namespace ZA.CoreService.ESBCertificateManager.Setup; + +public sealed class SelectContainersForm : Form +{ + private readonly SetupCoordinator _coordinator; + private readonly SonicSystemOption _system; + private readonly SonicContainerScanner _scanner; + + private readonly ComboBox _cmbCompany; + private readonly CheckedListBox _lstContainers; + private readonly TextBox _txtManual; + private readonly TextBox _txtUser; + private readonly TextBox _txtPassword; + private readonly Label _lblStatus; + private readonly Button _btnScan; + private readonly Button _btnSave; + private bool _running; + + public int SavedCount { get; private set; } + + public SelectContainersForm( + SetupCoordinator coordinator, + SonicSystemOption system) + { + _coordinator = coordinator; + _system = system; + _scanner = new SonicContainerScanner(coordinator.Settings.Runtime); + + Text = "Container auswählen"; + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + ClientSize = new Size(640, 620); + BackColor = SettingsUi.Background; + ForeColor = SettingsUi.Text; + Font = new Font("Segoe UI", 10f); + + Label title = new() + { + Text = "Container für System", + Location = new Point(28, 20), + AutoSize = true, + Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), + ForeColor = SettingsUi.Text + }; + + Label subtitle = new() + { + Text = + $"{system.ConnectionName} · {system.ConnectionUrl}" + + Environment.NewLine + + "Scan von Sonic oder manuell eintragen, dann übernehmen.", + Location = new Point(30, 54), + Size = new Size(580, 40), + ForeColor = SettingsUi.Muted + }; + + Panel card = SettingsUi.CreateCard(); + card.Location = new Point(28, 110); + card.Size = new Size(584, 430); + card.Paint += SettingsUi.PaintCardBorder; + + _cmbCompany = SettingsUi.CreateComboBox(); + _lstContainers = new CheckedListBox + { + BackColor = Color.FromArgb(12, 28, 48), + ForeColor = SettingsUi.Text, + BorderStyle = BorderStyle.FixedSingle, + CheckOnClick = true, + Font = new Font("Segoe UI", 10f) + }; + + _txtManual = SettingsUi.CreateTextBox(); + _txtUser = SettingsUi.CreateTextBox(); + _txtPassword = SettingsUi.CreateTextBox(); + _txtPassword.UseSystemPasswordChar = true; + + AddField(card, "Company", _cmbCompany, 18, 16, 540); + AddField(card, "Gefundene / gewählte Container", _lstContainers, 18, 76, 540, 150); + + Label manualLabel = SettingsUi.CreateFieldLabel("Manuell hinzufügen"); + manualLabel.Location = new Point(18, 240); + _txtManual.Location = new Point(18, 262); + _txtManual.Width = 400; + + Button btnAddManual = SettingsUi.CreateGhostButton("+"); + btnAddManual.Location = new Point(430, 260); + btnAddManual.Size = new Size(50, 32); + btnAddManual.Click += (_, _) => AddManualContainer(); + + Button btnSuggest = SettingsUi.CreateGhostButton("ct-ZADBService"); + btnSuggest.Location = new Point(490, 260); + btnSuggest.Size = new Size(68, 32); + btnSuggest.Font = new Font("Segoe UI", 7.5f, FontStyle.Bold); + btnSuggest.Click += (_, _) => + { + _txtManual.Text = "ct-ZADBService"; + AddManualContainer(); + }; + + AddField(card, "Sonic-Benutzer (für Scan)", _txtUser, 18, 308, 250); + AddField(card, "Kennwort", _txtPassword, 288, 308, 270); + + _btnScan = SettingsUi.CreatePrimaryButton("Von Sonic scannen"); + _btnScan.Location = new Point(18, 375); + _btnScan.Size = new Size(200, 36); + _btnScan.Click += async (_, _) => await ScanAsync(); + + _lblStatus = new Label + { + Location = new Point(230, 380), + Size = new Size(330, 30), + ForeColor = SettingsUi.Muted + }; + + card.Controls.Add(manualLabel); + card.Controls.Add(_txtManual); + card.Controls.Add(btnAddManual); + card.Controls.Add(btnSuggest); + card.Controls.Add(_btnScan); + card.Controls.Add(_lblStatus); + + _btnSave = SettingsUi.CreatePrimaryButton("Auswahl speichern"); + _btnSave.Location = new Point(352, 555); + _btnSave.Size = new Size(260, 42); + _btnSave.Click += async (_, _) => await SaveAsync(); + + Button skip = SettingsUi.CreateGhostButton("Später"); + skip.Location = new Point(230, 555); + skip.Size = new Size(110, 42); + skip.Click += (_, _) => + { + DialogResult = DialogResult.Cancel; + Close(); + }; + + Controls.Add(title); + Controls.Add(subtitle); + Controls.Add(card); + Controls.Add(skip); + Controls.Add(_btnSave); + + Shown += async (_, _) => await InitializeAsync(); + } + + private async Task InitializeAsync() + { + try + { + IReadOnlyList companies = + await _coordinator.LoadCompaniesAsync(); + + _cmbCompany.Items.Clear(); + + foreach (CompanyOption company in companies) + { + _cmbCompany.Items.Add(company); + } + + if (_cmbCompany.Items.Count > 0) + { + int deIndex = 0; + + for (int index = 0; index < companies.Count; index++) + { + if (string.Equals( + companies[index].CompanyCode, + "DE", + StringComparison.OrdinalIgnoreCase)) + { + deIndex = index; + break; + } + } + + _cmbCompany.SelectedIndex = deIndex; + } + + IReadOnlyList profiles = + await _coordinator.LoadCredentialsAsync( + _system.SonicConnectionId); + + SonicCredentialProfile? profile = + profiles.FirstOrDefault(item => item.IsDefault) + ?? profiles.FirstOrDefault(); + + if (profile is not null) + { + _txtUser.Text = profile.UserName; + _txtPassword.Text = profile.Secret; + _lblStatus.Text = + "Profil geladen — Scan möglich."; + _lblStatus.ForeColor = SettingsUi.Green; + } + else + { + _lblStatus.Text = + "Kein Profil — Benutzer/Kennwort für Scan eingeben oder manuell."; + _lblStatus.ForeColor = SettingsUi.Gold; + } + + // Bereits vorhandene Container vorselektieren + IReadOnlyList existing = + await _coordinator.LoadContainersAsync( + _system.SonicConnectionId); + + foreach (ContainerOption container in existing) + { + AddContainerName(container.ContainerName, check: true); + } + } + catch (Exception ex) + { + _lblStatus.Text = ex.Message; + _lblStatus.ForeColor = SettingsUi.Red; + } + } + + private void AddManualContainer() + { + string name = _txtManual.Text.Trim(); + + if (name.Length == 0) + { + return; + } + + AddContainerName(name, check: true); + _txtManual.Clear(); + _txtManual.Focus(); + } + + private void AddContainerName(string name, bool check) + { + for (int index = 0; index < _lstContainers.Items.Count; index++) + { + if (string.Equals( + _lstContainers.Items[index]?.ToString(), + name, + StringComparison.OrdinalIgnoreCase)) + { + _lstContainers.SetItemChecked(index, check); + return; + } + } + + int added = _lstContainers.Items.Add(name); + _lstContainers.SetItemChecked(added, check); + } + + private async Task ScanAsync() + { + if (_running) + { + return; + } + + _running = true; + _btnScan.Enabled = false; + _btnSave.Enabled = false; + Cursor = Cursors.WaitCursor; + _lblStatus.Text = "Scanne Domain…"; + _lblStatus.ForeColor = SettingsUi.Muted; + + try + { + ContainerScanResult result = + await _scanner.ScanAsync( + _system, + _txtUser.Text, + _txtPassword.Text); + + if (!result.Success) + { + _lblStatus.Text = result.Message; + _lblStatus.ForeColor = SettingsUi.Red; + return; + } + + foreach (string container in result.Containers) + { + AddContainerName(container, check: true); + } + + _lblStatus.Text = + result.Containers.Count == 0 + ? "Scan OK, aber keine Container gefunden — bitte manuell." + : $"{result.Containers.Count} Container gescannt und vorausgewählt."; + + _lblStatus.ForeColor = + result.Containers.Count == 0 + ? SettingsUi.Gold + : SettingsUi.Green; + } + catch (Exception ex) + { + _lblStatus.Text = ex.Message; + _lblStatus.ForeColor = SettingsUi.Red; + } + finally + { + _running = false; + _btnScan.Enabled = true; + _btnSave.Enabled = true; + Cursor = Cursors.Default; + } + } + + private async Task SaveAsync() + { + if (_running) + { + return; + } + + if (_cmbCompany.SelectedItem is not CompanyOption company) + { + MessageBox.Show( + this, + "Bitte eine Company wählen.", + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + List selected = _lstContainers.CheckedItems + .Cast() + .Select(item => item.ToString() ?? string.Empty) + .Where(name => name.Length > 0) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (selected.Count == 0) + { + MessageBox.Show( + this, + "Bitte mindestens einen Container angehakt lassen " + + "oder manuell hinzufügen.", + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + _running = true; + _btnSave.Enabled = false; + Cursor = Cursors.WaitCursor; + + try + { + IReadOnlyList existing = + await _coordinator.LoadContainersAsync( + _system.SonicConnectionId); + + HashSet existingNames = existing + .Select(item => item.ContainerName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + int created = 0; + + foreach (string containerName in selected) + { + if (existingNames.Contains(containerName)) + { + continue; + } + + await _coordinator.AddSonicContainerAsync( + company.CompanyId, + _system.SonicConnectionId, + containerName, + containerDisplayName: null, + restartTimeoutSeconds: 180); + + created++; + } + + SavedCount = created; + + MessageBox.Show( + this, + created == 0 + ? "Alle gewählten Container waren bereits vorhanden." + : $"{created} Container gespeichert und geprüft.", + "Prüfung OK", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show( + this, + ex.Message, + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + finally + { + _running = false; + _btnSave.Enabled = true; + Cursor = Cursors.Default; + } + } + + private static void AddField( + Control parent, + string label, + Control input, + int x, + int y, + int width, + int height = 30) + { + Label fieldLabel = SettingsUi.CreateFieldLabel(label); + fieldLabel.Location = new Point(x, y); + input.Location = new Point(x, y + 22); + input.Width = width; + input.Height = height; + parent.Controls.Add(fieldLabel); + parent.Controls.Add(input); + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs index d6aaebb..cfa5235 100644 --- a/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs +++ b/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs @@ -381,7 +381,9 @@ public sealed class SetupWizardForm : Form Panel toolbar = CreateToolbar( "+ Container hinzufügen", - async () => await AddContainerAsync()); + async () => await AddContainerAsync(), + secondaryText: "Von Sonic scannen", + onSecondary: async () => await ScanContainersForSelectedSystemAsync()); _lvContainers = SettingsUi.CreateListView(); _lvContainers.Dock = DockStyle.Fill; @@ -418,7 +420,9 @@ public sealed class SetupWizardForm : Form private Panel CreateToolbar( string addButtonText, - Func onAdd) + Func onAdd, + string? secondaryText = null, + Func? onSecondary = null) { Panel toolbar = new() { @@ -433,6 +437,16 @@ public sealed class SetupWizardForm : Form add.Width = 230; add.Click += async (_, _) => await onAdd(); + if (secondaryText is not null && onSecondary is not null) + { + Button secondary = SettingsUi.CreateGhostButton(secondaryText); + secondary.Dock = DockStyle.Left; + secondary.Width = 180; + secondary.Margin = new Padding(8, 0, 0, 0); + secondary.Click += async (_, _) => await onSecondary(); + toolbar.Controls.Add(secondary); + } + toolbar.Controls.Add(add); return toolbar; } @@ -692,6 +706,78 @@ public sealed class SetupWizardForm : Form "Prüfung OK", MessageBoxButtons.OK, MessageBoxIcon.Information); + + SonicSystemOption? createdSystem = _systems + .FirstOrDefault(system => + system.SonicConnectionId == createdId); + + if (createdSystem is null + && _lvSystems.SelectedItems.Count > 0) + { + createdSystem = + _lvSystems.SelectedItems[0].Tag as SonicSystemOption; + } + + if (createdSystem is null) + { + return; + } + + DialogResult next = MessageBox.Show( + this, + "Container jetzt per Sonic-Scan oder manuell zuordnen?", + "Container", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (next != DialogResult.Yes) + { + return; + } + + using SelectContainersForm selectForm = + new(_coordinator, createdSystem); + + if (selectForm.ShowDialog(this) == DialogResult.OK) + { + await LoadContainersListAsync(); + await RefreshAllAsync(); + } + } + + private async Task ScanContainersForSelectedSystemAsync() + { + SonicSystemOption? system = null; + + if (_lvSystems.SelectedItems.Count > 0) + { + system = _lvSystems.SelectedItems[0].Tag as SonicSystemOption; + } + + system ??= _systems.FirstOrDefault(); + + if (system is null) + { + // Systeme-Liste ggf. nachladen + await LoadSystemsListAsync(); + system = _systems.FirstOrDefault(); + } + + if (system is null) + { + ShowWarning( + "Bitte zuerst ein Sonic-System anlegen oder auswählen."); + return; + } + + using SelectContainersForm selectForm = + new(_coordinator, system); + + if (selectForm.ShowDialog(this) == DialogResult.OK) + { + await LoadContainersListAsync(); + await RefreshAllAsync(); + } } private async Task AddContainerAsync() diff --git a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool$Args.class b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool$Args.class index 980efa7f184df61b09b4c69b5da51a44f5840c34..729a334072f764f11a838e3d8e57756b3181a3c5 100644 GIT binary patch literal 1876 zcmaJ>-)|d55dL;!=Ps9PYnvqffn8h(iGAi0*G=0%QcB{sV47d_N=j3za&flC#l8#n zxhUcRA%PH5i%?Y|r~n}u zpI=r14B_PtaI`51=m?^nVXx&n#r)*utm}Ao(Giuiu3PFIs}!mXnpZ3fx8_+Q&%ko> zq^hF>8iTrMSF3Nhl?8^##1;FhJy^1x!l30h& z(hYzD^I?d3X@CkWV#Q@dxq&0rYnx4vK zFU^cu*1733Cp4sSP(en=Aq;H_*jlQ3qD%o7geRP<41InVZ+%O}5faigo0Zluj3*U5 zrDFu6o9}OkO81M`Yj%nHVpHJFa*ZVCR8Xygrb$Aoc!ub-B#~8&Gjy8f|81b+1a*aJ zt~Hm2Q+QUvb2@T(o*}#;M9%T3xRpk;k;nw^dwQ+gk~!+9th+#Muxo-^c&b*Or#`ml zOXS&Md3OG_Nqcb(uPcz23ao>J5)39D&@RcX<=Z%(X}#-ED;R18hg-psR&Z2;{SSPf za?~uhR>_M~MVXiH+z!a%G3Ysm202@z5fvm?Ku$At52uy~6tXlX2(C9MQj|3Se)~Mi12=KS*7)@&P5Sv3A z0<{0vUGVdF&^uxL2Gv--1vT>c3i?wz9hQwa5c zFQ$H=oOmPiS#;C#U8wv$g!%jE;nxu3*AeF*ppSou0e%A+eiK>#5oY+uu=y=G{8PNe zKgTux1wP}q@h$%fclp=&m4AcZ>CB(}drZ;`QFPoxm;?-vVfWClU^o_w{SEqpAr({W zb#7QGPS9AvJ{eq7mUgKaUn1%>W}1?;P=CU1Kk5s<)Gj~j8DDDHk81f+cluG!`cgan ms4x0b6+h}M&iPb;eRqGcNY|Ih$8jEWWHma(>0f^V`~Cy1YJgb) literal 1419 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 diff --git a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.jar b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.jar index 593d2b02cb2ff1babaf9b22f15b65999fc9d403b..e944331d06efe9d9456aeac007e73411bb363e41 100644 GIT binary patch delta 6287 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 delta 8762 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 C>$}AO diff --git a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java index 5edd744..35b014f 100644 --- a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java +++ b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java @@ -1,68 +1,72 @@ import java.lang.reflect.Method; import java.util.Hashtable; +import java.util.Iterator; +import java.util.Set; import javax.management.ObjectName; /** * Sonic-MfApi-Hilfsprogramm. * - * Unterstützte Befehle: - * - * validate - * Prüft Anmeldung und lesenden Zugriff auf den Container. - * Es wird kein Neustart ausgeführt. - * - * restart - * Startet den angegebenen Container neu. - * - * Kompilierung mit Java 8: + * Befehle: + * validate - Anmeldung + lesender Container-Zugriff + * restart - Container neu starten (stop/restart) + * list - Container der Domain auflisten * * javac -source 8 -target 8 SonicMfContainerTool.java */ -public final class SonicMfConta*nerTool +public final class SonicMfContainerTool { - public static final *tring TOOL_VERSION = - "2026*07-27-validate-and-restart"; + public static final String TOOL_VERSION = + "2026-07-29-validate-restart-list"; - *ublic static void main(String[] ar*s) + public static void main(String[] args) { - info("ToolVersion*" + TOOL_VERSION); + info("ToolVersion=" + TOOL_VERSION); try - * { - Args parsedArgu*ents = Args.parse(args); + { + Args parsedArguments = Args.parse(args); - * if (!"validate".equals(parsedAr*uments.command) - &&*!"restart".equals(parsedArguments.*ommand)) + if (!"validate".equals(parsedArguments.command) + && !"restart".equals(parsedArguments.command) + && !"list".equals(parsedArguments.command)) { - * fail( - "Usa*e: SonicMfContainerTool " - * + " "* + "--domain D * - + "--url U " -* + "--user U " - * + "--container C*" - + "[--timeout SEC]"); - - return; -* } - - if (isBl*nk(parsedArguments.domain) - * || isBlank(parsedArguments*url) - || isBlank(pa*sedArguments.user) - *|| isBlank(parsedArguments.contain*r)) - { - *ail( - "domain, *rl, user und container sind Pflich*."); - - return; - * } - - if (isBlank(*arsedArguments.password)) - * { fail( - * "Das Sonic-Kennwort wurde nicht über " - + "ESB_SONIC_PASSWORD bereitgestellt."); + "Usage: SonicMfContainerTool " + + " " + + "--domain D " + + "--url U " + + "--user U " + + "[--container C] " + + "[--timeout SEC]"); + return; + } + if (isBlank(parsedArguments.domain) + || isBlank(parsedArguments.url) + || isBlank(parsedArguments.user)) + { + fail("domain, url und user sind Pflicht."); + return; + } + + boolean needsContainer = + "validate".equals(parsedArguments.command) + || "restart".equals(parsedArguments.command); + + if (needsContainer + && isBlank(parsedArguments.container)) + { + fail("container ist Pflicht fuer validate/restart."); + return; + } + + if (isBlank(parsedArguments.password)) + { + fail( + "Das Sonic-Kennwort wurde nicht ueber " + + "ESB_SONIC_PASSWORD bereitgestellt."); return; } @@ -78,7 +82,13 @@ public final class SonicMfConta*nerTool try { - if ("validate".equals( + if ("list".equals(parsedArguments.command)) + { + listContainers( + connector, + parsedArguments.domain); + } + else if ("validate".equals( parsedArguments.command)) { validateConnection( @@ -102,7 +112,6 @@ public final class SonicMfConta*nerTool catch (Throwable throwable) { Throwable root = rootOf(throwable); - String message = root.getMessage(); if (message == null) @@ -117,6 +126,115 @@ public final class SonicMfConta*nerTool } } + private static void listContainers( + Object connector, + String domain) + throws Exception + { + Method queryNamesMethod = + findMethod( + connector.getClass(), + "queryNames", + new Class[] + { + ObjectName.class, + javax.management.QueryExp.class + }); + + if (queryNamesMethod == null) + { + throw new IllegalStateException( + "queryNames(ObjectName, QueryExp) wurde nicht gefunden."); + } + + ObjectName pattern = + new ObjectName("*:ID=AGENT"); + + Object rawResult = queryNamesMethod.invoke( + connector, + new Object[] + { + pattern, + null + }); + + if (!(rawResult instanceof Set)) + { + throw new IllegalStateException( + "queryNames lieferte kein Set."); + } + + Set names = (Set) rawResult; + String domainPrefix = domain + "."; + int count = 0; + + System.out.println("OK:ContainerListBegin"); + + Iterator iterator = names.iterator(); + + while (iterator.hasNext()) + { + Object entry = iterator.next(); + + if (!(entry instanceof ObjectName)) + { + continue; + } + + ObjectName objectName = (ObjectName) entry; + String canonical = objectName.getCanonicalName(); + + if (canonical == null) + { + continue; + } + + // Erwartet: domain.containerName:ID=AGENT + if (!canonical.regionMatches( + true, + 0, + domainPrefix, + 0, + domainPrefix.length())) + { + continue; + } + + int colon = canonical.indexOf(':'); + + if (colon <= domainPrefix.length()) + { + continue; + } + + String containerName = + canonical.substring( + domainPrefix.length(), + colon); + + if (isBlank(containerName)) + { + continue; + } + + System.out.println( + "CONTAINER=" + + containerName.trim()); + + count++; + } + + System.out.println( + "OK:ContainerListEnd count=" + + count + + " domain=" + + domain + + " tool=" + + TOOL_VERSION); + + System.out.flush(); + } + private static Object connect( String url, String user, @@ -126,17 +244,9 @@ public final class SonicMfConta*nerTool { Hashtable environment = new Hashtable(); - environment.put( - "ConnectionURLs", - url); - - environment.put( - "DefaultUser", - user); - - environment.put( - "DefaultPassword", - password); + environment.put("ConnectionURLs", url); + environment.put("DefaultUser", user); + environment.put("DefaultPassword", password); Class addressClass = Class.forName( "com.sonicsw.mf.jmx.client.JMSConnectorAddress"); @@ -160,18 +270,14 @@ public final class SonicMfConta*nerTool .getConstructor(new Class[] {}) .newInstance(new Object[] {}); - trySetTimeout( - connector, - timeoutMilliseconds); + trySetTimeout(connector, timeoutMilliseconds); - Method connectMethod = - findConnect(clientClass); + Method connectMethod = findConnect(clientClass); if (connectMethod == null) { throw new IllegalStateException( - "JMSConnectorClient.connect(...) " - + "wurde nicht gefunden."); + "JMSConnectorClient.connect(...) wurde nicht gefunden."); } Class[] parameterTypes = @@ -202,33 +308,18 @@ public final class SonicMfConta*nerTool return connector; } - /** - * Führt ausschließlich eine lesende Prüfung aus. - * - * Die erfolgreiche Verbindung bestätigt bereits die Anmeldung. - * Zusätzlich wird versucht, die MBean-Information des Containers - * zu lesen. Es wird keine stop-, start- oder restart-Operation - * aufgerufen. - */ private static void validateConnection( Object connector, String domain, String container) throws Exception { - String shortName = - shortContainer(container); + String shortName = shortContainer(container); - ObjectName objectName = - new ObjectName( - domain - + "." - + shortName - + ":ID=AGENT"); + ObjectName objectName = new ObjectName( + domain + "." + shortName + ":ID=AGENT"); - info( - "ValidatingObjectName=" - + objectName); + info("ValidatingObjectName=" + objectName); Method getMBeanInfoMethod = findMethod( @@ -242,9 +333,7 @@ public final class SonicMfConta*nerTool if (getMBeanInfoMethod == null) { throw new IllegalStateException( - "Die lesende MfApi-Methode " - + "getMBeanInfo(ObjectName) " - + "wurde nicht gefunden."); + "getMBeanInfo(ObjectName) wurde nicht gefunden."); } Object result = getMBeanInfoMethod.invoke( @@ -278,61 +367,34 @@ public final class SonicMfConta*nerTool String container) throws Exception { - String shortName = - shortContainer(container); + String shortName = shortContainer(container); - ObjectName objectName = - new ObjectName( - domain - + "." - + shortName - + ":ID=AGENT"); + ObjectName objectName = new ObjectName( + domain + "." + shortName + ":ID=AGENT"); - info( - "ObjectName=" - + objectName); + info("ObjectName=" + objectName); - StringBuffer attempts = - new StringBuffer(); - - String[] operations = - new String[] - { - "stop", - "restart" - }; - - for (int index = 0; - index < operations.length; - index++) + StringBuffer attempts = new StringBuffer(); + String[] operations = new String[] { - String operation = - operations[index]; + "stop", + "restart" + }; + + for (int index = 0; index < operations.length; index++) + { + String operation = operations[index]; try { - info( - "Trying MBean.invoke(" - + operation - + ")"); - - invokeNoArgs( - connector, - objectName, - operation); - - okRestart( - "MBean." + operation, - shortName, - domain); - + info("Trying MBean.invoke(" + operation + ")"); + invokeNoArgs(connector, objectName, operation); + okRestart("MBean." + operation, shortName, domain); return; } catch (Throwable throwable) { - Throwable root = - rootOf(throwable); - + Throwable root = rootOf(throwable); String line = "MBean." + operation @@ -342,30 +404,12 @@ public final class SonicMfConta*nerTool + root.getMessage(); warn(line); - - attempts - .append(line) - .append('\n'); - - if ("stop".equals(operation) - && looksLikeStopSideEffect(root)) - { - okRestart( - "MBean.stop(side-effect)", - shortName, - domain); - - return; - } + attempts.append(line).append('\n'); } } throw new IllegalStateException( - "Neustart fehlgeschlagen. " - + "ToolVersion=" - + TOOL_VERSION - + "\nAttempts:\n" - + attempts.toString()); + "Neustart fehlgeschlagen:\n" + attempts.toString()); } private static void invokeNoArgs( @@ -375,7 +419,8 @@ public final class SonicMfConta*nerTool throws Exception { Method invokeMethod = - connector.getClass().getMethod( + findMethod( + connector.getClass(), "invoke", new Class[] { @@ -385,86 +430,34 @@ public final class SonicMfConta*nerTool String[].class }); + if (invokeMethod == null) + { + throw new IllegalStateException( + "invoke(...) wurde nicht gefunden."); + } + invokeMethod.invoke( connector, new Object[] { objectName, operation, - new Object[0], - new String[0] + null, + null }); } - private static void disconnect( - Object connector) - { - if (connector == null) - { - return; - } - - try - { - connector - .getClass() - .getMethod("disconnect") - .invoke(connector); - } - catch (Exception ignored) - { - // Die eigentliche Prüfung oder der Neustart - // darf durch einen Disconnect-Fehler nicht - // überschrieben werden. - } - } - - private static Method findMethod( - Class targetClass, - String methodName, - Class[] parameterTypes) - { - try - { - return targetClass.getMethod( - methodName, - parameterTypes); - } - catch (Exception ignored) - { - return null; - } - } - - private static boolean looksLikeStopSideEffect( - Throwable root) - { - if (root == null - || root.getMessage() == null) - { - return false; - } - - String message = - root.getMessage().toLowerCase(); - - return message.indexOf("disconnect") >= 0 - || message.indexOf("closed") >= 0 - || message.indexOf("not connected") >= 0 - || message.indexOf("connection lost") >= 0; - } - private static void okRestart( - String method, - String shortName, + String path, + String container, String domain) { System.out.println( "OK:RestartInvoked" - + " method=" - + method + + " path=" + + path + " container=" - + shortName + + container + " domain=" + domain + " tool=" @@ -473,43 +466,24 @@ public final class SonicMfConta*nerTool System.out.flush(); } - private static Method findConnect( - Class clientClass) + private static void disconnect(Object connector) { - Method[] methods = - clientClass.getMethods(); - - Method bestMethod = null; - - for (int index = 0; - index < methods.length; - index++) + try { Method method = - methods[index]; + findMethod( + connector.getClass(), + "disconnect", + new Class[] {}); - if (!"connect".equals( - method.getName())) + if (method != null) { - continue; - } - - Class[] parameterTypes = - method.getParameterTypes(); - - if (parameterTypes.length == 1 - || parameterTypes.length == 2) - { - bestMethod = method; - - if (parameterTypes.length == 2) - { - return method; - } + method.invoke(connector, new Object[] {}); } } - - return bestMethod; + catch (Throwable ignored) + { + } } private static void trySetTimeout( @@ -518,238 +492,197 @@ public final class SonicMfConta*nerTool { try { - connector - .getClass() - .getMethod( + Method method = + findMethod( + connector.getClass(), "setTimeout", new Class[] { Long.TYPE - }) - .invoke( + }); + + if (method != null) + { + method.invoke( connector, new Object[] { - Long.valueOf( - timeoutMilliseconds) + Long.valueOf(timeoutMilliseconds) }); + } } - catch (Exception ignored) + catch (Throwable ignored) { - try + } + } + + private static Method findConnect(Class clientClass) + { + Method[] methods = clientClass.getMethods(); + + for (int index = 0; index < methods.length; index++) + { + Method method = methods[index]; + + if (!"connect".equals(method.getName())) { - connector - .getClass() - .getMethod( - "setRequestTimeout", - new Class[] - { - Long.TYPE - }) - .invoke( - connector, - new Object[] - { - Long.valueOf( - timeoutMilliseconds) - }); + continue; } - catch (Exception ignoredSecond) + + Class[] types = method.getParameterTypes(); + + if (types.length == 1 || types.length == 2) { - // Optional, nicht jede Sonic-Version - // besitzt diese Methode. + return method; } } + + return null; + } + + private static Method findMethod( + Class type, + String name, + Class[] parameterTypes) + { + try + { + return type.getMethod(name, parameterTypes); + } + catch (NoSuchMethodException exception) + { + return null; + } } private static Object boxTimeout( - Class type, + Class parameterType, long timeoutMilliseconds) { - if (type == Integer.TYPE - || type == Integer.class) + if (parameterType == Integer.TYPE + || parameterType == Integer.class) { - return Integer.valueOf( - (int) Math.min( - Integer.MAX_VALUE, - timeoutMilliseconds)); + long seconds = + Math.max(1L, timeoutMilliseconds / 1000L); + + if (seconds > Integer.MAX_VALUE) + { + seconds = Integer.MAX_VALUE; + } + + return Integer.valueOf((int) seconds); } - return Long.valueOf( - timeoutMilliseconds); + return Long.valueOf(timeoutMilliseconds); } - private static String shortContainer( - String container) + private static String shortContainer(String container) { - String value = - container.trim(); + String value = container.trim(); + int colon = value.indexOf(':'); - int dotIndex = - value.lastIndexOf('.'); + if (colon > 0) + { + value = value.substring(0, colon); + } - return dotIndex >= 0 - ? value.substring(dotIndex + 1) - : value; + int dot = value.lastIndexOf('.'); + + if (dot >= 0 && dot < value.length() - 1) + { + value = value.substring(dot + 1); + } + + return value; } - private static Throwable rootOf( - Throwable throwable) + private static Throwable rootOf(Throwable throwable) { - Throwable current = - throwable; + Throwable current = throwable; while (current.getCause() != null && current.getCause() != current) { - current = - current.getCause(); + current = current.getCause(); } return current; } - private static boolean isBlank( - String value) + private static boolean isBlank(String value) { - return value == null - || value.trim().length() == 0; + return value == null || value.trim().length() == 0; } - private static void info( - String message) + private static void info(String message) { - System.out.println( - "INFO:" + message); - - System.out.flush(); + System.err.println("INFO: " + message); } - private static void warn( - String message) + private static void warn(String message) { - System.out.println( - "WARN:" + message); - - System.out.flush(); + System.err.println("WARN: " + message); } - private static void fail( - String message) + private static void fail(String message) { - System.err.println( - "ERROR:" + message); - - System.err.flush(); - + System.err.println("ERROR: " + message); System.exit(1); } private static final class Args { - String command; - String domain; - String url; - String user; - String password; - String container; + private String command; + private String domain; + private String url; + private String user; + private String password; + private String container; + private int timeoutSec = 60; - int timeoutSec = 120; - - static Args parse( - String[] arguments) + private static Args parse(String[] args) { - Args parsedArguments = - new Args(); + Args result = new Args(); - if (arguments == null - || arguments.length == 0) + if (args == null || args.length == 0) { - return parsedArguments; + fail("Kein Befehl angegeben."); } - parsedArguments.command = - arguments[0]; + result.command = args[0]; + result.password = + System.getenv("ESB_SONIC_PASSWORD"); - for (int index = 1; - index < arguments.length; - index++) + for (int index = 1; index < args.length; index++) { - String key = - arguments[index]; + String token = args[index]; - String value = - index + 1 < arguments.length - ? arguments[index + 1] - : null; - - if ("--domain".equals(key) - && value != null) + if ("--domain".equals(token) && index + 1 < args.length) { - parsedArguments.domain = value; - index++; + result.domain = args[++index]; } - else if ("--url".equals(key) - && value != null) + else if ("--url".equals(token) && index + 1 < args.length) { - parsedArguments.url = value; - index++; + result.url = args[++index]; } - else if ("--user".equals(key) - && value != null) + else if ("--user".equals(token) && index + 1 < args.length) { - parsedArguments.user = value; - index++; + result.user = args[++index]; } - else if ("--password".equals(key) - && value != null) + else if ("--container".equals(token) + && index + 1 < args.length) { - parsedArguments.password = value; - index++; + result.container = args[++index]; } - else if ("--container".equals(key) - && value != null) + else if ("--timeout".equals(token) + && index + 1 < args.length) { - parsedArguments.container = value; - index++; - } - else if ("--timeout".equals(key) - && value != null) - { - try - { - parsedArguments.timeoutSec = - Integer.parseInt(value); - } - catch (Exception ignored) - { - // Standardwert beibehalten. - } - - index++; + result.timeoutSec = + Integer.parseInt(args[++index]); } } - if (isBlank( - parsedArguments.password)) - { - String environmentPassword = - System.getenv( - "ESB_SONIC_PASSWORD"); - - if (!isBlank( - environmentPassword)) - { - parsedArguments.password = - environmentPassword; - } - } - - return parsedArguments; + return result; } } - - private SonicMfContainerTool() - { - } -} \ No newline at end of file +}