From 0bf2e2f099c26722d763ea9c3eb725987f3ee2f1 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Thu, 13 Nov 2025 21:15:31 -0700 Subject: [PATCH] Fixed Launch Script / Fixed Site List / Fixed Database Behavior --- Borealis.ps1 | 87 ++++++++++++++++++ Data/Engine/Assemblies/community.db-shm | Bin 32768 -> 0 bytes Data/Engine/Assemblies/community.db-wal | 0 Data/Engine/Assemblies/official.db-shm | Bin 32768 -> 0 bytes Data/Engine/Assemblies/official.db-wal | 0 Data/Engine/assembly_management/databases.py | 54 ++++++----- Data/Engine/assembly_management/payloads.py | 66 +++++++------ Data/Engine/tests/assemblies/test_payloads.py | 7 +- .../web-interface/src/Sites/Site_List.jsx | 1 - 9 files changed, 153 insertions(+), 62 deletions(-) delete mode 100644 Data/Engine/Assemblies/community.db-shm delete mode 100644 Data/Engine/Assemblies/community.db-wal delete mode 100644 Data/Engine/Assemblies/official.db-shm delete mode 100644 Data/Engine/Assemblies/official.db-wal diff --git a/Borealis.ps1 b/Borealis.ps1 index 2fefd420..0fe25b47 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -1160,8 +1160,95 @@ switch ($choice) { Run-Step "Create Borealis Engine Virtual Python Environment" { $venvActivate = Join-Path $venvFolder 'Scripts\Activate' + $pyvenvCfg = Join-Path $venvFolder 'pyvenv.cfg' + $expectedPython = $pythonExe + $expectedPythonNorm = $null + $expectedHomeNorm = $null + try { + if (Test-Path $pythonExe -PathType Leaf) { + $expectedPython = (Resolve-Path $pythonExe -ErrorAction Stop).ProviderPath + } + } catch { + $expectedPython = $pythonExe + } + if ($expectedPython) { + $expectedPythonNorm = $expectedPython.ToLowerInvariant() + try { + $expectedHome = Split-Path -Path $expectedPython -Parent + } catch { + $expectedHome = $null + } + if ($expectedHome) { + $expectedHomeNorm = $expectedHome.ToLowerInvariant() + } + } + + $venvNeedsUpgrade = $false + if (Test-Path $pyvenvCfg -PathType Leaf) { + try { + $cfgLines = Get-Content -Path $pyvenvCfg -ErrorAction Stop + $cfgMap = @{} + foreach ($line in $cfgLines) { + $trimmed = $line.Trim() + if (-not $trimmed -or $trimmed.StartsWith('#')) { + continue + } + $parts = $trimmed -split '=', 2 + if ($parts.Count -ne 2) { + continue + } + $cfgMap[$parts[0].Trim().ToLowerInvariant()] = $parts[1].Trim() + } + + $cfgExecutable = $cfgMap['executable'] + $cfgHome = $cfgMap['home'] + + if ($cfgExecutable -and -not (Test-Path $cfgExecutable -PathType Leaf)) { + $venvNeedsUpgrade = $true + } elseif ($cfgHome -and -not (Test-Path $cfgHome -PathType Container)) { + $venvNeedsUpgrade = $true + } else { + if ($cfgExecutable -and $expectedPythonNorm) { + try { + $resolvedExe = (Resolve-Path $cfgExecutable -ErrorAction Stop).ProviderPath + } catch { + $resolvedExe = $cfgExecutable + } + if ($resolvedExe) { + $resolvedExeNorm = $resolvedExe.ToLowerInvariant() + } else { + $resolvedExeNorm = $null + } + if ($resolvedExeNorm -and $resolvedExeNorm -ne $expectedPythonNorm) { + $venvNeedsUpgrade = $true + } + } + if (-not $venvNeedsUpgrade -and $cfgHome -and $expectedHomeNorm) { + try { + $resolvedHome = (Resolve-Path $cfgHome -ErrorAction Stop).ProviderPath + } catch { + $resolvedHome = $cfgHome + } + if ($resolvedHome) { + $resolvedHomeNorm = $resolvedHome.ToLowerInvariant() + } else { + $resolvedHomeNorm = $null + } + if ($resolvedHomeNorm -and $resolvedHomeNorm -ne $expectedHomeNorm) { + $venvNeedsUpgrade = $true + } + } + } + } catch { + $venvNeedsUpgrade = $true + } + } + if (-not (Test-Path $venvActivate)) { & $pythonExe -m venv $venvFolder | Out-Null + } elseif ($venvNeedsUpgrade) { + Write-Host "Detected relocated Engine virtual environment. Rebuilding interpreter bindings..." -ForegroundColor Yellow + & $pythonExe -m venv --upgrade $venvFolder | Out-Null } $engineDataRoot = Join-Path $venvFolder 'Data' diff --git a/Data/Engine/Assemblies/community.db-shm b/Data/Engine/Assemblies/community.db-shm deleted file mode 100644 index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeIuAr62r3 None: """Remove all assemblies and payload metadata for the specified domain.""" @@ -284,15 +284,43 @@ class AssemblyDatabaseManager: conn.commit() finally: conn.close() - self._mirror_database(domain) # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ + def _ensure_runtime_database(self, domain: AssemblyDomain) -> None: + """Ensure the runtime database exists by copying the staging asset when needed.""" + + paths = self._paths[domain] + runtime_db = paths.runtime + staging_db = paths.staging + runtime_db.parent.mkdir(parents=True, exist_ok=True) + if runtime_db.exists(): + return + + if staging_db.exists(): + for suffix in ("", "-wal", "-shm"): + staging_candidate = staging_db.parent / f"{staging_db.name}{suffix}" + runtime_candidate = runtime_db.parent / f"{runtime_db.name}{suffix}" + if staging_candidate.exists(): + try: + shutil.copy2(staging_candidate, runtime_candidate) + except Exception as exc: # pragma: no cover - best effort seed + self._logger.debug( + "Failed to seed runtime assembly database file %s -> %s: %s", + staging_candidate, + runtime_candidate, + exc, + ) + return + + runtime_db.touch() + def _open_connection(self, domain: AssemblyDomain, *, readonly: bool = False) -> sqlite3.Connection: + self._ensure_runtime_database(domain) paths = self._paths[domain] flags = "ro" if readonly else "rwc" - uri = f"file:{paths.staging.as_posix()}?mode={flags}&cache=shared" + uri = f"file:{paths.runtime.as_posix()}?mode={flags}&cache=shared" conn = sqlite3.connect(uri, uri=True, timeout=30) if readonly: conn.isolation_level = None @@ -313,26 +341,6 @@ class AssemblyDatabaseManager: cur.execute(statement) conn.commit() - def _mirror_database(self, domain: AssemblyDomain) -> None: - paths = self._paths[domain] - staging_db = paths.staging - runtime_db = paths.runtime - runtime_db.parent.mkdir(parents=True, exist_ok=True) - - for suffix in ("", "-wal", "-shm"): - staging_candidate = staging_db.parent / f"{staging_db.name}{suffix}" - runtime_candidate = runtime_db.parent / f"{runtime_db.name}{suffix}" - if staging_candidate.exists(): - try: - shutil.copy2(staging_candidate, runtime_candidate) - except Exception as exc: # pragma: no cover - best effort mirror - self._logger.debug( - "Failed to mirror assembly database file %s -> %s: %s", - staging_candidate, - runtime_candidate, - exc, - ) - def _migrate_legacy_schema(self, cur: sqlite3.Cursor) -> None: """Upgrade legacy assembly/payload tables to the consolidated schema.""" diff --git a/Data/Engine/assembly_management/payloads.py b/Data/Engine/assembly_management/payloads.py index acfb938d..e1c22dd7 100644 --- a/Data/Engine/assembly_management/payloads.py +++ b/Data/Engine/assembly_management/payloads.py @@ -41,7 +41,7 @@ class PayloadManager: assembly_guid: Optional[str] = None, extension: Optional[str] = None, ) -> PayloadDescriptor: - """Persist payload content and mirror it to the runtime directory.""" + """Persist payload content inside the runtime directory only.""" resolved_guid = self._normalise_guid(assembly_guid or uuid.uuid4().hex) resolved_extension = self._normalise_extension(extension or self._default_extension(payload_type)) @@ -49,23 +49,15 @@ class PayloadManager: data = content.encode("utf-8") if isinstance(content, str) else bytes(content) checksum = hashlib.sha256(data).hexdigest() - staging_dir = self._payload_dir(self._staging_root, resolved_guid) runtime_dir = self._payload_dir(self._runtime_root, resolved_guid) - staging_dir.mkdir(parents=True, exist_ok=True) runtime_dir.mkdir(parents=True, exist_ok=True) file_name = f"payload{resolved_extension}" - staging_path = staging_dir / file_name runtime_path = runtime_dir / file_name - with staging_path.open("wb") as handle: + with runtime_path.open("wb") as handle: handle.write(data) - try: - shutil.copy2(staging_path, runtime_path) - except Exception as exc: # pragma: no cover - best effort mirror - self._logger.debug("Failed to mirror payload %s to runtime copy: %s", resolved_guid, exc) - descriptor = PayloadDescriptor( assembly_guid=resolved_guid, payload_type=payload_type, @@ -85,35 +77,40 @@ class PayloadManager: checksum = hashlib.sha256(data).hexdigest() now = _dt.datetime.utcnow() - staging_path = self._payload_dir(self._staging_root, descriptor.assembly_guid) / descriptor.file_name runtime_path = self._payload_dir(self._runtime_root, descriptor.assembly_guid) / descriptor.file_name - staging_path.parent.mkdir(parents=True, exist_ok=True) runtime_path.parent.mkdir(parents=True, exist_ok=True) - with staging_path.open("wb") as handle: + with runtime_path.open("wb") as handle: handle.write(data) - try: - shutil.copy2(staging_path, runtime_path) - except Exception as exc: # pragma: no cover - best effort mirror - self._logger.debug("Failed to mirror payload %s during update: %s", descriptor.assembly_guid, exc) - descriptor.size_bytes = len(data) descriptor.checksum = checksum descriptor.updated_at = now return descriptor def read_payload_bytes(self, descriptor: PayloadDescriptor) -> bytes: - """Retrieve payload content from the staging copy.""" + """Retrieve payload content from the runtime copy, falling back to staging if required.""" + runtime_path = self._payload_dir(self._runtime_root, descriptor.assembly_guid) / descriptor.file_name + if runtime_path.exists(): + return runtime_path.read_bytes() staging_path = self._payload_dir(self._staging_root, descriptor.assembly_guid) / descriptor.file_name - return staging_path.read_bytes() + if staging_path.exists(): + data = staging_path.read_bytes() + try: + shutil.copy2(staging_path, runtime_path) + except Exception as exc: # pragma: no cover - best effort seed + self._logger.debug("Failed to seed runtime payload %s from staging: %s", descriptor.assembly_guid, exc) + return data + raise FileNotFoundError(f"Payload content missing for {descriptor.assembly_guid}") def ensure_runtime_copy(self, descriptor: PayloadDescriptor) -> None: - """Ensure the runtime payload copy matches the staging content.""" + """Ensure the runtime payload copy exists.""" - staging_path = self._payload_dir(self._staging_root, descriptor.assembly_guid) / descriptor.file_name runtime_path = self._payload_dir(self._runtime_root, descriptor.assembly_guid) / descriptor.file_name + if runtime_path.exists(): + return + staging_path = self._payload_dir(self._staging_root, descriptor.assembly_guid) / descriptor.file_name if not staging_path.exists(): self._logger.warning("Payload missing on disk; guid=%s path=%s", descriptor.assembly_guid, staging_path) return @@ -124,20 +121,19 @@ class PayloadManager: self._logger.debug("Failed to mirror payload %s via ensure_runtime_copy: %s", descriptor.assembly_guid, exc) def delete_payload(self, descriptor: PayloadDescriptor) -> None: - """Remove staging and runtime payload files.""" + """Remove runtime payload files without mutating staging copies.""" - for root in (self._staging_root, self._runtime_root): - dir_path = self._payload_dir(root, descriptor.assembly_guid) - file_path = dir_path / descriptor.file_name - try: - if file_path.exists(): - file_path.unlink() - if dir_path.exists() and not any(dir_path.iterdir()): - dir_path.rmdir() - except Exception as exc: # pragma: no cover - best effort cleanup - self._logger.debug( - "Failed to remove payload directory %s (%s): %s", descriptor.assembly_guid, root, exc - ) + dir_path = self._payload_dir(self._runtime_root, descriptor.assembly_guid) + file_path = dir_path / descriptor.file_name + try: + if file_path.exists(): + file_path.unlink() + if dir_path.exists() and not any(dir_path.iterdir()): + dir_path.rmdir() + except Exception as exc: # pragma: no cover - best effort cleanup + self._logger.debug( + "Failed to remove runtime payload directory %s: %s", descriptor.assembly_guid, exc + ) # ------------------------------------------------------------------ # Helper methods diff --git a/Data/Engine/tests/assemblies/test_payloads.py b/Data/Engine/tests/assemblies/test_payloads.py index 22609be1..6255c180 100644 --- a/Data/Engine/tests/assemblies/test_payloads.py +++ b/Data/Engine/tests/assemblies/test_payloads.py @@ -24,14 +24,15 @@ def test_payload_manager_store_update_delete(tmp_path: Path) -> None: staging_path = staging_root / "abc123" / descriptor.file_name runtime_path = runtime_root / "abc123" / descriptor.file_name - assert staging_path.is_file() assert runtime_path.is_file() + assert not staging_path.exists() assert descriptor.size_bytes == len(content) updated = manager.update_payload(descriptor, content + "-v2") assert updated.size_bytes == len(content + "-v2") - assert staging_path.read_text(encoding="utf-8").endswith("-v2") + assert runtime_path.read_text(encoding="utf-8").endswith("-v2") + assert not staging_path.exists() manager.delete_payload(descriptor) - assert not staging_path.exists() assert not runtime_path.exists() + assert not staging_path.exists() diff --git a/Data/Engine/web-interface/src/Sites/Site_List.jsx b/Data/Engine/web-interface/src/Sites/Site_List.jsx index 745cff11..2fbc3e41 100644 --- a/Data/Engine/web-interface/src/Sites/Site_List.jsx +++ b/Data/Engine/web-interface/src/Sites/Site_List.jsx @@ -202,7 +202,6 @@ export default function SiteList({ onOpenDevicesForSite }) { {/* AG Grid */} -}