vmshot: make Pillow optional so --one works anywhere, and document the invocation that supplies it
This commit is contained in:
parent
a0caaa32fa
commit
96f2da8d2f
1 changed files with 41 additions and 4 deletions
|
|
@ -45,6 +45,22 @@ Usage
|
||||||
tools/vmshot.py --one 140 # full-size single guest, no sheet
|
tools/vmshot.py --one 140 # full-size single guest, no sheet
|
||||||
tools/vmshot.py --cols 3 --width 900
|
tools/vmshot.py --cols 3 --width 900
|
||||||
|
|
||||||
|
**Run it as ``tools/vmshot.py``, not ``python3 tools/vmshot.py``.** The shebang is
|
||||||
|
``uv run --with pillow``, so invoking the file directly supplies Pillow even on a host
|
||||||
|
that has none; putting an interpreter in front of it throws that away. Lane BR hit
|
||||||
|
exactly this and concluded the tool was unusable from the WSL host. ``--one`` now works
|
||||||
|
either way -- it writes the guest's framebuffer bytes verbatim and needs no Pillow --
|
||||||
|
and only the contact sheet requires it, with the install line in the error.
|
||||||
|
|
||||||
|
For a single frame during a measurement run, going straight to the host is better still
|
||||||
|
and is what lane BR ended up doing::
|
||||||
|
|
||||||
|
ssh spicy "echo 'screendump /tmp/x.png -f png' | qm monitor 146"
|
||||||
|
|
||||||
|
That is the *live* framebuffer. The wall at http://192.168.3.201:8140/ serves a cached
|
||||||
|
frame up to a poll cycle (~5 s) old, which reads exactly like a swallowed click and has
|
||||||
|
cost two lanes wasted clicks.
|
||||||
|
|
||||||
Output lands in ``~/sots-re/dumps/vmshot/`` (``dumps/`` is gitignored).
|
Output lands in ``~/sots-re/dumps/vmshot/`` (``dumps/`` is gitignored).
|
||||||
|
|
||||||
A guest that is stopped, paused, or whose screendump fails gets a labelled
|
A guest that is stopped, paused, or whose screendump fails gets a labelled
|
||||||
|
|
@ -68,7 +84,27 @@ import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
# Pillow is needed only to composite the contact sheet. `--one` writes the guest's
|
||||||
|
# framebuffer bytes straight through, and that is the mode a measurement lane actually
|
||||||
|
# wants -- lane BR could not use this tool at all from the WSL host because the import
|
||||||
|
# was unconditional and Pillow is not installed there. Fail at the point of use, with
|
||||||
|
# the fix in the message, rather than at import.
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
_PIL_ERROR = None
|
||||||
|
except ImportError as e: # pragma: no cover - depends on the host
|
||||||
|
Image = ImageDraw = ImageFont = None # type: ignore[assignment]
|
||||||
|
_PIL_ERROR = e
|
||||||
|
|
||||||
|
|
||||||
|
def _require_pil() -> None:
|
||||||
|
if _PIL_ERROR is not None:
|
||||||
|
sys.exit(
|
||||||
|
f"vmshot: the contact sheet needs Pillow, which is not installed here ({_PIL_ERROR}).\n"
|
||||||
|
" `--one <id>` works without it and writes the guest's framebuffer verbatim.\n"
|
||||||
|
" To build sheets on this host: uv pip install pillow"
|
||||||
|
)
|
||||||
|
|
||||||
HOST = "spicy"
|
HOST = "spicy"
|
||||||
SERVICE = os.environ.get("VMWATCH_URL", "http://192.168.3.201:8140")
|
SERVICE = os.environ.get("VMWATCH_URL", "http://192.168.3.201:8140")
|
||||||
|
|
@ -204,7 +240,7 @@ def grab(ids: list[int], per_vm_timeout: int = 20) -> dict[int, dict]:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
def _font(size: int, bold: bool = False):
|
||||||
try:
|
try:
|
||||||
return ImageFont.truetype(FONT_BOLD if bold else FONT_PATH, size)
|
return ImageFont.truetype(FONT_BOLD if bold else FONT_PATH, size)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|
@ -223,7 +259,7 @@ def _clean_monitor_log(log: str) -> str:
|
||||||
return "; ".join(keep)[:180]
|
return "; ".join(keep)[:180]
|
||||||
|
|
||||||
|
|
||||||
def make_tile(vmid: int, name: str, info: dict, width: int, stamp: str) -> Image.Image:
|
def make_tile(vmid: int, name: str, info: dict, width: int, stamp: str):
|
||||||
png = info.get("png")
|
png = info.get("png")
|
||||||
status = info.get("status", "?")
|
status = info.get("status", "?")
|
||||||
err = _clean_monitor_log(info.get("log", ""))
|
err = _clean_monitor_log(info.get("log", ""))
|
||||||
|
|
@ -301,7 +337,8 @@ def make_tile(vmid: int, name: str, info: dict, width: int, stamp: str) -> Image
|
||||||
return tile
|
return tile
|
||||||
|
|
||||||
|
|
||||||
def contact_sheet(fleet, shots, tile_w: int, cols: int, stamp: str) -> Image.Image:
|
def contact_sheet(fleet, shots, tile_w: int, cols: int, stamp: str):
|
||||||
|
_require_pil()
|
||||||
tiles = [make_tile(vid, name, shots.get(vid, {}), tile_w, stamp) for vid, name, _ in fleet]
|
tiles = [make_tile(vid, name, shots.get(vid, {}), tile_w, stamp) for vid, name, _ in fleet]
|
||||||
rows = (len(tiles) + cols - 1) // cols
|
rows = (len(tiles) + cols - 1) // cols
|
||||||
row_h = [
|
row_h = [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue