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

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

このコミットは、Go言語のプレイグラウンドツールである misc/goplay の動作を改善するためのものです。具体的には、ユーザーが入力したGoコードのコンパイルと実行を、現在の作業ディレクトリではなく一時ディレクトリで行うように変更し、同時にエラーメッセージの表示をよりユーザーフレンドリーにするための修正が含まれています。これにより、goplay サーバーの安定性とセキュリティが向上し、一時ファイルの管理がより適切に行われるようになります。

コミット

commit 041edbcc79ff6922436bc04cff6f8a7fe96566e0
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Feb 21 11:24:29 2012 +1100

    misc/goplay: remain in work directory, build in temp directory
    
    Fixes #2935.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5684048

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

https://github.com/golang/go/commit/041edbcc79ff6922436bc04cff6f8a7fe96566e0

元コミット内容

misc/goplay: remain in work directory, build in temp directory

このコミットは、goplay ツールがその作業ディレクトリに留まりつつ、ビルドプロセスを一時ディレクトリで行うように変更します。これにより、goplay の動作がよりクリーンで安全になります。

Fixes #2935.

このコミットは、Goプロジェクトの課題トラッカーにおける問題 #2935 を修正します。この問題の詳細は、公開されているGitHubのIssueとは異なる可能性があり、内部的なトラッカーを参照している可能性があります。しかし、コードの変更内容から、一時ファイルの管理とエラーメッセージの改善に関する問題であったと推測されます。

変更の背景

goplay は、ユーザーがGoコードをブラウザ上で記述し、その場でコンパイル・実行結果を確認できるウェブアプリケーションです。以前の実装では、ユーザーから送信されたGoコードを一時ファイルとして goplay プロセスのカレントワーキングディレクトリに直接作成し、そこでコンパイル・実行していました。このアプローチにはいくつかの問題がありました。

  1. 一時ファイルの散乱と衝突: 複数のユーザーが同時にコードを実行した場合、一時ファイルの名前が衝突する可能性があり、予期せぬエラーや誤った結果を引き起こす可能性がありました。また、goplay プロセスが異常終了した場合、一時ファイルがクリーンアップされずに残り、ディスクスペースを消費したり、ディレクトリを汚染したりする可能性がありました。
  2. セキュリティと分離の欠如: ユーザーが提供したコードが goplay プロセスのカレントワーキングディレクトリで直接実行されるため、潜在的なセキュリティリスクがありました。悪意のあるコードがファイルシステムにアクセスしたり、既存のファイルを変更したりする可能性を完全に排除できませんでした。
  3. エラーメッセージの不親切さ: コンパイルエラーや実行時エラーが発生した場合、Goツールが出力するメッセージには、一時ファイルの絶対パス(例: /tmp/goplay12345.go:) が含まれていました。これはユーザーにとって意味のない情報であり、デバッグの妨げになる可能性がありました。

このコミットは、これらの問題を解決するために、コンパイルと実行のプロセスをシステムの一時ディレクトリに完全に隔離し、エラーメッセージをユーザーフレンドリーに整形することで、goplay の堅牢性、セキュリティ、およびユーザーエクスペリエンスを向上させることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の知識が役立ちます。

  • Go言語の基本: Goプログラムの構造、パッケージ、コンパイルと実行の基本的な流れ (go build, go run コマンド)。
  • HTTPとWebサーバー: HTTPリクエストとレスポンスの概念、およびGo言語でWebサーバーを構築する際の基本的なパターン(net/http パッケージ)。goplay はWebアプリケーションとして動作します。
  • ファイルシステム操作: Goにおけるファイルやディレクトリの作成、読み書き、削除、パスの結合や分割といった基本的なファイルシステム操作(os, io/ioutil, path/filepath パッケージ)。特に、一時ディレクトリの概念と、os.TempDir() のような関数がどのように利用されるかを理解することが重要です。
  • 外部プロセスの実行: Goプログラムから外部コマンド(例: go build, go run)を実行する方法(os/exec パッケージ)。コマンドの標準出力や標準エラーをキャプチャする方法も関連します。
  • defer ステートメント: Go言語の defer キーワードは、関数がリターンする直前に指定された関数を実行することを保証します。これは、リソースのクリーンアップ(ファイルのクローズ、一時ファイルの削除など)に非常に便利です。
  • bytes.Buffer: bytes.Buffer は、可変長のバイトシーケンスを扱うためのGoの型です。効率的なバイト操作や、I/O操作のバッファとしてよく使用されます。このコミットでは、外部コマンドの出力をキャプチャするために使用されています。
  • 正規表現: テキストパターンマッチングのための正規表現の基本的な知識(regexp パッケージ)。このコミットでは、エラーメッセージから不要な部分を削除したり、パスを置換したりするために使用されています。

技術的詳細

このコミットの主要な技術的変更点は、Goコードのコンパイルと実行のロジックを Compile 関数から新しいヘルパー関数 compile に分離し、その中で一時ディレクトリを積極的に利用するようにしたことです。

  1. 一時ディレクトリの利用:

    • init() 関数内で filepath.EvalSymlinks(os.TempDir()) を呼び出し、システムの実際の一時ディレクトリのパスを取得しています。これにより、シンボリックリンクを解決した真のパスが使用され、エラーメッセージの書き換えが正確に行えるようになります。
    • compile 関数内で、filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq)) を使用して、一時ディレクトリ内にユニークな名前の一時ファイルパスを生成します。これにより、複数のリクエストが同時に処理されてもファイル名の衝突を防ぎ、各実行が独立した環境で行われるようになります。
    • 生成されたGoソースファイル (.go) とコンパイルされたバイナリファイル (.exe for Windows) は、この一時ディレクトリ内に作成されます。
  2. エラーメッセージの整形:

    • compile 関数内には defer を用いた匿名関数があり、コンパイルまたは実行エラーが発生した場合に、出力されるエラーメッセージを整形します。
    • commentRe.ReplaceAll(out, nil) は、Goツールが出力する # _/compile0 のようなコメント行を削除します。
    • bytes.Replace(out, []byte(src+":"), []byte("main.go:"), -1) は、エラーメッセージ内の実際の一時ファイルパス(例: /tmp/compile12345.go:) を、ユーザーにとってより分かりやすい main.go: に置換します。これにより、ユーザーは自分のコードが main.go であるかのようにエラーを解釈できます。
  3. run 関数の改善:

    • run 関数は、外部コマンドを実行するためのヘルパー関数です。以前はコマンドと引数のみを受け取っていましたが、このコミットで dir string 引数が追加されました。
    • cmd.Dir = dir を設定することで、run 関数は指定されたディレクトリでコマンドを実行できるようになりました。これは、go build コマンドを一時ディレクトリ内で実行するために不可欠です。
    • コマンドの標準出力と標準エラーを bytes.Buffer にリダイレクトするように変更されました (cmd.Stdout = &buf, cmd.Stderr = cmd.Stdout)。これにより、CombinedOutput() を使用するよりも柔軟かつ効率的にすべての出力をキャプチャできます。
  4. 一時ファイルの確実なクリーンアップ:

    • compile 関数内で、defer os.Remove(src)defer os.Remove(bin) が追加されました。これにより、関数が終了する際に、作成された一時ソースファイルと一時バイナリファイルが確実に削除されます。これは、エラーが発生した場合でもクリーンアップが行われるため、リソースリークを防ぎます。

これらの変更により、goplay はより堅牢で、安全で、ユーザーフレンドリーなツールになりました。

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

変更は misc/goplay/goplay.go ファイルに集中しています。

  1. Compile 関数の変更 (行 63-96):

    • 以前の Compile 関数内のコンパイルと実行ロジックが削除され、新しく導入された compile ヘルパー関数を呼び出すように変更されました。
  2. 新しい compile ヘルパー関数の追加 (行 80-107):

    • この関数が、一時ディレクトリの管理、ソースファイルの書き込み、Goプログラムのビルドと実行、およびエラーメッセージの整形という、このコミットの主要なロジックをすべてカプセル化しています。
    • bytespath/filepathregexpruntime パッケージが新しくインポートされています。
    • commentRetmpdir というグローバル変数が追加され、init() 関数で tmpdir が初期化されます。
  3. run 関数の変更 (行 146-154):

    • 関数シグネチャが func run(cmd ...string) ([]byte, error) から func run(dir string, args ...string) ([]byte, error) に変更され、コマンドを実行するディレクトリを指定できるようになりました。
    • cmd.Dir = dir が追加され、コマンドの実行ディレクトリが設定されます。
    • コマンドの出力キャプチャ方法が CombinedOutput() から bytes.Buffer を使用する方法に変更されました。

コアとなるコードの解説

Compile 関数の変更

// 変更前
func Compile(w http.ResponseWriter, req *http.Request) {
	// ... 一時ファイルの作成、書き込み、go run の実行ロジック ...
}

// 変更後
func Compile(w http.ResponseWriter, req *http.Request) {
	out, err := compile(req) // 新しい compile ヘルパー関数を呼び出す
	if err != nil {
		error_(w, out, err)
		return
	}
	w.Write(out)
}

Compile 関数は、HTTPリクエストを受け取り、Goコードをコンパイル・実行して結果を返すという役割は変わりませんが、その内部実装が大幅に簡素化されました。以前のバージョンでは、この関数が直接一時ファイルの作成、go run コマンドの実行、エラー処理を行っていましたが、新しいバージョンではすべての複雑なロジックを compile ヘルパー関数に委譲しています。これにより、Compile 関数はより読みやすく、その役割が明確になりました。

新しい compile ヘルパー関数

var (
	commentRe = regexp.MustCompile(`(?m)^#.*\\n`)
	tmpdir    string
)

func init() {
	var err error
	tmpdir, err = filepath.EvalSymlinks(os.TempDir())
	if err != nil {
		log.Fatal(err)
	}
}

func compile(req *http.Request) (out []byte, err error) {
	// 1. 一時ファイルパスの生成
	x := filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq))
	src := x + ".go"
	bin := x
	if runtime.GOOS == "windows" {
		bin += ".exe"
	}

	// 2. エラー出力の整形 (defer で遅延実行)
	defer func() {
		if err != nil {
			out = commentRe.ReplaceAll(out, nil) // Goツールのコメントを削除
		}
		out = bytes.Replace(out, []byte(src+":"), []byte("main.go:"), -1) // 一時パスを main.go に置換
	}()

	// 3. リクエストボディ (Goコード) を一時ファイルに書き込み
	body := new(bytes.Buffer)
	if _, err = body.ReadFrom(req.Body); err != nil {
		return
	}
	defer os.Remove(src) // 処理終了時に一時ソースファイルを削除
	if err = ioutil.WriteFile(src, body.Bytes(), 0666); err != nil {
		return
	}

	// 4. Goコードをビルド (一時ディレクトリ内で実行)
	dir, file := filepath.Split(src)
	out, err = run(dir, "go", "build", "-o", bin, file) // run 関数に dir を渡す
	defer os.Remove(bin) // 処理終了時に一時バイナリファイルを削除
	if err != nil {
		return
	}

	// 5. ビルドされたバイナリを実行 (goplay の作業ディレクトリで実行)
	return run("", bin) // run 関数に空の dir を渡す (カレントワーキングディレクトリ)
}

この compile 関数は、このコミットの心臓部です。

  • init()tmpdir: init() 関数はプログラム起動時に一度だけ実行され、システムの真の一時ディレクトリパスを tmpdir グローバル変数に保存します。これにより、後続の処理で正確な一時ディレクトリが利用されます。
  • 一時ファイルパスの生成: filepath.Joinstrconv.Itoa(<-uniq) を組み合わせて、毎回ユニークな一時ファイル名を生成します。これにより、同時リクエストによるファイル名衝突のリスクが排除されます。
  • エラー出力の整形: defer を使った匿名関数は、compile 関数がリターンする直前に実行されます。これにより、go buildgo run からのエラー出力に含まれる一時ファイルパスを main.go: に置換し、ユーザーにとって分かりやすいエラーメッセージを提供します。また、Goツールが出力する内部的なコメントも削除されます。
  • ソースコードの書き込みとクリーンアップ: リクエストボディから読み取ったGoコードは、ioutil.WriteFile を使って一時ソースファイル (.go) に書き込まれます。defer os.Remove(src) により、関数終了時にこの一時ファイルが自動的に削除されることが保証されます。
  • ビルドと実行の分離:
    • go build コマンドは、run(dir, "go", "build", "-o", bin, file) のように、一時ソースファイルが存在するディレクトリ (dir) で実行されます。これにより、ビルドプロセスが goplay のメインプロセスから隔離されます。
    • ビルドが成功すると、生成されたバイナリファイル (bin) は run("", bin) によって実行されます。ここで dir が空文字列であるため、バイナリは goplay プロセスが起動しているカレントワーキングディレクトリで実行されます。これは、goplay がその作業ディレクトリに留まるというコミットメッセージの意図を反映しています。
  • バイナリのクリーンアップ: defer os.Remove(bin) により、実行された一時バイナリファイルも関数終了時に自動的に削除されます。

run 関数の変更

// 変更前
func run(cmd ...string) ([]byte, error) {
	return exec.Command(cmd[0], cmd[1:]...).CombinedOutput()
}

// 変更後
func run(dir string, args ...string) ([]byte, error) {
	var buf bytes.Buffer
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = dir // コマンドの実行ディレクトリを設定
	cmd.Stdout = &buf
	cmd.Stderr = cmd.Stdout // 標準出力と標準エラーを同じバッファにリダイレクト
	err := cmd.Run()
	return buf.Bytes(), err
}

run 関数は、外部コマンドを実行し、その出力をバイトスライスとして返す汎用的なヘルパー関数です。

  • dir 引数の追加: 最も重要な変更は、dir string 引数が追加されたことです。これにより、呼び出し元はコマンドを実行する作業ディレクトリを指定できるようになりました。これは、compile 関数が go build を一時ディレクトリで実行するために不可欠です。
  • 出力キャプチャの改善: 以前は CombinedOutput() を使用していましたが、新しい実装では bytes.Buffer を使用して標準出力と標準エラーの両方をキャプチャします。cmd.Stdout = &bufcmd.Stderr = cmd.Stdout を設定することで、すべての出力が単一のバッファに書き込まれ、より柔軟な出力処理が可能になります。

これらの変更により、goplay はより堅牢で、安全で、ユーザーフレンドリーなツールになりました。

関連リンク

参考にした情報源リンク