[インデックス 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
) とコンパイルされたバイナリファイル (.exe
for 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