2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-10-20 14:45:45 +00:00

Relay console keystrokes from a single ordered consumer

nodeconsole spawned a task per input byte from the stdin reader callback
and kept no reference to it.  Two of those tasks overlap as soon as one
parks in relay_keypresses waiting on the VNC connection, so keystrokes
can reach the node out of order and the escape sequence state (buffer,
inputcontext, modkeys) is mutated by more than one task at a time.  With
the first keystroke relaying slowly, typing abcdef arrives as bcdefa.

Those tasks were also unreferenced, which asyncio documents as
collectable while still pending, so a keypress could be dropped.

Queue the bytes in the reader callback and process them from one
long-lived task instead.  Keep a reference to the watch_input task as
well, since collecting that one takes the whole input handler with it.
This commit is contained in:
Markus Hilger
2026-07-27 06:28:41 +02:00
parent cfcb406dba
commit 95d6c96363
+14 -3
View File
@@ -254,14 +254,23 @@ class InputHandler:
currloop = asyncio.get_running_loop()
self.fd = sys.stdin.fileno()
self.seqtimeout = None
self.pendinginput = asyncio.Queue()
self.inputconsumer = asyncio.create_task(self.consume_input())
currloop.add_reader(self.fd, self.handle_input)
return self
def handle_input(self):
data = os.read(self.fd, 1)
if not data:
return
asyncio.create_task(self.process_input(data))
self.pendinginput.put_nowait(data)
async def consume_input(self):
# One consumer, so a keystroke that blocks relaying cannot be overtaken
# by the next one, and the sequence buffer is only touched by one task
while True:
data = await self.pendinginput.get()
await self.process_input(data)
async def timeout_sequence(self, timeout=3, flushbuffer=False):
await asyncio.sleep(timeout)
@@ -814,15 +823,17 @@ def redraw():
sys.stdout.write('\n')
sys.stdout.flush()
resized = False
inputwatcher = None
async def do_screenshot():
global streaming
global resized
global numrows
global firstnodename
global inputwatcher
sess = client.Command()
if streaming:
asyncio.create_task(watch_input())
inputwatcher = asyncio.create_task(watch_input())
if options.tile:
imageformat = os.environ.get('CONFLUENT_IMAGE_PROTOCOL', 'kitty')
if imageformat not in ('kitty', 'iterm'):