Runtime.execの迷走

パッケージ製品開発担当の大です。こんにちは。
師走に入り、すっかり寒くなりましたね。

今回は、JavaのRumtime.execのお話をしようと思います。
ことの起こりは、今年4月のJDK7 Update21でのこの変更でした。

http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html#jruntime

On Windows platform, the decoding of command strings specified to Runtime.exec(String), Runtime.exec(String,String[]) and Runtime.exec(String,String[],File) methods, has been improved to follow the specification more closely. This may cause problems for applications that are using one or more of these methods with commands that contain spaces in the program name, or are invoking these methods with commands that are not quoted correctly.

For example, Runtime.getRuntime().exec("C:\\My Programs\\foo.exe bar") is an attempt to launch the program "C:\\My" with the arguments "Programs\\foo.exe" and "bar". This command is likely to fail with an exception to indicate "C:\My" cannot be found.

The example Runtime.getRuntime().exec("\"C:\\My Programs\\foo.exe\" bar") is an attempt to launch the program "\"C:\\My". This command will fail with an exception to indicate the program has an embedded quote.

Applications that need to launch programs with spaces in the program name should consider using the variants of Runtime.exec that allow the command and arguments to be specified in an array.

Windows上でのRuntime.execの動きが変わったということで、結構あちこちで話題になっていましたので、ご覧になった方も多いんじゃないでしょうか。

まぁ見るからにやばそうな変更で、実際そのとおりでした。

前準備

Runtime.execの動きがどう変わったのか。
検証する前準備として、以下のようなWin32アプリケーションを用意しました。
これをRuntime.execで起動して、動きをみようという目論見です。

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                       _In_opt_ HINSTANCE hPrevInstance,
                       _In_ LPTSTR lpCmdLine,
                       _In_ int nCmdShow)
{
    _tsetlocale( LC_ALL, _T(""));
    _tprintf(_T("{%s}\n"), lpCmdLine);
    return EXIT_SUCCESS;
}

渡されたコマンドライン引数を"{"と"}"で囲って標準出力に出力するだけです。
実行してみるとこんなかんじになります。

>Win32Echo.exe hoge あいう tekito="fuga" | more
{hoge あいう tekito="fuga" }

(ここでmoreにパイプしてるのは、Win32アプリはそのままだと標準出力をコンソールに吐かないからです)

コマンドライン引数がダブルクォートまで含めてそのままアプリケーションに渡ってきているのがわかりますね。.NETでは事情が違いますが、Win32ではコマンドライン引数のパースはこのようにアプリケーションに丸投げされていたのです。このため、Win32アプリに引数を渡す場合には、ダブルクォートまで含めて正確に渡さないと意図したとおり動かない可能性があるのです。それが良いか悪いかはまた別の話。

このへんの話も参考になります。

さて、このexeを c:\tekito folder\ という「空白を含む」フォルダにコピーしておきます。また、このフォルダを環境変数PATHに追加して、絶対パス指定じゃなくてもexeが動くようにしておきます。これで準備完了です。

Update17以前での動き

まずはUpdate17(21のひとつ前のバージョン)で、これまでの動きを確認してみます。パターンとして

  1. ダブルクォートが必要ない場合:
    Win32Echo.exe hoge fuga
  2. コマンドが空白を含む場合:
    "C:\tekito folder\Win32Echo.exe" hoge fuga
  3. 引数が空白を含む場合:
    Win32Echo.exe hoge="AB" fuga="C D" "E F"
  4. コマンドも引数も空白を含む場合
    "C:\tekito folder\Win32Echo.exe" hoge="AB" fuga="C D" "E F"

の4つを考えます。

こんなかんじでSpockでテストを書きました:

動作確認

動作確認(クリックで拡大)

(等幅フォントがない環境でも綺麗に見られるように画像にしています)
実行してみると、期待どおり動作します。

Update17以前での実行結果

Update17以前での実行結果(クリックで拡大)

Update21での変更 - Runtime.exec(String[])

さて、Update21での変更を確認します。Runtime.exec(String)で空白を含むコマンドは使えなくなってるから、Runtime.exec(String[])使ってね、ということでした。先ほどのテストを動かして確認してみます。

Update21での実行結果

Update21での実行結果(クリックで拡大)

そのとおりですね。

では、コマンドと引数を配列に分割して渡すようにしてみましょうか。

コマンドと引数を配列で渡す

コマンドと引数を配列で渡す(クリックで拡大)

実行結果

実行結果(クリックで拡大)

ダメですね。分割した場合、引数の hoge="AB" "E F" は大丈夫ですが、fuga="C D""fuga="C D"" と、余計なダブルクォートで囲まれて渡されてしまっています。

実はこの動作はUpdate17以前でも同じでした。このためにRuntime.exec(String [])を使わず、Runtime.exec(String)を使っていた人も多いのではないでしょうか。

要するに、Update21では、

Runtime.exec(String) Runtime.exec(String[])
ダブルクォートが必要ない場合
コマンドが空白を含む場合
引数が空白を含む場合
コマンドも引数も空白を含む場合

ということで、「コマンドも引数も空白を含む場合」には動作させる手段がなくなってしまいました。困りましたね。

Update25での変更 - allowAmbigousCommandsとセキュリティマネージャ

4月のUpdate21のリリースにつづいて、6月にUpdate25がリリースされました。ここでもRuntime.execがリリースノートに載っています:

要約すると、jdk.lang.Process.allowAmbigousCommandsというプロパティが追加されました。trueに設定すると、昔の動作に戻します。ただしSecurityManager不使用の場合のみ動作します。

では、このプロパティの有・無を追加して、Runtime.exec(String)を試してみます。

allowAmbigousCommandsとセキュリティマネージャ

allowAmbigousCommandsとセキュリティマネージャ(クリックで拡大)

Update25での実行結果

Update25での実行結果(クリックで拡大)

SecurityManager不使用かつ allowAmbigousCommands=true SecurityManagerを使用しているか、allowAmbigousCommands=falseまたは未指定
ダブルクォートが必要ない場合
コマンドが空白を含む場合
引数が空白を含む場合
コマンドも引数も空白を含む場合

というわけで、Update25でRuntime.exec(String)使う場合、SecurityManagerが指定されていない環境なら、allowAmbigousCommands=trueを指定すればUpdate17以前のように動くようになりました。

Update40での変更 - スペルミスとデフォルト値の変更

さて、Update25で導入されたallowAmbigousCommandsですが、リリース後にこんなチケットがあがりました。

・・・スペルミス!?
これにより、プロパティ名がallowAmbigousCommandsからallowAmbiguousCommandsに変更されました。
allowAmbigousCommandsに値を設定しても無視されます。

また、

ということで、allowAmbigousCommandsのデフォルト値はfalseだったのが、allowAmbiguousCommandsのデフォルト値は(SecurityManager不使用の場合は)trueになりました。

プロパティ名の変更

プロパティ名の変更(クリックで拡大)

実行結果

実行結果(クリックで拡大)

SecurityManager不使用かつ allowAmbiguousCommands=trueまたは未指定 SecurityManagerを使用しているか、allowAmbiguousCommands=false
ダブルクォートが必要ない場合
コマンドが空白を含む場合
引数が空白を含む場合
コマンドも引数も空白を含む場合

これらの変更が9月にリリースされたUpdate40で入ったのですが、今回はリリースノートには書かれておらず、バグフィックスとしてさらっと掲載されているだけなので、見落としている人も多いかもしれません。

最後に

というわけで、いくつかの変遷はありましたが、Update40以降はSecurityManagerが設定されていなければデフォルトでUpdate17以前の動作をするようになりました。もしUpdate21や25で解決できない問題にぶつかったら、素直にUpdate40以降にアップデートするのが良いと思います。

Java界隈は最近、言語仕様やライブラリも他の言語に負けないように急ピッチで機能強化されていっているように思います。それはそれで嬉しいんですが、やっぱり自分個人としてはJavaに求めるものって「固さ」なんですよね。今回の件は、互換性を失いかねない変更やその後の修正があまりにも軽く行われているようで、調査しながらなんだかちょっと残念な気分になったのでした。