feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, Mapping, Sequence
|
||||
|
||||
try: # pragma: no cover - exercised only on Windows hosts with registry access.
|
||||
import winreg
|
||||
except ImportError: # pragma: no cover
|
||||
winreg = None # type: ignore[assignment]
|
||||
|
||||
|
||||
SCDM_EXE_NAME = "SpaceClaim.exe"
|
||||
SCDM_CACHE_RELATIVE_PATH = Path("local") / "scdm_backend.json"
|
||||
SCDM_PATH_ENV_VARS = (
|
||||
"STEP_EDITOR_SCDM_EXE",
|
||||
"STEP_EDITOR_SPACECLAIM_EXE",
|
||||
"SPACECLAIM_EXE",
|
||||
)
|
||||
SCDM_DISABLE_ENV = "STEP_EDITOR_DISABLE_SCDM"
|
||||
SCDM_TIMEOUT_ENV = "STEP_EDITOR_SCDM_TIMEOUT"
|
||||
SCDM_CACHE_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScdmBackendInfo:
|
||||
path: Path
|
||||
source: str
|
||||
version: str = ""
|
||||
verified_at: str = ""
|
||||
run_script_ok: bool = False
|
||||
license_ok: bool | None = None
|
||||
message: str = ""
|
||||
|
||||
def to_cache(self) -> dict[str, object]:
|
||||
return {
|
||||
"schemaVersion": SCDM_CACHE_SCHEMA_VERSION,
|
||||
"path": str(self.path),
|
||||
"source": self.source,
|
||||
"version": self.version,
|
||||
"verifiedAt": self.verified_at,
|
||||
"runScriptOk": bool(self.run_script_ok),
|
||||
"licenseOk": self.license_ok,
|
||||
"message": self.message,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_cache(cls, payload: Mapping[str, object]) -> "ScdmBackendInfo | None":
|
||||
raw_path = str(payload.get("path") or "").strip()
|
||||
if not raw_path:
|
||||
return None
|
||||
path = Path(os.path.expandvars(raw_path)).expanduser()
|
||||
if not _is_spaceclaim_exe(path):
|
||||
return None
|
||||
return cls(
|
||||
path=path,
|
||||
source=str(payload.get("source") or "cache"),
|
||||
version=str(payload.get("version") or _version_from_path(path)),
|
||||
verified_at=str(payload.get("verifiedAt") or ""),
|
||||
run_script_ok=bool(payload.get("runScriptOk")),
|
||||
license_ok=_optional_bool(payload.get("licenseOk")),
|
||||
message=str(payload.get("message") or ""),
|
||||
)
|
||||
|
||||
|
||||
def project_root(project_root_override: str | Path | None = None) -> Path:
|
||||
return Path(project_root_override).expanduser() if project_root_override else Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def default_scdm_cache_path(project_root_override: str | Path | None = None) -> Path:
|
||||
return project_root(project_root_override) / SCDM_CACHE_RELATIVE_PATH
|
||||
|
||||
|
||||
def is_scdm_disabled(env: Mapping[str, str] | None = None) -> bool:
|
||||
value = (env or os.environ).get(SCDM_DISABLE_ENV, "")
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def load_scdm_backend_cache(
|
||||
*,
|
||||
project_root_override: str | Path | None = None,
|
||||
cache_path: str | Path | None = None,
|
||||
) -> ScdmBackendInfo | None:
|
||||
path = Path(cache_path).expanduser() if cache_path else default_scdm_cache_path(project_root_override)
|
||||
if not path.is_file():
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
return ScdmBackendInfo.from_cache(payload)
|
||||
|
||||
|
||||
def save_scdm_backend_cache(
|
||||
backend: ScdmBackendInfo,
|
||||
*,
|
||||
project_root_override: str | Path | None = None,
|
||||
cache_path: str | Path | None = None,
|
||||
) -> Path:
|
||||
path = Path(cache_path).expanduser() if cache_path else default_scdm_cache_path(project_root_override)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(backend.to_cache(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def discover_scdm_backend_candidates(
|
||||
*,
|
||||
manual_path: str | Path | None = None,
|
||||
include_env: bool = True,
|
||||
include_registry: bool = True,
|
||||
include_common: bool = True,
|
||||
include_path: bool = True,
|
||||
common_roots: Iterable[str | Path] | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
) -> tuple[ScdmBackendInfo, ...]:
|
||||
env_map = env or os.environ
|
||||
candidates: list[ScdmBackendInfo] = []
|
||||
|
||||
if manual_path:
|
||||
candidates.extend(_info_for_user_value(manual_path, "manual"))
|
||||
|
||||
if include_env:
|
||||
for env_name in SCDM_PATH_ENV_VARS:
|
||||
raw_value = env_map.get(env_name, "").strip()
|
||||
if raw_value:
|
||||
candidates.extend(_info_for_user_value(raw_value, f"env:{env_name}"))
|
||||
|
||||
if include_registry:
|
||||
candidates.extend(_registry_candidates())
|
||||
|
||||
if include_common:
|
||||
candidates.extend(_common_install_candidates(common_roots=common_roots, env=env_map))
|
||||
|
||||
if include_path:
|
||||
found = shutil.which(SCDM_EXE_NAME)
|
||||
if found:
|
||||
candidates.extend(_info_for_user_value(found, "PATH"))
|
||||
|
||||
return _dedupe_candidates(candidates)
|
||||
|
||||
|
||||
def resolve_scdm_backend(
|
||||
*,
|
||||
project_root_override: str | Path | None = None,
|
||||
cache_path: str | Path | None = None,
|
||||
manual_path: str | Path | None = None,
|
||||
prefer_cache: bool = True,
|
||||
save_cache: bool = True,
|
||||
validate: bool = False,
|
||||
include_env: bool = True,
|
||||
include_registry: bool = True,
|
||||
include_common: bool = True,
|
||||
include_path: bool = True,
|
||||
common_roots: Iterable[str | Path] | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
timeout_seconds: float | None = None,
|
||||
runner: Callable[..., subprocess.CompletedProcess[str]] | None = None,
|
||||
) -> dict[str, object]:
|
||||
env_map = env or os.environ
|
||||
if is_scdm_disabled(env_map):
|
||||
return {"ok": False, "reason": "disabled", "backend": None, "message": "SCDM backend is disabled by environment."}
|
||||
|
||||
if prefer_cache:
|
||||
cached = load_scdm_backend_cache(project_root_override=project_root_override, cache_path=cache_path)
|
||||
if cached is not None:
|
||||
if not validate or cached.run_script_ok:
|
||||
return _resolution_payload(cached, reason="cache", message="Using cached SCDM backend.")
|
||||
checked = verify_scdm_backend(cached, timeout_seconds=timeout_seconds, runner=runner)
|
||||
if checked.get("ok"):
|
||||
verified = _verified_backend_from_result(cached, checked)
|
||||
if save_cache:
|
||||
save_scdm_backend_cache(verified, project_root_override=project_root_override, cache_path=cache_path)
|
||||
return _resolution_payload(verified, reason="cache-verified", message="Cached SCDM backend passed smoke test.")
|
||||
|
||||
failures: list[dict[str, object]] = []
|
||||
candidates = discover_scdm_backend_candidates(
|
||||
manual_path=manual_path,
|
||||
include_env=include_env,
|
||||
include_registry=include_registry,
|
||||
include_common=include_common,
|
||||
include_path=include_path,
|
||||
common_roots=common_roots,
|
||||
env=env_map,
|
||||
)
|
||||
for candidate in candidates:
|
||||
backend = candidate
|
||||
if validate:
|
||||
checked = verify_scdm_backend(candidate, timeout_seconds=timeout_seconds, runner=runner)
|
||||
if not checked.get("ok"):
|
||||
failures.append(
|
||||
{
|
||||
"path": str(candidate.path),
|
||||
"source": candidate.source,
|
||||
"reason": checked.get("reason"),
|
||||
"message": checked.get("message"),
|
||||
}
|
||||
)
|
||||
continue
|
||||
backend = _verified_backend_from_result(candidate, checked)
|
||||
|
||||
if save_cache:
|
||||
save_scdm_backend_cache(backend, project_root_override=project_root_override, cache_path=cache_path)
|
||||
reason = "discovered-verified" if validate else "discovered"
|
||||
return _resolution_payload(backend, reason=reason, message=f"SCDM backend resolved from {backend.source}.")
|
||||
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "missing-spaceclaim",
|
||||
"backend": None,
|
||||
"candidates": (),
|
||||
"failures": tuple(failures),
|
||||
"message": "SpaceClaim.exe was not found. Ask the user to configure the SCDM path manually.",
|
||||
}
|
||||
|
||||
|
||||
def verify_scdm_backend(
|
||||
backend: ScdmBackendInfo | str | Path,
|
||||
*,
|
||||
timeout_seconds: float | None = None,
|
||||
work_dir: str | Path | None = None,
|
||||
runner: Callable[..., subprocess.CompletedProcess[str]] | None = None,
|
||||
) -> dict[str, object]:
|
||||
if isinstance(backend, ScdmBackendInfo):
|
||||
info = backend
|
||||
else:
|
||||
matches = _info_for_user_value(backend, "manual")
|
||||
if not matches:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "missing-exe",
|
||||
"path": str(Path(str(backend)).expanduser()),
|
||||
"message": "SpaceClaim.exe does not exist.",
|
||||
}
|
||||
info = matches[0]
|
||||
if not _is_spaceclaim_exe(info.path):
|
||||
return {"ok": False, "reason": "missing-exe", "path": str(info.path), "message": "SpaceClaim.exe does not exist."}
|
||||
|
||||
timeout = timeout_seconds if timeout_seconds is not None else _timeout_seconds()
|
||||
temp_context = None
|
||||
if work_dir is None:
|
||||
temp_context = tempfile.TemporaryDirectory(prefix="step_editor_scdm_")
|
||||
work_root = Path(temp_context.name)
|
||||
else:
|
||||
work_root = Path(work_dir).expanduser()
|
||||
work_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
script_path = work_root / "scdm_smoke.py"
|
||||
report_path = work_root / "scdm_smoke_result.json"
|
||||
script_path.write_text(_smoke_script(report_path), encoding="utf-8")
|
||||
command = scdm_run_script_command(info.path, script_path)
|
||||
run = runner or subprocess.run
|
||||
try:
|
||||
completed = run(
|
||||
command,
|
||||
cwd=str(work_root),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=max(float(timeout), 0.1),
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"ok": False, "reason": "timeout", "path": str(info.path), "message": "SCDM smoke test timed out."}
|
||||
except OSError as exc:
|
||||
return {"ok": False, "reason": "launch-failed", "path": str(info.path), "message": str(exc)}
|
||||
|
||||
returncode = int(getattr(completed, "returncode", -1))
|
||||
stdout = str(getattr(completed, "stdout", "") or "")
|
||||
stderr = str(getattr(completed, "stderr", "") or "")
|
||||
if returncode != 0:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "run-script-failed",
|
||||
"path": str(info.path),
|
||||
"returncode": returncode,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"message": (stderr or stdout or f"SCDM returned {returncode}.").strip(),
|
||||
}
|
||||
if not report_path.is_file():
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "missing-report",
|
||||
"path": str(info.path),
|
||||
"returncode": returncode,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"message": "SCDM smoke script finished but did not write a report.",
|
||||
}
|
||||
try:
|
||||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
return {"ok": False, "reason": "bad-report", "path": str(info.path), "message": str(exc)}
|
||||
if not isinstance(report, dict) or report.get("ok") is not True:
|
||||
return {"ok": False, "reason": "negative-report", "path": str(info.path), "message": str(report)}
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"path": str(info.path),
|
||||
"source": info.source,
|
||||
"version": str(report.get("version") or info.version or _version_from_path(info.path)),
|
||||
"verifiedAt": _utc_now(),
|
||||
"runScriptOk": True,
|
||||
"licenseOk": True,
|
||||
"returncode": returncode,
|
||||
"message": str(report.get("message") or "SCDM /RunScript smoke test passed."),
|
||||
}
|
||||
finally:
|
||||
if temp_context is not None:
|
||||
temp_context.cleanup()
|
||||
|
||||
|
||||
def scdm_run_script_command(spaceclaim_exe: str | Path, script_path: str | Path) -> list[str]:
|
||||
exe = Path(spaceclaim_exe).expanduser().resolve(strict=False)
|
||||
script = Path(script_path).expanduser().resolve(strict=False)
|
||||
return [
|
||||
str(exe),
|
||||
f"/RunScript={script}",
|
||||
"/Headless=True",
|
||||
"/ExitAfterScript=True",
|
||||
]
|
||||
|
||||
|
||||
def _info_for_user_value(value: str | Path, source: str) -> list[ScdmBackendInfo]:
|
||||
path = _spaceclaim_path_from_value(value)
|
||||
if path is None:
|
||||
return []
|
||||
return [ScdmBackendInfo(path=path, source=source, version=_version_from_path(path))]
|
||||
|
||||
|
||||
def _spaceclaim_path_from_value(value: str | Path) -> Path | None:
|
||||
text = os.path.expandvars(str(value)).strip().strip('"')
|
||||
if not text:
|
||||
return None
|
||||
path = Path(text).expanduser()
|
||||
possible = [path]
|
||||
if path.is_dir():
|
||||
possible = [
|
||||
path / SCDM_EXE_NAME,
|
||||
path / "SCDM" / SCDM_EXE_NAME,
|
||||
]
|
||||
for candidate in possible:
|
||||
if _is_spaceclaim_exe(candidate):
|
||||
return candidate.resolve(strict=False)
|
||||
return None
|
||||
|
||||
|
||||
def _is_spaceclaim_exe(path: Path) -> bool:
|
||||
return path.name.lower() == SCDM_EXE_NAME.lower() and path.is_file()
|
||||
|
||||
|
||||
def _registry_candidates() -> list[ScdmBackendInfo]:
|
||||
if os.name != "nt" or winreg is None:
|
||||
return []
|
||||
|
||||
candidates: list[ScdmBackendInfo] = []
|
||||
app_path_keys = (
|
||||
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\SpaceClaim.exe",
|
||||
r"SOFTWARE\Classes\Applications\SpaceClaim.exe\shell\open\command",
|
||||
)
|
||||
roots = ((winreg.HKEY_CURRENT_USER, "HKCU"), (winreg.HKEY_LOCAL_MACHINE, "HKLM"))
|
||||
views = (0, getattr(winreg, "KEY_WOW64_64KEY", 0), getattr(winreg, "KEY_WOW64_32KEY", 0))
|
||||
for root, root_label in roots:
|
||||
for access in views:
|
||||
for key_path in app_path_keys:
|
||||
for raw_value in _registry_key_values(root, key_path, access):
|
||||
for path in _paths_from_registry_value(raw_value):
|
||||
candidates.extend(_info_for_user_value(path, f"registry:{root_label}\\{key_path}"))
|
||||
candidates.extend(_uninstall_registry_candidates(root, root_label, access))
|
||||
return candidates
|
||||
|
||||
|
||||
def _registry_key_values(root: int, key_path: str, access: int) -> list[str]:
|
||||
values: list[str] = []
|
||||
try:
|
||||
with winreg.OpenKey(root, key_path, 0, winreg.KEY_READ | access) as key: # type: ignore[union-attr]
|
||||
for name in ("", "Path", "InstallPath", "InstallLocation"):
|
||||
try:
|
||||
value, _value_type = winreg.QueryValueEx(key, name) # type: ignore[union-attr]
|
||||
except OSError:
|
||||
continue
|
||||
if isinstance(value, str) and value.strip():
|
||||
values.append(value)
|
||||
except OSError:
|
||||
return []
|
||||
return values
|
||||
|
||||
|
||||
def _uninstall_registry_candidates(root: int, root_label: str, access: int) -> list[ScdmBackendInfo]:
|
||||
uninstall_key = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
|
||||
candidates: list[ScdmBackendInfo] = []
|
||||
try:
|
||||
with winreg.OpenKey(root, uninstall_key, 0, winreg.KEY_READ | access) as key: # type: ignore[union-attr]
|
||||
index = 0
|
||||
while True:
|
||||
try:
|
||||
subkey_name = winreg.EnumKey(key, index) # type: ignore[union-attr]
|
||||
except OSError:
|
||||
break
|
||||
index += 1
|
||||
try:
|
||||
with winreg.OpenKey(key, subkey_name, 0, winreg.KEY_READ | access) as subkey: # type: ignore[union-attr]
|
||||
display_name = _registry_string(subkey, "DisplayName")
|
||||
install_location = _registry_string(subkey, "InstallLocation")
|
||||
except OSError:
|
||||
continue
|
||||
if "spaceclaim" not in display_name.lower() and "ansys" not in display_name.lower():
|
||||
continue
|
||||
for path in _paths_from_registry_value(install_location):
|
||||
candidates.extend(_info_for_user_value(path, f"registry:{root_label}\\Uninstall"))
|
||||
except OSError:
|
||||
return []
|
||||
return candidates
|
||||
|
||||
|
||||
def _registry_string(key: object, name: str) -> str:
|
||||
try:
|
||||
value, _value_type = winreg.QueryValueEx(key, name) # type: ignore[union-attr]
|
||||
except OSError:
|
||||
return ""
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _paths_from_registry_value(value: str) -> list[str]:
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return []
|
||||
exe = _extract_exe_from_command(text)
|
||||
if exe:
|
||||
return [exe]
|
||||
return [
|
||||
text,
|
||||
str(Path(text) / SCDM_EXE_NAME),
|
||||
str(Path(text) / "SCDM" / SCDM_EXE_NAME),
|
||||
]
|
||||
|
||||
|
||||
def _extract_exe_from_command(command: str) -> str:
|
||||
text = command.strip()
|
||||
if not text:
|
||||
return ""
|
||||
if text.startswith('"'):
|
||||
end = text.find('"', 1)
|
||||
if end > 1:
|
||||
first = text[1:end]
|
||||
return first if first.lower().endswith(".exe") else ""
|
||||
lowered = text.lower()
|
||||
index = lowered.find(".exe")
|
||||
if index >= 0:
|
||||
return text[: index + 4]
|
||||
return ""
|
||||
|
||||
|
||||
def _common_install_candidates(
|
||||
*,
|
||||
common_roots: Iterable[str | Path] | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
) -> list[ScdmBackendInfo]:
|
||||
roots = list(common_roots) if common_roots is not None else _default_common_roots(env or os.environ)
|
||||
candidates: list[ScdmBackendInfo] = []
|
||||
for root in roots:
|
||||
base = Path(os.path.expandvars(str(root))).expanduser()
|
||||
if not base.is_dir():
|
||||
continue
|
||||
direct_paths = (
|
||||
base / SCDM_EXE_NAME,
|
||||
base / "SCDM" / SCDM_EXE_NAME,
|
||||
)
|
||||
for path in direct_paths:
|
||||
candidates.extend(_info_for_user_value(path, f"common:{base}"))
|
||||
version_dirs = sorted((item for item in base.glob("v*") if item.is_dir()), key=_version_sort_key, reverse=True)
|
||||
for version_dir in version_dirs:
|
||||
candidates.extend(_info_for_user_value(version_dir / "SCDM" / SCDM_EXE_NAME, f"common:{base}"))
|
||||
return candidates
|
||||
|
||||
|
||||
def _default_common_roots(env: Mapping[str, str]) -> tuple[Path, ...]:
|
||||
roots: list[Path] = []
|
||||
for env_name in ("ProgramW6432", "ProgramFiles", "ProgramFiles(x86)"):
|
||||
raw = env.get(env_name, "")
|
||||
if raw:
|
||||
roots.append(Path(raw) / "ANSYS Inc")
|
||||
for drive in ("C", "D", "E"):
|
||||
roots.append(Path(f"{drive}:/Program Files/ANSYS Inc"))
|
||||
roots.append(Path(f"{drive}:/softwaresInstallDir/ANSYS Inc"))
|
||||
return tuple(_dedupe_paths(roots))
|
||||
|
||||
|
||||
def _dedupe_candidates(candidates: Iterable[ScdmBackendInfo]) -> tuple[ScdmBackendInfo, ...]:
|
||||
result: list[ScdmBackendInfo] = []
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
key = str(candidate.path.resolve(strict=False)).casefold()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append(candidate)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _dedupe_paths(paths: Iterable[Path]) -> list[Path]:
|
||||
result: list[Path] = []
|
||||
seen: set[str] = set()
|
||||
for path in paths:
|
||||
key = str(path.resolve(strict=False)).casefold()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append(path)
|
||||
return result
|
||||
|
||||
|
||||
def _version_sort_key(path: Path) -> tuple[int, str]:
|
||||
match = re.search(r"v(\d+)", path.name, flags=re.IGNORECASE)
|
||||
return (int(match.group(1)) if match else -1, path.name.lower())
|
||||
|
||||
|
||||
def _version_from_path(path: Path) -> str:
|
||||
for part in path.parts:
|
||||
match = re.fullmatch(r"v\d+", part, flags=re.IGNORECASE)
|
||||
if match:
|
||||
return part
|
||||
return ""
|
||||
|
||||
|
||||
def _verified_backend_from_result(candidate: ScdmBackendInfo, result: Mapping[str, object]) -> ScdmBackendInfo:
|
||||
return ScdmBackendInfo(
|
||||
path=candidate.path,
|
||||
source=candidate.source,
|
||||
version=str(result.get("version") or candidate.version),
|
||||
verified_at=str(result.get("verifiedAt") or _utc_now()),
|
||||
run_script_ok=bool(result.get("runScriptOk")),
|
||||
license_ok=_optional_bool(result.get("licenseOk")),
|
||||
message=str(result.get("message") or candidate.message),
|
||||
)
|
||||
|
||||
|
||||
def _resolution_payload(backend: ScdmBackendInfo, *, reason: str, message: str) -> dict[str, object]:
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": reason,
|
||||
"backend": backend,
|
||||
"path": str(backend.path),
|
||||
"source": backend.source,
|
||||
"version": backend.version,
|
||||
"verifiedAt": backend.verified_at,
|
||||
"runScriptOk": backend.run_script_ok,
|
||||
"licenseOk": backend.license_ok,
|
||||
"message": message,
|
||||
}
|
||||
|
||||
|
||||
def _smoke_script(report_path: Path) -> str:
|
||||
report_literal = repr(str(report_path))
|
||||
return (
|
||||
"from __future__ import print_function\n"
|
||||
f"report_path = {report_literal}\n"
|
||||
"version = ''\n"
|
||||
"try:\n"
|
||||
" version = str(Application.Version)\n"
|
||||
"except Exception:\n"
|
||||
" version = ''\n"
|
||||
"payload = '{\"ok\": true, \"version\": \"' + version.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"') + '\", \"message\": \"RunScript reached\"}'\n"
|
||||
"handle = open(report_path, 'w')\n"
|
||||
"handle.write(payload)\n"
|
||||
"handle.close()\n"
|
||||
)
|
||||
|
||||
|
||||
def _timeout_seconds() -> float:
|
||||
try:
|
||||
return max(float(os.environ.get(SCDM_TIMEOUT_ENV, "") or 25.0), 0.1)
|
||||
except ValueError:
|
||||
return 25.0
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _optional_bool(value: object) -> bool | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
text = value.strip().lower()
|
||||
if text in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if text in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SCDM_CACHE_RELATIVE_PATH",
|
||||
"SCDM_DISABLE_ENV",
|
||||
"SCDM_EXE_NAME",
|
||||
"SCDM_PATH_ENV_VARS",
|
||||
"SCDM_TIMEOUT_ENV",
|
||||
"ScdmBackendInfo",
|
||||
"default_scdm_cache_path",
|
||||
"discover_scdm_backend_candidates",
|
||||
"is_scdm_disabled",
|
||||
"load_scdm_backend_cache",
|
||||
"project_root",
|
||||
"resolve_scdm_backend",
|
||||
"save_scdm_backend_cache",
|
||||
"scdm_run_script_command",
|
||||
"verify_scdm_backend",
|
||||
]
|
||||
Reference in New Issue
Block a user