Help us understand the problem. What is going on with this article?

go-taskでサーバーのライブリロードを実現する

More than 1 year has passed since last update.

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

以上で、ファイルを変更すると自動的にサーバーがリロードされるようになった。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした