package driver import ( "encoding/json" "fmt" "net/url" "os" "path/filepath" ) // settings holds pprof settings. type settings struct { // Configs holds a list of named UI configurations. Configs []namedConfig `json:"configs"` } // namedConfig associates a name with a config. type namedConfig struct { Name string `json:"name"` config } // settingsFileName returns the name of the file where settings should be saved. func settingsFileName() (string, error) { // Return "pprof/settings.json" under os.UserConfigDir(). dir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, "pprof", "settings.json"), nil } // readSettings reads settings from fname. func readSettings(fname string) (*settings, error) { data, err := os.ReadFile(fname) if err != nil { if os.IsNotExist(err) { return &settings{}, nil } return nil, fmt.Errorf("could not read settings: %w", err) } settings := &settings{} if err := json.Unmarshal(data, settings); err != nil { return nil, fmt.Errorf("could not parse settings: %w", err) } for i := range settings.Configs { settings.Configs[i].resetTransient() } return settings, nil } // writeSettings saves settings to fname. func writeSettings(fname string, settings *settings) error { data, err := json.MarshalIndent(settings, "", " ") if err != nil { return fmt.Errorf("could not encode settings: %w", err) } // create the settings directory if it does not exist // XDG specifies permissions 0700 when creating settings dirs: // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil { return fmt.Errorf("failed to create settings directory: %w", err) } if err := os.WriteFile(fname, data, 0644); err != nil { return fmt.Errorf("failed to write settings: %w", err) } return nil } // configMenuEntry holds information for a single config menu entry. type configMenuEntry struct { Name string URL string Current bool // Is this the currently selected config? UserConfig bool // Is this a user-provided config? } // configMenu returns a list of items to add to a menu in the web UI. func configMenu(fname string, u url.URL) []configMenuEntry { // Start with system configs. configs := []namedConfig{{Name: "Default", config: defaultConfig()}} if settings, err := readSettings(fname); err == nil { // Add user configs. configs = append(configs, settings.Configs...) } // Convert to menu entries. result := make([]configMenuEntry, len(configs)) lastMatch := -1 for i, cfg := range configs { dst, changed := cfg.config.makeURL(u) if !changed { lastMatch = i } // Use a relative URL to work in presence of stripping/redirects in webui.go. rel := &url.URL{RawQuery: dst.RawQuery, ForceQuery: true} result[i] = configMenuEntry{ Name: cfg.Name, URL: rel.String(), UserConfig: (i != 0), } } // Mark the last matching config as current if lastMatch >= 0 { result[lastMatch].Current = true } return result } // editSettings edits settings by applying fn to them. func editSettings(fname string, fn func(s *settings) error) error { settings, err := readSettings(fname) if err != nil { return err } if err := fn(settings); err != nil { return err } return writeSettings(fname, settings) } // setConfig saves the config specified in request to fname. func setConfig(fname string, request url.URL) error { q := request.Query() name := q.Get("config") if name == "" { return fmt.Errorf("invalid config name") } cfg := currentConfig() if err := cfg.applyURL(q); err != nil { return err } return editSettings(fname, func(s *settings) error { for i, c := range s.Configs { if c.Name == name { s.Configs[i].config = cfg return nil } } s.Configs = append(s.Configs, namedConfig{Name: name, config: cfg}) return nil }) } // removeConfig removes config from fname. func removeConfig(fname, config string) error { return editSettings(fname, func(s *settings) error { for i, c := range s.Configs { if c.Name == config { s.Configs = append(s.Configs[:i], s.Configs[i+1:]...) return nil } } return fmt.Errorf("config %s not found", config) }) }