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 }