2016/11/18

Linuxでファイルの変更を監視する(コマンドとGLib)

ファイルの変更を監視する方法についてを扱う。

以前、Pythonプログラムでファイルの変更を監視する方法についてを2009年の12月と2010年の3,4月に扱っているが、ここではLinuxが提供している機能についてとGIOライブラリを用いたOS非依存な方法(言語はPythonとVala)についてを扱い、タイムスタンプの比較を繰り返し行う方法は扱わない。

  1. Linuxのファイル変更通知の仕組みを利用する
    1. 監視を行うコマンド
  2. GLibの監視機能を用いてファイルを監視する
    1. Python(PyGI)での使用例
    2. Valaでの使用例

Linuxのファイル変更通知の仕組みを利用する

Linuxには “inotify” と呼ばれる機能が存在し、ファイルやディレクトリの変更があったときにプログラム内でその通知を受け取ることができ、変更の監視に役立つ。同機能はLinuxカーネル固有の命令としてC言語のプログラムからinotify(7)で説明されている通りに扱える他、専用のツール群(Debian/Ubuntuでは “inotify-tools” パッケージ)を用いて端末シェルからコマンドとして監視機能を用いることもできる。

この機能はLinux固有のため、他のカーネルを使用したOSでは使用できない。

監視を行うコマンド

コマンド用途
inotifywait対象に対して操作が行われるまで待機(操作が行われると正常終了)
inotifywatch対象に対して行われた各操作の統計情報を蓄積し、終了時にそれを表示

-e modifyオプションを付けると、内容の変更以外の操作が行われても通知は行われない(変更の監視に便利)。

特定のファイルの変更を監視し続ける(変更時に自動的に処理が行われる状態を保つ)にはループ処理を行って繰り返しinotifywaitを実行する。

下はファイル内容の変更に対して監視を行う簡単なシェルスクリプト例。

[任意]ファイル名:filewatcher.sh
#! /bin/sh

# inotify-toolsのinotifywaitを用いて単一ファイルの変更を監視
# 引数に対象ファイルのパスを指定して使う
#
# inotifyの仕組みはLinuxカーネル独自の機能なので
# 他のカーネルを使用したOSでは使用不可

if ! which inotifywait >/dev/null 2>&1; then
  echo "inotify-tools not installed." >&2
  exit 1
fi

if test ${#} -eq 0; then
  printf "USAGE: %s [FILE]\n" "${0}"
  exit 1
fi

if ! test -f "${1}"; then
  echo "File \"${1}\" does not exist." >&2
  exit 1
fi

while true; do
  # inotifywaitコマンドを用いると、-eオプションで指定したイベント(操作の種類)が
  # 発行されるまで待機し、削除されたりエラーが発生したりすると1が返る
  # ここでは変更(modify)のみ通知することにする
  inotifywait -qqe modify "${1}"
  if test ${?} -eq 0; then
    # ファイルが変更されたことを確認
    # ここにファイル変更時に自動的に実行したい処理を記述することで
    # ファイルが変更されたときに自動的にそのファイルに処理を行うことができる
    echo "** File \"${1}\" is modified **"
  else
    exit 1
  fi
done

GLibの監視機能を用いてファイルを監視する

GLibライブラリの一部であり、入出力に関係した処理を扱うGIOライブラリの中には、ファイルやディレクトリの監視機能が存在する。これはLinuxのようにOSが監視機能を持っている場合にはそれを用いて、そうでない場合には一定間隔ごとに状態を調べる形で監視を行うようになっている。

下はプログラム内における使い方を簡単に示したものとなる。

  1. パス名に対するGFileオブジェクトを作成
  2. 生成したオブジェクトのmonitor()系関数により監視を行うオブジェクトを得る(1番目の引数のフラグ指定により一部の挙動を指定できる)
  3. 監視を行うオブジェクトについて “changed” という名前のGObjectシグナルをハンドラ関数(引数は4つもしくはユーザデータ込みの5つ)と関連付ける
  4. GLibのメインループを作成してこれを動かす(GTK+アプリケーションでは既にGLibに基づいたメインループがあるためこの追加操作は不要)
  5. 監視対象に対して操作が行われると自動的にハンドラ関数が呼ばれる

ハンドラ関数の4番目の引数には操作の種類を示す定数値が渡され、これを用いて分岐を行うことで、特定の操作(例:内容の変更)でのみ処理を行うようにできる。この値は

  • PyGIではGio.FileMonitorFlags以下
  • ValaではGLib.FileMonitorFlags以下

に存在し、値の一覧はGIOライブラリのC言語リファレンスにある “enum GFileMonitorEvent” にある。また、監視を行うオブジェクトを得る際に指定するフラグについても同様にリファレンスの “enum GFileMonitorFlags” に一覧がある。

Python(PyGI)での使用例

[任意]ファイル名:filemon-gio.py
#! /usr/bin/python

from __future__ import print_function

import sys
try:
  from gi.repository import Gio, GLib
except ImportError:
  sys.exit ('typelib for GIO not found')


def filemon_changed_cb (filemon, gfile, other_gfile, event_type):
  if event_type == Gio.FileMonitorEvent.CHANGED:
    print ('** File {0} is modified **'.format (gfile.get_path ()))

def main ():
  if len (sys.argv) < 2:
    sys.exit ('USAGE: {0} [FILE]'.format (__file__))
  path = sys.argv[1]
  mon = Gio.File.new_for_path (path).monitor (Gio.FileMonitorFlags.NONE, None)
  mon.connect ('changed', filemon_changed_cb)
  try:
    GLib.MainLoop ().run ()
  except KeyboardInterrupt:
    pass


if __name__ == '__main__':
  main ()

Valaでの使用例

[任意]ファイル名:filemon-gio.vala
// valac --pkg posix --pkg gio-2.0 filemon-gio.vala -o filemon-gio

using Posix;

int
main (string[] args)
{
  if (args.length < 2)
  {
    printerr ("USAGE: %s [FILE]\n", args[0]);
    return EXIT_FAILURE;
  }
  var path = args[1];
  try
  {
    var mon = GLib.File.new_for_path (path).monitor (GLib.FileMonitorFlags.NONE, null);
    mon.changed.connect ((filemon, gfile, other_gfile, event_type) =>
    {
      if (event_type == GLib.FileMonitorEvent.CHANGED)
        print ("** File %s is modified **\n", gfile.get_path ());
    });
    new GLib.MainLoop ().run ();
  }
  catch (GLib.Error e)
  {
    printerr ("%s\n", e.message);
    return EXIT_FAILURE;
  }
  return EXIT_SUCCESS;
}
使用したバージョン:
  • inotify-tools 3.14
  • GLib 2.50.0
  • Python 2.7.12, 3.5.2
  • Vala 0.32.1