// write the uploadable file
var errUpload, errLocal error
if uploadOK {
- errUpload = os.WriteFile(uploadFileName, uploadContents, 0644)
+ _, errUpload = exclusiveWrite(uploadFileName, uploadContents)
}
// write the local file
- errLocal = os.WriteFile(localFileName, localContents, 0644)
+ _, errLocal = exclusiveWrite(localFileName, localContents)
/* Wrote the files */
// even though these errors won't occur, what should happen
return "", nil
}
+// exclusiveWrite attempts to create filename exclusively, and if successful,
+// writes content to the resulting file handle.
+//
+// It returns a boolean indicating whether the exclusive handle was acquired,
+// and an error indicating whether the operation succeeded.
+// If the file already exists, exclusiveWrite returns (false, nil).
+func exclusiveWrite(filename string, content []byte) (_ bool, rerr error) {
+ f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+ if err != nil {
+ if os.IsExist(err) {
+ return false, nil
+ }
+ return false, err
+ }
+ defer func() {
+ if err := f.Close(); err != nil && rerr == nil {
+ rerr = err
+ }
+ }()
+ if _, err := f.Write(content); err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
// return an existing ProgremReport, or create anew
func findProgReport(meta map[string]string, report *telemetry.Report) *telemetry.ProgramReport {
for _, prog := range report.Programs {
// try to upload the report, 'true' if successful
func (u *uploader) uploadReportContents(fname string, buf []byte) bool {
- b := bytes.NewReader(buf)
fdate := strings.TrimSuffix(filepath.Base(fname), ".json")
fdate = fdate[len(fdate)-len("2006-01-02"):]
- endpoint := u.uploadServerURL + "/" + fdate
+ newname := filepath.Join(u.dir.UploadDir(), fdate+".json")
+ if _, err := os.Stat(newname); err == nil {
+ // Another process uploaded but failed to clean up (or hasn't yet cleaned
+ // up). Ensure that cleanup occurs.
+ _ = os.Remove(fname)
+ return false
+ }
+
+ // Lock the upload, to prevent duplicate uploads.
+ {
+ lockname := newname + ".lock"
+ lockfile, err := os.OpenFile(lockname, os.O_CREATE|os.O_EXCL, 0666)
+ if err != nil {
+ u.logger.Printf("Failed to acquire lock %s: %v", lockname, err)
+ return false
+ }
+ _ = lockfile.Close()
+ defer os.Remove(lockname)
+ }
+
+ endpoint := u.uploadServerURL + "/" + fdate
+ b := bytes.NewReader(buf)
resp, err := http.Post(endpoint, "application/json", b)
if err != nil {
u.logger.Printf("Error upload %s to %s: %v", filepath.Base(fname), endpoint, err)
return false
}
// Store a copy of the uploaded report in the uploaded directory.
- newname := filepath.Join(u.dir.UploadDir(), fdate+".json")
if err := os.WriteFile(newname, buf, 0644); err == nil {
os.Remove(fname) // if it exists
}
mode, _ := telemetry.Default.Mode()
if mode == "off" {
// Telemetry is turned off. Crash reporting doesn't work without telemetry
- // at least set to "local", and the uploader isn't started in uploaderChild if
- // mode is "off"
+ // 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
}
return result
}
- // Crash monitoring and uploading both require a sidecar process.
- var (
- reportCrashes = config.ReportCrashes && crashmonitor.Supported()
- upload = config.Upload && mode != "off"
- )
- if reportCrashes || upload {
- switch v := os.Getenv(telemetryChildVar); v {
- case "":
- // The subprocess started by parent has X_TELEMETRY_CHILD=1.
- parent(reportCrashes, result)
- case "1":
- // 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")
- child(reportCrashes, upload, config.UploadStartTime, config.UploadURL)
- os.Exit(0)
- case "2":
- // Do nothing: see note above.
- default:
- log.Fatalf("unexpected value for %q: %q", telemetryChildVar, v)
+ var reportCrashes = config.ReportCrashes && crashmonitor.Supported()
+
+ switch v := os.Getenv(telemetryChildVar); v {
+ case "":
+ // The subprocess started by parent has GO_TELEMETRY_CHILD=1.
+ childShouldUpload := config.Upload && acquireUploadToken()
+ if reportCrashes || childShouldUpload {
+ parent(reportCrashes, childShouldUpload, result)
}
+ case "1":
+ // 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"
+ child(reportCrashes, upload, config.UploadStartTime, config.UploadURL)
+ os.Exit(0)
+ case "2":
+ // Do nothing: see note above.
+ default:
+ log.Fatalf("unexpected value for %q: %q", telemetryChildVar, v)
}
+
return result
}
//
// If telemetryChildVar is set to "2", this is a child of the child, and no
// further forking should occur.
-const telemetryChildVar = "X_TELEMETRY_CHILD"
+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(reportCrashes bool, result *StartResult) {
+func parent(reportCrashes, upload bool, result *StartResult) {
// This process is the application (parent).
// Fork+exec the telemetry child.
exe, err := os.Executable()
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
}
func uploaderChild(asof time.Time, uploadURL string) {
- if mode, _ := telemetry.Default.Mode(); mode == "off" {
- // There's no work to be done if telemetry is turned off.
- return
- }
- 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
- }
- tokenfilepath := filepath.Join(telemetry.Default.LocalDir(), "upload.token")
- ok, err := acquireUploadToken(tokenfilepath)
- if err != nil {
- log.Printf("error acquiring upload token: %v", err)
- return
- } else if !ok {
- // It hasn't been a day since the last upload.Run attempt or there's
- // a concurrently running uploader.
- return
- }
if err := upload.Run(upload.RunConfig{
UploadURL: uploadURL,
LogWriter: os.Stderr,
// 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(tokenfile string) (bool, error) {
+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
fi, err := os.Stat(tokenfile)
if err == nil {
if time.Since(fi.ModTime()) < period {
- return false, nil
+ 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
// the token to do rate limiting, not for correctness.
_ = os.Remove(tokenfile)
} else if !os.IsNotExist(err) {
- return false, fmt.Errorf("statting token file: %v", 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, nil
+ return false
}
- return false, fmt.Errorf("creating token file: %v", err)
+ log.Printf("error acquiring upload token: creating token file: %v", err)
+ return false
}
_ = f.Close()
- return true, nil
+ return true
}