高速起動Groovy
Side-B: Technical Part
目次基本編全体のクラス構成や、基本的な処理の流れのようなもの
がんばりどころ思ったほど単純じゃないのですね
ビルドこうやってビルドしてます
ぷちハック別マシン上のgroovyserverプロセスをつついてみよう
期待される効能実装面に色々やってるのを見て「へぇ~」と思う
自分でソースからビルドできるようになる
バグ対処も自分でできるようになる
報告&パッチください
ハック!ハック!ハック!
基本編
全体のクラス図based on: Ver 0.4-SNAPSHOT(7/24)
!"#$%&'()*スクリプト実行までの流れ
based on: Ver 0.4-SNAPSHOT(7/24)
高速学習GroovyServ
高速学習GroovyServ
$ groovyserver
GroovyServerのmain()が起動される
ServerSocketを作ってListenループ開始(デフォルトでは1961番ポート)
$ groovyclient -e “println(‘Hello, GroovyServ!’)”
groovyclient
groovyserver1961(default)
RequestWorkerを生成して、start()する
ClientConnection#openSession()を呼び出して、リクエストのヘッダ情報をパースした、InvocationRequestを取得する
groovyclientからのGroovyスクリプトを実行するためのスレッドを起動する
Groovyスクリプトを実行する
+,-./012345
67389:3467;5
<=6#+,4
スクリプト実行までの流れbased on: Ver 0.4-SNAPSHOT(7/24)
高速学習GroovyServ
高速学習GroovyServ
$ groovyserver
GroovyServerのmain()が起動される
System.in/out/errの標準入出力を自前のクラスに差し替える!リクエストごとに独立したソケットとの接続性を実現するため。後述。
ユーザ認証用のクッキーを生成して、~/.groovy/groovyserv/cookie に保存しておく
ServerSocketを作ってListenループ開始(デフォルトでは1961番ポート)
$ groovyclient -e “println(‘Hello, GroovyServ!’)”
groovyclient
groovyserver1961(default)
./groovy/groovyserv/cookie内に出力されているトークンがリクエストヘッダのCookieに設定される
Cookie: 425ba835cb32688b1
クライアントSocketがLoopback Addressのものであれば、RequestWorkerを生成する
RequestWorkerのコンストラクタで、クライアントごとのソケットやストリー
ム周りの色々をとりまとめたClientConnectionを生成する
CientConnectionのコンストラクタの最後で、自分自身をThreadGroupをキーにSingletonなリポジトリに登録する
RequestWorkerをstart()する
このとき、リクエストのヘッダ情報としてクライアントから渡されたクッキーのトークンが、サーバ側で持っているCookieと一致するかどうかチェックする
ClientConnection#openSession()を呼び出して、リクエストのヘッダ情報をパースした、InvocationRequestを取得する
groovyclient上の標準入力をソケット経由でサーバ側で受け取ってSystem.inに転送し続けるスレッドを起動する
groovyclientからのGroovyスクリプトを実行するためのスレッドを起動する
groovyclientが実行されたカレントディレクトリ(CWD)を以下のようにJVMに反映する1. システムプロパティ“user.dir”にセット2. JNAを利用してJVMプロセス自体のCWDを変更3. CWDをクラスパスに追加
GroovyMainという本家のクラスをちょっとだけ変更したGroovyMain2を使って、Groovyスクリプトを実行する
がんばりどころ
>?@A4BCDE3:2
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
クライアント-サーバ間通信の実態
スクリプトがSystem.out(err)を使って、標準出力をクライアントに送る
スクリプトがSystem.inを使って、クライアントからの標準入力を取得する
HTTPにちょっとだけ似た感じのオレオレ通信プロトコルを利用
groovyclientClientConnectionRepository
現在のThreadGroupに対するClientConnectionを返す
ClientConnection
OutputStream
!script"
println “Hello, GroovyServ!”
System.out
(StreamResponse
OutputStream)
プロトコルに基づいて変換したデータをSocketの
OutputStream#write()
に書き出す
スクリプトがSystem.out(err)
を使って、標準出力をクライアントに送るの図
Hello, GroovyServ!
if Channelヘッダ==”err”" 標準エラー出力へ!
if Channelヘッダ==”out”" 標準出力へ!
ClientConnectionRepository
現在のThreadGroupに対するClientConnectionを返す
ClientConnection
PipedOutputStream
PipedInputStream
groovyclient StreamRequestHandler
!script"
System.in.eachLine { ... }
接続されたパイプ
Socket.inputStream.read()して、プロトコルに基づいてパースしたボディ部をpipedOutputStream.write()に書き出す
System.in
(StreamRequestInputStream)ClientConnection内の
PipedInputStreamに処理を委譲
スクリプトがSystem.inを使って、クライアントからの標準入力
を取得するの図
なんでこんな面倒なことしてるの?[要請] クライアントからの切断要求(Size:-1)に随時反応させたい
[案A] StreamRequestInputStreamから、直接Socket#getInputStream()にディスパッチする方式
System.inアクセス時にはじめてクライアントからの入力が評価される
余計なパイプが挟まらないのでシンプル
[案B] StreamRequestInputStreamからPipedInputStream()にディスパッチする。パイプには別スレッドを使って逐次クライアントからの入力をコピーする
常にクライアント入力を監視できるため、切断要求に即時応答できる
(Piped~)
Request
InvocationRequest
初回にクライアントから送られるGroovyスクリプト自体を含む実行リクエスト
StreamRequest
クライアントへの標準入力を、サーバに送信するための、ストリーム型リクエスト
Response
StreamResponse
サーバ上のスクリプト実行結果としての標準出力と標準エラー出力を、クライアントに送信するための、ストリーム型レスポンス
オレオレ通信プロトコル
InvocationRequest'Cwd:' <cwd> LF
'Arg:' <argn> LF
'Arg:' <arg1> LF
'Arg:' <arg2> LF
'Cp:' <classpath> LF
'Cookie:' <cookie> LF
LF
where:
<cwd> is current working directory.
<arg1><arg2>.. are commandline arguments(optional).
<classpath>.. is the value of environment variable
CLASSPATH(optional).
<cookie> is authentication value which certify client is
the user who invoked the server.
LF is line feed (0x0a, '\n').
StreamRequest
'Size:' <size> LF
LF
<data from STDIN>
where:
<size> is the size of data to send to server. <size>==-1
means client exited.
<data from STDIN> is byte sequence from standard input.
StreamResponse
'Status:' <status> LF
'Channel:' <id> LF
'Size:' <size> LF
LF
<data for STDERR/STDOUT>
where:
<status> is exit status of invoked groovy script.
<id> is 'out' or 'err', where 'out' means standard output
of the program. 'err' means standard error of the program.
<size> is the size of chunk.
<data from STDERR/STDOUT> is byte sequence from standard
output/error.
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
Cookie / Loopback onlyセキュリティ対策のために以下の2つの機構が導入されている
Only from Loopback address
「見知らぬ別のマシンからつついちゃだめ」単純に、InetAddress#isLoopbackAddress()で判定
Cookie
「Loopback address経由でも、起動したサーバと同じかそれ以上のアクセス権限がないユーザからのリクエストはNG」HTTPのCookieとは全く関係ないサーバ起動時にランダム文字列をクッキーとして、~/.groovy/groovyserv/cookieに保存クライアントからのリクエストごとに、ファイルから読み取ったクッキーをCookieヘッダに付けて送りつけるサーバ側でリクエストパース時にクッキートークンが一致しなければ、Authentication failed
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
クライアントでCLASSPATH環境変数が指定されている場合
InvocationRequestのCpヘッダにそのままの文字列を設定してサーバに送信する
GroovyMain2に渡す前に、システムプロパティ“groovy.classpath”に設定しておくと、後はGroovyMain2で良きに計らってくれる
ただし、現状の実装では、他のリクエストや実行中スレッドがあるかもしれない中、安全にgroovy.classpathから使い終わったクラスパスを取り除くことができないため、追加されていく一方になる。
後始末するにはサーバを再起動するしかない(groovyserver -r、等)
クライアントで-cpオプションが指定された場合
InvocationRequestのArgヘッダ、つまり、コマンドライン引数の一部としてそのままサーバに送信する
そのままGroovyMain2にコマンドライン引数として渡すと、良きに計らってくれる
CLASSPATH
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
カレントディレクトリという概念が裏側に隠蔽されているJavaですが、きちんと対処しないと困る場面があります
GroovyServでは、サーバ起動時のCWDと、クライアント実行時のCWDが異なるため、何も対処をしないと・・・
CWD(Current Working Directory)
$ cd /tmp
$ groovyserver -r
$ cd /home/kobo
$ cat > hoge.txt
HOGE!!
^C
$ groovyclient -e ‘println(new File(“hoge.txt”).text)’
Caught: java.io.FileNotFoundException: hoge.txt (No such file or directory)
...SNIP...
よって、以下の対処が必要
システムプロパティ“user.dir”にセット
File#getAbsolutePath()に影響する
“"user.dir", which is initialized during jvm
startup, should be used as an informative/readonly
system property”(posted 2008-08-18)・・・だと・・・?!http://bugs.sun.com/bugdatabase/view_bug.do;:YfiG?bug_id=4117557
JNAを利用してJVMプロセス自体のCWDを変更
“user.dir”による絶対パス補完がなぜか漏れているFileInputStreamを含む多数のクラスのため
あと、CWDをクラスパスに追加しておく
Groovyでは、実行されたスクリプトと同じディレクトリにあるスクリプトは、クラスパスに入ってるものとして扱われるため
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Platform
interface CLibrary extends Library {
String libname = (Platform.isWindows() ? "msvcrt" : "c")
CLibrary INSTANCE = Native.loadLibrary(libname, CLibrary.class)
int chdir(String dir)
int _chdir(String dir)
}
と、依存ライブラリを追加しておいて#でOK!便利!
<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>3.2.2</version> </dependency>
JNAでcwdを変更するには、POMファイルで
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
「ポートをつついてサーバが生きてないなら、単純にgroovyserverシェルスクリプトを実行してるだけ」「え?」「え?」
Windowsの場合はgroovyserver.bat
PIDが簡単に扱えるLinux/MacOSXの場合は、PIDファイルを使って多重起動防止とかしてます。killも再起動もできます。
Windowsの場合は、起動してみて多重だったらAddress already in useエラーで落ちます
BATでPIDを使ってプロセス制御する方法知ってたら教えてください
透過的なサーバ起動
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
groovyclientで、サーバからのレスポンスを待っているときにCTRL+Cを実行すると・・・
“Size: -1”というヘッダを含むリクエストが飛ぶ
サーバ側のStreamRequestHandlerスレッドでこのリクエストを見つけると、サーバ側の処理を中断する
それほど大した話でもないですね
CTRL+C対応
クライアント-サーバ間通信の実態
見知らぬ人からのリクエストは処理しない
クラスパスの扱い
カレントディレクトリ(CWD)の扱い
クライアントから透過的にサーバを起動
クライアント側でCTRL+Cをするとサーバ処理が中断
スレッド/コネクション管理
正直まだまだ実装があまいですjava.util.concurrentパッケージを利用リクエストごとに2つの必須スレッドソケット越しの標準入力の監視・転送を行うスレッド(StreamRequestHandler)Groovyスクリプトを実行するスレッド(GroovyInvokeHandler)スクリプト内部で、スレッドが生成されるユースケースも対応しないといけない
スレッド/コネクション管理
コネクションがクローズされた場合クライアント側からサーバ側からNWトラブルで
Groovyスクリプトの実行が終わったらサブスレッドがない場合サブスレッドがまだ生きている場合
クライアント側でCTRL+Cされて中断されたら・・・・・・どの場合にどの順序で何を後始末していくのか、スレッドをどうやって待ち受けるか、色々ややこしくて、まだナイスな状態になってない鋭意検討&実装中!!!
書かなかったけど、がんばったその他のこと
System.exit()の取り扱い
スクリプトで実行されてもJVMを落とさないよ!
ビルド
Maven + GMaven
Maven + GMaven
GMavenはドキュメントが全然メンテナンスされてない。正直微妙
今ならGradleの方がよさげ(本家もGradleに移行中)
0.4-SNAPSHOTからgroovyファイルはコンパイルせずにそのまま実行ファイルとして提供するようにした
groovycでコンパイルすると、何故かJDKのAPIのprivateなコンストラクタの数の違いによって、IncompatibleClassChange
Errorが出たりしたので
IntegrationTestメインでGroovyServは、クライアントとサーバ間の相互作用が一番のポイント
なので、ビルドで生成されたgroovyclient実行ファイルをそのまま使って、結合/システムテスト的なテストをメインに実行している
maven-failsafe-pluginというプラグインを使うと“mvn integration-test”で結合テストが実行できるようになる
詳しくは http://maven.apache.org/plugins/maven-failsafe-plugin/index.html
現状ではときどきスレッド系のテストでmvnが固まってしまう
サブスレッドが絡んだときの終了処理でバグがあるためと思われる
対応策を検討中
How to Build
Maven2でビルド $ cd groovyserv-<Version>
$ mvn clean verify
バイナリパッケージ: target/groovyserv-<Version>-<OS>-<arch>-bin.zip
Integration-Test用の環境(パスを通せば実行することも可能。試行錯誤時に便利) target/groovyserv-<Version>-<OS>-<arch>.dir/groovyserv-<Version>-<OS>-<arch>
テストで失敗する場合:
EncodeITがエラーになるなど、文字化け系が怪しければ、文字エンコードをUTF-8に設定する $ export _JAVA_OPTIONS=-Dfile.encoding=UTF-8
テスト環境だけの問題であればすべてのテストをスキップする方法もある $ mvn -Dmaven.test.skip=true clean package
Windows上でビルドするためにはCygwinとgccが必要
ぷちハック>?:')DFFFF
Remote GroovyServ
セキュリティ対策のために導入されている以下をあえてOFFにしてみます
Only Loopback adress
Cookie
ただし、ハック版サーバを実行したまま放置した場合の被害等については責任を負いかねます
修正ポイントは3カ所groovyserver
Loopback AddressチェックをコメントアウトCookieチェックをコメントアウト
grovyclient
接続先ホストを“localhost”=>接続したいサーバに変更する
サーバ用マシンAでビルドするgroovyserverを起動する
クライアント用マシンBにソース一式をおくるgroovyclientを実行してみると・・・・・
まとめ
とまあ、GroovyServの中身はこんな感じです正直、完成度の低い部分はまだまだ残ってます自分のユースケースの範囲だと検出できない問題も多いので、是非みなさん使ってください!!ソースはGitHubで公開してます。clone/forkはご自由に!バグのパッチは大歓迎新機能や改善提案は採用できるかは別として、基本的にWelcomeです
GroovyServトップページhttp://kobo.github.com/groovyserv
GitHub
http://github.com/kobo/groovyserv