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