[インデックス 13010] ファイルの概要
このコミットは、Go言語のos/signal
パッケージにおけるWindows環境でのCtrl+Break
シグナルテストの信頼性を向上させるものです。具体的には、TestCtrlBreak
テストがビルド時に常に実行されるように変更され、テストの実行方法が改善されました。以前はテストがオプトイン(特定のフラグを立てないと実行されない)でしたが、この変更により、テスト対象のプロセスを独立したプロセスグループで起動し、そのプロセスに対して明示的にCtrl+Break
シグナルを送信するようになりました。これにより、テストの分離性と再現性が向上しています。
コミット
commit 0d55d9832f6b21a5c273073e1703d1d0ae5ecb02
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed May 2 17:05:52 2012 +1000
os/signal: run windows TestCtrlBreak during build
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6136054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0d55d9832f6b21a5c273073e1703d1d0ae5ecb02
元コミット内容
os/signal: run windows TestCtrlBreak during build
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6136054
変更の背景
この変更の主な背景は、Windows環境におけるCtrl+Break
シグナルハンドリングのテストの信頼性と網羅性を向上させることにあります。
-
テストの実行条件の改善: 以前の
TestCtrlBreak
は、flag.Bool
によって制御され、-run_ctlbrk_test
フラグが指定されない限り実行されませんでした。これは、CI/CD環境や通常の開発ワークフローにおいて、この重要なテストがスキップされる可能性を意味していました。コミットメッセージにある「run windows TestCtrlBreak during build」という文言から、ビルドプロセスの一部としてこのテストが常に実行されるようにすることが目的であったことが伺えます。これにより、シグナルハンドリングの回帰を早期に発見できるようになります。 -
シグナル送信の正確性: Windowsにおけるコンソールシグナル(
Ctrl+C
やCtrl+Break
など)は、プロセスグループに対して送信される特性があります。以前のテスト実装では、シグナルがテストプロセス自身に送信されていた可能性があり、これがテストの不安定性や誤った結果につながる可能性がありました。テスト対象のプロセスを独立したプロセスグループで起動し、そのプロセスグループのIDを指定してシグナルを送信することで、より現実的で正確なテストが可能になります。 -
テストの分離: テスト対象のコードを独立した実行可能ファイルとしてコンパイルし、それを別プロセスとして実行することで、テスト環境とテスト対象のコードがより明確に分離されます。これにより、テストの副作用がテストランナーに影響を与えたり、その逆の状況が発生したりするリスクが低減されます。
これらの改善により、Go言語のos/signal
パッケージがWindows環境でCtrl+Break
シグナルを正しく処理できることを、より堅牢かつ自動的に検証できるようになりました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Go言語の
os/signal
パッケージ:- Go言語でOSシグナルを扱うための標準パッケージです。
signal.Notify
関数を使って、特定のOSシグナルを受信するためのチャネルを設定できます。os.Interrupt
は、通常Ctrl+C
によって生成されるシグナルを表しますが、WindowsではCtrl+Break
もこれにマップされることがあります。
-
Go言語の
os/exec
パッケージ:- 外部コマンドを実行するためのパッケージです。
exec.Command
でコマンドと引数を指定し、cmd.Start()
でプロセスを起動、cmd.Wait()
でプロセスの終了を待ちます。cmd.SysProcAttr
フィールドを通じて、OS固有のプロセス作成属性を設定できます。
-
Go言語の
syscall
パッケージ:- 低レベルのOSプリミティブ(システムコール)へのアクセスを提供するパッケージです。
- Windows固有のAPI関数や定数にアクセスするために使用されます。
syscall.LoadDLL
やsyscall.FindProc
を使ってDLLから関数をロードし、p.Call
でその関数を呼び出すことができます。
-
Windowsのコンソールシグナルとプロセスグループ:
- Windowsでは、
Ctrl+C
やCtrl+Break
といったコンソールシグナルは、通常、フォアグラウンドのプロセスグループに属するすべてのプロセスに送信されます。 - プロセスグループ: 1つ以上のプロセスからなるグループで、コンソールシグナルを受け取る単位となります。新しいプロセスを作成する際に、新しいプロセスグループを作成するか、親プロセスのプロセスグループに参加するかを選択できます。
GenerateConsoleCtrlEvent
API: Windows API関数の一つで、指定されたコンソールシグナル(CTRL_C_EVENT
またはCTRL_BREAK_EVENT
)を、指定されたプロセスグループに送信するために使用されます。この関数は、テスト内でプログラム的にシグナルを生成するために利用されます。CreateProcess
API: Windowsで新しいプロセスを作成するための主要なAPI関数です。この関数には、プロセスの作成方法を制御するための様々なフラグ(dwCreationFlags
)を渡すことができます。CREATE_NEW_PROCESS_GROUP
フラグ:CreateProcess
関数に渡すことができるフラグの一つです。このフラグを指定してプロセスを作成すると、そのプロセスは新しいプロセスグループのルートプロセスとなり、独自のプロセスグループが作成されます。これにより、親プロセスから独立したシグナルハンドリングが可能になります。
- Windowsでは、
-
Ctrl+Break
とCtrl+C
の違い (Windows):- 両者ともコンソールシグナルですが、Windowsでは異なる動作をすることがあります。
Ctrl+C
は通常、SIGINT
(Goではos.Interrupt
)にマップされ、プロセスを終了させるための「ソフトな」シグナルとして扱われます。Ctrl+Break
は、より強制的な終了シグナルとして扱われることがあり、Ctrl+C
がブロックされている場合でも機能することがあります。このコミットではCtrl+Break
に焦点を当てています。
技術的詳細
このコミットにおける技術的な変更点は多岐にわたりますが、その中心はWindowsにおけるシグナルテストの堅牢化です。
-
TestCtrlBreak
のテスト戦略の変更:- 自己完結型テストバイナリの生成: 以前はテストランナー自身がシグナルを受信していましたが、新しいアプローチでは、
TestCtrlBreak
関数内で、シグナル受信ロジックを持つGoプログラムのソースコードを文字列として定義し、それを一時ファイルに書き込みます。 - 動的なコンパイルと実行: その一時ソースファイルを
go build
コマンドで実行可能ファイル(.exe
)としてコンパイルします。これにより、テスト対象のシグナルハンドリングロジックが、完全に独立したプロセスとして実行されることが保証されます。 - 独立したプロセスグループでの起動:
exec.Command
でこのコンパイル済みバイナリを実行する際、cmd.SysProcAttr
フィールドにsyscall.CREATE_NEW_PROCESS_GROUP
フラグを設定します。これは、子プロセスが新しいプロセスグループのルートプロセスとして起動することを意味します。これにより、GenerateConsoleCtrlEvent
でシグナルを送信する際に、この子プロセスグループのみをターゲットにできるようになります。 - PID指定のシグナル送信:
sendCtrlBreak
関数が変更され、シグナルを送信する対象のプロセスID(pid
)を受け取るようになりました。GenerateConsoleCtrlEvent
APIの第2引数にこのPIDを渡すことで、特定のプロセスグループにシグナルを正確に送信します。
- 自己完結型テストバイナリの生成: 以前はテストランナー自身がシグナルを受信していましたが、新しいアプローチでは、
-
syscall
パッケージの拡張:SysProcAttr.CreationFlags
の追加:src/pkg/syscall/exec_windows.go
のSysProcAttr
構造体にCreationFlags uint32
フィールドが追加されました。これにより、os/exec
パッケージを通じてWindowsのCreateProcess
APIに任意の作成フラグを渡すことが可能になります。これは、CREATE_NEW_PROCESS_GROUP
のような特定の動作を制御するために不可欠です。- 新しい定数の定義:
src/pkg/syscall/ztypes_windows.go
に以下の定数が追加されました。CREATE_NEW_PROCESS_GROUP = 0x00000200
: プロセスを新しいプロセスグループのルートとして作成するためのフラグ。CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1
:GenerateConsoleCtrlEvent
関数で使用される、Ctrl+C
およびCtrl+Break
シグナルを表す定数。これらの定数が追加されたことで、コードの可読性と移植性が向上しました。
-
エラーハンドリングと出力の改善:
- 子プロセス内で発生したエラーは
log.Fatalf
で出力され、その出力は親プロセス(テストランナー)によってbytes.Buffer
にキャプチャされます。これにより、テストが失敗した場合に、子プロセスからの詳細なエラーメッセージを確認できるようになり、デバッグが容易になります。
- 子プロセス内で発生したエラーは
これらの変更により、TestCtrlBreak
はより独立し、制御された環境で実行されるようになり、WindowsにおけるGoのシグナルハンドリングの正確性をより確実に検証できるようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/pkg/os/signal/signal_windows_test.go
:runCtrlBreakTest
フラグの削除。sendCtrlBreak
関数のシグネチャ変更と実装の更新。TestCtrlBreak
関数の大幅な書き換え。
--- a/src/pkg/os/signal/signal_windows_test.go +++ b/src/pkg/os/signal/signal_windows_test.go @@ -5,16 +5,16 @@ package signal import ( - "flag" + "bytes" "os" + "os/exec" + "path/filepath" "syscall" "testing" "time" ) -var runCtrlBreakTest = flag.Bool("run_ctlbrk_test", false, "force to run Ctrl+Break test") - -func sendCtrlBreak(t *testing.T) { +func sendCtrlBreak(t *testing.T, pid int) { d, e := syscall.LoadDLL("kernel32.dll") if e != nil { t.Fatalf("LoadDLL: %v\n", e) @@ -23,29 +23,74 @@ func sendCtrlBreak(t *testing.T) { if e != nil { t.Fatalf("FindProc: %v\n", e) } - r, _, e := p.Call(0, 0) + r, _, e := p.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid)) if r == 0 { t.Fatalf("GenerateConsoleCtrlEvent: %v\n", e) } } func TestCtrlBreak(t *testing.T) { - if !*runCtrlBreakTest { - t.Logf("test disabled; use -run_ctlbrk_test to enable") - return - } - go func() { - time.Sleep(1 * time.Second) - sendCtrlBreak(t) - }() + // create source file + const source = ` +package main + +import ( + "log" + "os" + "os/signal" + "time" +) + + +func main() { + c := make(chan os.Signal, 10) + signal.Notify(c) + select { + case s := <-c: + if s != os.Interrupt { + log.Fatalf("Wrong signal received: got %q, want %q\n", s, os.Interrupt) + } + case <-time.After(3 * time.Second): + log.Fatalf("Timeout waiting for Ctrl+Break\n") + } +} +` + name := filepath.Join(os.TempDir(), "ctlbreak") + src := name + ".go" + defer os.Remove(src) + f, err := os.Create(src) + if err != nil { + t.Fatalf("Failed to create %v: %v", src, err) + } + defer f.Close() + f.Write([]byte(source)) + + // compile it + exe := name + ".exe" + defer os.Remove(exe) + o, err := exec.Command("go", "build", "-o", exe, src).CombinedOutput() + if err != nil { + t.Fatalf("Failed to compile: %v\n%v", err, string(o)) + } + + // run it + cmd := exec.Command(exe) + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } + err = cmd.Start() + if err != nil { + t.Fatalf("Start failed: %v", err) + } + go func() { + time.Sleep(1 * time.Second) + sendCtrlBreak(t, cmd.Process.Pid) + }() + err = cmd.Wait() + if err != nil { + t.Fatalf("Program exited with error: %v\n%v", err, string(b.Bytes())) } }
-
src/pkg/syscall/exec_windows.go
:SysProcAttr
構造体にCreationFlags
フィールドを追加。StartProcess
関数内でCreateProcess
呼び出しにsys.CreationFlags
を渡すように変更。
--- a/src/pkg/syscall/exec_windows.go +++ b/src/pkg/syscall/exec_windows.go @@ -225,8 +225,9 @@ type ProcAttr struct { } type SysProcAttr struct { - HideWindow bool - CmdLine string // used if non-empty, else the windows command line is built by escaping the arguments passed to StartProcess + HideWindow bool + CmdLine string // used if non-empty, else the windows command line is built by escaping the arguments passed to StartProcess + CreationFlags uint32 } var zeroProcAttr ProcAttr @@ -313,7 +314,8 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle pi := new(ProcessInformation) - err = CreateProcess(argv0p, argvp, nil, nil, true, CREATE_UNICODE_ENVIRONMENT, createEnvBlock(attr.Env), dirp, si, pi) + flags := sys.CreationFlags | CREATE_UNICODE_ENVIRONMENT + err = CreateProcess(argv0p, argvp, nil, nil, true, flags, createEnvBlock(attr.Env), dirp, si, pi) if err != nil { return 0, 0, err }
-
src/pkg/syscall/ztypes_windows.go
:- 新しい定数
CREATE_NEW_PROCESS_GROUP
、CTRL_C_EVENT
、CTRL_BREAK_EVENT
を追加。
--- a/src/pkg/syscall/ztypes_windows.go +++ b/src/pkg/syscall/ztypes_windows.go @@ -146,6 +146,7 @@ const ( WAIT_OBJECT_0 = 0x00000000 WAIT_FAILED = 0xFFFFFFFF + CREATE_NEW_PROCESS_GROUP = 0x00000200 CREATE_UNICODE_ENVIRONMENT = 0x00000400 PROCESS_QUERY_INFORMATION = 0x00000400 @@ -162,6 +163,9 @@ const ( FILE_MAP_WRITE = 0x02 FILE_MAP_READ = 0x04 FILE_MAP_EXECUTE = 0x20 + + CTRL_C_EVENT = 0 + CTRL_BREAK_EVENT = 1 ) const (
- 新しい定数
コアとなるコードの解説
src/pkg/os/signal/signal_windows_test.go
-
runCtrlBreakTest
フラグの削除:- 以前は
TestCtrlBreak
の実行を制御していたflag.Bool
変数が削除されました。これにより、このテストは常に実行されるようになり、ビルドプロセスの一部として自動的に検証されるようになりました。
- 以前は
-
sendCtrlBreak(t *testing.T, pid int)
関数の変更:- この関数は、Windows APIの
GenerateConsoleCtrlEvent
を呼び出してCtrl+Break
シグナルを送信する役割を担います。 - 変更前は
p.Call(0, 0)
のように引数が固定されていましたが、変更後はp.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid))
となりました。 syscall.CTRL_BREAK_EVENT
は、送信するシグナルの種類をCtrl+Break
に指定します。uintptr(pid)
は、シグナルを送信する対象のプロセスグループID(またはプロセスID)を指定します。これにより、テスト対象の子プロセスに対して正確にシグナルを送信できるようになりました。
- この関数は、Windows APIの
-
TestCtrlBreak
関数の大幅な書き換え:- インラインソースコードの定義:
const source = \
...`として、シグナルを受信するGoプログラムの完全なソースコードが定義されています。このプログラムは、
os/signalパッケージを使って
os.Interrupt`シグナルを待ち受け、正しく受信したかを検証します。タイムアウトも設定されており、シグナルが来ない場合はエラーとなります。 - 一時ファイルの作成とコンパイル: 定義されたソースコードは一時的な
.go
ファイルに書き込まれ、その後go build
コマンドを使って実行可能ファイル(.exe
)にコンパイルされます。これにより、テスト対象のシグナルハンドリングロジックが、テストランナーとは完全に独立したプロセスとして実行されます。 - 子プロセスの起動と
SysProcAttr
の設定:cmd := exec.Command(exe)
でコンパイルされた実行可能ファイルを指定し、cmd.Start()
で子プロセスを起動します。- 最も重要な変更点の一つが
cmd.SysProcAttr
の設定です。cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,}
とすることで、子プロセスが新しいプロセスグループのルートプロセスとして起動するように指示しています。これにより、sendCtrlBreak
で送信されるCtrl+Break
シグナルが、この子プロセスグループのみに影響を与えることが保証されます。
- シグナル送信と待機:
go func() { ... sendCtrlBreak(t, cmd.Process.Pid) }()
: 別ゴルーチンで1秒待機後、sendCtrlBreak
を呼び出し、起動した子プロセスのPIDを渡してCtrl+Break
シグナルを送信します。err = cmd.Wait()
: 親プロセスは子プロセスの終了を待ちます。子プロセスがシグナルを正しく処理し、正常に終了すればテストは成功します。
- 出力のキャプチャとエラー報告:
cmd.Stdout
とcmd.Stderr
をbytes.Buffer
にリダイレクトすることで、子プロセスの標準出力と標準エラー出力をキャプチャします。これにより、子プロセス内で発生したlog.Fatalf
などのエラーメッセージを親プロセスで確認でき、テスト失敗時のデバッグ情報が豊富になります。
- インラインソースコードの定義:
src/pkg/syscall/exec_windows.go
-
SysProcAttr
構造体へのCreationFlags
フィールド追加:SysProcAttr
は、os/exec
パッケージがWindowsでプロセスを起動する際に、OS固有の属性を設定するために使用される構造体です。CreationFlags uint32
フィールドが追加されたことで、CreateProcess
APIに渡すdwCreationFlags
引数を、Goのコードから直接制御できるようになりました。これは、CREATE_NEW_PROCESS_GROUP
のような重要なフラグを設定するために不可欠です。
-
StartProcess
関数でのCreationFlags
の使用:StartProcess
関数は、Goのos/exec
パッケージのバックエンドで実際にWindowsプロセスを起動する関数です。CreateProcess
APIを呼び出す際に、flags := sys.CreationFlags | CREATE_UNICODE_ENVIRONMENT
という行が追加されました。これにより、SysProcAttr
で指定されたCreationFlags
が、既存のCREATE_UNICODE_ENVIRONMENT
フラグと組み合わされてCreateProcess
に渡されるようになりました。
src/pkg/syscall/ztypes_windows.go
- 新しい定数の定義:
CREATE_NEW_PROCESS_GROUP = 0x00000200
: この定数は、新しいプロセスを作成する際に、そのプロセスを新しいプロセスグループのルートとして設定するために使用されます。これにより、シグナルがそのプロセスグループにのみ送信されるようになります。CTRL_C_EVENT = 0
とCTRL_BREAK_EVENT = 1
: これらは、GenerateConsoleCtrlEvent
APIに渡すための、それぞれCtrl+C
とCtrl+Break
シグナルを表す定数です。これらの定数を明示的に定義することで、コードの意図が明確になり、マジックナンバーの使用が避けられます。
これらの変更は、Go言語がWindows環境でより正確かつ堅牢なシグナルハンドリングテストを実行できるようにするための基盤を構築しています。
関連リンク
- Go os/signal package documentation
- Go os/exec package documentation
- Go syscall package documentation
- Microsoft Docs: GenerateConsoleCtrlEvent function
- Microsoft Docs: CreateProcessA function (Process Creation Flags)
- Microsoft Docs: Process Groups
参考にした情報源リンク
- 上記の「関連リンク」セクションに記載されている公式ドキュメントおよびGo言語のソースコード。
- Windowsのコンソールシグナルとプロセスグループに関する一般的な技術記事。