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

[インデックス 12144] ファイルの概要

このコミットは、Go言語のダッシュボード(おそらくビルドシステムの一部)で使用されているexec.goファイルに対する変更です。具体的には、外部コマンドの実行と結果の取得を扱うロジックが、Go標準ライブラリのos/execパッケージの変更に合わせて更新されています。

コミット

commit 0427c583a5877223447ec73b740b97fc39b12894
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Feb 22 11:48:41 2012 -0800

    builder: update for os.Wait changes.
    
    This compiles again.
    
    R=golang-dev, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/5687078

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0427c583a5877223447ec73b740b97fc39b12894

元コミット内容

builder: update for os.Wait changes.

This compiles again.

R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/5687078

変更の背景

このコミットの背景には、Go言語の標準ライブラリであるos/execパッケージにおけるプロセスの待機(Wait)に関するAPIの変更があります。コミットメッセージにある「os.Wait changes」がそれを示しています。

Go言語は当時まだ比較的新しい言語であり、APIの安定化と改善が活発に行われていました。os/execパッケージは外部コマンドを実行するための重要な機能を提供しますが、その初期のバージョンでは、コマンドの実行結果(特に終了ステータス)の取得方法に改善の余地がありました。

以前のcmd.Run()メソッドは、コマンドの実行と待機を一度に行い、エラーが発生した場合に*exec.ExitError型を返すことで非ゼロの終了ステータスを通知していました。しかし、このエラーハンドリングのパターンが、特定のシナリオで期待通りに機能しない、あるいはより堅牢な方法が求められるようになった可能性があります。

この変更は、cmd.Run()の代わりにcmd.Start()cmd.Wait()を明示的に使用することで、プロセスの開始と終了待機を分離し、より明確で制御しやすいエラーハンドリングを実現することを目的としています。これにより、ビルドシステムが外部コマンドの実行結果を正確に把握し、それに基づいて適切な処理を行えるように修正されました。コミットメッセージの「This compiles again.」という記述から、API変更によって既存のコードがコンパイルエラーを起こすか、あるいは実行時エラーを引き起こすようになったため、この修正が必要になったことが伺えます。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とos/execパッケージの基本的な使い方を理解しておく必要があります。

  1. os/execパッケージ: Go言語で外部コマンドを実行するための標準ライブラリです。exec.Command関数を使って実行するコマンドと引数を指定し、exec.Cmd構造体を作成します。

  2. exec.Cmd構造体: 外部コマンドの実行に関する設定(コマンド名、引数、環境変数、作業ディレクトリ、標準入出力のリダイレクトなど)を保持する構造体です。

  3. cmd.Run()メソッド: exec.Cmd構造体のメソッドの一つで、コマンドを実行し、その完了を待ちます。コマンドが正常に終了した場合(終了ステータスが0)、nilを返します。コマンドが非ゼロの終了ステータスで終了した場合、*exec.ExitError型のエラーを返します。このエラーからExitStatus()メソッドを使って終了ステータスを取得できます。

  4. cmd.Start()メソッド: exec.Cmd構造体のメソッドの一つで、コマンドを非同期で実行します。コマンドの実行が開始された時点で制御を呼び出し元に戻します。コマンドの開始に失敗した場合(例: コマンドが見つからない、実行権限がないなど)にエラーを返します。

  5. cmd.Wait()メソッド: exec.Cmd構造体のメソッドの一つで、cmd.Start()で開始されたコマンドの完了を待ちます。コマンドが正常に終了した場合(終了ステータスが0)、nilを返します。コマンドが非ゼロの終了ステータスで終了した場合、*exec.ExitError型のエラーを返します。cmd.Start()と組み合わせて使用することで、コマンドの実行と待機を分離し、より柔軟な処理(例: 実行中に他の処理を行う、タイムアウトを設定するなど)が可能になります。

  6. *exec.ExitError: os/execパッケージで定義されているエラー型で、実行された外部コマンドが非ゼロの終了ステータスで終了した場合に返されます。この型はerrorインターフェースを満たし、ExitStatus()メソッドを通じてコマンドの終了ステータスを取得できます。

これらのメソッドの使い分けは、外部コマンドの実行フローをどのように制御したいかによって異なります。Run()はシンプルに実行と待機を一度に行う場合に便利ですが、Start()Wait()の組み合わせは、より詳細な制御が必要な場合に適しています。

技術的詳細

このコミットの技術的詳細は、os/execパッケージのCmd構造体のメソッドの利用方法の変更に集約されます。

変更前は、runLog関数内で外部コマンドを実行するためにcmd.Run()が使用されていました。

	err := cmd.Run()
	if err != nil {
		if ws, ok := err.(*exec.ExitError); ok {
			return b.String(), ws.ExitStatus(), nil
		}
	}
	return b.String(), 0, err

このコードでは、cmd.Run()がエラーを返した場合、それが*exec.ExitError型であるかをチェックし、そうであればその終了ステータスを関数の戻り値として利用していました。それ以外のエラー(例: コマンドが見つからない、権限がないなど)の場合は、そのままエラーを返していました。

変更後は、cmd.Run()の代わりにcmd.Start()cmd.Wait()の組み合わせが導入されました。

	startErr := cmd.Start()
	if startErr != nil {
		return "", 1, startErr
	}
	exitStatus := 0
	if err := cmd.Wait(); err != nil {
		exitStatus = 1 // TODO(bradfitz): this is fake. no callers care, so just return a bool instead.
	}
	return b.String(), exitStatus, nil

この新しいアプローチでは、以下の点が異なります。

  1. プロセスの開始と待機を分離:

    • まずcmd.Start()を呼び出してコマンドの実行を開始します。これにより、コマンドが実際に起動できたかどうかを確認できます。startErrnilでない場合、コマンドの起動自体に失敗したことを意味し、即座にエラーを返します。
    • 次にcmd.Wait()を呼び出して、開始されたコマンドの完了を待ちます。cmd.Wait()は、コマンドが非ゼロの終了ステータスで終了した場合にエラーを返します。
  2. 終了ステータスの扱い:

    • 変更前は*exec.ExitErrorからExitStatus()を直接取得していましたが、変更後はcmd.Wait()がエラーを返した場合にexitStatus1に設定しています。これは、TODOコメントにあるように、呼び出し元が具体的な終了ステータスの値に依存せず、成功/失敗のブール値のみを気にしているため、簡略化された処理です。将来的にはより正確な終了ステータスを返すように修正される可能性が示唆されています。

この変更は、os/execパッケージの内部的な変更(例えば、cmd.Run()*exec.ExitErrorを返す挙動が変更された、あるいはより厳密なエラーハンドリングが推奨されるようになったなど)に対応するためのものです。Start()Wait()の組み合わせは、プロセスのライフサイクルをより細かく制御できるため、堅牢なアプリケーションでは一般的に推奨されるパターンです。

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

変更はmisc/dashboard/builder/exec.goファイル内のrunLog関数に集中しています。

--- a/misc/dashboard/builder/exec.go
+++ b/misc/dashboard/builder/exec.go
@@ -28,7 +28,7 @@ func run(envv []string, dir string, argv ...string) error {
 // as well as writing it to logfile (if specified). It returns
 // process combined stdout and stderr output, exit status and error.
 // The error returned is nil, if process is started successfully,
-// even if exit status is not 0.
+// even if exit status is not successful.
 func runLog(envv []string, logfile, dir string, argv ...string) (string, int, error) {
 	if *verbose {
 		log.Println("runLog", argv)
@@ -51,11 +51,13 @@ func runLog(envv []string, logfile, dir string, argv ...string) (string, int, er
 	cmd.Stdout = w
 	cmd.Stderr = w
 
-	err := cmd.Run()
-	if err != nil {
-		if ws, ok := err.(*exec.ExitError); ok {
-			return b.String(), ws.ExitStatus(), nil
-		}
-	}
-	return b.String(), 0, err
+	startErr := cmd.Start()
+	if startErr != nil {
+		return "", 1, startErr
+	}
+	exitStatus := 0
+	if err := cmd.Wait(); err != nil {
+		exitStatus = 1 // TODO(bradfitz): this is fake. no callers care, so just return a bool instead.
+	}
+	return b.String(), exitStatus, nil
 }

コアとなるコードの解説

runLog関数は、指定されたコマンドを実行し、その標準出力と標準エラー出力をキャプチャし、ログファイルにも書き込み、最終的に結合された出力、終了ステータス、およびエラーを返します。

変更の核心は、コマンドの実行方法とエラーハンドリングのロジックです。

変更前:

  1. err := cmd.Run(): コマンドを実行し、完了を待ちます。エラーがあればerrに格納されます。
  2. if err != nil: エラーが発生した場合の処理。
  3. if ws, ok := err.(*exec.ExitError); ok: エラーが*exec.ExitError型であるかを確認します。これは、コマンドが非ゼロの終了ステータスで終了した場合に発生します。
  4. return b.String(), ws.ExitStatus(), nil: *exec.ExitErrorであれば、バッファの内容とコマンドの終了ステータスを返し、エラー自体はnilとして扱います(非ゼロ終了ステータスはエラーではない、という設計)。
  5. return b.String(), 0, err: それ以外のエラー(例: コマンドが見つからないなど)の場合は、バッファの内容、終了ステータス0(これは不正確)、および実際のエラーを返します。

変更後:

  1. startErr := cmd.Start(): コマンドの実行を開始します。これにより、コマンドが起動できたかどうかの初期チェックが行われます。
  2. if startErr != nil: コマンドの起動に失敗した場合(例: コマンドが見つからない、権限がないなど)の処理。
  3. return "", 1, startErr: 起動エラーが発生した場合、空の文字列、終了ステータス1(仮の値)、および起動エラー自体を返します。
  4. exitStatus := 0: 終了ステータスを初期化します。
  5. if err := cmd.Wait(); err != nil: cmd.Start()で開始されたコマンドの完了を待ちます。cmd.Wait()がエラーを返した場合(コマンドが非ゼロの終了ステータスで終了したことを意味します)。
  6. exitStatus = 1: コマンドが非ゼロの終了ステータスで終了したことを示すために、exitStatusを1に設定します。TODOコメントは、この値が仮のものであり、呼び出し元が詳細な終了ステータスを必要としないため簡略化されていることを示唆しています。
  7. return b.String(), exitStatus, nil: バッファの内容、計算された終了ステータス、およびnilエラーを返します。cmd.Wait()がエラーを返しても、それは関数の戻り値としてはエラーとして扱われない(終了ステータスで表現される)という設計です。

この変更により、コマンドの起動失敗と、起動後のコマンドの非ゼロ終了ステータスをより明確に区別できるようになりました。また、cmd.Run()の内部的な挙動変更に対応し、コードが再びコンパイルおよび実行可能になったと考えられます。

関連リンク

参考にした情報源リンク

  • Go言語 os/exec パッケージの公式ドキュメント
  • Go言語のコミット履歴と関連するコードレビュー(CL)
  • Go言語の初期のAPI変更に関する一般的な知識