diff --git a/Dokumentation.md b/Dokumentation.md
index a78a0d9..ce16327 100644
--- a/Dokumentation.md
+++ b/Dokumentation.md
@@ -380,7 +380,9 @@ scripts\package-release.cmd
Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen
Installer und Packaging-Werkzeug. Danach liegen der Paketordner
`artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare
-`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit.
+`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit. Zusaetzlich entstehen
+`BizTalkCheckmkPulse-Setup.zip.b64.txt` fuer `certutil -decode` und eine
+SHA-256-Textdatei.
### 8.2 Normales Servicekonto
@@ -392,6 +394,14 @@ starten.
Der Installer muss als lokaler Administrator laufen. Er vergibt keine
AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben.
+Beim Update wird zuerst die neue Paketversion im Staging-Verzeichnis mit neun
+Self-Test-Zeilen validiert. Bestehende AppSettings werden in die neue Config
+uebernommen; neue Keys stammen aus der neuen Standardkonfiguration. Die
+Runtime-Daten unter `%ProgramData%` bleiben bestehen. Erst danach wird der
+Task gestoppt und die Programmversion umgeschaltet. Scheitert ein Folgeschritt,
+versucht der Installer Programmverzeichnis, Checkmk-Wrapper und Scheduled Task
+auf den vorherigen Stand zurueckzusetzen.
+
Der installierte Task selbst laeuft mit `RunLevel Limited`. Der Installer
uebergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle;
es wird weder in einer Prozesskommandozeile noch in Config oder Log
@@ -542,8 +552,8 @@ BizTalk-Probes:
| `ProbeEndpointConnectivity` | `true` |
| `EndpointDiscoveryIntervalHours` | `168` |
| `EndpointProbeTimeoutMilliseconds` | `3000` |
-| `EndpointProbeMaxConcurrency` | `12` |
-| `EndpointMaxCount` | `500` |
+| `EndpointProbeMaxConcurrency` | `16` |
+| `EndpointMaxCount` | `100` |
Alarmierung:
@@ -580,6 +590,10 @@ Automatisiert:
- Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert,
- manuelle Endpoint-Overrides ueberleben den automatischen Wochenabgleich,
- nur nicht erreichbare Endpoints erscheinen im Checkmk-Detail,
+- 70 vollstaendig timeoutende eindeutige Ziele bleiben mit den Defaults in
+ einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte
+ Maximum von 100 Zielen in etwa 21 Sekunden,
+- Update-Config-Merge erhaelt bestehende Werte und fuegt neue Defaults hinzu,
- Snapshot-Roundtrip und Ersatz,
- SHA-256-Manipulation wird verworfen,
- Stale-Snapshot wird verworfen,
diff --git a/README.md b/README.md
index b431e1c..1d12931 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,8 @@ und das transportierbare ZIP:
```text
artifacts\
BizTalkCheckmkPulse-Setup.zip
+ BizTalkCheckmkPulse-Setup.zip.b64.txt
+ BizTalkCheckmkPulse-Setup.zip.sha256.txt
BizTalkCheckmkPulse-Setup\
Setup.exe
INSTALLATION.md
@@ -183,6 +185,14 @@ Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler
`Release`-Build in Visual Studio 2019 genuegt daher; ein separates
PowerShell-/WiX-/MSI-Tooling wird nicht benoetigt.
+Die Base64-TXT kann auf dem Windows-Zielsystem ohne Zusatzwerkzeug dekodiert
+werden:
+
+```cmd
+certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip
+certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256
+```
+
Format-Self-Test ohne WMI, SQL oder Event Log:
```cmd
@@ -226,7 +236,7 @@ normaler Bestandteil des Kontonamens verarbeitet.
Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert
werden. Dazu `Setup.exe` mit dem neuen Kennwort erneut ausfuehren. Bis dahin
-wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle acht Services
+wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle neun Services
als `UNKNOWN`.
Ein gMSA kann ueber die Checkbox ohne Kennworteingabe installiert werden; die
@@ -234,15 +244,22 @@ produktive Standardbeschreibung geht vom normalen Servicekonto aus.
Der Installer:
-1. kopiert EXE und Config nach
- `%ProgramFiles%\BizTalkCheckmkPulse`,
-2. erstellt `%ProgramData%\BizTalkCheckmkPulse\data` und `logs`,
-3. setzt explizite ACLs fuer Administratoren, Provider und `LocalSystem`,
-4. installiert nur den kleinen `.cmd`-Consumer unter
+1. prueft Paket und neun Self-Test-Zeilen, bevor der bestehende Task angehalten
+ wird,
+2. baut die neue Version in einem Staging-Verzeichnis auf und uebernimmt
+ vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten
+ unveraenderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert,
+3. schaltet die Programmdateien per Verzeichniswechsel um; bei einem Fehler
+ werden vorherige Version, Wrapper und Task best effort wiederhergestellt,
+4. behaelt `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot
+ und Logs bei,
+5. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`,
+6. setzt explizite ACLs fuer Administratoren, Provider und `LocalSystem`,
+7. installiert nur den kleinen `.cmd`-Consumer unter
`%ProgramData%\checkmk\agent\local`,
-5. registriert `BizTalk Checkmk Pulse Provider` minuetlich mit
+8. registriert `BizTalk Checkmk Pulse Provider` minuetlich mit
`IgnoreNew`, fuenf Minuten Laufzeitlimit und zwei Wiederholungen,
-6. fuehrt den Self-Test aus und startet den Provider einmalig.
+9. fuehrt den installierten Self-Test erneut aus und startet den Provider.
Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell.
@@ -332,8 +349,8 @@ Wichtige Werte:
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollstaendige URIs/Secrets. |
| `EndpointDiscoveryIntervalHours` | `168` | Intervall fuer den vollstaendigen Umgebungsabgleich. |
| `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. |
-| `EndpointProbeMaxConcurrency` | `12` | Begrenzte parallele Socket-Probes. |
-| `EndpointMaxCount` | `500` | Harte Obergrenze gegen fehlerhafte/uebergrosse Konfiguration. |
+| `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. |
+| `EndpointMaxCount` | `100` | Harte Obergrenze und Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. |
| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. |
| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. |
diff --git a/deployment/INSTALLATION.md b/deployment/INSTALLATION.md
index 52541bf..fedb536 100644
--- a/deployment/INSTALLATION.md
+++ b/deployment/INSTALLATION.md
@@ -1,5 +1,12 @@
# Installation auf dem BizTalk-Server
+Falls nur die TXT-Datei uebertragen wurde:
+
+```cmd
+certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip
+certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256
+```
+
1. `BizTalkCheckmkPulse-Setup.zip` vollstaendig in ein lokales Verzeichnis entpacken.
2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestaetigen.
3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel
@@ -8,6 +15,25 @@
5. Im Aufgabenplaner den Task `BizTalk Checkmk Pulse Provider` und danach den
Checkmk-Agent-Dump kontrollieren.
+Bei einer bereits vorhandenen ACC-Installation ist dies ein Update. Der
+Installer erkennt die bestehende Umgebung, behaelt Endpoint-Katalog, Snapshot
+und Logs sowie vorhandene AppSettings bei und ergaenzt neue Config-Keys aus dem
+Paket. Das BEW-Konto und sein aktuelles Kennwort muessen erneut eingegeben
+werden, weil der Scheduled Task mit den bestaetigten Zugangsdaten neu
+registriert wird.
+
+Die frueheren unveraenderten Endpoint-Defaults `12` parallele Probes und
+`EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100`
+migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten.
+
+Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet.
+Bei normalen Servicekonten prueft der Installer ausserdem Kennwort und
+Batch-Anmelderecht vor der Umschaltung.
+Der Task wird vor dem Dateitausch deaktiviert und sein Prozessende maximal zehn
+Sekunden abgewartet, damit keine laufende Provider-EXE ueberschrieben wird.
+Bei einem Fehler nach der Umschaltung versucht der Installer, vorherige
+Programmdateien, Wrapper und Task wiederherzustellen.
+
PowerShell wird fuer Installation, Update, Deinstallation und Laufzeit nicht
benoetigt. Das Kennwort wird direkt an die Windows-Aufgabenplanung uebergeben
und weder in einer Datei noch in einer Prozesskommandozeile abgelegt.
diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md
index 2faad1f..e387ea1 100644
--- a/docs/CheckmkServices.md
+++ b/docs/CheckmkServices.md
@@ -202,11 +202,13 @@ Metriken:
| --- | --- |
| `biztalk_endpoints_configured` | Eintraege in `endpoints.xml`, einschliesslich derzeit inaktiver. |
| `biztalk_endpoints_active` | Im aktuellen Lauf Started/Enabled und fuer die Probe ausgewaehlt. |
+| `biztalk_endpoints_unique_targets` | Nach Host/Port/Protokoll-Deduplizierung tatsaechlich verbundene Ziele. |
| `biztalk_endpoints_tested` | Vollstaendig abgeschlossene Artefaktprobes. |
| `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. |
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
| `biztalk_endpoints_unsupported` | Aktive externe Adressen ohne sicher bestimmbaren Host/Port. |
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Eintraege, deren BizTalk-Artefakt aktuell nicht aktiv ist. |
+| `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. |
Bei expliziten `udp://host:port`-Eintraegen kann ein generischer Check ohne
applikationsspezifisches Protokoll nur DNS, Route und erfolgreichen lokalen
diff --git a/docs/EndpointCatalog.md b/docs/EndpointCatalog.md
index 976aca2..8630fa1 100644
--- a/docs/EndpointCatalog.md
+++ b/docs/EndpointCatalog.md
@@ -26,6 +26,22 @@ Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr
geprueft. Neue Artefakte werden spaetestens beim naechsten Wochenabgleich
aufgenommen.
+## Laufzeitbudget
+
+Die Socket-Probes werden nach Protokoll, Host und Port dedupliziert und mit
+maximal `EndpointProbeMaxConcurrency=16` parallelen Verbindungen ausgefuehrt.
+Bei `EndpointProbeTimeoutMilliseconds=3000` benoetigen 70 vollstaendig
+timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze
+`EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa
+21 Sekunden. Nicht pruefbare aktive BizTalk-Artefakte zaehlen nicht gegen diese
+Grenze; sie werden separat als `unsupported` gemeldet.
+
+Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den
+zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem
+Update an `biztalk_endpoint_probe_duration_ms` sowie an `endpoint_probe_ms` und
+`elapsed_ms` im Provider-Log zu kontrollieren. Der Scheduled Task verhindert
+mit `IgnoreNew` ueberlappende Providerlaeufe.
+
## Sicherheitsmodell
Der Katalog speichert nicht die vollstaendige BizTalk-Adresse. Persistiert
diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md
index 10d84d2..78699c3 100644
--- a/docs/ExampleOutput.md
+++ b/docs/ExampleOutput.md
@@ -61,7 +61,7 @@ Artefakte. Nur die Textliste ist begrenzt.
Gesund, ohne Auflistung aller Ziele:
```text
-0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar; catalog_utc=2026-08-04T10:00:00.0000000Z
+0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar (unique_targets=44, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z
```
Fehlerhaft, nur mit nicht erreichbaren Zielen:
diff --git a/scripts/package-release.cmd b/scripts/package-release.cmd
index 7f4c15a..d138b3c 100644
--- a/scripts/package-release.cmd
+++ b/scripts/package-release.cmd
@@ -6,4 +6,6 @@ if errorlevel 1 exit /b 1
echo Install package written to artifacts\BizTalkCheckmkPulse-Setup
echo Install ZIP written to artifacts\BizTalkCheckmkPulse-Setup.zip
+echo Certutil Base64 written to artifacts\BizTalkCheckmkPulse-Setup.zip.b64.txt
+echo SHA-256 written to artifacts\BizTalkCheckmkPulse-Setup.zip.sha256.txt
exit /b 0
diff --git a/src/BizTalkCheckmkPulse.Packager/Program.cs b/src/BizTalkCheckmkPulse.Packager/Program.cs
index b471e4c..b0114c6 100644
--- a/src/BizTalkCheckmkPulse.Packager/Program.cs
+++ b/src/BizTalkCheckmkPulse.Packager/Program.cs
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text;
namespace BizTalkCheckmkPulse.Packager
{
@@ -17,6 +19,8 @@ namespace BizTalkCheckmkPulse.Packager
var package = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup");
var application = Path.Combine(package, "application");
var zip = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup.zip");
+ var base64 = zip + ".b64.txt";
+ var checksum = zip + ".sha256.txt";
if (Directory.Exists(package)) Directory.Delete(package, true);
Directory.CreateDirectory(application);
@@ -39,8 +43,15 @@ namespace BizTalkCheckmkPulse.Packager
if (File.Exists(zip)) File.Delete(zip);
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
+ WriteBase64(zip, base64);
+ File.WriteAllText(
+ checksum,
+ Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine,
+ new UTF8Encoding(false));
Console.WriteLine("Install package: " + package);
Console.WriteLine("Install ZIP: " + zip);
+ Console.WriteLine("Base64 TXT: " + base64);
+ Console.WriteLine("SHA-256 TXT: " + checksum);
return 0;
}
catch (Exception ex)
@@ -55,5 +66,29 @@ namespace BizTalkCheckmkPulse.Packager
if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source);
File.Copy(source, target, true);
}
+
+ private static void WriteBase64(string source, string target)
+ {
+ var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
+ var builder = new StringBuilder(encoded.Length + encoded.Length / 64 * 2 + 2);
+ for (var offset = 0; offset < encoded.Length; offset += 64)
+ {
+ builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset));
+ builder.Append('\n');
+ }
+ File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false));
+ }
+
+ private static string Sha256(string path)
+ {
+ using (var stream = File.OpenRead(path))
+ using (var algorithm = SHA256.Create())
+ {
+ var hash = algorithm.ComputeHash(stream);
+ var builder = new StringBuilder(hash.Length * 2);
+ foreach (var value in hash) builder.Append(value.ToString("x2"));
+ return builder.ToString();
+ }
+ }
}
}
diff --git a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj
index 017ab23..ada08f1 100644
--- a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj
+++ b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj
@@ -39,6 +39,7 @@
+
diff --git a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs
index 9c54d96..e9659b7 100644
--- a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs
+++ b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Xml;
@@ -27,6 +30,7 @@ namespace BizTalkCheckmkPulse.Setup
public void Install(string account, string password, bool isGmsa, string environmentName, Action report)
{
Validate(account, password, isGmsa, environmentName);
+ report = report ?? delegate { };
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier));
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")");
@@ -37,46 +41,153 @@ namespace BizTalkCheckmkPulse.Setup
RequireFile(sourceExe);
RequireFile(sourceConfig);
RequireFile(sourceWrapper);
-
- // Ein laufender Provider kann die installierte EXE waehrend eines Updates sperren.
- new TaskSchedulerService().DeleteIfExists(TaskName);
- report("Vorhandener Scheduled Task angehalten beziehungsweise fuer das Update entfernt.");
-
- Directory.CreateDirectory(installDirectory);
var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe");
var targetConfig = targetExe + ".config";
- File.Copy(sourceExe, targetExe, true);
- File.Copy(sourceConfig, targetConfig, true);
- SetEnvironment(targetConfig, environmentName);
- report("Programmdateien installiert: " + installDirectory);
+ var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
+ var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
+ var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
+ var previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null;
+ var hadExistingInstallation = Directory.Exists(installDirectory);
+ var taskRemoved = false;
+ var backupCreated = false;
+ var filesActivated = false;
+ var scheduler = new TaskSchedulerService();
- var dataDirectory = Path.Combine(runtimeDirectory, "data");
- var logDirectory = Path.Combine(runtimeDirectory, "logs");
- Directory.CreateDirectory(runtimeDirectory);
- Directory.CreateDirectory(dataDirectory);
- Directory.CreateDirectory(logDirectory);
+ try
+ {
+ RunSelfTest(sourceExe);
+ report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services.");
+ if (!isGmsa)
+ {
+ ValidateBatchLogon(account, password);
+ report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt.");
+ }
- ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
- ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
- ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute);
- ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
- report("Least-Privilege-Verzeichnisrechte gesetzt.");
+ Directory.CreateDirectory(stagingDirectory);
+ var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe");
+ var stagedConfig = stagedExe + ".config";
+ File.Copy(sourceExe, stagedExe, false);
+ var effectiveEnvironment = PrepareConfig(
+ sourceConfig,
+ stagedConfig,
+ File.Exists(targetConfig) ? targetConfig : null,
+ environmentName);
+ RunSelfTest(stagedExe);
+ report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + ".");
- Directory.CreateDirectory(checkmkLocalDirectory);
- File.Copy(sourceWrapper, Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"), true);
- report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
+ // Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
+ scheduler.DeleteIfExists(TaskName);
+ taskRemoved = true;
+ report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
- RunSelfTest(targetExe);
- report("Self-Test erfolgreich: acht Checkmk-Services.");
+ if (hadExistingInstallation)
+ {
+ Directory.Move(installDirectory, backupDirectory);
+ backupCreated = true;
+ }
- new TaskSchedulerService().RegisterAndStart(
- TaskName,
- targetExe,
- installDirectory,
- account,
- isGmsa ? null : password,
- isGmsa);
- report("Scheduled Task registriert: " + TaskName);
+ Directory.Move(stagingDirectory, installDirectory);
+ filesActivated = true;
+ report(hadExistingInstallation
+ ? "Programmdateien atomar auf die neue Version umgestellt."
+ : "Programmdateien installiert: " + installDirectory);
+
+ var dataDirectory = Path.Combine(runtimeDirectory, "data");
+ var logDirectory = Path.Combine(runtimeDirectory, "logs");
+ Directory.CreateDirectory(runtimeDirectory);
+ Directory.CreateDirectory(dataDirectory);
+ Directory.CreateDirectory(logDirectory);
+
+ ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
+ ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
+ ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute);
+ ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
+ report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten.");
+
+ Directory.CreateDirectory(checkmkLocalDirectory);
+ File.Copy(sourceWrapper, targetWrapper, true);
+ report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
+
+ RunSelfTest(targetExe);
+ report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
+
+ scheduler.RegisterAndStart(
+ TaskName,
+ targetExe,
+ installDirectory,
+ account,
+ isGmsa ? null : password,
+ isGmsa);
+ taskRemoved = false;
+ report("Scheduled Task registriert und einmalig gestartet: " + TaskName);
+
+ TryDeleteDirectory(backupDirectory, report);
+ }
+ catch (Exception installException)
+ {
+ var rollbackFailures = new List();
+ try
+ {
+ scheduler.DeleteIfExists(TaskName);
+ }
+ catch (Exception ex)
+ {
+ rollbackFailures.Add("Task stoppen: " + ex.Message);
+ }
+
+ if (filesActivated || backupCreated)
+ {
+ try
+ {
+ if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
+ if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory);
+ report("Vorherige Programmversion wiederhergestellt.");
+ }
+ catch (Exception ex)
+ {
+ rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message);
+ }
+ }
+
+ try
+ {
+ RestoreWrapper(targetWrapper, previousWrapper);
+ }
+ catch (Exception ex)
+ {
+ rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
+ }
+
+ if (taskRemoved && hadExistingInstallation && File.Exists(targetExe))
+ {
+ try
+ {
+ scheduler.RegisterAndStart(
+ TaskName,
+ targetExe,
+ installDirectory,
+ account,
+ isGmsa ? null : password,
+ isGmsa);
+ report("Scheduled Task fuer die vorherige Version wiederhergestellt.");
+ }
+ catch (Exception ex)
+ {
+ rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message);
+ }
+ }
+
+ var rollback = rollbackFailures.Count == 0
+ ? "Rollback erfolgreich."
+ : "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures);
+ throw new InvalidOperationException(
+ "Installation/Update fehlgeschlagen. " + rollback + " Ursache: " + installException.Message,
+ installException);
+ }
+ finally
+ {
+ TryDeleteDirectory(stagingDirectory, null);
+ }
}
public void Uninstall(bool keepRuntimeData, Action report)
@@ -108,14 +219,152 @@ namespace BizTalkCheckmkPulse.Setup
throw new FileNotFoundException("Installationspaket ist unvollstaendig. Datei fehlt: " + path, path);
}
- private static void SetEnvironment(string configPath, string environmentName)
+ internal string GetInstalledEnvironment()
{
- var document = new XmlDocument { PreserveWhitespace = true };
- document.Load(configPath);
- var setting = document.SelectSingleNode("/configuration/appSettings/add[@key='EnvironmentName']") as XmlElement;
- if (setting == null) throw new InvalidDataException("EnvironmentName fehlt in " + configPath + ".");
- setting.SetAttribute("value", environmentName);
- document.Save(configPath);
+ var config = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe.config");
+ if (!File.Exists(config)) return string.Empty;
+ try
+ {
+ var document = LoadXml(config);
+ var setting = FindAppSetting(document, "EnvironmentName");
+ return setting == null ? string.Empty : setting.GetAttribute("value").Trim();
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ internal bool IsInstalled
+ {
+ get { return File.Exists(Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe")); }
+ }
+
+ internal static string PrepareConfig(
+ string sourceConfig,
+ string stagedConfig,
+ string existingConfig,
+ string requestedEnvironment)
+ {
+ var document = LoadXml(sourceConfig);
+ if (!string.IsNullOrWhiteSpace(existingConfig) && File.Exists(existingConfig))
+ {
+ var existing = LoadXml(existingConfig);
+ var existingSettings = existing.SelectNodes("/configuration/appSettings/add[@key]");
+ if (existingSettings != null)
+ {
+ foreach (XmlNode node in existingSettings)
+ {
+ var element = node as XmlElement;
+ if (element == null) continue;
+ var key = element.GetAttribute("key");
+ var value = element.GetAttribute("value");
+ if (IsSupersededDefault(key, value)) continue;
+ var target = FindAppSetting(document, key);
+ if (target != null) target.SetAttribute("value", value);
+ }
+ }
+ }
+
+ var environmentSetting = FindAppSetting(document, "EnvironmentName");
+ if (environmentSetting == null)
+ throw new InvalidDataException("EnvironmentName fehlt in " + sourceConfig + ".");
+ if (!string.IsNullOrWhiteSpace(requestedEnvironment))
+ environmentSetting.SetAttribute("value", requestedEnvironment.Trim());
+
+ document.Save(stagedConfig);
+ return environmentSetting.GetAttribute("value").Trim();
+ }
+
+ private static XmlDocument LoadXml(string path)
+ {
+ var document = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
+ using (var reader = XmlReader.Create(path, new XmlReaderSettings
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ XmlResolver = null
+ }))
+ {
+ document.Load(reader);
+ }
+ return document;
+ }
+
+ private static XmlElement FindAppSetting(XmlDocument document, string key)
+ {
+ var nodes = document.SelectNodes("/configuration/appSettings/add[@key]");
+ if (nodes == null) return null;
+ foreach (XmlNode node in nodes)
+ {
+ var element = node as XmlElement;
+ if (element != null && string.Equals(element.GetAttribute("key"), key, StringComparison.Ordinal))
+ return element;
+ }
+ return null;
+ }
+
+ private static bool IsSupersededDefault(string key, string value)
+ {
+ return string.Equals(key, "EndpointProbeMaxConcurrency", StringComparison.Ordinal)
+ && string.Equals(value, "12", StringComparison.Ordinal)
+ || string.Equals(key, "EndpointMaxCount", StringComparison.Ordinal)
+ && string.Equals(value, "500", StringComparison.Ordinal);
+ }
+
+ private static void ValidateBatchLogon(string account, string password)
+ {
+ var separator = account.IndexOf('\\');
+ var domain = account.Substring(0, separator);
+ var user = account.Substring(separator + 1);
+ IntPtr token;
+ if (!LogonUser(user, domain, password, 4, 0, out token))
+ {
+ throw new InvalidOperationException(
+ "Collector-Anmeldung als Batch fehlgeschlagen: "
+ + new Win32Exception(Marshal.GetLastWin32Error()).Message
+ + ". Kennwort und lokales Recht 'Log on as a batch job' pruefen.");
+ }
+
+ CloseHandle(token);
+ }
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool LogonUser(
+ string userName,
+ string domain,
+ string password,
+ int logonType,
+ int logonProvider,
+ out IntPtr token);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool CloseHandle(IntPtr handle);
+
+ private static void RestoreWrapper(string path, byte[] previousContent)
+ {
+ if (previousContent == null)
+ {
+ if (File.Exists(path)) File.Delete(path);
+ return;
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ File.WriteAllBytes(path, previousContent);
+ }
+
+ private static void TryDeleteDirectory(string path, Action report)
+ {
+ if (!Directory.Exists(path)) return;
+ try
+ {
+ Directory.Delete(path, true);
+ }
+ catch (Exception ex)
+ {
+ if (report != null) report("Hinweis: temporaeres Verzeichnis konnte nicht entfernt werden: " + path + " (" + ex.Message + ")");
+ }
}
private static void ApplyDirectoryAcl(
@@ -168,7 +417,7 @@ namespace BizTalkCheckmkPulse.Setup
throw new InvalidOperationException("Self-Test hat das Zeitlimit ueberschritten.");
}
var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
- if (process.ExitCode != 0 || lines.Length != 8 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal)))
+ if (process.ExitCode != 0 || lines.Length != 9 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal)))
throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error);
}
}
diff --git a/src/BizTalkCheckmkPulse.Setup/MainForm.cs b/src/BizTalkCheckmkPulse.Setup/MainForm.cs
index d3b5806..4931c0b 100644
--- a/src/BizTalkCheckmkPulse.Setup/MainForm.cs
+++ b/src/BizTalkCheckmkPulse.Setup/MainForm.cs
@@ -55,7 +55,9 @@ namespace BizTalkCheckmkPulse.Setup
environment.Size = new Size(160, 23);
environment.DropDownStyle = ComboBoxStyle.DropDownList;
environment.Items.AddRange(new object[] { "(keine)", "ACC", "DEV", "TST", "PRD" });
- environment.SelectedIndex = 0;
+ var installedEnvironment = engine.GetInstalledEnvironment();
+ var installedIndex = environment.Items.IndexOf(installedEnvironment);
+ environment.SelectedIndex = installedIndex >= 0 ? installedIndex : 0;
gmsa.Location = new Point(190, 229);
gmsa.Size = new Size(420, 24);
@@ -81,7 +83,9 @@ namespace BizTalkCheckmkPulse.Setup
status.Multiline = true;
status.ReadOnly = true;
status.ScrollBars = ScrollBars.Vertical;
- status.Text = "Bereit. Setup.exe muss aus dem vollstaendig entpackten Installationspaket gestartet werden.";
+ status.Text = engine.IsInstalled
+ ? "Bestehende Installation erkannt. Update behaelt Runtime-Daten und vorhandene Konfigurationswerte bei; Konto und Kennwort werden fuer den Scheduled Task neu bestaetigt."
+ : "Bereit. Setup.exe muss aus dem vollstaendig entpackten Installationspaket gestartet werden.";
Controls.AddRange(new Control[]
{
diff --git a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..d147f79
--- /dev/null
+++ b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")]
+[assembly: AssemblyTitle("BizTalk Checkmk Pulse Setup")]
+[assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")]
+[assembly: AssemblyCompany("BEW")]
+[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
+[assembly: AssemblyVersion("2.2.1.0")]
+[assembly: AssemblyFileVersion("2.2.1.0")]
diff --git a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs
index a8f2ad5..6747094 100644
--- a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs
+++ b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs
@@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
+using System.Threading;
namespace BizTalkCheckmkPulse.Setup
{
@@ -10,6 +11,7 @@ namespace BizTalkCheckmkPulse.Setup
private const int TaskTriggerTime = 1;
private const int TaskActionExecute = 0;
private const int TaskInstancesIgnoreNew = 2;
+ private const int TaskStateRunning = 4;
public void RegisterAndStart(
string taskName,
@@ -91,6 +93,7 @@ namespace BizTalkCheckmkPulse.Setup
try
{
task = root.GetTask(taskName);
+ task.Enabled = false;
try
{
task.Stop(0);
@@ -99,6 +102,14 @@ namespace BizTalkCheckmkPulse.Setup
{
// Ein nicht laufender Task muss vor dem Loeschen nicht gestoppt werden.
}
+ for (var attempt = 0; attempt < 100 && (int)task.State == TaskStateRunning; attempt++)
+ {
+ Thread.Sleep(100);
+ }
+ if ((int)task.State == TaskStateRunning)
+ {
+ throw new TimeoutException("Scheduled Task konnte innerhalb von 10 Sekunden nicht beendet werden.");
+ }
root.DeleteTask(taskName, 0);
}
catch (COMException ex)
diff --git a/src/BizTalkCheckmkPulse/App.config b/src/BizTalkCheckmkPulse/App.config
index a5165e6..987ae1b 100644
--- a/src/BizTalkCheckmkPulse/App.config
+++ b/src/BizTalkCheckmkPulse/App.config
@@ -21,8 +21,9 @@
-
-
+
+
+
diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs
index 7299cb6..1c6aa9e 100644
--- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs
+++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs
@@ -391,18 +391,22 @@ namespace BizTalkCheckmkPulse
: CheckState.Ok;
var metrics = string.Format(
CultureInfo.InvariantCulture,
- "biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_tested={2};;;0|biztalk_endpoints_available={3};;;0|biztalk_endpoints_failed={4};;1;0|biztalk_endpoints_unsupported={5};;1;0|biztalk_endpoints_inactive_skipped={6};;;0",
+ "biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unsupported={6};;1;0|biztalk_endpoints_inactive_skipped={7};;;0|biztalk_endpoint_probe_duration_ms={8};;;0",
endpointState.Configured,
endpointState.Active,
+ endpointState.UniqueTargets,
endpointState.Results.Count,
available,
failed.Length,
endpointState.UnsupportedActive,
- endpointState.SkippedInactive);
+ endpointState.SkippedInactive,
+ endpointState.ProbeDurationMilliseconds);
var detail = new StringBuilder();
if (failed.Length == 0 && !incomplete)
{
- detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar");
+ detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar")
+ .Append(" (unique_targets=").Append(endpointState.UniqueTargets)
+ .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")");
}
else
{
@@ -410,7 +414,9 @@ namespace BizTalkCheckmkPulse
.Append(", tested=").Append(endpointState.Results.Count)
.Append(", available=").Append(available)
.Append(", failed=").Append(failed.Length)
- .Append(", unsupported_external=").Append(endpointState.UnsupportedActive);
+ .Append(", unsupported_external=").Append(endpointState.UnsupportedActive)
+ .Append(", unique_targets=").Append(endpointState.UniqueTargets)
+ .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds);
AppendLimitedList(detail, "unavailable", failed.Select(x =>
EndpointConnectivityProbe.Display(x.Endpoint) + "(" + EmptyAsUnknown(x.Failure) + ")"));
if (!string.IsNullOrWhiteSpace(endpointState.Failure))
diff --git a/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs b/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs
index 237d9fb..0cbe4da 100644
--- a/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs
+++ b/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs
@@ -89,8 +89,8 @@ namespace BizTalkCheckmkPulse
MachineName = machine,
EnvironmentName = catalogEnvironment,
SynchronizedUtc = synchronizedUtc,
- ActiveCandidates = ReadInt(root, "activeCandidates", 0, _maxEntries),
- UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, _maxEntries)
+ ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000),
+ UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000)
};
foreach (var node in root.Elements("Endpoint"))
diff --git a/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs b/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs
index 7896b01..66a056e 100644
--- a/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs
+++ b/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs
@@ -120,7 +120,15 @@ namespace BizTalkCheckmkPulse
try
{
- foreach (var probeResult in ProbeAllAsync(activeEntries).GetAwaiter().GetResult())
+ var probeStopwatch = Stopwatch.StartNew();
+ var probeResults = ProbeAllAsync(activeEntries).GetAwaiter().GetResult();
+ probeStopwatch.Stop();
+ state.ProbeDurationMilliseconds = probeStopwatch.ElapsedMilliseconds;
+ state.UniqueTargets = activeEntries
+ .Select(TargetKey)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Count();
+ foreach (var probeResult in probeResults)
{
state.Results.Add(probeResult);
if (!probeResult.Available)
@@ -192,10 +200,9 @@ namespace BizTalkCheckmkPulse
{
var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc);
catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty;
- if (catalog.Entries.Count > _options.EndpointMaxCount
- || catalog.ActiveCandidates > _options.EndpointMaxCount)
+ if (catalog.Entries.Count > _options.EndpointMaxCount)
{
- throw new InvalidDataException("Discovered endpoint candidates exceed EndpointMaxCount.");
+ throw new InvalidDataException("Probeable endpoints exceed EndpointMaxCount.");
}
return catalog;
@@ -221,7 +228,7 @@ namespace BizTalkCheckmkPulse
private async Task> ProbeAllAsync(EndpointCatalogEntry[] endpoints)
{
var unique = endpoints
- .GroupBy(x => x.Protocol.ToUpperInvariant() + "|" + x.Host.ToUpperInvariant() + "|" + x.Port)
+ .GroupBy(TargetKey, StringComparer.OrdinalIgnoreCase)
.ToArray();
var outcomes = new Dictionary(StringComparer.OrdinalIgnoreCase);
using (var gate = new SemaphoreSlim(_options.EndpointProbeMaxConcurrency))
@@ -248,7 +255,7 @@ namespace BizTalkCheckmkPulse
return endpoints.Select(endpoint =>
{
- var key = endpoint.Protocol.ToUpperInvariant() + "|" + endpoint.Host.ToUpperInvariant() + "|" + endpoint.Port;
+ var key = TargetKey(endpoint);
var outcome = outcomes[key];
return new EndpointProbeResult
{
@@ -260,6 +267,28 @@ namespace BizTalkCheckmkPulse
}).ToArray();
}
+ internal static long CalculateWorstCaseProbeMilliseconds(int targetCount, int concurrency, int timeoutMilliseconds)
+ {
+ if (targetCount <= 0)
+ {
+ return 0;
+ }
+
+ if (concurrency <= 0 || timeoutMilliseconds <= 0)
+ {
+ throw new ArgumentOutOfRangeException("concurrency");
+ }
+
+ return ((targetCount + concurrency - 1L) / concurrency) * timeoutMilliseconds;
+ }
+
+ private static string TargetKey(EndpointCatalogEntry endpoint)
+ {
+ return endpoint.Protocol.ToUpperInvariant()
+ + "|" + endpoint.Host.ToUpperInvariant()
+ + "|" + endpoint.Port;
+ }
+
private async Task ProbeOneAsync(EndpointCatalogEntry endpoint)
{
return string.Equals(endpoint.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)
diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs
index 79fc8f7..a45753d 100644
--- a/src/BizTalkCheckmkPulse/Models.cs
+++ b/src/BizTalkCheckmkPulse/Models.cs
@@ -349,6 +349,8 @@ namespace BizTalkCheckmkPulse
public int Active { get; set; }
public int SkippedInactive { get; set; }
public int UnsupportedActive { get; set; }
+ public int UniqueTargets { get; set; }
+ public long ProbeDurationMilliseconds { get; set; }
public string Failure { get; set; }
public List Results { get; private set; }
}
diff --git a/src/BizTalkCheckmkPulse/MonitoringOptions.cs b/src/BizTalkCheckmkPulse/MonitoringOptions.cs
index dafa858..bcedc01 100644
--- a/src/BizTalkCheckmkPulse/MonitoringOptions.cs
+++ b/src/BizTalkCheckmkPulse/MonitoringOptions.cs
@@ -85,8 +85,8 @@ namespace BizTalkCheckmkPulse
EndpointCatalogMaxBytes = 1048576;
EndpointDiscoveryIntervalHours = 168;
EndpointProbeTimeoutMilliseconds = 3000;
- EndpointProbeMaxConcurrency = 12;
- EndpointMaxCount = 500;
+ EndpointProbeMaxConcurrency = 16;
+ EndpointMaxCount = 100;
}
///
diff --git a/src/BizTalkCheckmkPulse/Program.cs b/src/BizTalkCheckmkPulse/Program.cs
index 84f3718..621cfd2 100644
--- a/src/BizTalkCheckmkPulse/Program.cs
+++ b/src/BizTalkCheckmkPulse/Program.cs
@@ -102,6 +102,10 @@ namespace BizTalkCheckmkPulse
+ result.EndpointConnectivity.Active
+ " endpoints_failed="
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
+ + " endpoints_unique_targets="
+ + result.EndpointConnectivity.UniqueTargets
+ + " endpoint_probe_ms="
+ + result.EndpointConnectivity.ProbeDurationMilliseconds
+ " elapsed_ms="
+ stopwatch.ElapsedMilliseconds);
return 0;
diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs
index 8152431..1b32362 100644
--- a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs
+++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs
@@ -6,5 +6,5 @@ using System.Reflection;
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
[assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
-[assembly: AssemblyVersion("2.2.0.0")]
-[assembly: AssemblyFileVersion("2.2.0.0")]
+[assembly: AssemblyVersion("2.2.1.0")]
+[assembly: AssemblyFileVersion("2.2.1.0")]
diff --git a/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj b/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj
index a9a5d74..aea08d4 100644
--- a/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj
+++ b/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj
@@ -39,6 +39,10 @@
{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}
BizTalkCheckmkPulse
+
+ {764AC43A-26D8-43C1-9121-13FC8A9CC8B1}
+ BizTalkCheckmkPulse.Setup
+
diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs
index a8bf0b1..f32eb91 100644
--- a/tests/BizTalkCheckmkPulse.Tests/Program.cs
+++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs
@@ -32,6 +32,8 @@ namespace BizTalkCheckmkPulse.Tests
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
+ Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval);
+ Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings);
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
@@ -454,11 +456,11 @@ namespace BizTalkCheckmkPulse.Tests
{
EnvironmentName = "ACC",
SynchronizedUtc = DateTime.UtcNow,
- ActiveCandidates = 1,
+ ActiveCandidates = 150,
UnsupportedCandidates = 0
};
catalog.Entries.Add(TestEndpoint("Orders", "api.example.test"));
- var store = new EndpointCatalogStore(path, 1048576, 500);
+ var store = new EndpointCatalogStore(path, 1048576, 100);
store.Write(catalog);
var loaded = store.Read("ACC");
@@ -493,6 +495,75 @@ namespace BizTalkCheckmkPulse.Tests
};
}
+ private static void EndpointProbeBudgetFitsMinuteInterval()
+ {
+ var options = new MonitoringOptions();
+ var seventyTargets = EndpointConnectivityProbe.CalculateWorstCaseProbeMilliseconds(
+ 70,
+ options.EndpointProbeMaxConcurrency,
+ options.EndpointProbeTimeoutMilliseconds);
+ var configuredMaximum = EndpointConnectivityProbe.CalculateWorstCaseProbeMilliseconds(
+ options.EndpointMaxCount,
+ options.EndpointProbeMaxConcurrency,
+ options.EndpointProbeTimeoutMilliseconds);
+
+ AssertEqual(15000, (int)seventyTargets, "70-target theoretical timeout budget");
+ Assert(configuredMaximum <= 30000, "configured endpoint worst-case must leave headroom in the minute interval");
+ }
+
+ private static void InstallerUpdatePreservesExistingSettings()
+ {
+ var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.InstallerTests." + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+ try
+ {
+ var source = Path.Combine(directory, "source.config");
+ var existing = Path.Combine(directory, "existing.config");
+ var staged = Path.Combine(directory, "staged.config");
+ File.WriteAllText(
+ source,
+ ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "",
+ new UTF8Encoding(false));
+ File.WriteAllText(
+ existing,
+ ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "",
+ new UTF8Encoding(false));
+
+ var effectiveEnvironment = BizTalkCheckmkPulse.Setup.InstallerEngine.PrepareConfig(
+ source,
+ staged,
+ existing,
+ string.Empty);
+ var merged = File.ReadAllText(staged);
+ AssertEqual("ACC", effectiveEnvironment, "preserved installer environment");
+ Assert(merged.Contains("key=\"EndpointProbeMaxConcurrency\" value=\"7\""), "existing operational value must be preserved");
+ Assert(merged.Contains("key=\"EndpointMaxCount\" value=\"100\""), "superseded old default must migrate to new bounded default");
+ Assert(merged.Contains("key=\"NewSetting\" value=\"new-default\""), "new source setting must be added");
+ Assert(merged.IndexOf("RemovedLegacySetting", StringComparison.Ordinal) < 0, "removed legacy key must not be resurrected");
+
+ effectiveEnvironment = BizTalkCheckmkPulse.Setup.InstallerEngine.PrepareConfig(
+ source,
+ staged,
+ existing,
+ "PRD");
+ AssertEqual("PRD", effectiveEnvironment, "requested environment override");
+ }
+ finally
+ {
+ if (Directory.Exists(directory)) Directory.Delete(directory, true);
+ }
+ }
+
private static void SnapshotRejectsTampering()
{
WithTemporarySnapshot((path, store) =>