"""Explicit start-if-stopped and new-ISO launch, preserving runtime authority.""" import json import os from pathlib import Path from . import media, session from .profiles import Profile, load_defaults, validate_name from .runtime import Runtime, RuntimeError HOST_RESERVE_MIB = 768 def available_memory_mib() -> int: """Linux MemAvailable accounts for reclaimable host caches.""" try: for line in Path('/proc/meminfo').read_text().splitlines(): if line.startswith('MemAvailable:'): return int(line.split()[1]) // 1024 except (OSError, ValueError, IndexError) as exc: raise RuntimeError('Cannot estimate available RAM from /proc/meminfo') from exc raise RuntimeError('Cannot estimate available RAM: MemAvailable is missing') def _memory_preflight(memory: int) -> None: available = available_memory_mib() if memory + HOST_RESERVE_MIB > available: raise RuntimeError(f'Not enough available RAM: VM needs {memory} MiB plus a {HOST_RESERVE_MIB} MiB host reserve; ' f'{available} MiB available. Choose less --memory or stop another VM') def session_manifest(runtime: Runtime, manifest: Path | None = None) -> Path | None: """Use an inherited session only when it belongs to these runtime roots.""" explicit = manifest is not None inherited = os.environ.get('HYDRA_SESSION_MANIFEST') if not explicit and not inherited: return None path = Path(manifest if explicit else inherited).expanduser().absolute() try: data = json.loads(path.read_text(encoding='utf-8')) if (not isinstance(data, dict) or any(not isinstance(data.get(key), str) or not Path(data[key]).is_absolute() or Path(data[key]) != getattr(runtime, key) for key in ('config', 'state', 'runtime')) or not isinstance(data.get('names'), list) or any(not isinstance(name, str) for name in data['names']) or not isinstance(data.get('terminal'), str) or not Path(data['terminal']).is_absolute() or not isinstance(data.get('directory'), str) or Path(data['directory']) != path.parent): raise ValueError('Invalid session manifest or different runtime roots') for name in data['names']: validate_name(name) return path except (OSError, ValueError) as exc: if explicit: raise session.SessionError('Invalid session manifest; start a fresh Hydra OS session') from exc return None def _view_preflight(runtime: Runtime, *, no_view: bool, manifest: Path | None) -> Path | None: if no_view: return None selected = session_manifest(runtime, manifest) session._display() session._viewer() return selected def _show(runtime: Runtime, name: str, *, no_view: bool, windowed: bool, manifest: Path | None) -> int: if no_view: return 0 if manifest is not None: return session.request_refresh(manifest, selected=name) return session.view(runtime, name, windowed=windowed) def open_vm(runtime: Runtime, name: str, *, windowed=False, no_view=False, manifest: Path | None = None) -> int: profile = runtime.profile(name) current = runtime.status(name) status = current['status'] if status == 'unknown': raise RuntimeError(f'Cannot open {name}: unknown VM identity; {current.get("error", "inspect its QMP identity before retrying")}') manifest = _view_preflight(runtime, no_view=no_view, manifest=manifest) if status == 'stopped': _memory_preflight(profile.memory) runtime.start(name) # Other verified QEMU states (for example paused) are viewable without a # new process. Runtime.start owns the lock and final identity recheck. return _show(runtime, name, no_view=no_view, windowed=windowed, manifest=manifest) def run_source(runtime: Runtime, source: str | Path, *, name: str, memory=4096, cpus=2, disk_size='40G', sha256: str | None = None, windowed=False, no_view=False, manifest: Path | None = None, defaults: Path | None = None, progress=None) -> int: validate_name(name) profile_path = runtime.config / f'{name}.toml' if profile_path.exists() or profile_path.is_symlink(): raise RuntimeError(f'VM {name} already exists; use hydra open {name}') paths = runtime.paths(name) if paths.disk.parent.exists() or paths.disk.parent.is_symlink(): raise RuntimeError(f'Existing VM state/disk for {name}; choose a new name and preserve the old disk') if runtime.status(name)['status'] != 'stopped': raise RuntimeError(f'Cannot create {name}: existing or unknown VM identity') preferences = load_defaults(defaults) if defaults is not None else {} # Validate requested allocations before downloading or creating anything. Profile(name, memory=memory, cpus=cpus, disk_size=disk_size, **preferences) manifest = _view_preflight(runtime, no_view=no_view, manifest=manifest) _memory_preflight(memory) source = str(source) if '://' in source: iso = media.download_iso(source, sha256=sha256, progress=progress) else: iso = media.validate_iso(Path(source).expanduser().resolve(), sha256=sha256) # Recheck after a potentially lengthy transfer. Profile publication itself # is exclusive ('x') and Runtime never replaces an existing disk. if paths.disk.parent.exists() or paths.disk.parent.is_symlink(): raise RuntimeError(f'Existing VM state/disk for {name}; choose a new name') _memory_preflight(memory) runtime.add(Profile(name, iso, memory, cpus, disk_size, **preferences)) runtime.start(name) return _show(runtime, name, no_view=no_view, windowed=windowed, manifest=manifest)