Skip to content

Commit 823e567

Browse files
committed
fix(local): replace bufio.Scanner with bufio.Reader and implement Execute
- Replace bufio.Scanner with bufio.Reader in streamStdout to handle long lines - Implement Local.Execute using exec.CommandContext with /bin/sh -c - Capture stdout/stderr separately, return stderr in error on non-zero exit - Add unit tests for Execute method
1 parent 014a293 commit 823e567

File tree

3 files changed

+125
-15
lines changed

3 files changed

+125
-15
lines changed

adk/backend/agentkit/code_template.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,11 @@ try:
246246
247247
# Check for stderr
248248
if result.stderr:
249-
print(f"Error executing command: {{result.stderr}}", file=sys.stderr)
249+
output_parts = []
250+
if result.stdout:
251+
output_parts.append(f"[stdout]:\n{{result.stdout.rstrip()}}")
252+
output_parts.append(f"[stderr]:\n{{result.stderr.rstrip()}}")
253+
print('\n'.join(output_parts), end='')
250254
sys.exit(result.returncode if result.returncode != 0 else 1)
251255
252256
# Print stdout

adk/backend/local/local.go

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,46 @@ func (s *Local) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteR
396396
return sr, nil
397397
}
398398

399+
func (s *Local) Execute(ctx context.Context, input *filesystem.ExecuteRequest) (result *filesystem.ExecuteResponse, err error) {
400+
if input.Command == "" {
401+
return nil, fmt.Errorf("command is required")
402+
}
403+
404+
if err := s.validateCommand(input.Command); err != nil {
405+
return nil, err
406+
}
407+
408+
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", input.Command)
409+
410+
var stdoutBuf, stderrBuf strings.Builder
411+
cmd.Stdout = &stdoutBuf
412+
cmd.Stderr = &stderrBuf
413+
414+
exitCode := 0
415+
if err := cmd.Run(); err != nil {
416+
var exitError *exec.ExitError
417+
if errors.As(err, &exitError) {
418+
exitCode = exitError.ExitCode()
419+
stdoutStr := stdoutBuf.String()
420+
stderrStr := stderrBuf.String()
421+
parts := []string{fmt.Sprintf("command exited with non-zero code %d", exitCode)}
422+
if stdoutStr != "" {
423+
parts = append(parts, "stdout: "+stdoutStr)
424+
}
425+
if stderrStr != "" {
426+
parts = append(parts, "stderr: "+stderrStr)
427+
}
428+
return nil, errors.New(strings.Join(parts, "\n"))
429+
}
430+
return nil, fmt.Errorf("failed to execute command: %w", err)
431+
}
432+
433+
return &filesystem.ExecuteResponse{
434+
Output: stdoutBuf.String(),
435+
ExitCode: &exitCode,
436+
}, nil
437+
}
438+
399439
// initStreamingCmd creates command with stdout and stderr pipes.
400440
func (s *Local) initStreamingCmd(ctx context.Context, command string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) {
401441
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
@@ -512,23 +552,27 @@ func (s *Local) readStderrAsync(stderr io.Reader) (*[]byte, <-chan error) {
512552

513553
// streamStdout streams stdout line by line to the writer.
514554
func (s *Local) streamStdout(ctx context.Context, cmd *exec.Cmd, stdout io.Reader, w *schema.StreamWriter[*filesystem.ExecuteResponse]) (bool, error) {
515-
scanner := bufio.NewScanner(stdout)
555+
reader := bufio.NewReader(stdout)
516556
hasOutput := false
517557

518-
for scanner.Scan() {
519-
hasOutput = true
520-
line := scanner.Text() + "\n"
521-
select {
522-
case <-ctx.Done():
523-
_ = cmd.Process.Kill()
524-
return hasOutput, ctx.Err()
525-
default:
526-
w.Send(&filesystem.ExecuteResponse{Output: line}, nil)
558+
for {
559+
line, err := reader.ReadString('\n')
560+
if line != "" {
561+
hasOutput = true
562+
select {
563+
case <-ctx.Done():
564+
_ = cmd.Process.Kill()
565+
return hasOutput, ctx.Err()
566+
default:
567+
w.Send(&filesystem.ExecuteResponse{Output: line}, nil)
568+
}
569+
}
570+
if err != nil {
571+
if err != io.EOF {
572+
return hasOutput, fmt.Errorf("error reading stdout: %w", err)
573+
}
574+
break
527575
}
528-
}
529-
530-
if err := scanner.Err(); err != nil {
531-
return hasOutput, fmt.Errorf("error reading stdout: %w", err)
532576
}
533577

534578
return hasOutput, nil

adk/backend/local/local_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,3 +777,65 @@ func TestExecuteStreaming(t *testing.T) {
777777
assert.Less(t, elapsed, 2*time.Second, "background command should return immediately without waiting")
778778
})
779779
}
780+
781+
func TestExecute(t *testing.T) {
782+
ctx := context.Background()
783+
s, err := NewBackend(ctx, &Config{})
784+
assert.NoError(t, err)
785+
786+
t.Run("simple echo", func(t *testing.T) {
787+
resp, err := s.Execute(ctx, &filesystem.ExecuteRequest{Command: "echo hello"})
788+
assert.NoError(t, err)
789+
assert.Equal(t, "hello\n", resp.Output)
790+
assert.NotNil(t, resp.ExitCode)
791+
assert.Equal(t, 0, *resp.ExitCode)
792+
})
793+
794+
t.Run("multi-line output", func(t *testing.T) {
795+
resp, err := s.Execute(ctx, &filesystem.ExecuteRequest{Command: "echo line1 && echo line2 && echo line3"})
796+
assert.NoError(t, err)
797+
assert.Equal(t, "line1\nline2\nline3\n", resp.Output)
798+
assert.Equal(t, 0, *resp.ExitCode)
799+
})
800+
801+
t.Run("empty command", func(t *testing.T) {
802+
_, err := s.Execute(ctx, &filesystem.ExecuteRequest{Command: ""})
803+
assert.Error(t, err)
804+
assert.Contains(t, err.Error(), "command is required")
805+
})
806+
807+
t.Run("non-zero exit code", func(t *testing.T) {
808+
_, err := s.Execute(ctx, &filesystem.ExecuteRequest{Command: "exit 1"})
809+
assert.Error(t, err)
810+
assert.Contains(t, err.Error(), "non-zero code")
811+
})
812+
813+
t.Run("non-zero exit code with stderr", func(t *testing.T) {
814+
_, err := s.Execute(ctx, &filesystem.ExecuteRequest{Command: "echo fail >&2 && exit 2"})
815+
assert.Error(t, err)
816+
assert.Contains(t, err.Error(), "non-zero code 2")
817+
assert.Contains(t, err.Error(), "fail")
818+
})
819+
820+
t.Run("command not found", func(t *testing.T) {
821+
_, err := s.Execute(ctx, &filesystem.ExecuteRequest{Command: "nonexistent_command_xyz"})
822+
assert.Error(t, err)
823+
})
824+
825+
t.Run("context cancellation", func(t *testing.T) {
826+
cancelCtx, cancel := context.WithCancel(ctx)
827+
cancel()
828+
_, err := s.Execute(cancelCtx, &filesystem.ExecuteRequest{Command: "sleep 10"})
829+
assert.Error(t, err)
830+
})
831+
832+
t.Run("context cancellation during execution", func(t *testing.T) {
833+
cancelCtx, cancel := context.WithCancel(ctx)
834+
go func() {
835+
time.Sleep(100 * time.Millisecond)
836+
cancel()
837+
}()
838+
_, err := s.Execute(cancelCtx, &filesystem.ExecuteRequest{Command: "sleep 10"})
839+
assert.Error(t, err)
840+
})
841+
}

0 commit comments

Comments
 (0)