zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

fs_inotify.ex (5533B)


      1 require Logger
      2 
      3 defmodule FileSystem.Backends.FSInotify do
      4   @moduledoc """
      5   This file is a fork from https://github.com/synrc/fs.
      6   FileSystem backend for linux, freebsd and openbsd, a GenServer receive data from Port, parse event
      7   and send it to the worker process.
      8   Need `inotify-tools` installed to use this backend.
      9 
     10   ## Backend Options
     11 
     12     * `:recursive` (bool, default: true), monitor directories and their contents recursively
     13 
     14   ## Executable File Path
     15 
     16   The default behaivour to find executable file is finding `inotifywait` from `$PATH`, there're two ways to custom it, useful when run `:file_system` with escript.
     17 
     18     * config with `config.exs`
     19       `config :file_system, :fs_inotify, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`
     20 
     21     * config with `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` os environment
     22       FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH
     23   """
     24 
     25   use GenServer
     26   @behaviour FileSystem.Backend
     27   @sep_char <<1>>
     28 
     29   def bootstrap do
     30     exec_file = executable_path()
     31     if is_nil(exec_file) do
     32       Logger.error "`inotify-tools` is needed to run `file_system` for your system, check https://github.com/rvoicilas/inotify-tools/wiki for more information about how to install it. If it's already installed but not be found, appoint executable file with `config.exs` or `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` env."
     33       {:error, :fs_inotify_bootstrap_error}
     34     else
     35       :ok
     36     end
     37   end
     38 
     39   def supported_systems do
     40     [{:unix, :linux}, {:unix, :freebsd}, {:unix, :openbsd}]
     41   end
     42 
     43   def known_events do
     44     [:created, :deleted, :closed, :modified, :isdir, :attribute, :undefined]
     45   end
     46 
     47   defp executable_path do
     48     executable_path(:system_env) || executable_path(:config) || executable_path(:system_path)
     49   end
     50 
     51   defp executable_path(:config) do
     52     Application.get_env(:file_system, :fs_inotify)[:executable_file]
     53   end
     54 
     55   defp executable_path(:system_env) do
     56     System.get_env("FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE")
     57   end
     58 
     59   defp executable_path(:system_path) do
     60     System.find_executable("inotifywait")
     61   end
     62 
     63   def parse_options(options) do
     64     case Keyword.pop(options, :dirs) do
     65       {nil, _} ->
     66         Logger.error "required argument `dirs` is missing"
     67         {:error, :missing_dirs_argument}
     68       {dirs, rest} ->
     69         format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist
     70         args = [
     71           '-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from',
     72           '-e', 'create', '-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-r'
     73           | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)
     74         ]
     75         parse_options(rest, args)
     76     end
     77   end
     78 
     79   defp parse_options([], result), do: {:ok, result}
     80   defp parse_options([{:recursive, true} | t], result) do
     81     parse_options(t, result)
     82   end
     83   defp parse_options([{:recursive, false} | t], result) do
     84     parse_options(t, result -- ['-r'])
     85   end
     86   defp parse_options([{:recursive, value} | t], result) do
     87     Logger.error "unknown value `#{inspect value}` for recursive, ignore"
     88     parse_options(t, result)
     89   end
     90   defp parse_options([h | t], result) do
     91     Logger.error "unknown option `#{inspect h}`, ignore"
     92     parse_options(t, result)
     93   end
     94 
     95   def start_link(args) do
     96     GenServer.start_link(__MODULE__, args, [])
     97   end
     98 
     99   def init(args) do
    100     {worker_pid, rest} = Keyword.pop(args, :worker_pid)
    101 
    102     case parse_options(rest) do
    103       {:ok, port_args} ->
    104         bash_args = ['-c', '#{executable_path()} "$0" "$@" & PID=$!; read a; kill -KILL $PID']
    105 
    106         all_args =
    107           case :os.type() do
    108             {:unix, :freebsd} ->
    109               bash_args ++ ['--'] ++ port_args
    110 
    111             _ ->
    112               bash_args ++ port_args
    113           end
    114 
    115         port = Port.open(
    116           {:spawn_executable, '/bin/sh'},
    117           [:stream, :exit_status, {:line, 16384}, {:args, all_args}, {:cd, System.tmp_dir!()}]
    118         )
    119 
    120         Process.link(port)
    121         Process.flag(:trap_exit, true)
    122 
    123         {:ok, %{port: port, worker_pid: worker_pid}}
    124 
    125       {:error, _} ->
    126         :ignore
    127     end
    128   end
    129 
    130   def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
    131     {file_path, events} = line |> parse_line
    132     send(state.worker_pid, {:backend_file_event, self(), {file_path, events}})
    133     {:noreply, state}
    134   end
    135 
    136   def handle_info({port, {:exit_status, _}}, %{port: port}=state) do
    137     send(state.worker_pid, {:backend_file_event, self(), :stop})
    138     {:stop, :normal, state}
    139   end
    140 
    141   def handle_info({:EXIT, port, _reason}, %{port: port}=state) do
    142     send(state.worker_pid, {:backend_file_event, self(), :stop})
    143     {:stop, :normal, state}
    144   end
    145 
    146   def handle_info(_, state) do
    147     {:noreply, state}
    148   end
    149 
    150   def parse_line(line) do
    151     {path, flags} =
    152       case line |> to_string |> String.split(@sep_char, trim: true) do
    153         [dir, flags, file] -> {Path.join(dir, file), flags}
    154         [path, flags]      -> {path, flags}
    155       end
    156     {path, flags |> String.split(",") |> Enum.map(&convert_flag/1)}
    157   end
    158 
    159   defp convert_flag("CREATE"),      do: :created
    160   defp convert_flag("MOVED_TO"),    do: :moved_to
    161   defp convert_flag("DELETE"),      do: :deleted
    162   defp convert_flag("MOVED_FROM"),  do: :moved_from
    163   defp convert_flag("ISDIR"),       do: :isdir
    164   defp convert_flag("MODIFY"),      do: :modified
    165   defp convert_flag("CLOSE_WRITE"), do: :modified
    166   defp convert_flag("CLOSE"),       do: :closed
    167   defp convert_flag("ATTRIB"),      do: :attribute
    168   defp convert_flag(_),             do: :undefined
    169 end