From 5a4e3c6d9d8cbbe2ae45501cfc895b56493e11fc Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 22 May 2026 08:10:49 +0300 Subject: [PATCH] Add local 1C runtime adapter execution for CF files --- .../src/runtime_adapter/main.py | 117 +++++++++++++++++- .../tests/test_runtime_adapter.py | 44 +++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/services/runtime-adapter/src/runtime_adapter/main.py b/services/runtime-adapter/src/runtime_adapter/main.py index 9ed65dc..97bd713 100644 --- a/services/runtime-adapter/src/runtime_adapter/main.py +++ b/services/runtime-adapter/src/runtime_adapter/main.py @@ -1,6 +1,10 @@ from __future__ import annotations import os +import shutil +import subprocess +import sys +import tempfile from enum import Enum from pathlib import Path @@ -134,7 +138,19 @@ async def runtime_import(request: RuntimeImportRequest) -> RuntimeImportResponse ], dump_plan=dump_plan, ) - raise HTTPException(status_code=501, detail="Designer execution runner is not implemented yet") + if source_kind in {"CF_FILE", "CFE_FILE"}: + if not request.path: + raise HTTPException(status_code=400, detail="path is required for CF/CFE import") + dump_root, execution_logs = _convert_local_cf_or_cfe_to_metadata_dump(request, platform_status) + return RuntimeImportResponse( + status="normalized", + mode=mode.value, + platform_found=True, + normalized_project=normalize_one_c_project(dump_root, project_id=request.project_id), + diagnostics=[*platform_status.diagnostics, *execution_logs], + dump_plan=dump_plan, + ) + raise HTTPException(status_code=501, detail=f"Designer execution runner is not implemented yet for {source_kind}") def _mode() -> RuntimeMode: @@ -217,6 +233,105 @@ def _designer_dump_plan(request: RuntimeImportRequest) -> list[str]: return plan +def _convert_local_cf_or_cfe_to_metadata_dump( + request: RuntimeImportRequest, + platform_status: RuntimePlatformResponse, +) -> tuple[Path, list[str]]: + source_kind = request.source_kind.upper() + payload_path = Path(request.path or "") + if not payload_path.exists(): + raise HTTPException(status_code=404, detail=f"Path not found: {request.path}") + if not platform_status.designer_path: + raise HTTPException(status_code=503, detail="1C Designer CLI path is not configured") + export_root = Path(tempfile.mkdtemp(prefix=f"sfera-runtime-{request.project_id or 'project'}-")) + builder_infobase = export_root / "builder-infobase" + logs: list[str] = [] + + _run_designer_command( + platform_status.designer_path, + ["CREATEINFOBASE", f"File={builder_infobase};"], + export_root / "create-builder-infobase.log", + "1C CREATEINFOBASE for local CF/CFE conversion", + ) + builder_args = ["/F", str(builder_infobase)] + artifacts_root = export_root / "artifacts" + artifacts_root.mkdir(parents=True, exist_ok=True) + shutil.copyfile(payload_path, artifacts_root / payload_path.name) + + if source_kind == "CF_FILE": + _run_designer_command( + platform_status.designer_path, + [*builder_args, "/LoadCfg", str(payload_path)], + export_root / "designer-loadcfg-local-cf.log", + "1C LoadCfg local CF", + ) + metadata_root = export_root / "configuration" + metadata_root.mkdir(parents=True, exist_ok=True) + _run_designer_command( + platform_status.designer_path, + [*builder_args, "/DumpConfigToFiles", str(metadata_root), "-Format", "Hierarchical"], + export_root / "designer-dumpconfigtofiles-local-cf.log", + "1C DumpConfigToFiles from local CF", + ) + shutil.copyfile(payload_path, metadata_root / payload_path.name) + logs.append("Local .cf converted to metadata export for server-side parsing.") + return export_root, logs + + if source_kind == "CFE_FILE": + extension_name = str(request.metadata.get("one_c_extension") or payload_path.stem).strip() + if not extension_name: + raise HTTPException(status_code=400, detail="Extension name is required for local CFE conversion.") + _run_designer_command( + platform_status.designer_path, + [*builder_args, "/LoadCfg", str(payload_path), "-Extension", extension_name, "/UpdateDBCfg"], + export_root / "designer-loadcfg-local-cfe.log", + "1C LoadCfg local CFE", + ) + metadata_root = export_root / "extension" + metadata_root.mkdir(parents=True, exist_ok=True) + _run_designer_command( + platform_status.designer_path, + [*builder_args, "/DumpConfigToFiles", str(metadata_root), "-Format", "Hierarchical", "-Extension", extension_name], + export_root / "designer-dumpconfigtofiles-local-cfe.log", + "1C DumpConfigToFiles from local CFE", + ) + shutil.copyfile(payload_path, metadata_root / payload_path.name) + logs.append("Local .cfe converted to metadata export for server-side parsing.") + return export_root, logs + + raise HTTPException(status_code=400, detail=f"Unsupported local 1C source: {source_kind}") + + +def _designer_process_command(designer_path: str, arguments: list[str]) -> list[str]: + path = Path(designer_path) + if path.suffix.casefold() == ".py": + return [sys.executable, designer_path, *arguments] + return [designer_path, *arguments] + + +def _run_designer_command(designer_path: str, arguments: list[str], log_path: Path, action_title: str, timeout_seconds: int = 180) -> None: + command = _designer_process_command(designer_path, arguments) + log_path.parent.mkdir(parents=True, exist_ok=True) + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + timeout=timeout_seconds, + check=False, + ) + output = completed.stdout or "" + log_path.write_text(output, encoding="utf-8") + if completed.returncode != 0: + tail = output[-4000:] if output else "" + raise HTTPException( + status_code=500, + detail=f"{action_title} failed with code {completed.returncode}. {tail}".strip(), + ) + + def _redact_connection_string(value: str) -> str: sensitive_keys = {"pwd", "password", "пароль"} chunks: list[str] = [] diff --git a/services/runtime-adapter/tests/test_runtime_adapter.py b/services/runtime-adapter/tests/test_runtime_adapter.py index 65626b0..d504bad 100644 --- a/services/runtime-adapter/tests/test_runtime_adapter.py +++ b/services/runtime-adapter/tests/test_runtime_adapter.py @@ -111,3 +111,47 @@ def test_runtime_platform_reports_capabilities(monkeypatch, tmp_path): payload = response.json() assert payload["platform_found"] is True assert "cf_dump_plan" in payload["capabilities"] + + +def test_runtime_adapter_local_1c_executes_cf_dump(monkeypatch, tmp_path): + designer = tmp_path / "fake_designer.py" + designer.write_text( + """ +import sys +from pathlib import Path + +args = sys.argv[1:] +if args and args[0] == "CREATEINFOBASE": + target = next(item for item in args[1:] if item.startswith("File="))[5:].rstrip(";") + Path(target).mkdir(parents=True, exist_ok=True) + raise SystemExit(0) +if "/DumpConfigToFiles" in args: + target = Path(args[args.index("/DumpConfigToFiles") + 1]) + target.mkdir(parents=True, exist_ok=True) + (target / "metadata.xml").write_text( + "", + encoding="utf-8", + ) + raise SystemExit(0) +raise SystemExit(0) +""", + encoding="utf-8", + ) + cf_file = tmp_path / "demo.cf" + cf_file.write_text("binary-placeholder", encoding="utf-8") + monkeypatch.setenv("RUNTIME_ADAPTER_MODE", "local_1c") + monkeypatch.setenv("ONEC_DESIGNER_PATH", str(designer)) + monkeypatch.setenv("ONEC_ENABLE_DESIGNER_EXECUTION", "true") + client = TestClient(app) + + response = client.post( + "/runtime/import", + json={"source_kind": "CF_FILE", "project_id": "cf-exec", "path": str(cf_file)}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "normalized" + assert payload["platform_found"] is True + assert payload["normalized_project"]["project_id"] == "cf-exec" + assert "Local .cf converted to metadata export" in "\n".join(payload["diagnostics"])