Skip to content

Commit bd2414d

Browse files
committed
Make run.go easier to read
Assisted-By: docker-agent
1 parent 0dbc1be commit bd2414d

4 files changed

Lines changed: 161 additions & 92 deletions

File tree

cmd/root/record.go

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,36 @@
11
package root
22

33
import (
4-
"fmt"
5-
"log/slog"
6-
"strings"
7-
"time"
8-
94
"github.com/docker/docker-agent/pkg/config"
10-
"github.com/docker/docker-agent/pkg/fake"
5+
"github.com/docker/docker-agent/pkg/recording"
116
)
127

138
// setupFakeProxy starts a fake proxy if fakeResponses is non-empty.
14-
// streamDelayMs controls simulated streaming: 0 = disabled, >0 = delay in milliseconds between chunks.
15-
// It returns a cleanup function that must be called when done (typically via defer).
9+
// It configures the runtime config's ModelsGateway to point to the proxy.
1610
func setupFakeProxy(fakeResponses string, streamDelayMs int, runConfig *config.RuntimeConfig) (cleanup func() error, err error) {
17-
if fakeResponses == "" {
18-
return func() error { return nil }, nil
19-
}
20-
21-
// Normalize path by stripping .yaml suffix (go-vcr adds it automatically)
22-
fakeResponses = strings.TrimSuffix(fakeResponses, ".yaml")
23-
24-
var opts []fake.ProxyOption
25-
if streamDelayMs > 0 {
26-
opts = append(opts,
27-
fake.WithSimulateStream(true),
28-
fake.WithStreamChunkDelay(time.Duration(streamDelayMs)*time.Millisecond),
29-
)
30-
}
31-
32-
proxyURL, cleanupFn, err := fake.StartProxy(fakeResponses, opts...)
11+
proxyURL, cleanupFn, err := recording.SetupFakeProxy(fakeResponses, streamDelayMs)
3312
if err != nil {
34-
return nil, fmt.Errorf("failed to start fake proxy: %w", err)
13+
return nil, err
3514
}
3615

37-
runConfig.ModelsGateway = proxyURL
38-
slog.Info("Fake mode enabled", "cassette", fakeResponses, "proxy", proxyURL)
16+
if proxyURL != "" {
17+
runConfig.ModelsGateway = proxyURL
18+
}
3919

4020
return cleanupFn, nil
4121
}
4222

4323
// setupRecordingProxy starts a recording proxy if recordPath is non-empty.
44-
// It handles auto-generating a filename when recordPath is "true" (from NoOptDefVal),
45-
// and normalizes the path by stripping any .yaml suffix.
46-
// Returns the cassette path (with .yaml extension) and a cleanup function.
47-
// The cleanup function must be called when done (typically via defer).
24+
// It configures the runtime config's ModelsGateway to point to the proxy.
4825
func setupRecordingProxy(recordPath string, runConfig *config.RuntimeConfig) (cassettePath string, cleanup func() error, err error) {
49-
if recordPath == "" {
50-
return "", func() error { return nil }, nil
51-
}
52-
53-
// Handle auto-generated filename (from NoOptDefVal)
54-
if recordPath == "true" {
55-
recordPath = fmt.Sprintf("cagent-recording-%d", time.Now().Unix())
56-
} else {
57-
recordPath = strings.TrimSuffix(recordPath, ".yaml")
58-
}
59-
60-
proxyURL, cleanupFn, err := fake.StartRecordingProxy(recordPath)
26+
cassettePath, proxyURL, cleanupFn, err := recording.SetupRecordingProxy(recordPath)
6127
if err != nil {
62-
return "", nil, fmt.Errorf("failed to start recording proxy: %w", err)
28+
return "", nil, err
6329
}
6430

65-
runConfig.ModelsGateway = proxyURL
66-
cassettePath = recordPath + ".yaml"
67-
68-
slog.Info("Recording mode enabled", "cassette", cassettePath, "proxy", proxyURL)
31+
if proxyURL != "" {
32+
runConfig.ModelsGateway = proxyURL
33+
}
6934

7035
return cassettePath, cleanupFn, nil
7136
}

cmd/root/run.go

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"log/slog"
99
"os"
1010
"path/filepath"
11-
goruntime "runtime"
12-
"runtime/pprof"
1311
"sync"
1412
"time"
1513

@@ -22,6 +20,7 @@ import (
2220
"github.com/docker/docker-agent/pkg/cli"
2321
"github.com/docker/docker-agent/pkg/config"
2422
"github.com/docker/docker-agent/pkg/paths"
23+
"github.com/docker/docker-agent/pkg/profiling"
2524
"github.com/docker/docker-agent/pkg/runtime"
2625
"github.com/docker/docker-agent/pkg/session"
2726
"github.com/docker/docker-agent/pkg/sessiontitle"
@@ -145,37 +144,16 @@ func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) error {
145144
func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []string, useTUI bool) error {
146145
slog.Debug("Starting agent", "agent", f.agentName)
147146

148-
// Start CPU profiling if requested
149-
if f.cpuProfile != "" {
150-
pf, err := os.Create(f.cpuProfile)
151-
if err != nil {
152-
return fmt.Errorf("failed to create CPU profile: %w", err)
153-
}
154-
if err := pprof.StartCPUProfile(pf); err != nil {
155-
pf.Close()
156-
return fmt.Errorf("failed to start CPU profile: %w", err)
157-
}
158-
defer pprof.StopCPUProfile()
159-
defer pf.Close()
160-
slog.Info("CPU profiling enabled", "file", f.cpuProfile)
161-
}
162-
163-
// Write memory profile at exit if requested
164-
if f.memProfile != "" {
165-
defer func() {
166-
mf, err := os.Create(f.memProfile)
167-
if err != nil {
168-
slog.Error("Failed to create memory profile", "error", err)
169-
return
170-
}
171-
defer mf.Close()
172-
goruntime.GC() // Get up-to-date statistics
173-
if err := pprof.WriteHeapProfile(mf); err != nil {
174-
slog.Error("Failed to write memory profile", "error", err)
175-
}
176-
slog.Info("Memory profile written", "file", f.memProfile)
177-
}()
147+
// Start profiling if requested
148+
stopProfiling, err := profiling.Start(f.cpuProfile, f.memProfile)
149+
if err != nil {
150+
return err
178151
}
152+
defer func() {
153+
if err := stopProfiling(); err != nil {
154+
slog.Error("Profiling cleanup failed", "error", err)
155+
}
156+
}()
179157

180158
var agentFileName string
181159
if len(args) > 0 {
@@ -271,10 +249,6 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
271249
}
272250
defer initialTeamCleanup()
273251

274-
if useTUI {
275-
applyTheme()
276-
}
277-
278252
if f.dryRun {
279253
out.Println("Dry run mode enabled. Agent initialized but will not execute.")
280254
return nil
@@ -284,19 +258,13 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
284258
return f.handleExecMode(ctx, out, rt, sess, args)
285259
}
286260

261+
applyTheme()
287262
opts, err := f.buildAppOpts(args)
288263
if err != nil {
289264
return err
290265
}
291266

292-
var sessStore session.Store
293-
switch typedRt := rt.(type) {
294-
case *runtime.LocalRuntime:
295-
sessStore = typedRt.SessionStore()
296-
case *runtime.PersistentRuntime:
297-
sessStore = typedRt.SessionStore()
298-
}
299-
267+
sessStore := rt.SessionStore()
300268
return runTUI(ctx, rt, sess, f.createSessionSpawner(agentSource, sessStore), initialTeamCleanup, opts...)
301269
}
302270

pkg/profiling/profiling.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Package profiling provides helpers for CPU and memory profiling.
2+
package profiling
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"os"
8+
"runtime"
9+
"runtime/pprof"
10+
)
11+
12+
// Stop is a function returned by Start that stops profiling and flushes
13+
// any buffered data. It must be called (typically via defer) when the
14+
// profiled section of code completes.
15+
type Stop func() error
16+
17+
// Start begins CPU and/or memory profiling based on the provided file
18+
// paths. Pass an empty string to skip the corresponding profile.
19+
// The returned Stop function must be called to finalise the profiles.
20+
func Start(cpuProfile, memProfile string) (Stop, error) {
21+
var closers []func() error
22+
23+
if cpuProfile != "" {
24+
f, err := os.Create(cpuProfile)
25+
if err != nil {
26+
return noop, fmt.Errorf("failed to create CPU profile: %w", err)
27+
}
28+
if err := pprof.StartCPUProfile(f); err != nil {
29+
f.Close()
30+
return noop, fmt.Errorf("failed to start CPU profile: %w", err)
31+
}
32+
closers = append(closers, func() error {
33+
pprof.StopCPUProfile()
34+
return f.Close()
35+
})
36+
}
37+
38+
if memProfile != "" {
39+
closers = append(closers, func() error {
40+
f, err := os.Create(memProfile)
41+
if err != nil {
42+
return fmt.Errorf("failed to create memory profile: %w", err)
43+
}
44+
defer f.Close()
45+
runtime.GC()
46+
if err := pprof.WriteHeapProfile(f); err != nil {
47+
return fmt.Errorf("failed to write memory profile: %w", err)
48+
}
49+
return nil
50+
})
51+
}
52+
53+
return func() error {
54+
// Run in reverse order so CPU profile is stopped before mem profile is written.
55+
var errs []error
56+
for i := len(closers) - 1; i >= 0; i-- {
57+
if err := closers[i](); err != nil {
58+
errs = append(errs, err)
59+
}
60+
}
61+
return errors.Join(errs...)
62+
}, nil
63+
}
64+
65+
func noop() error { return nil }

pkg/recording/recording.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Package recording provides helpers for recording and replaying AI API interactions.
2+
package recording
3+
4+
import (
5+
"fmt"
6+
"log/slog"
7+
"strings"
8+
"time"
9+
10+
"github.com/docker/docker-agent/pkg/fake"
11+
)
12+
13+
// SetupFakeProxy starts a fake proxy if fakeResponses is non-empty.
14+
// streamDelayMs controls simulated streaming: 0 = disabled, >0 = delay in milliseconds between chunks.
15+
// It returns the proxy URL and a cleanup function that must be called when done (typically via defer).
16+
func SetupFakeProxy(fakeResponses string, streamDelayMs int) (proxyURL string, cleanup func() error, err error) {
17+
if fakeResponses == "" {
18+
return "", noop, nil
19+
}
20+
21+
// Normalize path by stripping .yaml suffix (go-vcr adds it automatically)
22+
fakeResponses = strings.TrimSuffix(fakeResponses, ".yaml")
23+
24+
var opts []fake.ProxyOption
25+
if streamDelayMs > 0 {
26+
opts = append(opts,
27+
fake.WithSimulateStream(true),
28+
fake.WithStreamChunkDelay(time.Duration(streamDelayMs)*time.Millisecond),
29+
)
30+
}
31+
32+
proxyURL, cleanupFn, err := fake.StartProxy(fakeResponses, opts...)
33+
if err != nil {
34+
return "", nil, fmt.Errorf("failed to start fake proxy: %w", err)
35+
}
36+
37+
slog.Info("Fake mode enabled", "cassette", fakeResponses, "proxy", proxyURL)
38+
39+
return proxyURL, cleanupFn, nil
40+
}
41+
42+
// SetupRecordingProxy starts a recording proxy if recordPath is non-empty.
43+
// It handles auto-generating a filename when recordPath is "true" (from NoOptDefVal),
44+
// and normalizes the path by stripping any .yaml suffix.
45+
// Returns the cassette path (with .yaml extension), the proxy URL, and a cleanup function.
46+
// The cleanup function must be called when done (typically via defer).
47+
func SetupRecordingProxy(recordPath string) (cassettePath, proxyURL string, cleanup func() error, err error) {
48+
if recordPath == "" {
49+
return "", "", noop, nil
50+
}
51+
52+
// Handle auto-generated filename (from NoOptDefVal)
53+
if recordPath == "true" {
54+
recordPath = fmt.Sprintf("cagent-recording-%d", time.Now().Unix())
55+
} else {
56+
recordPath = strings.TrimSuffix(recordPath, ".yaml")
57+
}
58+
59+
proxyURL, cleanupFn, err := fake.StartRecordingProxy(recordPath)
60+
if err != nil {
61+
return "", "", nil, fmt.Errorf("failed to start recording proxy: %w", err)
62+
}
63+
64+
cassettePath = recordPath + ".yaml"
65+
66+
slog.Info("Recording mode enabled", "cassette", cassettePath, "proxy", proxyURL)
67+
68+
return cassettePath, proxyURL, cleanupFn, nil
69+
}
70+
71+
func noop() error { return nil }

0 commit comments

Comments
 (0)