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