new version full rebuild with claude opus 4.6 and own rdp daemon

This commit is contained in:
felixg
2026-02-28 21:19:52 +01:00
parent fac33c27b4
commit 6e9956bce9
19 changed files with 2185 additions and 326 deletions

View File

@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""
Patch guacamole-server source files to fix bugs in git main that abort()
or segfault the child process in debug builds (no -DNDEBUG).
--- Fix 1: src/libguac/display-flush.c ---
The frame-flush loop iterates over display layers to diff pending vs last frame.
When a layer has pending_frame.buffer == NULL (e.g. cursor/aux layers that have
never been drawn), the code does:
GUAC_ASSERT(current->pending_frame.buffer_is_external);
continue; <- BUG: never advances `current`
Two problems:
1. GUAC_ASSERT aborts the process if buffer_is_external is false, which happens
for any fresh layer that has not yet received display data. In a debug build
(no -DNDEBUG) this is a real assert -> abort() -> child process dies.
2. If the assert passes, `continue` loops forever on the same layer.
Fix: replace the GUAC_ASSERT with a proper pointer advance before the continue.
--- Fix 2: src/libguac/display-plan.c ---
Identical bug: the same GUAC_ASSERT(current->pending_frame.buffer_is_external)
pattern exists in display-plan.c (same layer-iteration loop, same missing pointer
advance before continue). Applying the same fix.
--- Fix 3: src/libguac/user.c ---
guac_user_supports_webp() iterates user->info.image_mimetypes without checking
whether the pointer itself is NULL. If the client did not send any image MIME
types (or guac_copy_mimetypes() returned NULL), the very first dereference
`*mimetype` causes a SIGSEGV in the display worker thread.
Fix: insert a NULL guard immediately after the pointer is loaded from the struct.
--- Fix 6: src/protocols/rdp/gdi.c (guac_rdp_gdi_end_paint) ---
With disable-gfx/legacy RDP, many Windows bitmap updates do NOT include
FrameMarker PDUs. guac_display_render_thread_notify_frame() (which wakes the
render thread to encode dirty tiles) is ONLY called from guac_rdp_gdi_mark_frame
— the FrameMarker handler. Legacy bitmaps that arrive without FrameMarkers mark
regions dirty but never wake the render thread → tiles are never encoded →
black squares on screen that only appear when a subsequent hover redraw (which
DOES carry a FrameMarker) finally wakes the thread for that region.
Root cause: the gdi_modified → notify_modified path in the main event loop only
fires AFTER the RDP event queue drains. During a continuous burst of bitmap
updates (initial desktop load), the queue never drains, so notify_modified is
never called, and the render thread sleeps indefinitely. Additionally, the render
thread applies lag compensation: if the browser hasn't ACK'd the last sync
instruction, processing_lag can reach 500 ms, and the render thread pauses up to
500 ms between flushes. Both factors combine to cause large areas of the screen
to remain black throughout the initial render.
Fix: call notify_frame unconditionally on every EndPaint. The render thread
naturally rate-limits output via MIN_FRAME_DURATION (10 ms = 100 fps max). To
prevent the processing_lag from reaching the 500 ms cap (which would cause the
render thread to throttle itself), the Node.js proxy (rdp.ts) immediately echoes
every sync instruction received from guacd back to guacd as a client ACK. This
keeps processing_lag near 0 ms so the render thread flushes as fast as
MIN_FRAME_DURATION allows. The browser's own sync ACKs are dropped by the proxy
to avoid double-updating the processing_lag measurement.
"""
import sys
def patch_file_replace(path, search_string, replacement_fn, description):
"""Replace the line containing search_string with replacement_fn(indent)."""
with open(path) as f:
lines = f.readlines()
patched = False
i = 0
while i < len(lines):
if search_string in lines[i]:
indent = lines[i][: len(lines[i]) - len(lines[i].lstrip())]
lines[i] = indent + replacement_fn(indent) + "\n"
patched = True
break
i += 1
if not patched:
print("ERROR: patch target not found in " + path + " (" + description + ")", file=sys.stderr)
sys.exit(1)
with open(path, "w") as f:
f.writelines(lines)
print("Patched " + path + " (" + description + ")")
def patch_file_insert_after(path, search_string, insert_lines, description):
"""Insert insert_lines (with matching indent) after the line containing search_string."""
with open(path) as f:
lines = f.readlines()
patched = False
i = 0
while i < len(lines):
if search_string in lines[i]:
indent = lines[i][: len(lines[i]) - len(lines[i].lstrip())]
for j, new_line in enumerate(insert_lines):
lines.insert(i + 1 + j, indent + new_line + "\n")
patched = True
break
i += 1
if not patched:
print("ERROR: patch target not found in " + path + " (" + description + ")", file=sys.stderr)
sys.exit(1)
with open(path, "w") as f:
f.writelines(lines)
print("Patched " + path + " (" + description + ")")
# Fix 1: display-flush.c
patch_file_replace(
"src/libguac/display-flush.c",
"GUAC_ASSERT(current->pending_frame.buffer_is_external)",
lambda indent: "current = current->pending_frame.next;",
"replace GUAC_ASSERT with pointer advance",
)
# Fix 2: display-plan.c (identical bug pattern)
patch_file_replace(
"src/libguac/display-plan.c",
"GUAC_ASSERT(current->pending_frame.buffer_is_external)",
lambda indent: "current = current->pending_frame.next;",
"replace GUAC_ASSERT with pointer advance",
)
# Fix 3: gdi.c - handle begin_paint context still open during desktop_resize.
# With the GFX pipeline on FreeRDP < 3.8.0, begin_paint may be active when
# desktop_resize fires, leaving current_context non-NULL. FreeRDP 3.8.0 fixed
# this by calling EndPaint inside gdi_resize(); backport that behaviour here.
gdi_path = "src/protocols/rdp/gdi.c"
with open(gdi_path) as f:
gdi_content = f.read()
gdi_old = (
" /* For FreeRDP versions prior to 3.8.0, EndPaint will not be called in\n"
" * `gdi_resize()`, so the current context should be NULL. If it is not\n"
" * NULL, it means that the current context is still open, and therefore the\n"
" * GDI buffer has not been flushed yet. */\n"
" GUAC_ASSERT(rdp_client->current_context == NULL);"
)
gdi_new = (
" /* For FreeRDP < 3.8.0, gdi_resize() does not call EndPaint internally.\n"
" * With the GFX pipeline begin_paint may still be active when desktop_resize\n"
" * fires, leaving current_context non-NULL. End the paint now so the context\n"
" * is properly closed before we open a new one for the resize. */\n"
" if (rdp_client->current_context != NULL)\n"
" guac_rdp_gdi_end_paint(context);"
)
if gdi_old not in gdi_content:
print("ERROR: patch target not found in " + gdi_path + " (desktop_resize assert)", file=sys.stderr)
sys.exit(1)
gdi_content = gdi_content.replace(gdi_old, gdi_new, 1)
with open(gdi_path, "w") as f:
f.write(gdi_content)
print("Patched " + gdi_path + " (desktop_resize assert -> end_paint guard)")
# Fix 5: user.c - NULL guard for image_mimetypes in guac_user_supports_webp
patch_file_insert_after(
"src/libguac/user.c",
"const char** mimetype = user->info.image_mimetypes;",
[
"",
"/* Guard: image_mimetypes may be NULL if client sent no image types */",
"if (mimetype == NULL)",
" return 0;",
"",
],
"NULL guard for image_mimetypes in guac_user_supports_webp",
)
# Fix 6: gdi.c - notify_frame on every EndPaint (no rate limit)
#
# Problem: legacy RDP bitmap-cache updates (taskbar icons, static content) carry
# no FrameMarker PDU. The gdi_modified → notify_modified path only fires after
# the RDP event queue drains — never during a burst — so tiles accumulate as
# black squares. Additionally the render thread's lag-compensation can pause up
# to 500 ms between flushes if the browser hasn't ACK'd the previous sync.
#
# Fix: call notify_frame unconditionally in EndPaint. The render thread's own
# MIN_FRAME_DURATION (10 ms) provides natural batching at up to 100 fps. The
# sync-flood / lag-compensation problem is solved at the proxy layer (rdp.ts):
# the Node.js proxy immediately echoes every sync timestamp back to guacd so
# processing_lag stays near 0 ms, and the browser's own ACKs are dropped to
# prevent double-updating the measurement.
gdi_end_paint_old = (
" /* There will be no further drawing operations */\n"
" rdp_client->current_context = NULL;\n"
" guac_display_layer_close_raw(default_layer, current_context);\n"
"\n"
" return TRUE;\n"
)
gdi_end_paint_new = (
" /* There will be no further drawing operations */\n"
" rdp_client->current_context = NULL;\n"
" guac_display_layer_close_raw(default_layer, current_context);\n"
"\n"
" /* Notify the render thread that new tiles are ready to encode.\n"
" * notify_frame rather than notify_modified ensures the frame counter\n"
" * advances and a sync is emitted after each batch. This causes legacy\n"
" * bitmap-cache updates (which carry no FrameMarker PDU) to be flushed\n"
" * immediately on every EndPaint instead of accumulating as black squares\n"
" * until the next FrameMarker. The render thread's MIN_FRAME_DURATION\n"
" * (10 ms) limits output to 100 fps; the Node.js proxy handles sync ACKs\n"
" * so guacd's processing_lag stays near 0 ms. */\n"
" if (rdp_client->render_thread != NULL)\n"
" guac_display_render_thread_notify_frame(rdp_client->render_thread);\n"
"\n"
" return TRUE;\n"
)
gdi_path = "src/protocols/rdp/gdi.c"
with open(gdi_path) as f:
gdi_end_content = f.read()
if gdi_end_paint_old not in gdi_end_content:
print("ERROR: patch target not found in " + gdi_path + " (notify_frame on EndPaint)", file=sys.stderr)
sys.exit(1)
gdi_end_content = gdi_end_content.replace(gdi_end_paint_old, gdi_end_paint_new, 1)
with open(gdi_path, "w") as f:
f.write(gdi_end_content)
print("Patched " + gdi_path + " (notify_frame on every EndPaint)")