telemetry: add MaybeChild, which executes child logic if applicable

In the Go command, we can't call telemetry.Start as the first thing when
the Go command starts because we can't do significant work before
doing toolchain switching. On the other hand, we do want to run the
child logic if the Go command is running as the telemetry child as the
first thing before doing toolchain selection: the parent has already
been started using the switched toolchain.

Add a telemetry.MaybeChild function that executes the child code if the
go command is running as the child that will be called immediately when
the go command starts. Start will continue to check if it's the child
for now to avoid breaking current usages of it but we might want to
remove that code from Start in the future to make things a bit simpler.

The initialization of the parent/child process that was done by start
has now been moved into parent and child. There is limited duplication
(for example, initializing the telemetry directory) but much of the work
is different between them. For instance, we don't need to check the mode
in the child function because it was already checked by the parent
before starting the child.

One unfortunate consequence of doing this is that the manipulation and
checking of the GO_TELEMETRY_CHILD environment variable now happens in
four places (EntryPoint, Start, parent, and startChild) rather than two
previously (Start and parent)

Change-Id: I3d31e44ee3a141b08ffd87f037f1a03f0513c1cc
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/592017
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/start.go b/start.go
index 76fa9b6..4b37a5c 100644
--- a/start.go
+++ b/start.go
@@ -86,59 +86,31 @@
 // 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 {
-	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
-	}
-
-	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)
-		}
+		return parent(config)
 	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)
+		child(config) // child will exit the process when it's done.
 	case "2":
-		// Do nothing: see note above.
+		// Do nothing: this was executed directly or indirectly by a child.
 	default:
 		log.Fatalf("unexpected value for %q: %q", telemetryChildVar, v)
 	}
 
-	return result
+	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
@@ -169,7 +141,41 @@
 // acquired by the parent, and the child should attempt an upload.
 const telemetryUploadVar = "GO_TELEMETRY_CHILD_UPLOAD"
 
-func parent(reportCrashes, upload bool, result *StartResult) {
+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 && crashmonitor.Supported()
+
+	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()
@@ -233,9 +239,29 @@
 	}()
 }
 
-func child(reportCrashes, upload bool, uploadStartTime time.Time, uploadURL string) {
+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"
+
+	reportCrashes := config.ReportCrashes && crashmonitor.Supported()
+	uploadStartTime := config.UploadStartTime
+	uploadURL := config.UploadURL
+
 	// 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
@@ -255,6 +281,8 @@
 		})
 	}
 	g.Wait()
+
+	os.Exit(0)
 }
 
 func uploaderChild(asof time.Time, uploadURL string) {