sqlinsight-pro/installer/verify-msi.py
Claude d764552c2c
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
CI / MSI-installer (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
MSI installer for Agent and Web
installer/SQLInsight.wxs (WiX 3 syntax) builds on Linux with wixl
(installer/build-msi.sh) and in Forgejo CI on every v* tag. Agent and Web
are installed self-contained as Windows services under virtual service
accounts (or a gMSA), with a settings page in the wizard or MSI
properties for silent installs; the values go to sqlinsight.ini, read
after appsettings.json. The data folder is locked to SYSTEM,
Administrators and the agent account, the event log source is
registered, the web port is opened in the firewall, and the automatic
update task is an optional feature. Upgrades happen in place, keep
edited settings and the chosen features; uninstall keeps the settings.

msi-tables.py writes what wixl lacks (component and feature conditions,
Permanent/NeverOverwrite, vital services, REG_EXPAND_SZ) and
verify-msi.py checks the result (sequencing, deferred actions, ASCII
only). test-msi-wine.sh runs install, upgrade and uninstall under Wine.
Installations made with the MSI get new versions as MSI packages run by
the update task, so Windows Installer stays in charge of the files.
The example instance is removed from the shipped appsettings.json, and
the CLI writes UTF-8 to the console.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 11:31:28 +00:00

94 lines
4.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""Checks a built MSI for the mistakes that cannot be caught without Windows (run by build-msi.sh and CI).
verify-msi.py <file.msi>
"""
import os
import subprocess
import sys
import tempfile
DEFERRED = 0x400
IN_SCRIPT_NO_IMPERSONATE = 0x800
def tables(msi):
with tempfile.TemporaryDirectory() as folder:
subprocess.run(["msidump", "-t", "-d", folder, os.path.abspath(msi)], check=True, capture_output=True, cwd=folder)
result = {}
for name in (n for n in os.listdir(folder) if n.endswith(".idt")):
with open(os.path.join(folder, name), "rb") as f:
raw = f.read()
lines = raw.decode("utf-8", errors="replace").splitlines()
header = lines[0].split("\t")
result[name[:-4]] = ([dict(zip(header, row.split("\t"))) for row in lines[3:] if row], raw)
return result
def main(msi):
t = tables(msi)
errors = []
def rows(name):
return t.get(name, ([], b""))[0]
for name, (_, raw) in t.items():
if any(b > 127 for b in raw):
errors.append(f"{name}: indeholder ikke-ASCII-tegn (databasen er codepage-neutral)")
sequence = {r["Action"]: int(r["Sequence"]) for r in rows("InstallExecuteSequence")}
actions = {r["Action"]: int(r["Type"]) for r in rows("CustomAction")}
start, end = sequence["InstallInitialize"], sequence["InstallFinalize"]
for action, kind in actions.items():
if kind & 0x3F == 50 and not kind & DEFERRED:
errors.append(f"{action}: kører et program uden at være deferred (ville køre som brugeren, uden rollback)")
if action not in sequence:
errors.append(f"{action}: er ikke i InstallExecuteSequence")
elif kind & DEFERRED and not start < sequence[action] < end:
errors.append(f"{action}: deferred, men ligger uden for InstallInitialize..InstallFinalize ({sequence[action]})")
elif kind & DEFERRED and not kind & IN_SCRIPT_NO_IMPERSONATE:
errors.append(f"{action}: deferred uden NoImpersonate (kører ikke som SYSTEM)")
def after(action, standard):
if sequence.get(action, 0) <= sequence[standard]:
errors.append(f"{action} skal ligge efter {standard}")
after("SetDataFolderAcl", "InstallServices") # the virtual service account must exist
after("CreateUpdateTask", "InstallFiles")
after("OpenFirewall", "ReplaceFirewallRule")
after("RemoveExistingProducts", "InstallExecute") # upgrade in place: settings files are kept
for action in ("DeleteUpdateTask", "CloseFirewall"):
condition = next(r["Condition"] for r in rows("InstallExecuteSequence") if r["Action"] == action)
if "UPGRADINGPRODUCTCODE" not in condition:
errors.append(f"{action}: skal ikke køre, når en opgradering fjerner den gamle version")
components = {r["Component"]: r for r in rows("Component")}
for name, bits in (("AgentSettings", 0x90), ("WebSettings", 0x90), ("AgentIni", 0x10), ("WebIni", 0x10)):
if int(components[name]["Attributes"]) & bits != bits:
errors.append(f"{name}: mangler Permanent/NeverOverwrite")
if not components["AgentServiceStart"]["Condition"]:
errors.append("AgentServiceStart: mangler betingelse")
if any(int(r["Attributes"]) & 0x100 == 0 for r in components.values()):
errors.append("Komponenter uden 64-bit-attribut")
for service in rows("ServiceInstall"):
if int(service["ErrorControl"]) & 0x8000 == 0:
errors.append(f"{service['Name']}: tjenesten er ikke Vital")
if not any(r["Feature_"] == "UpdateTask" for r in rows("Condition")):
errors.append("UpdateTask: mangler betingelsen, der bevarer opgaven ved opgradering")
if not any(r["Value"].startswith("#%") for r in rows("Registry") if r["Name"] == "EventMessageFile"):
errors.append("EventMessageFile er ikke REG_EXPAND_SZ")
if not any(r["Name"] == "Installer" and r["Value"] == "msi" for r in rows("Registry")):
errors.append("Registreringen Installer=msi mangler (opdateringer ville installere zip-pakker oven i MSI'en)")
if not any(int(r["Attributes"]) & 1 for r in rows("Upgrade") if r["ActionProperty"] == "OLDVERSIONFOUND"):
errors.append("Upgrade: MigrateFeatures mangler")
for error in errors:
print("FEJL:", error)
print(f"{os.path.basename(msi)}: {len(components)} komponenter, {len(rows('File'))} filer, {len(errors)} fejl")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))