fix(installer): avoid staged and installed capture class collision
This commit is contained in:
@@ -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)];
|
||||||
|
+}
|
||||||
Reference in New Issue
Block a user