feat(vm): add vm open to launch the VMware Workstation GUI
Open a VMX in the interactive Workstation GUI (not the headless vmrun path) for hands-on debugging of a clone. Spawns `vmware` detached (own session, stdio detached) and returns the child PID. Flags: --vmx (required), --power-on (-x), --fullscreen (-X), --new-window (-n), --display (sudo -u ci-runner strips $DISPLAY, so allow forcing it), --vmware-path. Warns when no X display is set. 7 tests; vm.py 96%.
This commit is contained in:
@@ -12,7 +12,9 @@ instead of scanning the local filesystem.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
@@ -483,4 +485,116 @@ def vm_new(
|
||||
click.echo(handle.identifier)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────── open
|
||||
|
||||
|
||||
def _launch_gui(argv: list[str], env: dict[str, str]) -> int:
|
||||
"""Spawn the VMware Workstation GUI detached. Returns the child PID.
|
||||
|
||||
Isolated so tests can monkeypatch it without spawning a real process. The
|
||||
GUI must outlive the CLI, so the child is started in its own session with
|
||||
its stdio detached.
|
||||
"""
|
||||
proc = subprocess.Popen( # argv is a fixed list, never a shell string
|
||||
argv,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
return proc.pid
|
||||
|
||||
|
||||
@vm.command("open")
|
||||
@click.option(
|
||||
"--vmx",
|
||||
"--vm-path",
|
||||
"vmx",
|
||||
required=True,
|
||||
help="Path to the VMX file to open in the VMware Workstation GUI.",
|
||||
)
|
||||
@click.option(
|
||||
"--vmware-path",
|
||||
"vmware_path",
|
||||
default="vmware",
|
||||
show_default=True,
|
||||
help="Path to the VMware Workstation GUI binary.",
|
||||
)
|
||||
@click.option(
|
||||
"--power-on",
|
||||
"power_on",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Power the VM on when it is opened (vmware -x).",
|
||||
)
|
||||
@click.option(
|
||||
"--fullscreen",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Power on and enter full-screen mode (vmware -X). Implies --power-on.",
|
||||
)
|
||||
@click.option(
|
||||
"--new-window",
|
||||
"new_window",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Open in a new window instead of a tab (vmware -n).",
|
||||
)
|
||||
@click.option(
|
||||
"--display",
|
||||
"display",
|
||||
default=None,
|
||||
help="X display to use (default: inherit $DISPLAY). e.g. ':0'.",
|
||||
)
|
||||
def vm_open(
|
||||
vmx: str,
|
||||
vmware_path: str,
|
||||
power_on: bool,
|
||||
fullscreen: bool,
|
||||
new_window: bool,
|
||||
display: str | None,
|
||||
) -> None:
|
||||
"""Open a VMX in the VMware Workstation GUI (run as the ci-runner user).
|
||||
|
||||
Launches the interactive GUI — unlike the headless ``vmrun`` path the rest
|
||||
of the orchestrator uses. The GUI needs an X display: when invoked through
|
||||
``sudo -u ci-runner`` the display is usually stripped, so authorise the
|
||||
service user first, e.g. ``xhost +SI:localuser:ci-runner``, and pass
|
||||
``--display :0`` (or export ``DISPLAY``).
|
||||
"""
|
||||
vmx_path = Path(vmx)
|
||||
if not vmx_path.is_file():
|
||||
raise click.ClickException(f"VMX file not found: {vmx}")
|
||||
|
||||
gui = shutil.which(vmware_path) or vmware_path
|
||||
|
||||
env = dict(os.environ)
|
||||
if display:
|
||||
env["DISPLAY"] = display
|
||||
if not env.get("DISPLAY"):
|
||||
click.echo(
|
||||
"[vm open] WARNING: no X display set ($DISPLAY empty and --display "
|
||||
"omitted). The GUI will fail to start. Pass --display :0 and run "
|
||||
"`xhost +SI:localuser:ci-runner` from a desktop session first.",
|
||||
err=True,
|
||||
)
|
||||
|
||||
argv = [gui]
|
||||
if new_window:
|
||||
argv.append("-n")
|
||||
if fullscreen:
|
||||
argv.append("-X")
|
||||
elif power_on:
|
||||
argv.append("-x")
|
||||
argv.append(str(vmx_path))
|
||||
|
||||
click.echo(f"[vm open] launching VMware GUI: {' '.join(argv)}")
|
||||
try:
|
||||
pid = _launch_gui(argv, env)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise click.ClickException(f"failed to launch VMware GUI: {exc}") from exc
|
||||
click.echo(f"[vm open] VMware GUI started (pid {pid}); display={env.get('DISPLAY', '')}")
|
||||
|
||||
|
||||
__all__ = ["cleanup_orphans", "vm"]
|
||||
|
||||
Reference in New Issue
Block a user