blob: ee3dec5a56c433675a4b4a1801d6184c8935c1fe [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.25
package debug
import (
"bufio"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime/trace"
"strings"
"sync"
"syscall"
"time"
)
var (
traceviewersMu sync.Mutex
traceviewers []*os.Process
kill = (*os.Process).Kill // windows, plan9; UNIX impl kills whole process group
sysProcAttr syscall.SysProcAttr // UNIX configuration to create process group
)
// KillTraceViewers kills all "go tool trace" processes started by
// /flightrecorder requests, for use in tests (see #74668).
func KillTraceViewers() {
traceviewersMu.Lock()
for _, p := range traceviewers {
kill(p) // ignore error
}
traceviewers = nil
traceviewersMu.Unlock()
}
// The FlightRecorder is a global resource, so create at most one per process.
var getRecorder = sync.OnceValues(func() (*trace.FlightRecorder, error) {
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
// half a minute is usually enough to know "what just happened?"
MinAge: 30 * time.Second,
})
if err := fr.Start(); err != nil {
return nil, err
}
return fr, nil
})
func startFlightRecorder() (http.HandlerFunc, error) {
fr, err := getRecorder()
if err != nil {
return nil, err
}
// Return a handler that writes the most recent flight record,
// starts a trace viewer server, and redirects to it.
return func(w http.ResponseWriter, r *http.Request) {
errorf := func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
http.Error(w, msg, http.StatusInternalServerError)
}
// Write the most recent flight record into a temp file.
f, err := os.CreateTemp("", "flightrecord")
if err != nil {
errorf("can't create temp file for flight record: %v", err)
return
}
if _, err := fr.WriteTo(f); err != nil {
f.Close() // ignore error
errorf("failed to write flight record: %s", err)
return
}
if err := f.Close(); err != nil {
errorf("failed to close flight record: %s", err)
return
}
tracefile, err := filepath.Abs(f.Name())
if err != nil {
errorf("can't absolutize name of trace file: %v", err)
return
}
// Run 'go tool trace' to start a new trace-viewer
// web server process. It will run until gopls terminates.
// (It would be nicer if we could just link it in; see #66843.)
cmd := exec.Command("go", "tool", "trace", tracefile)
cmd.SysProcAttr = &sysProcAttr
// Don't connect trace's std{out,err} to our os.Stderr directly,
// otherwise the child may outlive the parent in tests,
// and 'go test' will complain about unclosed pipes.
// Instead, interpose a pipe that will close when gopls exits.
// See CL 677262 for a better solution (a cmd/trace flag).
// (#66843 is of course better still.)
// Also, this notifies us of the server's readiness and URL.
urlC := make(chan string)
{
r, w, err := os.Pipe()
if err != nil {
errorf("can't create pipe: %v", err)
return
}
go func() {
// Copy from the pipe to stderr,
// keeping an eye out for the "listening on URL" string.
scan := bufio.NewScanner(r)
for scan.Scan() {
line := scan.Text()
if _, url, ok := strings.Cut(line, "Trace viewer is listening on "); ok {
urlC <- url
}
fmt.Fprintln(os.Stderr, line)
}
if err := scan.Err(); err != nil {
log.Printf("reading from pipe to cmd/trace: %v", err)
}
}()
cmd.Stderr = w
cmd.Stdout = w
}
// Suppress the usual cmd/trace behavior of opening a new
// browser tab by setting BROWSER to /usr/bin/true (a no-op).
cmd.Env = append(os.Environ(), "BROWSER=true")
if err := cmd.Start(); err != nil {
errorf("failed to start trace server: %s", err)
return
}
// Save the process so we can kill it when tests finish.
traceviewersMu.Lock()
traceviewers = append(traceviewers, cmd.Process)
traceviewersMu.Unlock()
// Some of the CI builders can be quite heavily loaded.
// Give them an extra grace period.
timeout := 10 * time.Second
if os.Getenv("GO_BUILDER_NAME") != "" {
timeout = 1 * time.Minute
}
select {
case addr := <-urlC:
// Success! Send a redirect to the new location.
// (This URL bypasses the help screen at /.)
http.Redirect(w, r, addr+"/trace?view=proc", 302)
case <-r.Context().Done():
errorf("canceled")
case <-time.After(timeout):
errorf("trace viewer failed to start within %v", timeout)
}
}, nil
}