[インデックス 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 プロセスのカレントワーキングディレクトリに直接作成し、そこでコンパイル・実行していました。このアプローチにはいくつかの問題がありました。
- 一時ファイルの散乱と衝突: 複数のユーザーが同時にコードを実行した場合、一時ファイルの名前が衝突する可能性があり、予期せぬエラーや誤った結果を引き起こす可能性がありました。また、
goplayプロセスが異常終了した場合、一時ファイルがクリーンアップされずに残り、ディスクスペースを消費したり、ディレクトリを汚染したりする可能性がありました。 - セキュリティと分離の欠如: ユーザーが提供したコードが
goplayプロセスのカレントワーキングディレクトリで直接実行されるため、潜在的なセキュリティリスクがありました。悪意のあるコードがファイルシステムにアクセスしたり、既存のファイルを変更したりする可能性を完全に排除できませんでした。 - エラーメッセージの不親切さ: コンパイルエラーや実行時エラーが発生した場合、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 に分離し、その中で一時ディレクトリを積極的に利用するようにしたことです。
-
一時ディレクトリの利用:
init()関数内でfilepath.EvalSymlinks(os.TempDir())を呼び出し、システムの実際の一時ディレクトリのパスを取得しています。これにより、シンボリックリンクを解決した真のパスが使用され、エラーメッセージの書き換えが正確に行えるようになります。compile関数内で、filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq))を使用して、一時ディレクトリ内にユニークな名前の一時ファイルパスを生成します。これにより、複数のリクエストが同時に処理されてもファイル名の衝突を防ぎ、各実行が独立した環境で行われるようになります。- 生成されたGoソースファイル (
.go) とコンパイルされたバイナリファイル (.exefor Windows) は、この一時ディレクトリ内に作成されます。
-
エラーメッセージの整形:
compile関数内にはdeferを用いた匿名関数があり、コンパイルまたは実行エラーが発生した場合に、出力されるエラーメッセージを整形します。commentRe.ReplaceAll(out, nil)は、Goツールが出力する# _/compile0のようなコメント行を削除します。bytes.Replace(out, []byte(src+":"), []byte("main.go:"), -1)は、エラーメッセージ内の実際の一時ファイルパス(例:/tmp/compile12345.go:) を、ユーザーにとってより分かりやすいmain.go:に置換します。これにより、ユーザーは自分のコードがmain.goであるかのようにエラーを解釈できます。
-
run関数の改善:run関数は、外部コマンドを実行するためのヘルパー関数です。以前はコマンドと引数のみを受け取っていましたが、このコミットでdir string引数が追加されました。cmd.Dir = dirを設定することで、run関数は指定されたディレクトリでコマンドを実行できるようになりました。これは、go buildコマンドを一時ディレクトリ内で実行するために不可欠です。- コマンドの標準出力と標準エラーを
bytes.Bufferにリダイレクトするように変更されました (cmd.Stdout = &buf,cmd.Stderr = cmd.Stdout)。これにより、CombinedOutput()を使用するよりも柔軟かつ効率的にすべての出力をキャプチャできます。
-
一時ファイルの確実なクリーンアップ:
compile関数内で、defer os.Remove(src)とdefer os.Remove(bin)が追加されました。これにより、関数が終了する際に、作成された一時ソースファイルと一時バイナリファイルが確実に削除されます。これは、エラーが発生した場合でもクリーンアップが行われるため、リソースリークを防ぎます。
これらの変更により、goplay はより堅牢で、安全で、ユーザーフレンドリーなツールになりました。
コアとなるコードの変更箇所
変更は misc/goplay/goplay.go ファイルに集中しています。
-
Compile関数の変更 (行 63-96):- 以前の
Compile関数内のコンパイルと実行ロジックが削除され、新しく導入されたcompileヘルパー関数を呼び出すように変更されました。
- 以前の
-
新しい
compileヘルパー関数の追加 (行 80-107):- この関数が、一時ディレクトリの管理、ソースファイルの書き込み、Goプログラムのビルドと実行、およびエラーメッセージの整形という、このコミットの主要なロジックをすべてカプセル化しています。
bytes、path/filepath、regexp、runtimeパッケージが新しくインポートされています。commentReとtmpdirというグローバル変数が追加され、init()関数でtmpdirが初期化されます。
-
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.Joinとstrconv.Itoa(<-uniq)を組み合わせて、毎回ユニークな一時ファイル名を生成します。これにより、同時リクエストによるファイル名衝突のリスクが排除されます。 - エラー出力の整形:
deferを使った匿名関数は、compile関数がリターンする直前に実行されます。これにより、go buildやgo 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 = &bufとcmd.Stderr = cmd.Stdoutを設定することで、すべての出力が単一のバッファに書き込まれ、より柔軟な出力処理が可能になります。
これらの変更により、goplay はより堅牢で、安全で、ユーザーフレンドリーなツールになりました。
関連リンク
- Go言語公式ウェブサイト: https://go.dev/
- Go Playground: https://go.dev/play/ (このコミットが関連するツールの実例)
参考にした情報源リンク
- Go言語の
osパッケージドキュメント: https://pkg.go.dev/os - Go言語の
io/ioutilパッケージドキュメント: https://pkg.go.dev/io/ioutil (Go 1.16以降はosおよびioパッケージに統合されていますが、当時のコードでは使用されていました) - Go言語の
path/filepathパッケージドキュメント: https://pkg.go.dev/path/filepath - Go言語の
os/execパッケージドキュメント: https://pkg.go.dev/os/exec - Go言語の
bytesパッケージドキュメント: https://pkg.go.dev/bytes - Go言語の
regexpパッケージドキュメント: https://pkg.go.dev/regexp - Go言語の
deferステートメントに関する公式ドキュメント: https://go.dev/tour/flowcontrol/12