Debug adapter protocol client implementation and bridge mode for IDE#78
Debug adapter protocol client implementation and bridge mode for IDE#78
Conversation
…t trigger state changes are run
internal/networking/unix_socket.go
Outdated
|
|
||
| l.closed = true | ||
|
|
||
| closeErr := l.listener.Close() |
There was a problem hiding this comment.
Release lock before calling Close()
There was a problem hiding this comment.
I implemented the retained error, which means we have to hold the lock until we've actually resolved the error value.
karolz-ms
left a comment
There was a problem hiding this comment.
Initial PR review part 2
| t.Parallel() | ||
| rootDir := shortTempDir(t) | ||
|
|
||
| listener, createErr := NewSecureSocketListener(rootDir, "afc-") |
There was a problem hiding this comment.
The test that we should really have is the following:
- Create a new socket listener and do Accept() (blocks).
- Launch 10 different goroutines and make them race to call Close() on the listener.
- Verify that Accept() returns with errClosed (a distinct error that allows the caller to differentiate between "somebody closed be because it is time to shut down" vs another, unexpected error).
- Verify that all calls to Close() returned with no error.
karolz-ms
left a comment
There was a problem hiding this comment.
Comments on the DAP implmentation, part 1
internal/dap/bridge.go
Outdated
| // If the adapter did not send a TerminatedEvent, synthesize one for the IDE. | ||
| // Also send an error OutputEvent if we exited due to a transport error. | ||
| terminated := b.terminatedEventSeen.Load() | ||
|
|
||
| if !terminated { | ||
| if loopErr != nil && !errors.Is(loopErr, io.EOF) && !errors.Is(loopErr, context.Canceled) { | ||
| b.sendErrorToIDE(fmt.Sprintf("Debug session ended unexpectedly: %v", loopErr)) | ||
| } else { | ||
| b.sendTerminatedToIDE() | ||
| } | ||
| } |
There was a problem hiding this comment.
I would prefer if the forwardAdapterToIDE goroutine was "told" to do this (e.g. via a separate channel). This eliminates the race between the two goroutines with regards to reading/writing terminatedEventSeen and also ensures that a single goroutine is writing to the IDE socket.
To be clear, having multiple goroutines writing to the same socket is not strictly forbidden, but adds another degree of asynchrony.
| func (b *DapBridge) handleOutputEvent(event *dap.OutputEvent) { | ||
| // Only capture output if runInTerminal wasn't used | ||
| // (if runInTerminal was used, we capture directly from the process) | ||
| if !b.runInTerminalUsed.Load() && b.config.OutputHandler != nil { |
There was a problem hiding this comment.
Seems like runInTerminalUsed is only used by the adapter --> IDE goroutine, so it can probably be just a regular bool flag.
karolz-ms
left a comment
There was a problem hiding this comment.
DAP bridge review, part 2
| type HandshakeReader struct { | ||
| conn net.Conn | ||
| } |
There was a problem hiding this comment.
Is there a particular reason to define HandshakeReader this way as oppose do
| type HandshakeReader struct { | |
| conn net.Conn | |
| } | |
| type HandshakeReader net.Conn |
| type HandshakeWriter struct { | ||
| conn net.Conn | ||
| } |
There was a problem hiding this comment.
Same question as for HandshakeReader
| type HandshakeWriter struct { | |
| conn net.Conn | |
| } | |
| type HandshakeWriter net.Conn |
| // Start begins listening on the Unix socket and accepting connections. | ||
| // This method blocks until the context is cancelled. | ||
| // Connections are handled in separate goroutines. | ||
| func (m *BridgeManager) Start(ctx context.Context) error { |
There was a problem hiding this comment.
Maybe "Run()" (suggesting a blocking nature of the method)
| // Close the listener when the context is cancelled so that Accept() unblocks. | ||
| // PrivateUnixSocketListener.Close() is idempotent, so the deferred Close above | ||
| // is still safe. | ||
| go func() { |
| conn, acceptErr := m.listener.Accept() | ||
| if acceptErr != nil { | ||
| // Check if context was cancelled (listener was closed by the goroutine above) | ||
| select { |
There was a problem hiding this comment.
You can just check ctx.Err() != ni, no need to do a select
| // handleConnection processes a single incoming connection. | ||
| func (m *BridgeManager) handleConnection(ctx context.Context, conn net.Conn) { | ||
| defer func() { | ||
| if r := recover(); r != nil { |
There was a problem hiding this comment.
Use resiliency.MakePanicError()
karolz-ms
left a comment
There was a problem hiding this comment.
Finished initial review. This is a fantastic and impressive change!
| } | ||
|
|
||
| func (t *connTransport) WriteMessage(msg dap.Message) error { | ||
| t.writeMu.Lock() |
There was a problem hiding this comment.
One more thing: if you want to make it highly probably that, upon contention, messages are written in the order in which WriteMessage() was called (as opposed to random), our concurrency.Semaphore type will ensure just that.
Implements a debug adapter protocol (DAP) client and support for a new debug adapter protocol bridge based IDE debugging mode.
Opening this as a draft because it's both very large and I want to spend more time on the Aspire side of the bridge to make sure things are fully stable before I mark it ready for review.