windows/svc: make IsWindowsService handle parent exit

IsWindowsService fails when parent process has exited.

This change makes IsWindowsService return false instead.

Added a test to verify the change.

Change-Id: Ibb2aa9478e8afd9e35011bbdc0985bdf8f0af9cc
Reviewed-on: https://go-review.googlesource.com/c/sys/+/294729
TryBot-Result: Go Bot <gobot@golang.org>
Run-TryBot: Alex Brainman <alex.brainman@gmail.com>
Reviewed-by: Jason A. Donenfeld <Jason@zx2c4.com>
Trust: Jason A. Donenfeld <Jason@zx2c4.com>
Trust: Alex Brainman <alex.brainman@gmail.com>
diff --git a/windows/svc/security.go b/windows/svc/security.go
index da6df1d..8cc6778 100644
--- a/windows/svc/security.go
+++ b/windows/svc/security.go
@@ -96,6 +96,13 @@
 	var psid uint32
 	err := windows.ProcessIdToSessionId(uint32(pbi[5]), &psid)
 	if err != nil {
+		if err == windows.ERROR_INVALID_PARAMETER {
+			// This error happens when Windows cannot find process parent.
+			// Perhaps process parent exited.
+			// Assume we are not running in a service, because service
+			// parent process (services.exe) cannot exit.
+			return false, nil
+		}
 		return false, err
 	}
 	if psid != 0 {
diff --git a/windows/svc/svc_test.go b/windows/svc/svc_test.go
index 5bf123d..a6c8bbc 100644
--- a/windows/svc/svc_test.go
+++ b/windows/svc/svc_test.go
@@ -171,3 +171,74 @@
 		t.Error("IsWindowsService retuns true when not running in a service.")
 	}
 }
+
+func TestIsWindowsServiceWhenParentExits(t *testing.T) {
+	if os.Getenv("GO_WANT_HELPER_PROCESS") == "parent" {
+		// in parent process
+
+		// Start the child and exit quickly.
+		child := exec.Command(os.Args[0], "-test.run=TestIsWindowsServiceWhenParentExits")
+		child.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=child")
+		err := child.Start()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, fmt.Sprintf("child start failed: %v", err))
+			os.Exit(1)
+		}
+		os.Exit(0)
+	}
+
+	if os.Getenv("GO_WANT_HELPER_PROCESS") == "child" {
+		// in child process
+		dumpPath := os.Getenv("GO_WANT_HELPER_PROCESS_FILE")
+		if dumpPath == "" {
+			// We cannot report this error. But main test will notice
+			// that we did not create dump file.
+			os.Exit(1)
+		}
+		var msg string
+		isSvc, err := svc.IsWindowsService()
+		if err != nil {
+			msg = err.Error()
+		}
+		if isSvc {
+			msg = "IsWindowsService retuns true when not running in a service."
+		}
+		err = os.WriteFile(dumpPath, []byte(msg), 0644)
+		if err != nil {
+			// We cannot report this error. But main test will notice
+			// that we did not create dump file.
+			os.Exit(2)
+		}
+		os.Exit(0)
+	}
+
+	// Run in a loop until it fails.
+	for i := 0; i < 10; i++ {
+		childDumpPath := filepath.Join(t.TempDir(), "issvc.txt")
+
+		parent := exec.Command(os.Args[0], "-test.run=TestIsWindowsServiceWhenParentExits")
+		parent.Env = append(os.Environ(),
+			"GO_WANT_HELPER_PROCESS=parent",
+			"GO_WANT_HELPER_PROCESS_FILE="+childDumpPath)
+		parentOutput, err := parent.CombinedOutput()
+		if err != nil {
+			t.Errorf("parent failed: %v: %v", err, string(parentOutput))
+		}
+		for i := 0; ; i++ {
+			if _, err := os.Stat(childDumpPath); err == nil {
+				break
+			}
+			time.Sleep(100 * time.Millisecond)
+			if i > 10 {
+				t.Fatal("timed out waiting for child ouput file to be created.")
+			}
+		}
+		childOutput, err := ioutil.ReadFile(childDumpPath)
+		if err != nil {
+			t.Fatalf("reading child ouput failed: %v", err)
+		}
+		if got, want := string(childOutput), ""; got != want {
+			t.Fatalf("child output: want %q, got %q", want, got)
+		}
+	}
+}