備忘録。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.Sys
に interface{}
型として保持している。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.Signal
は int
に対する Defined Type となっており、値自体がシグナル番号となる。
まとめ
Go で外部プロセスを終了させたシグナルを取得する方法をまとめた。ただし上記のコードは Unix 系に限る。
Windows 上で試したところ、同じメソッドを持つ golang.org/x/sys/windows.WaitStatus
は存在するものの、シグナルの存在はなかったことになっているスタブ実装となっていた(常に Exited()
が true
を、Signaled()
が false
を返す)。