|  | // Copyright 2024 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package telemetry | 
|  |  | 
|  | import ( | 
|  | "fmt" | 
|  | "log" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "sync" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/sync/errgroup" | 
|  | "golang.org/x/telemetry/counter" | 
|  | "golang.org/x/telemetry/internal/crashmonitor" | 
|  | "golang.org/x/telemetry/internal/telemetry" | 
|  | "golang.org/x/telemetry/internal/upload" | 
|  | ) | 
|  |  | 
|  | // Config controls the behavior of [Start]. | 
|  | type Config struct { | 
|  | // ReportCrashes, if set, will enable crash reporting. | 
|  | // ReportCrashes uses the [debug.SetCrashOutput] mechanism, which is a | 
|  | // process-wide resource. | 
|  | // Do not make other calls to that function within your application. | 
|  | // ReportCrashes is a non-functional unless the program is built with go1.23+. | 
|  | ReportCrashes bool | 
|  |  | 
|  | // Upload causes this program to periodically upload approved counters | 
|  | // from the local telemetry database to telemetry.go.dev. | 
|  | // | 
|  | // This option has no effect unless the user has given consent | 
|  | // to enable data collection, for example by running | 
|  | // cmd/gotelemetry or affirming the gopls dialog. | 
|  | // | 
|  | // (This feature is expected to be used only by gopls. | 
|  | // Longer term, the go command may become the sole program | 
|  | // responsible for uploading.) | 
|  | Upload bool | 
|  |  | 
|  | // TelemetryDir, if set, will specify an alternate telemetry | 
|  | // directory to write data to. If not set, it uses the default | 
|  | // directory. | 
|  | // This field is intended to be used for isolating testing environments. | 
|  | TelemetryDir string | 
|  |  | 
|  | // UploadStartTime, if set, overrides the time used as the upload start time, | 
|  | // which is the time used by the upload logic to determine whether counter | 
|  | // file data should be uploaded. Only counter files that have expired before | 
|  | // the start time are considered for upload. | 
|  | // | 
|  | // This field can be used to simulate a future upload that collects recently | 
|  | // modified counters. | 
|  | UploadStartTime time.Time | 
|  |  | 
|  | // UploadURL, if set, overrides the URL used to receive uploaded reports. If | 
|  | // unset, this URL defaults to https://telemetry.go.dev/upload. | 
|  | UploadURL string | 
|  | } | 
|  |  | 
|  | // Start initializes telemetry using the specified configuration. | 
|  | // | 
|  | // Start opens the local telemetry database so that counter increment | 
|  | // operations are durably recorded in the local file system. | 
|  | // | 
|  | // If [Config.Upload] is set, and the user has opted in to telemetry | 
|  | // uploading, this process may attempt to upload approved counters | 
|  | // to telemetry.go.dev. | 
|  | // | 
|  | // If [Config.ReportCrashes] is set, any fatal crash will be | 
|  | // recorded by incrementing a counter named for the stack of the | 
|  | // first running goroutine in the traceback. | 
|  | // | 
|  | // If either of these flags is set, Start re-executes the current | 
|  | // executable as a child process, in a special mode in which it | 
|  | // acts as a telemetry sidecar for the parent process (the application). | 
|  | // In that mode, the call to Start will never return, so Start must | 
|  | // be called immediately within main, even before such things as | 
|  | // inspecting the command line. The application should avoid expensive | 
|  | // steps or external side effects in init functions, as they will | 
|  | // be executed twice (parent and child). | 
|  | // | 
|  | // Start returns a StartResult, which may be awaited via [StartResult.Wait] to | 
|  | // wait for all work done by Start to complete. | 
|  | func Start(config Config) *StartResult { | 
|  | switch v := os.Getenv(telemetryChildVar); v { | 
|  | case "": | 
|  | // The subprocess started by parent has GO_TELEMETRY_CHILD=1. | 
|  | return parent(config) | 
|  | case "1": | 
|  | child(config) // child will exit the process when it's done. | 
|  | case "2": | 
|  | // Do nothing: this was executed directly or indirectly by a child. | 
|  | default: | 
|  | log.Fatalf("unexpected value for %q: %q", telemetryChildVar, v) | 
|  | } | 
|  |  | 
|  | return &StartResult{} | 
|  | } | 
|  |  | 
|  | // MaybeChild executes the telemetry child logic if the calling program is | 
|  | // the telemetry child process, and does nothing otherwise. It is meant to be | 
|  | // called as the first thing in a program that uses telemetry.Start but cannot | 
|  | // call telemetry.Start immediately when it starts. | 
|  | func MaybeChild(config Config) { | 
|  | if v := os.Getenv(telemetryChildVar); v == "1" { | 
|  | child(config) // child will exit the process when it's done. | 
|  | } | 
|  | // other values of the telemetryChildVar environment variable | 
|  | // will be handled by telemetry.Start. | 
|  | } | 
|  |  | 
|  | // A StartResult is a handle to the result of a call to [Start]. Call | 
|  | // [StartResult.Wait] to wait for the completion of all work done on behalf of | 
|  | // Start. | 
|  | type StartResult struct { | 
|  | wg sync.WaitGroup | 
|  | } | 
|  |  | 
|  | // Wait waits for the completion of all work initiated by [Start]. | 
|  | func (res *StartResult) Wait() { | 
|  | if res == nil { | 
|  | return | 
|  | } | 
|  | res.wg.Wait() | 
|  | } | 
|  |  | 
|  | var daemonize = func(cmd *exec.Cmd) {} | 
|  |  | 
|  | // If telemetryChildVar is set to "1" in the environment, this is the telemetry | 
|  | // child. | 
|  | // | 
|  | // If telemetryChildVar is set to "2", this is a child of the child, and no | 
|  | // further forking should occur. | 
|  | const telemetryChildVar = "GO_TELEMETRY_CHILD" | 
|  |  | 
|  | // If telemetryUploadVar is set to "1" in the environment, the upload token has been | 
|  | // acquired by the parent, and the child should attempt an upload. | 
|  | const telemetryUploadVar = "GO_TELEMETRY_CHILD_UPLOAD" | 
|  |  | 
|  | func parent(config Config) *StartResult { | 
|  | if config.TelemetryDir != "" { | 
|  | telemetry.Default = telemetry.NewDir(config.TelemetryDir) | 
|  | } | 
|  | result := new(StartResult) | 
|  |  | 
|  | mode, _ := telemetry.Default.Mode() | 
|  | if mode == "off" { | 
|  | // Telemetry is turned off. Crash reporting doesn't work without telemetry | 
|  | // at least set to "local". The upload process runs in both "on" and "local" modes. | 
|  | // In local mode the upload process builds local reports but does not do the upload. | 
|  | return result | 
|  | } | 
|  |  | 
|  | counter.Open() | 
|  |  | 
|  | if _, err := os.Stat(telemetry.Default.LocalDir()); err != nil { | 
|  | // There was a problem statting LocalDir, which is needed for both | 
|  | // crash monitoring and counter uploading. Most likely, there was an | 
|  | // error creating telemetry.LocalDir in the counter.Open call above. | 
|  | // Don't start the child. | 
|  | return result | 
|  | } | 
|  |  | 
|  | childShouldUpload := config.Upload && acquireUploadToken() | 
|  | reportCrashes := config.ReportCrashes | 
|  |  | 
|  | if reportCrashes || childShouldUpload { | 
|  | startChild(reportCrashes, childShouldUpload, result) | 
|  | } | 
|  |  | 
|  | return result | 
|  | } | 
|  |  | 
|  | func startChild(reportCrashes, upload bool, result *StartResult) { | 
|  | // This process is the application (parent). | 
|  | // Fork+exec the telemetry child. | 
|  | exe, err := os.Executable() | 
|  | if err != nil { | 
|  | // There was an error getting os.Executable. It's possible | 
|  | // for this to happen on AIX if os.Args[0] is not an absolute | 
|  | // path and we can't find os.Args[0] in PATH. | 
|  | log.Printf("failed to start telemetry sidecar: os.Executable: %v", err) | 
|  | return | 
|  | } | 
|  | cmd := exec.Command(exe, "** telemetry **") // this unused arg is just for ps(1) | 
|  | daemonize(cmd) | 
|  | cmd.Env = append(os.Environ(), telemetryChildVar+"=1") | 
|  | if upload { | 
|  | cmd.Env = append(cmd.Env, telemetryUploadVar+"=1") | 
|  | } | 
|  | cmd.Dir = telemetry.Default.LocalDir() | 
|  |  | 
|  | // The child process must write to a log file, not | 
|  | // the stderr file it inherited from the parent, as | 
|  | // the child may outlive the parent but should not prolong | 
|  | // the life of any pipes created (by the grandparent) | 
|  | // to gather the output of the parent. | 
|  | // | 
|  | // By default, we discard the child process's stderr, | 
|  | // but in line with the uploader, log to a file in debug | 
|  | // only if that directory was created by the user. | 
|  | fd, err := os.Stat(telemetry.Default.DebugDir()) | 
|  | if err != nil { | 
|  | if !os.IsNotExist(err) { | 
|  | log.Printf("failed to stat debug directory: %v", err) | 
|  | return | 
|  | } | 
|  | } else if fd.IsDir() { | 
|  | // local/debug exists and is a directory. Set stderr to a log file path | 
|  | // in local/debug. | 
|  | childLogPath := filepath.Join(telemetry.Default.DebugDir(), "sidecar.log") | 
|  | childLog, err := os.OpenFile(childLogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) | 
|  | if err != nil { | 
|  | log.Printf("opening sidecar log file for child: %v", err) | 
|  | return | 
|  | } | 
|  | defer childLog.Close() | 
|  | cmd.Stderr = childLog | 
|  | } | 
|  |  | 
|  | var crashOutputFile *os.File | 
|  | if reportCrashes { | 
|  | pipe, err := cmd.StdinPipe() | 
|  | if err != nil { | 
|  | log.Printf("StdinPipe: %v", err) | 
|  | return | 
|  | } | 
|  |  | 
|  | crashOutputFile = pipe.(*os.File) // (this conversion is safe) | 
|  | } | 
|  |  | 
|  | if err := cmd.Start(); err != nil { | 
|  | // The child couldn't be started. Log the failure. | 
|  | log.Printf("can't start telemetry child process: %v", err) | 
|  | return | 
|  | } | 
|  | if reportCrashes { | 
|  | crashmonitor.Parent(crashOutputFile) | 
|  | } | 
|  | result.wg.Add(1) | 
|  | go func() { | 
|  | cmd.Wait() // Release resources if cmd happens not to outlive this process. | 
|  | result.wg.Done() | 
|  | }() | 
|  | } | 
|  |  | 
|  | func child(config Config) { | 
|  | log.SetPrefix(fmt.Sprintf("telemetry-sidecar (pid %v): ", os.Getpid())) | 
|  |  | 
|  | if config.TelemetryDir != "" { | 
|  | telemetry.Default = telemetry.NewDir(config.TelemetryDir) | 
|  | } | 
|  |  | 
|  | // golang/go#67211: be sure to set telemetryChildVar before running the | 
|  | // child, because the child itself invokes the go command to download the | 
|  | // upload config. If the telemetryChildVar variable is still set to "1", | 
|  | // that delegated go command may think that it is itself a telemetry | 
|  | // child. | 
|  | // | 
|  | // On the other hand, if telemetryChildVar were simply unset, then the | 
|  | // delegated go commands would fork themselves recursively. Short-circuit | 
|  | // this recursion. | 
|  | os.Setenv(telemetryChildVar, "2") | 
|  | upload := os.Getenv(telemetryUploadVar) == "1" | 
|  |  | 
|  | // The crashmonitor and/or upload process may themselves record counters. | 
|  | counter.Open() | 
|  |  | 
|  | // Start crashmonitoring and uploading depending on what's requested | 
|  | // and wait for the longer running child to complete before exiting: | 
|  | // if we collected a crash before the upload finished, wait for the | 
|  | // upload to finish before exiting | 
|  | var g errgroup.Group | 
|  |  | 
|  | if config.ReportCrashes { | 
|  | g.Go(func() error { | 
|  | crashmonitor.Child() | 
|  | return nil | 
|  | }) | 
|  | } | 
|  | if upload { | 
|  | g.Go(func() error { | 
|  | uploaderChild(config.UploadStartTime, config.UploadURL) | 
|  | return nil | 
|  | }) | 
|  | } | 
|  | g.Wait() | 
|  |  | 
|  | os.Exit(0) | 
|  | } | 
|  |  | 
|  | func uploaderChild(asof time.Time, uploadURL string) { | 
|  | if err := upload.Run(upload.RunConfig{ | 
|  | UploadURL: uploadURL, | 
|  | LogWriter: os.Stderr, | 
|  | StartTime: asof, | 
|  | }); err != nil { | 
|  | log.Printf("upload failed: %v", err) | 
|  | } | 
|  | } | 
|  |  | 
|  | // acquireUploadToken acquires a token permitting the caller to upload. | 
|  | // To limit the frequency of uploads, only one token is issue per | 
|  | // machine per time period. | 
|  | // The boolean indicates whether the token was acquired. | 
|  | func acquireUploadToken() bool { | 
|  | if telemetry.Default.LocalDir() == "" { | 
|  | // The telemetry dir wasn't initialized properly, probably because | 
|  | // os.UserConfigDir did not complete successfully. In that case | 
|  | // there are no counters to upload, so we should just do nothing. | 
|  | return false | 
|  | } | 
|  | tokenfile := filepath.Join(telemetry.Default.LocalDir(), "upload.token") | 
|  | const period = 24 * time.Hour | 
|  |  | 
|  | // A process acquires a token by successfully creating a | 
|  | // well-known file. If the file already exists and has an | 
|  | // mtime age less then than the period, the process does | 
|  | // not acquire the token. If the file is older than the | 
|  | // period, the process is allowed to remove the file and | 
|  | // try to re-create it. | 
|  | fi, err := os.Stat(tokenfile) | 
|  | if err == nil { | 
|  | if time.Since(fi.ModTime()) < period { | 
|  | return false | 
|  | } | 
|  | // There's a possible race here where two processes check the | 
|  | // token file and see that it's older than the period, then the | 
|  | // first one removes it and creates another, and then a second one | 
|  | // removes the newly created file and creates yet another | 
|  | // file. Then both processes would act as though they had the token. | 
|  | // This is very rare, but it's also okay because we're only grabbing | 
|  | // the token to do rate limiting, not for correctness. | 
|  | _ = os.Remove(tokenfile) | 
|  | } else if !os.IsNotExist(err) { | 
|  | log.Printf("error acquiring upload taken: statting token file: %v", err) | 
|  | return false | 
|  | } | 
|  |  | 
|  | f, err := os.OpenFile(tokenfile, os.O_CREATE|os.O_EXCL, 0666) | 
|  | if err != nil { | 
|  | if os.IsExist(err) { | 
|  | return false | 
|  | } | 
|  | log.Printf("error acquiring upload token: creating token file: %v", err) | 
|  | return false | 
|  | } | 
|  | _ = f.Close() | 
|  | return true | 
|  | } |