[インデックス 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
パッケージの基本的な使い方を理解しておく必要があります。
-
os/exec
パッケージ: Go言語で外部コマンドを実行するための標準ライブラリです。exec.Command
関数を使って実行するコマンドと引数を指定し、exec.Cmd
構造体を作成します。 -
exec.Cmd
構造体: 外部コマンドの実行に関する設定(コマンド名、引数、環境変数、作業ディレクトリ、標準入出力のリダイレクトなど)を保持する構造体です。 -
cmd.Run()
メソッド:exec.Cmd
構造体のメソッドの一つで、コマンドを実行し、その完了を待ちます。コマンドが正常に終了した場合(終了ステータスが0)、nil
を返します。コマンドが非ゼロの終了ステータスで終了した場合、*exec.ExitError
型のエラーを返します。このエラーからExitStatus()
メソッドを使って終了ステータスを取得できます。 -
cmd.Start()
メソッド:exec.Cmd
構造体のメソッドの一つで、コマンドを非同期で実行します。コマンドの実行が開始された時点で制御を呼び出し元に戻します。コマンドの開始に失敗した場合(例: コマンドが見つからない、実行権限がないなど)にエラーを返します。 -
cmd.Wait()
メソッド:exec.Cmd
構造体のメソッドの一つで、cmd.Start()
で開始されたコマンドの完了を待ちます。コマンドが正常に終了した場合(終了ステータスが0)、nil
を返します。コマンドが非ゼロの終了ステータスで終了した場合、*exec.ExitError
型のエラーを返します。cmd.Start()
と組み合わせて使用することで、コマンドの実行と待機を分離し、より柔軟な処理(例: 実行中に他の処理を行う、タイムアウトを設定するなど)が可能になります。 -
*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
この新しいアプローチでは、以下の点が異なります。
-
プロセスの開始と待機を分離:
- まず
cmd.Start()
を呼び出してコマンドの実行を開始します。これにより、コマンドが実際に起動できたかどうかを確認できます。startErr
がnil
でない場合、コマンドの起動自体に失敗したことを意味し、即座にエラーを返します。 - 次に
cmd.Wait()
を呼び出して、開始されたコマンドの完了を待ちます。cmd.Wait()
は、コマンドが非ゼロの終了ステータスで終了した場合にエラーを返します。
- まず
-
終了ステータスの扱い:
- 変更前は
*exec.ExitError
からExitStatus()
を直接取得していましたが、変更後はcmd.Wait()
がエラーを返した場合にexitStatus
を1
に設定しています。これは、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
関数は、指定されたコマンドを実行し、その標準出力と標準エラー出力をキャプチャし、ログファイルにも書き込み、最終的に結合された出力、終了ステータス、およびエラーを返します。
変更の核心は、コマンドの実行方法とエラーハンドリングのロジックです。
変更前:
err := cmd.Run()
: コマンドを実行し、完了を待ちます。エラーがあればerr
に格納されます。if err != nil
: エラーが発生した場合の処理。if ws, ok := err.(*exec.ExitError); ok
: エラーが*exec.ExitError
型であるかを確認します。これは、コマンドが非ゼロの終了ステータスで終了した場合に発生します。return b.String(), ws.ExitStatus(), nil
:*exec.ExitError
であれば、バッファの内容とコマンドの終了ステータスを返し、エラー自体はnil
として扱います(非ゼロ終了ステータスはエラーではない、という設計)。return b.String(), 0, err
: それ以外のエラー(例: コマンドが見つからないなど)の場合は、バッファの内容、終了ステータス0(これは不正確)、および実際のエラーを返します。
変更後:
startErr := cmd.Start()
: コマンドの実行を開始します。これにより、コマンドが起動できたかどうかの初期チェックが行われます。if startErr != nil
: コマンドの起動に失敗した場合(例: コマンドが見つからない、権限がないなど)の処理。return "", 1, startErr
: 起動エラーが発生した場合、空の文字列、終了ステータス1(仮の値)、および起動エラー自体を返します。exitStatus := 0
: 終了ステータスを初期化します。if err := cmd.Wait(); err != nil
:cmd.Start()
で開始されたコマンドの完了を待ちます。cmd.Wait()
がエラーを返した場合(コマンドが非ゼロの終了ステータスで終了したことを意味します)。exitStatus = 1
: コマンドが非ゼロの終了ステータスで終了したことを示すために、exitStatus
を1に設定します。TODO
コメントは、この値が仮のものであり、呼び出し元が詳細な終了ステータスを必要としないため簡略化されていることを示唆しています。return b.String(), exitStatus, nil
: バッファの内容、計算された終了ステータス、およびnil
エラーを返します。cmd.Wait()
がエラーを返しても、それは関数の戻り値としてはエラーとして扱われない(終了ステータスで表現される)という設計です。
この変更により、コマンドの起動失敗と、起動後のコマンドの非ゼロ終了ステータスをより明確に区別できるようになりました。また、cmd.Run()
の内部的な挙動変更に対応し、コードが再びコンパイルおよび実行可能になったと考えられます。
関連リンク
- Go言語
os/exec
パッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語の公式リポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Go言語
os/exec
パッケージの公式ドキュメント - Go言語のコミット履歴と関連するコードレビュー(CL)
- Go言語の初期のAPI変更に関する一般的な知識