[インデックス 15258] ファイルの概要
このコミットは、Go言語のダッシュボードビルダにおけるビルド結果の記録方法を改善し、ビルダの失敗時のデバッグを容易にすることを目的としています。具体的には、サブコマンドの出力を制御するための新しい関数 runOutput を導入し、ビルドステータスをダッシュボードに送信できるように変更しています。
コミット
commit ea0292c61bfe85d85ef6f81fe1874227c5fc674a
Author: Dave Cheney <dave@cheney.net>
Date: Fri Feb 15 10:44:29 2013 +1100
misc/dashboard/builder: record build result on dashboard
This is part one of two changes intended to make it easier to debug builder failures.
runOutput allows us to control the io.Writer passed to a subcommand. The intention is to add additional debugging information before and after the build which will then be capture and sent to the dashboard.
In this proposal, the only additional information is the build status. See http://build.golang.org/log/e7b5bf435b4de1913fc61781b3295fb3f03aeb6e
R=adg
CC=golang-dev
https://golang.org/cl/7303090
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ea0292c61bfe85d85ef6f81fe1874227c5fc674a
元コミット内容
misc/dashboard/builder: record build result on dashboard
This is part one of two changes intended to make it easier to debug builder failures.
runOutput allows us to control the io.Writer passed to a subcommand. The intention is to add additional debugging information before and after the build which will then be capture and sent to the dashboard.
In this proposal, the only additional information is the build status. See http://build.golang.org/log/e7b5bf435b4de1913fc61781b3295fb3f03aeb6e
変更の背景
この変更は、Go言語のビルドダッシュボード(build.golang.org)におけるビルダの失敗をデバッグしやすくするための2段階の変更の第一段階です。既存のビルドプロセスでは、サブコマンドの出力が直接ログファイルに書き込まれるか、バッファに格納されるだけで、ビルドの成功/失敗ステータスや追加のデバッグ情報をダッシュボードに効率的に送信する仕組みが不足していました。
このコミットの主な目的は、サブコマンドの標準出力(stdout)と標準エラー出力(stderr)を柔軟に制御できるようにすることです。これにより、ビルドの前後に追加のデバッグ情報を挿入し、それらの情報をキャプチャしてダッシュボードに送信することが可能になります。この提案では、まずビルドステータス(成功/失敗)をダッシュボードに記録することに焦点を当てています。
前提知識の解説
- Go言語のビルドダッシュボード (build.golang.org): Go言語の公式ビルドシステムであり、様々なプラットフォームやアーキテクチャでのGoのビルドとテストの状況を監視するウェブサービスです。継続的インテグレーション(CI)システムとして機能し、Goの変更が様々な環境で正しく動作するかを確認します。ビルダは、このダッシュボードにビルド結果を報告します。
io.Writerインターフェース: Go言語の標準ライブラリioパッケージで定義されているインターフェースです。Write([]byte) (n int, err error)メソッドを持つ型はio.Writerとして扱われます。これにより、ファイル、ネットワーク接続、メモリバッファなど、様々な出力先に統一された方法でデータを書き込むことができます。bytes.Buffer:bytesパッケージで提供される可変長のバイトバッファです。io.Writerインターフェースを実装しており、メモリ上にデータを蓄積するのに便利です。os.OpenFile:osパッケージの関数で、指定されたファイルを開きます。os.O_WRONLY(書き込み専用)、os.O_CREATE(ファイルが存在しない場合に作成)、os.O_APPEND(書き込み時にファイルの末尾に追加) などのフラグを指定できます。io.MultiWriter:ioパッケージの関数で、複数のio.Writerを結合し、単一のio.Writerとして扱えるようにします。MultiWriterに書き込まれたデータは、結合されたすべてのWriterに書き込まれます。これにより、例えば、出力をファイルに書き込みつつ、同時にメモリバッファにも書き込むといったことが可能になります。os/execパッケージ: Go言語で外部コマンドを実行するためのパッケージです。exec.Commandでコマンドを構築し、cmd.Start()で実行を開始し、cmd.Wait()で完了を待ちます。cmd.Stdoutとcmd.Stderrフィールドにio.Writerを設定することで、コマンドの標準出力と標準エラー出力のリダイレクトを制御できます。waitWithTimeout: このコードベースで定義されているヘルパー関数で、指定されたタイムアウト時間内に外部コマンドの完了を待ちます。- 終了ステータス (Exit Status): 外部コマンドが終了する際に返す数値です。通常、
0は成功を示し、非ゼロの値はエラーを示します。 - ブール値 (Boolean) での成功/失敗表現: 従来の終了ステータス(整数)の代わりに、
true(成功)またはfalse(失敗)のブール値でコマンドの実行結果を示すアプローチです。これは、Go言語の慣習として、エラーがerror型で返されるため、成功/失敗の論理的な結果をブール値で表現する方が自然な場合があります。
技術的詳細
このコミットの核心は、misc/dashboard/builder/exec.go にある runLog 関数の変更と、新しい runOutput 関数の導入、そして misc/dashboard/builder/main.go でこれらの関数がどのように利用されるかの変更です。
misc/dashboard/builder/exec.go の変更
-
runLog関数の変更:- 変更前:
func runLog(timeout time.Duration, envv []string, logfile, dir string, argv ...string) (string, int, error)logfileという引数を受け取り、そのファイルにログを書き込む責任を持っていました。- コマンドの終了ステータスを
int型で返していました。
- 変更後:
func runLog(timeout time.Duration, envv []string, dir string, argv ...string) (string, bool, error)logfile引数が削除され、ログファイルへの書き込みの責任がなくなりました。- 終了ステータスを
intからboolに変更しました。trueは成功、falseは失敗を示します。これは、Goのエラーハンドリングの慣習(エラーはerror型で返す)により適合しています。 - 内部的には、新しく導入された
runOutputを呼び出すように変更されました。runLogは、runOutputの結果をbytes.Bufferに書き込み、その内容を文字列として返します。
- 変更前:
-
runOutput関数の新規導入:func runOutput(timeout time.Duration, envv []string, out io.Writer, dir string, argv ...string) (bool, error)- この関数は、外部コマンドの標準出力と標準エラー出力を、引数として渡された
io.Writer(out) に直接リダイレクトします。 runLogとは異なり、コマンドの出力を文字列として返す責任はありません。その代わりに、io.Writerを通じて出力の制御を呼び出し元に委ねます。- コマンドの実行が成功したかどうかを
boolで返します。エラーが発生した場合はerrorを返します。 - この関数が導入されたことで、呼び出し元は
io.MultiWriterなどを使用して、コマンドの出力を複数の場所に同時に送ることができるようになり、柔軟性が大幅に向上しました。
misc/dashboard/builder/main.go の変更
main.go では、主に Builder 型の buildHash メソッドと buildSubrepo メソッドで、runLog の呼び出しが新しい runOutput の利用に合わせて変更されています。
-
buildHashメソッドの変更:- ビルドコマンドの実行部分で、
runLogの代わりにrunOutputが使用されるようになりました。 var buildlog bytes.Bufferとf, err := os.Create(logfile)で、メモリバッファとログファイルの両方を作成します。w := io.MultiWriter(f, &buildlog)を使用して、コマンドの出力をログファイルとメモリバッファの両方に同時に書き込むio.Writerを作成します。ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd)のようにrunOutputを呼び出し、ビルドの成功/失敗をokブール値で受け取ります。- ビルドの終了時に、
fmt.Fprintf(w, "Build complete, duration %v. Result: %v\\n", runTime, errf())を使って、ビルドの完了時間と結果(成功/失敗/エラー)をログファイルとメモリバッファの両方に追記しています。これにより、ビルドの最終ステータスがログに明示的に記録されるようになりました。 b.recordResultを呼び出す際に、buildlog.String()を渡すことで、メモリバッファに蓄積されたビルドログ全体をダッシュボードに記録できるようになりました。
- ビルドコマンドの実行部分で、
-
buildSubrepoメソッドの変更:go getおよびgo testコマンドの実行部分で、runLogの戻り値のstatus(int) がok(bool) に変更されました。if err == nil && status != 0のような条件がif err == nil && !okに変更され、ブール値のokを直接評価するようになりました。これにより、コードがより簡潔になり、Goのエラーハンドリングの慣習に沿った形になりました。
これらの変更により、ビルドプロセスの出力制御がより柔軟になり、ビルドの成功/失敗ステータスをより明確に記録し、デバッグ情報をダッシュボードに送信する基盤が強化されました。
コアとなるコードの変更箇所
misc/dashboard/builder/exec.go
--- a/misc/dashboard/builder/exec.go
+++ b/misc/dashboard/builder/exec.go
@@ -29,42 +29,38 @@ func run(timeout time.Duration, envv []string, dir string, argv ...string) error
return waitWithTimeout(timeout, cmd)
}
-// runLog runs a process and returns the combined stdout/stderr,
-// 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 successful.
-func runLog(timeout time.Duration, envv []string, logfile, dir string, argv ...string) (string, int, error) {
- if *verbose {
- log.Println("runLog", argv)
- }
+// runLog runs a process and returns the combined stdout/stderr. 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 successful.
+func runLog(timeout time.Duration, envv []string, dir string, argv ...string) (string, bool, error) {
+ var b bytes.Buffer
+ ok, err := runOutput(timeout, envv, &b, dir, argv...)
+ return b.String(), ok, err
+}
- b := new(bytes.Buffer)
- var w io.Writer = b
- if logfile != "" {
- f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
- if err != nil {
- return "", 0, err
- }
- defer f.Close()
- w = io.MultiWriter(f, b)
- }
+// runOutput runs a process and directs any output to the supplied writer.
+// It returns exit status and error. The error returned is nil, if process
+// is started successfully, even if exit status is not successful.
+func runOutput(timeout time.Duration, envv []string, out io.Writer, dir string, argv ...string) (bool, error) {
+ if *verbose {
+ log.Println("runOutput", argv)
+ }
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Dir = dir
cmd.Env = envv
- cmd.Stdout = w
- cmd.Stderr = w
+ cmd.Stdout = out
+ cmd.Stderr = out
startErr := cmd.Start()
if startErr != nil {
- return "", 1, startErr
+ return false, startErr
}
- exitStatus := 0
if err := waitWithTimeout(timeout, cmd); err != nil {
- exitStatus = 1 // TODO(bradfitz): this is fake. no callers care, so just return a bool instead.
+ return false, err
}
- return b.String(), exitStatus, nil
+ return true, nil
}
func waitWithTimeout(timeout time.Duration, cmd *exec.Cmd) error
misc/dashboard/builder/main.go
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -9,6 +9,7 @@ import (
"encoding/xml"
"flag"
"fmt"
+ "io"
"io/ioutil"
"log"
"os"
@@ -272,21 +273,36 @@ func (b *Builder) buildHash(hash string) error {
srcDir := filepath.Join(workpath, "go", "src")
// build
+ var buildlog bytes.Buffer
logfile := filepath.Join(workpath, "build.log")
+ f, err := os.Create(logfile)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ w := io.MultiWriter(f, &buildlog)
+
cmd := *buildCmd
if !filepath.IsAbs(cmd) {
cmd = filepath.Join(srcDir, cmd)
}
startTime := time.Now()
- buildLog, status, err := runLog(*buildTimeout, b.envv(), logfile, srcDir, cmd)
+ ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd)
runTime := time.Now().Sub(startTime)
- if err != nil {
- return fmt.Errorf("%s: %s", *buildCmd, err)
+ errf := func() string {
+ if err != nil {
+ return fmt.Sprintf("error: %v", err)
+ }
+ if !ok {
+ return "failed"
+ }
+ return "success"
}
+ fmt.Fprintf(w, "Build complete, duration %v. Result: %v\\n", runTime, errf())
- if status != 0 {
+ if err != nil || !ok {
// record failure
- return b.recordResult(false, "", hash, "", buildLog, runTime)
+ return b.recordResult(false, "", hash, "", buildlog.String(), runTime)
}
// record success
@@ -372,9 +388,9 @@ func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error)
}
// fetch package and dependencies
- log, status, err := runLog(*cmdTimeout, env, "", goPath, goTool, "get", "-d", pkg+"/...")
- if err == nil && status != 0 {
- err = fmt.Errorf("go exited with status %d", status)
+ log, ok, err := runLog(*cmdTimeout, env, goPath, goTool, "get", "-d", pkg+"/...")
+ if err == nil && !ok {
+ err = fmt.Errorf("go exited with status 1")
}
if err != nil {
return log, err
@@ -387,9 +403,9 @@ func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error)
}
// test the package
- log, status, err = runLog(*buildTimeout, env, "", goPath, goTool, "test", "-short", pkg+"/...")
- if err == nil && status != 0 {
- err = fmt.Errorf("go exited with status %d", status)
+ log, ok, err = runLog(*buildTimeout, env, goPath, goTool, "test", "-short", pkg+"/...")
+ if err == nil && !ok {
+ err = fmt.Errorf("go exited with status 1")
}
return log, err
}
@@ -571,7 +587,7 @@ func commitPoll(key, pkg string) {
const N = 50 // how many revisions to grab
lockGoroot()
- data, _, err := runLog(*cmdTimeout, nil, "", pkgRoot, hgCmd("log",
+ data, _, err := runLog(*cmdTimeout, nil, pkgRoot, hgCmd("log",
"--encoding=utf-8",
"--limit="+strconv.Itoa(N),
"--template="+xmlLogTemplate)...,
@@ -663,7 +679,7 @@ func fullHash(root, rev string) (string, error) {
if root == goroot {
gorootMu.Lock()
}
- s, _, err := runLog(*cmdTimeout, nil, "", root,
+ s, _, err := runLog(*cmdTimeout, nil, root,
hgCmd("log",
"--encoding=utf-8",
"--rev="+rev,
コアとなるコードの解説
このコミットの主要な変更点は、外部コマンドの実行と出力処理の抽象化と柔軟性の向上です。
-
runLogからrunOutputへの責任の分離:- 以前の
runLogは、ログファイルへの書き込み、メモリバッファへの書き込み、そしてコマンドの終了ステータス(int)の返却という複数の責任を持っていました。 - 新しい
runOutput関数は、外部コマンドの出力を任意のio.Writerにリダイレクトするという単一の責任に特化しています。これにより、runOutputはより汎用的なユーティリティ関数となりました。 runLogはrunOutputを内部的に呼び出すことで、その責任の一部(メモリバッファへの書き込みと文字列としての出力返却)を維持しつつ、ログファイルへの書き込みの責任は呼び出し元に委ねる形になりました。
- 以前の
-
io.Writerを介した出力制御の柔軟性:runOutputがio.Writerを引数として受け取るようになったことで、呼び出し元はコマンドの出力を自由に制御できるようになりました。main.goのbuildHashメソッドでは、この柔軟性を活用してio.MultiWriterを使用しています。これにより、ビルドコマンドの出力が同時にログファイルとメモリバッファ(buildlog)の両方に書き込まれるようになりました。- この変更は、ビルドログをファイルに永続化しつつ、メモリ上のバッファにも保持することで、後でダッシュボードに送信する際にログ全体を簡単に取得できるようにするという、非常に実用的なメリットをもたらします。
-
終了ステータスの
intからboolへの変更:runLogおよびrunOutputの戻り値で、コマンドの成功/失敗を示すステータスがintからboolに変更されました。- Go言語では、エラーは通常
error型で返されるため、コマンドの実行が成功したかどうかをboolで示すことは、よりGoらしい慣習に沿っています。これにより、呼び出し元のコード(特にmain.goのbuildHashやbuildSubrepo)での条件分岐がif err != nil || !okのように簡潔になり、可読性が向上しました。
-
ビルド結果の明示的な記録:
buildHashメソッドの最後に、fmt.Fprintf(w, "Build complete, duration %v. Result: %v\\n", runTime, errf())という行が追加されました。- これは、ビルドの完了時間と最終的な結果(成功、失敗、エラー)を、
io.MultiWriterを通じてログファイルとメモリバッファの両方に明示的に書き込むものです。これにより、ログの末尾を見るだけでビルドの最終ステータスがすぐにわかるようになり、デバッグが容易になります。
これらの変更は、Goのビルドダッシュボードの信頼性とデバッグ能力を向上させるための重要なステップであり、特にビルダの失敗時に詳細な情報を効率的に収集・報告するための基盤を強化しています。
関連リンク
- 元の変更リスト (Gerrit Change-Id): https://golang.org/cl/7303090
- コミットメッセージで参照されているビルドログの例: http://build.golang.org/log/e7b5bf435b4de1913fc61781b3295fb3f03aeb6e
参考にした情報源リンク
- この解説は、提供されたコミット情報とGo言語の標準ライブラリに関する一般的な知識に基づいて作成されました。外部のウェブ検索は特に使用していません。