go-taskの-w
オプションを使って、HTTPサーバーでライブリロードをやろうとしたら、ちょっと工夫が必要だった。
go-taskの-wオプション
Golang用のタスクランナーgo-taskには、Watch機能があり、ファイルを監視して変更があったらタスクを自動実行する機能がある。
foo:
cmds:
- go run cmd/foo/main.go
sources:
- internal/**/*
こんな風に書いておいて、
task foo -w
-w
オプションをつけた状態でタスクを実行すると、sources
に指定されたファイルに変更があった時に自動的にgo run cmd/foo/main.go
が実行される。
この機能は、「ただ指定されたコマンドを実行するだけ」で、実行中だったコマンドを終了してくれる訳ではないので、サーバーのように起動し続けるタイプのコマンドの場合は、新しくコマンドが実行される前に古いコマンドのプロセスを終了させないとプロセスが多重に起動して(あるいは起動に失敗して)おかしなことになってしまう。
先に起動していたプロセスを終了する
新しいコマンドを実行する直前に、プロセスを終了するようにしたい。
foo:
cmds:
- ここで以前のプロセスが生きていたら終了したい
- go run cmd/foo/main.go
プロセスを終了するには、kill
コマンドを実行すれば良いが、そのためには先に実行されたコマンドのプロセスIDを知っておく必要がある。
起動時にプロセスIDを保存しておくようにする
コマンド実行時に、プロセスIDを保存するようにプログラム側を拡張する。保存先をコマンドライン引数で指定できるようにしておき、go-task
以外で起動した時に問題がおきないようにする。
func main() {
var pidFile = flag.String("pid-file", "", "Path to pid file")
flag.Parse()
if len(*pidFile) > 0 {
if err := ioutil.WriteFile(*pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0664); err != nil {
log.Printf("[WARNING] Failed to write pid file. %v\n", err)
}
defer func() { // 終了時にPIDファイルをちゃんと消すようにする
if err := os.Remove(*pidFile); err != nil {
log.Printf("[WARNING] Failed to delete pid file. %v\n", err)
}
}()
}
// プログラムのメイン...
...
}
実行中のプロセスIDはos.Getpid
で取得することができるので、これを--pid-file
で指定されたファイルに書き込む。書き込もうとした先にすでにファイルが存在する場合は異常な状態なので、本来はそのチェックをするべきだが、プログラムが読みにくくなるので上のコードでは省略した。deferを使って、プログラムが終了する時にPIDを書き込んだファイルを削除するようにしている。(異常終了した場合にはファイルが残る)
タスク実行時のコマンドに--pid-file
オプションを加えれば、タスク実行時にプロセスIDがファイルに出力されるようになる。
foo:
cmds:
- go run cmd/foo/main.go --pid-file=dev.pid
試しに実行してみて、cat
コマンドでpidを確認できれば成功。
cat dev.pid
これで、実行中のプロセスをKILLしたい場合は下記のコマンドでKILLできるようになった。
kill -TERM `cat dev.pid`
実行前にプロセスをKILLする
上の仕組みを使って、新しいプロセスを立ち上げる前に実行中のプロセスがあれば終了するようにする。
foo:
cmds:
- kill -TERM `cat dev.pid`
- go run cmd/foo/main.go --pid-file=dev.pid
ただし、これだけだと初回起動時などdev.pid
がない場合にエラーが発生してしまう。
foo:
cmds:
- cmd: kill -TERM `cat dev.pid`
ignore_error: true
- go run cmd/foo/main.go --pid-file=dev.pid
このような記法に変更して、ignore_error: true
を指定するとdev.pid
が存在せずエラーになった場合でもコマンドを続行できるようになる。これでも十分なのだが、画面にエラーが出力されてしまうので、コマンドの標準出力/エラー出力の内容を/dev/null
にリダイレクトするように設定する。
foo:
cmds:
- cmd: kill -TERM `cat dev.pid` > /dev/null 2>&1
ignore_error: true
- go run cmd/foo/main.go --pid-file=dev.pid
これで、コマンドを連続して叩いた場合には古いプロセスを終了して新しいプロセスを立ち上げられるようになった。
監視対象のファイルを指定する
最後に、Watchの対象となるファイルをsources
で指定する。
foo:
cmds:
- cmd: kill -TERM `cat dev.pid` > /dev/null 2>&1
ignore_error: true
- go run cmd/foo/main.go --pid-file=dev.pid
sources:
- internal/**/*
単純に全てのファイルにしてしまうとログやpid
ファイルなども監視対象となってしまいおかしなことになる可能性があるので注意する。
-wオプションをつけてタスクを実行する
task foo -w
以上で、ファイルを変更すると自動的にサーバーがリロードされるようになった。