1package driver 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/url" 7 "os" 8 "path/filepath" 9) 10 11// settings holds pprof settings. 12type settings struct { 13 // Configs holds a list of named UI configurations. 14 Configs []namedConfig `json:"configs"` 15} 16 17// namedConfig associates a name with a config. 18type namedConfig struct { 19 Name string `json:"name"` 20 config 21} 22 23// settingsFileName returns the name of the file where settings should be saved. 24func settingsFileName() (string, error) { 25 // Return "pprof/settings.json" under os.UserConfigDir(). 26 dir, err := os.UserConfigDir() 27 if err != nil { 28 return "", err 29 } 30 return filepath.Join(dir, "pprof", "settings.json"), nil 31} 32 33// readSettings reads settings from fname. 34func readSettings(fname string) (*settings, error) { 35 data, err := os.ReadFile(fname) 36 if err != nil { 37 if os.IsNotExist(err) { 38 return &settings{}, nil 39 } 40 return nil, fmt.Errorf("could not read settings: %w", err) 41 } 42 settings := &settings{} 43 if err := json.Unmarshal(data, settings); err != nil { 44 return nil, fmt.Errorf("could not parse settings: %w", err) 45 } 46 for i := range settings.Configs { 47 settings.Configs[i].resetTransient() 48 } 49 return settings, nil 50} 51 52// writeSettings saves settings to fname. 53func writeSettings(fname string, settings *settings) error { 54 data, err := json.MarshalIndent(settings, "", " ") 55 if err != nil { 56 return fmt.Errorf("could not encode settings: %w", err) 57 } 58 59 // create the settings directory if it does not exist 60 // XDG specifies permissions 0700 when creating settings dirs: 61 // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 62 if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil { 63 return fmt.Errorf("failed to create settings directory: %w", err) 64 } 65 66 if err := os.WriteFile(fname, data, 0644); err != nil { 67 return fmt.Errorf("failed to write settings: %w", err) 68 } 69 return nil 70} 71 72// configMenuEntry holds information for a single config menu entry. 73type configMenuEntry struct { 74 Name string 75 URL string 76 Current bool // Is this the currently selected config? 77 UserConfig bool // Is this a user-provided config? 78} 79 80// configMenu returns a list of items to add to a menu in the web UI. 81func configMenu(fname string, u url.URL) []configMenuEntry { 82 // Start with system configs. 83 configs := []namedConfig{{Name: "Default", config: defaultConfig()}} 84 if settings, err := readSettings(fname); err == nil { 85 // Add user configs. 86 configs = append(configs, settings.Configs...) 87 } 88 89 // Convert to menu entries. 90 result := make([]configMenuEntry, len(configs)) 91 lastMatch := -1 92 for i, cfg := range configs { 93 dst, changed := cfg.config.makeURL(u) 94 if !changed { 95 lastMatch = i 96 } 97 // Use a relative URL to work in presence of stripping/redirects in webui.go. 98 rel := &url.URL{RawQuery: dst.RawQuery, ForceQuery: true} 99 result[i] = configMenuEntry{ 100 Name: cfg.Name, 101 URL: rel.String(), 102 UserConfig: (i != 0), 103 } 104 } 105 // Mark the last matching config as current 106 if lastMatch >= 0 { 107 result[lastMatch].Current = true 108 } 109 return result 110} 111 112// editSettings edits settings by applying fn to them. 113func editSettings(fname string, fn func(s *settings) error) error { 114 settings, err := readSettings(fname) 115 if err != nil { 116 return err 117 } 118 if err := fn(settings); err != nil { 119 return err 120 } 121 return writeSettings(fname, settings) 122} 123 124// setConfig saves the config specified in request to fname. 125func setConfig(fname string, request url.URL) error { 126 q := request.Query() 127 name := q.Get("config") 128 if name == "" { 129 return fmt.Errorf("invalid config name") 130 } 131 cfg := currentConfig() 132 if err := cfg.applyURL(q); err != nil { 133 return err 134 } 135 return editSettings(fname, func(s *settings) error { 136 for i, c := range s.Configs { 137 if c.Name == name { 138 s.Configs[i].config = cfg 139 return nil 140 } 141 } 142 s.Configs = append(s.Configs, namedConfig{Name: name, config: cfg}) 143 return nil 144 }) 145} 146 147// removeConfig removes config from fname. 148func removeConfig(fname, config string) error { 149 return editSettings(fname, func(s *settings) error { 150 for i, c := range s.Configs { 151 if c.Name == config { 152 s.Configs = append(s.Configs[:i], s.Configs[i+1:]...) 153 return nil 154 } 155 } 156 return fmt.Errorf("config %s not found", config) 157 }) 158} 159