Skip to content

feat: WebSocket proof-of-concept prototype (GSoC 2026 proposal)#176

Open
xu826Jamin wants to merge 2 commits intoc2siorg:mainfrom
xu826Jamin:prototype/websocket-poc
Open

feat: WebSocket proof-of-concept prototype (GSoC 2026 proposal)#176
xu826Jamin wants to merge 2 commits intoc2siorg:mainfrom
xu826Jamin:prototype/websocket-poc

Conversation

@xu826Jamin
Copy link
Copy Markdown

@xu826Jamin xu826Jamin commented Mar 23, 2026

This prototype demonstrates the background reader thread architecture proposed in my GSoC 2026 application for Objective 4 (real-time GDB output via WebSockets).

Key claim proven: GDB *stopped records arrive asynchronously — without any client command triggering them. A naive SocketIO implementation that only emits responses inside the command handler silently drops breakpoint hits entirely. This prototype runs a dedicated reader thread per session that continuously polls pygdbmi and emits all record types — including async notify records — to the correct SocketIO room.

What to run:
cd prototype
pip install -r requirements.txt
gcc -g -O0 -o target target.exe target.c
python server.py
open http://localhost:5000

What to observe: click Start debug session. The BREAKPOINT HIT event arrives in the browser without any command being sent — the *stopped async record caught by the reader thread, not the command handler.

Also demonstrates:

Screenshot showing the BREAKPOINT HIT event arriving unprompted, local variables panel populated with real GDB state (x=10), and line 9 highlighted in the source panel.
Screenshot 2026-03-23 020839

Copilot AI review requested due to automatic review settings March 23, 2026 06:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a self-contained WebSocket-based proof-of-concept under prototype/ to demonstrate why GDB MI async *stopped records require a dedicated background reader thread (per session) rather than emitting only from command handlers.

Changes:

  • Introduces a Flask-SocketIO server that spins up a per-session GdbController plus a background reader thread to emit async notify records to the right room.
  • Adds a small C debug target and README/instructions for reproducing an “unprompted breakpoint hit” event in the browser UI.
  • Adds prototype-specific dependency list and a small diagnostic script.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
.gitignore Attempts to ignore prototype build output (currently incorrect pattern).
.dockerignore Adds standard Docker ignore rules for repo builds.
prototype/server.py Main prototype server + embedded frontend demonstrating background reader thread + room routing.
prototype/target.c Minimal C program with intended breakpoint location.
prototype/target.exe Prebuilt Windows binary added to repo (build artifact).
prototype/requirements.txt Prototype Python dependencies.
prototype/diag.py Quick local diagnostic for launching GDB and setting a breakpoint.
prototype/README.md Prototype explanation, architecture diagram, and run steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +34
# eventlet async mode required for background threads with SocketIO
socketio = SocketIO(app, async_mode="threading", cors_allowed_origins="*")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says “eventlet async mode required”, but SocketIO is configured with async_mode="threading" and the frontend forces transports: ['websocket']. In threading mode, Flask-SocketIO typically needs simple-websocket (or a different async_mode like eventlet/gevent) to support WebSocket transport; otherwise the connection can fail or fall back to polling (which you’ve disabled). Align the async_mode, dependencies, and client transport settings so the prototype runs out-of-the-box.

Suggested change
# eventlet async mode required for background threads with SocketIO
socketio = SocketIO(app, async_mode="threading", cors_allowed_origins="*")
# Use eventlet async mode so WebSocket transport works out-of-the-box
socketio = SocketIO(app, async_mode="eventlet", cors_allowed_origins="*")

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +190
sid = session.get("sid", "anon")
sess = get_or_create_session(sid)

# Tear down any existing GDB session for this user
if sess.get("controller"):
try:
sess["controller"].exit()
except Exception:
pass
sess["active"] = False

# Create a fresh isolated GdbController for this session
controller = GdbController()
sess["controller"] = controller
sess["active"] = True

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session state is mutated outside the _sessions_lock (e.g., setting sess["controller"], sess["active"], and tearing down an existing controller). The reader thread reads these fields under the lock, so this can race and observe partial updates. Wrap session lifecycle mutations in the same lock (or use per-session locks) to make the concurrency story match the intent described in comments.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +165
sid = session.get("sid") or "anon"
join_room(sid)
print(f"[connect] client joined room {sid[:8]}")
emit("connected", {"session_id": sid[:8]})
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If session['sid'] is missing, this falls back to the hard-coded room name "anon" and all such clients will join the same room (cross-user event leakage). Consider always generating a unique sid (e.g., create one here when absent) rather than using a shared default.

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +201
bp_resp = controller.write(f"-break-insert {BREAKPOINT_LINE}", timeout_sec=3)
bp_ok = any(r.get("message") == "done" for r in bp_resp)

emit("session_ready", {
"binary": os.path.basename(TARGET_BINARY),
"breakpoint": BREAKPOINT_LINE,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says “set breakpoint by function name”, but the code uses a bare line number (-break-insert {BREAKPOINT_LINE}). Also, a bare line number can be ambiguous in GDB MI; using target.c:{line} (or a function like main) is more reliable across platforms and matches the intent of the comment.

Suggested change
bp_resp = controller.write(f"-break-insert {BREAKPOINT_LINE}", timeout_sec=3)
bp_ok = any(r.get("message") == "done" for r in bp_resp)
emit("session_ready", {
"binary": os.path.basename(TARGET_BINARY),
"breakpoint": BREAKPOINT_LINE,
breakpoint_spec = "main"
bp_resp = controller.write(f"-break-insert {breakpoint_spec}", timeout_sec=3)
bp_ok = any(r.get("message") == "done" for r in bp_resp)
emit("session_ready", {
"binary": os.path.basename(TARGET_BINARY),
"breakpoint": breakpoint_spec,

Copilot uses AI. Check for mistakes.
Comment on lines +561 to +563
if __name__ == "__main__":
binary = TARGET_BINARY
if not os.path.exists(binary):
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entry-point block has a redundant nested if __name__ == "__main__": and inconsistent indentation inside it. This makes the script harder to read and is easy to break with future edits; simplify to a single guard and use consistent 4-space indentation.

Suggested change
if __name__ == "__main__":
binary = TARGET_BINARY
if not os.path.exists(binary):
binary = TARGET_BINARY
if not os.path.exists(binary):

Copilot uses AI. Check for mistakes.
_binary_name = "target.exe" if os.name == "nt" else "target"
TARGET_BINARY = os.path.join(os.path.dirname(os.path.abspath(__file__)), _binary_name)
TARGET_BINARY_GDB = TARGET_BINARY.replace("\\", "/")
BREAKPOINT_LINE = 9
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BREAKPOINT_LINE is set to 9, but in target.c the intended breakpoint location (the add() call) is on line 10. As-is, the breakpoint will land on the int y = 20; line, and the UI/readme will be inconsistent with what GDB reports.

Suggested change
BREAKPOINT_LINE = 9
BREAKPOINT_LINE = 10

Copilot uses AI. Check for mistakes.
flask>=3.0.0
flask-socketio>=5.3.6
pygdbmi>=0.10.0.0
eventlet>=0.35.0
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requirements.txt uses loose >= pins, which makes the prototype non-reproducible and can break unexpectedly with new dependency releases. The main server’s requirements file in this repo is pinned to exact versions; consider pinning here too (and ensure the selected versions match the chosen SocketIO async mode).

Suggested change
eventlet>=0.35.0
eventlet==0.35.0

Copilot uses AI. Check for mistakes.
|-- start_debug -----------> | |
| |-- GdbController() ---------->|
| |-- -file-exec-and-symbols --->|
| |-- -break-insert 9 ---------->|
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture diagram hard-codes -break-insert 9, but target.c’s annotated breakpoint location is on line 10 (and server.py defines BREAKPOINT_LINE). Update this line number (or reference the constant) so the README matches the actual demo behavior.

Suggested change
| |-- -break-insert 9 ---------->|
| |-- -break-insert 10 --------->|

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +55
```
gdb-websocket-prototype/
├── server.py # Flask-SocketIO backend with background reader thread
├── target.c # C program with a deliberate breakpoint location
├── target # compiled binary (build with command below)
└── README.md
```
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README “Files” section suggests a compiled binary is part of the prototype directory, but the setup section instructs building it locally. To keep the repo clean (and avoid platform-specific binaries), prefer not committing compiled outputs; document the build step and ignore the generated target/target.exe files.

Copilot uses AI. Check for mistakes.
from pygdbmi.gdbcontroller import GdbController
import os, time

TARGET = os.path.join(os.path.dirname(os.path.abspath(__file__)), "target.exe")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diag.py hard-codes target.exe, so it won’t work on non-Windows platforms (and server.py already handles target vs target.exe). Consider using the same OS-dependent binary name logic here to keep the prototype runnable cross-platform.

Suggested change
TARGET = os.path.join(os.path.dirname(os.path.abspath(__file__)), "target.exe")
_target_name = "target.exe" if os.name == "nt" else "target"
TARGET = os.path.join(os.path.dirname(os.path.abspath(__file__)), _target_name)

Copilot uses AI. Check for mistakes.
@xu826Jamin
Copy link
Copy Markdown
Author

Hi @Shubh942, @Niweera ! I'm Jamin, a 2nd year Computer Engineering student interested in GDB-UI for GSoC 2026. I just submitted PR #176. I'd love to join the C2SI Slack to connect with the community — could you share a fresh invite link? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants