Add PowerShell-free Windows installer

This commit is contained in:
2026-07-31 12:59:21 +02:00
parent 826d87fef7
commit f296a2de2f
19 changed files with 809 additions and 380 deletions
@@ -0,0 +1,176 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Xml;
namespace BizTalkCheckmkPulse.Setup
{
internal sealed class InstallerEngine
{
internal const string TaskName = "BizTalk Checkmk Pulse Provider";
private readonly string packageDirectory;
private readonly string installDirectory;
private readonly string runtimeDirectory;
private readonly string checkmkLocalDirectory;
public InstallerEngine(string packageDirectory)
{
this.packageDirectory = Path.GetFullPath(packageDirectory);
installDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkCheckmkPulse");
runtimeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkCheckmkPulse");
checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local");
}
public void Install(string account, string password, bool isGmsa, string environmentName, Action<string> report)
{
Validate(account, password, isGmsa, environmentName);
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier));
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")");
var sourceApplication = Path.Combine(packageDirectory, "application");
var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe");
var sourceConfig = sourceExe + ".config";
var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd");
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 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.");
Directory.CreateDirectory(checkmkLocalDirectory);
File.Copy(sourceWrapper, Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"), true);
report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
RunSelfTest(targetExe);
report("Self-Test erfolgreich: acht Checkmk-Services.");
new TaskSchedulerService().RegisterAndStart(
TaskName,
targetExe,
installDirectory,
account,
isGmsa ? null : password,
isGmsa);
report("Scheduled Task registriert: " + TaskName);
}
public void Uninstall(bool keepRuntimeData, Action<string> report)
{
new TaskSchedulerService().DeleteIfExists(TaskName);
report("Scheduled Task entfernt.");
var wrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
if (File.Exists(wrapper)) File.Delete(wrapper);
if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
if (!keepRuntimeData && Directory.Exists(runtimeDirectory)) Directory.Delete(runtimeDirectory, true);
}
private static void Validate(string account, string password, bool isGmsa, string environmentName)
{
if (string.IsNullOrWhiteSpace(account)) throw new ArgumentException("Collector-Konto fehlt.");
if (account.IndexOf('\\') <= 0 || account.EndsWith("\\", StringComparison.Ordinal))
throw new ArgumentException(@"Collector-Konto im Format DOMAIN\Benutzer eingeben, z.B. BEW\t231bizmon.");
if (isGmsa && !account.EndsWith("$", StringComparison.Ordinal))
throw new ArgumentException(@"Ein gMSA-Konto muss mit '$' enden, z.B. BEW\svc_biztalk_cmk$.");
if (!isGmsa && string.IsNullOrEmpty(password)) throw new ArgumentException("Kennwort fehlt.");
if (!new[] { "", "ACC", "DEV", "TST", "PRD" }.Contains(environmentName))
throw new ArgumentException("Ungueltige Umgebung.");
}
private static void RequireFile(string path)
{
if (!File.Exists(path))
throw new FileNotFoundException("Installationspaket ist unvollstaendig. Datei fehlt: " + path, path);
}
private static void SetEnvironment(string configPath, string environmentName)
{
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);
}
private static void ApplyDirectoryAcl(
string path,
SecurityIdentifier collectorSid,
FileSystemRights collectorRights,
FileSystemRights systemRights)
{
var inheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
var security = new DirectorySecurity();
security.SetAccessRuleProtection(true, false);
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
FileSystemRights.FullControl,
inheritance,
PropagationFlags.None,
AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
systemRights,
inheritance,
PropagationFlags.None,
AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(
collectorSid,
collectorRights,
inheritance,
PropagationFlags.None,
AccessControlType.Allow));
new DirectoryInfo(path).SetAccessControl(security);
}
private static void RunSelfTest(string executable)
{
var start = new ProcessStartInfo(executable, "--self-test")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
using (var process = Process.Start(start))
{
if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden.");
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
if (!process.WaitForExit(30000))
{
process.Kill();
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)))
throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error);
}
}
}
}