Go 言語の習作に watchevent ってファイルシステム監視するやつ作った

習作って言っても以前 Go 触ってたことはありましたが、 そのツールを作ったこと自体忘れてたぐらい記憶が抜け落ちていたので、改めて Go に再入門しました。

作ったもの

使い方は README.md 読んでください(まだ不十分だと思いますが、分からない点は issue に上げてくれると助かります…)。

github.com

各種バイナリは GitHub release のページにあります(watchevent コマンドしか入ってません)。

きっかけ

自分は Subsonic というストリーミングサーバーを自宅に立てて運用してますが、 いくつか不満がありました。

  1. flac ファイル特有の現象なのかは分からないが)flac ファイルを再生すると曲が終わっても次の曲に行ってくれない
    • 曲の時間が明らかに長かったりするので、終わりが正しく取得できてない?
  2. 音楽フォルダのスキャンが一日一回に限られている
    • 曲が追加されたら一定時間後に自動的にスキャンしてほしい
  3. 最近のバージョンのコードはクローズド(version 6.0-beta1 以降)

Subsonic に不満があったので OSS 版の Libresonic をインストールしてみた - Humanity

ちなみに上記記事では同じくストリーミングサーバの Libresonic に移行したと書いてるのですが、色々問題点があって結局戻ってきました。 それについては気が向いたら詳しく別記事で書きますが、なんと言っても第一の理由は今回利用する API が Libresonic では対応してなかったからです。 それが音楽フォルダのスキャンを開始する API です。

Subsonic では音楽フォルダに mp3 ファイルをポンと置いてもすぐ Web に現れる訳ではなく、 音楽フォルダのスキャンを行う必要があります。 しかしこのスキャンのタイミングがデフォルトでは夜の3時のみで、不便なことに定期スキャンは1日1回だけという制限があります(時間は変えられます)。

もちろんこれでは不便すぎるので Subsonic にはアップロード機能があって、 アップロードした音楽ファイルであれば即座に認識されます。 しかしアップロードするために ZIP で固めるのが面倒だったり、 何となくディレクトリ構造は手動で決めたいという欲求から自分はアップロード機能は使ってません。

しかしそれでは手元にある音楽ファイルを音楽フォルダに移動してすぐ聴きたいという時に不便です。 幸いスキャンは設定画面から手動でも行うことができるので、音楽フォルダに移動するたびにわざわざスキャンのボタンをポチっていましたが、できれば自動化したい。

そこで SubsonicREST API のページを眺めていると前述のスキャンを行うエンドポイントがあると書いてありました。 ならファイルの変更を検知して curl で叩くツール作ればいいのでは?ということで作りました。

Systemd で管理する

自分は watchevent は Systemd で管理して使ってますので、 ついでにそのインストール手順を書きます。

まずバイナリを GitHub release のページからダウンロードしてきて、 どこでもいいですが /opt/watchevent に置きます。

$ # ダウンロード URL は OS, arch によって変えてください
$ curl -LO https://github.com/tyru/go-watchevent/releases/download/v0.0.1/watchevent-v0.0.1-linux-amd64.tar.gz
$ tar xzf watchevent-v0.0.1-linux-amd64.tar.gz
$ sudo mkdir /opt/watchevent/
$ sudo install -o root -g root -m 0755 watchevent-v0.0.1-linux-amd64/watchevent /opt/watchevent/watchevent

次に各設定ファイルを作ります。

/etc/systemd/system/watchevent.service

[Unit]
Description=Watchevent

[Service]
Type=simple
EnvironmentFile=-/etc/default/watchevent
ExecStart=/opt/watchevent/watchevent -c /etc/watchevent/watchevent.yml $DIRECTORY
PrivateTmp=true

[Install]
WantedBy=multi-user.target

/etc/default/watchevent

DIRECTORY="-d {監視対象のディレクトリ}"

ちなみにこんな風に2個以上監視することもできます。

DIRECTORY="-d dir1 -d dir2"

/etc/watchevent/watchevent.yml

action:
  - name: post-subsonic-rescan
    on: [all]
    # 30s after latest file change, access to Subsonic API
    interval: 30s
    interval_action:
      - on: [all]
        do: cancel
    run: curl 'http://localhost:4040/rest/startScan?u=user&t=token&s=salt&v=1.15.0&c=watchevent-curl'

log:
  level: "info"
  encoding: "console"
  #encoding: "json"
  encoderConfig:
    messageKey: "msg"
    levelKey: "level"
    timeKey: "time"
    nameKey: "name"
    callerKey: "caller"
    stacktraceKey: "stacktrace"
    levelEncoder: "capital"
    timeEncoder: "iso8601"
    durationEncoder: "string"
    callerEncoder: "short"
  outputPaths:
    - "stdout"
  errorOutputPaths:
    - "stderr"

ちなみに Subsonic API の解説もしておくと、curl コマンドの引数の URL の token はこんな風に求めます。

$ echo -n '{パスワード}{適当に決めたsalt}' | md5sum 

で出力された MD5 値を token に指定します。 うまくいかなければ u (username), p (password), c (client) パラメータだけでもいけます(がセキュリティ上よろしくない)。

上記設定ファイルを作った後、

$ sudo systemctl daemon-reload
$ sudo systemctl enable watchevent
$ sudo systemctl start watchevent

として起動します。 ちゃんと動作してるかログが見たい方は

$ journalctl -ef -u watchevent

とかすると watchevent の出力が tail -f されるので眺めるといいかもしれません。

Go 言語の range は2番目の返り値に同じ参照を返す場合がある

言葉で説明するのだるいので次のコードを見てください。

package main

import (
    "fmt"
)

func main() {
    array := []int{1, 2, 3}
 
    // これだと &a が同じアドレスになってしまうことがある
    // (range は array[i] のコピーを返す)
    for i, a := range array {
        fmt.Printf("%d: %p\n", i, &a)
    }
    // こうするか
    fmt.Println("----------------------------")
    for i := range array {
        a := array[i]
        fmt.Printf("%d: %p\n", i, &a)
    }
    // こうしないといけない
    fmt.Println("----------------------------")
    for i := range array {
        fmt.Printf("%d: %p\n", i, &array[i])
    }
}

for i, a := range ... { とした時に &a が毎回同じアドレスになってしまうことがある、ということです(結局言葉で説明…)。 あくまで range の2番目の返り値は配列の各要素へのポインタではなく値を取得したい場合に使うらしいです。 なので以下のコードはダメ。

package main

import (
  "fmt"
)

type MyObj struct {
  id int
}

func main() {
  array := []MyObj{MyObj{id: 1}, MyObj{id: 2}, MyObj{id: 3}}

  // id >= 2 の要素だけ抜き出す
  filtered := filterGreaterThan1(array)
  // 出力すると自分の環境だと 3 が2回表示された
  for _, a := range filtered {
    fmt.Println(a.id)
  }
}

func filterGreaterThan1(array []MyObj) []*MyObj {
  filtered := make([]*MyObj, 0, len(array))
  for _, a := range array {
    if a.id > 1 {
      // a は同じアドレスの可能性があるため &a を持ちまわると同じ参照を指してしまうことがある
      filtered = append(filtered, &a)
    }
  }
  return filtered
}

Special Thanks

Subsonic に不満があったので OSS 版の Libresonic をインストールしてみた

ずっと Subsonic というストリーミングサーバーを使っていた。 1ドル /月 払えば Android アプリでも接続できるようになる。 概ね満足だったんだけどそれでもいくつか不満があった。

  1. flac ファイル特有の現象なのかは分からないが)flac ファイルを再生すると曲が終わっても次の曲に行ってくれない
    • 曲の時間が明らかに長かったりするので、終わりが正しく取得できてない?
  2. 音楽フォルダのスキャンが一日一回に限られている
    • 曲が追加されたら一定時間後に自動的にスキャンしてほしい
  3. 最近のバージョンのコードはクローズド(version 6.0-beta1 以降)

これらの問題点を解決するためにLibresonic という SubsonicOSS 版を試してみた。

ちなみに 2 に関しては解決しなかったが、スキャンを行う REST API はあるようなので、曲が追加されたら REST API 叩くスクリプトを書けば解決しそう。 *1 (追記 2017/8/27 0:30)勘違いでした。Subsonic にはありますが現在の Libresonic にはありません

Java 8+ のインストール

$ sudo yum install java-1.8.0-openjdk
$ java -version
openjdk version "1.8.0_141"
OpenJDK Runtime Environment (build 1.8.0_141-b16)
OpenJDK 64-Bit Server VM (build 25.141-b16, mixed mode)

java-1.8.0-openjdk を入れれば自動的に Java 8 になってるはず。 なってなかったら alternatives コマンドで Java 8 を選ぶようにしてください。 以下は Java 7 と 8 が入ってる環境の図。

$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
   1           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.141-2.6.10.1.el7_3.x86_64/jre/bin/java)
*+ 2           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.el7_3.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:

Standalone バージョンのインストール

WAR を持ってきて java -jar {war} すれば動くやつです。 それを systemd で動かすようにします。

まずユーザの作成。

$ sudo useradd --system libresonic
$ id libresonic
uid=994(libresonic) gid=992(libresonic) groups=992(libresonic)

次に必要なディレクトリを作りつつ WAR をダウンロードしてインストール。

$ sudo mkdir /opt/libresonic
$ curl -LO https://libresonic.org/release/libresonic-v6.2.war
$ sudo install -o root -g root -m 644 libresonic-v6.2.war /opt/libresonic/libresonic.war

$ sudo mkdir /var/libresonic/
$ sudo chown -R libresonic:libresonic /var/libresonic/

次に systemd の設定ファイルを作成。

$ vim /etc/default/libresonic
JAVA_HOME=/usr/lib/jvm/jre

$ vim /etc/systemd/system/libresonic.service
[Unit]
Description=Libresonic
After=network.target

[Service]
Type=simple
EnvironmentFile=-/etc/default/libresonic
ExecStart=/usr/bin/java -jar /opt/libresonic/libresonic.war
User=libresonic
Group=libresonic
PrivateTmp=true
Restart=always

[Install]
WantedBy=multi-user.target

設定ファイルをロード。

$ sudo systemctl daemon-reload

ffmpeg のインストール

qiita.com

インストールした後、以下の手順を実行

$ sudo mkdir /var/libresonic/transcode
$ cd /var/libresonic/transcode
$ ln -s /usr/bin/ffmpeg
$ ls -alh
lrwxrwxrwx 1 user user   15 mai    4 19:57 ffmpeg -> /usr/bin/ffmpeg

起動

いよいよ起動ですが、結構時間かかります。 HP ProLiant MicroServer (Gen8 ではない) で 74秒 ぐらい。

都度 sudo systemctl status -l libresonic するのもだるいので、 sudo systemctl start libresonic したら journalctl -ef -u libresonic で起動するまでログを眺めておくといいでしょう。

$ sudo systemctl start libresonic
$ journalctl -ef -u libresonic

初回は必要なファイル作成や DB の初期化などでだばーっとログが出ます。 起動したらこんなログが出ます。

2017-08-21 00:43:55.960  INFO --- o.l.player.boot.Application              : Started Application in 72.08 seconds (JVM running for 74.073)

起動後 /var/libresonic がこんな感じになってました。

$ ls -F /var/libresonic/
db/  libresonic.log  libresonic.properties  lucene2/  rollback.sql

Could not verify the provided CSRF token because your session was not found.

公式のインストール手順通りにインストールして java -jar libresonic.war と実行してみたのだが、Web UI でログイン後にこんなページに飛ばされてしまう。

f:id:tyru:20170821011158p:plain

ログファイル(/var/libresonic/libresonic.log)を見ても特にエラーは出ていなかった。

Tomcat のインストールしたら直った?

$ sudo yum install tomcat
$ sudo yum remove tomcat tomcat-lib

とかやってたら正常にログインできるようになってしまった。謎。

========================================================================================================================================================================================
 Package                                                 アーキテクチャー                    バージョン                                      リポジトリー                          容量
========================================================================================================================================================================================
インストール中:
 tomcat                                                  noarch                              7.0.69-12.el7_3                                 updates                               89 k
依存性関連でのインストールをします:
 apache-commons-collections                              noarch                              3.2.1-22.el7_2                                  base                                 509 k
 apache-commons-daemon                                   x86_64                              1.0.13-6.el7                                    base                                  54 k
 apache-commons-dbcp                                     noarch                              1.4-17.el7                                      base                                 167 k
 apache-commons-logging                                  noarch                              1.1.2-7.el7                                     base                                  78 k
 apache-commons-pool                                     noarch                              1.6-9.el7                                       base                                 113 k
 avalon-framework                                        noarch                              4.3-10.el7                                      base                                  88 k
 avalon-logkit                                           noarch                              2.1-14.el7                                      base                                  87 k
 ecj                                                     x86_64                              1:4.2.1-8.el7                                   base                                 1.4 M
 geronimo-jms                                            noarch                              1.1.1-19.el7                                    base                                  31 k
 geronimo-jta                                            noarch                              1.1.1-17.el7                                    base                                  20 k
 javamail                                                noarch                              1.4.6-8.el7                                     base                                 758 k
 log4j                                                   noarch                              1.2.17-15.el7                                   base                                 443 k
 tomcat-el-2.2-api                                       noarch                              7.0.69-12.el7_3                                 updates                               80 k
 tomcat-jsp-2.2-api                                      noarch                              7.0.69-12.el7_3                                 updates                               93 k
 tomcat-lib                                              noarch                              7.0.69-12.el7_3                                 updates                              3.8 M
 tomcat-servlet-3.0-api                                  noarch                              7.0.69-12.el7_3                                 updates                              211 k
 xalan-j2                                                noarch                              2.7.1-23.el7                                    base                                 1.9 M
 xerces-j2                                               noarch                              2.11.0-17.el7_0                                 base                                 1.1 M
 xml-commons-apis                                        noarch                              1.4.01-16.el7                                   base                                 227 k
 xml-commons-resolver                                    noarch                              1.2-15.el7                                      base                                 108 k

このうちのどれかが必要なパッケージだったのか?*2 今となっては謎。雑。

*1:でもどうせなら Libresonic 本体にそういう機能があってほしいのでいつか pull request 送りたい

*2:そもそも入るバージョンは Tomcat 7 だけど公式ドキュメントで説明されてるディレクトリ名が tomcat8 なので 8 のはず…(明確にどのバージョン使えとは書いてない…)