gopls/internal/cmd: use localhost:0 to avoid port race

Previously, TestMCPCommandHTTP executed in the following order:
- grab a random port using net.Listen("tcp", "localhost:0")
- record the port number
- close the listener socket
- launch gopls with -listen=localhost:<port>

A race condition could occur between closing the listener and gopls
starting up, allowing another process to claim the port and causing
gopls to fail with "address already in use".

This CL shifts port selection from the test function to gopls by
passing -listen=localhost:0 to gopls. Since net.Listen guarantees
atomic port allocation, gopls selects and binds the port directly,
and the test reads the allocated listening address from gopls stderr
logs.

Fixes golang/go#79983

Change-Id: Ic5be1881c74d8722a9661773984ad4e0ac0bf352
Reviewed-on: https://go-review.googlesource.com/c/tools/+/802761
Reviewed-by: Madeline Kalil <mkalil@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/internal/cmd/mcp_test.go b/gopls/internal/cmd/mcp_test.go
index bfb9c86..6071804 100644
--- a/gopls/internal/cmd/mcp_test.go
+++ b/gopls/internal/cmd/mcp_test.go
@@ -10,13 +10,11 @@
 	"context"
 	"encoding/json"
 	"fmt"
-	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"runtime"
 	"slices"
-	"strconv"
 	"strings"
 	"testing"
 	"time"
@@ -205,9 +203,7 @@
 
 func MyFun() {}
 `)
-	port := strconv.Itoa(getRandomPort())
-	addr := "localhost:" + port
-	goplsCmd := exec.Command(os.Args[0], "-v", "mcp", "-listen="+addr)
+	goplsCmd := exec.Command(os.Args[0], "-v", "mcp", "-listen=localhost:0")
 	goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain")
 	goplsCmd.Dir = tree
 	goplsCmd.Stdout = os.Stderr
@@ -232,19 +228,19 @@
 		goplsCmd.Wait()
 	}()
 
-	// Wait for the MCP server to start listening. The referenced log occurs
-	// after the connection is opened via net.Listen and the HTTP handlers are
-	// set up.
-	ready := make(chan bool, 1)
+	// Wait for the MCP server to start listening and send the address through
+	// the channel. The referenced log occurs after the connection is opened via
+	// net.Listen and the HTTP handlers are set up.
+	ready := make(chan string, 1)
 	go func() {
 		defer close(ready)
-		// Copy from the pipe to stderr, keeping an eye out for the "mcp http
-		// server listening" string.
+		// Copy from the pipe to stderr, keeping an eye out for the "Gopls MCP
+		// server: listening on <addr>" string.
 		scan := bufio.NewScanner(stderr)
 		for scan.Scan() {
 			line := scan.Text()
-			if strings.Contains(line, "mcp http server listening") {
-				ready <- true
+			if _, after, ok := strings.Cut(line, "Gopls MCP server: listening on "); ok {
+				ready <- strings.TrimSpace(after)
 			}
 			fmt.Fprintln(os.Stderr, line)
 		}
@@ -253,9 +249,10 @@
 		}
 	}()
 
+	var addr string
 	select {
-	case ok := <-ready:
-		if !ok {
+	case addr = <-ready:
+		if addr == "" {
 			t.Fatalf("gopls mcp server exited without starting")
 		}
 	case <-time.After(60 * time.Second):
@@ -430,18 +427,6 @@
 	return buf.String()
 }
 
-// getRandomPort returns the number of a random available port. Inherently racy:
-// nothing stops another process from listening on it - but this should be fine
-// for testing purposes.
-func getRandomPort() int {
-	listener, err := net.Listen("tcp", "localhost:0")
-	if err != nil {
-		panic(err)
-	}
-	defer listener.Close()
-	return listener.Addr().(*net.TCPAddr).Port
-}
-
 // supportsFsnotify returns true if fsnotify supports the os.
 func supportsFsnotify(os string) bool {
 	return os == "darwin" || os == "linux" || os == "windows"
diff --git a/gopls/internal/mcp/mcp.go b/gopls/internal/mcp/mcp.go
index a50b29f..e00e92c 100644
--- a/gopls/internal/mcp/mcp.go
+++ b/gopls/internal/mcp/mcp.go
@@ -56,18 +56,22 @@
 	if strings.HasPrefix(address, ":") {
 		return fmt.Errorf("address %s implicitly binds all network interfaces; please use an explicit host such as 0.0.0.0 (all interfaces) or localhost (safer)", address)
 	}
-	log.Printf("Gopls MCP server: starting up on http")
+
 	listener, err := net.Listen("tcp", address)
 	if err != nil {
 		return err
 	}
 	defer listener.Close()
 
-	// TODO(hxjiang): expose the MCP server address to the LSP client.
-	if isDaemon {
-		log.Printf("Gopls MCP daemon: listening on address %s...", listener.Addr())
+	{
+		kind := "server"
+		if isDaemon {
+			kind = "daemon"
+		}
+
+		log.Printf("Gopls MCP %s: listening on %s", kind, listener.Addr())
+		defer log.Printf("Gopls MCP %s: exiting", kind)
 	}
-	defer log.Printf("Gopls MCP server: exiting")
 
 	svr := http.Server{
 		Handler: HTTPHandler(sessions, isDaemon, rootsHandler),
@@ -81,7 +85,6 @@
 		<-ctx.Done()
 		svr.Close() // ignore error
 	}()
-	log.Printf("mcp http server listening")
 	return svr.Serve(listener)
 }