GroovyServ - Technical Part

Post on 10-May-2015

2,080 views 0 download

Tags:

transcript

高速起動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