Play Framework で開発用Webサーバと同時に grunt/gulp を起動する

手元の環境でWebアプリケーションを開発するために、複数のプログラムを手動で起動しないといけないのは面倒だ。たとえば、rackupとgruntを同時に起動しておかないと、lessやjsをコンパイルしながらページを表示できないという風だと、いつも両方が起動しているように気をつけないといけないし、そのこと忘れてしまってなぜかデザインが当たらないなどといって悩むはめになる。

RubyのforemanやPerlのProcletなどを使うと、複数のプログラムを同時に起動したり終了したりすることができる。先ほどの例だと、Procfileにrackupとgruntの起動コマンドを書いて、開発環境でWebアプリケーションを起動するときにはforeman startするということにしておけば、必要なプログラムがすべて起動しているという状態に簡単にできて便利だ。

一方最近自分は、Play Frameworkで開発をしてる。Play Framework では sbt から開発用のWebサーバを起動する。sbtの起動は時間がかかるので、sbt自体は起動したままにして、sbtのコンソールから、Webサーバを起動したり停止したりできる仕組みになっている。このような仕組みなので、foremanから直接Webサーバを起動するのは難しい。(できなくはないけど、都度sbtを起動しなおすことになるので遅い)

調べてみたところ、sbtとPlay Frameworkの機能をうまく使うと、Play Frameworkの Webサーバが実行されている間だけ、別のプログラムを起動することができるようだった(参考:Playframework 2.2 Grunt Runner · GitHub)。

Play Frameworkには playRunHooks というsbtのSettingがある。これにPlayRunHookというtraitを実装したobjectを設定しておくと、Webアプリケーションが起動するタイミングや終了するタイミングにhookをかけれる。sbtには別のプロセスを起動する仕組みがあるので組み合わせると以下のようになる。(参考にさせていただいた、Playframework 2.2 Grunt Runner · GitHubをちょっと汎用的にしてる)

// project/RunSubProcess.scala
import sbt._
import Process._
import Keys._
import play.PlayRunHook
 
object RunSubProcess {
  def apply(command: String): PlayRunHook = {
 
    object RunSubProcessHook extends PlayRunHook {
 
      var process: Option[Process] = None
 
      override def beforeStarted(): Unit = {
        process = Some(Process(command).run)
      }
 
      override def afterStopped(): Unit = {
        process.map(p => p.destroy())
        process = None
      }
    }
 
    RunSubProcessHook
  }
}
// build.sbt
name := "hoge"
 
// ... 中略 ...

play.Project.playScalaSettings
 
playRunHooks += RunSubProcess("grunt watch")

これで無事、Play FrameworkのWebサーバを起動するだけで、同時に別のプログラムも起動することができるようになった。余計なことをきにする必要が減って便利になった。