[インデックス 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言語の標準ライブラリに関する一般的な知識に基づいて作成されました。外部のウェブ検索は特に使用していません。