11

In C# the executing program can detect if it's running in the debugger using:

System.Diagnostics.Debugger.IsAttached

Is there an equivalent in Go? I have some timeouts which I would like to be disabled while I am stepping through the code. Thanks!

I am using the GoLand debugger.

Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
Philip Beber
  • 1,115
  • 12
  • 18
  • 2
    What debugger are you using? – Adrian Dec 19 '17 at 02:08
  • 2
    "Is there an equivalent in Go?" One would hope not. Code should behave the same while debugging as when not. Otherwise, you're not properly debugging. – Jonathan Hall Dec 19 '17 at 08:56
  • Thanks @Adrian, I updated the question. – Philip Beber Dec 19 '17 at 15:11
  • 2
    @Flimzy - debugged code will execute slower than non-debugged code. This seems unavoidable to me. Are you suggesting to "properly debug" that I step through the code at 10,000 lines per second? – Philip Beber Dec 19 '17 at 15:14
  • Check the GoLand documentation. I know in the case of Delve, you can pass build tags in the `dlv` command, which you could use to switch behavior to be more debug-able. – Adrian Dec 19 '17 at 15:22
  • @Flimzy it could be easily abused, but it can make debugging much more practical. For instance, I like to increase timeouts when debugging. – HuBeZa Dec 19 '19 at 09:46
  • That's easily done without using evil anti-patterns, too. – Jonathan Hall Dec 19 '19 at 09:59
  • @Flimzy What would be an easy way to include debug output only if the developer is debugging the code? (**Note**: Not in a `debug` build; only if they are actively debugging.) – Ian Boyd Jul 26 '22 at 14:05

5 Answers5

11

As far as I know, there is no built-in way to do this in the manner you described. But you can do more or less the same using build tags to indicate that the delve debugger is running. You can pass build tags to dlv with the --build-flags argument. This is basically the same technique as I described in How can I check if the race detector is enabled at runtime?

isdelve/delve.go

// +build delve

package isdelve

const Enabled = true

isdelve/nodelve.go:

// +build !delve

package isdelve

const Enabled = false

a.go:

package main

import (
    "isdelve"
    "fmt"
)

func main() {
    fmt.Println("delve", isdelve.Enabled)
}

In Goland, you can enable this under 'Run/Debug Configurations', by adding the following into 'Go tool arguments:'

-tags=delve

Goland Run/Debug Configurations window


If you are outside of Goland, running go run a.go will report delve false, if you want to run dlv on its own, use dlv debug --build-flags='-tags=delve' a.go; this will report delve true.


Alternatively, you can use delve's set command to manually set a variable after starting the debugger.

cwash
  • 4,185
  • 5
  • 43
  • 53
Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
4

If you assume that the debugger used will be Delve, you can check on the Delve process(es). There are at least two cases to consider (maybe more).

  1. Delve launched your process. In this case, when you call os.Getppid() to get the pid of your parent process, that process will be Delve.
  2. Delve didn't launch your process, but did attach to it later. In this case, you'd need to look for all running Delve processes, look at their command lines, and see if any was launched with a command line including "attach ", where is the result of calling os.Getpid(). This relies on the assumption that you're not finding an old Delve, running with an older PID that happens to match yours. (I forget what the rules on on reuse of PIDs by the OS).

Note that the os functions used by 1 and 2 are different. One gets the parent PID, the other gets your PID.

Some very basic code to do 1 looks like this:

func isLaunchedByDebugger() bool {
    // gops executable must be in the path. See https://github.com/google/gops
    gopsOut, err := exec.Command("gops", strconv.Itoa(os.Getppid())).Output()
    if err == nil && strings.Contains(string(gopsOut), "\\dlv.exe") {
        // our parent process is (probably) the Delve debugger
        return true
    }
    return false
}
John Rusk - MSFT
  • 613
  • 5
  • 10
2

On Linux, you can read the /proc/self/status file to retrieve TracerPid field, the PID of debugger if any.

func GetTracerPid() (int, error) {
    file, err := os.Open("/proc/self/status")
    if err != nil {
        return -1, fmt.Errorf("can't open process status file: %w", err)
    }
    defer file.Close()

    for {
        var tpid int
        num, err := fmt.Fscanf(file, "TracerPid: %d\n", &tpid)
        if err == io.EOF {
            break
        }
        if num != 0 {
            return tpid, nil
        }
    }

    return -1, errors.New("unknown format of process status file")
}

How to use:

tpid, err := GetTracerPid()
if err != nil {
    log.Println("something went wrong", err)
} else if tpid != 0 {
    fmt.Println("we're under debugging: tracer_pid", tpid)
} else {
    fmt.Println("we're free of tracing")
}
mymedia
  • 572
  • 6
  • 26
2

Here is a very simple-minded solution detecting Delve if your process is run by it, not attached afterwards. That's by far the majority use case, at least

package isdebugging

import (
    "os"

    "github.com/mitchellh/go-ps"
)

// IsDebugging will return true if the process was launched from Delve or the
// gopls language server debugger.
//
// It does not detect situations where a debugger attached after process start.
func IsDebugging() bool {
    pid := os.Getppid()

    // We loop in case there were intermediary processes like the gopls language server.
    for pid != 0 {
        switch p, err := ps.FindProcess(pid); {
        case err != nil:
            return false
        case p.Executable() == "dlv":
            return true
        default:
            pid = p.PPid()
        }
    }
    return false
}
FGM
  • 2,830
  • 1
  • 31
  • 31
  • 1
    Nice! I'm using VSCode and there is no way to add custom enviroment variables when debugging. So I couldn't set something like `APP_MODE=test`. Your code solved this issue for me; now I can just call `IsDebugging()` at runtime and if it's `true` then I can set the environment variable manually `os.Setenv("APP_MODE", "test")`. Thanks for sharing, cheers mate. – Alfonso Jul 18 '22 at 17:40
0

For case 2 we can set the program to wait for some signal (SIGUSR1) and attach debugger during this wait.
The code of main.go can be like this:

package main

import (
    "os"
    "os/signal"
    "syscall"
    "fmt"
    "github.com/my/repo/cmd"
)

const (
    waitForSignalEnv       = "WAIT_FOR_DEBUGGER"
    debuggerPort           = "4321"
)

func main() {
    // Waiting for debugger attach in case if waitForSignalEnv!=""
    if os.Getenv(waitForSignalEnv) != "" {
        sigs := make(chan os.Signal, 1)
        goOn := make(chan bool, 1)
        signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT, syscall.SIGUSR1)

        go func() {
            sig := <-sigs
            if sig == syscall.SIGUSR1 {
                goOn <- true
            } else if (sig == syscall.SIGTERM || sig == syscall.SIGINT ){
                fmt.Printf("Exiting ...")
                os.Exit(0)
            }
        }()     
            
        fmt.Printf("%s env is set, waiting SIGUSR1.\nYou can run remote debug in vscode and attach dlv debugger:\n\n", waitForSignalEnv)
    
        pid := os.Getpid()
        fmt.Printf("dlv attach --continue --accept-multiclient --headless --listen=:%s %d\n", debuggerPort, pid)
        fmt.Printf("\nLaunch remote debugger in vscode to port %d and then give SIGUSR1 to the process\n", debuggerPort)
        fmt.Printf("kill -SIGUSR1 %d\n", pid)
        
        <-goOn
        fmt.Printf("Continue ...")
    }
    cmd.Execute()
}

launch.json of vscode:

{
    "name": "myprog-remote-debug",
    "type": "go",
    "request": "launch",
    "remotePath": "${env:GOPATH}/src/github.com/my/repo",
    "mode": "remote",
    "port": 4321,
    "host": "127.0.0.1",
    "program": "${env:GOPATH}/src/github.com/my/repo",   
    "showLog": true,
    "trace": "verbose" 

}

Explanation: we launch the program with env WAIT_FOR_DEBUGGER=true, for example

export WAIT_FOR_DEBUGGER=true
./myprog -f values.yaml

It will output dlv attach ... command and kill -SIGUSR <pid> :

WAIT_FOR_DEBUGGER env is set, waiting SIGUSR1.
You can run remote debug in vscode and attach dlv debugger:

dlv attach --continue --accept-multiclient --headless --listen=:4321 556127

Launch remote debugger in vscode to port 4321 and then give SIGUSR1 to the process
kill -SIGUSR1 556127

Run the dlv attach ... above
Then go to VS Code and run myprog-remote-debug. Set breakpoints before
Then give him kill -SIGUSR1 556127

and breakpoints will work

kosta
  • 31
  • 2
  • 1
    Thanks but I'm not sure what this has to do with my question. I wasn't asking about attaching a debugger. I want to know if a debugger is attached already. – Philip Beber Aug 31 '20 at 03:03