ReadyMedia の inotify 監視が中断してしまう事象を改善

# 概要

[ReadyMedia](http://minidlna.sourceforge.net/) は、inotify の仕組みを利用してファイルシステムのイベントを監視し、メディアディレクトリ内のファイル追加・削除を動的に認識します。しかし、メディアディレクトリのファイルシステムがアンマウントされると、イベント監視が中断され、再度マウントしてもイベント監視は中断されたままとなります。メディアディレクトリを automount でマウントしている場合は特に発生しやすくなります。

本記事では、イベント監視を再開させるための改造を行います。

# 対象ソフトウェア

ReadyMedia - minidlna-1.3.0

# 改造ポイント

- inotify イベント監視スレッド
  inotify のイベントを監視するための無限ループが監視専用のスレッドとして動作しています。この監視スレッドの処理を再開することが改造のポイントです。

- 無限ループの抜け出しと監視スレッドの処理再開
  定常状態では主に無限ループ内の poll() システムコールで inotify イベントを待っている状態です。シグナルでこの poll() を中断して無限ループを抜け出し、監視スレッドの先頭に戻って処理を再開するように改造します。

- メインスレッドと監視スレッドのシグナル処理
  シグナルには、既存では無視されるように設定されている SIGUSR2 を使用します。メインスレッドで SIGUSR2 を受けたあと、監視スレッドに SIGUSR2 を通知することで poll() を中断します。

# 改造詳細

以下の 4 つのファイルを改造します。

- minidlna.c
  ReadyMedia の本体です。
  メインスレッドの処理が実装されています。

- monitor.c
  inotify イベントを監視する監視スレッドです。

- upnpglobalvars.c
  ReadyMedia 全体をスコープとするグローバル変数が定義されています。

- upnpglobalvars.h
  ReadyMedia 全体をスコープとするグローバル変数が宣言されています。

## minidlna.c

4箇所改造します。

### 改造【1】

main() 内で定義されている監視スレッドの識別子です。シグナルハンドラから参照できるようにグローバル変数として定義し直します。minidlna.c 内だけで参照するため upnpglobalvars.c には定義しません。

```diff_c
@@ -99,16 +99,18 @@
 #include "tivo_utils.h"
 #include "avahi.h"

 #if SQLITE_VERSION_NUMBER < 3005001
 # warning "Your SQLite3 library appears to be too old!  Please use 3.5.1 or newer."
 # define sqlite3_threadsafe() 0
 #endif

+static pthread_t inotify_thread = 0;
+
 static LIST_HEAD(httplisthead, upnphttp) upnphttphead;

 /* OpenAndConfHTTPSocket() :
  * setup the socket used to handle incoming HTTP connections. */
 static int
 OpenAndConfHTTPSocket(unsigned short port)
 {
        int s;
```

### 改造【2】

シグナルハンドラです。メインスレッドが受信したときだけ監視スレッドにシグナルを送信します。送信の前にリスタートフラグ restart_inotify を立てておきます。この restart_inotify は monitor.c でも使用するため、upnpglobalvars.c で定義します（後述）。

```diff_c
@@ -207,16 +209,26 @@
 {
        signal(sig, sigusr1);
        DPRINTF(E_WARN, L_GENERAL, "received signal %d, clear cache\n", sig);

        memset(&clients, '\0', sizeof(clients));
 }

 static void
+sigusr2(int sig)
+{
+       if (pthread_self()!=inotify_thread) {
+               DPRINTF(E_WARN, L_GENERAL, "received signal %d, request to restart inotify thread\n", sig);
+               restart_inotify = 1;
+               pthread_kill(inotify_thread, SIGUSR2);
+       }
+}
+
+static void
 sighup(int sig)
 {
        signal(sig, sighup);
        DPRINTF(E_WARN, L_GENERAL, "received signal %d, reloading\n", sig);

        reload_ifaces(1);
        log_reopen();
 }
```

### 改造【3】

シグナルハンドラを設定します。既存実装では SIGUSR2 には SIG_IGN が設定されていますが、これを改造【2】で定義した sigusr2 に変更します。

```diff_c
@@ -1043,17 +1055,17 @@
        if (sigaction(SIGTERM, &sa, NULL))
                DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGTERM");
        if (sigaction(SIGINT, &sa, NULL))
                DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGINT");
        if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
                DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGPIPE");
        if (signal(SIGHUP, &sighup) == SIG_ERR)
                DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGHUP");
-       if (signal(SIGUSR2, SIG_IGN) == SIG_ERR)
+       if (signal(SIGUSR2, sigusr2) == SIG_ERR)
                DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGUSR2");
        signal(SIGUSR1, &sigusr1);
        sa.sa_handler = process_handle_child_termination;
        if (sigaction(SIGCHLD, &sa, NULL))
                DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGCHLD");

        if (writepidfile(pidfilename, pid, uid) != 0)
                pidfilename = NULL;
```

### 改造【4】

改造【1】でグローバル変数として定義し直した監視スレッドの識別子を削除します。

```diff_c
@@ -1098,17 +1110,16 @@
        int smonitor = -1;
        struct upnphttp * e = 0;
        struct upnphttp * next;
        struct timeval tv, timeofday, lastnotifytime = {0, 0};
        time_t lastupdatetime = 0, lastdbtime = 0;
        u_long timeout; /* in milliseconds */
        int last_changecnt = 0;
        pid_t scanner_pid = 0;
-       pthread_t inotify_thread = 0;
        struct event ssdpev, httpev, monev;
 #ifdef TIVO_SUPPORT
        uint8_t beacon_interval = 5;
        int sbeacon = -1;
        struct sockaddr_in tivo_bcast;
        struct timeval lastbeacontime = {0, 0};
        struct event beaconev;
 #endif
```

## monitor.c

6 箇所改造します。

### 改造【1】

既存実装では、監視スレッドの無限ループを抜けるとリソースが解放されてスレッドが終了します。スレッドを再開するように改造することで、リソースの解放が複数回実行されることになりますが、既存実装のままだと free() 済アドレスに対して二重に free() が呼ばれます。これを回避するための改造です。

```diff_c
@@ -246,16 +246,18 @@
                last_w = w;
                inotify_rm_watch(fd, w->wd);
                free(w->path);
                rm_watches++;
                w = w->next;
                free(last_w);
        }

+       watches = NULL;
+
        return rm_watches;
 }
 #endif

 int
 monitor_remove_file(const char * path)
 {
        char sql[128];
```

### 改造【2】

quitting は、ReadyMedhia 終了時に立てられるフラグです。リスタートフラグの restart_inotify も同じように扱います。

```diff_c
@@ -541,17 +543,17 @@
        dir_types = valid_media_types(path);

        ds = opendir(path);
        if( !ds )
        {
                DPRINTF(E_ERROR, L_INOTIFY, "opendir failed! [%s]\n", strerror(errno));
                return -1;
        }
-       while( !quitting && (e = readdir(ds)) )
+       while( !quitting && !restart_inotify && (e = readdir(ds)) )
        {
                if( e->d_name[0] == '.' )
                        continue;
                esc_name = escape_tag(e->d_name, 1);
                snprintf(path_buf, sizeof(path_buf), "%s/%s", path, e->d_name);
                switch( e->d_type )
                {
                        case DT_DIR:
```

### 改造【3】

前半はシグナルをブロックする処理です。SIGUSR2 を受信できるように、ブロックされるシグナル集合から SIGUSR2 を削除します。

後半が監視スレッドの実処理の開始位置です。inotify_init() で inotify 監視の初期化しています。監視スレッドの処理を再開するためのラベルを定義し、リスタートフラグ restart_inotify を 0 で初期化します。

```diff_c
@@ -624,18 +626,22 @@
        char path_buf[PATH_MAX];
        int length, i = 0;
        char * esc_name = NULL;
        struct stat st;
        sigset_t set;

        sigfillset(&set);
        sigdelset(&set, SIGCHLD);
+       sigdelset(&set, SIGUSR2);
        pthread_sigmask(SIG_BLOCK, &set, NULL);

+restart:
+       restart_inotify = 0;
+
        pollfds[0].fd = inotify_init();
        pollfds[0].events = POLLIN;

        if ( pollfds[0].fd < 0 )
                DPRINTF(E_ERROR, L_INOTIFY, "inotify_init() failed!\n");

        while( GETFLAG(SCANNING_MASK) )
        {
```

### 改造【4】

改造【2】と同じです。

```diff_c
@@ -644,17 +650,17 @@
                sleep(1);
        }
        inotify_create_watches(pollfds[0].fd);
        if (setpriority(PRIO_PROCESS, 0, 19) == -1)
                DPRINTF(E_WARN, L_INOTIFY,  "Failed to reduce inotify thread priority\n");
        sqlite3_release_memory(1<<31);
        lav_register_all();

-       while( !quitting )
+       while( !quitting && !restart_inotify )
        {
                int timeout = -1;
                if (next_pl_fill)
                {
                        time_t diff = next_pl_fill - time(NULL);
                        if (diff < 0)
                                timeout = 0;
                        else
```

### 改造【5】

前半は、無限ループをを抜け出す改造です。差分表示のため欠けていますが、length は poll() システムコールの返り値です。シグナルを受けると返り値は負になり、errno には EINTR が設定されます。既存実装では無限ループを continue していますが、無限ループを抜け出すように改造します。抜け出す前にリスタートフラグ restart_inotify を立てておきます。

後半の改造は【2】と同じです。

```diff_c
@@ -668,28 +674,31 @@
                                fill_playlists();
                                next_pl_fill = 0;
                        }
                        continue;
                }
                else if( length < 0 )
                {
                        if( (errno == EINTR) || (errno == EAGAIN) )
-                               continue;
+                       {
+                               restart_inotify = 1;
+                               break;
+                       }
                        else
                                DPRINTF(E_ERROR, L_INOTIFY, "read failed!\n");
                }
                else
                {
                        length = read(pollfds[0].fd, buffer, BUF_LEN);
                        buffer[BUF_LEN-1] = '\0';
                }

                i = 0;
-               while( !quitting && i < length )
+               while( !quitting && !restart_inotify && i < length )
                {
                        struct inotify_event * event = (struct inotify_event *) &buffer[i];
                        if( event->len )
                        {
                                if( *(event->name) == '.' )
                                {
                                        i += EVENT_SIZE + event->len;
                                        continue;
```

### 改造【6】

監視スレッドを再開するための改造です。無限ループから抜け出て、改造【1】で対処したリソース解放処理が呼ばれたあとの最後の部分です。

quitting フラグが立っていない（終了が要求されていない）、かつ、リスタートフラグ restart_inotify が立っている（再開が要求されている）場合に、改造【3】の restart ラベルに戻り監視スレッドを再開します。

```diff_c
@@ -740,11 +749,17 @@
                        }
                        i += EVENT_SIZE + event->len;
                }
        }
        inotify_remove_watches(pollfds[0].fd);
 quitting:
        close(pollfds[0].fd);

+       if ( !quitting && restart_inotify )
+       {
+               DPRINTF(E_WARN, L_INOTIFY, "restart inotify thread\n");
+               goto restart;
+       }
+
        return 0;
 }
 #endif
```

## upnpglobalvars.c / upnpglobalvars.h

リスタートフラグ restart_inotify の定義と宣言です。

### 定義

```diff_c
@@ -86,5 +86,6 @@
 struct media_dir_s * media_dirs = NULL;
 struct album_art_name_s * album_art_names = NULL;
 volatile short int quitting = 0;
+volatile short int restart_inotify = 0;
 volatile uint32_t updateID = 0;
 const char *force_sort_criteria = NULL;
```

### 宣言

```diff_c
@@ -242,6 +242,7 @@
 extern struct media_dir_s *media_dirs;
 extern struct album_art_name_s *album_art_names;
 extern volatile short int quitting;
+extern volatile short int restart_inotify;
 extern volatile uint32_t updateID;
 extern const char *force_sort_criteria;
```

# 動作確認

改造が正しく動作するか確認します。

## 設定ファイル編集

inotify の動作を確認できるように、/etc/minidlna.conf に以下を設定して minidlnad を起動します。

```
log_level=inotify=debug
```

## 通常動作

ReadyMedhia 起動後、一度もファイルシウテムがアンマウントされていない通常の状態での動作を確認します。

  - ファイルシステムのマウントポイントを /spool とします（automount でマウントされているものとします）。
  - 動画のメディアディレクトリを /spool/video とします。

メディアディレクトリ内にサブディレクトリを作成します。
   
```
$ mkdir /spool/video/a
```
   
ReadyMedia のログファイルを確認すると、サブディレクトリの作成が認識されていることがわかります。
   
```
monitor.c:710: debug: The directory /spool/video/a was created.
monitor.c:155: info: Added watch to /spool/video/a [124]
```

サブディレクトリ内に動画ファイルをコピーします。

```
$ cp a.mp4 /spool/video/a/
```

動画ファイルが認識されています。

```
monitor.c:404: debug: Adding: /spool/video/a/a.mp
```

動画ファイルとサブディレクトリを削除します。

```console
$ rm /spool/video/a/a.mp4
$ rmdir /spool/video/a
```

それぞれ認識されています。

```
monitor.c:740: debug: The file /spool/video/a/a.mp4 was deleted.
monitor.c:740: debug: The directory /spool/video/a was deleted.
```

## ファイルシステムのアンマウント

ここで /spool ファイルシステムをアンマウントします。

```console
$ sudo umount /spool
```

先程と同じようにサブディレクトリを作成して動画ファイルをコピーします。アンマウントしていましたが、この操作によって自動的にマウントされます。

```console
$ mkdir /spool/video/b
$ cp b.mp4 /spool/video/b/
```

ReadyMedia のログファイルにはこれらの作成が記録されていません。inotify のイベント監視が無効化されています。

## シグナル送信

ReadyMedia のプロセスにシグナルを送信します。

```console
$ sudo pkill -USR2 minidlnad
```

シグナルを受けて監視スレッドが再開されているログが記録されています。先程作成したサブディレクトリや動画ファイルは認識されていません。

```
minidlna.c:220: warn: received signal 12, request to restart inotify thread
monitor.c:759: warn: restart inotify thread
```

改めてサブディレクトリを作成して動画ファイルをコピーします。

```console
$ mkdir /spool/video/c
$ cp c.mp4 /spool/video/c/
```

それぞれ認識されており、改造が有効であることが分かります。

```
monitor.c:155: info: Added watch to /spool/video/c [124]
monitor.c:404: debug: Adding: /spool/video/c/c.mp4
```

なお、先程認識されなかったサブディレクトリ b にファイルをコピーしても認識されません。

# 使い方

動作確認の通り、ファイルシステムがアンマウントされたあとシグナルを送信するまでの間に発生した inotify イベントは拾われません。そのため、本改造の使用方法は以下のようになります。

1. ファイルシステムをマウントする（automountならディレクトを参照するだけ）。
1. シグナル SIGUSR2 を送信する。
1. メディアディレクトリにメディアを追加する。