Refactor DAP attach: transport + connector strategies (replaces #10)#11
Merged
Refactor DAP attach: transport + connector strategies (replaces #10)#11
Conversation
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 #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>
Addresses review feedback on the initial refactor: - DapClient JSDoc: explain wire format, responsibilities, and link to the canonical DAP spec at microsoft.github.io/debug-adapter-protocol/overview. JSDoc on `pid` explaining why it's undefined for TCP transports. - Zod-validated message envelopes: `handleMessage` now parses via schemas in `src/dap/protocol.ts` (ProtocolMessage / Response / Event) instead of casting. JSON parse and schema errors are logged via the existing dap logger (warn level) instead of silently swallowed. - `handleIncomingData` extracted from the inline transport callback. - Strongly-typed `DapClient.send`: new `DapCommandMap` / `DapEventMap` in `src/dap/protocol.ts` give typed args and return the response body directly (drops `response.body as X` at ~13 call sites in session.ts). Optional-args commands (`configurationDone`, `disconnect`) get a rest-tuple conditional type so they remain callable without `undefined`. Vendor extensions like Java's `redefineClasses` fall through to the untyped string overload. - `DapClient.waitForEvent(name, timeoutMs)` helper replaces the hand-rolled `armInitializedWaiter` — listener + timer cleanup live in one place. - Session features moved to a strategy pattern: `SessionCapabilities` → `SessionFeatures` (rename avoids collision with DAP's `DebugProtocol.Capabilities` on the adapter side). `DapRuntimeConfig.features` is now `Partial<SessionFeatures>` — runtime configs only list overrides on top of `DEFAULT_DAP_FEATURES`. `DapSession` no longer has the `isJava` conditional. - Transport description in `wsUrl`: `dap://python/tcp(127.0.0.1:5678)` vs `dap://lldb/spawn(lldb-dap)` makes `dbg status` clear about whether the adapter is spawned or attached. - `_runtime` narrowed via zod: new `CanonicalRuntimeSchema` derives a `CanonicalRuntime` union from a single const tuple. `RUNTIME_CONFIGS` is typed `Record<CanonicalRuntime, DapRuntimeConfig>` so adding a new runtime without a config is a compile error. `resolveRuntime` now narrows `string` → `CanonicalRuntime`. - `getDap()` → `client` getter on `DapSession`. All ~20 call sites read `this.client.send(...)`. Throw semantics preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
useTcpAttachboolean and the dual-modeDapClient.DapSession.launchandDapSession.attachinto a singlerunHandshake()template; the debugpy-fires-initialized-during-initialize race is now fixed structurally (listener armed before theinitializerequest is sent) instead of via a separate_initializedPromisefield.python -m debugpy --listen <port> --wait-for-client, attaches, and verifies a breakpoint hits inside the running loop.Why reject #10 as-is
#10 is right about the bug but the fix bolted a second mode onto
DapClient:proc: Subprocess | null+tcpSocket: net.Socket | nullwithif (this.tcpSocket) ... else if (this.proc)branches in the constructor,writeMessage, anddisconnect.get pid()returns0for TCP (a lie — should be "no pid").DapSession.attach()forks into two near-duplicated attach sequences switched onconfig.useTcpAttach.useTcpAttachboolean collapses two orthogonal concerns — transport vs provisioning — onto one axis.Design
Two small interfaces, each with two implementations today:
debugpyConfig.attach()returnsTcpAttachConnector(host, port)— exactly PR #10's behaviour, but declarative.javaConfig.attach()keepsSpawnAdapterConnector(the java adapter bridges DAP→JDWP). Adding Go (delve, TCP) or Ruby (rdbg, both) is now a config-file change.RED → GREEN
New test:
tests/integration/python/python-attach.test.tsmain): times out with `this test timed out after 15000ms`. Pre-refactor `DapSession.attach("64596", "debugpy")` spawned `debugpy.adapter` and sent `{ pid: 64596 }` — there's no such pid, so the adapter hangs waiting.Two subtleties worth calling out
Test plan
🤖 Generated with Claude Code