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  	"cmd/go/internal/cfg"
    21  )
    22  
    23  // pickUnusedPort finds an unused port by trying to listen on port 0
    24  // and letting the OS pick a port, then closing that connection and
    25  // returning that port number.
    26  // This is inherently racy.
    27  func pickUnusedPort() (int, error) {
    28  	l, err := net.Listen("tcp", "localhost:0")
    29  	if err != nil {
    30  		return 0, err
    31  	}
    32  	port := l.Addr().(*net.TCPAddr).Port
    33  	if err := l.Close(); err != nil {
    34  		return 0, err
    35  	}
    36  	return port, nil
    37  }
    38  
    39  func doPkgsite(urlPath, fragment string) error {
    40  	port, err := pickUnusedPort()
    41  	if err != nil {
    42  		return fmt.Errorf("failed to find port for documentation server: %v", err)
    43  	}
    44  	addr := fmt.Sprintf("localhost:%d", port)
    45  	path, err := url.JoinPath("http://"+addr, urlPath)
    46  	if err != nil {
    47  		return fmt.Errorf("internal error: failed to construct url: %v", err)
    48  	}
    49  	if fragment != "" {
    50  		path += "#" + fragment
    51  	}
    52  
    53  	if file := os.Getenv("TEST_GODOC_URL_FILE"); file != "" {
    54  		return os.WriteFile(file, []byte(path+"\n"), 0666)
    55  	}
    56  
    57  	// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
    58  	// and instead wait for the child process to handle the signal and
    59  	// exit before exiting ourselves.
    60  	signal.Ignore(signalsToIgnore...)
    61  
    62  	// Prepend the local download cache to GOPROXY to get around deprecation checks.
    63  	env := os.Environ()
    64  	vars, err := runCmd(env, goCmd(), "env", "GOPROXY", "GOMODCACHE")
    65  	fields := strings.Fields(vars)
    66  	if err == nil && len(fields) == 2 {
    67  		goproxy, gomodcache := fields[0], fields[1]
    68  		gomodcache = filepath.Join(gomodcache, "cache", "download")
    69  		// Convert absolute path to file URL. pkgsite will not accept
    70  		// Windows absolute paths because they look like a host:path remote.
    71  		// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
    72  		if strings.HasPrefix(gomodcache, "/") {
    73  			gomodcache = "file://" + gomodcache
    74  		} else {
    75  			gomodcache = "file:///" + filepath.ToSlash(gomodcache)
    76  		}
    77  		env = append(env, "GOPROXY="+gomodcache+","+goproxy)
    78  	}
    79  
    80  	const version = "v0.0.0-20251223195805-1a3bd3c788fe"
    81  	cmd := exec.Command(goCmd(), "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version,
    82  		"-gorepo", cfg.GOROOT,
    83  		"-http", addr,
    84  		"-open", path)
    85  	cmd.Env = env
    86  	cmd.Stdout = os.Stderr
    87  	cmd.Stderr = os.Stderr
    88  
    89  	if err := cmd.Run(); err != nil {
    90  		if ee, ok := errors.AsType[*exec.ExitError](err); ok {
    91  			// Exit with the same exit status as pkgsite to avoid
    92  			// printing of "exit status" error messages.
    93  			// Any relevant messages have already been printed
    94  			// to stdout or stderr.
    95  			os.Exit(ee.ExitCode())
    96  		}
    97  		return err
    98  	}
    99  
   100  	return nil
   101  }
   102  

View as plain text