diff --git a/AI-README.md b/AI-README.md
index fe8b32e..e13f2c6 100644
--- a/AI-README.md
+++ b/AI-README.md
@@ -2,7 +2,7 @@
## Projekt und Sicherheitsziel
-Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.3.0.
+Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.3.1.
## Installerinvarianten
@@ -35,6 +35,8 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
- Datei-Logging und optionale GUI-Logweiterleitung sind Diagnosekanäle und dürfen niemals einen fachlichen Planschritt oder dessen Fehlerbehandlung unterbrechen.
+- Ein Laufzeitlogpfad gilt erst nach erfolgreichem Create/Write/Flush/Delete-Test als verfügbar. Die Reihenfolge ProgramData, LocalAppData, EXE-`Logs`, Temp ist stabil; ein Append-Fehler muss denselben Datensatz auf dem nächsten Kandidaten erneut versuchen.
+- Fallback und Totalausfall des Dateiloggings müssen mit Pfad und Exception im Grid sichtbar sein. Jeder GUI-Start persistiert einen Verifikationsdatensatz; der WMI-freie Self-Test prüft einen vollständigen Log-Write/Read-Roundtrip.
- Scheduler-Preflight darf nur für Receive Locations mit passendem Adaptername oder `scheduler:`-URI laufen; normale Adapter dürfen keine zusätzliche Abhängigkeit erhalten.
- Adapter-DLLs dürfen nur aus explizit konfigurierten, per Registry erkannten oder konventionellen Produktverzeichnissen und nur nach vollständiger Assemblyidentitätsprüfung geladen werden. Der Toolprozess verändert niemals den GAC.
- Laufzeitlogs halten 30 Kalendertage, komprimieren abgeschlossene Tage und rehydrieren maximal 10.000 neue Einträge ins Grid. Ein Archiv-/Lesefehler darf keine BizTalk-Operation beeinflussen.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92289a7..cc58435 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,20 @@
# Changelog
+## [2.3.1] - 2026-08-26
+### Added
+- A real runtime-log write/read roundtrip to the WMI-free installer self-test and a durable startup verification record naming the active daily file.
+- Regression coverage for startup fallback, append-time failover without record loss and total storage failure diagnostics.
+- A dedicated PROD storage-validation runbook in Markdown and plain text.
+
+### Changed
+- Runtime logging now verifies actual writes and uses the deterministic fallback order ProgramData, LocalAppData, executable-local `Logs`, then Temp.
+- **Log Folder** opens the currently verified path; fallback and total failure diagnostics include attempted paths and exceptions in the Operation Log grid.
+
+### Fixed
+- Successful directory creation can no longer be mistaken for working file logging when the subsequent append is denied.
+- Runtime append failures are no longer swallowed silently; the failed record is retried on the next local candidate and a durable/visible warning is emitted.
+
## [2.3.0] - 2026-08-26
### Added
- Process-local, assembly-identity-checked resolution and preflight for ScheduledTask `scheduler:` receive locations, including automatic BizTalk/adapter directory and registry discovery plus an explicit configuration override.
diff --git a/Dokumentation.md b/Dokumentation.md
index 0de704d..7d47d0c 100644
--- a/Dokumentation.md
+++ b/Dokumentation.md
@@ -68,7 +68,8 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
- Beim Start wird geprüft, ob die Anwendung mit Administratorrechten läuft. Ohne erhöhte Rechte wird eine Fehlermeldung angezeigt und die Anwendung beendet.
- Echte Shutdown-/Restore-Aktionen verlangen erst nach Erzeugung und Speicherung des frischen Plans eine zusätzliche Bestätigung mit Zielserver, Plandatei und exakter Zahl ausführbarer Schritte.
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
-- Zusätzlich wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Nur wenn ProgramData nicht verfügbar ist, wird auf das EXE-Verzeichnis zurückgefallen.
+- Primär wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Jeder Kandidat muss einen echten Create/Write/Flush/Delete-Test bestehen. Die Fallbackreihenfolge ist LocalAppData, `Logs` unter dem EXE-Verzeichnis und Temp.
+- Jeder GUI-Start hängt einen Prüfdatensatz an die aktive Tagesdatei an. **Log Folder** öffnet diesen tatsächlich beschreibbaren Pfad; ein Fallback oder der vollständige Ausfall des Dateiloggings wird mit Pfaden und Exception im Grid angezeigt.
- Der aktuelle Tag bleibt als `.log` lesbar. Abgeschlossene Vortage werden als `.log.gz` komprimiert; aktueller Tag plus 29 Vortage bleiben erhalten.
- Beim Programmstart werden bis zu 10.000 der neuesten Einträge aus `.log` und `.log.gz` chronologisch in das Operation Log geladen.
- Der Kopfbereich zeigt den zuletzt erkannten Umgebungsstatus aus den Host Instances.
@@ -145,7 +146,7 @@ Die Standardkonfiguration enthält BizTalk Server 2020 und den im PROD-Screensho
- Emergency-Dateien: `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*`
- Diff: `diff.json`, `diff.csv`, `diff.html`
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
-- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log` und komprimierte Vortage `*.log.gz`
+- Laufzeitlogs: primär `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log`, andernfalls der im Grid und über **Log Folder** ausgewiesene verifizierte Fallback; komprimierte Vortage `*.log.gz`
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
## Fehleranalyse
diff --git a/Installation.md b/Installation.md
index 2bde1eb..91f9aae 100644
--- a/Installation.md
+++ b/Installation.md
@@ -15,6 +15,8 @@ Das Release erzeugt folgende Dateien unter `artifacts`:
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
- `BizTalkPlatformManagementTool-Setup.zip`: direkt entpackbares Installationspaket
+- `BizTalkPlatformManagementTool-2.3.1-source.zip.b64.txt`: vollständiger Quellstand als Base64-TXT
+- `BizTalkPlatformManagementTool-2.3.1-source.zip.sha256.txt`: SHA-256 des Quellarchivs
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
@@ -38,11 +40,26 @@ Der Hash aus `certutil` muss exakt dem Wert in der SHA-256-Datei entsprechen. Da
Installationsziele:
- Programm: `%ProgramFiles%\BizTalkPlatformManagementTool`
-- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
+- Laufzeitlogs primär: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`
- Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool\Setup\Uninstall.exe`
- Startmenü: `BizTalk Platform Management Tool`
+## Laufzeitlog-Ablage prüfen
+
+Der normale Laufzeitlogordner liegt nicht im Programmverzeichnis. `%ProgramData%` entspricht üblicherweise `C:\ProgramData` und ist im Explorer standardmäßig ausgeblendet. Dass unter `C:\Program Files\BizTalkPlatformManagementTool` nur EXE, Konfiguration und Installationsstatus sichtbar sind, ist daher allein noch kein Logfehler.
+
+Ab Version 2.3.1 akzeptiert die Anwendung einen Ablageort erst nach einem echten Create/Write/Flush/Delete-Test. Sie prüft in dieser Reihenfolge:
+
+1. `%ProgramData%\BizTalkPlatformManagementTool\Logs`
+2. `%LocalAppData%\BizTalkPlatformManagementTool\Logs`
+3. `%ProgramFiles%\BizTalkPlatformManagementTool\Logs`
+4. `%TEMP%\BizTalkPlatformManagementTool\Logs`
+
+Danach wird bei jedem Programmstart die Zeile `Runtime log storage verified by startup append. Active file: ...` wirklich in die Tagesdatei geschrieben und im Grid angezeigt. **Log Folder** öffnet genau den aktiven Pfad. Bei einem Fallback nennt eine gelbe Grid-Warnung den verworfenen Pfad samt Exception. Kann kein Kandidat beschrieben werden, erscheint `RUNTIME FILE LOGGING UNAVAILABLE`; vor einer echten Wartungsoperation müssen dann Berechtigungen, Datenträger oder Sicherheitssoftware geklärt werden.
+
+Der WMI-freie Installer-Self-Test schreibt und liest zusätzlich einen eigenen temporären Runtime-Logmarker. Damit wird eine Version, deren Logger grundsätzlich keine Datei erzeugen kann, bereits vor der Aktivierung abgelehnt. Die PROD-Abnahme des realen Maschinenpfads bleibt nach dem Update erforderlich.
+
## Update und Rollback
Der Installer verändert eine bestehende Installation erst nach erfolgreicher Paketprüfung:
@@ -99,7 +116,7 @@ Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale
## Emergency Restore nach einem Teilabbruch
-Version 2.3.0 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
+Version 2.3.1 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
Für ScheduledTask-Receive-Locations enthält die installierte EXE-Konfiguration `AdapterAssemblySearchPaths`. Normalerweise ist keine Anpassung nötig: BizTalk- und ScheduledTask-Installationsverzeichnisse werden zusätzlich automatisch erkannt. Nur bei einem abweichenden lokalen Installationspfad darf der vorhandene Wert um diesen Pfad ergänzt werden. Die Assembly muss aus genau dieser BizTalk-/Adapterinstallation stammen; das Tool verändert den GAC nicht.
diff --git a/PROD-Laufzeitlog-Ablage-2.3.1.txt b/PROD-Laufzeitlog-Ablage-2.3.1.txt
new file mode 100644
index 0000000..8f94ba2
--- /dev/null
+++ b/PROD-Laufzeitlog-Ablage-2.3.1.txt
@@ -0,0 +1,38 @@
+PROD-RUNBOOK: LAUFZEITLOG-ABLAGE - VERSION 2.3.1
+Stand: 2026-08-26
+
+EINORDNUNG
+Der Installationsordner C:\Program Files\BizTalkPlatformManagementTool ist nicht
+der primaere Logordner. Dieser liegt unter
+%ProgramData%\BizTalkPlatformManagementTool\Logs. ProgramData ist im Explorer
+normalerweise ausgeblendet. Im Program-Files-Ordner wird daher im Normalfall
+kein Log erwartet.
+
+FIX IN 2.3.1
+- Echter Create/Write/Flush/Delete-Test vor Auswahl eines Logpfads.
+- Reihenfolge: ProgramData, LocalAppData, EXE-Unterordner Logs, Temp.
+- Append-Fehler: derselbe Datensatz wird im naechsten Kandidaten wiederholt.
+- Fallback/Totalausfall erscheinen mit Pfad und Exception im Grid.
+- Jeder GUI-Start schreibt einen Startup-Verifikationseintrag in Datei und Grid.
+- Der Installer-Self-Test prueft einen temporaeren Write/Read-Roundtrip.
+- 30 Kalendertage, abgeschlossene Tage GZip, maximal 10.000 Grid-Eintraege.
+
+PROD-ABNAHME
+1. Setup 2.3.1 als Administrator installieren/aktualisieren.
+2. Erfolgreichen Ziel-Self-Test im Setup-Log pruefen.
+3. Tool starten, noch keine reale BizTalk-Operation ausfuehren.
+4. Gruenen Eintrag "Runtime log storage verified by startup append" pruefen.
+5. Gelbe Fallbackwarnung bzw. RUNTIME FILE LOGGING UNAVAILABLE ausschliessen.
+6. Log Folder oeffnen. Genannte Tagesdatei muss existieren und den Eintrag
+ dieses Starts enthalten.
+7. Diagnose ausfuehren, Tool schliessen und neu starten.
+8. Diagnosehistorie muss im Grid und in der Tagesdatei vorhanden sein.
+9. Erst danach ScheduledTask zuerst als Dry-run und nach Planreview real testen.
+
+BEI TOTALAUSFALL
+Keine reale Wartungsoperation beginnen. Vollstaendigen Grid-Eintrag, Benutzer,
+freien Speicher, Endpoint-Security-Ereignisse und ACLs der vier Kandidaten
+sichern. ACLs nicht eigenmaechtig aufweiten.
+
+DETAILFASSUNG
+docs\PROD-Laufzeitlog-Ablage-2026-08-26.md
diff --git a/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt b/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt
index 45c3f53..9e1913b 100644
--- a/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt
+++ b/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt
@@ -1,6 +1,6 @@
PROD-RUNBOOK: SCHEDULEDTASK-STEUERUNG UND PERSISTENTES LAUFZEITLOGGING
Stand: 2026-08-26
-Zielversion: 2.3.0
+Zielversion: 2.3.1
Adapter: BizTalk ScheduledTask Adapter 7.0.2
Adapterpfad: C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2
@@ -8,14 +8,15 @@ URSACHE
MSBTS_ReceiveLocation.Enable/Disable validiert die Transportdaten. Der Adapter
benoetigt dabei Microsoft.BizTalk.Scheduler.dll (beobachtet: Version 3.13.0.0).
Die Datei liegt in der BizTalk-Installation, ist aber nicht zwingend im GAC
-aufloesbar. Version 2.3.0 laedt sie nach Assemblyidentitaetspruefung nur in den
+aufloesbar. Version 2.3.1 laedt sie nach Assemblyidentitaetspruefung nur in den
laufenden Toolprozess. Das Tool kopiert keine DLL und aendert den GAC nicht.
VORBEREITUNG
1. before.json, Reports und vorhandene Logs extern sichern.
-2. Setup 2.3.0 als Administrator installieren/aktualisieren.
-3. setup_version=2.3.0.0 und erfolgreichen Ziel-Self-Test pruefen.
-4. Tool als Administrator starten, Log Folder oeffnen.
+2. Setup 2.3.1 als Administrator installieren/aktualisieren.
+3. setup_version=2.3.1.0 und erfolgreichen Ziel-Self-Test pruefen.
+4. Tool als Administrator starten, gruenen Startup-Log-Pruefeintrag kontrollieren,
+ dann Log Folder oeffnen. Normalfall: %ProgramData%\BizTalkPlatformManagementTool\Logs.
5. Im lokalen BizTalk-Ordner Microsoft.BizTalk.Scheduler.dll pruefen.
6. Nur bei Sonderpfad AdapterAssemblySearchPaths in der EXE.config erweitern.
Mehrere Pfade mit Semikolon trennen. Keine fremde/alte DLL kopieren.
@@ -46,6 +47,13 @@ LOGGING-TEST
6. Aufbewahrung: aktueller Tag plus 29 Vortage.
7. Grid: neueste maximal 10.000 Eintraege; Dateien bleiben vollstaendig.
+HINWEIS ZUM PFAD
+ProgramData ist nicht Program Files und im Explorer normalerweise ausgeblendet.
+Unter Program Files ist im Normalfall kein Log sichtbar. Die Reihenfolge lautet:
+ProgramData, LocalAppData, EXE-Unterordner Logs, Temp. Fallback oder Totalausfall
+muss ab 2.3.1 mit Pfad und Exception im Grid erscheinen. Die Tagesdatei muss den
+bei jedem Start geschriebenen Verifikationseintrag enthalten.
+
FEHLERFALL SICHERN
1. Aktuelle .log und relevante .log.gz aus Log Folder.
2. shutdown-plan.json oder restore-plan.json.
@@ -65,7 +73,7 @@ ABNAHME
- Teilfehler bleibt Failed, spaetere unabhaengige Schritte laufen weiter.
- Historie erscheint nach Neustart im Grid.
- Vortage komprimiert, 30 Kalendertage aufbewahrt.
-- Setup/Tool/Reports zeigen 2.3.0 bzw. 2.3.0-net461.
+- Setup/Tool/Reports zeigen 2.3.1 bzw. 2.3.1-net461.
Detailfassung:
docs\PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
diff --git a/README.md b/README.md
index a37668a..1b1a95f 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
- Dry-run mode enabled by default
- WMI access through `root\MicrosoftBizTalkServer`
- Startup check for administrator rights
-- Detailed operation logging in the GUI and daily rolling log files under ProgramData
+- Detailed operation logging in the GUI and daily rolling log files in a write-verified local directory
- Automatic operation-log restoration into the grid after an application restart
- Thirty-day runtime-log retention with GZip compression for completed daily logs
- Process-local ScheduledTask adapter dependency resolution for `scheduler:` receive locations without modifying the GAC
@@ -83,9 +83,13 @@ Orchestrations that were `Bound` are deliberately left unchanged during restore
- Timestamped `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*` and `emergency-restore-diff-*` files
- `diff.json`, `diff.csv`, `diff.html`
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
-- Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
+- Runtime logs primarily under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
-The current day remains a plain `BizTalkPlatformManagementTool-yyyy-MM-dd.log`. Completed daily logs are compressed to `.log.gz`; the current day plus the previous 29 calendar days are retained. Older records are removed on startup. Up to the newest 10,000 retained entries are automatically restored from plain and compressed files into the Operation Log grid after a restart. If ProgramData is unexpectedly unavailable, logging falls back to the executable directory.
+The current day remains a plain `BizTalkPlatformManagementTool-yyyy-MM-dd.log`. Completed daily logs are compressed to `.log.gz`; the current day plus the previous 29 calendar days are retained. Older records are removed on startup. Up to the newest 10,000 retained entries are automatically restored from plain and compressed files into the Operation Log grid after a restart.
+
+At every start, the application performs an actual create/write/flush/delete probe and then appends a visible startup verification record. The ordered locations are ProgramData, the current account's LocalAppData, `Logs` below the executable directory, and Temp. **Log Folder** opens the path that really accepted the write. A fallback or complete storage failure is shown with attempted paths and exception details in the Operation Log; it is never silently ignored.
+
+`C:\Program Files\BizTalkPlatformManagementTool` therefore normally contains no log file. ProgramData is a separate, normally hidden Windows directory. An install-local `Logs` subdirectory appears only when both ProgramData and LocalAppData were rejected and Program Files itself permits the fallback write.
## Troubleshooting
@@ -97,6 +101,8 @@ For a ScheduledTask receive location, the operation plan retains adapter name an
Error records in the grid, execution report and file log include exception type, HRESULT, complete inner-exception chain, available Fusion loader information and stack trace. For the PROD validation and support bundle, follow [ScheduledTask control and runtime logging runbook](docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md).
+For the specific PROD finding "no logs visible below Program Files" and the version 2.3.1 storage verification, follow [PROD runtime-log storage validation](docs/PROD-Laufzeitlog-Ablage-2026-08-26.md).
+
Snapshot and plan JSON files are written as UTF-8 without BOM. Loading is tolerant of existing files that contain a UTF-8 BOM or a visible BOM marker from previous encoding conversions.
JSON snapshots and plans are written through a same-directory temporary file and atomic replacement. Snapshot comparison keys artifacts by application plus name, preventing collisions between equal artifact names in different applications. CSV fields that could be interpreted as spreadsheet formulas are neutralized.
@@ -127,5 +133,6 @@ Targeted German inline comments explain non-obvious operational decisions such a
- [Optional all-users desktop shortcut stability](docs/Installer-Optionale-Desktopverknuepfung-2026-08-24.md)
- [ACC runtime shutdown incident and recovery fix](docs/ACC-Runtime-Shutdown-Exception-2026-08-19.md)
- [PROD ScheduledTask control and runtime logging runbook](docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md)
+- [PROD runtime-log storage validation and 2.3.1 fix](docs/PROD-Laufzeitlog-Ablage-2026-08-26.md)
- [AI maintainer handoff](AI-README.md)
- [References](REFERENCES.md)
diff --git a/docs/PROD-Laufzeitlog-Ablage-2026-08-26.md b/docs/PROD-Laufzeitlog-Ablage-2026-08-26.md
new file mode 100644
index 0000000..c006e81
--- /dev/null
+++ b/docs/PROD-Laufzeitlog-Ablage-2026-08-26.md
@@ -0,0 +1,44 @@
+# PROD-Runbook: Laufzeitlog-Ablage und Version 2.3.1
+
+**Stand:** 2026-08-26
+**Zielversion:** 2.3.1
+
+## Einordnung des Screenshots
+
+Der gezeigte Ordner `C:\Program Files\BizTalkPlatformManagementTool` ist der Programmordner. Der primäre Laufzeitlogordner ist dagegen `%ProgramData%\BizTalkPlatformManagementTool\Logs`, üblicherweise `C:\ProgramData\BizTalkPlatformManagementTool\Logs`. `C:\ProgramData` ist im Explorer standardmäßig ausgeblendet. Im Programmordner wird deshalb im Normalfall kein Log erwartet.
+
+Die frühere Implementierung konnte dennoch einen echten Fehler verdecken: Sie prüfte nur, ob sich der Ordner anlegen ließ, und ignorierte einen Fehler beim nachfolgenden `AppendAllText`. Damit konnte das Grid funktionieren, obwohl kein dauerhaftes Log geschrieben wurde.
+
+## Fix in 2.3.1
+
+- Jeder Kandidat muss einen echten Create/Write/Flush/Delete-Test bestehen.
+- Die Reihenfolge ist ProgramData, LocalAppData, EXE-Unterordner `Logs`, Temp.
+- Ein späterer Append-Fehler wiederholt denselben Datensatz auf dem nächsten Kandidaten.
+- Fallback und vollständiger Ausfall erscheinen mit Pfad und Exception im Grid.
+- Jeder GUI-Start schreibt `Runtime log storage verified by startup append. Active file: ...` in Grid und Tagesdatei.
+- Der Installer-Self-Test prüft einen vollständigen temporären Write/Read-Roundtrip.
+- Tagesdateien werden nach Abschluss komprimiert; aktueller Tag plus 29 Vortage bleiben erhalten. Bis zu 10.000 Einträge werden nach Neustart ins Grid geladen.
+
+## PROD-Abnahme nach Update
+
+1. Setup 2.3.1 als Administrator ausführen und erfolgreichen Ziel-Self-Test prüfen.
+2. Tool als Administrator starten, noch keine reale BizTalk-Operation ausführen.
+3. Im Grid den grünen Startup-Verifikationseintrag prüfen und seinen vollständigen Dateipfad notieren.
+4. Auf eine gelbe Pfad-Fallbackwarnung oder `RUNTIME FILE LOGGING UNAVAILABLE` achten.
+5. **Log Folder** öffnen und prüfen, dass exakt die genannte Tagesdatei existiert.
+6. Datei öffnen und den Startup-Verifikationseintrag dieses Starts prüfen.
+7. **Diagnose** ausführen, Tool regulär schließen und neu starten.
+8. Prüfen, dass Diagnoseeinträge wieder im Grid erscheinen und auch in der Tagesdatei stehen.
+9. Erst danach den ScheduledTask-Test zunächst als Dry-run und nach Planreview real durchführen.
+
+Bei `RUNTIME FILE LOGGING UNAVAILABLE` keine reale Wartungsoperation beginnen. Screenshot des vollständigen Grid-Eintrags, aktiven Benutzer, freien Speicher, Endpoint-Security-Ereignisse und die ACLs der vier genannten Kandidaten sichern. Keine ACL eigenmächtig aufweiten; die Ursache mit dem Serverbetrieb klären.
+
+## Supportpaket
+
+- Aktuelle `.log` und relevante `.log.gz` aus **Log Folder**
+- Screenshot der Startup-Verifikation beziehungsweise vollständigen Speicherwarnung
+- `shutdown-plan.json`/`restore-plan.json` und passender `*-result.json`-Report
+- `before.json`, Nachher-Snapshot und unveränderte EXE-Konfiguration
+- Setup-Log mit `setup_version=2.3.1.0` und erfolgreichem Ziel-Self-Test
+
+Die Windows-/BizTalk-/PROD-Prüfung bleibt die endgültige Abnahme; die portable Regressionstoolchain simuliert die drei Speicherfehlerpfade ohne BizTalk.
diff --git a/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md b/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
index 65eb8b6..fec6574 100644
--- a/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
+++ b/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
@@ -2,7 +2,7 @@
**Stand:** 2026-08-26
-**Zielversion:** 2.3.0
+**Zielversion:** 2.3.1
**Betroffener Adapter:** BizTalk ScheduledTask Adapter 7.0.2
@@ -12,14 +12,14 @@
`MSBTS_ReceiveLocation.Enable` und `.Disable` validieren die Transportdaten einer ScheduledTask-Receive-Location. Der Adapter benötigt dafür `Microsoft.BizTalk.Scheduler.dll`. Diese BizTalk-Assembly liegt im BizTalk-Installationsverzeichnis, ist aber nicht in jeder Umgebung im GAC auflösbar. Der Fehler lautete deshalb sinngemäß `Could not load file or assembly Microsoft.BizTalk.Scheduler, Version=3.13.0.0`.
-Version 2.3.0 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-URI. Unmittelbar vor der WMI-Mutation sucht sie die Assembly in der lokalen BizTalk-/Adapterinstallation, prüft ihre .NET-Assemblyidentität und lädt sie nur für den laufenden Toolprozess. Weitere angeforderte Abhängigkeiten werden nach derselben Identitätsprüfung aufgelöst. Das Tool kopiert keine Fremd-DLL, registriert nichts und verändert den GAC nicht.
+Version 2.3.1 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-URI. Unmittelbar vor der WMI-Mutation sucht sie die Assembly in der lokalen BizTalk-/Adapterinstallation, prüft ihre .NET-Assemblyidentität und lädt sie nur für den laufenden Toolprozess. Weitere angeforderte Abhängigkeiten werden nach derselben Identitätsprüfung aufgelöst. Das Tool kopiert keine Fremd-DLL, registriert nichts und verändert den GAC nicht.
## Vorbereitung
1. Vorhandene `before.json`, Ergebnisdateien und Laufzeitlogs außerhalb des Installationsordners sichern.
-2. Setup 2.3.0 als Administrator installieren beziehungsweise aktualisieren.
-3. Im Setup-Log `setup_version=2.3.0.0` und erfolgreichen Ziel-Self-Test prüfen.
-4. Tool als Administrator starten und mit **Log Folder** `%ProgramData%\BizTalkPlatformManagementTool\Logs` öffnen.
+2. Setup 2.3.1 als Administrator installieren beziehungsweise aktualisieren.
+3. Im Setup-Log `setup_version=2.3.1.0` und erfolgreichen Ziel-Self-Test prüfen.
+4. Tool als Administrator starten, den grünen Eintrag `Runtime log storage verified by startup append` prüfen und mit **Log Folder** den dort genannten aktiven Pfad öffnen. Normalfall ist `%ProgramData%\BizTalkPlatformManagementTool\Logs`.
5. Prüfen, dass der lokale BizTalk-Installationsordner `Microsoft.BizTalk.Scheduler.dll` enthält.
6. Nur wenn BizTalk oder der Adapter abweichend installiert wurde: `AdapterAssemblySearchPaths` in `BizTalkPlatformManagementTool.exe.config` um den vorhandenen lokalen Ordner ergänzen. Mehrere Pfade werden mit Semikolon getrennt. Keine DLL aus ACC, einer alten BizTalk-Version oder einem Downloadordner kopieren.
@@ -48,11 +48,13 @@ Version 2.3.0 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-UR
3. Das Operation Log muss die Einträge des vorherigen Prozesses mit vollständigem Datum und Uhrzeit wieder anzeigen.
4. **Clear** leert nur das Grid. Nach erneutem Programmstart wird die aufbewahrte Historie wieder geladen.
5. **Log Folder** muss den lokalen Ordner öffnen.
-6. Die aktuelle Tagesdatei heißt `BizTalkPlatformManagementTool-yyyy-MM-dd.log`.
+6. Die aktuelle Tagesdatei heißt `BizTalkPlatformManagementTool-yyyy-MM-dd.log` und enthält den bei diesem Start erzeugten Verifikationseintrag.
7. Nach dem ersten Start an einem Folgetag wird der abgeschlossene Vortag zu `.log.gz` komprimiert.
8. Aktueller Tag plus 29 Vortage bleiben erhalten. Einträge außerhalb dieses Fensters werden beim Start entfernt.
9. Das Grid lädt höchstens die neuesten 10.000 Einträge; die 30-Tage-Dateien bleiben davon unabhängig vollständig erhalten.
+ProgramData ist ein separates, üblicherweise ausgeblendetes Windows-Verzeichnis. Im Installationsordner unter Program Files wird im Normalfall kein Log erwartet. Version 2.3.1 prüft ProgramData durch einen echten Schreibzugriff; danach folgen LocalAppData, ein `Logs`-Unterordner im Installationsverzeichnis und Temp. Jeder Fallback oder vollständige Ausfall muss im Grid mit dem betroffenen Pfad und der Exception sichtbar sein.
+
## Fehlerfall und Supportpaket
Bei einem Fehler keine Assembly austauschen und den GAC nicht spontan verändern. Folgende unveränderte Evidenz sichern:
@@ -75,6 +77,6 @@ Der Fehlerdatensatz enthält Exceptiontyp, HRESULT, innere Ausnahmen, vorhandene
- Ergebnisreport und Nachher-Snapshot werden auch bei einem Teilfehler soweit möglich geschrieben.
- Historisches Log erscheint nach Neustart wieder im Grid.
- Vortagslogs werden komprimiert und exakt 30 Kalendertage aufbewahrt.
-- Setup, Tool und Ergebnisdateien melden Version 2.3.0 beziehungsweise `2.3.0-net461`.
+- Setup, Tool und Ergebnisdateien melden Version 2.3.1 beziehungsweise `2.3.1-net461`.
Die lokale Mono-Toolchain prüft Resolverlogik, Identitätsgrenze, Persistenz, Kompression und Aufbewahrung ohne BizTalk. Die endgültige Freigabe erfordert diesen realen Windows-/BizTalk-/PROD-Test.
diff --git a/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs b/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs
index c94bae8..40fc551 100644
--- a/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs
+++ b/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
private const string ProductName = "BizTalk Platform Management Tool";
/// Aktuelle Produktversion des Installers und Uninstall-Eintrags.
- private const string ProductVersion = "2.3.0";
+ private const string ProductVersion = "2.3.1";
///
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
diff --git a/src/BizTalkPlatformManagementTool.Setup/MainForm.cs b/src/BizTalkPlatformManagementTool.Setup/MainForm.cs
index 51c7ce0..f2989cf 100644
--- a/src/BizTalkPlatformManagementTool.Setup/MainForm.cs
+++ b/src/BizTalkPlatformManagementTool.Setup/MainForm.cs
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{
AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
- Text = "BizTalk Platform Management Tool 2.3.0"
+ Text = "BizTalk Platform Management Tool 2.3.1"
});
root.Controls.Add(new Label
{
diff --git a/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs b/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs
index 651ca73..98d30af 100644
--- a/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs
+++ b/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
[assembly: ComVisible(false)]
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
-[assembly: AssemblyVersion("2.3.0.0")]
-[assembly: AssemblyFileVersion("2.3.0.0")]
+[assembly: AssemblyVersion("2.3.1.0")]
+[assembly: AssemblyFileVersion("2.3.1.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
diff --git a/src/BizTalkPlatformManagementTool.Setup/app.manifest b/src/BizTalkPlatformManagementTool.Setup/app.manifest
index 19abc9c..e114ec1 100644
--- a/src/BizTalkPlatformManagementTool.Setup/app.manifest
+++ b/src/BizTalkPlatformManagementTool.Setup/app.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs b/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs
index 7b6f1b5..e77903e 100644
--- a/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs
+++ b/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
-[assembly: AssemblyVersion("2.3.0.0")]
-[assembly: AssemblyFileVersion("2.3.0.0")]
+[assembly: AssemblyVersion("2.3.1.0")]
+[assembly: AssemblyFileVersion("2.3.1.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
diff --git a/src/BizTalkPlatformManagementTool/RuntimeSelfTest.cs b/src/BizTalkPlatformManagementTool/RuntimeSelfTest.cs
index 944fcd7..a0f5ad7 100644
--- a/src/BizTalkPlatformManagementTool/RuntimeSelfTest.cs
+++ b/src/BizTalkPlatformManagementTool/RuntimeSelfTest.cs
@@ -37,6 +37,16 @@ namespace BizTalkPlatformManagementTool
SnapshotStore.SaveSnapshotSet(Path.Combine(directory, "before.json"), loaded);
SnapshotStore.SaveDiffSet(Path.Combine(directory, "diff.json"), diff);
+ var runtimeLogDirectory = Path.Combine(directory, "runtime-logs");
+ var runtimeLogger = new OperationLogger(null, runtimeLogDirectory);
+ runtimeLogger.Info("runtime log self-test marker");
+ if (!runtimeLogger.IsFileLoggingAvailable
+ || !File.Exists(runtimeLogger.LogFilePath)
+ || !runtimeLogger.ReadRecentEntries(10).Any(x => x.Message == "runtime log self-test marker"))
+ {
+ throw new InvalidOperationException("Runtime log write/read self-test failed.");
+ }
+
var operationService = new BizTalkOperationService(null);
var emergencyPlan = operationService.CreateEmergencyRestorePlan(loaded, loaded.Server);
if (emergencyPlan.Steps.Count == 0 || emergencyPlan.Steps[0].Kind != "WindowsService" || emergencyPlan.Steps[0].Name != "ENTSSO")
diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
index 13be557..f39b3fa 100644
--- a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
+++ b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
///
/// Current tool version written into generated snapshots.
///
- public const string Version = "2.3.0-net461";
+ public const string Version = "2.3.1-net461";
///
/// Fallback application name used when WMI does not expose an application property.
diff --git a/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs
index 25d950f..2db6077 100644
--- a/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs
+++ b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs
@@ -89,16 +89,28 @@ namespace BizTalkPlatformManagementTool.Services
private readonly Action _sink;
///
- /// Directory where daily log files are written.
+ /// Ordered local directories available for primary and fallback logging.
///
- private readonly string _logDirectory;
+ private readonly List _candidateDirectories;
+
+ /// Index of the currently active writable directory, or minus one.
+ private int _activeDirectoryIndex;
+
+ /// Directory where daily log files are currently written.
+ private string _logDirectory;
+
+ /// Latest actionable storage warning for the operator.
+ private string _storageWarning;
+
+ /// Last storage notice already forwarded to the UI sink.
+ private string _reportedStorageNotice;
///
- /// Initializes a new logger that writes beside the executable.
+ /// Initializes a logger that verifies ProgramData and ordered local fallback directories.
///
/// Optional callback that receives entries for display.
public OperationLogger(Action sink)
- : this(sink, ResolveLogDirectory())
+ : this(sink, BuildLogDirectoryCandidates())
{
}
@@ -106,15 +118,42 @@ namespace BizTalkPlatformManagementTool.Services
/// Optional callback that receives new entries.
/// Directory used for plain and compressed logs.
internal OperationLogger(Action sink, string logDirectory)
+ : this(sink, new[] { logDirectory })
+ {
+ }
+
+ /// Initializes a logger with ordered primary and fallback directories.
+ /// Optional callback that receives new entries.
+ /// Ordered local directories to verify.
+ internal OperationLogger(Action sink, IEnumerable candidateDirectories)
{
_sink = sink;
- _logDirectory = Path.GetFullPath(logDirectory);
- Directory.CreateDirectory(_logDirectory);
- MaintainLogs(DateTime.Now.Date);
+ _candidateDirectories = NormalizeDirectories(candidateDirectories).ToList();
+ _activeDirectoryIndex = -1;
+ SelectInitialLogDirectory();
+ if (_logDirectory != null)
+ {
+ MaintainLogs(DateTime.Now.Date);
+ }
}
/// Gets the directory containing active and compressed runtime logs.
- public string LogDirectory { get { return _logDirectory; } }
+ public string LogDirectory
+ {
+ get { lock (FileLock) { return _logDirectory ?? string.Empty; } }
+ }
+
+ /// Gets whether a daily runtime log can currently be written.
+ public bool IsFileLoggingAvailable
+ {
+ get { lock (FileLock) { return _activeDirectoryIndex >= 0 && !string.IsNullOrWhiteSpace(_logDirectory); } }
+ }
+
+ /// Gets the latest fallback or total-storage-failure diagnostic.
+ public string StorageWarning
+ {
+ get { lock (FileLock) { return _storageWarning; } }
+ }
///
/// Gets the path of the daily log file for the current date.
@@ -123,7 +162,12 @@ namespace BizTalkPlatformManagementTool.Services
{
get
{
- return Path.Combine(_logDirectory, LogFilePrefix + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + LogFileExtension);
+ lock (FileLock)
+ {
+ return string.IsNullOrWhiteSpace(_logDirectory)
+ ? string.Empty
+ : BuildLogFilePath(_logDirectory, DateTime.Now);
+ }
}
}
@@ -141,9 +185,14 @@ namespace BizTalkPlatformManagementTool.Services
}
var entries = new List();
+ var directory = LogDirectory;
+ if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
+ {
+ return entries;
+ }
try
{
- var files = Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension + "*")
+ var files = Directory.GetFiles(directory, LogFilePrefix + "*" + LogFileExtension + "*")
.Where(IsSupportedLogFile)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToArray();
@@ -213,13 +262,30 @@ namespace BizTalkPlatformManagementTool.Services
Message = message
};
- WriteToFile(entry);
+ var storageNotice = WriteToFile(entry);
+ DeliverToSink(entry);
+ if (!string.IsNullOrWhiteSpace(storageNotice))
+ {
+ var warning = new LogEntry
+ {
+ Timestamp = DateTime.Now,
+ Level = LogLevel.Warning,
+ Message = storageNotice
+ };
+ WriteToFile(warning);
+ DeliverToSink(warning);
+ }
+ }
+
+ /// Forwards one record to the optional GUI without affecting runtime work.
+ /// The record to display.
+ private void DeliverToSink(LogEntry entry)
+ {
if (_sink == null)
{
return;
}
-
try
{
_sink(entry);
@@ -242,39 +308,79 @@ namespace BizTalkPlatformManagementTool.Services
/// Appends one entry to the current daily log file.
///
/// The entry to write.
- private void WriteToFile(LogEntry entry)
+ /// A new operator-visible storage notice, or null.
+ private string WriteToFile(LogEntry entry)
{
- try
- {
- var line = string.Format(
- CultureInfo.InvariantCulture,
- "[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
- entry.Timestamp,
- entry.Level.ToString().ToUpperInvariant(),
- (entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n"),
- Environment.NewLine);
+ var line = string.Format(
+ CultureInfo.InvariantCulture,
+ "[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
+ entry.Timestamp,
+ entry.Level.ToString().ToUpperInvariant(),
+ (entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n"),
+ Environment.NewLine);
- lock (FileLock)
- {
- File.AppendAllText(LogFilePath, line);
- }
- }
- catch
+ lock (FileLock)
{
- // Ein Logfehler darf niemals eine fachliche BizTalk-Operation abbrechen.
+ var failures = new List();
+ var previousDirectory = _logDirectory;
+ var startIndex = _activeDirectoryIndex >= 0 ? _activeDirectoryIndex : 0;
+ for (var index = startIndex; index < _candidateDirectories.Count; index++)
+ {
+ var candidate = _candidateDirectories[index];
+ string error;
+ if (!TryAppend(candidate, line, out error))
+ {
+ failures.Add(candidate + " => " + error);
+ continue;
+ }
+
+ _activeDirectoryIndex = index;
+ _logDirectory = candidate;
+ if (!string.Equals(previousDirectory, candidate, StringComparison.OrdinalIgnoreCase))
+ {
+ _storageWarning = "Runtime log path switched to writable fallback '" + candidate
+ + "'. Failed path(s): " + string.Join(" | ", failures.ToArray());
+ return TakeUnreportedStorageNotice(_storageWarning);
+ }
+ return null;
+ }
+
+ _activeDirectoryIndex = -1;
+ _logDirectory = null;
+ _storageWarning = "RUNTIME FILE LOGGING UNAVAILABLE. No candidate directory accepted an append. Attempted: "
+ + string.Join(" | ", failures.ToArray()) + ". Operations continue and remain visible in the grid, but no durable runtime log is being written.";
+ return TakeUnreportedStorageNotice(_storageWarning);
}
}
+ /// Returns a storage notice only once per distinct failure state.
+ /// The current diagnostic.
+ /// The notice when it has not been reported before; otherwise null.
+ private string TakeUnreportedStorageNotice(string notice)
+ {
+ if (string.Equals(_reportedStorageNotice, notice, StringComparison.Ordinal))
+ {
+ return null;
+ }
+ _reportedStorageNotice = notice;
+ return notice;
+ }
+
///
/// Compresses completed daily logs and removes all records outside retention.
///
/// The current local date.
internal void MaintainLogs(DateTime today)
{
+ var directory = LogDirectory;
+ if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
+ {
+ return;
+ }
try
{
var cutoff = today.Date.AddDays(-(RetentionDays - 1));
- foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension + "*"))
+ foreach (var file in Directory.GetFiles(directory, LogFilePrefix + "*" + LogFileExtension + "*"))
{
DateTime fileDate;
if (!TryGetLogDate(file, out fileDate))
@@ -409,23 +515,144 @@ namespace BizTalkPlatformManagementTool.Services
return DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
}
- ///
- /// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
- ///
- /// Ein verwendbares Verzeichnis für die täglichen Laufzeitlogs.
- private static string ResolveLogDirectory()
+ /// Selects the first directory that passes a real create/write/delete probe.
+ private void SelectInitialLogDirectory()
{
- var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
- var preferred = Path.Combine(commonData, "BizTalkPlatformManagementTool", "Logs");
+ var failures = new List();
+ for (var index = 0; index < _candidateDirectories.Count; index++)
+ {
+ string error;
+ if (!TryVerifyWritable(_candidateDirectories[index], out error))
+ {
+ failures.Add(_candidateDirectories[index] + " => " + error);
+ continue;
+ }
+ _activeDirectoryIndex = index;
+ _logDirectory = _candidateDirectories[index];
+ if (index > 0)
+ {
+ _storageWarning = "Primary runtime log path is not writable. Using verified fallback '"
+ + _logDirectory + "'. Failed path(s): " + string.Join(" | ", failures.ToArray());
+ }
+ return;
+ }
+
+ _storageWarning = "RUNTIME FILE LOGGING UNAVAILABLE. No candidate directory passed the startup write probe. Attempted: "
+ + (failures.Count == 0 ? "(none)" : string.Join(" | ", failures.ToArray()))
+ + ". Operations remain visible in the grid, but no durable runtime log is being written.";
+ }
+
+ /// Builds primary and local fallback directories in deterministic order.
+ /// ProgramData, LocalAppData, executable-local and Temp candidates.
+ private static IEnumerable BuildLogDirectoryCandidates()
+ {
+ yield return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
+ "BizTalkPlatformManagementTool",
+ "Logs");
+ yield return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "BizTalkPlatformManagementTool",
+ "Logs");
+ yield return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
+ yield return Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "Logs");
+ }
+
+ /// Normalizes and de-duplicates candidate paths without requiring existence.
+ /// Raw ordered paths.
+ /// Safe absolute unique paths.
+ private static IEnumerable NormalizeDirectories(IEnumerable directories)
+ {
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var directory in directories ?? Enumerable.Empty())
+ {
+ if (string.IsNullOrWhiteSpace(directory))
+ {
+ continue;
+ }
+ string fullPath;
+ try
+ {
+ fullPath = Path.GetFullPath(directory.Trim()).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+ catch
+ {
+ continue;
+ }
+ if (seen.Add(fullPath))
+ {
+ yield return fullPath;
+ }
+ }
+ }
+
+ /// Performs a real write probe and removes the temporary probe file.
+ /// Candidate directory.
+ /// Detailed failure when the probe fails.
+ /// True only after bytes were created and flushed successfully.
+ private static bool TryVerifyWritable(string directory, out string error)
+ {
+ var probe = Path.Combine(directory, ".runtime-log-write-probe-" + Guid.NewGuid().ToString("N") + ".tmp");
try
{
- Directory.CreateDirectory(preferred);
- return preferred;
+ Directory.CreateDirectory(directory);
+ using (var stream = new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ {
+ stream.WriteByte(0x42);
+ stream.Flush();
+ }
+ File.Delete(probe);
+ error = null;
+ return true;
}
- catch
+ catch (Exception ex)
{
- return AppDomain.CurrentDomain.BaseDirectory;
+ error = ex.GetType().Name + ": " + ex.Message;
+ try
+ {
+ if (File.Exists(probe))
+ {
+ File.Delete(probe);
+ }
+ }
+ catch
+ {
+ // Probe cleanup is best-effort and the original write error remains authoritative.
+ }
+ return false;
}
}
+
+ /// Appends one physical line to a candidate daily log.
+ /// Candidate directory.
+ /// Serialized log line.
+ /// Detailed append failure.
+ /// True after the append completed.
+ private static bool TryAppend(string directory, string line, out string error)
+ {
+ try
+ {
+ Directory.CreateDirectory(directory);
+ File.AppendAllText(BuildLogFilePath(directory, DateTime.Now), line, new UTF8Encoding(false));
+ error = null;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ error = ex.GetType().Name + ": " + ex.Message;
+ return false;
+ }
+ }
+
+ /// Builds the stable daily file path for a directory and local date.
+ /// Active directory.
+ /// Timestamp whose local date selects the file.
+ /// Full daily log path.
+ private static string BuildLogFilePath(string directory, DateTime timestamp)
+ {
+ return Path.Combine(
+ directory,
+ LogFilePrefix + timestamp.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + LogFileExtension);
+ }
}
}
diff --git a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
index 37db196..baaa847 100644
--- a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
+++ b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
@@ -155,7 +155,18 @@ namespace BizTalkPlatformManagementTool.Ui
BuildUi();
FormClosing += MainFormClosing;
LoadLogHistory();
- _logger.Info("Log file: " + _logger.LogFilePath);
+ if (!string.IsNullOrWhiteSpace(_logger.StorageWarning))
+ {
+ _logger.Warning(_logger.StorageWarning);
+ }
+ if (_logger.IsFileLoggingAvailable)
+ {
+ _logger.Success("Runtime log storage verified by startup append. Active file: " + _logger.LogFilePath);
+ }
+ else
+ {
+ _logger.Error("Runtime file logging is unavailable. Use the Operation Log grid and resolve the storage warning before a real maintenance operation.");
+ }
}
///
@@ -591,6 +602,10 @@ namespace BizTalkPlatformManagementTool.Ui
{
try
{
+ if (!_logger.IsFileLoggingAvailable || string.IsNullOrWhiteSpace(_logger.LogDirectory))
+ {
+ throw new InvalidOperationException(_logger.StorageWarning ?? "No writable runtime log directory is active.");
+ }
Directory.CreateDirectory(_logger.LogDirectory);
Process.Start("explorer.exe", _logger.LogDirectory);
}
diff --git a/src/BizTalkPlatformManagementTool/app.manifest b/src/BizTalkPlatformManagementTool/app.manifest
index dcb46c1..b90625a 100644
--- a/src/BizTalkPlatformManagementTool/app.manifest
+++ b/src/BizTalkPlatformManagementTool/app.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/tests/BizTalkPlatformManagementTool.Tests/Program.cs b/tests/BizTalkPlatformManagementTool.Tests/Program.cs
index feda2b7..b588dbe 100644
--- a/tests/BizTalkPlatformManagementTool.Tests/Program.cs
+++ b/tests/BizTalkPlatformManagementTool.Tests/Program.cs
@@ -39,6 +39,9 @@ namespace BizTalkPlatformManagementTool.Tests
Run("ScheduledReceivePlanCarriesAdapterMetadata", ScheduledReceivePlanCarriesAdapterMetadata);
Run("AdapterAssemblyResolverRequiresMatchingIdentity", AdapterAssemblyResolverRequiresMatchingIdentity);
Run("OperationLogPersistsCompressesAndRetainsThirtyDays", OperationLogPersistsCompressesAndRetainsThirtyDays);
+ Run("OperationLogUsesVerifiedStartupFallback", OperationLogUsesVerifiedStartupFallback);
+ Run("OperationLogFailsOverAfterAppendFailure", OperationLogFailsOverAfterAppendFailure);
+ Run("OperationLogSurfacesTotalStorageFailure", OperationLogSurfacesTotalStorageFailure);
Run("ExceptionDiagnosticsContainSupportContext", ExceptionDiagnosticsContainSupportContext);
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
@@ -244,6 +247,66 @@ namespace BizTalkPlatformManagementTool.Tests
});
}
+ /// Prüft, dass erst ein real beschreibbarer Pfad als aktiv gilt.
+ private static void OperationLogUsesVerifiedStartupFallback()
+ {
+ InTemp(directory =>
+ {
+ var blocked = Path.Combine(directory, "blocked-primary");
+ var fallback = Path.Combine(directory, "writable-fallback");
+ File.WriteAllText(blocked, "not a directory");
+
+ var logger = new OperationLogger(null, new[] { blocked, fallback });
+ Assert(logger.IsFileLoggingAvailable, "writable fallback was not activated");
+ Assert(string.Equals(logger.LogDirectory, fallback, StringComparison.OrdinalIgnoreCase), "wrong fallback directory selected");
+ Assert(logger.StorageWarning.Contains("Primary runtime log path is not writable"), "startup fallback warning missing");
+ logger.Info("verified fallback marker");
+ Assert(File.Exists(logger.LogFilePath), "fallback log file was not created");
+ Assert(File.ReadAllText(logger.LogFilePath).Contains("verified fallback marker"), "fallback append was not durable");
+ });
+ }
+
+ /// Prüft automatisches Failover, wenn der aktive Pfad später ausfällt.
+ private static void OperationLogFailsOverAfterAppendFailure()
+ {
+ InTemp(directory =>
+ {
+ var primary = Path.Combine(directory, "primary");
+ var fallback = Path.Combine(directory, "fallback");
+ var visible = new List();
+ var logger = new OperationLogger(visible.Add, new[] { primary, fallback });
+ Assert(string.Equals(logger.LogDirectory, primary, StringComparison.OrdinalIgnoreCase), "primary directory was not initially selected");
+
+ Directory.Delete(primary, true);
+ File.WriteAllText(primary, "now blocked");
+ logger.Error("runtime failover marker");
+
+ Assert(string.Equals(logger.LogDirectory, fallback, StringComparison.OrdinalIgnoreCase), "append failure did not switch to fallback");
+ Assert(File.ReadAllText(logger.LogFilePath).Contains("runtime failover marker"), "failed primary record was not written to fallback");
+ Assert(visible.Any(x => x.Level == LogLevel.Warning && x.Message.Contains("Runtime log path switched")), "runtime fallback was not visible in the sink");
+ });
+ }
+
+ /// Prüft eine sichtbare Warnung, wenn kein Dateipfad beschreibbar ist.
+ private static void OperationLogSurfacesTotalStorageFailure()
+ {
+ InTemp(directory =>
+ {
+ var blockedOne = Path.Combine(directory, "blocked-one");
+ var blockedTwo = Path.Combine(directory, "blocked-two");
+ File.WriteAllText(blockedOne, "not a directory");
+ File.WriteAllText(blockedTwo, "not a directory");
+ var visible = new List();
+ var logger = new OperationLogger(visible.Add, new[] { blockedOne, blockedTwo });
+ Assert(!logger.IsFileLoggingAvailable, "logger reported unavailable candidates as writable");
+ Assert(logger.StorageWarning.Contains("RUNTIME FILE LOGGING UNAVAILABLE"), "total startup failure diagnostic missing");
+
+ logger.Error("grid-only marker");
+ Assert(visible.Any(x => x.Message == "grid-only marker"), "business entry disappeared with file logging failure");
+ Assert(visible.Any(x => x.Level == LogLevel.Warning && x.Message.Contains("RUNTIME FILE LOGGING UNAVAILABLE")), "file logging failure was not visible in the grid sink");
+ });
+ }
+
/// Prüft Typ, HRESULT, innere Ausnahme und Stacktrace für Supportfälle.
private static void ExceptionDiagnosticsContainSupportContext()
{