System#exitを呼び出してもVMを終了させない方法

こんな感じででけた。もしこのコードを試すのであれば、ラーニングテストなので、適宜org.junit.Testをインポートしてね。

@Test
public void exitしないことを確認しよう() throws Exception {
	System.setSecurityManager(new SecurityManager() {
		@Override
		public void checkExit(int status) {
			throw new SecurityException();
		}
		@Override
		public void checkPermission(final Permission perm) {
		}
		@Override
		public void checkPermission(final Permission perm, final Object context) {
		}
	});
	try {
		//  はーい、ここ注目ですよー。
		System.exit(0);
	} catch (Exception e) {
	}
	System.out.println("System#exitを実行したのに終了しなかったYO!");
}

実行してみると、こんな感じでコンソールに表示されるはず。

System#exitを実行したのに終了しなかったYO!
Exception in thread "main" java.lang.SecurityException
	at learning.Learning$1.checkExit(Learning.java:17)
	at java.lang.Runtime.exit(Runtime.java:88)
	at java.lang.System.exit(System.java:921)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:202)

SecurityExceptionが後に表示されるのは、なぜなんでしょう?そこは分かってないです。なんでこんな事を試したか、というと、アプリケーションの中にスクリプトコードでカスタマイズできる部分を用意し、ユーザに自由にカスタマイズしてもらう事を想定すると、System#exitを実行されてVMが終了されるのは非常に困るからです。実際、JAMCircleのスクリプトコンソールにこんな感じで組み込んでみると、JRubyのコードからSystem.exitを実行しても終了されないようになりました。

SecurityManager securityManager = System.getSecurityManager();
System.setSecurityManager(new SecurityManager() {
	@Override
	public void checkExit(int status) {
		throw new SecurityException();
	}
	@Override
	public void checkPermission(final Permission perm) {
	}
	@Override
	public void checkPermission(final Permission perm, final Object context) {
	}
});
// run ruby code
runtimeAdapter.eval(runtime, script);
// retrieve security manager
System.setSecurityManager(securityManager);

そういうのはpolicyに書くといいよ、というお話もありますが、JAMCircleみたいな単体アプリケーションを配布する事を考えると、ユーザにpolicyを修正して頂く事になるので現実的ではありません。本当はJRuby等のスクリプトコンテナの方で設定できるのがスクリプトコンテナの利用者からするといいと思うんです。なんとなくアプリケーションの責務ってよりも、スクリプトエンジンの責務っぽいですから。ただ、これをこのまま組み込んでしまうと、スクリプトから任意のファイルを触れると思うので、それもセキュリティ上よろしくないですよね。実際に組み込むのはもう少し考える事がありそうです。

追記

コメントでも頂きましたが、上記のコンソールに表示されるスタックとレースは、RemoteTestRunner内でSystem#exitが呼ばれているからでした。

2010/08/13追記

スクリプト実行中に他の操作で終了処理するとプロセスが落ちませんね。ちゃんと呼び出し元を判別する必要があるみたい。

本腰入れてJRuby(その4) スクリプトの世界からJavaの世界へ値を渡すには

Java側から呼び出すときにBSFManager#eval()で呼び出せば、返値として最後に評価したオブジェクトが返却されますが、複数のオブジェクトを返すにはどうするのがいいんだろうと。答えはBSFManager#registerBean(String,Object)で登録し、Javaの方に処理が帰ってきたらBSFManager#lookupBean(String)でいいらしいよ。JRubyから値を返すんだったらこんな感じ。

require 'yaml'
conf = YAML.load(なんかのファイル)
$bsf.registerBean("conf",conf)

本腰入れてJRuby(その3) Apache BSFさんと仲良くなろう

BSFのドキュメントがほとんどなくて困ってうろうろしたので、分かった事を書いていく。
BSFを使ったスクリプトの呼び出しはこんな感じ。

BSFManager manager = new BSFManager();
String hello = "Javaの世界からこんにちは!";
manager.registerBean("hello", hello); // (1)
InputStreamReader reader = new InputStreamReader(this.getClass().getResource("test.rb").openStream());
manager.exec("ruby", "(java)", -1, -1, IOUtils.getStringFromReader(reader));

test.rbは、この処理を実装しているクラスと同じパッケージに置いている事を前提にしています。(ってあたりまえかw)
test.rbの中身はこんな感じ。

hello = $bsf.lookupBean("hello")
puts hello

$bsfというのは、BSFManagerをラップしたBSFFunctionsというクラスの暗黙オブジェクト。BSFManager#registerBeanで登録したBeanをlookupBeanで取得することができます。

BSFManagerにはもう一つ、BSFManager#declareBean(String beanName,Object bean,Class beanClass)というインターフェースがあり、こちらを使うと宣言したbeanはさっきの$bsfからlookupBeanしなくてもそのまま使えます。どういうことかと言うと、

String hello = "Javaの世界からこんにちは!";
// manager.registerBean("hello", hello); // (1)
manager.declareBean("hello", hello,String.class); // (1)をこう置き換える
puts $hello

と言うように呼べます。が、BSFManagerのソースを見てみると、すべてのScriptEngineに対してdeclareBeanで宣言したBeanが登録されていました。局所的に使うBeanを登録するにはちょっとスコープが広すぎるわけです。(Rubyの世界だと'$'がついている変数はグローバルスコープなんすね。Rhinoだと"bsf"のまんまで呼び出せた。他の言語だとどうなんだろう。)

BSFの対応言語はJRuby以外にもJavaScript(Rhino)やJython、Groovyとかがあり、実行時にどの言語かを指定することができます。(Scalaがなくてビックリした)

String source;
// JRuby
manager.exec("ruby", "(java)", -1, -1,source);
// Rhino
manager.exec("javascript", "(java)", -1, -1,source);
// Jython
manager.exec("jython", "(java)", -1, -1,source);
// Groovy
manager.exec("groovy", "(java)", -1, -1,source);

BSFManagerと同じパッケージにあるLanguage.propertiesでScriptEngineとマッピングしているようです。またファイルの拡張子からどの言語か判断して実行するインターフェースもあります。

String langType = BSFManager.getLangFromFilename(filename);

Java6でサポートされたJSR233のインターフェースの方が実行速度的に優位ですが、Java5でも動く環境にするのならBSFを使わないといけません。JRubyのインターフェースを直で叩く事も考えられますが、JRubyの開発者は推奨していません。(そりゃそうだ。)

本腰入れてJRuby その2

java -jar jruby-complete.jar --command gem install rake

ってやるとどうなるか?答えはまずuser.homeの直下に.jrubyというディレクトリが作成されて、そこにrubyの標準ライブラリ等が展開されます。そこをJRUBY_HOMEとして、rakeのライブラリがインストールされるという手筈になっているみたい。簡単にruby 1.8互換のライブラリを使うことができるっつーのはいいね。
JRUBY_HOMEの場所は起動時に、システムプロパティとして

-Djruby.home=パス

追記すればいいらしい。

本腰入れてJRubyを使いたくなってきた。

jruby-complete.jarはrubyのライブラリ一式を含んでいる。mavan-repositoryにも置いてあるけど、JRubyのソースを落としてきて

ant jar-complete

rubygemsによる拡張ができるかどうか、までは調べてないけれど、たぶん環境変数とか、システムプロパティでどうにかできるレベルだと思われる。
それよりも、Rubyのライブラリが同梱されていることの心強さ。これはいい。Javaでこれだけのライブラリを、こんなフットプリントではなかなか手に入らない気がする。Javaってどれだけライブラリのせいで太らされるのか、なんか分からされた。
で、Java5ベースで行きたいのでBSFのお世話になってますが、BSFはJRubyに添付されているやつはちょっと手が加えられているらしいので、Apacheから最新版を落とした方がいいらしい。