groovyserv - technical part

66
高速起動 Groovy Side-B: Technical Part

Upload: yasuharu-nakano

Post on 10-May-2015

2.080 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: GroovyServ - Technical Part

高速起動Groovy

Side-B: Technical Part

Page 2: GroovyServ - Technical Part

目次基本編全体のクラス構成や、基本的な処理の流れのようなもの

がんばりどころ思ったほど単純じゃないのですね

ビルドこうやってビルドしてます

ぷちハック別マシン上のgroovyserverプロセスをつついてみよう

Page 3: GroovyServ - Technical Part

期待される効能実装面に色々やってるのを見て「へぇ~」と思う

自分でソースからビルドできるようになる

バグ対処も自分でできるようになる

報告&パッチください

ハック!ハック!ハック!

Page 4: GroovyServ - Technical Part

基本編

Page 5: GroovyServ - Technical Part

全体のクラス図based on: Ver 0.4-SNAPSHOT(7/24)

Page 6: GroovyServ - Technical Part
Page 7: GroovyServ - Technical Part

!"#$%&'()*スクリプト実行までの流れ

based on: Ver 0.4-SNAPSHOT(7/24)

高速学習GroovyServ

高速学習GroovyServ

Page 8: GroovyServ - Technical Part

$ groovyserver

Page 9: GroovyServ - Technical Part

GroovyServerのmain()が起動される

Page 10: GroovyServ - Technical Part

ServerSocketを作ってListenループ開始(デフォルトでは1961番ポート)

Page 11: GroovyServ - Technical Part

$ groovyclient -e “println(‘Hello, GroovyServ!’)”

groovyclient

groovyserver1961(default)

Page 12: GroovyServ - Technical Part

RequestWorkerを生成して、start()する

Page 13: GroovyServ - Technical Part

ClientConnection#openSession()を呼び出して、リクエストのヘッダ情報をパースした、InvocationRequestを取得する

Page 14: GroovyServ - Technical Part

groovyclientからのGroovyスクリプトを実行するためのスレッドを起動する

Page 15: GroovyServ - Technical Part

Groovyスクリプトを実行する

Page 16: GroovyServ - Technical Part

+,-./012345

67389:3467;5

<=6#+,4

スクリプト実行までの流れbased on: Ver 0.4-SNAPSHOT(7/24)

高速学習GroovyServ

高速学習GroovyServ

Page 17: GroovyServ - Technical Part

$ groovyserver

Page 18: GroovyServ - Technical Part

GroovyServerのmain()が起動される

Page 19: GroovyServ - Technical Part

System.in/out/errの標準入出力を自前のクラスに差し替える!リクエストごとに独立したソケットとの接続性を実現するため。後述。

Page 20: GroovyServ - Technical Part

ユーザ認証用のクッキーを生成して、~/.groovy/groovyserv/cookie に保存しておく

Page 21: GroovyServ - Technical Part

ServerSocketを作ってListenループ開始(デフォルトでは1961番ポート)

Page 22: GroovyServ - Technical Part

$ groovyclient -e “println(‘Hello, GroovyServ!’)”

groovyclient

groovyserver1961(default)

./groovy/groovyserv/cookie内に出力されているトークンがリクエストヘッダのCookieに設定される

Cookie: 425ba835cb32688b1

Page 23: GroovyServ - Technical Part

クライアントSocketがLoopback Addressのものであれば、RequestWorkerを生成する

Page 24: GroovyServ - Technical Part

RequestWorkerのコンストラクタで、クライアントごとのソケットやストリー

ム周りの色々をとりまとめたClientConnectionを生成する

CientConnectionのコンストラクタの最後で、自分自身をThreadGroupをキーにSingletonなリポジトリに登録する

Page 25: GroovyServ - Technical Part

RequestWorkerをstart()する

Page 26: GroovyServ - Technical Part

このとき、リクエストのヘッダ情報としてクライアントから渡されたクッキーのトークンが、サーバ側で持っているCookieと一致するかどうかチェックする

ClientConnection#openSession()を呼び出して、リクエストのヘッダ情報をパースした、InvocationRequestを取得する

Page 27: GroovyServ - Technical Part

groovyclient上の標準入力をソケット経由でサーバ側で受け取ってSystem.inに転送し続けるスレッドを起動する

Page 28: GroovyServ - Technical Part

groovyclientからのGroovyスクリプトを実行するためのスレッドを起動する

Page 29: GroovyServ - Technical Part

groovyclientが実行されたカレントディレクトリ(CWD)を以下のようにJVMに反映する1. システムプロパティ“user.dir”にセット2. JNAを利用してJVMプロセス自体のCWDを変更3. CWDをクラスパスに追加

Page 30: GroovyServ - Technical Part

GroovyMainという本家のクラスをちょっとだけ変更したGroovyMain2を使って、Groovyスクリプトを実行する

Page 31: GroovyServ - Technical Part

がんばりどころ

>?@A4BCDE3:2

Page 32: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 33: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 34: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

スクリプトがSystem.out(err)を使って、標準出力をクライアントに送る

スクリプトがSystem.inを使って、クライアントからの標準入力を取得する

HTTPにちょっとだけ似た感じのオレオレ通信プロトコルを利用

Page 35: GroovyServ - Technical Part

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”" 標準出力へ!

Page 36: GroovyServ - Technical Part

ClientConnectionRepository

現在のThreadGroupに対するClientConnectionを返す

ClientConnection

PipedOutputStream

PipedInputStream

groovyclient StreamRequestHandler

!script"

System.in.eachLine { ... }

接続されたパイプ

Socket.inputStream.read()して、プロトコルに基づいてパースしたボディ部をpipedOutputStream.write()に書き出す

System.in

(StreamRequestInputStream)ClientConnection内の

PipedInputStreamに処理を委譲

スクリプトがSystem.inを使って、クライアントからの標準入力

を取得するの図

Page 37: GroovyServ - Technical Part

なんでこんな面倒なことしてるの?[要請] クライアントからの切断要求(Size:-1)に随時反応させたい

[案A] StreamRequestInputStreamから、直接Socket#getInputStream()にディスパッチする方式

System.inアクセス時にはじめてクライアントからの入力が評価される

余計なパイプが挟まらないのでシンプル

[案B] StreamRequestInputStreamからPipedInputStream()にディスパッチする。パイプには別スレッドを使って逐次クライアントからの入力をコピーする

常にクライアント入力を監視できるため、切断要求に即時応答できる

(Piped~)

Page 38: GroovyServ - Technical Part

Request

InvocationRequest

初回にクライアントから送られるGroovyスクリプト自体を含む実行リクエスト

StreamRequest

クライアントへの標準入力を、サーバに送信するための、ストリーム型リクエスト

Response

StreamResponse

サーバ上のスクリプト実行結果としての標準出力と標準エラー出力を、クライアントに送信するための、ストリーム型レスポンス

オレオレ通信プロトコル

Page 39: GroovyServ - Technical Part

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').

Page 40: GroovyServ - Technical Part

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.

Page 41: GroovyServ - Technical Part

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.

Page 42: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 43: GroovyServ - Technical Part

Cookie / Loopback onlyセキュリティ対策のために以下の2つの機構が導入されている

Only from Loopback address

「見知らぬ別のマシンからつついちゃだめ」単純に、InetAddress#isLoopbackAddress()で判定

Cookie

「Loopback address経由でも、起動したサーバと同じかそれ以上のアクセス権限がないユーザからのリクエストはNG」HTTPのCookieとは全く関係ないサーバ起動時にランダム文字列をクッキーとして、~/.groovy/groovyserv/cookieに保存クライアントからのリクエストごとに、ファイルから読み取ったクッキーをCookieヘッダに付けて送りつけるサーバ側でリクエストパース時にクッキートークンが一致しなければ、Authentication failed

Page 44: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 45: GroovyServ - Technical Part

クライアントでCLASSPATH環境変数が指定されている場合

InvocationRequestのCpヘッダにそのままの文字列を設定してサーバに送信する

GroovyMain2に渡す前に、システムプロパティ“groovy.classpath”に設定しておくと、後はGroovyMain2で良きに計らってくれる

ただし、現状の実装では、他のリクエストや実行中スレッドがあるかもしれない中、安全にgroovy.classpathから使い終わったクラスパスを取り除くことができないため、追加されていく一方になる。

後始末するにはサーバを再起動するしかない(groovyserver -r、等)

クライアントで-cpオプションが指定された場合

InvocationRequestのArgヘッダ、つまり、コマンドライン引数の一部としてそのままサーバに送信する

そのままGroovyMain2にコマンドライン引数として渡すと、良きに計らってくれる

CLASSPATH

Page 46: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 47: GroovyServ - Technical Part

カレントディレクトリという概念が裏側に隠蔽されている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...

Page 48: GroovyServ - Technical Part

よって、以下の対処が必要

システムプロパティ“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では、実行されたスクリプトと同じディレクトリにあるスクリプトは、クラスパスに入ってるものとして扱われるため

Page 49: GroovyServ - Technical Part

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ファイルで

Page 50: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 51: GroovyServ - Technical Part

「ポートをつついてサーバが生きてないなら、単純にgroovyserverシェルスクリプトを実行してるだけ」「え?」「え?」

Windowsの場合はgroovyserver.bat

PIDが簡単に扱えるLinux/MacOSXの場合は、PIDファイルを使って多重起動防止とかしてます。killも再起動もできます。

Windowsの場合は、起動してみて多重だったらAddress already in useエラーで落ちます

BATでPIDを使ってプロセス制御する方法知ってたら教えてください

透過的なサーバ起動

Page 52: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 53: GroovyServ - Technical Part

groovyclientで、サーバからのレスポンスを待っているときにCTRL+Cを実行すると・・・

“Size: -1”というヘッダを含むリクエストが飛ぶ

サーバ側のStreamRequestHandlerスレッドでこのリクエストを見つけると、サーバ側の処理を中断する

それほど大した話でもないですね

CTRL+C対応

Page 54: GroovyServ - Technical Part

クライアント-サーバ間通信の実態

見知らぬ人からのリクエストは処理しない

クラスパスの扱い

カレントディレクトリ(CWD)の扱い

クライアントから透過的にサーバを起動

クライアント側でCTRL+Cをするとサーバ処理が中断

スレッド/コネクション管理

Page 55: GroovyServ - Technical Part

正直まだまだ実装があまいですjava.util.concurrentパッケージを利用リクエストごとに2つの必須スレッドソケット越しの標準入力の監視・転送を行うスレッド(StreamRequestHandler)Groovyスクリプトを実行するスレッド(GroovyInvokeHandler)スクリプト内部で、スレッドが生成されるユースケースも対応しないといけない

スレッド/コネクション管理

Page 56: GroovyServ - Technical Part

コネクションがクローズされた場合クライアント側からサーバ側からNWトラブルで

Groovyスクリプトの実行が終わったらサブスレッドがない場合サブスレッドがまだ生きている場合

クライアント側でCTRL+Cされて中断されたら・・・・・・どの場合にどの順序で何を後始末していくのか、スレッドをどうやって待ち受けるか、色々ややこしくて、まだナイスな状態になってない鋭意検討&実装中!!!

Page 57: GroovyServ - Technical Part

書かなかったけど、がんばったその他のこと

System.exit()の取り扱い

スクリプトで実行されてもJVMを落とさないよ!

Page 58: GroovyServ - Technical Part

ビルド

Page 59: GroovyServ - Technical Part

Maven + GMaven

Maven + GMaven

GMavenはドキュメントが全然メンテナンスされてない。正直微妙

今ならGradleの方がよさげ(本家もGradleに移行中)

0.4-SNAPSHOTからgroovyファイルはコンパイルせずにそのまま実行ファイルとして提供するようにした

groovycでコンパイルすると、何故かJDKのAPIのprivateなコンストラクタの数の違いによって、IncompatibleClassChange

Errorが出たりしたので

Page 60: GroovyServ - Technical Part

IntegrationTestメインでGroovyServは、クライアントとサーバ間の相互作用が一番のポイント

なので、ビルドで生成されたgroovyclient実行ファイルをそのまま使って、結合/システムテスト的なテストをメインに実行している

maven-failsafe-pluginというプラグインを使うと“mvn integration-test”で結合テストが実行できるようになる

詳しくは http://maven.apache.org/plugins/maven-failsafe-plugin/index.html

現状ではときどきスレッド系のテストでmvnが固まってしまう

サブスレッドが絡んだときの終了処理でバグがあるためと思われる

対応策を検討中

Page 61: GroovyServ - Technical Part

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が必要

Page 62: GroovyServ - Technical Part

ぷちハック>?:')DFFFF

Page 63: GroovyServ - Technical Part

Remote GroovyServ

セキュリティ対策のために導入されている以下をあえてOFFにしてみます

Only Loopback adress

Cookie

ただし、ハック版サーバを実行したまま放置した場合の被害等については責任を負いかねます

Page 64: GroovyServ - Technical Part

修正ポイントは3カ所groovyserver

Loopback AddressチェックをコメントアウトCookieチェックをコメントアウト

grovyclient

接続先ホストを“localhost”=>接続したいサーバに変更する

サーバ用マシンAでビルドするgroovyserverを起動する

クライアント用マシンBにソース一式をおくるgroovyclientを実行してみると・・・・・

Page 65: GroovyServ - Technical Part

まとめ

Page 66: GroovyServ - Technical Part

とまあ、GroovyServの中身はこんな感じです正直、完成度の低い部分はまだまだ残ってます自分のユースケースの範囲だと検出できない問題も多いので、是非みなさん使ってください!!ソースはGitHubで公開してます。clone/forkはご自由に!バグのパッチは大歓迎新機能や改善提案は採用できるかは別として、基本的にWelcomeです

GroovyServトップページhttp://kobo.github.com/groovyserv

GitHub

http://github.com/kobo/groovyserv