summaryrefslogtreecommitdiff
path: root/internal/flamegraph/webserver.go
blob: b1a68e939d831eac19da9b4adf23d4c45cab2346 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package flamegraph

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"
)

// ServeSVG starts a small HTTP server that serves a single flamegraph SVG.
//
// It prints a URL of the form http://HOSTNAME:PORT/abs/path/to.svg and blocks until
// the user presses Ctrl+C or the process receives SIGTERM, at which point the server
// is shut down gracefully.
func ServeSVG(svgFile string) error {
	absPath, err := filepath.Abs(svgFile)
	if err != nil {
		return fmt.Errorf("resolve svg path: %w", err)
	}
	urlPath := buildURLPath(absPath)
	srv := &http.Server{Handler: buildSVGHandler(absPath, urlPath)}

	listener, err := listenRandomPort()
	if err != nil {
		return err
	}
	defer listener.Close()

	hostname, port := serverHostPort(listener)
	printServerURL(hostname, port, urlPath)

	errCh := make(chan error, 1)
	go func() {
		errCh <- srv.Serve(listener)
	}()

	return waitForStop(srv, errCh)
}

func buildURLPath(absPath string) string {
	urlPath := filepath.ToSlash(absPath)
	if !strings.HasPrefix(urlPath, "/") {
		return "/" + urlPath
	}
	return urlPath
}

func buildSVGHandler(absPath, urlPath string) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, urlPath, http.StatusFound)
	})
	mux.HandleFunc(urlPath, func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, absPath)
	})
	return mux
}

func listenRandomPort() (net.Listener, error) {
	listener, err := net.Listen("tcp", ":0")
	if err != nil {
		return nil, fmt.Errorf("start web server: %w", err)
	}
	return listener, nil
}

func serverHostPort(listener net.Listener) (string, int) {
	hostname, err := os.Hostname()
	if err != nil {
		hostname = "localhost"
	}
	port := listener.Addr().(*net.TCPAddr).Port
	return hostname, port
}

func printServerURL(hostname string, port int, urlPath string) {
	fmt.Printf("Flamegraph available at http://%s:%d%s\n", hostname, port, urlPath)
	fmt.Println("Press Ctrl+C to stop the web server.")
}

func waitForStop(srv *http.Server, errCh <-chan error) error {
	stopCh := make(chan os.Signal, 1)
	signal.Notify(stopCh, os.Interrupt, syscall.SIGTERM)
	defer signal.Stop(stopCh)

	select {
	case sig := <-stopCh:
		_ = sig
	case serveErr := <-errCh:
		if serveErr != nil && serveErr != http.ErrServerClosed {
			return serveErr
		}
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		return fmt.Errorf("shutdown web server: %w", err)
	}

	serveErr := <-errCh
	if serveErr != nil && serveErr != http.ErrServerClosed {
		return serveErr
	}
	return nil
}