diff options
Diffstat (limited to 'internal/flamegraph')
| -rw-r--r-- | internal/flamegraph/liveserver.go | 79 | ||||
| -rw-r--r-- | internal/flamegraph/liveserver_open_test.go | 130 |
2 files changed, 208 insertions, 1 deletions
diff --git a/internal/flamegraph/liveserver.go b/internal/flamegraph/liveserver.go index 801b841..dda1af1 100644 --- a/internal/flamegraph/liveserver.go +++ b/internal/flamegraph/liveserver.go @@ -3,8 +3,12 @@ package flamegraph import ( "context" "encoding/json" + "errors" "fmt" "net/http" + "os/exec" + "runtime" + "strings" "time" ) @@ -14,18 +18,91 @@ var liveServerTimeouts = serverTimeouts{ idleTimeout: 60 * time.Second, } +type LiveServerOptions struct { + AutoOpenBrowser bool + OpenCommand string +} + +var openBrowserURLFn = openBrowserURL + // ServeLive starts the live flamegraph HTTP server and blocks until ctx is canceled. func ServeLive(ctx context.Context, lt *LiveTrie, interval time.Duration) error { + return ServeLiveWithOptions(ctx, lt, interval, LiveServerOptions{}) +} + +// ServeLiveWithOptions starts the live flamegraph server with runtime options. +func ServeLiveWithOptions(ctx context.Context, lt *LiveTrie, interval time.Duration, options LiveServerOptions) error { mux := http.NewServeMux() mux.HandleFunc("/", handleLivePage()) mux.HandleFunc("/events", handleSSE(lt, interval)) mux.HandleFunc("/reset", handleReset(lt)) mux.HandleFunc("/order", handleOrder(lt)) return runServer(ctx, mux, liveServerTimeouts, func(hostname string, port int) { - fmt.Printf("Live flamegraph available at http://%s:%d/\n", hostname, port) + url := fmt.Sprintf("http://%s:%d/", hostname, port) + fmt.Printf("Live flamegraph available at %s\n", url) + if err := maybeOpenLiveBrowser(url, options); err != nil { + fmt.Printf("Live flamegraph browser auto-open failed: %v\n", err) + } }) } +func maybeOpenLiveBrowser(url string, options LiveServerOptions) error { + if !options.AutoOpenBrowser { + return nil + } + return openBrowserURLFn(url, options.OpenCommand) +} + +func openBrowserURL(url, openCommand string) error { + parts, err := browserOpenCommandParts(runtime.GOOS, openCommand, url) + if err != nil { + return err + } + cmd := exec.Command(parts[0], parts[1:]...) + if err := cmd.Start(); err != nil { + return err + } + go func() { _ = cmd.Wait() }() + return nil +} + +func browserOpenCommandParts(goos, openCommand, url string) ([]string, error) { + var parts []string + if trimmed := strings.TrimSpace(openCommand); trimmed != "" { + parts = strings.Fields(trimmed) + } else { + parts = defaultBrowserCommand(goos) + } + if len(parts) == 0 { + return nil, errors.New("empty browser open command") + } + + containsURL := false + for i := range parts { + if strings.Contains(parts[i], "{url}") { + parts[i] = strings.ReplaceAll(parts[i], "{url}", url) + containsURL = true + } + } + if !containsURL { + parts = append(parts, url) + } + return parts, nil +} + +func defaultBrowserCommand(goos string) []string { + switch goos { + case "darwin": + return []string{"open", "-a", "Firefox"} + case "linux": + return []string{"firefox"} + case "windows": + return []string{"cmd", "/c", "start"} + default: + return []string{"firefox"} + } +} + func handleLivePage() http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") diff --git a/internal/flamegraph/liveserver_open_test.go b/internal/flamegraph/liveserver_open_test.go new file mode 100644 index 0000000..9d280b9 --- /dev/null +++ b/internal/flamegraph/liveserver_open_test.go @@ -0,0 +1,130 @@ +package flamegraph + +import ( + "errors" + "testing" +) + +func TestBrowserOpenCommandPartsUsesLinuxDefault(t *testing.T) { + got, err := browserOpenCommandParts("linux", "", "http://localhost:1234/") + if err != nil { + t.Fatalf("browserOpenCommandParts returned error: %v", err) + } + want := []string{"firefox", "http://localhost:1234/"} + if len(got) != len(want) { + t.Fatalf("len(parts) = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestBrowserOpenCommandPartsUsesDarwinDefault(t *testing.T) { + got, err := browserOpenCommandParts("darwin", "", "http://localhost:1234/") + if err != nil { + t.Fatalf("browserOpenCommandParts returned error: %v", err) + } + want := []string{"open", "-a", "Firefox", "http://localhost:1234/"} + if len(got) != len(want) { + t.Fatalf("len(parts) = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestBrowserOpenCommandPartsOverrideAppendsURL(t *testing.T) { + got, err := browserOpenCommandParts("linux", "chromium --new-window", "http://localhost:1234/") + if err != nil { + t.Fatalf("browserOpenCommandParts returned error: %v", err) + } + want := []string{"chromium", "--new-window", "http://localhost:1234/"} + if len(got) != len(want) { + t.Fatalf("len(parts) = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestBrowserOpenCommandPartsOverrideReplacesPlaceholder(t *testing.T) { + got, err := browserOpenCommandParts("linux", "open-browser --target={url}", "http://localhost:1234/") + if err != nil { + t.Fatalf("browserOpenCommandParts returned error: %v", err) + } + want := []string{"open-browser", "--target=http://localhost:1234/"} + if len(got) != len(want) { + t.Fatalf("len(parts) = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestMaybeOpenLiveBrowserDisabledSkipsOpen(t *testing.T) { + called := false + orig := openBrowserURLFn + openBrowserURLFn = func(url, openCommand string) error { + called = true + return nil + } + t.Cleanup(func() { openBrowserURLFn = orig }) + + err := maybeOpenLiveBrowser("http://localhost:1234/", LiveServerOptions{AutoOpenBrowser: false}) + if err != nil { + t.Fatalf("maybeOpenLiveBrowser returned error: %v", err) + } + if called { + t.Fatalf("expected browser opener not to be called when disabled") + } +} + +func TestMaybeOpenLiveBrowserEnabledCallsOpen(t *testing.T) { + called := false + orig := openBrowserURLFn + openBrowserURLFn = func(url, openCommand string) error { + called = true + if url != "http://localhost:1234/" { + t.Fatalf("url = %q, want %q", url, "http://localhost:1234/") + } + if openCommand != "chromium" { + t.Fatalf("openCommand = %q, want %q", openCommand, "chromium") + } + return nil + } + t.Cleanup(func() { openBrowserURLFn = orig }) + + err := maybeOpenLiveBrowser("http://localhost:1234/", LiveServerOptions{ + AutoOpenBrowser: true, + OpenCommand: "chromium", + }) + if err != nil { + t.Fatalf("maybeOpenLiveBrowser returned error: %v", err) + } + if !called { + t.Fatalf("expected browser opener to be called") + } +} + +func TestMaybeOpenLiveBrowserPropagatesOpenError(t *testing.T) { + orig := openBrowserURLFn + openBrowserURLFn = func(url, openCommand string) error { + return errors.New("launch failed") + } + t.Cleanup(func() { openBrowserURLFn = orig }) + + err := maybeOpenLiveBrowser("http://localhost:1234/", LiveServerOptions{ + AutoOpenBrowser: true, + }) + if err == nil || err.Error() != "launch failed" { + t.Fatalf("expected launch failed error, got %v", err) + } +} |
