fix(installer): avoid staged and installed capture class collision

This commit is contained in:
ENELIX Agent
2026-10-03 07:34:44 +00:00
parent fe3f46ba0e
commit 6d9aba74b0
2 changed files with 105 additions and 0 deletions
+34
View File
@@ -95,3 +95,37 @@ commissioning. Economic replay-based automatic family selection, full corrected
feedback/control integration, long-term outbox/server retention and real live/ feedback/control integration, long-term outbox/server retention and real live/
failure acceptance remain open. Existing trial gates are unchanged. No sensor, failure acceptance remain open. Existing trial gates are unchanged. No sensor,
accounting or hardware evidence identifier is fabricated by data ingestion. accounting or hardware evidence identifier is fabricated by data ingestion.
## Installer class-path correction (2026-10-03)
The original commissioning installer loaded `source/libs/NetzfahrplanV4Messaufnahme.php`
while importing observation history. A later Manager callback loads the installed
copy at `modules/Enelix-EMS/libs/NetzfahrplanV4Messaufnahme.php`. PHP's once-only file
inclusion does not deduplicate class declarations across those different files.
The dependent `NetzfahrplanV4Bilanzierung` class is subject to the same collision.
The tested installer-only fix is preserved in
`examples/V4ApplicationData/application-class-loading.patch` (SHA256
`6d53d74d4059bb1ae3a446350637a4f4a4a73cc2fd64d2fc2f0967bd23ec7173`).
When building a commissioning package, load both classes only from the installed
module tree, after validating their expected source hashes. Before any require,
check both already-loaded origins without autoload. Reject a foreign staged origin
with a controlled error; do not hide it by wrapping the class declaration in a guard.
Include `installed_capture_bootstrap.php` in the package checksum map. Do not change
the installed-module manifest or bless unrelated source changes.
The Lihrenmoos staging entrypoint has this fix applied, with backups of its original
installer and checksum map. No running modules, data files, timers, native callbacks,
server components or actuator settings were changed by the repair. Actual completion
still requires the user's fresh Symcon invocation of the corrected installer.
Validation: the original two-path failure was reproduced with the real PHP classes.
Thirteen additional isolated installer scenarios passed, including callback/reload
loading order, preloaded dependencies, history import, repeat/finished import,
foreign-class refusal, partial-import preservation, source drift, registration wait
and first installation. These are PHP CLI tests with mocked IPS in temporary trees,
not a completed test in the actual Symcon kernel.
Evidence on the test host:
`/srv/agent/netplan-v4-application-build/class-loading-fix/TEST_RESULTS.json` and
`FIX_RESULT.json`. The test harness and exact candidate source are retained there.
@@ -0,0 +1,71 @@
--- a/install.php
+++ b/install.php
@@ -19,6 +19,8 @@
}
$before=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
$report['files']=v4StageInstall(__DIR__,'/var/lib/symcon/modules/Enelix-EMS');
+ require_once __DIR__.'/installed_capture_bootstrap.php';
+ $report['classLoading']=v4ApplicationLoadInstalledCapture('/var/lib/symcon/modules/Enelix-EMS',__DIR__.'/MANIFEST.json');
$controls=[];
foreach (IPS_GetModuleList() as $mid) if ((IPS_GetModule($mid)['Prefix']??'')==='MC') $controls=array_merge($controls,IPS_GetInstanceListByModuleID($mid));
if (count($controls)!==1||!function_exists('MC_ReloadModule')) throw new RuntimeException('Module Control nicht eindeutig.');
@@ -38,7 +40,7 @@
$marker=$dir.'/observer-import.json';
if (!is_file($marker)&&!IPS_GetProperty($manager,'NetzfahrplanV4MessdatenAktiv')) {
if (glob($dir.'/raw-*.jsonl')!==[]) throw new RuntimeException('Messdaten ohne Importabschluss vorhanden; nicht doppelt importieren.');
- require_once __DIR__.'/source/libs/NetzfahrplanV4Messaufnahme.php';
+ // Reuse the hash-checked installed class; never load the staged copy.
$count=0;$last=0;$cutoff=time()-172800;
$files=glob('/srv/agent/netplan-v4-separated-observer-stage/data/raw-*.jsonl');sort($files,SORT_STRING);
foreach ($files as $file) {
--- /dev/null
+++ b/installed_capture_bootstrap.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+/** Installer-only bootstrap. Load the same files the installed Manager uses.
+ * No IPS calls, source edits, state changes, or autoload of an unknown class.
+ */
+function v4ApplicationLoadInstalledCapture(string $target, string $manifestFile): array
+{
+ if (is_link($target) || realpath($target) !== $target || !is_dir($target)) {
+ throw new RuntimeException('Unerwartetes Modulverzeichnis.');
+ }
+ $manifest = json_decode((string) file_get_contents($manifestFile), true, 64, JSON_THROW_ON_ERROR);
+ if (($manifest['mode'] ?? null) !== 'data_application_only') {
+ throw new RuntimeException('Unerwarteter Installationsumfang.');
+ }
+ $classes = [
+ 'Belevo\\EnelixEMS\\NetzfahrplanV4Bilanzierung' => 'libs/NetzfahrplanV4Bilanzierung.php',
+ 'Belevo\\EnelixEMS\\NetzfahrplanV4Messaufnahme' => 'libs/NetzfahrplanV4Messaufnahme.php',
+ ];
+ // Check BOTH origins before loading either file, including the transitive dependency.
+ foreach ($classes as $class => $relative) {
+ $path = $target . '/' . $relative;
+ $expected = $manifest['files'][$relative]['after'] ?? null;
+ if (!is_string($expected) || !preg_match('/^[a-f0-9]{64}$/D', $expected)
+ || is_link($path) || !is_file($path) || realpath($path) !== $path
+ || hash_file('sha256', $path) !== $expected) {
+ throw new RuntimeException('Installierte Messbibliothek weicht vom geprueften Paket ab: ' . $relative);
+ }
+ if (class_exists($class, false)) {
+ $origin = (new ReflectionClass($class))->getFileName();
+ if ($origin !== $path) {
+ throw new RuntimeException('Messbibliothek bereits aus anderem Pfad geladen. Installationsaufruf separat ausfuehren, ohne weitere Sammler-Includes.');
+ }
+ }
+ }
+ foreach ($classes as $class => $relative) {
+ $path = $target . '/' . $relative;
+ if (!class_exists($class, false)) {
+ require_once $path;
+ }
+ if (!class_exists($class, false) || (new ReflectionClass($class))->getFileName() !== $path) {
+ throw new RuntimeException('Installierte Messbibliothek konnte nicht eindeutig geladen werden.');
+ }
+ }
+ return ['source' => 'installed_module_only', 'stagedClassesLoaded' => false,
+ 'files' => array_values($classes)];
+}