Go で、デバッガの attach まで実行を待機させる

Node.js や Java (JVM) 等はランタイムが大きいこともあり、本体側にデバッギ支援の機能を持っている。

node --inspect か、 NODE_OPTIONS="--inspect" ~ で、Node.js 側がポートを開いてデバッガの接続を待ち、 --inspect であれば、接続後にすぐ実行を開始する。 --inspect-brk であれば、接続後に即 break する。

一方で、Golang などのネイティブバイナリへコンパイル&リンクをする言語では、実行バイナリ本体にデバッギ支援の機能を持たせるような富豪なことはできない。dlv(1) がデバッガである。よって、以下のいずれかでデバッガを動かしてやる必要がある。

  • dlv 下でコマンドを実行する
  • 既存プロセスへ dlv が attach する

デバッガが動いた後で、dlv が開くポートへインターフェイス(IDE 等)で接続する。前者の方が簡単だ。だが、後者のように起動したいケースも多い。思いついたオプションや引数を指定してコマンドラインから実行したいとか、他のツールの配下で動いている、などのケースで。

その場合は、何とかして dlv が接続してくるまで、プロセスを待たせる必要がある。 main へ入るやいなや、下記のようなルーチンを呼んでおけば良い。インターバルを置きつつプロセスのリストを眺めて、自身の PID に対して dlv が接続しているプロセスが見つかったらループを抜ける。

// WaitForDebugger waits for a debugger to connect if the environment variable $DEBUG is set
//
// noinspection GoUnusedExportedFunction, GoUnnecessarilyExportedIdentifiers
func WaitForDebugger() {
	if os.Getenv("DEBUG") == "" && os.Getenv("WAIT") == "" && os.Getenv("WAIT_FOR_DEBUGGER") == "" {
		return
	}
	pid := os.Getpid()
	_, _ = fmt.Fprintf(os.Stderr, "Process %d is waiting\n", pid)
	for {
		time.Sleep(1 * time.Second)
		if (func() bool {
			cmd := exec.Command("ps", "w")
			stdout, err := cmd.StdoutPipe()
			if err != nil {
				log.Panicf("panic f74bdca (%v)", err)
			}
			defer func() { _ = stdout.Close() }()
			reader := bufio.NewReader(stdout)
			err = cmd.Start()
			if err != nil {
				log.Panicf("panic e137dd7 (%v)", err)
			}
			defer func() { _ = cmd.Wait() }()
			for {
				line, err := reader.ReadString('\n')
				if err == io.EOF && len(line) == 0 {
					break
				}
				if strings.Contains(line, "dlv") &&
					strings.Contains(line, fmt.Sprintf("attach %d", pid)) {
					return true
				}
			}
			return false
		})() {
			_, _ = fmt.Fprintf(os.Stderr, "Debugger connected")
			break
		}
	}
}

後は、dlv で breakpoint を指定してから、PID で attach してやれば良い。

$ go build -gcflags=all="-N -l" -o ~/go/bin/foo ~ // 最適化は切っておいた方が良い
$ foo hoge fuga

... ← 何か変だと気づいてデバッグしたくなった

$ DEBUG=1 foo hoge fuga # 環境変数 DEBUG を設定して実行すると、デバッガを待つ
2021/09/08 14:25:34 Process 98245 is waiting
2021/09/08 14:26:41 Debugger connected ← デバッガが接続すると再開する

...

上記で、sleep の位置をエラーチェックの後にしてしまうと、dlv がまだデバッグの準備が済んでいない間にプログラムが再開してしまい、breakpoint を先に抜けてしまうことがある。

引数まで含んだプロセス一覧を取得できる golang ライブラリってありませんかね? そうすれば上記、もうすこしきれいに、なおかつ OS 非依存に書けると思うんですが。

デバッギが USR1 あたりのシグナルを受信したら再始動という案もあったが、毎度シグナルを手投げするのがめんどくさかったので、タイマーにした。 // go - How can I see if the GoLand debugger is running in the program? - Stack Overflow