From eba699d7bcc9839dfd8151242d5d78ff742d7564 Mon Sep 17 00:00:00 2001 From: felixg Date: Sun, 1 Mar 2026 12:13:12 +0100 Subject: [PATCH] remove all guacd references --- .env | 5 +- .env.example | 5 +- docker/guacd.Dockerfile | 124 --------------- docker/patch-display-flush.py | 240 ------------------------------ frontend/package.json | 3 +- frontend/src/types/guacamole.d.ts | 84 ----------- 6 files changed, 5 insertions(+), 456 deletions(-) delete mode 100644 docker/guacd.Dockerfile delete mode 100644 docker/patch-display-flush.py delete mode 100644 frontend/src/types/guacamole.d.ts diff --git a/.env b/.env index e78832d..3814c6a 100644 --- a/.env +++ b/.env @@ -11,9 +11,8 @@ JWT_SECRET=c4baf6aca61629fcdf2285be2162e662ffec79a5db4e24d8bad2556f6f10c8c5 ADMIN_USER=admin ADMIN_PASSWORD=admin123 -# Apache Guacamole daemon -GUACD_HOST=guacd -GUACD_PORT=4822 +# RDP daemon WebSocket URL +RDPD_URL=ws://rdpd:7777 # Backend port PORT=3000 diff --git a/.env.example b/.env.example index 3f5e304..7433534 100644 --- a/.env.example +++ b/.env.example @@ -11,9 +11,8 @@ JWT_SECRET=change-me-to-a-secure-jwt-secret ADMIN_USER=admin ADMIN_PASSWORD=admin123 -# Apache Guacamole daemon -GUACD_HOST=guacd -GUACD_PORT=4822 +# RDP daemon WebSocket URL +RDPD_URL=ws://rdpd:7777 # Backend port PORT=3000 diff --git a/docker/guacd.Dockerfile b/docker/guacd.Dockerfile deleted file mode 100644 index 5b20b8b..0000000 --- a/docker/guacd.Dockerfile +++ /dev/null @@ -1,124 +0,0 @@ -# syntax=docker/dockerfile:1 -# -# Custom guacd image built against FreeRDP 3.8.0+ on Ubuntu 24.04. -# -# Why FreeRDP from source: Ubuntu 24.04 ships freerdp3-dev 3.5.1. -# guacamole-server git main (1.7-dev) targets FreeRDP 3.8.0+: -# - gdi.c's GUAC_ASSERT(current_context == NULL) is gated #if MINOR < 8 -# so with 3.5.x it fires when GFX calls begin_paint before desktop_resize -# - gdi_resize() in 3.8.0+ calls EndPaint internally, which guacamole relies on -# - GFX pipeline surface synchronisation is fixed in 3.8.0+ -# Building 3.x HEAD from source (always >= 3.8.0 by now) gives us the -# correct FreeRDP that guacamole-server git main was developed against. -# -# Build notes: -# - CPPFLAGS=-DHAVE_FREERDP_VERIFYCERTIFICATEEX=1 fixes a macro name mismatch -# in guacamole-server: configure.ac generates HAVE_STRUCT_FREERDP_VERIFYCERTIFICATEEX -# (AC_CHECK_MEMBERS adds STRUCT_ prefix) but rdp.c checks -# HAVE_FREERDP_VERIFYCERTIFICATEEX (no STRUCT_). Without this the VerifyCertificateEx -# callback is never registered, causing a silent drop ~430ms after keymap load. -# - display-flush.c / display-plan.c patch: loop over layers doesn't advance -# `current` before `continue` when pending_frame.buffer is NULL → infinite loop -# or GUAC_ASSERT abort. Fixed by replacing the GUAC_ASSERT with a pointer advance. -# - gdi.c patch: GUAC_ASSERT(current_context == NULL) in desktop_resize fires on -# FreeRDP 3.5.x because the GFX pipeline calls begin_paint then gdi_resize before -# end_paint. Replaced with a guard that calls end_paint if context is still open. -# With FreeRDP 3.8.0+ this code is inside #if MINOR < 8 so the patch is harmless. -# - user.c patch: guac_user_supports_webp() dereferences image_mimetypes without -# NULL check → SIGSEGV when client didn't send image MIME types. - -FROM ubuntu:24.04 - -COPY docker/patch-display-flush.py /patch-display-flush.py - -ENV DEBIAN_FRONTEND=noninteractive - -# Build dependencies for guacamole-server AND FreeRDP source build -RUN apt-get update && apt-get install -y --no-install-recommends \ - autoconf \ - automake \ - build-essential \ - ca-certificates \ - cmake \ - curl \ - git \ - libcairo2-dev \ - libjpeg-turbo8-dev \ - libossp-uuid-dev \ - libpango1.0-dev \ - libpng-dev \ - libpulse-dev \ - libssl-dev \ - libssh2-1-dev \ - libtelnet-dev \ - libtool \ - libvncserver-dev \ - libwebp-dev \ - libwebsockets-dev \ - libkrb5-dev \ - libavcodec-dev \ - libavutil-dev \ - libswscale-dev \ - libusb-1.0-0-dev \ - pkgconf \ - zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* - -# Build FreeRDP 3.x from source (3.8.0+ required for guacamole-server git main). -# We install to /usr with lib under lib/ (not lib/x86_64-linux-gnu/) so that -# pkg-config --variable=libdir freerdp3 returns /usr/lib cleanly. -# Optional features (audio, printing, X11 display, H264, Kerberos) are disabled -# since guacd runs headless and we have disable-audio: true. -RUN git clone --depth=1 --branch 3.22.0 https://github.com/FreeRDP/FreeRDP.git /tmp/freerdp \ - && cmake -S /tmp/freerdp -B /tmp/freerdp-build \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DCMAKE_INSTALL_LIBDIR=lib \ - -DWITH_X11=OFF \ - -DWITH_WAYLAND=OFF \ - -DWITH_PULSEAUDIO=OFF \ - -DWITH_ALSA=OFF \ - -DWITH_OSS=OFF \ - -DWITH_CUPS=OFF \ - -DWITH_FFMPEG=OFF \ - -DWITH_OPENH264=OFF \ - -DWITH_JPEG=ON \ - -DWITH_CAIRO=ON \ - -DWITH_CHANNELS=ON \ - -DWITH_CLIENT=ON \ - -DWITH_CLIENT_COMMON=ON \ - -DWITH_SERVER=OFF \ - -DWITH_SAMPLE=OFF \ - -DBUILD_TESTING=OFF \ - -DWITH_GSSAPI=OFF \ - -DWITH_FUSE=OFF \ - && cmake --build /tmp/freerdp-build -j"$(nproc)" \ - && cmake --install /tmp/freerdp-build \ - && ldconfig \ - && rm -rf /tmp/freerdp /tmp/freerdp-build - -# Build guacamole-server from git main against our newly installed FreeRDP 3.x. -RUN FREERDP_PLUGIN_DIR=$(pkg-config --variable=libdir freerdp3 2>/dev/null)/freerdp3 \ - && echo "Building guacamole-server (git main) with FreeRDP plugin dir: ${FREERDP_PLUGIN_DIR}" \ - && echo "FreeRDP version: $(pkg-config --modversion freerdp3)" \ - && git clone --depth=1 https://github.com/apache/guacamole-server.git \ - && cd guacamole-server \ - && python3 /patch-display-flush.py \ - && autoreconf -fi \ - && CPPFLAGS="-Wno-error=deprecated-declarations -DHAVE_FREERDP_VERIFYCERTIFICATEEX=1" \ - CFLAGS="-O2 -Wno-error=unused-variable" \ - ./configure \ - --prefix=/usr \ - --sysconfdir=/etc \ - --with-freerdp-plugin-dir="${FREERDP_PLUGIN_DIR}" \ - && make -j"$(nproc)" \ - && make install \ - && ldconfig \ - && cd / && rm -rf guacamole-server - -# guacd log level is passed via -L flag; exposed as env var for docker-compose -ENV GUACD_LOG_LEVEL=info - -EXPOSE 4822 - -CMD sh -c "exec /usr/sbin/guacd -b 0.0.0.0 -f -L \"${GUACD_LOG_LEVEL}\"" diff --git a/docker/patch-display-flush.py b/docker/patch-display-flush.py deleted file mode 100644 index 0ce4fe5..0000000 --- a/docker/patch-display-flush.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/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)") diff --git a/frontend/package.json b/frontend/package.json index 1df97a4..d5804aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,8 +14,7 @@ "@xterm/xterm": "^5.5.0", "antd": "^5.20.5", "axios": "^1.7.7", - "guacamole-common-js": "^1.5.0", - "react": "^18.3.1", +"react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.2", "zustand": "^4.5.5" diff --git a/frontend/src/types/guacamole.d.ts b/frontend/src/types/guacamole.d.ts deleted file mode 100644 index fffe3d0..0000000 --- a/frontend/src/types/guacamole.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -// Custom type declarations for guacamole-common-js. -// We declare this instead of using @types/guacamole-common-js to avoid version mismatches. - -declare namespace Guacamole { - interface Tunnel { - onerror: ((status: Status) => void) | null; - onstatechange: ((state: number) => void) | null; - } - - class WebSocketTunnel implements Tunnel { - constructor( - tunnelURL: string, - crossDomain?: boolean, - extraTunnelHeaders?: Record - ); - onerror: ((status: Status) => void) | null; - onstatechange: ((state: number) => void) | null; - } - - class Client { - constructor(tunnel: Tunnel); - connect(data?: string): void; - disconnect(): void; - getDisplay(): Display; - sendKeyEvent(pressed: number, keysym: number): void; - sendMouseState(mouseState: Mouse.State, applyDisplayScale?: boolean): void; - onerror: ((status: Status) => void) | null; - onstatechange: ((state: number) => void) | null; - } - - class Display { - getElement(): HTMLDivElement; - getWidth(): number; - getHeight(): number; - scale(scale: number): void; - onresize: (() => void) | null; - } - - class Mouse { - constructor(element: Element); - onmousedown: ((mouseState: Mouse.State) => void) | null; - onmouseup: ((mouseState: Mouse.State) => void) | null; - onmousemove: ((mouseState: Mouse.State) => void) | null; - onmouseout: ((mouseState: Mouse.State) => void) | null; - } - - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Mouse { - class State { - x: number; - y: number; - left: boolean; - middle: boolean; - right: boolean; - up: boolean; - down: boolean; - constructor( - x: number, - y: number, - left: boolean, - middle: boolean, - right: boolean, - up: boolean, - down: boolean - ); - } - } - - class Keyboard { - constructor(element: Element | Document); - onkeydown: ((keysym: number) => void) | null; - onkeyup: ((keysym: number) => void) | null; - } - - class Status { - code: number; - message: string; - constructor(code: number, message?: string); - } -} - -declare module 'guacamole-common-js' { - export = Guacamole; -}