diff --git a/src/gpu_rent/provision.py b/src/gpu_rent/provision.py index 4a92b02..158fe1b 100644 --- a/src/gpu_rent/provision.py +++ b/src/gpu_rent/provision.py @@ -229,15 +229,27 @@ print(__import__("json").dumps(found)) _ASSISTENT_SQLITE_BIN_PY = r""" from pathlib import Path import json +skip = {"obj", "node_modules"} + +def assistent_dlls(root: Path): + out = [] + if not root.is_dir(): + return out + for dll in root.rglob("*.dll"): + if any(p in skip for p in dll.parts): + continue + name = dll.name.lower() + if name == "swarmassistentextension.dll" or ( + name.startswith("swarmextensionswarm-assistent") and name.endswith(".dll") + ): + out.append(dll) + return out + roots = [Path("/mnt/swarm_data"), Path("/opt/swarmui")] seen = set() found = [] for root in roots: - if not root.is_dir(): - continue - for dll in root.rglob("SwarmAssistentExtension.dll"): - if any(p in {"obj", "node_modules"} for p in dll.parts): - continue + for dll in assistent_dlls(root): key = str(dll) if key in seen: continue @@ -247,6 +259,7 @@ for root in roots: pcl = list(dll_dir.glob("SQLitePCLRaw*.dll")) bundle = dll_dir / "SQLitePCLRaw.bundle_e_sqlite3.dll" native = list(dll_dir.glob("runtimes/**/e_sqlite3.*")) + list(dll_dir.glob("runtimes/**/libe_sqlite3.*")) + native_flat = list(dll_dir.glob("e_sqlite3.*")) + list(dll_dir.glob("libe_sqlite3.*")) found.append({ "path": str(dll.parent.parent) if dll.parent.name.startswith("net") else str(dll_dir), "name": "swarm-assistent", @@ -256,51 +269,84 @@ for root in roots: "sqlitepcl": bool(pcl), "sqlite_bundle": bundle.is_file(), "sqlite_native": bool(native), + "sqlite_native_flat": bool(native_flat), }) print(json.dumps(found)) """ _ASSISTENT_SQLITE_REPAIR_PY = r""" from pathlib import Path -import json, shutil +import json, os, shutil skip = {"obj", "node_modules"} -dlls = [] -for root in (Path("/mnt/swarm_data"), Path("/opt/swarmui")): + +def assistent_dlls(root: Path): + out = [] if not root.is_dir(): - continue - for dll in root.rglob("SwarmAssistentExtension.dll"): + return out + for dll in root.rglob("*.dll"): if any(p in skip for p in dll.parts): continue - dlls.append(dll) + name = dll.name.lower() + if name == "swarmassistentextension.dll" or ( + name.startswith("swarmextensionswarm-assistent") and name.endswith(".dll") + ): + out.append(dll) + return out + +def find_native(d: Path): + for rel in ( + Path("runtimes/linux-x64/native/libe_sqlite3.so"), + Path("runtimes/linux-x64/native/e_sqlite3.so"), + ): + p = d / rel + if p.is_file(): + return p + for p in d.glob("runtimes/**/libe_sqlite3.so"): + return p + for p in d.glob("runtimes/**/e_sqlite3.so"): + return p + return None + +def link_native(dest_dir: Path, src: Path): + for name in ("libe_sqlite3.so", "e_sqlite3.so"): + out = dest_dir / name + if out.exists() or out.is_symlink(): + continue + try: + os.symlink(src, out) + except OSError: + shutil.copy2(src, out) + +dlls = [] +for root in (Path("/mnt/swarm_data"), Path("/opt/swarmui")): + dlls.extend(assistent_dlls(root)) def has_sqlite(d): if not (d / "Microsoft.Data.Sqlite.dll").is_file(): return False if not list(d.glob("SQLitePCLRaw*.dll")): return False - return (d / "SQLitePCLRaw.bundle_e_sqlite3.dll").is_file() or list( - d.glob("runtimes/**/e_sqlite3.*") - ) or list(d.glob("runtimes/**/libe_sqlite3.*")) + if (d / "libe_sqlite3.so").is_file() or (d / "e_sqlite3.so").is_file(): + return True + return bool(list(d.glob("runtimes/**/libe_sqlite3.*")) or list(d.glob("runtimes/**/e_sqlite3.*"))) donors = [d.parent for d in dlls if has_sqlite(d.parent)] if not donors: nuget = Path.home() / ".nuget" / "packages" - extra = [] if nuget.is_dir(): - extra.extend(nuget.rglob("Microsoft.Data.Sqlite.dll")) - extra.extend(nuget.rglob("SQLitePCLRaw.core.dll")) - # last resort: any copy on the disk under swarm - for root in (Path("/opt/swarmui"), Path("/root/.nuget")): - if root.is_dir(): - extra.extend(root.rglob("Microsoft.Data.Sqlite.dll")) - for f in extra: - if f.parent not in donors: - donors.append(f.parent) + for f in nuget.rglob("Microsoft.Data.Sqlite.dll"): + if f.parent not in donors: + donors.append(f.parent) copied = [] +linked = [] missing = [] for dll in dlls: dest = dll.parent + native = find_native(dest) + if native is not None: + link_native(dest, native) + linked.append({"dll_dir": str(dest), "native": str(native)}) if has_sqlite(dest): continue src = next((d for d in donors if d != dest and has_sqlite(d)), None) @@ -311,7 +357,6 @@ for dll in dlls: continue names = ["Microsoft.Data.Sqlite.dll"] names += [p.name for p in src.glob("SQLitePCLRaw*.dll")] - names += [p.name for p in src.glob("e_sqlite3.*")] for name in names: fp = src / name if fp.is_file(): @@ -326,7 +371,11 @@ for dll in dlls: out.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(fp, out) copied.append({"dll_dir": str(dest), "from": str(src)}) -print(json.dumps({"copied": copied, "missing": missing, "dlls": [str(d) for d in dlls]})) + n2 = find_native(dest) + if n2 is not None: + link_native(dest, n2) + linked.append({"dll_dir": str(dest), "native": str(n2)}) +print(json.dumps({"copied": copied, "linked": linked, "missing": missing, "dlls": [str(d) for d in dlls]})) """ @@ -394,10 +443,14 @@ def repair_assistent_sqlite_bins(cfg: Config, host: str, log: Log) -> bool: return False for row in copied: log(f"Assistent Sqlite: скопировал рядом с {row.get('dll_dir')}") + for row in data.get("linked") or []: + log(f"Assistent Sqlite: symlink native → {row.get('dll_dir')}") for path in missing: log(f"⚠ Assistent Sqlite: нет donor DLL для {path}") if copied: return True + if data.get("linked"): + return True if not missing: log("Assistent Sqlite: private deps уже рядом с DLL") return True @@ -435,7 +488,7 @@ def verify_assistent_sqlite_bins(cfg: Config, host: str, log: Log) -> bool: ) continue if row.get("sqlite_dll") and row.get("sqlitepcl") and ( - row.get("sqlite_bundle") or row.get("sqlite_native") + row.get("sqlite_bundle") or row.get("sqlite_native") or row.get("sqlite_native_flat") ): log(f"Assistent {name}: Microsoft.Data.Sqlite + SQLitePCLRaw (+ native) рядом с DLL") ok_any = True @@ -1387,13 +1440,39 @@ def ensure_swarmui_running(cfg: Config, host: str, log: Log, restart: bool) -> N if restart and active == "active": log("systemctl restart swarmui (новые extensions/autocomplete)") run_ssh(cfg, host, "sudo -n systemctl restart swarmui", timeout=120) + _wait_swarmui_extension_build(cfg, host, log) return if active != "active": log("systemctl start swarmui") run_ssh(cfg, host, "sudo -n systemctl start swarmui", timeout=120) + _wait_swarmui_extension_build(cfg, host, log) run_ssh(cfg, host, "sudo -n systemctl enable swarmui", check=False) +def _wait_swarmui_extension_build(cfg: Config, host: str, log: Log, timeout_sec: int = 120) -> None: + """Wait until SwarmUI compiles extensions (Assistent DLL appears in bin/extensions).""" + import time + + deadline = time.monotonic() + timeout_sec + while time.monotonic() < deadline: + try: + raw = run_ssh( + cfg, + host, + "python3 - <<'PY'\n" + _ASSISTENT_SQLITE_BIN_PY + "\nPY", + check=False, + timeout=25, + ).strip() + rows = json.loads(raw.splitlines()[-1]) if raw else [] + if isinstance(rows, list) and rows and any(r.get("dll") for r in rows): + log("SwarmUI: extension DLL готов") + return + except Exception: + pass + time.sleep(5) + log("⚠ SwarmUI: extension DLL не появился за 2 мин — repair Sqlite может не сработать") + + _OLLAMA_TAGS_PY = r""" import json, urllib.request try: