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

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

このコミットは、Go言語のディストリビューションツール(dist)におけるWindows環境での特定のバグ修正に関するものです。具体的には、fatal()関数が呼び出された際に発生する可能性のある再帰ループを防ぐことを目的としています。

コミット

commit ef1d2a32ea0b096b1aca9f1bda096f0718f6a2ef
Author: Daniel Theophanes <kardianos@gmail.com>
Date:   Thu Feb 9 23:10:27 2012 -0500

    dist: prevent recusive loop on windows when fatal() is called.
    
    Fixes #2931.
    
    R=golang-dev, alex.brainman
    CC=golang-dev, rsc
    https://golang.org/cl/5651049

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

https://github.com/golang/go/commit/ef1d2a32ea0b096b1aca9f1bda096f0718f6a2ef

元コミット内容

dist: prevent recusive loop on windows when fatal() is called. Fixes #2931.

変更の背景

このコミットは、Go言語のIssue #2931を修正するために行われました。Goのビルドプロセスで使用されるdistツールは、バックグラウンドでプロセスを実行し、その終了を待機する機能を持っています。Windows環境において、バックグラウンドプロセスの終了コードを取得する際にエラーが発生し、そのエラーをfatal()関数で処理しようとすると、特定の条件下で再帰的なループに陥る可能性がありました。

具体的には、bgwait1関数内でGetExitCodeProcessが失敗した場合、fatal()が呼び出されます。このfatal()の呼び出しが、さらにバックグラウンドジョブのクリーンアップ処理をトリガーし、そのクリーンアップ処理が再びエラーを引き起こすことで、無限ループが発生する可能性があったと考えられます。このような状況は、システムの安定性を損ない、ビルドプロセスを停止させる原因となります。

前提知識の解説

  • distツール: Go言語のソースコードからGoのツールチェイン(コンパイラ、リンカなど)をビルドするために使用される内部ツールです。Goのビルドシステムの中核をなす部分の一つです。
  • fatal()関数: プログラムの実行を致命的なエラーで終了させるための関数です。通常、エラーメッセージを出力し、プログラムを異常終了させます。
  • Windows API GetExitCodeProcess: 指定されたプロセスの終了コードを取得するために使用されるWindows API関数です。プロセスがまだ実行中の場合、STILL_ACTIVEが返されます。
  • CloseHandle: Windows API関数で、開いているオブジェクトハンドルを閉じます。プロセスハンドルやスレッドハンドルなど、システムリソースへの参照を解放するために重要です。ハンドルを閉じないと、リソースリークが発生する可能性があります。
  • バックグラウンドジョブ: メインのプログラム実行とは独立して、並行して実行されるタスクやプロセスを指します。distツールでは、コンパイルやリンクなどのタスクをバックグラウンドで実行することがあります。
  • 再帰ループ: プログラムが自分自身を繰り返し呼び出し、終了条件が満たされないために無限に実行され続ける状態を指します。これは通常、プログラミングエラーによって引き起こされ、スタックオーバーフローやシステムリソースの枯渇につながります。

技術的詳細

このコミットの核心は、bgwait1関数におけるエラーハンドリングの改善と、バックグラウンドジョブのハンドルクローズ処理の分離です。

変更前は、bgwait1関数内でGetExitCodeProcessが失敗した場合、またはバックグラウンドジョブが非ゼロの終了コードを返した場合にfatal()が呼び出されていました。このfatal()の呼び出しの直後に、CloseHandleでプロセスハンドルとスレッドハンドルを閉じ、bg配列から該当するジョブを削除する処理が行われていました。

問題は、fatal()が呼び出された際に、その後のCloseHandleや配列操作が適切に実行されない、あるいはfatal()の内部処理がさらにbgwait1を呼び出すような状況(再帰)を引き起こす可能性があった点です。特に、fatal()が呼び出されるような致命的なエラー状況下では、リソースの解放が不完全になり、それがさらなる問題を引き起こすことが考えられます。

このコミットでは、以下の変更が導入されました。

  1. bgwaitclose関数の導入: バックグラウンドジョブのプロセスハンドルとスレッドハンドルを閉じ、bg配列からジョブを削除する処理をbgwaitcloseという新しい静的関数に分離しました。これにより、リソース解放のロジックがカプセル化され、再利用性が向上します。
  2. エラー発生時の即時クリーンアップとリターン: bgwait1関数内でGetExitCodeProcessが失敗した場合、またはmode==CheckExitで非ゼロの終了コードが返された場合、fatal()を呼び出す前にbgwaitclose(i)を呼び出すように変更されました。そして、fatal()の呼び出し後にはreturn;を追加し、それ以上bgwait1関数内の処理が続行されないようにしました。

この変更により、fatal()が呼び出されるような致命的なエラーが発生した場合でも、関連するプロセスハンドルとスレッドハンドルが確実に閉じられ、リソースリークや再帰ループの原因となる可能性のある状態が回避されます。fatal()がプログラムを終了させる前に、最低限のクリーンアップが行われるようになったため、より堅牢なエラーハンドリングが実現されました。

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

変更はsrc/cmd/dist/windows.cファイルに集中しています。

--- a/src/cmd/dist/windows.c
+++ b/src/cmd/dist/windows.c
@@ -371,6 +371,19 @@ genrun(Buf *b, char *dir, int mode, Vec *argv, int wait)
 	bfree(&cmd);
 }
 
+// closes the background job for bgwait1
+static void
+bgwaitclose(int i)
+{
+	if(i < 0 || i >= nbg)
+		return;
+
+	CloseHandle(bg[i].pi.hProcess);
+	CloseHandle(bg[i].pi.hThread);
+	
+	bg[i] = bg[--nbg];
+}
+
 // bgwait1 waits for a single background job
 static void
 bgwait1(void)
@@ -391,14 +404,19 @@ bgwait1(void)
 
 	cmd = bg[i].cmd;
 	mode = bg[i].mode;
-\tif(!GetExitCodeProcess(bg[i].pi.hProcess, &code))\n+\tif(!GetExitCodeProcess(bg[i].pi.hProcess, &code)) {\n+\t\tbgwaitclose(i);\n \t\tfatal(\"GetExitCodeProcess: %s\", errstr());\n-\tif(mode==CheckExit && code != 0)\n+\t\treturn;\n+\t}\n+\n+\tif(mode==CheckExit && code != 0) {\n+\t\tbgwaitclose(i);\n \t\tfatal(\"FAILED: %s\", cmd);\n-\tCloseHandle(bg[i].pi.hProcess);\n-\tCloseHandle(bg[i].pi.hThread);\n+\t\treturn;\n+\t}\n \n-\tbg[i] = bg[--nbg];\n+\tbgwaitclose(i);\n }
 
 void

コアとなるコードの解説

  1. bgwaitclose関数の追加:

    • この関数は、引数iで指定されたバックグラウンドジョブのプロセスハンドルとスレッドハンドルを閉じ、bg配列からそのジョブを削除する責任を持ちます。
    • if(i < 0 || i >= nbg): 無効なインデックスが渡された場合の境界チェックです。
    • CloseHandle(bg[i].pi.hProcess);CloseHandle(bg[i].pi.hThread);: PROCESS_INFORMATION構造体に含まれるプロセスハンドルとスレッドハンドルを閉じます。これにより、OSリソースが解放されます。
    • bg[i] = bg[--nbg];: 削除対象のジョブを配列の末尾のジョブで上書きし、nbg(バックグラウンドジョブの数)をデクリメントすることで、配列から論理的に削除します。これは、配列の要素を詰める一般的な手法です。
  2. bgwait1関数の変更:

    • if(!GetExitCodeProcess(bg[i].pi.hProcess, &code)) { ... }: GetExitCodeProcessが失敗した場合の処理です。
      • bgwaitclose(i);: エラーが発生した直後に、該当するバックグラウンドジョブのリソースを解放します。
      • fatal("GetExitCodeProcess: %s", errstr());: 致命的なエラーとして報告します。
      • return;: fatal()が呼び出された後、関数を即座に終了させ、それ以上の処理が続行されないようにします。これにより、再帰ループの可能性を排除します。
    • if(mode==CheckExit && code != 0) { ... }: ジョブが非ゼロの終了コードを返した場合(エラー終了)の処理です。
      • bgwaitclose(i);: 同様に、エラー終了の場合もリソースを解放します。
      • fatal("FAILED: %s", cmd);: 失敗したコマンドを報告します。
      • return;: 関数を即座に終了させます。
    • bgwaitclose(i); (変更後): 正常終了した場合も、以前は直接CloseHandleと配列操作を行っていた部分が、新しく導入されたbgwaitclose(i)の呼び出しに置き換えられました。これにより、コードの重複が解消され、保守性が向上しています。

この変更により、エラー発生時でもリソースが確実に解放され、fatal()の呼び出しが再帰ループを引き起こす可能性が排除されました。

関連リンク

  • Go Issue #2931: このコミットが修正したGoのIssueへの直接リンクは、GoのIssueトラッカーの変更により、古い形式のURLでは直接アクセスできない場合があります。しかし、コミットメッセージに明記されているため、このコミットがこのIssueを解決したことは確かです。
  • Go CL 5651049: https://golang.org/cl/5651049 - このコミットのコードレビューページです。レビューの経緯や議論が確認できます。

参考にした情報源リンク

  • Go CL 5651049: https://golang.org/cl/5651049
  • Windows API Documentation (GetExitCodeProcess, CloseHandle): Microsoft Learnなどの公式ドキュメント
  • Go言語のソースコード(src/cmd/dist/windows.c
  • 一般的なGo言語のビルドプロセスとdistツールの役割に関する知識