github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/internal/debugger/debugger.go (about)

     1  // Copyright 2021 The ChromiumOS Authors
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  // Package debugger provides the ability to start binaries under a debugger.
     6  package debugger
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net"
    14  	"os/exec"
    15  	"regexp"
    16  	"runtime"
    17  	"strconv"
    18  	"strings"
    19  
    20  	"golang.org/x/sys/unix"
    21  
    22  	"go.chromium.org/tast/core/errors"
    23  	"go.chromium.org/tast/core/internal/logging"
    24  	"go.chromium.org/tast/core/ssh"
    25  )
    26  
    27  // A DebugTarget represents a go binary that can be debugged.
    28  type DebugTarget string
    29  
    30  // Valid DebugTargets are listed below.
    31  const (
    32  	LocalBundle      DebugTarget = "local"
    33  	RemoteBundle     DebugTarget = "remote"
    34  	LocalTestRunner  DebugTarget = "local-test-runner"
    35  	RemoteTestRunner DebugTarget = "remote-test-runner"
    36  )
    37  
    38  // DlvDUTEnv is the environment variables required to run dlv on DUTs.
    39  // Setting XDG_CONFIG_HOME to the stateful partition is required to stop it
    40  // writing to ~/.config/dlv, which is in a read-only partition.
    41  var DlvDUTEnv = []string{"XDG_CONFIG_HOME=/mnt/stateful_partition/xdg_config"}
    42  
    43  // DlvHostEnv is the environment variables required to run dlv on a host machine.
    44  var DlvHostEnv = []string{}
    45  
    46  // IsRunningOnDUT returns whether the current process is running on the DUT.
    47  func IsRunningOnDUT() bool {
    48  	// We want to change the error messages based on whether this is running on the DUT or the host.
    49  	// We can't necessarily know in the caller because it doesn't distinguish between remote bundles and local bundles.
    50  	lsb, err := ioutil.ReadFile("/etc/lsb-release")
    51  	return err == nil && strings.Contains(string(lsb), "CHROMEOS_RELEASE_BOARD")
    52  }
    53  
    54  // FindPreemptiveDebuggerErrors pre-emptively checks potential errors, to ensure better error messages for users.
    55  func FindPreemptiveDebuggerErrors(port int, remoteCommand bool) error {
    56  	isDUT := IsRunningOnDUT()
    57  
    58  	if _, err := exec.LookPath("dlv"); err != nil {
    59  		if runtime.GOARCH == "arm" {
    60  			return errors.New("delve isn't supported for arm32 (https://github.com/go-delve/delve/issues/2051). If possible, try installing a 64 bit OS onto your dut (eg. hana64 instead of hana)")
    61  		} else if isDUT {
    62  			return errors.New(`dlv doesn't exist on your DUT. To install on supported architectures (x86, amd64, arm64), run "emerge-<board> dev-go/delve" and then cros deploy it`)
    63  		} else {
    64  			return errors.New(`dlv doesn't exist on your host machine. To install, run "sudo emerge dev-go/delve"`)
    65  		}
    66  	}
    67  
    68  	// The host machine needs to set up a port forward, so the port *should* be in use.
    69  	if !isDUT && remoteCommand {
    70  		return nil
    71  	}
    72  
    73  	machine := "host"
    74  	if isDUT {
    75  		machine = "DUT"
    76  	}
    77  
    78  	// If there is no debugger, then we'll return a pid of -1.
    79  	getCurrentDebugger := func() (pid int, err error) {
    80  		cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port))
    81  		out, err := cmd.Output()
    82  		// Status code 1 indicates no process found.
    83  		if err != nil {
    84  			return -1, nil
    85  		}
    86  		// The start of the line is process name, then PID.
    87  		match := regexp.MustCompile(`(?m)^([^\s]+)\s*([0-9]+)\b`).FindStringSubmatch(string(out))
    88  
    89  		pname := match[1]
    90  		pid, err = strconv.Atoi(match[2])
    91  		if err != nil {
    92  			return 0, err
    93  		}
    94  		if pname != "dlv" {
    95  			return 0, errors.Errorf("port %d in use by process %s with pid %d on %s machine", port, pname, pid, machine)
    96  		}
    97  		return pid, err
    98  	}
    99  
   100  	pid, err := getCurrentDebugger()
   101  	if err != nil || pid == -1 {
   102  		return err
   103  	}
   104  
   105  	// When you control-c an ongoing test, the test continues until completion.
   106  	// Thus, the common scenario here may occur if we don't kill the debugger:
   107  	// 1) Start a test.
   108  	// 2) Before connecting to the debugger, you realise your code had a mistake.
   109  	// 3) Fix your code.
   110  	// 4) Control-C the current test, and rerun
   111  	// 5) Since the test is considered started (the binary was executed), it runs
   112  	//    until completion. Since it's waiting for a debugger, this will be until
   113  	//    it times out.
   114  	// 6) Tast attempts to start a debugger, but the port is already in use.
   115  	// Since ensuring that the debugger is running correctly is within the scope
   116  	// of tast, and not the end user, we should kill the process for them
   117  	// (especially since finding the pid and killing it on a remote machine is a
   118  	// pain).
   119  	if err := unix.Kill(pid, unix.SIGKILL); err != nil {
   120  		return errors.Wrapf(err, "port %d already in use by debugger on %s. Attempted to kill the existing debugger, but failed: ", port, machine)
   121  	}
   122  	// Unfortunately unix only allows you to wait on child processes, so we need to busy wait here.
   123  	// Although this is an infinite loop, SIGKILL should ensure that the process cannot save itself.
   124  	// If sigkill succeeds, the process will die in a timely manner.
   125  	for {
   126  		pid, err := getCurrentDebugger()
   127  		if err != nil || pid == -1 {
   128  			return err
   129  		}
   130  	}
   131  }
   132  
   133  // ForwardPort forwards a port from port to the ssh'd machine on the same port for the debugger.
   134  // The existing SSHConn.ForwardLocalToRemote is unsuitable for our use case because it assumes
   135  // that both channels will stop writing, and also because it attempts to accept multiple connections.
   136  func ForwardPort(ctx context.Context, sshConn *ssh.Conn, port int) error {
   137  	ctx, cancel := context.WithCancel(ctx)
   138  	onError := func(err error) {
   139  		logging.Infof(ctx, "Error while port forwarding: %s", err.Error())
   140  		cancel()
   141  	}
   142  
   143  	localAddress := fmt.Sprintf(":%d", port)
   144  	remoteAddress, err := sshConn.GenerateRemoteAddress(port)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	listener, err := net.Listen("tcp", localAddress)
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	go func() {
   155  		defer listener.Close()
   156  		client, err := listener.Accept()
   157  		if err != nil {
   158  			onError(err)
   159  			return
   160  		}
   161  		defer client.Close()
   162  
   163  		server, err := sshConn.Dial("tcp", remoteAddress)
   164  		if err != nil {
   165  			onError(errors.Wrapf(err,
   166  				"Unable to connect to DUT:%d. If the DUT is running on a different network, "+
   167  					"you may need to try port forwarding over SSH (ssh -R %d:localhost:%d <dut>), "+
   168  					"then `tast run -attachdebugger=local:%d -debuggerportforward=false. <dut> <test>", port, port, port, port))
   169  			return
   170  		}
   171  		defer server.Close()
   172  
   173  		ch := make(chan error)
   174  		go func() {
   175  			_, err := io.Copy(client, server)
   176  			ch <- err
   177  		}()
   178  		go func() {
   179  			_, err := io.Copy(server, client)
   180  			ch <- err
   181  		}()
   182  
   183  		// When detaching a debugger, only the server -> client copy returns early,
   184  		// so we only wait for one of them to close.
   185  		if err := <-ch; err != nil {
   186  			onError(err)
   187  		}
   188  
   189  	}()
   190  	return nil
   191  }
   192  
   193  // RewriteDebugCommand takes a go binary and a set of arguments it takes,
   194  // and if a debug port was provided, rewrites it as a command that instead
   195  // runs a debugger and waits on that port before running the binary.
   196  func RewriteDebugCommand(debugPort int, env []string, cmd string, args ...string) (newCmd string, newArgs []string) {
   197  	if debugPort == 0 {
   198  		return cmd, args
   199  	}
   200  	if cmd == "env" {
   201  		for i, arg := range args {
   202  			if !strings.Contains(arg, "=") {
   203  				env = append(env, args[:i]...)
   204  				cmd = args[i]
   205  				args = args[i+1:]
   206  				break
   207  			}
   208  		}
   209  	}
   210  
   211  	// Tast uses stdout to interact with the binary. Remove all delve output to
   212  	// stdout with --log-dest=/dev/null, since critical things go to stderr anyway.
   213  	return "env", append(append(env,
   214  		[]string{"dlv", "exec", cmd,
   215  			"--api-version=2",
   216  			"--headless",
   217  			fmt.Sprintf("--listen=:%d", debugPort),
   218  			"--log-dest=/dev/null",
   219  			"--"}...), args...)
   220  
   221  }
   222  
   223  // PrintWaitingMessage outputs a "Waiting for debugger" message, if required.
   224  func PrintWaitingMessage(ctx context.Context, debugPort int) {
   225  	if debugPort != 0 {
   226  		logging.Infof(ctx, "Waiting for debugger on port %d", debugPort)
   227  	}
   228  }