Go の os.ProcessState からプロセスを終了させたシグナルを取得する

備忘録。Go の os.StartProcess()os/exec.Cmd で実行した外部プロセスがシグナルによって終了した場合に、そのシグナルを取得する方法について。Go のバージョンは1.16。

注: GOOS=linux or GOOS=darwin で動作すること、および GOOS=windows で動作しないことを確認済み。他の環境は不明。

準備: シグナルで終了するプログラム

シグナルが発生しうるプログラムとして、以下の C コードを使う。

#include <stdlib.h>

int main(int argc, char *argv[]) {
  return atoi(argv[1]);
}

このプログラムは引数に数値を与えたら、それを終了ステータスとする。しかし、引数を与えなかった場合は範囲外アクセスを起こし、 SIGSEGV によって終了する(とは言い切れないが、ここではそういうことにする)。

これをコンパイルして ./program として配置しておく。

コマンド実行する Go コード

引数で受け取ったコマンドを実行し、その終了ステータスもしくはシグナルを表示するプログラムを考える。方針は以下:

  • os/exec でコマンド実行
  • os.ProcessState からプロセスの状態を取得
  • シグナルの詳細は golang.org/x/sys/unix.WaitStatus から取得

まず全体像はこちら。

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"

    "golang.org/x/sys/unix"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "usage: %s program [args...]\n", os.Args[0])
        os.Exit(1)
    }
    cmd := exec.Command(os.Args[1], os.Args[2:]...)
    cmd.Run()
    ps := cmd.ProcessState

    if ps.Exited() {
        fmt.Printf("exited with %d\n", ps.ExitCode())
        os.Exit(0)
    }

    var sys interface{} = ps.Sys()
  wsOrigin, ok := sys.(syscall.WaitStatus)
    if !ok {
        fmt.Fprintf(os.Stderr, "unknown (*ProcessState).Sys(): %T %#v", sys, sys)
    }

    ws := unix.WaitStatus(wsOrigin)
    if ws.Signaled() {
        s := ws.Signal()
        fmt.Printf("signaled with %d (%s)\n", s, s.String())
    }
}

この Go コードを使って先程 C で書いたプログラムを実行すると、以下の出力が得られる。

# 正常終了する場合
$ go run main.go ./program 42
exited with 42

# SIGSEGV により終了する場合
$ go run main.go ./program
signaled with 11 (segmentation fault)

解説

cmd := exec.Command(os.Args[1], os.Args[2:]...)
cmd.Run()
var ps *os.ProcessState = cmd.ProcessState

(*exec.Cmd).Run() でコマンドを同期的に実行する。実行したプロセスの情報は Cmd.ProcessState(+os.ProcessState) 型として保持している。

if ps.Exited() {
  var status int = ps.ExitCode()
    fmt.Printf("exited with %d\n", status)
    os.Exit(0)
}

通常終了したかどうかは (*ProcesState).Exited() で判定できる。通常終了した場合、(*ProcessStatus).ExitStatus() で終了ステータスを取得できる。

var sys interface{} = ps.Sys()
wsOrigin, ok := sys.(syscall.WaitStatus)
if !ok {
  fmt.Fprintf(os.Stderr, "unknown (*ProcessState).Sys(): %T %#v", sys, sys)
}

シグナルのような OS 依存の情報は ProcessStatte.Sysinterface{} 型として保持している。Unix 系 OS 場合、実際の型は syscall.WaitStatus である。

ws := unix.WaitStatus(wsOrigin)

ここがややこしいのだが、Go 1.4 以降は syscall パッケージは事実上の非推奨となっており、利用する OS に応じて golang.org/x/sys 以下のパッケージを利用するべきである(詳細はこちら)。Linux, Mac の場合は golang.org/x/sys/unix が移行先となる。

執筆時点では syscall.WaitStatus のままでも問題は起きないが、ここではキチンと golang.org/x/sys/unix.WaitStatus 型に変換している(どちらも uint32 に対する Defined Type なので問題ない)。

if ws.Signaled() {
  var s syscall.Signal = ws.Signal()
  fmt.Printf("signaled with %d (%s)\n", s, s.String())
}

終了ステータスと似た形式でシグナルの判定と取得を行う。golang.org/x/sys/unix.WaitStatus.Signaled() でシグナルにより終了したかを判定し、golang.org/x/sys/unix.WaitStatus.Signal) で終了させたシグナルを syscall.Signal 型として取得できる(結局ここは syscall パッケージ……)。

syscall.Signalint に対する Defined Type となっており、値自体がシグナル番号となる。

まとめ

Go で外部プロセスを終了させたシグナルを取得する方法をまとめた。ただし上記のコードは Unix 系に限る。

Windows 上で試したところ、同じメソッドを持つ golang.org/x/sys/windows.WaitStatus は存在するものの、シグナルの存在はなかったことになっているスタブ実装となっていた(常に Exited()true を、Signaled()false を返す)。

そもそも Windowsプロセスモデルについては何もわかっていない……