Add live TLS certificate probing and improve restart error handling.
Configure CertificateCheckUrl per container for curl-like TLS checks, classify Sonic permission errors, and extend setup wizard for container management. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -427,6 +427,7 @@ BEGIN
|
||||
[SonicConnectionId] [int] NOT NULL,
|
||||
[ContainerName] [nvarchar](250) NOT NULL,
|
||||
[ContainerDisplayName] [nvarchar](250) NULL,
|
||||
[CertificateCheckUrl] [nvarchar](500) NULL,
|
||||
[RestartTimeoutSeconds] [int] NOT NULL,
|
||||
[IsActive] [bit] NOT NULL,
|
||||
[CreationDateTime] [datetime2](3) NOT NULL,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
DATABASE: [EsbZertifikatManager]
|
||||
OBJECTNAME: SonicContainer.CertificateCheckUrl
|
||||
ACTION: ALTER
|
||||
IDX: 005
|
||||
DATE: 2026-07-31
|
||||
USER: CUR
|
||||
COMMENT: URL/Host:Port fuer TLS-Zertifikatspruefung (curl-aehnlich) am Container.
|
||||
*/
|
||||
USE [EsbZertifikatManager];
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.SonicContainer', N'CertificateCheckUrl') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[SonicContainer]
|
||||
ADD [CertificateCheckUrl] [nvarchar](500) NULL;
|
||||
END;
|
||||
GO
|
||||
@@ -46,6 +46,16 @@ Wenn der alte Platzhalter noch in SQL liegt:
|
||||
Zusätzlich: in `appsettings.json` ist `"Demo": { "Enabled": true, ... }` –
|
||||
die App zeigt den Demo-Pfad auch ohne SQL-Update in der Zielliste (Kopieren testen).
|
||||
|
||||
## TLS-Prüf-URL am Container (bestehende DB)
|
||||
|
||||
Für Live-Zertifikatsprüfung (curl-ähnlich) die Spalte nachziehen:
|
||||
|
||||
```text
|
||||
05_AddCertificateCheckUrl.sql
|
||||
```
|
||||
|
||||
Danach in Einstellungen → Container die Prüf-URL setzen, z. B. `https://server:8443`.
|
||||
|
||||
## Always Encrypted
|
||||
|
||||
| Objekt | Name |
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
# Dateiübersicht nach Ordnerstruktur
|
||||
|
||||
Was **jede einzelne Datei** macht – inkl. Sonic Management API.
|
||||
Projektroot der App: `ZA.CoreService.ESBCertificateManager/`
|
||||
|
||||
---
|
||||
|
||||
## 0. Gesamtbild (wer ruft wen)
|
||||
|
||||
```text
|
||||
Program.cs
|
||||
└─ Form1.cs (UI)
|
||||
├─ Zertifikat lesen → CertificateInfo
|
||||
├─ Ziele ← SonicContainerDiscovery (KnownContainers / Live)
|
||||
└─ Neustart
|
||||
└─ DeploymentOrchestrator
|
||||
└─ RestartExecutor
|
||||
└─ SonicManagementClient
|
||||
└─ SonicMfApiExecutor
|
||||
└─ java + Tools/SonicMfContainerTool
|
||||
└─ Sonic JARs (externe API)
|
||||
└─ Domain Manager tcp://…:13070
|
||||
```
|
||||
|
||||
Konfiguration: `appsettings.json` → `AppSettingsLoader` → `AppSettings` / `SonicConnection`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Projektroot (`ZA.CoreService.ESBCertificateManager/`)
|
||||
|
||||
### `Program.cs`
|
||||
Einstiegspunkt der WinForms-App.
|
||||
`ApplicationConfiguration.Initialize()` + `Application.Run(new Form1())`.
|
||||
Keine Business-Logik.
|
||||
|
||||
### `Form1.cs`
|
||||
**Gesamte Benutzeroberfläche** (Dark-Design, Sidebar, Zertifikatskarte, Grid, Buttons).
|
||||
Wichtige Aufgaben:
|
||||
|
||||
| Bereich | Was passiert |
|
||||
|---------|----------------|
|
||||
| Zertifikat | Dateidialog → `X509Certificate2` → Subject/Issuer/Fingerprint/Gültigkeit anzeigen |
|
||||
| Ziele | Grid aus `KnownContainers`; Button „Container laden“ → Live-Discovery |
|
||||
| Prüfung | `ValidateRestartOnly` (ContainerName + SonicConnectionName) |
|
||||
| Neustart | Bestätigung → `DeploymentOrchestrator.RestartOnlyAsync` → Ergebnisdialog |
|
||||
| Status | Statuszeile, Fortschritt im Grid, SonicHome/Java-Anzeige |
|
||||
|
||||
Deploy-Button ist vorhanden im Layout, aber **ausgeblendet/deaktiviert**.
|
||||
|
||||
### `Form1.Designer.cs`
|
||||
WinForms-Designer-Stub. `InitializeComponent()` ist praktisch leer – das Layout wird in `Form1.cs` per Code gebaut.
|
||||
|
||||
### `Form1.resx`
|
||||
Ressourcen-Datei zum Formular (Standard WinForms). Kaum eigene Inhalte.
|
||||
|
||||
### `appsettings.json`
|
||||
**Laufzeitkonfiguration** – die „Online“-Quelle für Sonic:
|
||||
|
||||
- `LogDirectory` – wohin Logs geschrieben werden
|
||||
- `SonicConnections[]` – Domain, URL, User/Pass, Java, Lib-Pfad, **KnownContainers**
|
||||
|
||||
Beispiel-Verbindung `DE-Test` → Domain `proalpha-test`, Container `ct-ZADBService`.
|
||||
|
||||
### `ZA.CoreService.ESBCertificateManager.csproj`
|
||||
Projektdatei: .NET 8 WinForms, NuGet (Configuration), welche Dateien nach `bin` kopiert werden (`appsettings`, `Tools`, `Assets`, `Docs`).
|
||||
|
||||
### `ZA.CoreService.ESBCertificateManager.csproj.user`
|
||||
Lokale Visual-Studio-Benutzereinstellungen (Debugger usw.). Nicht fachlich relevant.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ordner `Configuration/`
|
||||
|
||||
### `AppSettingsLoader.cs`
|
||||
Liest `appsettings.json` über `Microsoft.Extensions.Configuration` und bindet sie an `AppSettings`.
|
||||
Sucht zuerst im Output-Verzeichnis (`AppContext.BaseDirectory`), sonst im Arbeitsverzeichnis.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ordner `Models/` (Datenstrukturen)
|
||||
|
||||
### `AppSettings.cs`
|
||||
Root-Settings-Objekt:
|
||||
|
||||
- `LogDirectory`
|
||||
- `List<SonicConnection> SonicConnections`
|
||||
|
||||
### `SonicConnection.cs`
|
||||
Eine Sonic-/SMC-Verbindung:
|
||||
|
||||
| Property | Bedeutung |
|
||||
|----------|-----------|
|
||||
| `Name` | Alias, z. B. `DE-Test` |
|
||||
| `DomainName` | z. B. `proalpha-test` |
|
||||
| `ConnectionUrl` | `tcp://host:port` wie SMC |
|
||||
| `Username` / `Password` | SMC-Login |
|
||||
| `SonicHome` / `JavaHome` / `JavaPath` | Pfade |
|
||||
| `MfClientLibPath` | Ordner mit Client-JARs |
|
||||
| `KnownContainers` | Containerliste für die UI |
|
||||
| `TimeoutSeconds` / `PostRestartDelaySeconds` | Timeouts |
|
||||
| `ManagementModeDisplay` | Anzeigetext „MfApi …“ |
|
||||
|
||||
Enthält noch ältere Hilfsfelder/Enums (WinRM etc.) aus der Historie – der aktive Client nutzt nur den MfApi-Pfad.
|
||||
|
||||
### `DeploymentTarget.cs`
|
||||
Ein **Ziel** im Grid (ein Container zum Neustarten):
|
||||
|
||||
- `Id`, `Name`, `Environment`, `IsActive`
|
||||
- `ContainerName` – z. B. `ct-ZADBService`
|
||||
- `SonicConnectionName` – Verweis auf `SonicConnection.Name`
|
||||
- `RestartType` – für Neustart typischerweise `SonicContainer`
|
||||
- Weitere Felder (TargetDirectory, Tls*, Xapi*) können noch im Model stehen, werden für den aktuellen Neustart-Pfad nicht mehr aktiv genutzt
|
||||
|
||||
### `CertificateInfo.cs`
|
||||
Ergebnis der lokalen Zertifikatsprüfung:
|
||||
|
||||
- Subject, Issuer
|
||||
- FingerprintSha256
|
||||
- ValidFrom / ValidUntil
|
||||
- IsCurrentlyValid
|
||||
|
||||
### `DeploymentRunResult.cs`
|
||||
Ergebnisstrukturen nach einem Lauf:
|
||||
|
||||
- `DeploymentRunResult` – Gesamtlauf (Liste Zielergebnisse, Zeiten, OverallSuccess)
|
||||
- `TargetStepResult` – ein Ziel (Success, StatusText, Detail, …)
|
||||
- `ValidationIssue` / `PreflightValidationResult` – Vorabprüfung
|
||||
- `TargetProgressUpdate` – Fortschritt für die UI
|
||||
|
||||
---
|
||||
|
||||
## 4. Ordner `Services/` (Geschäftslogik)
|
||||
|
||||
### `DeploymentOrchestrator.cs`
|
||||
Orchestriert **nur Neustart** (kein Deploy/TLS):
|
||||
|
||||
1. `ValidateRestartOnly` → Preflight
|
||||
2. `RestartOnlyAsync` → für jedes Ziel `RestartExecutor`
|
||||
3. Schreibt Log über `RunLogger`
|
||||
4. Meldet Progress an die UI
|
||||
|
||||
### `RestartExecutor.cs`
|
||||
Mappt ein `DeploymentTarget` auf die passende `SonicConnection` und ruft
|
||||
`SonicManagementClient.RestartContainerAsync(containerName)` auf.
|
||||
|
||||
### `PreflightValidator.cs`
|
||||
Prüft vor dem Neustart:
|
||||
|
||||
- mindestens ein Ziel ausgewählt
|
||||
- `ContainerName` gesetzt
|
||||
- `SonicConnectionName` gesetzt
|
||||
|
||||
### `SonicManagementClient.cs`
|
||||
**Dünne Fassade** zur MfApi (kein WinRM/HTTP/XApi mehr):
|
||||
|
||||
| Methode | Zweck |
|
||||
|---------|--------|
|
||||
| `CheckConnectionAsync` | Ping / Soft-Fallback KnownContainers+TCP |
|
||||
| `GetContainersAsync` | Liste von Containern (API oder KnownContainers) |
|
||||
| `RestartContainerAsync` | Neustart + Wartezeit `PostRestartDelaySeconds` |
|
||||
|
||||
### `SonicMfApiExecutor.cs`
|
||||
**Brücke C# → Java.** Das ist der technische Kern „unsere Seite der API“:
|
||||
|
||||
1. Java 8+ finden (`JavaPath` / Suche)
|
||||
2. Classpath aus allen JARs unter `MfClientLibPath` / `SonicHome\lib` bauen
|
||||
3. `Tools\SonicMfContainerTool.class` laden (Version prüfen)
|
||||
4. Prozess starten: `java -cp … SonicMfContainerTool ping|list|restart …`
|
||||
5. Passwort über Env `ESB_SONIC_PASSWORD`
|
||||
6. Erfolg bei Neustart nur wenn Output `OK:RestartInvoked` enthält
|
||||
|
||||
Öffentliche Methoden:
|
||||
|
||||
- `TestConnectionAsync` → Tool-Befehl `ping`
|
||||
- `ListContainersAsync` → `list`
|
||||
- `RestartAsync` → `restart`
|
||||
- `ResolveRuntimePaths` → für Statuszeile in der UI
|
||||
|
||||
### `SonicContainerDiscovery.cs`
|
||||
Baut die Zielliste:
|
||||
|
||||
- `BuildTargetsFromConfig()` – aus `KnownContainers` in appsettings (Start der App)
|
||||
- `DiscoverAsync()` – live per `SonicManagementClient` listen + mit KnownContainers mergen
|
||||
|
||||
### `RunLogger.cs`
|
||||
Schreibt Textdateien unter `LogDirectory` (ein Log pro Lauf). Redaktiert ggf. secrets-ähnliche Strings.
|
||||
|
||||
### `PathResolver.cs`
|
||||
Löst relative Pfade gegen `AppContext.BaseDirectory` auf (z. B. für Logs).
|
||||
|
||||
---
|
||||
|
||||
## 5. Ordner `Tools/` (Java + Sonic-API)
|
||||
|
||||
Hier liegt der **eigentliche Aufruf der Sonic Management Application API**.
|
||||
|
||||
### `SonicMfContainerTool.java`
|
||||
Kleines Java-Hilfsprogramm (Reflection, kompilierbar ohne Sonic-JARs auf dem Compile-Classpath).
|
||||
|
||||
**Befehle:**
|
||||
|
||||
| Befehl | Bedeutung |
|
||||
|--------|-----------|
|
||||
| `ping` | Connect + Agenten zählen |
|
||||
| `list` | Containernamen der Domain ausgeben |
|
||||
| `restart` | Lifecycle auf einem Container |
|
||||
|
||||
**Connect (API-Login):**
|
||||
|
||||
```text
|
||||
Hashtable: ConnectionURLs, DefaultUser, DefaultPassword
|
||||
→ new JMSConnectorAddress(env)
|
||||
→ new JMSConnectorClient().connect(address, timeout)
|
||||
```
|
||||
|
||||
Klassen aus Sonic-JARs:
|
||||
|
||||
- `com.sonicsw.mf.jmx.client.JMSConnectorAddress`
|
||||
- `com.sonicsw.mf.jmx.client.JMSConnectorClient`
|
||||
|
||||
**Neustart – Ziel:**
|
||||
|
||||
```text
|
||||
ObjectName = {Domain}.{Container}:ID=AGENT
|
||||
Beispiel: proalpha-test.ct-ZADBService:ID=AGENT
|
||||
```
|
||||
|
||||
**Neustart – Wege (nacheinander):**
|
||||
|
||||
1. **MBean.invoke** `"stop"` / `"restart"` / `"shutdown"` ← bevorzugt remote
|
||||
2. **IAgentProxy** über `MFProxyFactory.createAgentProxy` (oft *unbounded*-Fehler)
|
||||
3. **DomainManager**-Operationen wie `restartContainer` / `stopContainer`
|
||||
|
||||
Erfolg:
|
||||
|
||||
```text
|
||||
OK:RestartInvoked method=…
|
||||
INFO:ToolVersion=…
|
||||
```
|
||||
|
||||
### `SonicMfContainerTool.class` / `SonicMfContainerTool$Args.class`
|
||||
Vorkompilierter Java-8-Bytecode (major 52), den die App zur Laufzeit nutzt (ohne `javac` auf dem Zielrechner).
|
||||
|
||||
### `SonicMfContainerTool.jar`
|
||||
Optional verpackte Variante des Tools (Classpath-Alternative).
|
||||
|
||||
### `verify-mfapi-classpath.bat`
|
||||
Hilfsskript: prüft manuell, ob `JMSConnectorAddress` mit den JARs unter `C:\DEV\MQ10.0\lib` ladbar ist.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ordner `Docs/` (Dokumentation)
|
||||
|
||||
### `Komplette-Erklaerung.md`
|
||||
Schritt-für-Schritt: Zertifikat, Preflight, C#-Kette, Java-API, Fehlerbilder, Präsentationsskript.
|
||||
|
||||
### `Neustart-und-Management-API.md`
|
||||
Kompaktere API-/Neustart-Erklärung inkl. „Was ist unnötig in der großen Sonic-API?“.
|
||||
|
||||
### `Dateiuebersicht-nach-Ordner.md`
|
||||
Diese Datei – Katalog aller Projektdateien.
|
||||
|
||||
---
|
||||
|
||||
## 7. Ordner `Assets/`
|
||||
|
||||
### `logo.png`
|
||||
UI-/Branding-Grafik.
|
||||
|
||||
### `TestCertificates/esb-test-cert.cer`
|
||||
Beispiel-Zertifikat zum lokalen Test der Dateierkennung (kein Sonic-Deploy).
|
||||
|
||||
---
|
||||
|
||||
## 8. Ordner `Demo/`
|
||||
|
||||
### `Demo/Central/esb-cert.cer`
|
||||
Weiteres Beispielzertifikat. Nicht Teil der Online-Neustart-Logik.
|
||||
|
||||
---
|
||||
|
||||
## 9. Sibling-Ordner `doc-10.0.10/` (neben der App, im Repo)
|
||||
|
||||
Produkt-Dokumentation Aurea CX Messenger / Sonic (nicht App-Code).
|
||||
|
||||
### `CXMessenger_2017_R3.htm`
|
||||
Index der offiziellen Doku. Wichtiger Link:
|
||||
|
||||
- **Management Application API Reference** → `Docs2017/api/mgmt_api/`
|
||||
Das ist die API-Familie, die SMC und unser Java-Tool nutzen.
|
||||
|
||||
### `Docs2017/api/analytics-offloader/…`
|
||||
Javadoc-Stub für Analytics Offload – **nicht** der Neustart-Pfad.
|
||||
|
||||
### PDFs / vollständige `mgmt_api`
|
||||
Im Index verlinkt; können im Checkout fehlen. Für Reviews idealerweise nachziehen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Die Sonic Management Application API – detailliert
|
||||
|
||||
### 10.1 Was „API“ hier bedeutet
|
||||
|
||||
Es ist **keine REST-URL der WinForms-App**.
|
||||
Es ist die **Java Management Application API** von Sonic/Aurea:
|
||||
|
||||
- Transport: JMS zum Domain Manager (`tcp://host:port`)
|
||||
- Steuerung: JMX-ähnliche ObjectNames + Operationen
|
||||
- Authentifizierung: SMC-User/Pass
|
||||
|
||||
Dieselbe Schicht wie der Restart in der Sonic Management Console.
|
||||
|
||||
### 10.2 Welche Sonic-Klassen wir wirklich brauchen
|
||||
|
||||
| Klasse | Aufgabe |
|
||||
|--------|---------|
|
||||
| `JMSConnectorAddress` | Verbindungsparameter kapseln |
|
||||
| `JMSConnectorClient` | Verbinden / disconnect / invoke / queryNames |
|
||||
| `MFProxyFactory` | `createAgentProxy` |
|
||||
| `IAgentProxy` | typisierte Methoden stop/restart/… (remote oft eingeschränkt) |
|
||||
| ObjectName `domain.container:ID=AGENT` | Ziel-Agent |
|
||||
|
||||
Benötigte JARs typischerweise unter `C:\DEV\MQ10.0\lib`:
|
||||
|
||||
- `mgmt_client.jar`
|
||||
- `mfcontext.jar`
|
||||
- `sonic_Client.jar`
|
||||
(+ weitere Abhängigkeiten im selben Ordner)
|
||||
|
||||
### 10.3 Warum die Sonic-API „viele unnötige Funktionen“ hat
|
||||
|
||||
Die Produkt-API deckt ab:
|
||||
|
||||
- Messaging (Queues, Topics, Durable Subs)
|
||||
- Broker-Admin
|
||||
- ESB
|
||||
- Metrics / Notifications
|
||||
- Config / Directory
|
||||
|
||||
**Für unseren Use-Case reichen 3 Schritte:** Connect → Agent finden → Stop/Restart.
|
||||
|
||||
Der Rest der JARs/Javadoc ist Produktumfang, nicht App-Feature.
|
||||
|
||||
### 10.4 „Unbounded client connector“
|
||||
|
||||
Remote-Clients (SMC, unser Tool) sind oft **unbounded**.
|
||||
Dann schlägt `IAgentProxy.restart()` fehl mit:
|
||||
|
||||
```text
|
||||
Operation unsupported for unbounded client connector
|
||||
```
|
||||
|
||||
Deshalb bevorzugt das Tool **MBean.invoke("stop")**.
|
||||
Operativ entspricht das oft SMC-Restart: Stop + Auto-Relaunch durch Launch Daemon.
|
||||
|
||||
### 10.5 Sequenz eines Neustarts (API-Ebene)
|
||||
|
||||
```text
|
||||
1. C#: RestartExecutor → SonicManagementClient → SonicMfApiExecutor
|
||||
2. C#: startet java -cp Tools;lib\*.jar SonicMfContainerTool restart …
|
||||
3. Java: JMSConnectorClient.connect(url, user, pass)
|
||||
4. Java: ObjectName = proalpha-test.ct-ZADBService:ID=AGENT
|
||||
5. Java: connector.invoke(on, "stop", …) // oder Fallback
|
||||
6. Java: OK:RestartInvoked
|
||||
7. C#: PostRestartDelaySeconds warten
|
||||
8. UI: Erfolg/Fehler + Log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Zertifikatsprüfung – welche Dateien
|
||||
|
||||
| Datei | Rolle |
|
||||
|-------|--------|
|
||||
| `Form1.cs` | Dateiauswahl, PFX-Passwort, Anzeige |
|
||||
| `Models/CertificateInfo.cs` | Ergebnisobjekt |
|
||||
| `Assets/...` / `Demo/...` | optionale Testdateien |
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. Datei wählen (`.cer/.crt/.pem/.pfx`)
|
||||
2. Als `X509Certificate2` laden
|
||||
3. Subject/Issuer/Fingerprint/Gültigkeit berechnen
|
||||
4. Anzeigen – **lokal verifiziert**
|
||||
|
||||
Nicht enthalten: Kopieren auf ESB, TLS-Probe gegen Live-Host, CA-Kettenprüfung.
|
||||
|
||||
---
|
||||
|
||||
## 12. Was absichtlich fehlt (gelöscht / deaktiviert)
|
||||
|
||||
| Früher | Status |
|
||||
|--------|--------|
|
||||
| `Data/targets.sample.json` + JSON-Repository | entfernt – Ziele aus KnownContainers |
|
||||
| SQL-Repos / Schema | entfernt |
|
||||
| WinRM / LocalCmd / stopcontainer.bat-Executor | entfernt |
|
||||
| CertificateDeployer / TlsCertificateProbe | entfernt |
|
||||
| XApi-Import | entfernt |
|
||||
| Deploy-Button | UI ausgeblendet |
|
||||
|
||||
---
|
||||
|
||||
## 13. Schnell-Tabelle „Datei → Ein Satz“
|
||||
|
||||
| Datei | Ein Satz |
|
||||
|-------|----------|
|
||||
| `Program.cs` | Startet die App |
|
||||
| `Form1.cs` | UI + Zertifikat + Neustart-Klick |
|
||||
| `Form1.Designer.cs` | Leerer Designer-Stub |
|
||||
| `Form1.resx` | Formular-Ressourcen |
|
||||
| `appsettings.json` | Sonic-Verbindung + KnownContainers + Java/Libs |
|
||||
| `*.csproj` | Build/Copy-Regeln |
|
||||
| `Configuration/AppSettingsLoader.cs` | JSON → Settings-Objekt |
|
||||
| `Models/AppSettings.cs` | Settings-Root |
|
||||
| `Models/SonicConnection.cs` | Eine SMC-/Domain-Verbindung |
|
||||
| `Models/DeploymentTarget.cs` | Ein Container-Ziel im Grid |
|
||||
| `Models/CertificateInfo.cs` | Lokales Zertifikats-Ergebnis |
|
||||
| `Models/DeploymentRunResult.cs` | Lauf-/Validierungs-DTOs |
|
||||
| `Services/DeploymentOrchestrator.cs` | Neustart über alle Ziele |
|
||||
| `Services/RestartExecutor.cs` | Ziel → SonicClient |
|
||||
| `Services/PreflightValidator.cs` | Pflichtfelder prüfen |
|
||||
| `Services/SonicManagementClient.cs` | Fassade ping/list/restart |
|
||||
| `Services/SonicMfApiExecutor.cs` | Java-Prozess + Classpath |
|
||||
| `Services/SonicContainerDiscovery.cs` | KnownContainers + Live-Liste |
|
||||
| `Services/RunLogger.cs` | Datei-Logs |
|
||||
| `Services/PathResolver.cs` | Relative Pfade |
|
||||
| `Tools/SonicMfContainerTool.java` | Sonic Management API Aufrufe |
|
||||
| `Tools/*.class` / `.jar` | Vorkompiliertes Tool |
|
||||
| `Tools/verify-mfapi-classpath.bat` | Classpath-Selbsttest |
|
||||
| `Docs/*.md` | Erklärungen |
|
||||
| `Assets/*` | Logo / Testzertifikat |
|
||||
| `Demo/*` | Demo-Zertifikat |
|
||||
| `doc-10.0.10/*` | Offizielle Sonic/CX-Messenger-Doku |
|
||||
|
||||
---
|
||||
|
||||
*Ende der Dateiübersicht. Für Ablauf-Details siehe `Komplette-Erklaerung.md`.*
|
||||
@@ -0,0 +1,507 @@
|
||||
# MfApi-Restart – vollständige Erklärung
|
||||
|
||||
Stand: Tool-Version `2026-07-24d-restart-only`
|
||||
Zweck dieses Dokuments: erklären, **wie die Sonic Management Application API (MfApi) in dieser App genutzt wird**, Schicht für Schicht – von Button-Klick bis MBean-Aufruf.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was ist „die API“ hier?
|
||||
|
||||
Es gibt **keine eigene REST-API** der WinForms-App.
|
||||
|
||||
Gemeint ist die **Sonic / Aurea MF Management Application API** (wie in der Sonic Management Console, SMC):
|
||||
|
||||
| Begriff | Bedeutung |
|
||||
|---------|-----------|
|
||||
| **MfApi** | Management Framework API über JMS/JMX-Client |
|
||||
| **Domain Manager** | zentrale Verwaltung der Sonic-Domain |
|
||||
| **Container / Agent** | laufende ESB-Instanz, z. B. `ct-ZADBService` |
|
||||
| **SMC** | Sonic Management Console (UI mit denselben Credentials/URL) |
|
||||
| **MBean** | verwaltbares Objekt im Domain Manager (`ObjectName`) |
|
||||
|
||||
Die App startet lokal einen **Java-Prozess**, der die offiziellen Sonic-Client-JARs lädt und denselben Neustart-Intent ausführt wie **Restart** in der SMC.
|
||||
|
||||
### Warum Java und nicht reines .NET?
|
||||
|
||||
Die Sonic-Client-Bibliotheken (`mgmt_client.jar`, `mfcontext.jar`, `sonic_Client.jar`, …) sind **Java**.
|
||||
C# orchestriert nur: Konfiguration lesen → `java.exe` starten → stdout auswerten.
|
||||
|
||||
---
|
||||
|
||||
## 2. Was die App bewusst *nicht* mehr macht
|
||||
|
||||
Nach dem Slimming ist die API-Schicht **nur Restart**:
|
||||
|
||||
| Entfernt | Früher |
|
||||
|----------|--------|
|
||||
| `ping` | Verbindungstest über Agent-Query |
|
||||
| `list` | Live-Containerliste aus dem Domain Manager |
|
||||
| IAgentProxy-Fallback | oft „Operation unsupported for unbounded client connector“ |
|
||||
| DomainManager-Scan | `restartContainer` / Manager-MBeans |
|
||||
| WinRM / `stopcontainer.bat` | lokaler Script-Pfad |
|
||||
| HTTP-REST-Pfade | ungenutzt |
|
||||
|
||||
Container kommen ausschließlich aus **`appsettings.json` → `KnownContainers`**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architektur-Überblick
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ WinForms UI (Form1) │
|
||||
│ Button „ESB neu starten“ │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DeploymentOrchestrator │
|
||||
│ Preflight → Schleife über Ziele → Logging / Progress │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RestartExecutor │
|
||||
│ Ziel → SonicConnection (Name-Match) │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SonicManagementClient │
|
||||
│ Restart + PostRestartDelay │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SonicMfApiExecutor (.NET) │
|
||||
│ Java finden, Classpath bauen, Process starten, Output prüfen │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ Process: java -cp … SonicMfContainerTool restart …
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SonicMfContainerTool (Java) │
|
||||
│ connect → MBean.invoke(stop|restart) → OK:RestartInvoked │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ tcp://…:13070 (JMS Management)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Sonic Domain Manager / Broker │
|
||||
│ ObjectName: proalpha-test.ct-ZADBService:ID=AGENT │
|
||||
│ Launch Daemon startet Container nach stop ggf. neu │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Konfiguration (`appsettings.json`)
|
||||
|
||||
Beispiel (aktuell):
|
||||
|
||||
```json
|
||||
{
|
||||
"LogDirectory": "Logs",
|
||||
"SonicConnections": [
|
||||
{
|
||||
"Name": "DE-Test",
|
||||
"DomainName": "proalpha-test",
|
||||
"ConnectionUrl": "tcp://dekun-painwbdet:13070",
|
||||
"Username": "Administrator",
|
||||
"Password": "Administrator",
|
||||
"SonicHome": "C:\\DEV\\MQ10.0",
|
||||
"JavaHome": "C:\\Program Files (x86)\\Java\\jre1.8.0_501",
|
||||
"JavaPath": "C:\\Program Files (x86)\\Java\\jre1.8.0_501\\bin\\java.exe",
|
||||
"MfClientLibPath": "C:\\DEV\\MQ10.0\\lib",
|
||||
"KnownContainers": [ "ct-ZADBService" ],
|
||||
"TimeoutSeconds": 120,
|
||||
"PostRestartDelaySeconds": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Felder erklärt
|
||||
|
||||
| Feld | Rolle |
|
||||
|------|--------|
|
||||
| **Name** | Alias der Verbindung. `DeploymentTarget.SonicConnectionName` muss dazu passen (hier `DE-Test`). |
|
||||
| **DomainName** | Sonic-Domain (`proalpha-test`). Teil des JMX-`ObjectName`. |
|
||||
| **ConnectionUrl** | Dieselbe TCP-URL wie in der SMC (`tcp://host:port`). |
|
||||
| **Username / Password** | SMC-Login (nicht Windows/WinRM). Passwort geht als Env `ESB_SONIC_PASSWORD` an Java. |
|
||||
| **SonicHome** | Installationsroot; Fallback für `lib\*.jar`, wenn `MfClientLibPath` leer/unbrauchbar. |
|
||||
| **JavaPath** | Bevorzugter Pfad zu `java.exe` (**Java 8+**, Class major ≥ 52). |
|
||||
| **JavaHome** | Alternativ `JavaHome\bin\java.exe`. |
|
||||
| **MfClientLibPath** | Ordner mit Client-JARs (`mgmt_client.jar`, `mfcontext.jar`, …). |
|
||||
| **KnownContainers** | Liste der Container-**Kurznamen** für die Grid-Ziele. |
|
||||
| **TimeoutSeconds** | Timeout für Java-Prozess und Tool-Connect (5–600 s, Clamp in Code). |
|
||||
| **PostRestartDelaySeconds** | Wartezeit **nach** erfolgreichem `OK:RestartInvoked` (0–120 s), bevor UI „fertig“ meldet. |
|
||||
|
||||
### Wichtige Namensunterscheidung
|
||||
|
||||
| Name | Beispiel | Ist |
|
||||
|------|----------|-----|
|
||||
| Verbindungs-Alias | `DE-Test` | Eintrag in `SonicConnections[].Name` |
|
||||
| Domain | `proalpha-test` | Sonic-Domain |
|
||||
| Container | `ct-ZADBService` | Agent / Container in SMC |
|
||||
|
||||
Häufiger Fehler: Alias `DE-Test` als Containername verwenden. Der ObjectName wäre dann falsch.
|
||||
|
||||
Korrekt:
|
||||
|
||||
```text
|
||||
proalpha-test.ct-ZADBService:ID=AGENT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Ziele (DeploymentTarget)
|
||||
|
||||
Beim Start baut `SonicContainerDiscovery.BuildTargetsFromConfig()` aus jeder Connection und jedem `KnownContainers`-Eintrag ein Ziel:
|
||||
|
||||
| Property | Quelle / Wert |
|
||||
|----------|----------------|
|
||||
| `Name` | `"DE-Test / ct-ZADBService"` |
|
||||
| `Environment` | `DomainName` |
|
||||
| `ContainerName` | `ct-ZADBService` |
|
||||
| `SonicConnectionName` | `DE-Test` |
|
||||
| `RestartType` | `SonicContainer` |
|
||||
|
||||
Es gibt **keinen Live-Query** mehr („Container laden“ wurde entfernt).
|
||||
|
||||
---
|
||||
|
||||
## 6. Aufrufkette – jede Methode
|
||||
|
||||
### 6.1 UI – `Form1`
|
||||
|
||||
| Schritt | Methode / Ereignis | Aufgabe |
|
||||
|---------|-------------------|---------|
|
||||
| 1 | `btnRestartOnly.Click` | Startet `RunRestartOnlyAsync()` |
|
||||
| 2 | `GetSelectedTargets()` | Angehakte Grid-Zeilen |
|
||||
| 3 | `_orchestrator.ValidateRestartOnly(...)` | Vorabprüfung |
|
||||
| 4 | MessageBox-Bestätigung | Nutzer bestätigt Neustart |
|
||||
| 5 | `_orchestrator.RestartOnlyAsync(...)` | eigentlicher Lauf |
|
||||
| 6 | Grid / Statuszeile | Ergebnis anzeigen (kein Erfolgs-Popup) |
|
||||
|
||||
### 6.2 `PreflightValidator.ValidateRestartOnly`
|
||||
|
||||
Prüft grob:
|
||||
|
||||
- mindestens ein Ziel gewählt
|
||||
- `ContainerName` gesetzt
|
||||
- `SonicConnectionName` gesetzt
|
||||
|
||||
Fehler → Abbruch **vor** Java.
|
||||
|
||||
### 6.3 `DeploymentOrchestrator`
|
||||
|
||||
| Methode | Aufgabe |
|
||||
|---------|---------|
|
||||
| `ValidateRestartOnly` | reicht an Preflight durch |
|
||||
| `RestartOnlyAsync` | Log öffnen, alle gewählten Ziele nacheinander |
|
||||
| `RestartTargetOnlyAsync` | Progress „Neustart…“ → Executor → `TargetStepResult` |
|
||||
|
||||
Kein Deploy, kein TLS, kein SQL.
|
||||
|
||||
### 6.4 `RestartExecutor.ExecuteAsync`
|
||||
|
||||
1. `ContainerName` prüfen
|
||||
2. `SonicConnection` per `Name == target.SonicConnectionName` finden
|
||||
3. `new SonicManagementClient(connection)`
|
||||
4. `RestartContainerAsync(target.ContainerName)`
|
||||
|
||||
### 6.5 `SonicManagementClient.RestartContainerAsync`
|
||||
|
||||
1. `_mfApi.RestartAsync(containerName)`
|
||||
2. Bei Erfolg: `Task.Delay(PostRestartDelaySeconds)`
|
||||
3. Status/Detail-String für UI/Log zurückgeben
|
||||
|
||||
Die Verzögerung gibt dem Launch Daemon Zeit, den Container wieder hochzufahren. Sie ist **kein** aktiver SMC-Status-Poll.
|
||||
|
||||
### 6.6 `SonicMfApiExecutor` – die C#-Brücke
|
||||
|
||||
#### `PrepareTool()`
|
||||
|
||||
1. **Java** über `ResolveJavaExe()`:
|
||||
- `JavaPath`
|
||||
- `JavaHome\bin\java.exe`
|
||||
- `JAVA_HOME` / `JRE_HOME`
|
||||
- `java.exe` aus PATH
|
||||
2. **Classpath** über `ResolveSonicClasspath()`:
|
||||
- alle `*.jar` aus `MfClientLibPath`, sonst `SonicHome\lib` / `SonicHome`
|
||||
- bevorzugte Reihenfolge: `mgmt_client.jar`, `mfcontext.jar`, `sonic_Client.jar`, `sonic_Crypto.jar`, `mf_common.jar`, dann Rest
|
||||
3. **Tool-Verzeichnis** mit `SonicMfContainerTool.class` (`Tools\` neben der EXE)
|
||||
|
||||
Ergebnis-Classpath grob:
|
||||
|
||||
```text
|
||||
<App>\Tools;<MfClientLibPath>\mgmt_client.jar;...;<weitere jars>
|
||||
```
|
||||
|
||||
#### `RestartAsync(containerName)`
|
||||
|
||||
Startet:
|
||||
|
||||
```text
|
||||
java -cp "<Tools>;<jars>" SonicMfContainerTool restart
|
||||
--domain proalpha-test
|
||||
--url tcp://dekun-painwbdet:13070
|
||||
--user Administrator
|
||||
--container ct-ZADBService
|
||||
--timeout 120
|
||||
```
|
||||
|
||||
- Argumente über `ProcessStartInfo.ArgumentList` (keine manuellen Quotes → sonst Classpath-Bugs)
|
||||
- Passwort: Umgebungsvariable `ESB_SONIC_PASSWORD`
|
||||
- stdout/stderr lesen, auf Timeout killen
|
||||
|
||||
#### Erfolgskriterium in C#
|
||||
|
||||
Alles muss gelten:
|
||||
|
||||
1. `ExitCode == 0`
|
||||
2. Ausgabe enthält `OK:RestartInvoked`
|
||||
3. Ausgabe enthält **kein** `ERROR:`
|
||||
|
||||
Sonst Fehlertext (+ Classpath-Hinweis bei `ClassNotFoundException` / `JMSConnectorAddress`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Java-Tool – der eigentliche API-Call
|
||||
|
||||
Datei: `Tools/SonicMfContainerTool.java`
|
||||
Bytecode: `Tools/SonicMfContainerTool.class` (Java 8 / major 52)
|
||||
Version-Stamp: `TOOL_VERSION = "2026-07-24d-restart-only"` (erste INFO-Zeile)
|
||||
|
||||
### 7.1 `main`
|
||||
|
||||
1. `INFO:ToolVersion=…` ausgeben
|
||||
2. Args parsen – nur Befehl `restart` erlaubt
|
||||
3. Pflicht: `--domain`, `--url`, `--user`, `--container`
|
||||
4. Passwort: `--password` oder Env `ESB_SONIC_PASSWORD`
|
||||
5. `connect(...)`
|
||||
6. `restart(...)`
|
||||
7. `disconnect` im `finally`
|
||||
|
||||
### 7.2 `connect` – Einloggen wie SMC
|
||||
|
||||
```text
|
||||
Hashtable env:
|
||||
ConnectionURLs = tcp://…
|
||||
DefaultUser = Administrator
|
||||
DefaultPassword = …
|
||||
|
||||
JMSConnectorAddress(env)
|
||||
JMSConnectorClient()
|
||||
optional: setTimeout / setRequestTimeout
|
||||
client.connect(address [, timeout])
|
||||
```
|
||||
|
||||
Klassen (aus den JARs, per Reflection):
|
||||
|
||||
- `com.sonicsw.mf.jmx.client.JMSConnectorAddress`
|
||||
- `com.sonicsw.mf.jmx.client.JMSConnectorClient`
|
||||
|
||||
Reflection statt direkter Compile-Abhängigkeit: das Tool kompiliert ohne Sonic-JARs auf dem Build-Rechner; zur Laufzeit müssen die JARs im Classpath liegen.
|
||||
|
||||
### 7.3 `restart` – ObjectName und Lifecycle
|
||||
|
||||
1. Kurzname bilden (`proalpha-test.ct-ZADBService` → `ct-ZADBService`)
|
||||
2. ObjectName:
|
||||
|
||||
```text
|
||||
<DomainName>.<ContainerKurzname>:ID=AGENT
|
||||
→ proalpha-test.ct-ZADBService:ID=AGENT
|
||||
```
|
||||
|
||||
3. Operationen der Reihe nach:
|
||||
|
||||
| Versuch | Operation | Bedeutung |
|
||||
|---------|-----------|-----------|
|
||||
| 1 | `stop` | Agent stoppen (SMC macht oft faktisch Stop; Daemon startet neu) |
|
||||
| 2 | `restart` | falls vorhanden und erlaubt |
|
||||
|
||||
4. Aufruf:
|
||||
|
||||
```text
|
||||
connector.invoke(objectName, op, new Object[0], new String[0])
|
||||
```
|
||||
|
||||
Das ist der JMX/`MBeanServerConnection.invoke`-Weg über den Sonic-Management-Connector.
|
||||
|
||||
### 7.4 Side-Effect bei `stop`
|
||||
|
||||
Wenn `stop` eine Exception wirft, deren Message auf Verbindungsabbruch hindeutet (`disconnect`, `closed`, `not connected`, `connection lost`), wertet das Tool das als **Erfolg**:
|
||||
|
||||
```text
|
||||
OK:RestartInvoked method=MBean.stop(side-effect) …
|
||||
```
|
||||
|
||||
Grund: der Agent geht runter und reißt oft die Management-Session – das ist erwartbar, kein „echter“ Fehlschlag.
|
||||
|
||||
### 7.5 Erfolgszeile
|
||||
|
||||
```text
|
||||
OK:RestartInvoked method=MBean.stop container=ct-ZADBService domain=proalpha-test tool=2026-07-24d-restart-only
|
||||
```
|
||||
|
||||
C# sucht genau nach `OK:RestartInvoked`.
|
||||
|
||||
### 7.6 Ausgabe-Konventionen
|
||||
|
||||
| Präfix | Bedeutung |
|
||||
|---------|-----------|
|
||||
| `INFO:` | Fortschritt / ObjectName / Trying … |
|
||||
| `WARN:` | fehlgeschlagener Versuch, Weiterversuch |
|
||||
| `ERROR:` | fatal, ExitCode ≠ 0 |
|
||||
| `OK:RestartInvoked` | Erfolg |
|
||||
|
||||
---
|
||||
|
||||
## 8. Sequenz (zeitlich)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as Form1
|
||||
participant Orch as DeploymentOrchestrator
|
||||
participant RE as RestartExecutor
|
||||
participant MC as SonicManagementClient
|
||||
participant EX as SonicMfApiExecutor
|
||||
participant JV as SonicMfContainerTool
|
||||
participant DM as Sonic Domain Manager
|
||||
|
||||
UI->>Orch: RestartOnlyAsync(selected)
|
||||
Orch->>RE: ExecuteAsync(target)
|
||||
RE->>MC: RestartContainerAsync(ct-ZADBService)
|
||||
MC->>EX: RestartAsync(...)
|
||||
EX->>EX: PrepareTool (java + jars + class)
|
||||
EX->>JV: Process start restart …
|
||||
JV->>DM: JMSConnectorClient.connect
|
||||
JV->>DM: invoke(…:ID=AGENT, "stop")
|
||||
DM-->>JV: ok / disconnect side-effect
|
||||
JV-->>EX: OK:RestartInvoked
|
||||
EX-->>MC: Success=true
|
||||
MC->>MC: Delay PostRestartDelaySeconds
|
||||
MC-->>UI: Status + Detail
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Dateien und Verantwortlichkeiten
|
||||
|
||||
| Datei | Rolle |
|
||||
|-------|--------|
|
||||
| `Form1.cs` | UI, Auswahl, Bestätigung, Status |
|
||||
| `Services/DeploymentOrchestrator.cs` | Lauf orchestrieren, Log |
|
||||
| `Services/PreflightValidator.cs` | Vorabprüfung |
|
||||
| `Services/RestartExecutor.cs` | Ziel → Connection |
|
||||
| `Services/SonicManagementClient.cs` | Restart + Delay |
|
||||
| `Services/SonicMfApiExecutor.cs` | Java-Prozess, Classpath, Erfolgsauswertung |
|
||||
| `Services/SonicContainerDiscovery.cs` | KnownContainers → Grid-Ziele |
|
||||
| `Models/SonicConnection.cs` | Konfigurationsmodell |
|
||||
| `Models/DeploymentTarget.cs` | Zielzeile |
|
||||
| `Tools/SonicMfContainerTool.java` | API-Client (Quellcode) |
|
||||
| `Tools/SonicMfContainerTool.class` | ausgeliefert / Java-8-Bytecode |
|
||||
| `appsettings.json` | Verbindungen + Containerliste |
|
||||
|
||||
---
|
||||
|
||||
## 10. Laufzeit-Voraussetzungen
|
||||
|
||||
1. **Java 8+** erreichbar (`JavaPath` empfohlen)
|
||||
2. **`MfClientLibPath`** zeigt auf Ordner mit Sonic-Client-JARs
|
||||
3. **`Tools\SonicMfContainerTool.class`** liegt neben der gebauten App
|
||||
4. Netzwerk zu `ConnectionUrl` (Port z. B. 13070)
|
||||
5. Gültige SMC-Credentials
|
||||
6. Containername stimmt mit SMC überein (`ct-ZADBService`)
|
||||
7. Domain Manager / Launch Daemon müssen Container nach Stop wieder starten können
|
||||
|
||||
### Tool neu kompilieren
|
||||
|
||||
```text
|
||||
javac --release 8 -encoding UTF-8 Tools\SonicMfContainerTool.java
|
||||
```
|
||||
|
||||
(oder `-source 1.8 -target 1.8`)
|
||||
|
||||
Danach App neu bauen, damit `.class` nach `bin\…\Tools\` kopiert wird (csproj Content-Copy).
|
||||
|
||||
---
|
||||
|
||||
## 11. Typische Fehler und Ursache
|
||||
|
||||
| Symptom | Ursache | Maßnahme |
|
||||
|---------|---------|----------|
|
||||
| `ClassNotFoundException: JMSConnectorAddress` | Classpath ohne MF-JARs / falsche Quotes | `MfClientLibPath` prüfen; `ArgumentList` nicht manuell quoten |
|
||||
| `UnsupportedClassVersionError` | Runtime zu alt oder `.class` zu neu | Java 8+ Runtime; Tool mit `--release 8` bauen |
|
||||
| `Java nicht gefunden` | `JavaPath` falsch | appsettings korrigieren |
|
||||
| `Tools\SonicMfContainerTool.class nicht gefunden` | Build/Copy fehlt | Projekt neu bauen |
|
||||
| `Neustart fehlgeschlagen` + Attempts | MBean `stop`/`restart` abgelehnt | ObjectName/Rechte/Containerstatus in SMC prüfen |
|
||||
| Timeout | Domain Manager hängt / Netz | `TimeoutSeconds`, Firewall, Broker |
|
||||
| Falscher Container | Alias statt Kurzname | `KnownContainers`: `ct-ZADBService` |
|
||||
| Kein `ToolVersion=` in Output | alte/falsche `.class` | Tools neu kompilieren, Output-Ordner prüfen |
|
||||
|
||||
---
|
||||
|
||||
## 12. Verhältnis zur SMC
|
||||
|
||||
| SMC | Diese App |
|
||||
|-----|-----------|
|
||||
| Login mit User/Pass auf Domain-URL | dieselben Werte in appsettings |
|
||||
| Container im Baum wählen | `KnownContainers` + Grid |
|
||||
| Rechtsklick → Restart | `MBean.invoke("stop"|"restart")` auf `…:ID=AGENT` |
|
||||
| UI zeigt Online/Offline | App wartet nur `PostRestartDelaySeconds`, pollt Status nicht |
|
||||
|
||||
Operativ oft: **Stop am Agent-MBean**; der **Launch Daemon** bringt den Container wieder hoch – analog zu vielen SMC-Restart-Verhalten bei remote „unbounded“ Clients.
|
||||
|
||||
Früher getestet: `IAgentProxy.restart()` schlägt remote häufig mit
|
||||
`Operation unsupported for unbounded client connector` fehl – deshalb der direkte MBean-Weg.
|
||||
|
||||
---
|
||||
|
||||
## 13. Sicherheitshinweise
|
||||
|
||||
- Passwort steht in `appsettings.json` (Klartext) und kurzzeitig in der Prozess-Umgebung `ESB_SONIC_PASSWORD`.
|
||||
- Nicht in öffentliche Repos committen bzw. Secrets auslagern.
|
||||
- Logs (`LogDirectory`) können Tool-Output enthalten – ggf. sensible Zeilen beachten.
|
||||
|
||||
---
|
||||
|
||||
## 14. Manueller Test (ohne UI)
|
||||
|
||||
Mit denselben Werten wie in appsettings:
|
||||
|
||||
```text
|
||||
set ESB_SONIC_PASSWORD=Administrator
|
||||
|
||||
java -cp "Tools;C:\DEV\MQ10.0\lib\*" SonicMfContainerTool restart ^
|
||||
--domain proalpha-test ^
|
||||
--url tcp://dekun-painwbdet:13070 ^
|
||||
--user Administrator ^
|
||||
--container ct-ZADBService ^
|
||||
--timeout 120
|
||||
```
|
||||
|
||||
Erwartet u. a.:
|
||||
|
||||
```text
|
||||
INFO:ToolVersion=2026-07-24d-restart-only
|
||||
INFO:ObjectName=proalpha-test.ct-ZADBService:ID=AGENT
|
||||
INFO:Trying MBean.invoke(stop)
|
||||
OK:RestartInvoked method=MBean.stop container=ct-ZADBService domain=proalpha-test tool=2026-07-24d-restart-only
|
||||
```
|
||||
|
||||
(Unter Windows Classpath ggf. mit `;` und expliziten JAR-Namen statt `*`, je nach Shell.)
|
||||
|
||||
---
|
||||
|
||||
## 15. Kurzfassung
|
||||
|
||||
1. UI wählt Ziel aus `KnownContainers`.
|
||||
2. C# findet die passenden Sonic-Verbindungsdaten.
|
||||
3. C# startet Java mit MF-Client-JARs + `SonicMfContainerTool`.
|
||||
4. Java verbindet sich wie die SMC an den Domain Manager.
|
||||
5. Java ruft auf dem Agent-MBean `stop` (sonst `restart`) auf.
|
||||
6. Bei `OK:RestartInvoked` wartet C# noch `PostRestartDelaySeconds` und meldet Erfolg.
|
||||
|
||||
**Der einzige echte Sonic-API-Call der App ist:**
|
||||
`JMSConnectorClient.invoke(<domain>.<container>:ID=AGENT, "stop"|"restart", …)`.
|
||||
@@ -1651,7 +1651,8 @@ namespace ZA.CoreService.ESBCertificateManager
|
||||
private async Task ProbeTargetCertificatesAsync(
|
||||
IReadOnlySet<int>? preferLoadedCertificateForTargetIds = null)
|
||||
{
|
||||
CertificateProbeService probe = new();
|
||||
CertificateProbeService fileProbe = new();
|
||||
TlsEndpointProbeService tlsProbe = new();
|
||||
string? password = _loadedCertificatePassword;
|
||||
Func<string?>? passwordPrompt = password is null
|
||||
? null
|
||||
@@ -1664,8 +1665,37 @@ namespace ZA.CoreService.ESBCertificateManager
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bevorzugt Live-TLS-Probe (curl-ähnlich), wenn URL am Container gesetzt.
|
||||
if (!string.IsNullOrWhiteSpace(target.TlsHost)
|
||||
&& target.TlsPort is int tlsPort)
|
||||
{
|
||||
string checkUrl =
|
||||
string.IsNullOrWhiteSpace(target.TlsServerName)
|
||||
? $"{target.TlsHost}:{tlsPort}"
|
||||
: $"https://{target.TlsServerName}:{tlsPort}";
|
||||
|
||||
CertificateProbeResult tlsResult = await Task.Run(
|
||||
() => tlsProbe.Probe(checkUrl));
|
||||
|
||||
if (IsDisposed || !dgvTargets.Columns.Contains("Expiry"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (tlsResult.Success && tlsResult.Certificate is not null)
|
||||
{
|
||||
row.Cells["Expiry"].Value =
|
||||
"TLS · "
|
||||
+ FormatCertificateExpiryCell(tlsResult.Certificate);
|
||||
continue;
|
||||
}
|
||||
|
||||
row.Cells["Expiry"].Value = "TLS · nicht erreichbar";
|
||||
continue;
|
||||
}
|
||||
|
||||
CertificateProbeResult result = await Task.Run(
|
||||
() => probe.Probe(target.FullTargetPath, passwordPrompt));
|
||||
() => fileProbe.Probe(target.FullTargetPath, passwordPrompt));
|
||||
|
||||
if (IsDisposed || !dgvTargets.Columns.Contains("Expiry"))
|
||||
{
|
||||
@@ -1697,6 +1727,13 @@ namespace ZA.CoreService.ESBCertificateManager
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.AccessDenied)
|
||||
{
|
||||
row.Cells["Expiry"].Value =
|
||||
CertificateProbeResult.AccessDeniedShortText;
|
||||
continue;
|
||||
}
|
||||
|
||||
row.Cells["Expiry"].Value = result.Success
|
||||
? "Datei fehlt"
|
||||
: "nicht erreichbar";
|
||||
@@ -1940,13 +1977,27 @@ namespace ZA.CoreService.ESBCertificateManager
|
||||
runResult.TargetResults
|
||||
.Where(result => !result.Success)
|
||||
.Select(result =>
|
||||
$"{result.TargetName}: {result.StatusText}"
|
||||
$"{result.TargetName}"
|
||||
+ Environment.NewLine
|
||||
+ (result.Detail ?? string.Empty)));
|
||||
+ SonicErrorClassifier.FormatRestartFailure(
|
||||
result.StatusText,
|
||||
result.Detail)));
|
||||
|
||||
bool permissionIssue =
|
||||
SonicErrorClassifier.LooksLikeMissingPermission(details);
|
||||
|
||||
if (permissionIssue)
|
||||
{
|
||||
SetStatus(
|
||||
"Neustart nicht möglich – fehlende Benutzerrechte.",
|
||||
isError: true);
|
||||
}
|
||||
|
||||
CopyableErrorDialog.Show(
|
||||
this,
|
||||
"Neustart fehlgeschlagen (MfApi)",
|
||||
permissionIssue
|
||||
? "Keine Rechte für den Neustart"
|
||||
: "Neustart fehlgeschlagen (MfApi)",
|
||||
string.IsNullOrWhiteSpace(details)
|
||||
? "Neustart fehlgeschlagen (keine Details)."
|
||||
: details);
|
||||
@@ -1958,11 +2009,26 @@ namespace ZA.CoreService.ESBCertificateManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus($"Neustart fehlgeschlagen: {ex.Message}", isError: true);
|
||||
string formatted =
|
||||
SonicErrorClassifier.FormatRestartFailure(
|
||||
"Neustart fehlgeschlagen",
|
||||
ex.ToString());
|
||||
|
||||
bool permissionIssue =
|
||||
SonicErrorClassifier.LooksLikeMissingPermission(formatted);
|
||||
|
||||
SetStatus(
|
||||
permissionIssue
|
||||
? "Neustart nicht möglich – fehlende Benutzerrechte."
|
||||
: $"Neustart fehlgeschlagen: {ex.Message}",
|
||||
isError: true);
|
||||
|
||||
CopyableErrorDialog.Show(
|
||||
this,
|
||||
"Neustart fehlgeschlagen (MfApi)",
|
||||
ex.ToString());
|
||||
permissionIssue
|
||||
? "Keine Rechte für den Neustart"
|
||||
: "Neustart fehlgeschlagen (MfApi)",
|
||||
formatted);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -2078,13 +2144,27 @@ namespace ZA.CoreService.ESBCertificateManager
|
||||
runResult.TargetResults
|
||||
.Where(result => !result.Success)
|
||||
.Select(result =>
|
||||
$"{result.TargetName}: {result.StatusText}"
|
||||
$"{result.TargetName}"
|
||||
+ Environment.NewLine
|
||||
+ (result.Detail ?? string.Empty)));
|
||||
+ SonicErrorClassifier.FormatRestartFailure(
|
||||
result.StatusText,
|
||||
result.Detail)));
|
||||
|
||||
bool permissionIssue =
|
||||
SonicErrorClassifier.LooksLikeMissingPermission(details);
|
||||
|
||||
if (permissionIssue)
|
||||
{
|
||||
SetStatus(
|
||||
"Austausch/Neustart nicht möglich – fehlende Benutzerrechte.",
|
||||
isError: true);
|
||||
}
|
||||
|
||||
CopyableErrorDialog.Show(
|
||||
this,
|
||||
"Austausch fehlgeschlagen",
|
||||
permissionIssue
|
||||
? "Keine Rechte für den Neustart"
|
||||
: "Austausch fehlgeschlagen",
|
||||
details);
|
||||
|
||||
await ProbeTargetCertificatesAsync(exchangedTargetIds);
|
||||
|
||||
@@ -26,6 +26,11 @@ public sealed class ContainerOption
|
||||
|
||||
public string? ContainerDisplayName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint für TLS-Zertifikatsprüfung, z. B. https://host:8443 oder host:8443.
|
||||
/// </summary>
|
||||
public string? CertificateCheckUrl { get; init; }
|
||||
|
||||
public required string CompanyCode { get; init; }
|
||||
|
||||
public required string ConnectionName { get; init; }
|
||||
|
||||
@@ -34,7 +34,20 @@ public sealed class CertificateProbeService
|
||||
+ string.Join(", ", SupportedExtensions));
|
||||
}
|
||||
|
||||
if (!File.Exists(normalized))
|
||||
PathReachability reachability = AssessPathReachability(normalized);
|
||||
|
||||
if (reachability == PathReachability.AccessDenied)
|
||||
{
|
||||
return CertificateProbeResult.ForAccessDenied(normalized);
|
||||
}
|
||||
|
||||
if (reachability == PathReachability.Unreachable)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
"Pfad nicht erreichbar.");
|
||||
}
|
||||
|
||||
if (reachability == PathReachability.FileMissing)
|
||||
{
|
||||
return CertificateProbeResult.Missing(
|
||||
normalized,
|
||||
@@ -72,6 +85,14 @@ public sealed class CertificateProbeService
|
||||
"Das Kennwort ist falsch oder die Datei ist beschädigt.");
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return CertificateProbeResult.ForAccessDenied(normalized);
|
||||
}
|
||||
catch (IOException ex) when (IsAccessOrLogonFailure(ex))
|
||||
{
|
||||
return CertificateProbeResult.ForAccessDenied(normalized);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
@@ -151,6 +172,154 @@ public sealed class CertificateProbeService
|
||||
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private enum PathReachability
|
||||
{
|
||||
FileExists,
|
||||
FileMissing,
|
||||
AccessDenied,
|
||||
Unreachable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unterscheidet „Datei fehlt“ von „Share/Pfad ohne Admin-Konto nicht erreichbar“.
|
||||
/// File.Exists liefert bei fehlenden Credentials oft nur false.
|
||||
/// </summary>
|
||||
private static PathReachability AssessPathReachability(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
return PathReachability.FileExists;
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return PathReachability.AccessDenied;
|
||||
}
|
||||
catch (IOException ex) when (IsAccessOrLogonFailure(ex))
|
||||
{
|
||||
return PathReachability.AccessDenied;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return PathReachability.Unreachable;
|
||||
}
|
||||
|
||||
string? directory = Path.GetDirectoryName(filePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
return PathReachability.Unreachable;
|
||||
}
|
||||
|
||||
return AssessDirectoryReachability(directory);
|
||||
}
|
||||
|
||||
private static PathReachability AssessDirectoryReachability(string directory)
|
||||
{
|
||||
string current = directory.TrimEnd('\\', '/');
|
||||
|
||||
while (!string.IsNullOrWhiteSpace(current))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(current))
|
||||
{
|
||||
// Exists kann bei UNC ohne Rechte „lügen“ – Auflisten erzwingen.
|
||||
_ = Directory.EnumerateFileSystemEntries(current)
|
||||
.Any();
|
||||
return PathReachability.FileMissing;
|
||||
}
|
||||
|
||||
if (current.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Exists=false: echter Zugriffsfehler oder Ordner fehlt.
|
||||
_ = Directory.GetFileSystemEntries(current);
|
||||
return PathReachability.FileMissing;
|
||||
}
|
||||
|
||||
// Lokaler Ordner fehlt → für Erst-Deployment als „Datei fehlt“ werten.
|
||||
return PathReachability.FileMissing;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return PathReachability.AccessDenied;
|
||||
}
|
||||
catch (IOException ex) when (IsAccessOrLogonFailure(ex))
|
||||
{
|
||||
return PathReachability.AccessDenied;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
string? parent = GetParentPath(current);
|
||||
|
||||
if (parent is null || parent == current)
|
||||
{
|
||||
return current.StartsWith(@"\\", StringComparison.Ordinal)
|
||||
? PathReachability.Unreachable
|
||||
: PathReachability.FileMissing;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
continue;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return PathReachability.Unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
return PathReachability.Unreachable;
|
||||
}
|
||||
|
||||
private static string? GetParentPath(string path)
|
||||
{
|
||||
string trimmed = path.TrimEnd('\\', '/');
|
||||
|
||||
if (trimmed.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
// \\server\share → Stop; darunter weiter nach oben.
|
||||
string withoutPrefix = trimmed[2..];
|
||||
int slash = withoutPrefix.IndexOfAny(['\\', '/']);
|
||||
|
||||
if (slash < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int second = withoutPrefix.IndexOfAny(['\\', '/'], slash + 1);
|
||||
|
||||
if (second < 0)
|
||||
{
|
||||
// Bereits Share-Root \\server\share
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
string? parent = Path.GetDirectoryName(trimmed);
|
||||
return string.IsNullOrWhiteSpace(parent) ? null : parent;
|
||||
}
|
||||
|
||||
private static bool IsAccessOrLogonFailure(IOException ex)
|
||||
{
|
||||
// HRESULT-Lower-Word = Win32-Fehlercode
|
||||
int win32 = ex.HResult & 0xFFFF;
|
||||
|
||||
return win32 is
|
||||
5 or // ERROR_ACCESS_DENIED
|
||||
53 or // ERROR_BAD_NETPATH
|
||||
67 or // ERROR_BAD_NET_NAME
|
||||
86 or // ERROR_INVALID_PASSWORD
|
||||
1326 or // ERROR_LOGON_FAILURE
|
||||
59 or // ERROR_UNEXP_NET_ERR
|
||||
64 or // ERROR_NETNAME_DELETED
|
||||
1219 or // ERROR_SESSION_CREDENTIAL_CONFLICT
|
||||
1240 or // ERROR_LOGIN_WKSTA_RESTRICTION
|
||||
1245 or // ERROR_ACCOUNT_RESTRICTION
|
||||
1396; // ERROR_WRONG_TARGET_NAME
|
||||
}
|
||||
|
||||
private static string? NormalizePath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
@@ -233,10 +402,14 @@ public sealed class CertificateProbeService
|
||||
|
||||
public sealed class CertificateProbeResult
|
||||
{
|
||||
public const string AccessDeniedShortText = "Admin nötig";
|
||||
|
||||
public bool Success { get; init; }
|
||||
|
||||
public bool FileExists { get; init; }
|
||||
|
||||
public bool AccessDenied { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public string Message { get; init; } = string.Empty;
|
||||
@@ -289,6 +462,19 @@ public sealed class CertificateProbeResult
|
||||
};
|
||||
}
|
||||
|
||||
public static CertificateProbeResult ForAccessDenied(string path)
|
||||
{
|
||||
return new CertificateProbeResult
|
||||
{
|
||||
Success = false,
|
||||
FileExists = false,
|
||||
AccessDenied = true,
|
||||
Path = path,
|
||||
Message =
|
||||
"Kein Zugriff auf den Pfad – Admin-Konto nötig."
|
||||
};
|
||||
}
|
||||
|
||||
public static CertificateProbeResult PasswordRequired(string path)
|
||||
{
|
||||
return new CertificateProbeResult
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed class DeploymentTargetRepository
|
||||
container.[SonicContainerId],
|
||||
container.[ContainerName],
|
||||
container.[ContainerDisplayName],
|
||||
container.[CertificateCheckUrl],
|
||||
container.[RestartTimeoutSeconds],
|
||||
|
||||
connection.[ConnectionName],
|
||||
@@ -119,6 +120,9 @@ public sealed class DeploymentTargetRepository
|
||||
int containerDisplayNameOrdinal =
|
||||
reader.GetOrdinal("ContainerDisplayName");
|
||||
|
||||
int certificateCheckUrlOrdinal =
|
||||
reader.GetOrdinal("CertificateCheckUrl");
|
||||
|
||||
int restartTimeoutOrdinal =
|
||||
reader.GetOrdinal("RestartTimeoutSeconds");
|
||||
|
||||
@@ -144,6 +148,11 @@ public sealed class DeploymentTargetRepository
|
||||
reader,
|
||||
containerDisplayNameOrdinal);
|
||||
|
||||
string? certificateCheckUrl =
|
||||
ReadNullableString(
|
||||
reader,
|
||||
certificateCheckUrlOrdinal);
|
||||
|
||||
string companyCode =
|
||||
reader.GetString(companyCodeOrdinal);
|
||||
|
||||
@@ -152,6 +161,22 @@ public sealed class DeploymentTargetRepository
|
||||
? containerName
|
||||
: containerDisplayName;
|
||||
|
||||
string tlsHost = string.Empty;
|
||||
int? tlsPort = null;
|
||||
string tlsServerName = string.Empty;
|
||||
|
||||
if (TlsEndpointProbeService.TryParseEndpoint(
|
||||
certificateCheckUrl,
|
||||
out string host,
|
||||
out int port,
|
||||
out string serverName,
|
||||
out _))
|
||||
{
|
||||
tlsHost = host;
|
||||
tlsPort = port;
|
||||
tlsServerName = serverName;
|
||||
}
|
||||
|
||||
targets.Add(new DeploymentTarget
|
||||
{
|
||||
Id = reader.GetInt32(targetIdOrdinal),
|
||||
@@ -188,11 +213,11 @@ public sealed class DeploymentTargetRepository
|
||||
RestartTimeoutSeconds =
|
||||
reader.GetInt32(restartTimeoutOrdinal),
|
||||
|
||||
TlsHost = string.Empty,
|
||||
TlsHost = tlsHost,
|
||||
|
||||
TlsPort = null,
|
||||
TlsPort = tlsPort,
|
||||
|
||||
TlsServerName = string.Empty,
|
||||
TlsServerName = tlsServerName,
|
||||
|
||||
ExpectedFingerprint = null,
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Erkennt typische Sonic-/MfApi-Fehlermeldungen und liefert
|
||||
/// verständliche Hinweise für die UI.
|
||||
/// </summary>
|
||||
public static class SonicErrorClassifier
|
||||
{
|
||||
private static readonly string[] PermissionMarkers =
|
||||
[
|
||||
"ManagePermissionDenied",
|
||||
"ConfigurePermissionDenied",
|
||||
"ManagementPermissionDenied",
|
||||
"PermissionDenied",
|
||||
"MFSecurityException",
|
||||
"SecurityException",
|
||||
"Access is denied",
|
||||
"Access denied",
|
||||
"not authorized",
|
||||
"nicht autorisiert",
|
||||
"permission denied",
|
||||
"keine Berechtigung",
|
||||
"Insufficient privileges",
|
||||
"Unauthorized"
|
||||
];
|
||||
|
||||
public static bool LooksLikeMissingPermission(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string marker in PermissionMarkers)
|
||||
{
|
||||
if (text.Contains(marker, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string PermissionDeniedUserMessage =>
|
||||
"Keine Rechte für den Neustart.\n\n"
|
||||
+ "Der hinterlegte Sonic-Benutzer darf diesen Container "
|
||||
+ "nicht neu starten (Manage-/Restart-Recht fehlt).\n\n"
|
||||
+ "Bitte in der Sonic Management Console die Rechte prüfen "
|
||||
+ "oder ein Profil mit ausreichenden Rechten verwenden.";
|
||||
|
||||
public static string FormatRestartFailure(
|
||||
string? status,
|
||||
string? detail)
|
||||
{
|
||||
string combined = string.Join(
|
||||
Environment.NewLine,
|
||||
new[] { status, detail }
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||
|
||||
if (LooksLikeMissingPermission(combined))
|
||||
{
|
||||
return PermissionDeniedUserMessage
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ "--- Technische Details ---"
|
||||
+ Environment.NewLine
|
||||
+ combined;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(combined)
|
||||
? "Neustart fehlgeschlagen (keine Details)."
|
||||
: combined;
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,21 @@ public sealed class SonicManagementClient : IDisposable
|
||||
$"Domain={_connection.DomainName}; URL={_connection.ConnectionUrl}\n{output}");
|
||||
}
|
||||
|
||||
return (false, "Neustart fehlgeschlagen (MfApi)",
|
||||
$"Container '{containerName}', Domain '{_connection.DomainName}', URL {_connection.ConnectionUrl}\n" +
|
||||
$"Fehler: {error}\n\n{output}");
|
||||
string technical =
|
||||
$"Container '{containerName}', Domain '{_connection.DomainName}', URL {_connection.ConnectionUrl}\n"
|
||||
+ $"Fehler: {error}\n\n{output}";
|
||||
|
||||
if (SonicErrorClassifier.LooksLikeMissingPermission(technical))
|
||||
{
|
||||
return (
|
||||
false,
|
||||
"Keine Rechte für den Neustart",
|
||||
SonicErrorClassifier.FormatRestartFailure(
|
||||
"Keine Rechte für den Neustart",
|
||||
technical));
|
||||
}
|
||||
|
||||
return (false, "Neustart fehlgeschlagen (MfApi)", technical);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -380,6 +380,228 @@ public sealed class SonicSetupRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateCredentialAsync(
|
||||
int sonicCredentialId,
|
||||
int sonicConnectionId,
|
||||
string credentialName,
|
||||
string userName,
|
||||
string secret,
|
||||
bool isDefault,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicCredentialId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicCredentialId));
|
||||
}
|
||||
|
||||
if (sonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicConnectionId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credentialName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Profilname fehlt.",
|
||||
nameof(credentialName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Sonic-Benutzername fehlt.",
|
||||
nameof(userName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das Sonic-Kennwort fehlt.",
|
||||
nameof(secret));
|
||||
}
|
||||
|
||||
string normalizedCredentialName =
|
||||
credentialName.Trim();
|
||||
|
||||
string normalizedUserName =
|
||||
userName.Trim();
|
||||
|
||||
if (normalizedCredentialName.Length > 150)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Profilname darf maximal 150 Zeichen enthalten.",
|
||||
nameof(credentialName));
|
||||
}
|
||||
|
||||
if (normalizedUserName.Length > 256)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Benutzername darf maximal 256 Zeichen enthalten.",
|
||||
nameof(userName));
|
||||
}
|
||||
|
||||
if (secret.Length > 512)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das Kennwort darf maximal 512 Zeichen enthalten.",
|
||||
nameof(secret));
|
||||
}
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlTransaction transaction =
|
||||
(SqlTransaction)
|
||||
await connection.BeginTransactionAsync(
|
||||
cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (isDefault)
|
||||
{
|
||||
const string clearDefaultSql = """
|
||||
UPDATE [dbo].[SonicCredential]
|
||||
SET
|
||||
[IsDefault] = 0,
|
||||
[ModifiedDateTime] = SYSUTCDATETIME(),
|
||||
[ModifiedBy] = SUSER_SNAME()
|
||||
WHERE [SonicConnectionId] =
|
||||
@SonicConnectionId
|
||||
AND [IsDefault] = 1
|
||||
AND [SonicCredentialId] <>
|
||||
@SonicCredentialId;
|
||||
""";
|
||||
|
||||
await using SqlCommand clearDefaultCommand =
|
||||
new(
|
||||
clearDefaultSql,
|
||||
connection,
|
||||
transaction);
|
||||
|
||||
clearDefaultCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
clearDefaultCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicCredentialId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicCredentialId
|
||||
});
|
||||
|
||||
await clearDefaultCommand.ExecuteNonQueryAsync(
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
const string updateSql = """
|
||||
UPDATE [dbo].[SonicCredential]
|
||||
SET
|
||||
[CredentialName] = @CredentialName,
|
||||
[CredentialUserName] = @CredentialUserName,
|
||||
[CredentialSecret] = @CredentialSecret,
|
||||
[IsDefault] = @IsDefault,
|
||||
[ModifiedDateTime] = SYSUTCDATETIME(),
|
||||
[ModifiedBy] = SUSER_SNAME()
|
||||
WHERE [SonicCredentialId] = @SonicCredentialId
|
||||
AND [SonicConnectionId] = @SonicConnectionId
|
||||
AND [IsActive] = 1;
|
||||
""";
|
||||
|
||||
await using SqlCommand updateCommand =
|
||||
new(
|
||||
updateSql,
|
||||
connection,
|
||||
transaction);
|
||||
|
||||
updateCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicCredentialId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicCredentialId
|
||||
});
|
||||
|
||||
updateCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
updateCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialName",
|
||||
SqlDbType.NVarChar,
|
||||
150)
|
||||
{
|
||||
Value = normalizedCredentialName
|
||||
});
|
||||
|
||||
updateCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialUserName",
|
||||
SqlDbType.NVarChar,
|
||||
256)
|
||||
{
|
||||
Value = normalizedUserName
|
||||
});
|
||||
|
||||
updateCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CredentialSecret",
|
||||
SqlDbType.NVarChar,
|
||||
512)
|
||||
{
|
||||
Value = secret
|
||||
});
|
||||
|
||||
updateCommand.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@IsDefault",
|
||||
SqlDbType.Bit)
|
||||
{
|
||||
Value = isDefault
|
||||
});
|
||||
|
||||
int affected =
|
||||
await updateCommand.ExecuteNonQueryAsync(
|
||||
cancellationToken);
|
||||
|
||||
if (affected == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Benutzerprofil Id {sonicCredentialId} "
|
||||
+ "wurde nicht gefunden oder ist inaktiv.");
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
await transaction.RollbackAsync(
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Die ursprüngliche Exception soll erhalten bleiben.
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SonicCredentialProfile>>
|
||||
GetCredentialsAsync(
|
||||
int sonicConnectionId,
|
||||
@@ -663,6 +885,7 @@ public sealed class SonicSetupRepository
|
||||
container.[CompanyId],
|
||||
container.[ContainerName],
|
||||
container.[ContainerDisplayName],
|
||||
container.[CertificateCheckUrl],
|
||||
container.[RestartTimeoutSeconds],
|
||||
company.[CompanyCode],
|
||||
connection.[ConnectionName]
|
||||
@@ -729,6 +952,9 @@ public sealed class SonicSetupRepository
|
||||
int displayOrdinal =
|
||||
reader.GetOrdinal("ContainerDisplayName");
|
||||
|
||||
int checkUrlOrdinal =
|
||||
reader.GetOrdinal("CertificateCheckUrl");
|
||||
|
||||
containers.Add(
|
||||
new ContainerOption
|
||||
{
|
||||
@@ -753,6 +979,11 @@ public sealed class SonicSetupRepository
|
||||
? null
|
||||
: reader.GetString(displayOrdinal),
|
||||
|
||||
CertificateCheckUrl =
|
||||
reader.IsDBNull(checkUrlOrdinal)
|
||||
? null
|
||||
: reader.GetString(checkUrlOrdinal),
|
||||
|
||||
RestartTimeoutSeconds =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("RestartTimeoutSeconds")),
|
||||
@@ -1057,6 +1288,7 @@ public sealed class SonicSetupRepository
|
||||
string containerName,
|
||||
string? containerDisplayName,
|
||||
int restartTimeoutSeconds,
|
||||
string? certificateCheckUrl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (companyId <= 0)
|
||||
@@ -1093,6 +1325,9 @@ public sealed class SonicSetupRepository
|
||||
"Timeout muss zwischen 10 und 3600 Sekunden liegen.");
|
||||
}
|
||||
|
||||
string? checkUrl =
|
||||
TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO [dbo].[SonicContainer]
|
||||
(
|
||||
@@ -1100,6 +1335,7 @@ public sealed class SonicSetupRepository
|
||||
[SonicConnectionId],
|
||||
[ContainerName],
|
||||
[ContainerDisplayName],
|
||||
[CertificateCheckUrl],
|
||||
[RestartTimeoutSeconds],
|
||||
[IsActive],
|
||||
[CreationDateTime],
|
||||
@@ -1112,6 +1348,7 @@ public sealed class SonicSetupRepository
|
||||
@SonicConnectionId,
|
||||
@ContainerName,
|
||||
@ContainerDisplayName,
|
||||
@CertificateCheckUrl,
|
||||
@RestartTimeoutSeconds,
|
||||
1,
|
||||
SYSUTCDATETIME(),
|
||||
@@ -1159,6 +1396,17 @@ public sealed class SonicSetupRepository
|
||||
: display
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CertificateCheckUrl",
|
||||
SqlDbType.NVarChar,
|
||||
500)
|
||||
{
|
||||
Value = checkUrl is null
|
||||
? DBNull.Value
|
||||
: checkUrl
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@RestartTimeoutSeconds",
|
||||
@@ -1457,6 +1705,7 @@ public sealed class SonicSetupRepository
|
||||
string containerName,
|
||||
string? containerDisplayName,
|
||||
int restartTimeoutSeconds,
|
||||
string? certificateCheckUrl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicContainerId <= 0)
|
||||
@@ -1499,6 +1748,9 @@ public sealed class SonicSetupRepository
|
||||
"Timeout muss zwischen 10 und 3600 Sekunden liegen.");
|
||||
}
|
||||
|
||||
string? checkUrl =
|
||||
TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl);
|
||||
|
||||
const string sql = """
|
||||
UPDATE [dbo].[SonicContainer]
|
||||
SET
|
||||
@@ -1506,6 +1758,7 @@ public sealed class SonicSetupRepository
|
||||
[SonicConnectionId] = @SonicConnectionId,
|
||||
[ContainerName] = @ContainerName,
|
||||
[ContainerDisplayName] = @ContainerDisplayName,
|
||||
[CertificateCheckUrl] = @CertificateCheckUrl,
|
||||
[RestartTimeoutSeconds] = @RestartTimeoutSeconds,
|
||||
[ModifiedDateTime] = SYSUTCDATETIME(),
|
||||
[ModifiedBy] = SUSER_SNAME()
|
||||
@@ -1559,6 +1812,17 @@ public sealed class SonicSetupRepository
|
||||
: display
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@CertificateCheckUrl",
|
||||
SqlDbType.NVarChar,
|
||||
500)
|
||||
{
|
||||
Value = checkUrl is null
|
||||
? DBNull.Value
|
||||
: checkUrl
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@RestartTimeoutSeconds",
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.RegularExpressions;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Liest das präsentierte TLS-Zertifikat eines Endpoints
|
||||
/// (entspricht grob <c>curl -v https://host:port</c>).
|
||||
/// </summary>
|
||||
public sealed class TlsEndpointProbeService
|
||||
{
|
||||
private static readonly Regex HostPortRegex = new(
|
||||
@"^(?<host>[^:/]+)(:(?<port>\d{1,5}))?$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public CertificateProbeResult Probe(
|
||||
string? checkUrl,
|
||||
int timeoutMilliseconds = 8000)
|
||||
{
|
||||
if (!TryParseEndpoint(
|
||||
checkUrl,
|
||||
out string host,
|
||||
out int port,
|
||||
out string serverName,
|
||||
out string? parseError))
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
parseError ?? "Ungültige Prüf-URL.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource cts = new(timeoutMilliseconds);
|
||||
using TcpClient client = new();
|
||||
|
||||
using (cts.Token.Register(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Timeout beendet die Verbindung.
|
||||
}
|
||||
}))
|
||||
{
|
||||
client.Connect(host, port);
|
||||
}
|
||||
|
||||
using SslStream ssl = new(
|
||||
client.GetStream(),
|
||||
leaveInnerStreamOpen: false,
|
||||
userCertificateValidationCallback:
|
||||
static (_, _, _, _) => true);
|
||||
|
||||
ssl.AuthenticateAsClient(serverName);
|
||||
|
||||
if (ssl.RemoteCertificate is null)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
$"Kein Zertifikat von {host}:{port} erhalten.");
|
||||
}
|
||||
|
||||
using X509Certificate2 certificate = new(ssl.RemoteCertificate);
|
||||
CertificateInfo info = ToCertificateInfo(certificate);
|
||||
|
||||
return CertificateProbeResult.Found(
|
||||
$"{host}:{port}",
|
||||
info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
$"TLS-Prüfung {host}:{port} fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseEndpoint(
|
||||
string? value,
|
||||
out string host,
|
||||
out int port,
|
||||
out string serverName,
|
||||
out string? error)
|
||||
{
|
||||
host = string.Empty;
|
||||
port = 443;
|
||||
serverName = string.Empty;
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
error = "Keine Prüf-URL hinterlegt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string trimmed = value.Trim();
|
||||
|
||||
if (Uri.TryCreate(trimmed, UriKind.Absolute, out Uri? uri)
|
||||
&& (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
|
||||
|| uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
|
||||
|| uri.Scheme.Equals("ssl", StringComparison.OrdinalIgnoreCase)
|
||||
|| uri.Scheme.Equals("tcp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri.Host))
|
||||
{
|
||||
error = "Host in der Prüf-URL fehlt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = uri.Host;
|
||||
serverName = uri.Host;
|
||||
port = uri.IsDefaultPort
|
||||
? (uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
|
||||
? 80
|
||||
: 443)
|
||||
: uri.Port;
|
||||
|
||||
if (port is < 1 or > 65535)
|
||||
{
|
||||
error = "Port muss zwischen 1 und 65535 liegen.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Match match = HostPortRegex.Match(trimmed);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
error =
|
||||
"Prüf-URL ungültig. Beispiele:\n"
|
||||
+ "https://server:8443\n"
|
||||
+ "server:8443\n"
|
||||
+ "server";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = match.Groups["host"].Value;
|
||||
serverName = host;
|
||||
|
||||
if (match.Groups["port"].Success)
|
||||
{
|
||||
if (!int.TryParse(match.Groups["port"].Value, out port)
|
||||
|| port is < 1 or > 65535)
|
||||
{
|
||||
error = "Port muss zwischen 1 und 65535 liegen.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(host);
|
||||
}
|
||||
|
||||
public static string? NormalizeCheckUrl(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string trimmed = value.Trim();
|
||||
|
||||
if (!TryParseEndpoint(
|
||||
trimmed,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out string? error))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
error ?? "Ungültige Prüf-URL.",
|
||||
nameof(value));
|
||||
}
|
||||
|
||||
if (trimmed.Length > 500)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Die Prüf-URL darf maximal 500 Zeichen enthalten.",
|
||||
nameof(value));
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private static CertificateInfo ToCertificateInfo(
|
||||
X509Certificate2 certificate)
|
||||
{
|
||||
string subject = certificate.GetNameInfo(
|
||||
X509NameType.SimpleName,
|
||||
forIssuer: false);
|
||||
|
||||
string issuer = certificate.GetNameInfo(
|
||||
X509NameType.SimpleName,
|
||||
forIssuer: true);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
{
|
||||
subject = certificate.Subject;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(issuer))
|
||||
{
|
||||
issuer = certificate.Issuer;
|
||||
}
|
||||
|
||||
string fingerprint = certificate.GetCertHashString(
|
||||
HashAlgorithmName.SHA256);
|
||||
|
||||
fingerprint = string.Join(
|
||||
":",
|
||||
Enumerable.Range(0, fingerprint.Length / 2)
|
||||
.Select(index => fingerprint.Substring(index * 2, 2)));
|
||||
|
||||
DateTimeOffset validFrom = new(certificate.NotBefore);
|
||||
DateTimeOffset validUntil = new(certificate.NotAfter);
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
|
||||
return new CertificateInfo
|
||||
{
|
||||
Subject = subject,
|
||||
Issuer = issuer,
|
||||
FingerprintSha256 = fingerprint,
|
||||
ValidFrom = validFrom,
|
||||
ValidUntil = validUntil,
|
||||
IsCurrentlyValid = now >= validFrom && now <= validUntil
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
using ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Setup;
|
||||
|
||||
@@ -11,8 +12,10 @@ public sealed class AddContainerForm : Form
|
||||
private readonly ComboBox _cmbSystem;
|
||||
private readonly TextBox _txtName;
|
||||
private readonly TextBox _txtDisplay;
|
||||
private readonly TextBox _txtCheckUrl;
|
||||
private readonly TextBox _txtTimeout;
|
||||
private readonly Button _btnSave;
|
||||
private readonly Button _btnProbeUrl;
|
||||
private bool _running;
|
||||
|
||||
public int? CreatedSonicContainerId { get; private set; }
|
||||
@@ -36,7 +39,7 @@ public sealed class AddContainerForm : Form
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
ShowInTaskbar = false;
|
||||
ClientSize = new Size(540, 470);
|
||||
ClientSize = new Size(540, 560);
|
||||
BackColor = SettingsUi.Background;
|
||||
ForeColor = SettingsUi.Text;
|
||||
Font = new Font("Segoe UI", 10f);
|
||||
@@ -55,7 +58,7 @@ public sealed class AddContainerForm : Form
|
||||
Label subtitle = new()
|
||||
{
|
||||
Text =
|
||||
"Container einer Company und einem Sonic-System zuordnen.",
|
||||
"Container zuordnen und optional TLS-Prüf-URL hinterlegen.",
|
||||
Location = new Point(30, 58),
|
||||
AutoSize = true,
|
||||
ForeColor = SettingsUi.Muted
|
||||
@@ -63,13 +66,14 @@ public sealed class AddContainerForm : Form
|
||||
|
||||
Panel card = SettingsUi.CreateCard();
|
||||
card.Location = new Point(28, 95);
|
||||
card.Size = new Size(484, 290);
|
||||
card.Size = new Size(484, 370);
|
||||
card.Paint += SettingsUi.PaintCardBorder;
|
||||
|
||||
_cmbCompany = SettingsUi.CreateComboBox();
|
||||
_cmbSystem = SettingsUi.CreateComboBox();
|
||||
_txtName = SettingsUi.CreateTextBox();
|
||||
_txtDisplay = SettingsUi.CreateTextBox();
|
||||
_txtCheckUrl = SettingsUi.CreateTextBox();
|
||||
_txtTimeout = SettingsUi.CreateTextBox();
|
||||
_txtTimeout.Text = "180";
|
||||
|
||||
@@ -77,6 +81,7 @@ public sealed class AddContainerForm : Form
|
||||
{
|
||||
_txtName.Text = _existing.ContainerName;
|
||||
_txtDisplay.Text = _existing.ContainerDisplayName ?? string.Empty;
|
||||
_txtCheckUrl.Text = _existing.CertificateCheckUrl ?? string.Empty;
|
||||
_txtTimeout.Text = _existing.RestartTimeoutSeconds.ToString();
|
||||
}
|
||||
|
||||
@@ -85,15 +90,39 @@ public sealed class AddContainerForm : Form
|
||||
AddField(card, "Container-Name", _txtName, 18, 138, 440);
|
||||
AddField(card, "Anzeigename (optional)", _txtDisplay, 18, 198, 280);
|
||||
AddField(card, "Timeout (s)", _txtTimeout, 318, 198, 140);
|
||||
AddField(
|
||||
card,
|
||||
"Zertifikat prüfen (URL / Host:Port)",
|
||||
_txtCheckUrl,
|
||||
18,
|
||||
258,
|
||||
440);
|
||||
|
||||
Label urlHint = new()
|
||||
{
|
||||
Text = "z. B. https://server:8443 oder server:8443",
|
||||
Location = new Point(18, 312),
|
||||
AutoSize = true,
|
||||
ForeColor = SettingsUi.Muted,
|
||||
Font = new Font("Segoe UI", 8.5f)
|
||||
};
|
||||
|
||||
_btnProbeUrl = SettingsUi.CreateGhostButton("URL prüfen");
|
||||
_btnProbeUrl.Location = new Point(318, 318);
|
||||
_btnProbeUrl.Size = new Size(140, 30);
|
||||
_btnProbeUrl.Click += (_, _) => ProbeUrl();
|
||||
|
||||
card.Controls.Add(urlHint);
|
||||
card.Controls.Add(_btnProbeUrl);
|
||||
|
||||
_btnSave = SettingsUi.CreatePrimaryButton(
|
||||
IsEditMode ? "Änderungen speichern" : "Container anlegen");
|
||||
_btnSave.Location = new Point(292, 405);
|
||||
_btnSave.Location = new Point(292, 490);
|
||||
_btnSave.Size = new Size(220, 40);
|
||||
_btnSave.Click += async (_, _) => await SaveAsync();
|
||||
|
||||
Button cancel = SettingsUi.CreateGhostButton("Abbrechen");
|
||||
cancel.Location = new Point(170, 405);
|
||||
cancel.Location = new Point(170, 490);
|
||||
cancel.Size = new Size(110, 40);
|
||||
cancel.Click += (_, _) =>
|
||||
{
|
||||
@@ -110,6 +139,53 @@ public sealed class AddContainerForm : Form
|
||||
Shown += async (_, _) => await LoadLookupsAsync();
|
||||
}
|
||||
|
||||
private void ProbeUrl()
|
||||
{
|
||||
string url = _txtCheckUrl.Text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Bitte zuerst eine Prüf-URL eintragen.",
|
||||
"URL prüfen",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
TlsEndpointProbeService probe = new();
|
||||
CertificateProbeResult result = probe.Probe(url);
|
||||
|
||||
if (result.Success && result.Certificate is not null)
|
||||
{
|
||||
string expiry = result.Certificate.ValidUntil
|
||||
.ToLocalTime()
|
||||
.ToString("dd.MM.yyyy HH:mm");
|
||||
|
||||
MessageBox.Show(
|
||||
this,
|
||||
$"TLS-Zertifikat erreichbar.\n\n"
|
||||
+ $"Subject: {result.Certificate.Subject}\n"
|
||||
+ $"Aussteller: {result.Certificate.Issuer}\n"
|
||||
+ $"Gültig bis: {expiry}\n"
|
||||
+ $"Status: {(result.Certificate.IsCurrentlyValid ? "gültig" : "abgelaufen")}",
|
||||
"URL prüfen",
|
||||
MessageBoxButtons.OK,
|
||||
result.Certificate.IsCurrentlyValid
|
||||
? MessageBoxIcon.Information
|
||||
: MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBox.Show(
|
||||
this,
|
||||
result.Message,
|
||||
"URL prüfen",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
private async Task LoadLookupsAsync()
|
||||
{
|
||||
try
|
||||
@@ -211,6 +287,29 @@ public sealed class AddContainerForm : Form
|
||||
return;
|
||||
}
|
||||
|
||||
string? checkUrl = _txtCheckUrl.Text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(checkUrl))
|
||||
{
|
||||
checkUrl = null;
|
||||
}
|
||||
else if (!TlsEndpointProbeService.TryParseEndpoint(
|
||||
checkUrl,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out string? parseError))
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
parseError ?? "Ungültige Prüf-URL.",
|
||||
"Container",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
_txtCheckUrl.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
_running = true;
|
||||
_btnSave.Enabled = false;
|
||||
Cursor = Cursors.WaitCursor;
|
||||
@@ -225,7 +324,8 @@ public sealed class AddContainerForm : Form
|
||||
system.SonicConnectionId,
|
||||
_txtName.Text,
|
||||
_txtDisplay.Text,
|
||||
timeout);
|
||||
timeout,
|
||||
checkUrl);
|
||||
|
||||
CreatedSonicContainerId = _existing.SonicContainerId;
|
||||
}
|
||||
@@ -237,7 +337,8 @@ public sealed class AddContainerForm : Form
|
||||
system.SonicConnectionId,
|
||||
_txtName.Text,
|
||||
_txtDisplay.Text,
|
||||
timeout);
|
||||
timeout,
|
||||
checkUrl);
|
||||
}
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
|
||||
@@ -8,6 +8,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
{
|
||||
private readonly SetupCoordinator _coordinator;
|
||||
private readonly SonicSystemOption _system;
|
||||
private readonly SonicCredentialProfile? _existing;
|
||||
|
||||
private readonly TextBox _txtProfileName;
|
||||
private readonly TextBox _txtUserName;
|
||||
@@ -16,6 +17,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
private readonly CheckBox _chkDefault;
|
||||
private readonly Button _btnSave;
|
||||
private readonly Button _btnCancel;
|
||||
private readonly Label _lblPasswordHint;
|
||||
|
||||
private bool _operationRunning;
|
||||
|
||||
@@ -25,9 +27,12 @@ public sealed class AddCredentialProfileForm : Form
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool IsEditMode => _existing is not null;
|
||||
|
||||
public AddCredentialProfileForm(
|
||||
SetupCoordinator coordinator,
|
||||
SonicSystemOption system)
|
||||
SonicSystemOption system,
|
||||
SonicCredentialProfile? existing = null)
|
||||
{
|
||||
_coordinator =
|
||||
coordinator
|
||||
@@ -39,13 +44,17 @@ public sealed class AddCredentialProfileForm : Form
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(system));
|
||||
|
||||
Text = "Benutzerprofil hinzufügen";
|
||||
_existing = existing;
|
||||
|
||||
Text = IsEditMode
|
||||
? "Benutzerprofil bearbeiten"
|
||||
: "Benutzerprofil hinzufügen";
|
||||
|
||||
StartPosition =
|
||||
FormStartPosition.CenterParent;
|
||||
|
||||
ClientSize =
|
||||
new Size(560, 525);
|
||||
new Size(560, 545);
|
||||
|
||||
FormBorderStyle =
|
||||
FormBorderStyle.FixedDialog;
|
||||
@@ -65,8 +74,9 @@ public sealed class AddCredentialProfileForm : Form
|
||||
|
||||
Label title = new()
|
||||
{
|
||||
Text =
|
||||
$"Profil für {_system.ConnectionName}",
|
||||
Text = IsEditMode
|
||||
? $"Profil bearbeiten · {_system.ConnectionName}"
|
||||
: $"Profil für {_system.ConnectionName}",
|
||||
|
||||
Location =
|
||||
new Point(28, 22),
|
||||
@@ -112,7 +122,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
CreateTextBox(185);
|
||||
|
||||
_txtProfileName.Text =
|
||||
"Administrator";
|
||||
_existing?.CredentialName ?? "Administrator";
|
||||
|
||||
Label userLabel =
|
||||
CreateLabel(
|
||||
@@ -123,7 +133,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
CreateTextBox(250);
|
||||
|
||||
_txtUserName.Text =
|
||||
"Administrator";
|
||||
_existing?.UserName ?? "Administrator";
|
||||
|
||||
Label passwordLabel =
|
||||
CreateLabel(
|
||||
@@ -136,13 +146,25 @@ public sealed class AddCredentialProfileForm : Form
|
||||
_txtPassword.UseSystemPasswordChar =
|
||||
true;
|
||||
|
||||
_lblPasswordHint = new Label
|
||||
{
|
||||
Text = IsEditMode
|
||||
? "Leer lassen = bisheriges Kennwort behalten"
|
||||
: string.Empty,
|
||||
|
||||
Location = new Point(30, 343),
|
||||
AutoSize = true,
|
||||
ForeColor = Color.FromArgb(169, 184, 200),
|
||||
Font = new Font("Segoe UI", 8.5f)
|
||||
};
|
||||
|
||||
Label repeatLabel =
|
||||
CreateLabel(
|
||||
"Kennwort wiederholen",
|
||||
355);
|
||||
365);
|
||||
|
||||
_txtPasswordRepeat =
|
||||
CreateTextBox(380);
|
||||
CreateTextBox(390);
|
||||
|
||||
_txtPasswordRepeat.UseSystemPasswordChar =
|
||||
true;
|
||||
@@ -153,10 +175,10 @@ public sealed class AddCredentialProfileForm : Form
|
||||
"Als Standardprofil verwenden",
|
||||
|
||||
Location =
|
||||
new Point(30, 425),
|
||||
new Point(30, 435),
|
||||
|
||||
AutoSize = true,
|
||||
Checked = true,
|
||||
Checked = _existing?.IsDefault ?? true,
|
||||
|
||||
ForeColor =
|
||||
Color.FromArgb(241, 245, 249)
|
||||
@@ -167,7 +189,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
Text = "Abbrechen",
|
||||
|
||||
Location =
|
||||
new Point(125, 465),
|
||||
new Point(125, 480),
|
||||
|
||||
Size =
|
||||
new Size(110, 38),
|
||||
@@ -189,11 +211,12 @@ public sealed class AddCredentialProfileForm : Form
|
||||
|
||||
_btnSave = new Button
|
||||
{
|
||||
Text =
|
||||
"Verbindung prüfen und hinzufügen",
|
||||
Text = IsEditMode
|
||||
? "Prüfen und speichern"
|
||||
: "Verbindung prüfen und hinzufügen",
|
||||
|
||||
Location =
|
||||
new Point(245, 465),
|
||||
new Point(245, 480),
|
||||
|
||||
Size =
|
||||
new Size(280, 38),
|
||||
@@ -228,6 +251,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
Controls.Add(_txtUserName);
|
||||
Controls.Add(passwordLabel);
|
||||
Controls.Add(_txtPassword);
|
||||
Controls.Add(_lblPasswordHint);
|
||||
Controls.Add(repeatLabel);
|
||||
Controls.Add(_txtPasswordRepeat);
|
||||
Controls.Add(_chkDefault);
|
||||
@@ -328,7 +352,7 @@ public sealed class AddCredentialProfileForm : Form
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
if (!IsEditMode && string.IsNullOrEmpty(password))
|
||||
{
|
||||
ShowWarning("Kennwort fehlt.");
|
||||
|
||||
@@ -344,7 +368,8 @@ public sealed class AddCredentialProfileForm : Form
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(
|
||||
if (!string.IsNullOrEmpty(password)
|
||||
&& !string.Equals(
|
||||
password,
|
||||
passwordRepeat,
|
||||
StringComparison.Ordinal))
|
||||
@@ -372,13 +397,27 @@ public sealed class AddCredentialProfileForm : Form
|
||||
|
||||
try
|
||||
{
|
||||
CreatedProfile =
|
||||
await _coordinator.AddCredentialAsync(
|
||||
_system,
|
||||
profileName,
|
||||
userName,
|
||||
password,
|
||||
_chkDefault.Checked);
|
||||
if (IsEditMode && _existing is not null)
|
||||
{
|
||||
CreatedProfile =
|
||||
await _coordinator.UpdateCredentialAsync(
|
||||
_system,
|
||||
_existing,
|
||||
profileName,
|
||||
userName,
|
||||
password,
|
||||
_chkDefault.Checked);
|
||||
}
|
||||
else
|
||||
{
|
||||
CreatedProfile =
|
||||
await _coordinator.TestAndAddCredentialAsync(
|
||||
_system,
|
||||
profileName,
|
||||
userName,
|
||||
password,
|
||||
_chkDefault.Checked);
|
||||
}
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
@@ -391,7 +430,11 @@ public sealed class AddCredentialProfileForm : Form
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Profil konnte nicht angelegt werden.",
|
||||
string.IsNullOrWhiteSpace(exception.Message)
|
||||
? (IsEditMode
|
||||
? "Profil konnte nicht aktualisiert werden."
|
||||
: "Profil konnte nicht angelegt werden.")
|
||||
: exception.Message,
|
||||
"Benutzerprofil",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -423,7 +466,9 @@ public sealed class AddCredentialProfileForm : Form
|
||||
_btnSave.Text =
|
||||
isRunning
|
||||
? "Verbindung wird geprüft ..."
|
||||
: "Verbindung prüfen und hinzufügen";
|
||||
: IsEditMode
|
||||
? "Prüfen und speichern"
|
||||
: "Verbindung prüfen und hinzufügen";
|
||||
}
|
||||
|
||||
private void ShowWarning(string message)
|
||||
@@ -435,4 +480,4 @@ public sealed class AddCredentialProfileForm : Form
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,10 @@ public sealed class AddTargetPathForm : Form
|
||||
if (!_lastProbe.Success)
|
||||
{
|
||||
_lblProbeResult.ForeColor = Color.FromArgb(220, 100, 100);
|
||||
_lblProbeResult.Text = _lastProbe.Message;
|
||||
_lblProbeResult.Text = _lastProbe.AccessDenied
|
||||
? CertificateProbeResult.AccessDeniedShortText
|
||||
+ " – Anmeldung am Freigabe-Pfad erforderlich."
|
||||
: _lastProbe.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,78 @@ public sealed class SetupCoordinator
|
||||
|
||||
return createdProfile;
|
||||
}
|
||||
|
||||
public async Task<SonicCredentialProfile> UpdateCredentialAsync(
|
||||
SonicSystemOption system,
|
||||
SonicCredentialProfile existing,
|
||||
string credentialName,
|
||||
string userName,
|
||||
string secret,
|
||||
bool isDefault,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(system);
|
||||
ArgumentNullException.ThrowIfNull(existing);
|
||||
|
||||
if (system.SonicConnectionId <= 0
|
||||
|| existing.SonicCredentialId <= 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Das ausgewählte Profil oder System ist ungültig.");
|
||||
}
|
||||
|
||||
string passwordToStore =
|
||||
string.IsNullOrEmpty(secret)
|
||||
? existing.Secret
|
||||
: secret;
|
||||
|
||||
CredentialTestResult testResult =
|
||||
await _credentialTester.TestAsync(
|
||||
system,
|
||||
userName,
|
||||
passwordToStore,
|
||||
system.ValidationContainerName,
|
||||
cancellationToken);
|
||||
|
||||
if (!testResult.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Benutzerprofil wurde nicht gespeichert."
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ testResult.Message);
|
||||
}
|
||||
|
||||
await _repository.UpdateCredentialAsync(
|
||||
existing.SonicCredentialId,
|
||||
system.SonicConnectionId,
|
||||
credentialName,
|
||||
userName,
|
||||
passwordToStore,
|
||||
isDefault,
|
||||
cancellationToken);
|
||||
|
||||
IReadOnlyList<SonicCredentialProfile> profiles =
|
||||
await _repository.GetCredentialsAsync(
|
||||
system.SonicConnectionId,
|
||||
cancellationToken);
|
||||
|
||||
SonicCredentialProfile? updated =
|
||||
profiles.FirstOrDefault(
|
||||
profile =>
|
||||
profile.SonicCredentialId
|
||||
== existing.SonicCredentialId);
|
||||
|
||||
if (updated is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Benutzerprofil wurde gespeichert, "
|
||||
+ "konnte anschließend aber nicht geladen werden.");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
public async Task<SetupCheckResult> CheckAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -396,6 +468,7 @@ public sealed class SetupCoordinator
|
||||
string containerName,
|
||||
string? containerDisplayName,
|
||||
int restartTimeoutSeconds,
|
||||
string? certificateCheckUrl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
int id = await _setupRepository.AddSonicContainerAsync(
|
||||
@@ -404,6 +477,7 @@ public sealed class SetupCoordinator
|
||||
containerName,
|
||||
containerDisplayName,
|
||||
restartTimeoutSeconds,
|
||||
certificateCheckUrl,
|
||||
cancellationToken);
|
||||
|
||||
await EnsureExistsAsync(
|
||||
@@ -520,6 +594,7 @@ public sealed class SetupCoordinator
|
||||
string containerName,
|
||||
string? containerDisplayName,
|
||||
int restartTimeoutSeconds,
|
||||
string? certificateCheckUrl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _setupRepository.UpdateSonicContainerAsync(
|
||||
@@ -529,6 +604,7 @@ public sealed class SetupCoordinator
|
||||
containerName,
|
||||
containerDisplayName,
|
||||
restartTimeoutSeconds,
|
||||
certificateCheckUrl,
|
||||
cancellationToken);
|
||||
|
||||
await EnsureExistsAsync(
|
||||
|
||||
@@ -36,6 +36,7 @@ public sealed class SetupWizardForm : Form
|
||||
private Button _btnSaveSelection = null!;
|
||||
private Button _btnFinish = null!;
|
||||
private Button _btnAddCredential = null!;
|
||||
private Button _btnEditCredential = null!;
|
||||
|
||||
private IReadOnlyList<SonicSystemOption> _systems = [];
|
||||
private IReadOnlyList<SonicCredentialProfile> _profiles = [];
|
||||
@@ -385,11 +386,12 @@ public sealed class SetupWizardForm : Form
|
||||
|
||||
_lvContainers = SettingsUi.CreateListView();
|
||||
_lvContainers.Dock = DockStyle.Fill;
|
||||
_lvContainers.Columns.Add("Company", 80);
|
||||
_lvContainers.Columns.Add("Container", 180);
|
||||
_lvContainers.Columns.Add("Anzeige", 160);
|
||||
_lvContainers.Columns.Add("System", 160);
|
||||
_lvContainers.Columns.Add("Timeout", 90);
|
||||
_lvContainers.Columns.Add("Company", 70);
|
||||
_lvContainers.Columns.Add("Container", 150);
|
||||
_lvContainers.Columns.Add("Anzeige", 120);
|
||||
_lvContainers.Columns.Add("System", 120);
|
||||
_lvContainers.Columns.Add("Prüf-URL", 180);
|
||||
_lvContainers.Columns.Add("Timeout", 80);
|
||||
_lvContainers.DoubleClick += async (_, _) => await EditSelectedContainerAsync();
|
||||
|
||||
FinishPanelLayout(_panelContainers, _lvContainers, toolbar);
|
||||
@@ -484,6 +486,12 @@ public sealed class SetupWizardForm : Form
|
||||
_btnAddCredential.Click += async (_, _) =>
|
||||
await AddCredentialAsync();
|
||||
|
||||
_btnEditCredential = SettingsUi.CreateGhostButton("Profil bearbeiten");
|
||||
_btnEditCredential.Location = new Point(550, 122);
|
||||
_btnEditCredential.Size = new Size(160, 34);
|
||||
_btnEditCredential.Click += async (_, _) =>
|
||||
await EditCredentialAsync();
|
||||
|
||||
_lblSystemDetails = new Label
|
||||
{
|
||||
Location = new Point(24, 180),
|
||||
@@ -502,6 +510,7 @@ public sealed class SetupWizardForm : Form
|
||||
card.Controls.Add(credentialLabel);
|
||||
card.Controls.Add(_cmbCredential);
|
||||
card.Controls.Add(_btnAddCredential);
|
||||
card.Controls.Add(_btnEditCredential);
|
||||
card.Controls.Add(_lblSystemDetails);
|
||||
card.Controls.Add(_btnSaveSelection);
|
||||
|
||||
@@ -625,6 +634,10 @@ public sealed class SetupWizardForm : Form
|
||||
item.SubItems.Add(container.ContainerName);
|
||||
item.SubItems.Add(container.ContainerDisplayName ?? "—");
|
||||
item.SubItems.Add(container.ConnectionName);
|
||||
item.SubItems.Add(
|
||||
string.IsNullOrWhiteSpace(container.CertificateCheckUrl)
|
||||
? "—"
|
||||
: container.CertificateCheckUrl);
|
||||
item.SubItems.Add(container.RestartTimeoutSeconds + " s");
|
||||
item.Tag = container;
|
||||
_lvContainers.Items.Add(item);
|
||||
@@ -722,7 +735,12 @@ public sealed class SetupWizardForm : Form
|
||||
return "Kennwort nötig";
|
||||
}
|
||||
|
||||
return result.Success ? "fehlt" : "n/a";
|
||||
if (result.AccessDenied)
|
||||
{
|
||||
return CertificateProbeResult.AccessDeniedShortText;
|
||||
}
|
||||
|
||||
return result.Success ? "fehlt" : "nicht erreichbar";
|
||||
}
|
||||
|
||||
private static string? PromptCertificatePassword(IWin32Window owner)
|
||||
@@ -1228,19 +1246,9 @@ public sealed class SetupWizardForm : Form
|
||||
}
|
||||
|
||||
await SystemChangedAsync();
|
||||
SelectCredential(dialog.CreatedProfile.SonicCredentialId);
|
||||
|
||||
for (int index = 0; index < _cmbCredential.Items.Count; index++)
|
||||
{
|
||||
if (_cmbCredential.Items[index] is SonicCredentialProfile profile
|
||||
&& profile.SonicCredentialId
|
||||
== dialog.CreatedProfile.SonicCredentialId)
|
||||
{
|
||||
_cmbCredential.SelectedIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox.Show(
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Profil geladen.",
|
||||
"Benutzerprofil",
|
||||
@@ -1248,6 +1256,53 @@ public sealed class SetupWizardForm : Form
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private async Task EditCredentialAsync()
|
||||
{
|
||||
if (_cmbSystem.SelectedItem is not SonicSystemOption system)
|
||||
{
|
||||
ShowWarning("Bitte System wählen.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cmbCredential.SelectedItem is not SonicCredentialProfile profile)
|
||||
{
|
||||
ShowWarning("Bitte Profil wählen.");
|
||||
return;
|
||||
}
|
||||
|
||||
using AddCredentialProfileForm dialog =
|
||||
new(_coordinator, system, profile);
|
||||
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK
|
||||
|| dialog.CreatedProfile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SystemChangedAsync();
|
||||
SelectCredential(dialog.CreatedProfile.SonicCredentialId);
|
||||
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Profil aktualisiert.",
|
||||
"Benutzerprofil",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private void SelectCredential(int sonicCredentialId)
|
||||
{
|
||||
for (int index = 0; index < _cmbCredential.Items.Count; index++)
|
||||
{
|
||||
if (_cmbCredential.Items[index] is SonicCredentialProfile profile
|
||||
&& profile.SonicCredentialId == sonicCredentialId)
|
||||
{
|
||||
_cmbCredential.SelectedIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SystemChangedAsync()
|
||||
{
|
||||
if (_cmbSystem.SelectedItem is not SonicSystemOption system)
|
||||
@@ -1275,9 +1330,12 @@ public sealed class SetupWizardForm : Form
|
||||
Environment.NewLine
|
||||
+ "Noch kein Benutzerprofil — bitte hinzufügen.";
|
||||
_btnSaveSelection.Enabled = false;
|
||||
_btnEditCredential.Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_btnEditCredential.Enabled = true;
|
||||
|
||||
_btnSaveSelection.Enabled = true;
|
||||
|
||||
LocalSetupSelection? selection = _coordinator.LoadSelection();
|
||||
|
||||
Reference in New Issue
Block a user