Top > Linux > lsyncd イベントとシステムコマンド連携

lsyncd を使ったファイル変更をトリガーとする OS コマンド連携

lsyncd のイベントを検出してシステムコマンド実行する設定ファイルを作成する
システムコマンド連携には Layer3 の設定サンプルで実装可能であるが、もうすこし細かいハンドリングを行なわせるために Layer2 の内容を使用してハンドリングする。

Layer2 の機能を使う設定ファイル(プログラム)のサンプル

Lsyncd の設定ファイルはただの設定値の羅列でなく LUA というスクリプト言語で記述することができる。
Config Layer 2: Default Config

システムコマンド連携 サンプル要件

機能説明用としてファイル変更とのシステムコマンドが連携が発生する以下の機能を記述する

  1. ディレクトリ /tmp/src 以下のファイルの変更を /tmp/trace に同名の仮ファイルとして伝播する
  2. 仮ファイルはサイズゼロでオリジナルファイルのタイムスタンプを複製する
  3. 伝搬する対象元ファイルは任意に設定可能な拡張子に制限する
  4. 制限) 親、サブディレクトリの操作(作成、移動、削除など)は動作対象外とする(※1)
  5. 補足) 誤改変を防ぐために設定セクションとプログラムを分ける

(※1) このサンプルでは伝搬先に (3.) で制限した拡張子をもつサブディレクトリがあり、かつ、空の状態のときにサブディレクトリを消す操作ができてしまう。

拡張子を ".txt" に制限した場合の動き

イベント
作成) /tmp/src/a.txt           --->  作成) /tmp/trace/a.txt
作成) /tmp/src/b.zip           --->  なにもしない
作成) /tmp/src/c.txt           --->  作成) /tmp/trace/c.txt
移動) /tmp/src/c.txt -> d.txt  --->  移動) /tmp/trace/c.txt ---> d.txt
移動) /tmp/src/d.txt -> e.bak  --->  削除) /tmp/trace/d.txt
移動) /tmp/src/e.bak -> e.txt  --->  作成) /tmp/trace/e.txt
削除) /tmp/a.txt               --->  削除) /tmp/trace/a.txt
--
-- User configuration file for lsyncd.
--
-- Layer2 example for os.execute.
--  
--
--  BEGIN Customize <rivus.jp>
--
local ltrim = function(s, c)
  return string.match(s, "[^"..c.."].*")
end
local rtrim = function(s, c)
  return string.match(s, ".*[^"..c.."]")
end
local bracket = function(s)
  return "["..tostring(s) .."]"
end  
local split = function(s, sep)
  local res={};
  if (sep == nil) then
    return {s}
  end
  for p in string.gmatch(s, "([^"..sep.."]+)") do
    res[#res+1] = p
  end
  return res
end
-- log event info (debug)
local event_info = function(e, ...)
  local msg = bracket(...)
  msg = msg.."*[evnet:" .. e.etype .."]"
  msg = msg.."[source Pathname:"..e.sourcePathname.. "]"
  msg = msg.."[target Pathname"..e.targetPathname.. "]"
  log("All others", msg)
end
--
MyCustom = {}
MyCustom.new = function()
  local obj = {}
  --
  -- variable
  --
  obj.command_path = "/usr/bin"
  obj.include = ""
  obj.extensions = {}
  --
  -- function
  --
  -- contains (extension check)
  obj.contains = function(fname)
    if (#obj.extensions == 0) then
      return true
    end
    for i, m in ipairs(obj.extensions) do
      if (string.match(fname, ".+"..m.."$")) then
        return true
      end
    end
    return false
  end
  -- touch file
  obj.touch_r = function(e)
    -- event_info(e, "touch_r")
    if (e.isdir or not obj.contains(e.sourcePathname)) then
      log("All others", "skip touch ".. bracket(e.sourcePath))
    else
      local binary = obj.command_path .. '/touch'
      local bin_opt = "--reference="..e.sourcePathname
      local file = e.targetPathname
      -- *************************
      -- ** binary command here ** touch (binary ver.)
      spawn(e, binary, bin_opt, file)
      log("All others", "touch ".. bracket(file))
    end
  end
  -- remove file
  obj.remove = function(e)
    -- event_info(e, "remove")
    if (e.isdir or not obj.contains(e.sourcePathname)) then
      log("All others", "skip remove ".. bracket(e.sourcePath))
    else
      -- *********************
      -- ** OS command here **
      local ret = os.remove(e.targetPathname)
      log("All others","remove "..bracket(e.targetPathname).." = ".. bracket(ret))
    end
  end
  -- move file
  obj.move = function(oe, de)
    -- event_info(oe, "move orig")
    -- event_info(de, "move dest")
    --
    -- # mv origFile destFile
    local ofile_s = oe.sourcePathname  -- sync{source=}/origFile
    local dfile_s = de.sourcePathname  -- sync{source=}/destFile
    --
    local ofile_t = oe.targetPathname  -- sync{target=}/origFile
    local dfile_t = de.targetPathname  -- sync{target=}/destFile
    --
    local in_ext_o = obj.contains(ofile_s)  -- extension check (original file)
    local in_ext_d = obj.contains(dfile_s)  -- extension check (renamed file)
 
    if (oe.isdir or (in_ext_o == false and in_ext_d == false)) then
      -- rename dir or extension not match / # mv abc.mismatch_ext1 abc.mismatch_ext2
      log("All others", "skip move ".. bracket(ofile_s).."->"..bracket(dfile_s))
    elseif (in_ext_o and in_ext_d) then
      -- rename / # mv abc.txt abc.log
      -- *********************
      -- ** OS command here **
      local ret = os.rename(ofile_t, dfile_t)
      log("All others", "move ".. bracket(ofile_t).."->"..bracket(dfile_t).." = "..bracket(ret))
    elseif (in_ext_d == false) then
      -- target file remove (extension no match) / # mv abc.txt abc.mismatch_ext
      -- *********************
      -- ** OS command here **
      local ret = os.remove(ofile_t)
      log("All others", "move -> remove "..bracket(ofile_t).." = "..bracket(ret))
    elseif (in_ext_o == false) then
      -- target file touch (extension match) / # mv abc.mismatch_ext abc.txt
      local binary = obj.command_path .. "/touch"
      local bin_opt = "--reference="..dfile_s
      local file = dfile_t
      local shell_cmd = binary .. " "..bin_opt.." "..file
      -- ************************
      -- ** shell command here ** touch (shell ver.)
      spawnShell(oe, shell_cmd)
      log("All others","move -> touch ".. bracket(dfile_t))
    else
      log("Error","move -> nop ".. bracket(ofile_s).. " -> " .. bracket(dfile_s))
    end
  end
  --
  -- setttings
  obj.settings = function(mySettings)
    if (mySettings.command_path ~= nil) then
      local s = rtrim(mySettings.command_path, "/ ")
      obj.command_path = s;
    end
    if (mySettings.include ~= nil) then
      local s = ltrim(rtrim(mySettings.include, ", "),", ")
      s = string.gsub (s, "%.", "%%.")    -- escape dotmark
      obj.include = s
      obj.extensions = split(s, ",")
    end
    log("Normal","command path = "..bracket(obj.command_path))
    log("Normal","include filter = "..bracket(obj.include))
  end
  --
  -- event handler
  obj.myHandler={
    delay = 0,
    maxProcesses = 1,
    -- onCreate = obj.touch_r,
    onModify = obj.touch_r,
    onMove   = obj.move,
    onDelete = obj.remove,
    onStartup= 'if [ "$(ls -A ^source)" ];then rsync -a --include "*/" --exclude "*" ^source ^target;fi'
  }
  return obj
end
myCustom = MyCustom.new()
--
-- MyCustom settings
--  see also : MyCustom::settings
--
myCustom.settings {
  command_path = "/bin/",
  include=".txt,.log,.doc"  -- only file extension
}
--  END Customize
--
--
-- lsyncd config
--
--
settings {
    logfile = "/var/log/lsyncd.log",
    statusFile = "/tmp/lsyncd.stat",
    statusInterval = 10,
}
--
sync {
  myCustom.myHandler,
  source = "/tmp/my_src",
  target = "/tmp/my_trace",
}

システムコマンドを実行するには LUA 組み込み関数 os.execute を呼び出す方法があるが
lsyncd 側の Layer2 に記載がある spawn 関数(バイナリコマンド呼び出し) および spawnShell 関数(bash -c 呼び出し) を使用している。

標準の os.execute を使用するとコマンドが完了するまで lsyncd デーモン全体がブロックされてしまうが lsyncd の spawn / spawnShell では子プロセスで実行させ、すぐに制御が返却されると記述されている。 逆言うとコマンド自体のリターンステータスがプログラムからわからないという諸刃。ログファイルにはコードが出力されている。

os.rename や os.remove については制限する記載は見当たらない。execute のように終了時間が予測できない類のものではないので問題にならないのかもしれない。

注意

onAction で呼び出された関数内でエラーが発生すると lsyncd のプロセスも一緒に終了してしまいます。
このサンプル中の関数は nil 引数へ対応など作りを雑なのでフルスクラッチすることを勧めます。

コマンドラインでの動作確認

設定ファイルを自作する場合 service でいきなり確認をするのではなくコマンド実行でデバッグすると良い。

# lsyncd -help 
  ヘルプ
 
# lsyncd -nodaemon /tmp/my_lsyncd.conf
  Normal と Error のログが標準出力にも表示される
 
# lsyncd -nodaemon -log all /tmp/my_lsyncd.conf
  Normal Error に加え All others ログも出る

ログのカテゴリは "Error" / "Normal" / "All others" があり log 関数の第一引数に左記と完全に一致する文字列で指定する。

LAU の感想

有名なゲーム関連やルータあたりでも使われている言語という事ですが
今回 LUA というスクリプト言語を使っての感想ですが独自の文字列のパターンマッチングと nil 操作のエラーの癖で少し苦労させられました。
Lsyncd で簡単に書けてしまうので余計な機能を追加したせいもあります。Layer4 と Layer3 でほとんど処理がカバーできると思います。

日本語での解説資料は多くはないようです。非公式ですが言語マニュアルを日本語化した方いるのでそちらも参考にすると良いかもしれません。

スクリプト言語でプログラムしたことのある方ならばすぐに取り組めると思います。
日本語は 8bit 単位のバイト列のように見えてしまうのでファイル名自体を操作するなどの場合には要注意です。

関連事項

lsyncd イベントとシステムコマンド連携の関連トピックス

日本オラクル
■ 日本オラクル 株式会社
■ オラクルマスター資格 (オラクルマスターとは
■ 会員制(無料)の公式技術サイト