#!/usr/bin/env python3 """Grab one full framebuffer from a no-auth VNC server and save as PNG. Why VNC and not QMP screendump: with virtio-vga-gl the guest renders into a GL scanout and screendump fails with "no surface"; QEMU's VNC server reads the GL framebuffer back, so this always works. Pair the VM with `-display egl-headless -vnc 127.0.0.1:17`. Stdlib only (manual RFB handshake + raw-encoding framebuffer + PNG writer) so it runs anywhere python3 exists. Usage: vncshot.py [host] [port] # default 127.0.0.1:5917 """ import socket, struct, sys, zlib def recv_exact(s, n): buf = b"" while len(buf) < n: chunk = s.recv(n - len(buf)) if not chunk: raise EOFError("server closed") buf += chunk return buf def png_write(path, w, h, rgb): def chunk(tag, data): c = struct.pack(">I", len(data)) + tag + data return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) raw = b"".join(b"\x00" + rgb[y * w * 3:(y + 1) * w * 3] for y in range(h)) out = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw, 6)) + chunk(b"IEND", b"")) with open(path, "wb") as f: f.write(out) def main(): out = sys.argv[1] host = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1" port = int(sys.argv[3]) if len(sys.argv) > 3 else 5917 s = socket.create_connection((host, port), timeout=30) s.settimeout(30) ver = recv_exact(s, 12) # "RFB 003.008\n" s.sendall(b"RFB 003.008\n") ntypes = recv_exact(s, 1)[0] types = recv_exact(s, ntypes) if 1 not in types: sys.exit(f"no 'None' security type offered: {list(types)}") s.sendall(b"\x01") res = struct.unpack(">I", recv_exact(s, 4))[0] if res != 0: sys.exit("security handshake failed") s.sendall(b"\x01") # ClientInit: shared w, h = struct.unpack(">HH", recv_exact(s, 4)) recv_exact(s, 16) # server pixel format (ignored) namelen = struct.unpack(">I", recv_exact(s, 4))[0] recv_exact(s, namelen) # SetPixelFormat: 32bpp truecolor, shifts r=16 g=8 b=0 (little-endian BGRX) pf = struct.pack(">BBBBHHHBBBxxx", 32, 24, 0, 1, 255, 255, 255, 16, 8, 0) s.sendall(b"\x00\x00\x00\x00" + pf) s.sendall(struct.pack(">BxH i", 2, 1, 0)) # SetEncodings: Raw only s.sendall(struct.pack(">BBHHHH", 3, 0, 0, 0, w, h)) # full update request fb = bytearray(w * h * 4) got = False while not got: mtype = recv_exact(s, 1)[0] if mtype == 0: # FramebufferUpdate recv_exact(s, 1) nrects = struct.unpack(">H", recv_exact(s, 2))[0] for _ in range(nrects): x, y, rw, rh = struct.unpack(">HHHH", recv_exact(s, 8)) enc = struct.unpack(">i", recv_exact(s, 4))[0] if enc == 0: data = recv_exact(s, rw * rh * 4) for row in range(rh): off = ((y + row) * w + x) * 4 fb[off:off + rw * 4] = data[row * rw * 4:(row + 1) * rw * 4] elif enc == -224: # LastRect break else: sys.exit(f"unsupported encoding {enc}") got = True elif mtype == 2: # Bell pass elif mtype == 3: # ServerCutText recv_exact(s, 3) ln = struct.unpack(">I", recv_exact(s, 4))[0] recv_exact(s, ln) else: sys.exit(f"unexpected server message {mtype}") rgb = bytearray(w * h * 3) for i in range(w * h): rgb[i * 3 + 0] = fb[i * 4 + 2] rgb[i * 3 + 1] = fb[i * 4 + 1] rgb[i * 3 + 2] = fb[i * 4 + 0] png_write(out, w, h, bytes(rgb)) print(f"saved {out} ({w}x{h})") if __name__ == "__main__": main()