diff --git a/README.md b/README.md index 7df884da2c99c6db09b531c8c1ea68bb3e96453f..ebd088a0e80d1e58ebdad8e16331e61523d13049 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ExFSWatch +FileSystem ========= A file change watcher wrapper based on [fs](https://github.com/synrc/fs) @@ -15,7 +15,7 @@ NOTE: On Linux you need to install inotify-tools. ## Usage -Put `exfswatch` in the `deps` and `application` part of your mix.exs +Put `file_system` in the `deps` and `application` part of your mix.exs ``` elixir defmodule Excellent.Mixfile do @@ -25,24 +25,44 @@ defmodule Excellent.Mixfile do ... end - def application do - [applications: [:exfswatch, :logger]] - end - defp deps do [ - { :exfswatch, "~> 0.4.2", only: :test }, + { :file_system, "~> 0.1.0", only: :test }, ] end ... end ``` + +### Subscription API + +You can spawn a worker and subscribe to events from it: + +```elixir +{:ok, pid} = FileSystem.Worker.start_link(dirs: ["/path/to/some/files"]) +FileSystem.Worker.subscribe(pid) +``` + +The pid you subscribed from will now receive messages like + +``` +{:file_event, worker_pid, {file_path, events}} +``` +and +``` +{:file_event, worker_pid, :stop} +``` + +### Callback API + +You can also `use FileSystem` to define a module with a callback that will be called when filesystem events occur. This requires you to specify directories to watch at compile-time. + write `lib/monitor.ex` ```elixir defmodule Monitor do - use ExFSWatch, dirs: ["/tmp/fswatch"] + use FileSystem, dirs: ["/tmp/test"] def callback(:stop) do IO.puts "STOP" @@ -67,7 +87,7 @@ For each platform, you can pass extra arguments to the underlying listener proce Here is an example to get instant notifications on file changes for Mac OS X: ```elixir -use ExFSWatch, dirs: ["/tmp/fswatch"], listener_extra_args: "--latency=0.0" +use FileSystem, dirs: ["/tmp/test"], listener_extra_args: "--latency=0.0" ``` See the [fs source](https://github.com/synrc/fs/tree/master/c_src) for more details. @@ -75,10 +95,5 @@ See the [fs source](https://github.com/synrc/fs/tree/master/c_src) for more deta ## List Events from Backend ```shell -iex > ExFSWatch.known_events +iex > FileSystem.known_events ``` - -## TODO - -- [ ] GenEvent mode -- [ ] Unit Testing diff --git a/c_src/bsd/main.c b/c_src/bsd/main.c deleted file mode 100644 index 72bcb2b4f2a94cb21532807226c97a1848edbf2b..0000000000000000000000000000000000000000 --- a/c_src/bsd/main.c +++ /dev/null @@ -1,34 +0,0 @@ -#include <sys/time.h> -#include <sys/event.h> -#include <sys/stat.h> -#include <fcntl.h> -#include <stdlib.h> -#include <unistd.h> - -int main(int argc, char *argv[]) { - struct kevent event; - struct kevent change; - int fd, kq, nev; - if ((fd = open(argv[1], O_RDONLY)) == -1) return 1; - EV_SET(&change, fd, EVFILT_VNODE , EV_ADD - | EV_ENABLE - | EV_DISABLE - | EV_CLEAR - | EV_DELETE - | EV_EOF - | EV_RECEIPT - | EV_DISPATCH - | EV_ONESHOT, - NOTE_DELETE - | NOTE_RENAME - | NOTE_EXTEND - | NOTE_ATTRIB - | NOTE_LINK - | NOTE_REVOKE - | NOTE_WRITE, 0, 0); - if ((kq = kqueue()) == -1) return 1; - nev = kevent(kq, &change, 1, &event, 1, NULL); - if (nev < 0) { return 1; } else if (nev > 0) { if (event.flags & EV_ERROR) { return 1; } } - close(kq); - return 0; -} diff --git a/lib/exfswatch.ex b/lib/exfswatch.ex deleted file mode 100644 index 1026ed2a72551395a5e764f96fc7a168b81c883e..0000000000000000000000000000000000000000 --- a/lib/exfswatch.ex +++ /dev/null @@ -1,57 +0,0 @@ -require Logger - -defmodule ExFSWatch do - defmacro __using__(options) do - extra_args = - options - |> Keyword.get(:listener_extra_args, "") - |> String.split - |> Enum.map(&to_char_list/1) - quote do - def __dirs__, do: unquote(Keyword.fetch!(options, :dirs)) - def __listener_extra_args__, do: unquote(extra_args) - def start, do: ExFSWatch.Supervisor.start_child __MODULE__ - def child_spec do - Supervisor.Spec.worker(ExFSWatch.Worker, [__MODULE__], id: __MODULE__) - end - end - end - - @backend (case :os.type() do - {:unix, :darwin} -> ExFSWatch.Backends.Fsevents - {:unix, :freebsd} -> ExFSWatch.Backends.Kqueue - {:unix, :linux} -> ExFSWatch.Backends.InotifyWait - {:win32, :nt} -> ExFSWatch.Backends.InotifyWaitWin32 - _ -> nil - end) - - def start(_, _) do - if os_supported?() and port_found?() do - ExFSWatch.Supervisor.start_link - else - Logger.error "ExFSWatch start failed" - if os_supported?() do - Logger.error "backend port not found: #{@backend}" - else - Logger.error "fs does not support the current operating system" - end - {:ok, self()} - end - end - - def os_supported? do - not is_nil @backend - end - - def port_found? do - @backend.find_executable() - end - - def known_events do - @backend.known_events() - end - - def backend do - @backend - end -end diff --git a/lib/exfswatch/backends/fsevents.ex b/lib/exfswatch/backends/fsevents.ex deleted file mode 100644 index 314870992d48b225af2038c220cc6f7d38885784..0000000000000000000000000000000000000000 --- a/lib/exfswatch/backends/fsevents.ex +++ /dev/null @@ -1,33 +0,0 @@ -alias ExFSWatch.Utils - -defmodule ExFSWatch.Backends.Fsevents do - - def find_executable do - :code.priv_dir(:exfswatch) ++ '/mac_listener' - end - - def start_port(path, listener_extra_args) do - path = path |> Utils.format_path() - args = listener_extra_args ++ ['-F' | path] - Port.open( - {:spawn_executable, find_executable()}, - [:stream, :exit_status, {:line, 16384}, {:args, args}, {:cd, System.tmp_dir!()}] - ) - end - - def known_events do - [ :mustscansubdirs, :userdropped, :kerneldropped, :eventidswrapped, :historydone, - :rootchanged, :mount, :unmount, :created, :removed, :inodemetamod, :renamed, :modified, - :finderinfomod, :changeowner, :xattrmod, :isfile, :isdir, :issymlink, :ownevent, - ] - end - - def line_to_event(line) do - [_event_id, flags, path] = :string.tokens(line, [?\t]) - [_, flags] = :string.tokens(flags, [?=]) - {:ok, t, _} = :erl_scan.string(flags ++ '.') - {:ok, flags} = :erl_parse.parse_term(t) - {path, flags} - end - -end diff --git a/lib/exfswatch/backends/inotify_wait.ex b/lib/exfswatch/backends/inotify_wait.ex deleted file mode 100644 index dba0e7f8fecb7f64bdd02f7c9c9d41b1afad957c..0000000000000000000000000000000000000000 --- a/lib/exfswatch/backends/inotify_wait.ex +++ /dev/null @@ -1,65 +0,0 @@ -alias ExFSWatch.Utils - -defmodule ExFSWatch.Backends.InotifyWait do - - def find_executable do - System.find_executable("sh") |> to_charlist - end - - def start_port(path, listener_extra_args) do - path = path |> Utils.format_path() - args = [ - '-c', 'inotifywait $0 $@ & PID=$!; read a; kill $PID', - '-m', '-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'create', - '-r'] ++ listener_extra_args ++ path - Port.open( - {:spawn_executable, find_executable()}, - [:stream, :exit_status, {:line, 16384}, {:args, args}, {:cd, System.tmp_dir!()}] - ) - end - - def known_events do - [:created, :deleted, :renamed, :closed, :modified, :isdir, :attribute, :undefined] - end - - def line_to_event(line) do - line - |> to_string - |> scan1 - |> scan2(line) - end - - def scan1(line) do - re = ~r/^(.*) ([A-Z_,]+) (.*)$/ - case Regex.scan re, line do - [] -> {:error, :unknown} - [[_, path, events, file]] -> - {Path.join(path, file), parse_events(events)} - end - - end - def scan2({:error, :unknown}, line) do - re = ~r/^(.*) ([A-Z_,]+)$/ - case Regex.scan re, line do - [] -> {:error, :unknown} - [[_, path, events]] -> - {path, parse_events(events)} - end - end - def scan2(res, _), do: res - - def parse_events(events) do - String.split(events, ",") - |> Enum.map(&(convert_flag &1)) - end - - def convert_flag("CREATE"), do: :created - def convert_flag("DELETE"), do: :deleted - def convert_flag("ISDIR"), do: :isdir - def convert_flag("MODIFY"), do: :modified - def convert_flag("CLOSE_WRITE"), do: :modified - def convert_flag("CLOSE"), do: :closed - def convert_flag("MOVED_TO"), do: :renamed - def convert_flag("ATTRIB"), do: :attribute - def convert_flag(_), do: :undefined -end diff --git a/lib/exfswatch/backends/inotify_wait_win32.ex b/lib/exfswatch/backends/inotify_wait_win32.ex deleted file mode 100644 index acfadc248651263efad7d3079eebd00ec385f381..0000000000000000000000000000000000000000 --- a/lib/exfswatch/backends/inotify_wait_win32.ex +++ /dev/null @@ -1,37 +0,0 @@ -alias ExFSWatch.Utils - -defmodule ExFSWatch.Backends.InotifyWaitWin32 do - - @re :re.compile('^(.*\\\\.*) ([A-Z_,]+) (.*)$', [:unicode]) |> elem(1) - - def find_executable do - :code.priv_dir(:exfswatch) ++ '/inotifywait.exe' - end - - def start_port(path, listener_extra_args) do - path = path |> Utils.format_path() - args = listener_extra_args ++ ['-m', '-r' | path] - Port.open( - {:spawn_executable, find_executable()}, - [:stream, :exit_status, {:line, 16384}, {:args, args}, {:cd, System.tmp_dir!()}] - ) - end - - def known_events do - [:created, :modified, :removed, :renamed, :undefined] - end - - def line_to_event(line) do - {:match, [dir, flags, dir_entry]} = :re.run(line, @re, [{:capture, :all_but_first, :list}]) - flags = for f <- :string.tokens(flags, ','), do: convert_flag(f) - path = :filename.join(dir, dir_entry) - {path, flags} - end - - defp convert_flag('CREATE'), do: :created - defp convert_flag('MODIFY'), do: :modified - defp convert_flag('DELETE'), do: :removed - defp convert_flag('MOVED_TO'), do: :renamed - defp convert_flag(_), do: :undefined - -end diff --git a/lib/exfswatch/backends/kqueue.ex b/lib/exfswatch/backends/kqueue.ex deleted file mode 100644 index 81adba2f1fe98fa3595e15237aa7ca91a69be539..0000000000000000000000000000000000000000 --- a/lib/exfswatch/backends/kqueue.ex +++ /dev/null @@ -1,26 +0,0 @@ -alias ExFSWatch.Utils - -defmodule ExFSWatch.Backends.Kqueue do - - def known_events do - [:created, :deleted, :renamed, :closed, :modified, :isdir, :undefined] - end - - def find_executable do - :code.priv_dir(:exfswatch) ++ '/kqueue' - end - - def start_port(path, listener_extra_args) do - path = path |> Utils.format_path() - args = listener_extra_args ++ [path] - Port.open( - {:spawn_executable, find_executable()}, - [:stream, :exit_status, {:line, 16384}, {:args, args}, {:cd, System.tmp_dir!()}] - ) - end - - def line_to_event(line) do - {'.', line} - end - -end diff --git a/lib/exfswatch/supervisor.ex b/lib/exfswatch/supervisor.ex deleted file mode 100644 index 566935d7f82d435bacf1732dfdb49b077a07695e..0000000000000000000000000000000000000000 --- a/lib/exfswatch/supervisor.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule ExFSWatch.Supervisor do - use Supervisor - - def start_link do - Supervisor.start_link(__MODULE__, [], name: __MODULE__) - end - - def start_child(module) do - Supervisor.start_child(__MODULE__, [module]) - end - - def init([]) do - [ worker(ExFSWatch.Worker, [], [restart: :transient]) - ] |> supervise(strategy: :simple_one_for_one) - end -end diff --git a/lib/exfswatch/utils.ex b/lib/exfswatch/utils.ex deleted file mode 100644 index 218a8db03b642636361b7a86ab89db93b071bcc5..0000000000000000000000000000000000000000 --- a/lib/exfswatch/utils.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule ExFSWatch.Utils do - - def format_path(path) when is_list(path) do - for i <- path do - i |> Path.absname |> to_char_list - end - end - - def format_path(path) do - [path] |> format_path - end - -end diff --git a/lib/exfswatch/worker.ex b/lib/exfswatch/worker.ex deleted file mode 100644 index f02fe6d4ba232ec5dd358ab9fd674744a5a2471e..0000000000000000000000000000000000000000 --- a/lib/exfswatch/worker.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule ExFSWatch.Worker do - use GenServer - - defstruct [:port, :backend, :module] - - def start_link(module) do - GenServer.start_link(__MODULE__, module, name: module) - end - - def init(module) do - backend = ExFSWatch.backend - port = backend.start_port(module.__dirs__, module.__listener_extra_args__) - {:ok, %__MODULE__{port: port, backend: backend, module: module}} - end - - def handle_info({port, {:data, {:eol, line}}}, %__MODULE__{port: port, backend: backend, module: module}=sd) do - {file_path, events} = backend.line_to_event(line) - module.callback(file_path |> to_string, events) - {:noreply, sd} - end - - def handle_info({port, {:exit_status, 0}}, %__MODULE__{port: port, module: module}) do - module.callback(:stop) - {:stop, :killed} - end - - def handle_info(_, sd) do - {:noreply, sd} - end -end diff --git a/lib/file_system.ex b/lib/file_system.ex new file mode 100644 index 0000000000000000000000000000000000000000..2d34560991933bc50de6afe35d08ce92a6cbe120 --- /dev/null +++ b/lib/file_system.ex @@ -0,0 +1,21 @@ +defmodule FileSystem do + defmacro __using__(options) do + quote do + @file_system_module_options unquote(options) + @before_compile FileSystem.ModuleApi + end + end + + def start_link(args) do + FileSystem.Worker.start_link(args) + end + + def subscribe(pid) do + GenServer.call(pid, :subscribe) + end + + @backend FileSystem.Backend.backend + def backend, do: @backend + def known_events, do: @backend.known_events() + +end diff --git a/lib/file_system/backend.ex b/lib/file_system/backend.ex new file mode 100644 index 0000000000000000000000000000000000000000..3912622f258fe284ec903c0cb377396e05ff8e3b --- /dev/null +++ b/lib/file_system/backend.ex @@ -0,0 +1,28 @@ +defmodule FileSystem.Backend do + @callback bootstrap() :: any() + @callback supported_systems() :: [{atom(), atom()}] + @callback known_events() :: [atom()] + @callback find_executable() :: Sting.t + + def backend do + os_type = :os.type() + backend = + Application.get_env(:file_system, :backend, + case os_type do + {:unix, :darwin} -> :fs_mac + {:unix, :linux} -> :fs_inotify + {:unix, :freebsd} -> :fs_inotify + {:win32, :nt} -> :fs_windows + _ -> nil + end + ) |> case do + nil -> raise "undefined backend" + :fs_mac -> FileSystem.Backends.FSMac + :fs_inotify -> FileSystem.Backends.FSInotify + :fs_windows -> FileSystem.Backends.FSWindows + any -> any + end + os_type in backend.supported_systems || raise "unsupported system for current backend" + backend + end +end diff --git a/lib/file_system/backends/fs_inotify.ex b/lib/file_system/backends/fs_inotify.ex new file mode 100644 index 0000000000000000000000000000000000000000..141682fbed2828e3e0aa90f9711c9dce339fc0ae --- /dev/null +++ b/lib/file_system/backends/fs_inotify.ex @@ -0,0 +1,80 @@ +require Logger +alias FileSystem.Utils + +defmodule FileSystem.Backends.FSInotify do + use GenServer + @behaviour FileSystem.Backend + @sep_char <<1>> + + def bootstrap do + exec_file = find_executable() + unless File.exists?(exec_file) do + 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." + raise CompileError + end + end + + def supported_systems do + [{:unix, :linux}, {:unix, :freebsd}] + end + + def known_events do + [:created, :deleted, :renamed, :closed, :modified, :isdir, :attribute, :undefined] + end + + def find_executable do + System.find_executable("inotifywait") + end + + def start_link(args) do + GenServer.start_link(__MODULE__, args, []) + end + + def init(args) do + port_path = Utils.format_path(args[:dirs]) + format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist + port_args = [ + '-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'create', + '-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-r' + ] ++ Utils.format_args(args[:listener_extra_args]) ++ port_path + port = Port.open( + {:spawn_executable, to_charlist(find_executable())}, + [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] + ) + {:ok, %{port: port, worker_pid: args[:worker_pid]}} + end + + def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do + {file_path, events} = line |> parse_line + send(state.worker_pid, {:backend_file_event, self(), {file_path, events}}) + {:noreply, state} + end + + def handle_info({port, {:exit_status, _}}, %{port: port}=state) do + send(state.worker_pid, {:backend_file_event, self(), :stop}) + {:stop, :normal, state} + end + + def handle_info(_, state) do + {:noreply, state} + end + + def parse_line(line) do + {path, flags} = + case line |> to_string |> String.split(@sep_char, trim: true) do + [dir, flags, file] -> {Path.join(dir, file), flags} + [path, flags] -> {path, flags} + end + {path, flags |> String.split(",") |> Enum.map(&convert_flag/1)} + end + + defp convert_flag("CREATE"), do: :created + defp convert_flag("DELETE"), do: :deleted + defp convert_flag("ISDIR"), do: :isdir + defp convert_flag("MODIFY"), do: :modified + defp convert_flag("CLOSE_WRITE"), do: :modified + defp convert_flag("CLOSE"), do: :closed + defp convert_flag("MOVED_TO"), do: :renamed + defp convert_flag("ATTRIB"), do: :attribute + defp convert_flag(_), do: :undefined +end diff --git a/lib/file_system/backends/fs_mac.ex b/lib/file_system/backends/fs_mac.ex new file mode 100644 index 0000000000000000000000000000000000000000..f816f05247e7e9a11e14c85e71a2bfcf9caa6a09 --- /dev/null +++ b/lib/file_system/backends/fs_mac.ex @@ -0,0 +1,71 @@ +require Logger +alias FileSystem.Utils + +defmodule FileSystem.Backends.FSMac do + use GenServer + @behaviour FileSystem.Backend + + def bootstrap do + exec_file = find_executable() + unless File.exists?(exec_file) do + Logger.info "Compiling executable file..." + cmd = "clang -framework CoreFoundation -framework CoreServices -Wno-deprecated-declarations c_src/mac/*.c -o #{exec_file}" + if Mix.shell.cmd(cmd) > 0 do + Logger.error "Compile executable file error, try to run `#{cmd}` manually." + raise CompileError + else + Logger.info "Compile executable file, Done." + end + end + end + + def supported_systems do + [{:unix, :darwin}] + end + + def known_events do + [ :mustscansubdirs, :userdropped, :kerneldropped, :eventidswrapped, :historydone, + :rootchanged, :mount, :unmount, :created, :removed, :inodemetamod, :renamed, :modified, + :finderinfomod, :changeowner, :xattrmod, :isfile, :isdir, :issymlink, :ownevent, + ] + end + + def find_executable do + (:code.priv_dir(:file_system) ++ '/mac_listener') |> to_string + end + + def start_link(args) do + GenServer.start_link(__MODULE__, args, []) + end + + def init(args) do + port_path = Utils.format_path(args[:dirs]) + port_args = Utils.format_args(args[:listener_extra_args]) ++ ['-F' | port_path] + port = Port.open( + {:spawn_executable, to_charlist(find_executable())}, + [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] + ) + {:ok, %{port: port, worker_pid: args[:worker_pid]}} + end + + def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do + {file_path, events} = line |> parse_line + send(state.worker_pid, {:backend_file_event, self(), {file_path, events}}) + {:noreply, state} + end + + def handle_info({port, {:exit_status, _}}, %{port: port}=state) do + send(state.worker_pid, {:backend_file_event, self(), :stop}) + {:stop, :normal, state} + end + + def handle_info(_, state) do + {:noreply, state} + end + + def parse_line(line) do + [_, _, events, path] = line |> to_string |> String.split(["\t", "="]) + {path, events |> String.split(~w|[ , ]|, trim: true) |> Enum.map(&String.to_existing_atom/1)} + end + +end diff --git a/lib/file_system/backends/fs_windows.ex b/lib/file_system/backends/fs_windows.ex new file mode 100644 index 0000000000000000000000000000000000000000..1242ebd66b3057ad4a4578c8d32c239e242a9013 --- /dev/null +++ b/lib/file_system/backends/fs_windows.ex @@ -0,0 +1,75 @@ +require Logger +alias FileSystem.Utils + +defmodule FileSystem.Backends.FSWindows do + use GenServer + @behaviour FileSystem.Backend + @sep_char <<1>> + + def bootstrap do + exec_file = find_executable() + unless File.exists?(exec_file) do + Logger.error "Can't find executable `inotifywait.exe`, make sure the file is in your priv dir." + raise CompileError + end + end + + def supported_systems do + [{:win32, :nt}] + end + + def known_events do + [:created, :modified, :removed, :renamed, :undefined] + end + + def find_executable do + (:code.priv_dir(:file_system) ++ '/inotifywait.exe') |> to_string + end + + def start_link(args) do + GenServer.start_link(__MODULE__, args, []) + end + + def init(args) do + port_path = Utils.format_path(args[:dirs]) + format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist + port_args = Utils.format_args(args[:listener_extra_args]) ++ [ + '--format', format, '--quiet', '-m', '-r' | port_path + ] + port = Port.open( + {:spawn_executable, to_charlist(find_executable())}, + [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] + ) + {:ok, %{port: port, worker_pid: args[:worker_pid]}} + end + + def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do + {file_path, events} = line |> parse_line + send(state.worker_pid, {:backend_file_event, self(), {file_path, events}}) + {:noreply, state} + end + + def handle_info({port, {:exit_status, _}}, %{port: port}=state) do + send(state.worker_pid, {:backend_file_event, self(), :stop}) + {:stop, :normal, state} + end + + def handle_info(_, state) do + {:noreply, state} + end + + def parse_line(line) do + {path, flags} = + case line |> to_string |> String.split(@sep_char, trim: true) do + [dir, flags, file] -> {Enum.join([dir, file], "\\"), flags} + [path, flags] -> {path, flags} + end + {path, flags |> String.split(",") |> Enum.map(&convert_flag/1)} + end + + defp convert_flag("CREATE"), do: :created + defp convert_flag("MODIFY"), do: :modified + defp convert_flag("DELETE"), do: :removed + defp convert_flag("MOVED_TO"), do: :renamed + defp convert_flag(_), do: :undefined +end diff --git a/lib/file_system/module_api.ex b/lib/file_system/module_api.ex new file mode 100644 index 0000000000000000000000000000000000000000..3139ba66530942a2c498781eac7c512584d67de3 --- /dev/null +++ b/lib/file_system/module_api.ex @@ -0,0 +1,25 @@ +defmodule FileSystem.ModuleApi do + defmacro __before_compile__(%Macro.Env{module: module}) do + options = Module.get_attribute(module, :file_system_module_options) + quote do + def start do + {:ok, worker_pid} = FileSystem.start_link(unquote(options)) + pid = spawn_link(fn -> + FileSystem.subscribe(worker_pid) + await_events(worker_pid) + end) + {:ok, pid} + end + + defp await_events(pid) do + receive do + {:file_event, ^pid, :stop} -> + callback(:stop) + {:file_event, ^pid, {file_path, events}} -> + callback(file_path, events) + await_events(pid) + end + end + end + end +end diff --git a/lib/file_system/utils.ex b/lib/file_system/utils.ex new file mode 100644 index 0000000000000000000000000000000000000000..b24eeb9a337e1d8374c42e32f83d5d58bf96306b --- /dev/null +++ b/lib/file_system/utils.ex @@ -0,0 +1,20 @@ +defmodule FileSystem.Utils do + def format_path(path) when is_list(path) do + for i <- path do + i |> Path.absname |> to_charlist + end + end + + def format_path(path) do + [path] |> format_path + end + + def format_args(nil), do: [] + def format_args(str) when is_binary(str) do + str |> String.split |> format_args + end + def format_args(list) when is_list(list) do + list |> Enum.map(&to_charlist/1) + end + +end diff --git a/lib/file_system/worker.ex b/lib/file_system/worker.ex new file mode 100644 index 0000000000000000000000000000000000000000..f110770bc27a2bdbd1b72bd5e4509a04ec4eb52f --- /dev/null +++ b/lib/file_system/worker.ex @@ -0,0 +1,34 @@ +defmodule FileSystem.Worker do + use GenServer + + def start_link(args) do + {args, opts} = Keyword.split(args, [:dirs, :listener_extra_args]) + GenServer.start_link(__MODULE__, args, opts) + end + + def init(args) do + {:ok, backend_pid} = FileSystem.backend.start_link([{:worker_pid, self()} | args]) + {:ok, %{backend_pid: backend_pid, subscribers: %{}}} + end + + def handle_call(:subscribe, {pid, _}, state) do + ref = Process.monitor(pid) + state = put_in(state, [:subscribers, ref], pid) + {:reply, :ok, state} + end + + def handle_info({:backend_file_event, backend_pid, file_event}, %{backend_pid: backend_pid}=state) do + state.subscribers |> Enum.each(fn {_ref, subscriber_pid} -> + send(subscriber_pid, {:file_event, self(), file_event}) + end) + {:noreply, state} + end + + def handle_info({:DOWN, _pid, _, ref, _reason}, state) do + {:noreply, pop_in(state.subscribers[ref])} + end + + def handle_info(_, state) do + {:noreply, state} + end +end diff --git a/mix.exs b/mix.exs index 3b7be3ec32a342d433247236c86caaef787d7fdd..00767448646fa68603cf46f4cec57f56ef51a2bd 100644 --- a/mix.exs +++ b/mix.exs @@ -1,27 +1,21 @@ -defmodule Mix.Tasks.Compile.Exfswatch do +defmodule Mix.Tasks.Compile.FileSystem do def run(_) do - case :os.type() do - {:unix, :darwin} -> - Mix.shell.cmd("clang -framework CoreFoundation -framework CoreServices -Wno-deprecated-declarations c_src/mac/*.c -o priv/mac_listener") - {:unix, :freebsd} -> - Mix.shell.cmd("cc c_src/bsd/*.c -o priv/kqueue") - _ -> - :ok - end + FileSystem.backend.bootstrap end end -defmodule ExFSWatch.Mixfile do + +defmodule FileSystem.Mixfile do use Mix.Project def project do - [ app: :exfswatch, - version: "0.4.2", - elixir: "~> 1.0", - compilers: [ :exfswatch, :elixir, :app ], + [ app: :file_system, + version: "0.1.0", + elixir: "~> 1.5-rc", + compilers: [:elixir, :app, :file_system], deps: deps(), - description: "A file change watcher wrapper based on [fs](https://github.com/synrc/fs)", - source_url: "https://github.com/falood/exfswatch", + description: "A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)", + source_url: "https://github.com/falood/file_system", package: package(), docs: [ extras: ["README.md"], @@ -31,18 +25,19 @@ defmodule ExFSWatch.Mixfile do end def application do - [ mod: { ExFSWatch, [] }, - applications: [:logger], + [ + extra_applications: [:logger], ] end defp deps do - [ { :ex_doc, "~> 0.14", only: :docs }, + [ + { :ex_doc, "~> 0.14", only: :docs }, ] end defp package do - %{ maintainers: ["Xiangrong Hao"], + %{ maintainers: ["Xiangrong Hao", "Max Veytsman"], files: [ "lib", "README.md", "mix.exs", "c_src/bsd/main.c", @@ -55,7 +50,7 @@ defmodule ExFSWatch.Mixfile do "priv/inotifywait.exe", ], licenses: ["WTFPL"], - links: %{"Github" => "https://github.com/falood/exfswatch"} + links: %{"Github" => "https://github.com/falood/file_system"} } end end diff --git a/test/backends/fs_inotify_test.exs b/test/backends/fs_inotify_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..74ceb13a8e53ad518b05a943a3eff7dd9aa282e9 --- /dev/null +++ b/test/backends/fs_inotify_test.exs @@ -0,0 +1,48 @@ +defmodule FileSystem.Backends.FSInotifyTest do + use ExUnit.Case + import FileSystem.Backends.FSInotify + + test "dir write close" do + assert {"/one/two/file", [:modified, :closed]} == + ~w|/one/two/ CLOSE_WRITE,CLOSE file| |> to_port_line |> parse_line + end + + test "dir create" do + assert {"/one/two/file", [:created]} == + ~w|/one/two/ CREATE file| |> to_port_line |> parse_line + end + + test "dir moved to" do + assert {"/one/two/file", [:renamed]} == + ~w|/one/two/ MOVED_TO file| |> to_port_line |> parse_line + end + + test "dir is_dir create" do + assert {"/one/two/dir", [:created, :isdir]} == + ~w|/one/two/ CREATE,ISDIR dir| |> to_port_line |> parse_line + end + + test "file write close" do + assert {"/one/two/file", [:modified, :closed]} == + ~w|/one/two/file CLOSE_WRITE,CLOSE| |> to_port_line |> parse_line + end + + test "file delete_self" do + assert {"/one/two/file", [:undefined]} == + ~w|/one/two/file DELETE_SELF| |> to_port_line |> parse_line + end + + test "whitespace in path" do + assert {"/one two/file", [:modified, :closed]} == + ["/one two", "CLOSE_WRITE,CLOSE", "file"] |> to_port_line |> parse_line + + assert {"/one/two/file 1", [:modified, :closed]} == + ["/one/two", "CLOSE_WRITE,CLOSE", "file 1"] |> to_port_line |> parse_line + + assert {"/one two/file 1", [:modified, :closed]} == + ["/one two", "CLOSE_WRITE,CLOSE", "file 1"] |> to_port_line |> parse_line + end + + defp to_port_line(list), do: list |> Enum.join(<<1>>) |> to_charlist + +end diff --git a/test/backends/fs_mac_test.exs b/test/backends/fs_mac_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..dd6ee658adf67008a9714c0579aee2021b1b61e8 --- /dev/null +++ b/test/backends/fs_mac_test.exs @@ -0,0 +1,14 @@ +defmodule FileSystem.Backends.FSMacTest do + use ExUnit.Case + import FileSystem.Backends.FSMac + + test "file modified" do + assert {"/one/two/file", [:inodemetamod, :modified]} == + parse_line('37425557\t0x00011400=[inodemetamod,modified]\t/one/two/file') + end + + test "whitespace in path" do + assert {"/one two/file", [:inodemetamod, :modified]} == + parse_line('37425557\t0x00011400=[inodemetamod,modified]\t/one two/file') + end +end diff --git a/test/backends/fsevents.exs b/test/backends/fsevents.exs deleted file mode 100644 index d50a0c95b5f9fff9776a3eb46b8298e19bea9a2e..0000000000000000000000000000000000000000 --- a/test/backends/fsevents.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule ExFSWatch.Backends.FseventsTest do - use ExUnit.Case - import ExFSWatch.Backends.Fsevents - - test "file modified" do - assert line_to_event('37425557\t0x00011400=[inodemetamod,modified]\t/one/two/file') == - {'/one/two/file', [:inodemetamod, :modified]} - end -end diff --git a/test/backends/inotify_wait_test.exs b/test/backends/inotify_wait_test.exs deleted file mode 100644 index 64b383c12ca0bd2dd4827cfae73c12eb25d57eff..0000000000000000000000000000000000000000 --- a/test/backends/inotify_wait_test.exs +++ /dev/null @@ -1,34 +0,0 @@ -defmodule ExFSWatch.Backends.InotifyWaitTest do - use ExUnit.Case - import ExFSWatch.Backends.InotifyWait - - test "dir write close" do - assert line_to_event("/one/two/ CLOSE_WRITE,CLOSE file") == - {"/one/two/file", [:modified, :closed]} - end - - test "dir create" do - assert line_to_event("/one/two/ CREATE file") == - {"/one/two/file", [:created]} - end - - test "dir moved to" do - assert line_to_event("/one/two/ MOVED_TO file") == - {"/one/two/file", [:renamed]} - end - - test "dir is_dir create" do - assert line_to_event("/one/two/ CREATE,ISDIR dir") == - {"/one/two/dir", [:created, :isdir]} - end - - test "file write close" do - assert line_to_event("/one/two/file CLOSE_WRITE,CLOSE") == - {"/one/two/file", [:modified, :closed]} - end - - test "file delete_self" do - assert line_to_event("/one/two/file DELETE_SELF") == - {"/one/two/file", [:undefined]} - end -end diff --git a/test/file_system_test.exs b/test/file_system_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..9edd86bba7addd3307c4dacfd7ad95dcc41235a3 --- /dev/null +++ b/test/file_system_test.exs @@ -0,0 +1,12 @@ +defmodule FileSystemTest do + use ExUnit.Case + + test "subscribe api" do + tmp_dir = System.cmd("mktemp", ["-d"]) |> elem(0) |> String.trim + {:ok, pid} = FileSystem.start_link(dirs: [tmp_dir]) + FileSystem.subscribe(pid) + File.touch("#{tmp_dir}/a") + assert_receive {:file_event, ^pid, {_path, _events}}, 5000 + File.rm_rf!(tmp_dir) + end +end diff --git a/test/module_api_test.exs b/test/module_api_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..67ab92137e28b771f50b4008cc2e41dde80be253 --- /dev/null +++ b/test/module_api_test.exs @@ -0,0 +1,22 @@ +defmodule FileSystem.ModuleApiTest do + use ExUnit.Case + + test "module api" do + tmp_dir = System.cmd("mktemp", ["-d"]) |> elem(0) |> String.trim + Process.register(self(), :module_api_test) + ref = System.unique_integer + + defmodule MyMonitor do + use FileSystem, dirs: [tmp_dir] + @ref ref + + def callback(:stop), do: :stop + def callback(path, events), do: send(:module_api_test, {@ref, path, events}) + end + + MyMonitor.start + File.touch("#{tmp_dir}/a") + assert_receive {^ref, _path, _events}, 5000 + File.rm_rf!(tmp_dir) + end +end