filemonのファイル書き込みイベントをアプリからハンドリングしてみる

[NetBSD Advent Calendar 2021](https://qiita.com/advent-calendar/2021/netbsd) 8日目の記事です。今日は[6日目の記事](https://qiita.com/furandon_pig/items/5d324b06a0456e3080c8)で紹介した[filemon(4)](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)を利用した少し実用的なアプリを作成してみようと思います。

## ファイルイベントをトリガーにしてスクリプトを走らせる

[6日目の記事](https://qiita.com/furandon_pig/items/5d324b06a0456e3080c8)で[filemon(4)](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)を使用する簡単なサンプルを紹介していました。

サンプルでは指定したプロセスで発生したファイルイベントが `/tmp` 以下のファイルに書き出されてくるというものでした。filemonの挙動はこれで観察できるのですが、もう少し実用的なアプリが欲しいものです。

## ifwatchdコマンドっぽくしてみる

NetBSDには[ifwatchd(8)](https://man.netbsd.org/NetBSD-9.2/i386/ifwatchd.8)というコマンドがあり、ネットワークインタフェースのUP/DOWN等をトリガーにして、指定したスクリプトを走らせることができます。

`ifwatchd` コマンドに指定可能なオプションは以下のようになっています。 `-u up-script` のような形で、指定したイベントに紐づけて実行するスクリプトを指定できます。

```
     ifwatchd [-hiqv] [-A arrival-script] [-c carrier-script]
              [-D departure-script] [-d down-script] [-u up-script]
              [-n no-carrier-script] ifname(s)
```

`filemon` でも `-w write-handler-script` のように、ファイルに書き込みがあった場合に指定されたスクリプトを実行できるようにしてみます。

## 今回作成するアプリ

ここまでの内容を踏まえて、作成するアプリのインタフェースを考えてみます。とりあえずは実行したいコマンドとファイル書き込み時に実行させるスクリプトを指定できれば良さそうです。
( `-w` を指定しない場合は、単にコマンドを実行するだけ)

```sh
$ filemon_watch [-w write-handler-script] <command>
```

## filemonでの書き込みイベントの監視

`filemon` では、 `W` のイベントタイプを補足すれば書き込みイベントの補足が可能です。

| イベントタイプ | 意味 |
| --- | --- |
| `W` | 読み書きするファイルのオープン([open(2)](https://man.netbsd.org/NetBSD-9.2/i386/open.2)) |

ファイルイベントが発生するたびに監視用のファイルに追記されるため、都度ファイルをオープンして追記された部分のみをチェックする感じになります。
(ちょうど `tail -f` コマンドのような挙動になります)

アプリの大まかな構造は以下のようになります。べた書きで処理を記述していますが、指定したコマンドの実行と、ファイルイベントのチェックと書き込みイベントに紐づくスクリプトの実行が一連のコードで実現できています。

アプリのソースコードは以下のGistに置いてあります。

 * [nbsd_filemon_watch_write_event.c](https://gist.github.com/furandon-pig/c6c7c94548d7134b6f224cfdb8e5c5b0)

```c
    pid = fork();
    switch(pid) {
        case -1:
            fprintf(stderr, "cannot fork");
            break;
        case 0:
            printf("filemon: %s\n", temp_path);
            pid = getpid();
            ioctl(filemon_fd, FILEMON_SET_PID, &pid); // ファイルイベントが書き込まれるファイル。
            execvp(argv[0], argv); // 指定したコマンドを実行する。
            _exit(1);

            break;
        default:
           lseek(temp_fd, SEEK_SET, 0);

           // 指定したコマンドが終了するまでループする。
           is_done = 1;
           while (is_done) {
               fp = fopen(temp_path, "r");  // ファイルイベントが書き込まれているファイルを開く。
               if (fp) {
                   count = 0;
                   while (fgets(buf, BUFSIZ-1, fp)) {
                       if (count++ < read_line) {  // すでにチェックしたイベントはスキップする。
                           continue;
                       }

                       if (buf[0] == 'W') {  // ファイルへの書き込みイベントがあった場合。
                           // "W <pid> <file>"のフォーマットで書き込まれているのパースする。
                           // "W 521 cal.txt" => "cal.txt"
                           char *path = strchr(buf, ' ');
                           path = strchr(++path, ' ');
                           ++path;

                           switch(fork()) {
                                case -1:
                                    fprintf(stderr, "cannot fork");
                                    break;
                                case 0:
                                    // 書き込みイベントに紐づけられたスクリプトがあったら実行する。
                                    if (w_cmd) {
                                        char *script[] = { w_cmd, path, NULL };
                                        execvp(script[0], script);
                                    }
                                default:
                                    wait(&status);
                           }
                       }
                       if (buf[0] == '#') { // 指定したコマンドが終了したらループを抜ける。
                           is_done = 0;
                           break;
                       }
                   }
                   read_line += count - read_line;  // 読み込んだ行数(＝ファイルイベントをチェックした行数)を保持しておく。

                   fclose(fp);
                   usleep(700000);  // 一定時間スリープする(ここでは700ミリ秒)。
               }
           }

           wait(&status);
           close(filemon_fd);
           close(temp_fd);

           break;
    }
```

## 実行例

作成したアプリを実行してみます。 `-w` オプションで書き込みイベントをハンドリングするスクリプトはになります(単に通知ダイアログを表示するだけです)。

```sh
#!/bin/sh
notify-send -u normal 'ファイル書き込み' "$1 に書き込みがありました。"
```

シェルの実行・終了時にもファイルが書き込まれていたり、C言語プログラムをコンパイルする際には、 `.s` `.o` が生成されていることが見て取れます。

![nbsd1208_01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61137/c287fd91-c197-a730-a428-26e40c49d59d.gif)

## まとめ

[filemon(4)](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)を利用した少し実用的なアプリを作成してみました。filemonで取得可能なイベントは他にもあるため、各イベントに実行したいスクリプトを紐づけられるようにすると実用度が上がりそうです。