diff --git a/docs/netplan-v4-application.md b/docs/netplan-v4-application.md index bf964b0..452f504 100644 --- a/docs/netplan-v4-application.md +++ b/docs/netplan-v4-application.md @@ -95,3 +95,37 @@ commissioning. Economic replay-based automatic family selection, full corrected feedback/control integration, long-term outbox/server retention and real live/ failure acceptance remain open. Existing trial gates are unchanged. No sensor, 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. diff --git a/examples/V4ApplicationData/application-class-loading.patch b/examples/V4ApplicationData/application-class-loading.patch new file mode 100644 index 0000000..5b39d69 --- /dev/null +++ b/examples/V4ApplicationData/application-class-loading.patch @@ -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 @@ ++ '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)]; ++}