play framework2をjenkinsでコンパイルする場合。-Dsbt.log.noformat=trueのオプションをつける
問題
jenkinsでコンパイルする際ののログに以下のように色付きの文字がおかしくログに残る。
0m[[0minfo[0m] [0mLoading project definition from ....
jenkinsで単純に以下のコマンドを実行するようにしていると、色付きの文字がおかしくログに残ってしまいます。
~/play2.1.1/play clean update compile test
解決方法
オプションを追加
play -Dsbt.log.noformat=true clean update compile test
すると色付きでおかしかった文字が問題なく色なしでログに残ります。
[info] Loading project definition from ....
ソース探索
Play20 / framework / src / console / src / main / scala / Console.scalaの200行目あたりに今回の該当する部分がありました。
object Colors { import scala.Console._ lazy val isANSISupported = { Option(System.getProperty("sbt.log.noformat")).map(_ != "true").orElse { Option(System.getProperty("os.name")) .map(_.toLowerCase) .filter(_.contains("windows")) .map(_ => false) }.getOrElse(true) } def red(str: String): String = if (isANSISupported) (RED + str + RESET) else str def blue(str: String): String = if (isANSISupported) (BLUE + str + RESET) else str def cyan(str: String): String = if (isANSISupported) (CYAN + str + RESET) else str def green(str: String): String = if (isANSISupported) (GREEN + str + RESET) else str def magenta(str: String): String = if (isANSISupported) (MAGENTA + str + RESET) else str def white(str: String): String = if (isANSISupported) (WHITE + str + RESET) else str def black(str: String): String = if (isANSISupported) (BLACK + str + RESET) else str def yellow(str: String): String = if (isANSISupported) (YELLOW + str + RESET) else str }
色づけの条件(isANSISupported)に、
Option(System.getProperty("sbt.log.noformat")).map(_ != "true")
とあるので、-Dsbt.log.noformat=trueとしておくことで色づけされなくなります。
play framework2 の起動(run)でポートを指定する方法と、失敗パターン
-Dオプションでポートを指定する
play -Dhttp.port=9001 run
すると↓のログが出力されます。
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0%0:9001
失敗するパターン
play run -Dhttp.port=9001
この方法だと失敗します。
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0%0:9000
runの後ろにオプションを指定しても反映されない。 デフォルト値の9000で実行されます。
※追加するオプションはrunの手前で実行する。
ソースを覗く
port指定する場所をソースからみてみます。
Play20 / framework / src / play / src / main / scala / play / core / server / NettyServer.scalaの230行目あたり。
val server = new NettyServer( new StaticApplication(applicationPath), Option(System.getProperty("http.port")).map(Integer.parseInt(_)).getOrElse(9000), Option(System.getProperty("https.port")).map(Integer.parseInt(_)), Option(System.getProperty("http.address")).getOrElse("0.0.0.0") )
ここで指定しているオプションは、http.port、https.port、http.addressの3つ。
OptionはScala特有の記述方法で、PHPでいうとissetとかempty関数みたいな利用方法のよう。 scala-cookbook:Option Monad nullや存在しない値の処理を隠蔽するに詳しく書いてました。
Scalaの見方が分かってくると少しずつ楽しくなってきます。
def createServer(applicationPath: File): Option[NettyServer] = { //~~~省略~~~ }
createServerという関数?の定義にOption[NettyServer]が利用されていて、 使っているところが以下の部分です。
def main(args: Array[String]) { args.headOption.orElse( Option(System.getProperty("user.dir"))).map(new File(_)).filter(p => p.exists && p.isDirectory).map { applicationPath => createServer(applicationPath).getOrElse(System.exit(-1)) }.getOrElse { println("Not a valid Play application") } }
Optionが多用されてますね。user.dirのプロパティがあれば、 その値を元にFileオブジェクトを生成してexistsとisDirectorycで存在チェックし、問題なければサーバ起動という処理の流れです。
System.getProperty("user.dir")は何を示しているのか。
System.getProperty("user.dir")が何を表すのか分からなかったので、調べてみました。 ソースを探しても見つからないのでググる。 プロパティで指定したキー("user.dir")はカレントディレクトリだそうです。
応用 Java Application - Utility classes - Propertiesにありました。
代表的なプロパティのキーバリューをコピペして整形してメモる。
key | value |
---|---|
file.separator | ファイルの区切り文字(UNIXなら、'/') |
java.class.path | Java クラスパス |
java.class.version | Java クラスのバージョン |
java.home | Java がインストールされているディレクトリ |
java.vendor.url | Java ベンダのURL |
java.version | Java のバージョン |
line.separator | 行の区切り |
os.arch | OSのアーキテクチャ |
os.name | OS名 |
path.separator | パスの区切り(':') |
user.dir | カレント・ディレクトリ |
user.home | ユーザのホーム・ディレクトリ |
user.name | ユーザ名 |
他の言語であれば、envとかで取得する項目のようですが、javaではプロパティを利用するそうです。 .netでいうと、ConfigurationSettingsみたいなものでしょうか。
ec2のmicroインスタンスでPlay!2.1.1を動かす。
play frameworkに関するのslideshare(Play2.0+Javaでサービスを本番稼働させた話 #play_ja)を見ててそうそうと自分もハマった部分があったので、自分なりの解決策を。
問題
Amazon EC2 microでPlay!2.0が起動しない。 java -‐Xms512M -‐Xmx1536M でPlay!2.0はデフォルト起動される
- --Xms512M:初期ヒープサイズを512Mに設定
- --Xmx1536M:最大ヒープサイズを1536Mに設定
- microインスタンスのメモリは613 Mでswapがない
解決策
cloud-initのYAMLをgistに書いてみました。
ec2のマイクロインスタンスでplayを動かす準備 ・時間の設定 ・文字コードの設定 ・swap領域の作成
cloud-initを利用して、インスタンス立ち上げ時にswapを作るという方法です。
swapの作成方法はAdding swap to an EC2 micro instance on Amazonを参考にしました。
いつものスポットインスタンスの起動スクリプトに組み込んで使ってます。
#!/bin/bash AMI='ami-xxxxxxxx'; INSTANCE_TYPE='t1.micro'; PRICE='.003'; SUBNET='subnet-xxxxxxxx'; KEYPAIR='xxxxxxxxxx'; SECURITYGROUP='sg-xxxxxxxx'; DATA='#cloud-config repo_upgrade: all packages: - java-devel - git runcmd: - [cp, /usr/share/zoneinfo/Asia/Tokyo, /etc/localtime] - [sed, -i, 's/LANG="en_US.UTF-8"/LANG="ja_JP.UTF-8"/', /etc/sysconfig/i18n] - [dd, if=/dev/zero, of=/var/swapfile, bs=1M, count=1024] - [mkswap, /var/swapfile] - [swapon, /var/swapfile] '; #スポットインスタンスのリクエストを行う SIR_ID=` ec2rsi $AMI -t $INSTANCE_TYPE -p $PRICE -n 1 -r one-time -s $SUBNET -k $KEYPAIR -g $SECURITYGROUP -d "$DATA" | awk '{if(NR==1) print $2;}' ` #2分間待機 sleep 120 TMP_EC2=''; while true; do #ec2のインスタンス情報取得(スポットインスタンスのIDを指定) TMP_EC2=`ec2din --filter "spot-instance-request-id=$SIR_ID"`; if test ${#TMP_EC2} -gt 0 ; then #情報が取得できた場合 #インスタンス情報、ローカルIPの情報を取得する INSTANCE_ID=`echo $TMP_EC2 | awk '{ print $5 }'`; LOCAL_IP=`echo $TMP_EC2 | awk '{ print $52 }'`; #ループを抜ける break fi #30秒待機 sleep 30 done #IPアドレスを取得する EIP_OBJ=`ec2allocaddr -d vpc`; EIPALLOC=`echo $EIP_OBJ | awk '{ print $4 }'` GLOBAL_IP=`echo $EIP_OBJ | awk '{ print $2 }'` sleep 10 #IPアドレスをec2インスタンスに割り当てる EIPASSOC_OBJ=`ec2assocaddr -a $EIPALLOC -i $INSTANCE_ID -p $LOCAL_IP ` sleep 10 EIPASSOC=`echo $EIPASSOC_OBJ | awk '{ print $4}'` #結果出力 echo "[info]instance_id: $INSTANCE_ID"; echo "[info]globalId: $GLOBAL_IP"; echo "[info]eipassoc: $EIPASSOC"; echo "[info]eipalloc: $EIPALLOC"; echo "[example] ssh -i ${KEYPAIR}.pem ec2-user@${GLOBAL_IP} "; echo "[example] ec2disaddr -a $EIPASSOC ; ec2reladdr -a $EIPALLOC ; ec2kill $INSTANCE_ID ; "
なんだかんだいって、microインスタンスとsmallインスタンスには価格的な大きな壁があるので、 play framework2もmicroで使いたいです。 24h365d動かすならawsは少し高い気がしますが、実験はawsが手っ取り早いです。
iframeフォーエバー! play20にあるcometのサンプルを改造して簡易チャットルームを作りました。
akkaのアクターモデルがちょびっとだけ理解できたmitsugeekです。
今回はplay2.1.1のsampleにあるcomet-clockを改造して簡単なチャットルームを作ってみました。
cometを利用したwebの時刻通知アプリcomet-clock
comet-clockは↓のような画面で、サーバから100ミリ秒毎に時刻をブラウザにpushするアプリです。 cometの実装方法としてiframeを利用してscriptタグのレスポンスを小刻みに返す方法で、Forever Iframeと呼ぶそうです。
このアプリがplay framework2のサンプルとして付属してます。 今回はこのサンプルを改造して、チャットアプリにしてみました。
イメージ・・・
ニックネームを入力してチャットルームに入室。
入力域に文字を入力して、送信を押すとメッセージが送信されます。
入室と退室(ブラウザを閉じる)とメッセージの送信それだけです。 メッセージの保存等は行っていません。超シンプルです。
別タブでアクセスして、入室すると入室したことがリアルタイムで通知されます。
ブラウザを閉じると、退室も通知します。
ipadで簡単に試したのですが(wifi)、safariを閉じて別のアプリを起動してからsafariに戻っても、通信はそのまま繋がっているみたいでびっくりしました。枯れた技術の組み合わせでリアルタイムなチャットができるんだとちょっとだけ感動。
感想
lingrには到底及ばないですが、ひとまずチャットができるところまで作ってみましたのでgithubに上げてときます。(iframeを利用したcometですので通信が途切れたかどうかの判定ができてませんし、XSSの脆弱性あります。)
今回はcometという技術とアクターモデルというアーキテクチャを噛み砕きながら作っていく感じでかなり勉強になりました。技術的にはまだまだ身にしみない感じです。ココ(ie10関連ページ)見る限り、IE10ではComet ストリーミングのサポートって書いてて、XMLHttpRequestの応答をマルチパートで返せるみたいな感じらしいので、ajaxでcometするのが良いお作法なんだと思います。
このサンプルを動かしてみたい
#java-develのパッケージをインストール(playがjavacを利用します。) sudo yum install java-devel #ホームディレクトリで作業 cd ~ #play2.1.1をダウンロード wget http://downloads.typesafe.com/play/2.1.1/play-2.1.1.zip #解凍 unzip play-2.1.1.zip #チャットアプリを取得 git clone https://github.com/mitsugeek/comet_chat_play2_example_iframe.git #カレントディレクトリを移動 cd comet_chat_play2_example_iframe/ #アプリ起動 ~/play-2.1.1/play run
以上で 9000番ポートにアクセスすると動くと思います。
参考
GMOクラウド VPSにplay2.1.0のセットアップ
yum install wget yum install unzip yum install java-1.6.0-openjdk yum install java-1.6.0-openjdk-devel wget http://downloads.typesafe.com/play/2.1.0/play-2.1.0.zip unzip play-2.1.0.zip vi .bash_profile #> play-2.1.0のパスを.bash_profileに追加 source .bash_profile
.bash_profile
# .bash_profile # Get the aliases and functions if [ -f ~/.bashrc ]; then . ~/.bashrc fi # User specific environment and startup programs PATH=$PATH:$HOME/bin PATH=$PATH:$HOME/play-2.1.0 export PATH
playのセットアップ完了。
アプリの作成
play new test #>[Enter] [2](java) [Enter] cd test play clean compile test stage #クリーン、コンパイル、テスト、 ./stage/start #> アプリケーション実行
これでOK!
少しだけソース修正
package controllers; import play.*; import play.mvc.*; import views.html.*; public class Application extends Controller { public static Result index() { // return ok(index.render("Your new application is ready.") return ok(index.render("あなたの新しいアプリケーションの準備完了!") } }
コンパイル〜テスト。。。
play clean compile test stage [info] Loading project definition from /root/test/project [info] Set current project to test (in build file:/root/test/) [success] Total time: 0 s, completed 2013/03/14 7:43:48 [info] Updating {file:/root/test/}test... [info] Resolving org.hibernate.javax.persistence#hibernate-jpa-2.0-api;1.0.1.Fin [info] Done updating. [info] Compiling 4 Scala sources and 2 Java sources to /root/test/target/scala-2.10/classes... [success] Total time: 17 s, completed 2013/03/14 7:44:05 [info] Compiling 2 Java sources to /root/test/target/scala-2.10/test-classes... [error] Test IntegrationTest.test failed: java.lang.AssertionError: <'<?xml version="1.0" encoding="utf-8"?> [error] <html> [error] <head> [error] <title> [error] Welcome to Play 2.1 [error] </title> [error] <link rel="stylesheet" media="screen" href="/assets/stylesheets/main.css"/> [error] <link rel="shortcut icon" type="image/png" href="/assets/images/favicon.png"/> [error] <script src="/assets/javascripts/jquery-1.9.0.min.js" type="text/javascript"> [error] </script> [error] </head> [error] <body style="zoom: 1;"> [error] <h1> [error] あなたの新しいアプリケーションの準備完了! [error] </h1> [error] </body> [error] </html> [error] '> should contain the String:<'Your new application is ready.'> [error] at play.test.Helpers.running(Helpers.java:426) [error] at IntegrationTest.test(IntegrationTest.java:20) [error] ... [error] Caused by: java.lang.AssertionError: <'<?xml version="1.0" encoding="utf-8"?> [error] <html> [error] <head> [error] <title> [error] Welcome to Play 2.1 [error] </title> [error] <link rel="stylesheet" media="screen" href="/assets/stylesheets/main.css"/> [error] <link rel="shortcut icon" type="image/png" href="/assets/images/favicon.png"/> [error] <script src="/assets/javascripts/jquery-1.9.0.min.js" type="text/javascript"> [error] </script> [error] </head> [error] <body style="zoom: 1;"> [error] <h1> [error] あなたの新しいアプリケーションの準備完了! [error] </h1> [error] </body> [error] </html> [error] '> should contain the String:<'Your new application is ready.'> [error] at org.fest.assertions.Fail.failure(Fail.java:228) [error] at org.fest.assertions.Assert.failure(Assert.java:149) [error] at org.fest.assertions.StringAssert.contains(StringAssert.java:73) [error] at IntegrationTest$1.invoke(IntegrationTest.java:23) [error] at IntegrationTest$1.invoke(IntegrationTest.java:20) [error] at play.test.Helpers.running(Helpers.java:424) [error] ... 32 more [info] IntegrationTest [info] x IntegrationTest.test [info] [info] [info] Total for test IntegrationTest [info] Finished in 0.039 seconds [info] 1 tests, 1 failures, 0 errors [info] ApplicationTest [info] + ApplicationTest.simpleCheck [info] + ApplicationTest.renderTemplate [info] [info] [info] Total for test ApplicationTest [info] Finished in 0.014 seconds [info] 2 tests, 0 failures, 0 errors [error] Failed: : Total 3, Failed 1, Errors 0, Passed 2, Skipped 0 [error] Failed tests: [error] IntegrationTest java.lang.RuntimeException: Tests unsuccessful at scala.sys.package$.error(package.scala:27) at scala.Predef$.error(Predef.scala:123) at sbt.Tests$.showResults(Tests.scala:192) at sbt.Defaults$$anonfun$testTasks$6.apply(Defaults.scala:296) at sbt.Defaults$$anonfun$testTasks$6.apply(Defaults.scala:294) at sbt.Scoped$$anonfun$hf4$1.apply(Structure.scala:580) at sbt.Scoped$$anonfun$hf4$1.apply(Structure.scala:580) at scala.Function1$$anonfun$compose$1.apply(Function1.scala:49) at sbt.Scoped$Reduced$$anonfun$combine$1$$anonfun$apply$12.apply(Structure.scala:311) at sbt.Scoped$Reduced$$anonfun$combine$1$$anonfun$apply$12.apply(Structure.scala:311) at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:41) at sbt.std.Transform$$anon$5.work(System.scala:71) at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:232) at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:232) at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:18) at sbt.Execute.work(Execute.scala:238) at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:232) at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:232) at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:160) at sbt.CompletionService$$anon$2.call(CompletionService.scala:30) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) at java.util.concurrent.FutureTask.run(FutureTask.java:166) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) at java.util.concurrent.FutureTask.run(FutureTask.java:166) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1146) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:679) [error] (test:test) Tests unsuccessful [error] Total time: 9 s, completed 2013/03/14 7:44:14
IntegrationTestで失敗。
「inMemoryDatabaseでポート3333でfakeApplicationを立ち上げて、ブラウザでアクセスして結果に該当の文字列が含まれているかどうか」というテストに引っかかっておりました。
というわけでテスト修正
import org.junit.*; import play.mvc.*; import play.test.*; import play.libs.F.*; import static play.test.Helpers.*; import static org.fest.assertions.Assertions.*; import static org.fluentlenium.core.filter.FilterConstructor.*; public class IntegrationTest { /** * add your integration test here * in this example we just check if the welcome page is being shown */ @Test public void test() { running(testServer(3333, fakeApplication(inMemoryDatabase())), HTMLUNIT, new Callback<TestBrowser>() { public void invoke(TestBrowser browser) { browser.goTo("http://localhost:3333"); // assertThat(browser.pageSource()).contains("Your new application is ready."); assertThat(browser.pageSource()).contains("あなたの新しいアプリケーションの準備完了!"); } }); } }
再度コンパイル、テスト。でそのまま実行
play clean compile test stage ./target/start
プログラム修正ができました。
play framework 2.1.0(java) + redisをサクッと試す。(2)
前回の続きです。
前回はplay framework2.0.xだったのだけれども、playがバージョンアップしたので今回は2.1.0でjedisを使ってみました。
versionアップに関しては、この辺の差分を見れば前回のソースを利用できると思いますが、今回はplay new からのやり直しで試しました。 基本的には前回と同じ手順で問題ありません。
Application.javaの変更点は、redisへのセットだけです。
jedis.set("test", "residOK!");
一応ソース全体を。
package controllers; import play.*; import play.mvc.*; import views.html.*; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class Application extends Controller { public static Result index() { JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost"); Jedis jedis = pool.getResource(); String result = ""; try { jedis.set("test", "residOK!"); result = jedis.get("test"); } finally { pool.returnResource(jedis); } pool.destroy(); return ok(index.render("jedis.set:" + result)); } public static Result redisHello(){ return ok("test"); } }
次回はredisのソートとかをやってみたいと思います。
play framework(java) + redisをサクッと試す。(1)
playのインストールは このへんを参考に。
redisのインストールは redis 2.2.12のインストールメモ(amazon Linux)を参考に。
では早速playのプロジェクトを作るところから、ライブラリの導入等まで。
・プロジェクト作成
play new redisplay
・jedis導入(redisのjavaクライアントの1つ)
cd redisplay/ mkdir lib wget https://github.com/downloads/xetorthio/jedis/jedis-2.1.0.jar
・commons-pool導入(jedisが必要としている)
wget http://ftp.riken.jp/net/apache//commons/pool/binaries/commons-pool-1.6-bin.tar.gz tar zxvf commons-pool-1.6-bin.tar.gz mv commons-pool-1.6/commons-pool-1.6.jar ./ rm commons-pool-1.6 -rf rm commons-pool-1.6-bin.tar.gz
・ライブラリの導入ができたので、次にソースの修正
cd ../
vim app/controllers/Application.java
package controllers; import play.*; import play.mvc.*; import views.html.*; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class Application extends Controller { public static Result index() { JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost"); Jedis jedis = pool.getResource(); String result = ""; try { result = jedis.get("testKey"); } finally { pool.returnResource(jedis); } pool.destroy(); return ok(result); } }
そして実行
play run
ブラウザで確認。
エラー発生。redisの戻り値がnullの為エラー。
・redis-cliを立ち上げて値をセットする
[root@localhost redisplay]# ~/redis-2.6.9/src/redis-cli redis 127.0.0.1:6379> set testKey "test ABC" OK
セットできたので、 アプリを再度起動して、ブラウザでアクセス。
test ABCと画面に表示されました。いい感じです。
今回はredis導入して、redisから値を取得して画面に表示する所までサクッと試してみました。 次回は値のセットをやってみたいと思います。
参考: http://www.playframework-ja.org/documentation/2.0.2/SBTDependencies http://d.hatena.ne.jp/hrendoh/20110901/1314887550 http://blog.livedoor.jp/tattyamm/archives/4208836.html
centOSにplay2.0をインストール。
rootで作業を開始
sudo su -
cd ~/
java-1.6.0-openjdk-develをインストール
yum install java-devel
playをダウンロードして展開
wget http://download.playframework.org/releases/play-2.0.4.zip unzip play-2.0.4.zip
playコマンドをどこでも利用できるようにPATHに追加する為、.bash_profileを編集
vim ~/.bash_profile
PATHにplayを追加
export PATH="$PATH":$JAVA_HOME/bin:$HOME/play-2.0.4
変更内容を反映
source ~/.bash_profile
playを試す
play new HelloWorld
cd HelloWorld
play run
ブラウザで、9000番ポートにアクセス。
参考: http://ottokeyo.blogspot.jp/2012/09/centosplay-framework.html