Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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シグナルハンドリングのテストの信頼性と網羅性を向上させることにあります。

  1. テストの実行条件の改善: 以前のTestCtrlBreakは、flag.Boolによって制御され、-run_ctlbrk_testフラグが指定されない限り実行されませんでした。これは、CI/CD環境や通常の開発ワークフローにおいて、この重要なテストがスキップされる可能性を意味していました。コミットメッセージにある「run windows TestCtrlBreak during build」という文言から、ビルドプロセスの一部としてこのテストが常に実行されるようにすることが目的であったことが伺えます。これにより、シグナルハンドリングの回帰を早期に発見できるようになります。

  2. シグナル送信の正確性: Windowsにおけるコンソールシグナル(Ctrl+CCtrl+Breakなど)は、プロセスグループに対して送信される特性があります。以前のテスト実装では、シグナルがテストプロセス自身に送信されていた可能性があり、これがテストの不安定性や誤った結果につながる可能性がありました。テスト対象のプロセスを独立したプロセスグループで起動し、そのプロセスグループのIDを指定してシグナルを送信することで、より現実的で正確なテストが可能になります。

  3. テストの分離: テスト対象のコードを独立した実行可能ファイルとしてコンパイルし、それを別プロセスとして実行することで、テスト環境とテスト対象のコードがより明確に分離されます。これにより、テストの副作用がテストランナーに影響を与えたり、その逆の状況が発生したりするリスクが低減されます。

これらの改善により、Go言語のos/signalパッケージがWindows環境でCtrl+Breakシグナルを正しく処理できることを、より堅牢かつ自動的に検証できるようになりました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Go言語のos/signalパッケージ:

    • Go言語でOSシグナルを扱うための標準パッケージです。
    • signal.Notify関数を使って、特定のOSシグナルを受信するためのチャネルを設定できます。
    • os.Interruptは、通常Ctrl+Cによって生成されるシグナルを表しますが、WindowsではCtrl+Breakもこれにマップされることがあります。
  2. Go言語のos/execパッケージ:

    • 外部コマンドを実行するためのパッケージです。
    • exec.Commandでコマンドと引数を指定し、cmd.Start()でプロセスを起動、cmd.Wait()でプロセスの終了を待ちます。
    • cmd.SysProcAttrフィールドを通じて、OS固有のプロセス作成属性を設定できます。
  3. Go言語のsyscallパッケージ:

    • 低レベルのOSプリミティブ(システムコール)へのアクセスを提供するパッケージです。
    • Windows固有のAPI関数や定数にアクセスするために使用されます。
    • syscall.LoadDLLsyscall.FindProcを使ってDLLから関数をロードし、p.Callでその関数を呼び出すことができます。
  4. Windowsのコンソールシグナルとプロセスグループ:

    • Windowsでは、Ctrl+CCtrl+Breakといったコンソールシグナルは、通常、フォアグラウンドのプロセスグループに属するすべてのプロセスに送信されます。
    • プロセスグループ: 1つ以上のプロセスからなるグループで、コンソールシグナルを受け取る単位となります。新しいプロセスを作成する際に、新しいプロセスグループを作成するか、親プロセスのプロセスグループに参加するかを選択できます。
    • GenerateConsoleCtrlEvent API: Windows API関数の一つで、指定されたコンソールシグナル(CTRL_C_EVENTまたはCTRL_BREAK_EVENT)を、指定されたプロセスグループに送信するために使用されます。この関数は、テスト内でプログラム的にシグナルを生成するために利用されます。
    • CreateProcess API: Windowsで新しいプロセスを作成するための主要なAPI関数です。この関数には、プロセスの作成方法を制御するための様々なフラグ(dwCreationFlags)を渡すことができます。
    • CREATE_NEW_PROCESS_GROUPフラグ: CreateProcess関数に渡すことができるフラグの一つです。このフラグを指定してプロセスを作成すると、そのプロセスは新しいプロセスグループのルートプロセスとなり、独自のプロセスグループが作成されます。これにより、親プロセスから独立したシグナルハンドリングが可能になります。
  5. Ctrl+BreakCtrl+Cの違い (Windows):

    • 両者ともコンソールシグナルですが、Windowsでは異なる動作をすることがあります。
    • Ctrl+Cは通常、SIGINT(Goではos.Interrupt)にマップされ、プロセスを終了させるための「ソフトな」シグナルとして扱われます。
    • Ctrl+Breakは、より強制的な終了シグナルとして扱われることがあり、Ctrl+Cがブロックされている場合でも機能することがあります。このコミットではCtrl+Breakに焦点を当てています。

技術的詳細

このコミットにおける技術的な変更点は多岐にわたりますが、その中心はWindowsにおけるシグナルテストの堅牢化です。

  1. TestCtrlBreakのテスト戦略の変更:

    • 自己完結型テストバイナリの生成: 以前はテストランナー自身がシグナルを受信していましたが、新しいアプローチでは、TestCtrlBreak関数内で、シグナル受信ロジックを持つGoプログラムのソースコードを文字列として定義し、それを一時ファイルに書き込みます。
    • 動的なコンパイルと実行: その一時ソースファイルをgo buildコマンドで実行可能ファイル(.exe)としてコンパイルします。これにより、テスト対象のシグナルハンドリングロジックが、完全に独立したプロセスとして実行されることが保証されます。
    • 独立したプロセスグループでの起動: exec.Commandでこのコンパイル済みバイナリを実行する際、cmd.SysProcAttrフィールドにsyscall.CREATE_NEW_PROCESS_GROUPフラグを設定します。これは、子プロセスが新しいプロセスグループのルートプロセスとして起動することを意味します。これにより、GenerateConsoleCtrlEventでシグナルを送信する際に、この子プロセスグループのみをターゲットにできるようになります。
    • PID指定のシグナル送信: sendCtrlBreak関数が変更され、シグナルを送信する対象のプロセスID(pid)を受け取るようになりました。GenerateConsoleCtrlEvent APIの第2引数にこのPIDを渡すことで、特定のプロセスグループにシグナルを正確に送信します。
  2. syscallパッケージの拡張:

    • SysProcAttr.CreationFlagsの追加: src/pkg/syscall/exec_windows.goSysProcAttr構造体に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シグナルを表す定数。これらの定数が追加されたことで、コードの可読性と移植性が向上しました。
  3. エラーハンドリングと出力の改善:

    • 子プロセス内で発生したエラーはlog.Fatalfで出力され、その出力は親プロセス(テストランナー)によってbytes.Bufferにキャプチャされます。これにより、テストが失敗した場合に、子プロセスからの詳細なエラーメッセージを確認できるようになり、デバッグが容易になります。

これらの変更により、TestCtrlBreakはより独立し、制御された環境で実行されるようになり、WindowsにおけるGoのシグナルハンドリングの正確性をより確実に検証できるようになりました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. 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()))
     	}
      }
    
  2. 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
      	}
    
  3. src/pkg/syscall/ztypes_windows.go:

    • 新しい定数CREATE_NEW_PROCESS_GROUPCTRL_C_EVENTCTRL_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)を指定します。これにより、テスト対象の子プロセスに対して正確にシグナルを送信できるようになりました。
  • 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.Stdoutcmd.Stderrbytes.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 = 0CTRL_BREAK_EVENT = 1: これらは、GenerateConsoleCtrlEvent APIに渡すための、それぞれCtrl+CCtrl+Breakシグナルを表す定数です。これらの定数を明示的に定義することで、コードの意図が明確になり、マジックナンバーの使用が避けられます。

これらの変更は、Go言語がWindows環境でより正確かつ堅牢なシグナルハンドリングテストを実行できるようにするための基盤を構築しています。

関連リンク

参考にした情報源リンク

  • 上記の「関連リンク」セクションに記載されている公式ドキュメントおよびGo言語のソースコード。
  • Windowsのコンソールシグナルとプロセスグループに関する一般的な技術記事。