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のひとつ前のバージョン)で、これまでの動きを確認してみます。パターンとして
- ダブルクォートが必要ない場合:
Win32Echo.exe hoge fuga
- コマンドが空白を含む場合:
"C:\tekito folder\Win32Echo.exe" hoge fuga
- 引数が空白を含む場合:
Win32Echo.exe hoge="AB" fuga="C D" "E F"
- コマンドも引数も空白を含む場合
"C:\tekito folder\Win32Echo.exe" hoge="AB" fuga="C D" "E F"
の4つを考えます。
こんなかんじでSpockでテストを書きました:
(等幅フォントがない環境でも綺麗に見られるように画像にしています)
実行してみると、期待どおり動作します。
Update21での変更 - Runtime.exec(String[])
さて、Update21での変更を確認します。Runtime.exec(String)
で空白を含むコマンドは使えなくなってるから、Runtime.exec(String[])
使ってね、ということでした。先ほどのテストを動かして確認してみます。
そのとおりですね。
では、コマンドと引数を配列に分割して渡すようにしてみましょうか。
ダメですね。分割した場合、引数の 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)
を試してみます。
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に求めるものって「固さ」なんですよね。今回の件は、互換性を失いかねない変更やその後の修正があまりにも軽く行われているようで、調査しながらなんだかちょっと残念な気分になったのでした。