Skip to content

Commit 7a1049f

Browse files
committed
Redirect output
1 parent daaa0a8 commit 7a1049f

File tree

4 files changed

+163
-9
lines changed

4 files changed

+163
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
bin/
22
*.log
33
testdata/sample/sample
4+
.idea

pkg/debugger/client.go

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package debugger
22

33
import (
4+
"bufio"
45
"context"
56
"fmt"
7+
"io"
68
"net"
79
"os"
810
"os/exec"
911
"path/filepath"
1012
"time"
1113

1214
"github.com/go-delve/delve/pkg/logflags"
15+
"github.com/go-delve/delve/pkg/proc"
1316
"github.com/go-delve/delve/service"
1417
"github.com/go-delve/delve/service/api"
1518
"github.com/go-delve/delve/service/debugger"
@@ -20,16 +23,28 @@ import (
2023

2124
// Client encapsulates the Delve debug client functionality
2225
type Client struct {
23-
client *rpc2.RPCClient
24-
target string
25-
pid int
26-
server *rpccommon.ServerImpl
27-
tempDir string
26+
client *rpc2.RPCClient
27+
target string
28+
pid int
29+
server *rpccommon.ServerImpl
30+
tempDir string
31+
outputChan chan OutputMessage // Channel for captured output
32+
stopOutput chan struct{} // Channel to signal stopping output capture
33+
}
34+
35+
// OutputMessage represents a captured output message
36+
type OutputMessage struct {
37+
Source string `json:"source"` // "stdout" or "stderr"
38+
Content string `json:"content"`
39+
Timestamp time.Time `json:"timestamp"`
2840
}
2941

3042
// NewClient creates a new Delve client wrapper
3143
func NewClient() *Client {
32-
return &Client{}
44+
return &Client{
45+
outputChan: make(chan OutputMessage, 100), // Buffer for output messages
46+
stopOutput: make(chan struct{}),
47+
}
3348
}
3449

3550
// LaunchProgram starts a new program with debugging enabled
@@ -68,6 +83,18 @@ func (c *Client) LaunchProgram(program string, args []string) error {
6883
return fmt.Errorf("couldn't start listener: %s", err)
6984
}
7085

86+
// Create pipes for stdout and stderr using the proc.Redirector function
87+
stdoutReader, stdoutRedirect, err := proc.Redirector()
88+
if err != nil {
89+
return fmt.Errorf("failed to create stdout redirector: %v", err)
90+
}
91+
92+
stderrReader, stderrRedirect, err := proc.Redirector()
93+
if err != nil {
94+
stdoutRedirect.File.Close()
95+
return fmt.Errorf("failed to create stderr redirector: %v", err)
96+
}
97+
7198
logger.Printf("DEBUG: Creating Delve config")
7299
// Create Delve config
73100
config := &service.Config{
@@ -80,9 +107,15 @@ func (c *Client) LaunchProgram(program string, args []string) error {
80107
Backend: "default",
81108
CheckGoVersion: true,
82109
DisableASLR: true,
110+
Stdout: stdoutRedirect,
111+
Stderr: stderrRedirect,
83112
},
84113
}
85114

115+
// Start goroutines to capture output
116+
go c.captureOutput(stdoutReader, "stdout")
117+
go c.captureOutput(stderrReader, "stderr")
118+
86119
logger.Printf("DEBUG: Creating debug server")
87120
// Create and start the debugging server
88121
server := rpccommon.NewServer(config)
@@ -172,6 +205,10 @@ func (c *Client) AttachToProcess(pid int) error {
172205
return fmt.Errorf("couldn't start listener: %s", err)
173206
}
174207

208+
// Note: When attaching to an existing process, we can't easily redirect its stdout/stderr
209+
// as those file descriptors are already connected. Output capture is limited for attach mode.
210+
logger.Printf("DEBUG: Note: Output redirection is limited when attaching to an existing process")
211+
175212
logger.Printf("DEBUG: Creating Delve config for attach")
176213
// Create Delve config for attaching to process
177214
config := &service.Config{
@@ -310,6 +347,9 @@ func (c *Client) Close() error {
310347
return nil
311348
}
312349

350+
// Signal to stop output capturing goroutines
351+
close(c.stopOutput)
352+
313353
// Create a context with timeout to prevent indefinite hanging
314354
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
315355
defer cancel()
@@ -410,12 +450,38 @@ func (c *Client) DebugSourceFile(sourceFile string, args []string) error {
410450

411451
logger.Printf("DEBUG: Compiling source file %s to %s", absPath, outputBinary)
412452

453+
// Create pipes for build output
454+
buildStdoutReader, buildStdoutWriter, err := os.Pipe()
455+
if err != nil {
456+
os.RemoveAll(tempDir)
457+
return fmt.Errorf("failed to create stdout pipe: %v", err)
458+
}
459+
defer buildStdoutReader.Close()
460+
defer buildStdoutWriter.Close()
461+
413462
// Run go build with optimizations disabled
414463
buildCmd := exec.Command("go", "build", "-gcflags", "all=-N -l", "-o", outputBinary, absPath)
415-
buildOutput, err := buildCmd.CombinedOutput()
464+
buildCmd.Stdout = buildStdoutWriter
465+
buildCmd.Stderr = buildStdoutWriter
466+
467+
err = buildCmd.Start()
416468
if err != nil {
417-
os.RemoveAll(tempDir) // Clean up temp directory on error
418-
return fmt.Errorf("failed to compile source file: %v\nOutput: %s", err, buildOutput)
469+
os.RemoveAll(tempDir)
470+
return fmt.Errorf("failed to start compilation: %v", err)
471+
}
472+
473+
// Capture build output
474+
go func() {
475+
scanner := bufio.NewScanner(buildStdoutReader)
476+
for scanner.Scan() {
477+
logger.Printf("Build output: %s", scanner.Text())
478+
}
479+
}()
480+
481+
err = buildCmd.Wait()
482+
if err != nil {
483+
os.RemoveAll(tempDir)
484+
return fmt.Errorf("failed to compile source file: %v", err)
419485
}
420486

421487
// Launch the compiled binary with the debugger
@@ -975,3 +1041,47 @@ func (c *Client) GetExecutionPosition() (*ExecutionPosition, error) {
9751041

9761042
return result, nil
9771043
}
1044+
1045+
// captureOutput reads from a reader and sends the output to the output channel
1046+
func (c *Client) captureOutput(reader io.ReadCloser, source string) {
1047+
defer reader.Close()
1048+
1049+
scanner := bufio.NewScanner(reader)
1050+
for scanner.Scan() {
1051+
select {
1052+
case <-c.stopOutput:
1053+
return
1054+
case c.outputChan <- OutputMessage{
1055+
Source: source,
1056+
Content: scanner.Text(),
1057+
Timestamp: time.Now(),
1058+
}:
1059+
}
1060+
}
1061+
}
1062+
1063+
// GetCapturedOutput returns the next captured output message
1064+
// Returns nil when there are no more messages
1065+
func (c *Client) GetCapturedOutput() *OutputMessage {
1066+
select {
1067+
case msg := <-c.outputChan:
1068+
return &msg
1069+
default:
1070+
return nil
1071+
}
1072+
}
1073+
1074+
// GetAllCapturedOutput returns all currently available captured output messages
1075+
func (c *Client) GetAllCapturedOutput() []OutputMessage {
1076+
var messages []OutputMessage
1077+
1078+
// Collect all available messages without blocking
1079+
for {
1080+
select {
1081+
case msg := <-c.outputChan:
1082+
messages = append(messages, msg)
1083+
default:
1084+
return messages
1085+
}
1086+
}
1087+
}

pkg/mcp/server.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func (s *MCPDebugServer) registerTools() {
108108
s.addExamineVariableTool()
109109
s.addListScopeVariablesTool()
110110
s.addGetExecutionPositionTool()
111+
s.addGetDebuggerOutputTool()
111112
}
112113

113114
// addPingTool adds a simple ping tool for health checks
@@ -294,6 +295,15 @@ func (s *MCPDebugServer) addGetExecutionPositionTool() {
294295
s.server.AddTool(positionTool, s.GetExecutionPosition)
295296
}
296297

298+
// addGetDebuggerOutputTool registers the get_debugger_output tool
299+
func (s *MCPDebugServer) addGetDebuggerOutputTool() {
300+
outputTool := mcp.NewTool("get_debugger_output",
301+
mcp.WithDescription("Get captured stdout and stderr from the debugged program"),
302+
)
303+
304+
s.server.AddTool(outputTool, s.GetDebuggerOutput)
305+
}
306+
297307
// newErrorResult creates a tool result that represents an error
298308
func newErrorResult(format string, args ...interface{}) *mcp.CallToolResult {
299309
result := mcp.NewToolResultText(fmt.Sprintf("Error: "+format, args...))
@@ -705,4 +715,29 @@ func (s *MCPDebugServer) GetExecutionPosition(ctx context.Context, request mcp.C
705715
}
706716

707717
return mcp.NewToolResultText(string(jsonBytes)), nil
718+
}
719+
720+
// GetDebuggerOutput retrieves any captured stdout/stderr from the debugged program
721+
func (s *MCPDebugServer) GetDebuggerOutput(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
722+
if s.debugClient == nil || !s.debugClient.IsConnected() {
723+
return newErrorResult("No active debug session"), nil
724+
}
725+
726+
// Get all captured output
727+
messages := s.debugClient.GetAllCapturedOutput()
728+
729+
// Format the messages into a response
730+
var response struct {
731+
Messages []debugger.OutputMessage `json:"messages"`
732+
}
733+
response.Messages = messages
734+
735+
// Convert to JSON
736+
outputJSON, err := json.Marshal(response)
737+
if err != nil {
738+
return newErrorResult("Failed to marshal output messages: %v", err), nil
739+
}
740+
741+
// Return the output
742+
return mcp.NewToolResultText(string(outputJSON)), nil
708743
}

pkg/mcp/server_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,14 @@ func TestDebugWorkflow(t *testing.T) {
615615
// Allow time for program to complete
616616
time.Sleep(300 * time.Millisecond)
617617

618+
// Check for captured output
619+
outputRequest := mcp.CallToolRequest{}
620+
outputResult, err := server.GetDebuggerOutput(ctx, outputRequest)
621+
if err == nil && outputResult != nil {
622+
outputText := getTextContent(outputResult)
623+
t.Logf("Captured program output: %s", outputText)
624+
}
625+
618626
// Clean up by closing the debug session
619627
closeRequest := mcp.CallToolRequest{}
620628
closeResult, err := server.Close(ctx, closeRequest)

0 commit comments

Comments
 (0)