Harden ACC installer update and endpoint performance

This commit is contained in:
2026-08-04 10:27:10 +02:00
parent 22a4725056
commit 8888ab97c0
23 changed files with 581 additions and 77 deletions
+17 -3
View File
@@ -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,
+27 -10
View File
@@ -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. |
+26
View File
@@ -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.
+2
View File
@@ -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
+16
View File
@@ -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
+1 -1
View File
@@ -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:
+2
View File
@@ -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
@@ -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();
}
}
}
}
@@ -39,6 +39,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="InstallerEngine.cs" />
<Compile Include="MainForm.cs" />
<Compile Include="Program.cs" />
+290 -41
View File
@@ -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<string> 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<string>();
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<string> 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<string> 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);
}
}
+6 -2
View File
@@ -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[]
{
@@ -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")]
@@ -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)
+3 -2
View File
@@ -21,8 +21,9 @@
<!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. -->
<add key="EndpointDiscoveryIntervalHours" value="168" />
<add key="EndpointProbeTimeoutMilliseconds" value="3000" />
<add key="EndpointProbeMaxConcurrency" value="12" />
<add key="EndpointMaxCount" value="500" />
<add key="EndpointProbeMaxConcurrency" value="16" />
<!-- Bei 3 s Timeout und 16 parallelen Probes maximal ca. 21 s fuer 100 eindeutige Ziele. -->
<add key="EndpointMaxCount" value="100" />
<add key="QueryTimeoutSeconds" value="25" />
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
@@ -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))
@@ -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"))
@@ -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<IReadOnlyCollection<EndpointProbeResult>> 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<string, NetworkOutcome>(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<NetworkOutcome> ProbeOneAsync(EndpointCatalogEntry endpoint)
{
return string.Equals(endpoint.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)
+2
View File
@@ -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<EndpointProbeResult> Results { get; private set; }
}
+2 -2
View File
@@ -85,8 +85,8 @@ namespace BizTalkCheckmkPulse
EndpointCatalogMaxBytes = 1048576;
EndpointDiscoveryIntervalHours = 168;
EndpointProbeTimeoutMilliseconds = 3000;
EndpointProbeMaxConcurrency = 12;
EndpointMaxCount = 500;
EndpointProbeMaxConcurrency = 16;
EndpointMaxCount = 100;
}
/// <summary>
+4
View File
@@ -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;
@@ -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")]
@@ -39,6 +39,10 @@
<Project>{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}</Project>
<Name>BizTalkCheckmkPulse</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\BizTalkCheckmkPulse.Setup\BizTalkCheckmkPulse.Setup.csproj">
<Project>{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}</Project>
<Name>BizTalkCheckmkPulse.Setup</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+73 -2
View File
@@ -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,
"<configuration><appSettings>"
+ "<add key=\"EnvironmentName\" value=\"\" />"
+ "<add key=\"EndpointProbeMaxConcurrency\" value=\"16\" />"
+ "<add key=\"EndpointMaxCount\" value=\"100\" />"
+ "<add key=\"NewSetting\" value=\"new-default\" />"
+ "</appSettings></configuration>",
new UTF8Encoding(false));
File.WriteAllText(
existing,
"<configuration><appSettings>"
+ "<add key=\"EnvironmentName\" value=\"ACC\" />"
+ "<add key=\"EndpointProbeMaxConcurrency\" value=\"7\" />"
+ "<add key=\"EndpointMaxCount\" value=\"500\" />"
+ "<add key=\"RemovedLegacySetting\" value=\"legacy\" />"
+ "</appSettings></configuration>",
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) =>