xorg: refuse a window that is not on the screen, and keep an inline screenshot within the bus (review of #154)
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered

A window that is unmapped or on a hidden workspace was cut from the screen anyway, which shows what is
there instead of it. An inline image of up to 4 MiB could not cross the bus, whose largest message is
1 MiB; it is now held to 700 KiB and halved in size until it fits, saying so.
This commit is contained in:
2026-10-09 12:23:51 +02:00
parent 52a4827983
commit 4fe8958dda
5 changed files with 120 additions and 13 deletions
+6 -3
View File
@@ -71,7 +71,7 @@ which is a cpp directive error under plain `xrdb -merge` but is skipped here.
| `xorg_input_devices` | r | the input devices, and each pointer's libinput settings |
| `xorg_input_set` | a | tap, tap drag, natural scroll, disable while typing, left handed, middle emulation, enabled, acceleration, by device id or name |
| `xorg_keyboard` | r/a | the XKB map; set layout, variant, model, options |
| `xorg_screenshot` | d | a PNG of the screen, one output or one window, inside the home (default `~/Pictures/Screenshots/`); with `inline`, the PNG in the answer as base64 (at most 4 MiB) |
| `xorg_screenshot` | d | a PNG of the screen, one output or one window, inside the home (default `~/Pictures/Screenshots/`); with `inline`, the PNG in the answer as base64 (at most 700 KiB; a larger one is halved in size until it fits, and `inline_scale` says so). A window that is not viewable is refused |
| `xorg_windows` | r | the windows as the X server knows them, managed or not: id, name, instance and class, size and place; with a class or name filter (or `detail`), each one's depth (`argb`), visual, map state, override-redirect, types, states, role and own opacity |
| `xorg_x_log` | r | the X server's log for the running session: version, start, errors (and warnings) |
@@ -95,8 +95,11 @@ into a PNG itself. **A window is cut from the dump of the whole screen** at the
never dumped by its id: `xwd -id` asks the server for the window's colormap entries, which a window of
a 32-bit (alpha) visual refused with `BadColor (X_QueryColors)` on a workstation (2026-10-09), and the
cut shows the window as the compositor shows it, which is what a question about a window that looks
wrong needs. A window under another one shows what covers it. `inline` answers the image itself, for a
session that cannot read the machine's files.
wrong needs. A window under another one shows what covers it, and a window that is not viewable (unmapped, or on a
workspace not shown) is refused rather than photographed as whatever is at its place. `inline` answers
the image itself, for a session that cannot read the machine's files: a tool's answer crosses the bus,
whose largest message is NATS's default of 1 MiB (the `nats` module sets no other), so the PNG is held to
700 KiB (about 935 KiB as base64) and halved in size until it fits.
`xorg_windows` exists because a window manager lists only the windows it manages, and says nothing a
compositor goes by. A window of depth 32 has an alpha channel: the compositor blends it with what is
+3 -2
View File
@@ -146,12 +146,13 @@ func tools(d desktop.Desk) []stdio.Tool {
Description: "Take a screenshot to a PNG file: the whole screen, one output, or one window by its X id " +
"(cut from the screen as the compositor shows it, so a see-through window shows what is behind it). " +
"Written under the account's home (default ~/Pictures/Screenshots/screenshot-<time>.png). Answers " +
"the path, size and bytes; with inline, also the PNG itself as base64 (at most 4 MiB).",
"the path, size and bytes; with inline, also the PNG itself as base64 (at most 700 KiB, halved in size until it " +
"fits; inline_scale says so). A window that is not viewable (unmapped, or on a workspace not shown) is refused.",
Input: desktop.Schema(map[string]any{
"output": desktop.Str("only this output (optional)"),
"window": desktop.Str("only this window, by its X id, e.g. 0x3a00007 (optional)"),
"path": desktop.Str("where to write it, inside the home (optional)"),
"inline": desktop.Flag("also answer the PNG as base64, for a session that cannot read the file (default false)"),
"inline": desktop.Flag("also answer the PNG as base64, at most 700 KiB, for a session that cannot read the file (default false)"),
}),
Run: call(x.screenshot),
},
+47 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
@@ -495,8 +496,28 @@ func (x xorg) keyboard(ctx context.Context, a desktop.Args) (any, error) {
var windowID = regexp.MustCompile(`^(0x[0-9a-fA-F]{1,8}|[0-9]{1,10})$`)
// mostInline is the largest PNG a screenshot carries in its answer.
const mostInline = 4 << 20
// mostInline is the largest PNG a screenshot carries in its answer. A tool's answer crosses the bus,
// whose largest message is 1 MiB; 700 KiB is about 935 KiB as base64, which leaves room for the rest.
const mostInline = 700 << 10
// shrink averages each by×by block of an image into one pixel.
func shrink(img image.Image, by int) *image.RGBA {
b := img.Bounds()
out := image.NewRGBA(image.Rect(0, 0, b.Dx()/by, b.Dy()/by))
for y := 0; y < out.Bounds().Dy(); y++ {
for x := 0; x < out.Bounds().Dx(); x++ {
var r, g, bl, n uint32
for dy := 0; dy < by; dy++ {
for dx := 0; dx < by; dx++ {
cr, cg, cb, _ := img.At(b.Min.X+x*by+dx, b.Min.Y+y*by+dy).RGBA()
r, g, bl, n = r+cr, g+cg, bl+cb, n+1
}
}
out.SetRGBA(x, y, color.RGBA{uint8(r / n >> 8), uint8(g / n >> 8), uint8(bl / n >> 8), 0xff})
}
}
return out
}
func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
inline, _, err := a.Bool("inline")
@@ -535,6 +556,12 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
if err != nil {
return nil, err
}
// A window that is not viewable is not on the screen: its rectangle would show what is there
// instead, which is a picture of the wrong thing.
if g.Mapped != "" && g.Mapped != "IsViewable" {
return nil, fmt.Errorf("window %s is %s, not on the screen (IsUnMapped: not shown; IsUnviewable: "+
"its parent is not shown, e.g. on another workspace): bring it to the screen first", w, g.Mapped)
}
cut = &g
}
args := []string{"-silent", "-out", dump.Name(), "-root"}
@@ -593,11 +620,25 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
b := shot.Bounds()
answer := map[string]any{"session": s.Display, "path": path, "width": b.Dx(), "height": b.Dy(), "bytes": buf.Len()}
if inline {
// For a session that cannot read the machine's files: the image itself, within a bound.
if buf.Len() > mostInline {
answer["inline"] = fmt.Sprintf("not included: %d bytes is more than %d; take one output or one window", buf.Len(), mostInline)
// For a session that cannot read the machine's files: the image itself, within the bus's bound.
// An image larger than mostInline is halved until it fits, and the answer says by how much; the
// file keeps the full size.
small, scale := buf.Bytes(), 1
for len(small) > mostInline && shot.Bounds().Dx()/(scale*2) >= 64 {
scale *= 2
var b bytes.Buffer
if err := png.Encode(&b, shrink(shot, scale)); err != nil {
return nil, err
}
small = b.Bytes()
}
if len(small) > mostInline {
answer["inline"] = fmt.Sprintf("not included: %d bytes even at 1/%d; take one output or one window", len(small), scale)
} else {
answer["png_base64"] = base64.StdEncoding.EncodeToString(buf.Bytes())
answer["png_base64"] = base64.StdEncoding.EncodeToString(small)
if scale > 1 {
answer["inline_scale"] = fmt.Sprintf("1/%d of the file's size, to fit the bus", scale)
}
}
}
return answer, nil
+8 -2
View File
@@ -219,8 +219,11 @@ func (x xorg) windowGeometry(ctx context.Context, env []string, id string) (imag
return ParseGeometry(r.Stdout)
}
// image4 is a rectangle on the screen.
type image4 struct{ X, Y, W, H int }
// image4 is a rectangle on the screen, and the window's map state there.
type image4 struct {
X, Y, W, H int
Mapped string
}
// ParseGeometry reads the absolute corner and the size from `xwininfo -id`.
func ParseGeometry(text string) (image4, error) {
@@ -231,6 +234,9 @@ func ParseGeometry(text string) (image4, error) {
for prefix, into := range map[string]*int{
"Absolute upper-left X:": &g.X, "Absolute upper-left Y:": &g.Y, "Width:": &g.W, "Height:": &g.H,
} {
if strings.HasPrefix(line, "Map State:") {
g.Mapped = strings.TrimSpace(strings.TrimPrefix(line, "Map State:"))
}
if strings.HasPrefix(line, prefix) {
if n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, prefix))); err == nil {
*into = n
+56
View File
@@ -164,3 +164,59 @@ func TestAWindowsScreenshotIsCutFromTheScreenNotDumpedByItsID(t *testing.T) {
t.Fatal("no geometry is refused")
}
}
// screenFake is a desk whose xwd writes a w×h screen of noise and whose xwininfo
// answers one window with the given map state.
func screenFake(t *testing.T, w, h int, state string) desktop.Desk {
f := &fakeDesk{answers: map[string]desktop.Result{
"xwininfo -id 0x2a00221": {Stdout: " Absolute upper-left X: 1\n Absolute upper-left Y: 1\n Width: 2\n Height: 2\n Map State: " + state + "\n"},
}}
d := f.desk(true)
run := d.Run
d.Run = func(ctx context.Context, env []string, stdin []byte, name string, args ...string) desktop.Result {
if name == "xwd" {
for i, a := range args {
if a == "-out" {
// Noise, which a PNG cannot compress: the worst case for the bound.
os.WriteFile(args[i+1], xwdOf(w, h, true, func(x, y int) (uint8, uint8, uint8) {
n := uint32(x)*2654435761 ^ uint32(y)*2246822519
n ^= n >> 13
n *= 3266489917
return uint8(n), uint8(n >> 8), uint8(n >> 16)
}), 0o644)
}
}
return desktop.Result{}
}
return run(ctx, env, stdin, name, args...)
}
return d
}
func TestAWindowThatIsNotViewableIsNotPhotographed(t *testing.T) {
home := t.TempDir()
t.Setenv("MESH_OPERATOR_HOME", home)
for _, state := range []string{"IsUnMapped", "IsUnviewable"} {
_, err := byName(t, screenFake(t, 4, 3, state), "xorg_screenshot")(map[string]any{"window": "0x2a00221", "path": "~/s.png"})
if err == nil || !strings.Contains(err.Error(), state) {
t.Errorf("%s: %v", state, err)
}
}
}
func TestAnInlineImageFitsTheBusAndIsScaledDownWhenItWouldNot(t *testing.T) {
home := t.TempDir()
t.Setenv("MESH_OPERATOR_HOME", home)
got, err := byName(t, screenFake(t, 800, 600, "IsViewable"), "xorg_screenshot")(map[string]any{"path": "~/s.png", "inline": true})
if err != nil {
t.Fatal(err)
}
a := got.(map[string]any)
b64, _ := a["png_base64"].(string)
if b64 == "" || len(b64) > 1<<20-64<<10 {
t.Fatalf("an inline image within the bus's 1 MiB, with room for the answer: %d bytes; %v", len(b64), a["inline"])
}
if a["bytes"].(int) > mostInline && a["inline_scale"] == nil {
t.Fatalf("a file larger than the bound is answered scaled down, and says so: %v", a["inline_scale"])
}
}