From c8501556849463476a71881bc80780be0134859a Mon Sep 17 00:00:00 2001 From: "Yunior C. Fonseca Reyna" Date: Tue, 21 Apr 2026 12:38:37 +0200 Subject: [PATCH] fix environment registry initialization in settings --- app/backend/api/services/environment.py | 25 +++- app/backend/api/services/settings_service.py | 131 ++++++++++++++++--- app/backend/main.py | 48 ++++--- 3 files changed, 165 insertions(+), 39 deletions(-) diff --git a/app/backend/api/services/environment.py b/app/backend/api/services/environment.py index 4afbffc..b524bfa 100644 --- a/app/backend/api/services/environment.py +++ b/app/backend/api/services/environment.py @@ -25,17 +25,30 @@ # ****************************************************************************** import os + def prepareEnvironment(): """Prepare the Scipion environment with all variables""" from scipion.__main__ import Vars + variables = Vars.init() os.environ.update(variables) - # Trigger Config initialization once environment is ready + import pyworkflow - pwVARS = pyworkflow.Config.getVars() - variables.update(pwVARS) + pyworkflow.Config.setDomain("pwem") - pyworkflow.Config.getDomain() - # Update the environment now with pyworkflow values. + domain = pyworkflow.Config.getDomain() + + # Force protocol registry loading so VariablesRegistry is complete + domain.getProtocols() + + pwVars = pyworkflow.Config.getVars() + + # Load config-derived values first + os.environ.update(pwVars) + + # Keep backend bootstrap values as priority os.environ.update(variables) - os.chdir(variables.get(pyworkflow.SCIPION_HOME_VAR)) + + scipionHome = variables.get(pyworkflow.SCIPION_HOME_VAR) or os.environ.get(pyworkflow.SCIPION_HOME_VAR) + if scipionHome: + os.chdir(scipionHome) diff --git a/app/backend/api/services/settings_service.py b/app/backend/api/services/settings_service.py index dc93701..e65d6fc 100644 --- a/app/backend/api/services/settings_service.py +++ b/app/backend/api/services/settings_service.py @@ -524,28 +524,94 @@ def patchInstanceSettings( stored = mapper.upsertInstanceSettings(_modelDump(normalized)) return _modelValidate(InstanceSettingsOut, stored or {}) + def _registryCount(self) -> int: + # registryCount + try: + return len(list(VariablesRegistry.__iter__())) + except Exception: + return -1 + + def _warmupEnvironmentRegistry(self) -> None: + # warmupEnvironmentRegistry + from scipion.__main__ import Vars + from pyworkflow.project import Manager + from pyworkflow.template import TemplateList + + logger.warning("registry count [start]: %s", self._registryCount()) + + Vars.init() + logger.warning("registry count [after Vars.init]: %s", self._registryCount()) + + pyworkflow.Config.getVars() + logger.warning("registry count [after Config.getVars]: %s", self._registryCount()) + + pyworkflow.Config.setDomain("pwem") + domain = pyworkflow.Config.getDomain() + logger.warning("registry count [after Config.getDomain]: %s", self._registryCount()) + + try: + domain.getProtocols() + except Exception: + logger.exception("Error warming domain.getProtocols()") + logger.warning("registry count [after domain.getProtocols]: %s", self._registryCount()) + + try: + Manager() + except Exception: + logger.exception("Error warming Manager()") + logger.warning("registry count [after Manager()]: %s", self._registryCount()) + + try: + tempList = TemplateList() + tempList.addScipionTemplates(None) + tempList.addPluginTemplates(None) + except Exception: + logger.exception("Error warming TemplateList plugins") + logger.warning("registry count [after TemplateList warmup]: %s", self._registryCount()) + def getEnvironmentVariables(self, currentUser: Any) -> list[dict[str, str]]: # getEnvironmentVariables self._requireAdmin(currentUser) + with _envLock: + self._warmupEnvironmentRegistry() + rows = [] - for v in VariablesRegistry.__iter__(): + + for variable in VariablesRegistry.__iter__(): try: + variableName = _toStr(getattr(variable, "name", "")).strip() + if not variableName: + continue + + currentValue = getattr(variable, "value", None) + if variableName in os.environ: + currentValue = os.environ.get(variableName, "") + variable.value = currentValue + + defaultValue = getattr(variable, "default", None) + if defaultValue is not None: + variable.isDefault = str(defaultValue) == str(currentValue) + rows.append( { - "name": str(v.name), - "value": "" if v is None else str(v.value), - "default": "" if v.default is None else str(v.default), - "description": "" if v.description is None else str(v.description), - "source": "" if v.source is None else str(v.source), - "isDefault": "" if v.isDefault is None else v.isDefault, - "type": "STRING" if v.var_type is None else str(v.var_type.name), + "name": variableName, + "value": "" if currentValue is None else str(currentValue), + "default": "" if getattr(variable, "default", None) is None else str(variable.default), + "description": "" if getattr(variable, "description", None) is None else str( + variable.description), + "source": "" if getattr(variable, "source", None) is None else str(variable.source), + "isDefault": False if getattr(variable, "isDefault", None) is None else bool( + variable.isDefault), + "type": "STRING" + if getattr(variable, "var_type", None) is None + else str(variable.var_type.name), } ) except Exception: - print(v.name) + continue - rows.sort(key=lambda x: (x.get("name") or "").upper()) + rows.sort(key=lambda item: (item.get("name") or "").upper()) return rows def patchEnvironmentVariables(self, currentUser: Any, patch: Dict[str, Any]) -> list[dict[str, str]]: @@ -558,13 +624,46 @@ def patchEnvironmentVariables(self, currentUser: Any, patch: Dict[str, Any]) -> detail="Invalid payload: expected an object mapping variable names to values.", ) - for v in VariablesRegistry.__iter__(): - if v.name in patch: - v.value = patch[v.name] - v.isDefault = False + changed = False + + with _envLock: + patchItems: dict[str, str] = {} + for rawName, rawValue in patch.items(): + name = _toStr(rawName).strip() + if not name: + continue + patchItems[name] = "" if rawValue is None else str(rawValue) + + if patchItems: + for variable in VariablesRegistry.__iter__(): + try: + variableName = _toStr(getattr(variable, "name", "")).strip() + if not variableName or variableName not in patchItems: + continue + + nextValue = patchItems[variableName] + currentValue = "" if getattr(variable, "value", None) is None else str(variable.value) + + if currentValue == nextValue and os.environ.get(variableName) == nextValue: + continue + + os.environ[variableName] = nextValue + variable.value = nextValue + + defaultValue = getattr(variable, "default", None) + if defaultValue is not None: + variable.isDefault = str(defaultValue) == str(nextValue) + else: + variable.isDefault = False + + changed = True + except Exception: + continue + + if changed: + VariablesRegistry.save(pyworkflow.Config.SCIPION_CONFIG) - VariablesRegistry.save(pyworkflow.Config.SCIPION_CONFIG) - if patch: + if changed: triggerBackendReloadIfEnabled() return self.getEnvironmentVariables(currentUser) diff --git a/app/backend/main.py b/app/backend/main.py index 73ac00c..53f259e 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -27,10 +27,14 @@ from pathlib import Path from app.backend.bootstrap import bootstrapEnv +from app.backend.api.services.environment import prepareEnvironment # bootstrapEnvFirst bootstrapEnv() +# prepareScipionEnvironmentBeforeImportingRouters +prepareEnvironment() + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -40,7 +44,6 @@ from app.backend.api.routers.auth_router import router as auth from app.backend.api.routers.user_router import router as users from app.backend.api.routers.settings_router import router as settingsRouter -from app.backend.api.services.environment import prepareEnvironment from app.backend.utils.error_handlers import registerAllErrorHandlers from starlette.staticfiles import StaticFiles from starlette.exceptions import HTTPException as StarletteHttpException @@ -63,18 +66,24 @@ async def get_response(self, path: str, scope): def _buildApiApp() -> FastAPI: # buildApiApp - apiApp = FastAPI(title="Scipion API", - debug=True, - docs_url="/docs", - redoc_url="/redoc", - openapi_url="/openapi.json",) + apiApp = FastAPI( + title="Scipion API", + debug=True, + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", + ) # registerCustomErrorHandlers registerAllErrorHandlers(apiApp) apiApp.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:5173", "http://localhost:5174", "http://127.0.0.1:5173",], + allow_origins=[ + "http://localhost:5173", + "http://localhost:5174", + "http://127.0.0.1:5173", + ], allow_credentials=True, allow_methods=["*"], allow_headers=[ @@ -88,7 +97,7 @@ def _buildApiApp() -> FastAPI: "Scipion-Colormap", "Colormap", "X-Preview-Schema", - "X-Preview-Name" + "X-Preview-Name", ], expose_headers=[ "Content-Disposition", @@ -109,9 +118,6 @@ def _buildApiApp() -> FastAPI: ], ) - # prepareScipionEnvironment - prepareEnvironment() - # includeRouters apiApp.include_router(projects) apiApp.include_router(protocols) @@ -158,16 +164,25 @@ def _resolveWebDistPath() -> Path: apiMountPath = _normalizeMountPath(os.getenv("API_MOUNT_PATH") or "/api") # alwaysMountApiUnderApiMountPath -app = FastAPI(title="Scipion Web", debug=True, docs_url=None, - redoc_url=None, - openapi_url=None,) +app = FastAPI( + title="Scipion Web", + debug=True, + docs_url=None, + redoc_url=None, + openapi_url=None, +) app.mount(apiMountPath, apiApp) + @app.get("/health", include_in_schema=False) def health_check(): # healthCheckRoot - return {"status": "ok", "mode": "api-only" if not serveWeb else "combined", "apiMountPath": apiMountPath} + return { + "status": "ok", + "mode": "api-only" if not serveWeb else "combined", + "apiMountPath": apiMountPath, + } # optional: keepConvenienceRedirects @@ -187,5 +202,4 @@ def openapi_redirect(): if serveWeb and webDistPath and (webDistPath / "index.html").exists(): # mountSpaStaticRootLast - app.mount("/", SpaStaticFiles(directory=str(webDistPath), html=True), name="web") - + app.mount("/", SpaStaticFiles(directory=str(webDistPath), html=True), name="web") \ No newline at end of file