Skip to content

feat(python): support attaching to a running debugpy server over TCP - #10

Closed
pldesch-chift wants to merge 1 commit into
theodo-group:mainfrom
pldesch-chift:feat/python-tcp-attach
Closed

feat(python): support attaching to a running debugpy server over TCP#10
pldesch-chift wants to merge 1 commit into
theodo-group:mainfrom
pldesch-chift:feat/python-tcp-attach

Conversation

@pldesch-chift

Copy link
Copy Markdown
Contributor

Problem

dbg attach <port> --runtime python was broken. When called, it would:

  1. Spawn a new debugpy.adapter process
  2. Send it an attach request to connect to the running debugpy server

This is adapter-on-top-of-adapter — it doesn't work. The new adapter process exited immediately, causing DAP adapter process terminated.

There was also a separate race condition: debugpy fires the initialized DAP event during the initialize round-trip, before waitForInitialized() had registered its listener — causing a guaranteed timeout.

Solution

1. TCP attach mode (useTcpAttach flag)

Added DapClient.connectTcp(host, port) — a new static constructor that opens a direct TCP connection to a running DAP server instead of spawning a subprocess. When useTcpAttach: true is set on a runtime config, attach() uses this path and speaks DAP directly over the socket (the same way VS Code does).

debugpyConfig now sets useTcpAttach: true.

Usage:

# Start API with debugpy listening
python -m debugpy --listen 5678 -m uvicorn myapp:app

# Attach from another terminal
dbg attach 5678 --runtime python

2. Race condition fix in initializeAdapter()

Moved the initialized event listener registration to before sending the initialize request, storing it as _initializedPromise. waitForInitialized() now awaits this pre-registered promise instead of creating a new listener after the fact.

Files changed

  • src/dap/client.ts — TCP transport support (connectTcp, readLoopTcp, updated disconnect/writeMessage)
  • src/dap/runtimes/python.ts — add useTcpAttach: true
  • src/dap/runtimes/types.ts — add useTcpAttach?: boolean to DapRuntimeConfig
  • src/dap/session.ts — TCP attach branch + _initializedPromise race condition fix

Testing

Tested end-to-end against a FastAPI/uvicorn app running under debugpy. Breakpoints hit, variables inspectable, step/continue working.

Note: the existing Python integration tests were already failing on main before these changes (pre-existing issue unrelated to this PR).

When a Python process is started with `python -m debugpy --listen <port>`,
the process itself is the DAP server. The previous attach flow would spawn
a second debugpy.adapter process and try to proxy through it, which failed.

This PR adds two fixes:

1. TCP attach mode (useTcpAttach flag)
   - New `DapClient.connectTcp(host, port)` static method that opens a
     direct TCP connection to a running DAP server instead of spawning
     a subprocess.
   - New `useTcpAttach` flag on `DapRuntimeConfig`. When set, `attach()`
     connects directly over TCP and sends the standard DAP sequence
     (initialize → attach → configurationDone) without spawning an adapter.
   - `debugpyConfig` sets `useTcpAttach: true`.

2. Race condition fix in initializeAdapter()
   - Debugpy fires the DAP `initialized` event during the `initialize`
     round-trip, before `waitForInitialized()` had a chance to register
     its listener — causing a guaranteed timeout.
   - Fixed by registering the `initialized` listener inside
     `initializeAdapter()` before sending the `initialize` request, and
     storing it as `_initializedPromise` for `waitForInitialized()` to await.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pldesch-chift
pldesch-chift marked this pull request as ready for review April 17, 2026 15:19
@waltoss

waltoss commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Thank you for you submission !
Agree that dbg should support both launching the DAP server or attaching to an exciting one.

But I prefer seperating the concerns between the client / the transporter / and the planner that decides the strategy to either attach or launch. See #11

I close this one

@waltoss waltoss closed this Apr 24, 2026
waltoss added a commit that referenced this pull request Apr 24, 2026
…ctor

Refactor DAP attach: transport + connector strategies (replaces #10)
pldesch-chift pushed a commit to pldesch-chift/debug-that that referenced this pull request Jun 1, 2026
Replaces the `useTcpAttach` boolean and dual-mode DapClient with two
orthogonal abstractions:

  DapTransport  — byte-level I/O (Stdio/Tcp)
  DapConnector  — transport provisioning (SpawnAdapter/TcpAttach)

Each DapRuntimeConfig now returns a DapConnectPlan (connector + request
args) from launch() and optional attach(). DapSession.launch/attach
collapse into a single runHandshake() template, and the
debugpy-fires-`initialized`-during-initialize race is now fixed
structurally (listener armed before the initialize request is sent) instead
of via a separate one-shot promise field.

Fixes PR theodo-group#10's issue (adapter-on-adapter when attaching to debugpy):
debugpyConfig.attach() returns a TcpAttachConnector, so dbg speaks DAP
directly over the socket to the running debugpy — no subprocess in between.

Adds an integration test (tests/integration/python/python-attach.test.ts)
that spawns `python -m debugpy --listen <port> --wait-for-client`, attaches,
and verifies a breakpoint hits. RED on pre-refactor main (times out with
the adapter-on-adapter bug), GREEN here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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