telemetry: add support for uploading

No support for rate limiting yet but want to keep things small here.
This change adds a dependency on x/sync for errgroup.

I've also had to run go mod tidy in the godev module because it has a
replacement instead of depending on a version of this module.

For golang/go#65500

Change-Id: Icbcd8d3d58fb443c14cf21e339baa3edb76fbca1
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/564615
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/gotelemetry/main.go b/cmd/gotelemetry/main.go
index 58d5def..6538d97 100644
--- a/cmd/gotelemetry/main.go
+++ b/cmd/gotelemetry/main.go
@@ -341,7 +341,7 @@
 
 func runUpload(_ []string) {
 	upload.Run(&upload.Control{
-		Logging: os.Stderr,
+		Logger: os.Stderr,
 	})
 }
 
diff --git a/go.mod b/go.mod
index 74d5da9..3b9d932 100644
--- a/go.mod
+++ b/go.mod
@@ -4,4 +4,7 @@
 
 require golang.org/x/mod v0.15.0
 
-require golang.org/x/sys v0.17.0
+require (
+	golang.org/x/sync v0.6.0
+	golang.org/x/sys v0.17.0
+)
diff --git a/go.sum b/go.sum
index b89e429..e33729a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
 golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/godev/go.mod b/godev/go.mod
index 5d55415..cd93047 100644
--- a/godev/go.mod
+++ b/godev/go.mod
@@ -30,7 +30,7 @@
 	golang.org/x/crypto v0.14.0 // indirect
 	golang.org/x/net v0.17.0 // indirect
 	golang.org/x/oauth2 v0.13.0 // indirect
-	golang.org/x/sync v0.4.0 // indirect
+	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/godev/go.sum b/godev/go.sum
index 246be43..0ad42e0 100644
--- a/godev/go.sum
+++ b/godev/go.sum
@@ -106,8 +106,8 @@
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/start.go b/start.go
index 0a05933..01c0dfd 100644
--- a/start.go
+++ b/start.go
@@ -10,8 +10,10 @@
 	"os"
 	"os/exec"
 
+	"golang.org/x/sync/errgroup"
 	"golang.org/x/telemetry/counter"
 	"golang.org/x/telemetry/internal/crashmonitor"
+	"golang.org/x/telemetry/upload"
 )
 
 // Config controls the behavior of [Start].
@@ -47,11 +49,15 @@
 //
 // 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. [But: this option doesn't appear to exist yet.]
+// to telemetry.go.dev.
 //
-// If [Config.ReportCrashes] is set, Start re-executes the current
-// executable as a child process, in a special mode in which it acts
-// as a crash monitor for the parent process (the application).
+// 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
@@ -59,23 +65,21 @@
 // be executed twice (parent and child).
 func Start(config Config) {
 	counter.Open()
-	if !config.ReportCrashes || !crashmonitor.Supported() {
-		// TODO(matloob): Once support for uploading is added to start,
-		// we'll just start the uploader instead of returning here.
-		return
-	}
-	if os.Getenv(telemetryChildVar) != "" {
-		child(config.Logger)
-		panic("unreachable")
-	}
 
-	parent(config.Logger)
+	if (config.ReportCrashes && crashmonitor.Supported()) || config.Upload {
+		if os.Getenv(telemetryChildVar) != "" {
+			child(config)
+			panic("unreachable")
+		}
+
+		parent(config)
+	}
 }
 
 const telemetryChildVar = "X_TELEMETRY_CHILD"
 
-func parent(logw io.Writer) {
-	logger := log.New(logw, "", 0)
+func parent(config Config) {
+	logger := log.New(config.Logger, "", 0)
 	// This process is the application (parent).
 	// Fork+exec the telemetry child.
 	exe, err := os.Executable()
@@ -86,19 +90,44 @@
 	cmd.Env = append(os.Environ(), telemetryChildVar+"=1")
 	cmd.Stderr = os.Stderr
 	cmd.Stdout = os.Stderr
-	pipe, err := cmd.StdinPipe()
-	if err != nil {
-		logger.Fatalf("StdinPipe: %v", err)
-	}
 
-	crashmonitor.Parent(pipe.(*os.File)) // (this conversion is safe)
+	if config.ReportCrashes {
+		pipe, err := cmd.StdinPipe()
+		if err != nil {
+			logger.Fatalf("StdinPipe: %v", err)
+		}
+
+		crashmonitor.Parent(pipe.(*os.File)) // (this conversion is safe)
+	}
 
 	if err := cmd.Start(); err != nil {
 		logger.Fatalf("can't start telemetry child process: %v", err)
 	}
 }
 
-func child(logw io.Writer) {
-	// TODO(matloob): add support for uploading.
-	crashmonitor.Child(logw)
+func child(config Config) {
+	// 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.Upload {
+		g.Go(func() error {
+			uploaderChild(config.Logger)
+			return nil
+		})
+	}
+	if config.ReportCrashes {
+		g.Go(func() error {
+			crashmonitor.Child(config.Logger)
+			return nil
+		})
+	}
+	g.Wait()
+}
+
+func uploaderChild(logger io.Writer) {
+	// TODO(matloob): Do rate-limiting here.
+	upload.Run(&upload.Control{Logger: logger})
 }
diff --git a/upload/upload.go b/upload/upload.go
index 0a2b13a..122b725 100644
--- a/upload/upload.go
+++ b/upload/upload.go
@@ -14,8 +14,8 @@
 // Run generates and uploads reports, as allowed by the mode file.
 // A nil Control is legal.
 func Run(c *Control) {
-	if c != nil && c.Logging != nil {
-		upload.SetLogOutput(c.Logging)
+	if c != nil && c.Logger != nil {
+		upload.SetLogOutput(c.Logger)
 	}
 	// ignore error: failed logging should not block uploads
 	upload.LogIfDebug("")
@@ -32,7 +32,7 @@
 // reporting and uploading choices.
 // Future versions may also allow the user to set the upload URL.
 type Control struct {
-	// Logging provides a io.Writer for error messages during uploading
+	// Logger provides a io.Writer for error messages during uploading
 	// nil is legal and no log messages get generated
-	Logging io.Writer
+	Logger io.Writer
 }