Source file src/cmd/go/internal/doc/pkgsite.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build !cmd_go_bootstrap
     6  
     7  package doc
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"net"
    13  	"net/url"
    14  	"os"
    15  	"os/exec"
    16  	"os/signal"
    17  	"path/filepath"
    18  	"strings"
    19  )
    20  
    21  // pickUnusedPort finds an unused port by trying to listen on port 0
    22  // and letting the OS pick a port, then closing that connection and
    23  // returning that port number.
    24  // This is inherently racy.
    25  func pickUnusedPort() (int, error) {
    26  	l, err := net.Listen("tcp", "localhost:0")
    27  	if err != nil {
    28  		return 0, err
    29  	}
    30  	port := l.Addr().(*net.TCPAddr).Port
    31  	if err := l.Close(); err != nil {
    32  		return 0, err
    33  	}
    34  	return port, nil
    35  }
    36  
    37  func doPkgsite(urlPath string) error {
    38  	port, err := pickUnusedPort()
    39  	if err != nil {
    40  		return fmt.Errorf("failed to find port for documentation server: %v", err)
    41  	}
    42  	addr := fmt.Sprintf("localhost:%d", port)
    43  	path, err := url.JoinPath("http://"+addr, urlPath)
    44  	if err != nil {
    45  		return fmt.Errorf("internal error: failed to construct url: %v", err)
    46  	}
    47  
    48  	// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
    49  	// and instead wait for the child process to handle the signal and
    50  	// exit before exiting ourselves.
    51  	signal.Ignore(signalsToIgnore...)
    52  
    53  	// Prepend the local download cache to GOPROXY to get around deprecation checks.
    54  	env := os.Environ()
    55  	vars, err := runCmd(env, goCmd(), "env", "GOPROXY", "GOMODCACHE")
    56  	fields := strings.Fields(vars)
    57  	if err == nil && len(fields) == 2 {
    58  		goproxy, gomodcache := fields[0], fields[1]
    59  		gomodcache = filepath.Join(gomodcache, "cache", "download")
    60  		// Convert absolute path to file URL. pkgsite will not accept
    61  		// Windows absolute paths because they look like a host:path remote.
    62  		// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
    63  		if strings.HasPrefix(gomodcache, "/") {
    64  			gomodcache = "file://" + gomodcache
    65  		} else {
    66  			gomodcache = "file:///" + filepath.ToSlash(gomodcache)
    67  		}
    68  		env = append(env, "GOPROXY="+gomodcache+","+goproxy)
    69  	}
    70  
    71  	const version = "v0.0.0-20250714212547-01b046e81fe7"
    72  	cmd := exec.Command(goCmd(), "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version,
    73  		"-gorepo", buildCtx.GOROOT,
    74  		"-http", addr,
    75  		"-open", path)
    76  	cmd.Env = env
    77  	cmd.Stdout = os.Stderr
    78  	cmd.Stderr = os.Stderr
    79  
    80  	if err := cmd.Run(); err != nil {
    81  		var ee *exec.ExitError
    82  		if errors.As(err, &ee) {
    83  			// Exit with the same exit status as pkgsite to avoid
    84  			// printing of "exit status" error messages.
    85  			// Any relevant messages have already been printed
    86  			// to stdout or stderr.
    87  			os.Exit(ee.ExitCode())
    88  		}
    89  		return err
    90  	}
    91  
    92  	return nil
    93  }
    94  

View as plain text