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