github.com/safing/portbase@v0.19.5/api/endpoints_debug.go (about)

     1  package api
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"runtime/pprof"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/safing/portbase/info"
    15  	"github.com/safing/portbase/modules"
    16  	"github.com/safing/portbase/utils/debug"
    17  )
    18  
    19  func registerDebugEndpoints() error {
    20  	if err := RegisterEndpoint(Endpoint{
    21  		Path:        "ping",
    22  		Read:        PermitAnyone,
    23  		ActionFunc:  ping,
    24  		Name:        "Ping",
    25  		Description: "Pong.",
    26  	}); err != nil {
    27  		return err
    28  	}
    29  
    30  	if err := RegisterEndpoint(Endpoint{
    31  		Path:        "ready",
    32  		Read:        PermitAnyone,
    33  		ActionFunc:  ready,
    34  		Name:        "Ready",
    35  		Description: "Check if Portmaster has completed starting and is ready.",
    36  	}); err != nil {
    37  		return err
    38  	}
    39  
    40  	if err := RegisterEndpoint(Endpoint{
    41  		Path:        "debug/stack",
    42  		Read:        PermitAnyone,
    43  		DataFunc:    getStack,
    44  		Name:        "Get Goroutine Stack",
    45  		Description: "Returns the current goroutine stack.",
    46  	}); err != nil {
    47  		return err
    48  	}
    49  
    50  	if err := RegisterEndpoint(Endpoint{
    51  		Path:        "debug/stack/print",
    52  		Read:        PermitAnyone,
    53  		ActionFunc:  printStack,
    54  		Name:        "Print Goroutine Stack",
    55  		Description: "Prints the current goroutine stack to stdout.",
    56  	}); err != nil {
    57  		return err
    58  	}
    59  
    60  	if err := RegisterEndpoint(Endpoint{
    61  		Path:     "debug/cpu",
    62  		MimeType: "application/octet-stream",
    63  		Read:     PermitAnyone,
    64  		DataFunc: handleCPUProfile,
    65  		Name:     "Get CPU Profile",
    66  		Description: strings.ReplaceAll(`Gather and return the CPU profile.
    67  This data needs to gathered over a period of time, which is specified using the duration parameter.
    68  
    69  You can easily view this data in your browser with this command (with Go installed):
    70  "go tool pprof -http :8888 http://127.0.0.1:817/api/v1/debug/cpu"
    71  `, `"`, "`"),
    72  		Parameters: []Parameter{{
    73  			Method:      http.MethodGet,
    74  			Field:       "duration",
    75  			Value:       "10s",
    76  			Description: "Specify the formatting style. The default is simple markdown formatting.",
    77  		}},
    78  	}); err != nil {
    79  		return err
    80  	}
    81  
    82  	if err := RegisterEndpoint(Endpoint{
    83  		Path:     "debug/heap",
    84  		MimeType: "application/octet-stream",
    85  		Read:     PermitAnyone,
    86  		DataFunc: handleHeapProfile,
    87  		Name:     "Get Heap Profile",
    88  		Description: strings.ReplaceAll(`Gather and return the heap memory profile.
    89  		
    90  		You can easily view this data in your browser with this command (with Go installed):
    91  		"go tool pprof -http :8888 http://127.0.0.1:817/api/v1/debug/heap"
    92  		`, `"`, "`"),
    93  	}); err != nil {
    94  		return err
    95  	}
    96  
    97  	if err := RegisterEndpoint(Endpoint{
    98  		Path:     "debug/allocs",
    99  		MimeType: "application/octet-stream",
   100  		Read:     PermitAnyone,
   101  		DataFunc: handleAllocsProfile,
   102  		Name:     "Get Allocs Profile",
   103  		Description: strings.ReplaceAll(`Gather and return the memory allocation profile.
   104  		
   105  		You can easily view this data in your browser with this command (with Go installed):
   106  		"go tool pprof -http :8888 http://127.0.0.1:817/api/v1/debug/allocs"
   107  		`, `"`, "`"),
   108  	}); err != nil {
   109  		return err
   110  	}
   111  
   112  	if err := RegisterEndpoint(Endpoint{
   113  		Path:        "debug/info",
   114  		Read:        PermitAnyone,
   115  		DataFunc:    debugInfo,
   116  		Name:        "Get Debug Information",
   117  		Description: "Returns debugging information, including the version and platform info, errors, logs and the current goroutine stack.",
   118  		Parameters: []Parameter{{
   119  			Method:      http.MethodGet,
   120  			Field:       "style",
   121  			Value:       "github",
   122  			Description: "Specify the formatting style. The default is simple markdown formatting.",
   123  		}},
   124  	}); err != nil {
   125  		return err
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // ping responds with pong.
   132  func ping(ar *Request) (msg string, err error) {
   133  	// TODO: Remove upgrade to "ready" when all UI components have transitioned.
   134  	if modules.IsStarting() || modules.IsShuttingDown() {
   135  		return "", ErrorWithStatus(errors.New("portmaster is not ready, reload (F5) to try again"), http.StatusTooEarly)
   136  	}
   137  
   138  	return "Pong.", nil
   139  }
   140  
   141  // ready checks if Portmaster has completed starting.
   142  func ready(ar *Request) (msg string, err error) {
   143  	if modules.IsStarting() || modules.IsShuttingDown() {
   144  		return "", ErrorWithStatus(errors.New("portmaster is not ready, reload (F5) to try again"), http.StatusTooEarly)
   145  	}
   146  	return "Portmaster is ready.", nil
   147  }
   148  
   149  // getStack returns the current goroutine stack.
   150  func getStack(_ *Request) (data []byte, err error) {
   151  	buf := &bytes.Buffer{}
   152  	err = pprof.Lookup("goroutine").WriteTo(buf, 1)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	return buf.Bytes(), nil
   157  }
   158  
   159  // printStack prints the current goroutine stack to stderr.
   160  func printStack(_ *Request) (msg string, err error) {
   161  	_, err = fmt.Fprint(os.Stderr, "===== PRINTING STACK =====\n")
   162  	if err == nil {
   163  		err = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   164  	}
   165  	if err == nil {
   166  		_, err = fmt.Fprint(os.Stderr, "===== END OF STACK =====\n")
   167  	}
   168  	if err != nil {
   169  		return "", err
   170  	}
   171  	return "stack printed to stdout", nil
   172  }
   173  
   174  // handleCPUProfile returns the CPU profile.
   175  func handleCPUProfile(ar *Request) (data []byte, err error) {
   176  	// Parse duration.
   177  	duration := 10 * time.Second
   178  	if durationOption := ar.Request.URL.Query().Get("duration"); durationOption != "" {
   179  		parsedDuration, err := time.ParseDuration(durationOption)
   180  		if err != nil {
   181  			return nil, fmt.Errorf("failed to parse duration: %w", err)
   182  		}
   183  		duration = parsedDuration
   184  	}
   185  
   186  	// Indicate download and filename.
   187  	ar.ResponseHeader.Set(
   188  		"Content-Disposition",
   189  		fmt.Sprintf(`attachment; filename="portmaster-cpu-profile_v%s.pprof"`, info.Version()),
   190  	)
   191  
   192  	// Start CPU profiling.
   193  	buf := new(bytes.Buffer)
   194  	if err := pprof.StartCPUProfile(buf); err != nil {
   195  		return nil, fmt.Errorf("failed to start cpu profile: %w", err)
   196  	}
   197  
   198  	// Wait for the specified duration.
   199  	select {
   200  	case <-time.After(duration):
   201  	case <-ar.Context().Done():
   202  		pprof.StopCPUProfile()
   203  		return nil, context.Canceled
   204  	}
   205  
   206  	// Stop CPU profiling and return data.
   207  	pprof.StopCPUProfile()
   208  	return buf.Bytes(), nil
   209  }
   210  
   211  // handleHeapProfile returns the Heap profile.
   212  func handleHeapProfile(ar *Request) (data []byte, err error) {
   213  	// Indicate download and filename.
   214  	ar.ResponseHeader.Set(
   215  		"Content-Disposition",
   216  		fmt.Sprintf(`attachment; filename="portmaster-memory-heap-profile_v%s.pprof"`, info.Version()),
   217  	)
   218  
   219  	buf := new(bytes.Buffer)
   220  	if err := pprof.Lookup("heap").WriteTo(buf, 0); err != nil {
   221  		return nil, fmt.Errorf("failed to write heap profile: %w", err)
   222  	}
   223  	return buf.Bytes(), nil
   224  }
   225  
   226  // handleAllocsProfile returns the Allocs profile.
   227  func handleAllocsProfile(ar *Request) (data []byte, err error) {
   228  	// Indicate download and filename.
   229  	ar.ResponseHeader.Set(
   230  		"Content-Disposition",
   231  		fmt.Sprintf(`attachment; filename="portmaster-memory-allocs-profile_v%s.pprof"`, info.Version()),
   232  	)
   233  
   234  	buf := new(bytes.Buffer)
   235  	if err := pprof.Lookup("allocs").WriteTo(buf, 0); err != nil {
   236  		return nil, fmt.Errorf("failed to write allocs profile: %w", err)
   237  	}
   238  	return buf.Bytes(), nil
   239  }
   240  
   241  // debugInfo returns the debugging information for support requests.
   242  func debugInfo(ar *Request) (data []byte, err error) {
   243  	// Create debug information helper.
   244  	di := new(debug.Info)
   245  	di.Style = ar.Request.URL.Query().Get("style")
   246  
   247  	// Add debug information.
   248  	di.AddVersionInfo()
   249  	di.AddPlatformInfo(ar.Context())
   250  	di.AddLastReportedModuleError()
   251  	di.AddLastUnexpectedLogs()
   252  	di.AddGoroutineStack()
   253  
   254  	// Return data.
   255  	return di.Bytes(), nil
   256  }