1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
|
#!/usr/bin/env python3
"""Big kitty images over the real socket, with a reader that stalls.
The headless suite proves the graphics *model*; this proves the delivery path
it cannot see: a pane image is re-emitted to the client as decoded RGBA, which
is megabytes of base64 in one indivisible message. Two bugs lived there, both
invisible headless:
- the outbox cap was applied to backlog *plus message*, so a message bigger
than the cap was silently undeliverable forever -- and the model advanced
anyway, losing the deletions that rode in the dropped frame. The visible
symptom was a screenshot parked at a fixed spot on the screen, immune to
scrolling, forever.
- once the cap bounded only the backlog, a queued image legitimately held
the backlog over the cap until it drained -- and push_frame read that as
a dead client and closed the connection. The visible symptom was the
session auto-detaching whenever an image crossed the wire.
So: a pane draws an image whose transmission exceeds MAX_OUTBOX, while the
client reads nothing. The session must stay attached, the whole transmission
must arrive once the reader comes back, and scrolling the image away must
deliver its deletion.
"""
import fcntl
import os
import pty
import re
import select
import struct
import sys
import termios
import time
import uuid
BIN = os.environ.get(
"SLOSH_BIN", os.path.join(os.path.dirname(__file__), "..", "build", "slosh")
)
SESSION = "test-" + uuid.uuid4().hex[:8]
# 1100x1000 RGB is 3.3MB raw, 4.4MB as base64: bigger than the server's 4MB
# outbox cap, so delivery *requires* the cap to bound backlog, not messages.
CHILD = r"""
import base64, sys, time
w, h = 1100, 1000
img = base64.b64encode(bytes([200, 60, 60]) * (w * h)).decode()
sys.stdout.write("\x1b_Ga=T,f=24,q=2,s=%d,v=%d,i=7,c=40,r=10,m=1;\x1b\\" % (w, h))
for off in range(0, len(img), 4096):
last = off + 4096 >= len(img)
sys.stdout.write("\x1b_Gm=%d;%s\x1b\\" % (0 if last else 1, img[off:off+4096]))
sys.stdout.write("\r\nIMG-SENT\r\n")
sys.stdout.flush()
time.sleep(2) # visible for a few frames, like a screenshot you read past
for i in range(200):
sys.stdout.write("line %d\r\n" % i) # ...and then scrolled away
sys.stdout.flush()
time.sleep(30)
"""
# A small image that stays put while the screen around it changes: the counter
# repaints one line in place (\r, no newline), so nothing scrolls and the
# picture neither moves nor changes. Every frame it provokes is a frame that
# must NOT say the placement again.
STILL = r"""
import base64, sys, time
px = base64.b64encode(bytes([255, 0, 0] * 8)).decode() # 4x2 RGB
sys.stdout.write("\x1b_Ga=T,f=24,q=2,s=4,v=2,i=7,c=6,r=2;%s\x1b\\\r\n" % px)
sys.stdout.flush()
for i in range(120):
sys.stdout.write("tick %d \r" % i)
sys.stdout.flush()
time.sleep(0.02)
time.sleep(30)
"""
# One image, two placements, then the pixels replaced -- which deletes both
# placements on the client, so the program says them again, same geometry.
# Both must reach the client again: "unchanged" is measured against what the
# client is holding, and after a retransmit it is holding neither.
REPLACED = r"""
import base64, sys, time
px1 = base64.b64encode(bytes([255, 0, 0] * 8)).decode() # 4x2 red
px2 = base64.b64encode(bytes([0, 255, 0] * 8)).decode() # 4x2 green
def both(px):
sys.stdout.write("\x1b[1;1H")
sys.stdout.write("\x1b_Ga=t,f=24,q=2,s=4,v=2,i=7;%s\x1b\\" % px)
sys.stdout.write("\x1b_Ga=p,i=7,p=1,q=2,c=6,r=2\x1b\\")
sys.stdout.write("\x1b[1;20H")
sys.stdout.write("\x1b_Ga=p,i=7,p=2,q=2,c=6,r=2\x1b\\")
sys.stdout.flush()
both(px1)
time.sleep(2)
both(px2)
time.sleep(30)
"""
def spawn(cols=80, rows=24, child=None, session=None):
pid, fd = pty.fork()
if pid == 0:
os.environ["TERM"] = "xterm-ghostty"
os.environ["SLOSH_CONFIG"] = "/nonexistent/slosh.kdl"
os.execv(
BIN, [BIN, "-s", session or SESSION, "--", "python3", "-c", child or CHILD]
)
os._exit(127)
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
return pid, fd
def drain(fd, idle=0.4, limit=8.0):
buf, last, start = b"", time.time(), time.time()
while time.time() - start < limit:
r, _, _ = select.select([fd], [], [], 0.05)
if r:
try:
chunk = os.read(fd, 65536)
except OSError:
break
if not chunk:
break
buf += chunk
last = time.time()
elif time.time() - last > idle:
break
return buf
def unsynced(buf):
"""kitty commands that reach the client outside a synchronized update.
A frame is a cell diff and then the placements that sit on top of it. A
terminal that presents the diff before the placements arrive draws the
cells under a picture with no picture on them, so every image blinks once
per frame -- which is what happens the moment anything re-chunks the
stream on the way out (a mirror reading the pty a few KB at a time). The
markers are what stop the halves being presented separately, so every
image command has to be inside a pair.
"""
depth, out = 0, []
for m in re.finditer(rb"\x1b\[\?2026h|\x1b\[\?2026l|\x1b_G([^\x1b]*)\x1b", buf):
if m.group() == b"\x1b[?2026h":
depth += 1
elif m.group() == b"\x1b[?2026l":
depth -= 1
elif depth <= 0:
out.append(m.group(1)[:40])
return out
def check(name, cond, detail=""):
print(f"{'ok ' if cond else 'FAIL'} {name}{'' if cond else ' <- ' + detail}")
return cond
def gfx_commands(buf):
"""(controls, payload_len) for every _G escape in the stream."""
out = []
for m in re.finditer(rb"\x1b_G([^\x1b]*)\x1b", buf):
ctl, _, payload = m.group(1).partition(b";")
out.append((ctl, len(payload)))
return out
def main():
ok = True
pid, fd = spawn()
# Say nothing, hear nothing: the child pours the image into its pty while
# the client's own output backs up unread, which is what a slow terminal
# over ssh looks like from the server's side.
time.sleep(4.0)
buf = drain(fd, idle=0.6, limit=30.0)
ok &= check(
"the display stayed attached through the flood",
os.waitpid(pid, os.WNOHANG) == (0, 0),
repr(buf[-120:]),
)
sent = 0
for ctl, n in gfx_commands(buf):
if b"a=t" in ctl or (b"a=" not in ctl and b"m=" in ctl):
sent += n
ok &= check(
"a transmission bigger than the outbox cap arrives whole",
sent >= (1100 * 1000 * 3 + 2) // 3 * 4,
f"{sent} bytes of base64 reached the client",
)
cmds = gfx_commands(buf)
placed = any(ctl.startswith(b"a=p") for ctl, _ in cmds)
ok &= check("and the image is placed while visible", placed, repr(buf[-300:]))
loose = unsynced(buf)
ok &= check(
"every image command travels inside a synchronized update",
not loose,
f"{len(loose)} outside: {loose[:4]}",
)
# The child's flood scrolled it away while nothing was being read: the
# deletion rode a frame the full outbox may well have dropped, and it must
# be said again until it lands -- or the client draws the placement at a
# fixed spot forever, which is exactly how this was reported.
deleted = any(ctl.startswith(b"a=d,d=i") for ctl, _ in cmds)
ok &= check("scrolled away, the placement is deleted", deleted, repr(buf[-300:]))
# Scrolling back up must re-place it, and from the transmission the client
# already has: no second copy of the pixels.
buf = b""
for _ in range(20):
os.write(fd, b"\x01\x1b[5~") # C-a PageUp
buf += drain(fd, idle=0.1, limit=3.0)
buf += drain(fd, idle=0.4, limit=8.0)
back = [ctl for ctl, _ in gfx_commands(buf)]
ok &= check(
"scrolled back, it is placed again",
any(c.startswith(b"a=p") for c in back),
repr(buf[-300:]),
)
ok &= check(
"without retransmitting the pixels",
not any(c.startswith(b"a=t") for c in back),
str(back[:6]),
)
# A second terminal has its own kitty image namespace. The first client's
# `sent` bit must not make a new viewer receive only a placement for pixels
# its terminal has never seen.
pid2, fd2 = spawn()
second = drain(fd2, idle=0.6, limit=30.0)
second_cmds = gfx_commands(second)
ok &= check(
"a joining client receives its own image transmission",
any(c.startswith(b"a=t") for c, _ in second_cmds),
str(second_cmds[:8]),
)
ok &= check(
"and joining it leaves the first client attached",
os.waitpid(pid, os.WNOHANG) == (0, 0),
)
# A placement is a standing instruction, not a per-frame one: a picture
# that has not moved must not be re-sent while the screen around it
# repaints. Re-sending it is invisible on a terminal that keeps the pixels
# and ruinous on anything mirroring the stream, which re-decodes the image
# and rebuilds its view every time it hears the placement again.
pid3, fd3 = spawn(child=STILL, session=SESSION + "-still")
still = drain(fd3, idle=0.6, limit=20.0)
places = [ctl for ctl, _ in gfx_commands(still) if ctl.startswith(b"a=p")]
frames = still.count(b"\x1b[?2026h")
ok &= check(
"a still picture is placed once, however much repaints around it",
len(places) == 1 and frames > 10,
f"{len(places)} placements across {frames} frames",
)
os.kill(pid3, 9)
# The other edge of "place once": a retransmit invalidates every placement
# of the image (kitty deletes them all when the data is replaced), not
# just the one that rode in the same call as the transmit. Un-invalidated,
# the second placement is judged unchanged and never said again -- a
# picture the client deleted and we believe it still has.
pid4, fd4 = spawn(child=REPLACED, session=SESSION + "-replaced")
replaced = drain(fd4, idle=3.5, limit=20.0)
transmits = [m.start() for m in re.finditer(rb"\x1b_Ga=t,", replaced)]
replaces = (
[c for c, _ in gfx_commands(replaced[transmits[1] :]) if c.startswith(b"a=p")]
if len(transmits) >= 2
else []
)
ok &= check(
"a retransmit re-places every placement of the image",
len(transmits) >= 2 and len(replaces) == 2,
f"{len(transmits)} transmits, then {len(replaces)} placements: {replaces[:4]}",
)
os.kill(pid4, 9)
os.write(fd2, b"\x01q")
drain(fd2, idle=0.2, limit=2.0)
for child in (pid, pid2):
try:
os.kill(child, 9)
except ProcessLookupError:
pass
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
|