1// Copyright 2023 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package telemetry manages the telemetry mode file. 6package telemetry 7 8import ( 9 "fmt" 10 "os" 11 "path/filepath" 12 "runtime" 13 "strings" 14 "time" 15) 16 17// Default is the default directory containing Go telemetry configuration and 18// data. 19// 20// If Default is uninitialized, Default.Mode will be "off". As a consequence, 21// no data should be written to the directory, and so the path values of 22// LocalDir, UploadDir, etc. must not matter. 23// 24// Default is a global for convenience and testing, but should not be mutated 25// outside of tests. 26// 27// TODO(rfindley): it would be nice to completely eliminate this global state, 28// or at least push it in the golang.org/x/telemetry package 29var Default Dir 30 31// A Dir holds paths to telemetry data inside a directory. 32type Dir struct { 33 dir, local, upload, debug, modefile string 34} 35 36// NewDir creates a new Dir encapsulating paths in the given dir. 37// 38// NewDir does not create any new directories or files--it merely encapsulates 39// the telemetry directory layout. 40func NewDir(dir string) Dir { 41 return Dir{ 42 dir: dir, 43 local: filepath.Join(dir, "local"), 44 upload: filepath.Join(dir, "upload"), 45 debug: filepath.Join(dir, "debug"), 46 modefile: filepath.Join(dir, "mode"), 47 } 48} 49 50func init() { 51 cfgDir, err := os.UserConfigDir() 52 if err != nil { 53 return 54 } 55 Default = NewDir(filepath.Join(cfgDir, "go", "telemetry")) 56} 57 58func (d Dir) Dir() string { 59 return d.dir 60} 61 62func (d Dir) LocalDir() string { 63 return d.local 64} 65 66func (d Dir) UploadDir() string { 67 return d.upload 68} 69 70func (d Dir) DebugDir() string { 71 return d.debug 72} 73 74func (d Dir) ModeFile() string { 75 return d.modefile 76} 77 78// SetMode updates the telemetry mode with the given mode. 79// Acceptable values for mode are "on", "off", or "local". 80// 81// SetMode always writes the mode file, and explicitly records the date at 82// which the modefile was updated. This means that calling SetMode with "on" 83// effectively resets the timeout before the next telemetry report is uploaded. 84func (d Dir) SetMode(mode string) error { 85 return d.SetModeAsOf(mode, time.Now()) 86} 87 88// SetModeAsOf is like SetMode, but accepts an explicit time to use to 89// back-date the mode state. This exists only for testing purposes. 90func (d Dir) SetModeAsOf(mode string, asofTime time.Time) error { 91 mode = strings.TrimSpace(mode) 92 switch mode { 93 case "on", "off", "local": 94 default: 95 return fmt.Errorf("invalid telemetry mode: %q", mode) 96 } 97 if d.modefile == "" { 98 return fmt.Errorf("cannot determine telemetry mode file name") 99 } 100 // TODO(rfindley): why is this not 777, consistent with the use of 666 below? 101 if err := os.MkdirAll(filepath.Dir(d.modefile), 0755); err != nil { 102 return fmt.Errorf("cannot create a telemetry mode file: %w", err) 103 } 104 105 asof := asofTime.UTC().Format(time.DateOnly) 106 // Defensively guarantee that we can parse the asof time. 107 if _, err := time.Parse(time.DateOnly, asof); err != nil { 108 return fmt.Errorf("internal error: invalid mode date %q: %v", asof, err) 109 } 110 111 data := []byte(mode + " " + asof) 112 return os.WriteFile(d.modefile, data, 0666) 113} 114 115// Mode returns the current telemetry mode, as well as the time that the mode 116// was effective. 117// 118// If there is no effective time, the second result is the zero time. 119// 120// If Mode is "off", no data should be written to the telemetry directory, and 121// the other paths values referenced by Dir should be considered undefined. 122// This accounts for the case where initializing [Default] fails, and therefore 123// local telemetry paths are unknown. 124func (d Dir) Mode() (string, time.Time) { 125 if d.modefile == "" { 126 return "off", time.Time{} // it's likely LocalDir/UploadDir are empty too. Turn off telemetry. 127 } 128 data, err := os.ReadFile(d.modefile) 129 if err != nil { 130 return "local", time.Time{} // default 131 } 132 mode := string(data) 133 mode = strings.TrimSpace(mode) 134 135 // Forward compatibility for https://go.dev/issue/63142#issuecomment-1734025130 136 // 137 // If the modefile contains a date, return it. 138 if idx := strings.Index(mode, " "); idx >= 0 { 139 d, err := time.Parse(time.DateOnly, mode[idx+1:]) 140 if err != nil { 141 d = time.Time{} 142 } 143 return mode[:idx], d 144 } 145 146 return mode, time.Time{} 147} 148 149// DisabledOnPlatform indicates whether telemetry is disabled 150// due to bugs in the current platform. 151const DisabledOnPlatform = false || 152 // The following platforms could potentially be supported in the future: 153 runtime.GOOS == "openbsd" || // #60614 154 runtime.GOOS == "solaris" || // #60968 #60970 155 runtime.GOOS == "android" || // #60967 156 runtime.GOOS == "illumos" || // #65544 157 // These platforms fundamentally can't be supported: 158 runtime.GOOS == "js" || // #60971 159 runtime.GOOS == "wasip1" || // #60971 160 runtime.GOOS == "plan9" // https://github.com/golang/go/issues/57540#issuecomment-1470766639 161