From eab44e71f4bae5656fae538cd0a85aa441fbe30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 9 Nov 2023 08:27:23 +0100 Subject: [PATCH] Add CI, remove warnings on Elixir v1.17-dev (#89) --- .formatter.exs | 3 + .github/workflows/ci.yml | 55 ++++++++++ .travis.yml | 40 -------- config/config.exs | 5 - lib/file_system.ex | 4 +- lib/file_system/backend.ex | 48 ++++++--- lib/file_system/backends/fs_inotify.ex | 92 ++++++++++++----- lib/file_system/backends/fs_mac.ex | 87 +++++++++++----- lib/file_system/backends/fs_poll.ex | 19 ++-- lib/file_system/backends/fs_windows.ex | 62 +++++++---- lib/file_system/worker.ex | 13 ++- mix.exs | 4 +- test/backends/fs_inotify_test.exs | 137 +++++++++++++++++++------ test/backends/fs_mac_test.exs | 20 ++-- test/backends/fs_poll_test.exs | 2 +- test/file_system_test.exs | 15 +-- 16 files changed, 411 insertions(+), 195 deletions(-) create mode 100644 .formatter.exs create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 config/config.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..2bed17c --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..943b0e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-20.04 + env: + MIX_ENV: test + strategy: + fail-fast: false + matrix: + include: + - pair: + elixir: 1.12.4 + otp: 23.3 + - pair: + elixir: 1.15.7 + otp: 26.1 + lint: lint + steps: + - uses: actions/checkout@v3 + + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.pair.otp}} + elixir-version: ${{matrix.pair.elixir}} + + - uses: actions/cache@v3 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix- + + - run: mix deps.get + + - run: mix format --check-formatted + if: ${{ matrix.lint }} + + - run: mix deps.unlock --check-unused + if: ${{ matrix.lint }} + + - run: mix deps.compile + + - run: mix compile --warnings-as-errors + if: ${{ matrix.lint }} + + - run: mix test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa28731..0000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: elixir -matrix: - include: - - os: linux - sudo: required - otp_release: 19.3 - elixir: 1.3.4 - - - os: linux - sudo: required - otp_release: 19.3 - elixir: 1.4.5 - - - os: linux - sudo: required - otp_release: 20.0 - elixir: 1.4.5 - - - os: linux - sudo: required - otp_release: 19.3 - elixir: 1.5.0 - - - os: linux - sudo: required - otp_release: 20.0 - elixir: 1.5.0 - - - os: linux - sudo: required - otp_release: 23.0 - elixir: 1.10.4 - -notifications: - recipients: - - self@falood.me -before_install: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install inotify-tools; fi -before_script: - - mix deps.get --only test diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index 4a99dfd..0000000 --- a/config/config.exs +++ /dev/null @@ -1,5 +0,0 @@ -use Mix.Config - -if :test == Mix.env() do - config :logger, backends: [] -end diff --git a/lib/file_system.ex b/lib/file_system.ex index 31e22d3..a02a8fb 100644 --- a/lib/file_system.ex +++ b/lib/file_system.ex @@ -41,7 +41,7 @@ defmodule FileSystem do iex> FileSystem.subscribe(:worker) """ - @spec start_link(Keyword.t) :: GenServer.on_start() + @spec start_link(Keyword.t()) :: GenServer.on_start() def start_link(options) do FileSystem.Worker.start_link(options) end @@ -55,7 +55,7 @@ defmodule FileSystem do {:file_event, worker_pid, :stop} """ - @spec subscribe(GenServer.server) :: :ok + @spec subscribe(GenServer.server()) :: :ok def subscribe(pid) do GenServer.call(pid, :subscribe) end diff --git a/lib/file_system/backend.ex b/lib/file_system/backend.ex index 8b23760..c390a14 100644 --- a/lib/file_system/backend.ex +++ b/lib/file_system/backend.ex @@ -24,8 +24,7 @@ defmodule FileSystem.Backend do def backend(backend) do with {:ok, module} <- backend_module(backend), :ok <- validate_os(backend, module), - :ok <- module.bootstrap - do + :ok <- module.bootstrap() do {:ok, module} else {:error, reason} -> {:error, reason} @@ -34,40 +33,55 @@ defmodule FileSystem.Backend do defp backend_module(nil) do case :os.type() do - {:unix, :darwin} -> :fs_mac - {:unix, :linux} -> :fs_inotify - {:unix, :freebsd} -> :fs_inotify - {:unix, :openbsd} -> :fs_inotify - {:win32, :nt} -> :fs_windows - system -> {:unsupported_system, system} - end |> backend_module + {:unix, :darwin} -> :fs_mac + {:unix, :linux} -> :fs_inotify + {:unix, :freebsd} -> :fs_inotify + {:unix, :openbsd} -> :fs_inotify + {:win32, :nt} -> :fs_windows + system -> {:unsupported_system, system} + end + |> backend_module end - defp backend_module(:fs_mac), do: {:ok, FileSystem.Backends.FSMac} + + defp backend_module(:fs_mac), do: {:ok, FileSystem.Backends.FSMac} defp backend_module(:fs_inotify), do: {:ok, FileSystem.Backends.FSInotify} defp backend_module(:fs_windows), do: {:ok, FileSystem.Backends.FSWindows} - defp backend_module(:fs_poll), do: {:ok, FileSystem.Backends.FSPoll} + defp backend_module(:fs_poll), do: {:ok, FileSystem.Backends.FSPoll} + defp backend_module({:unsupported_system, system}) do - Logger.error "I'm so sorry but `file_system` does NOT support your current system #{inspect system} for now." + Logger.error( + "I'm so sorry but `file_system` does NOT support your current system #{inspect(system)} for now." + ) + {:error, :unsupported_system} end + defp backend_module(module) do functions = module.__info__(:functions) - {:start_link, 1} in functions && - {:bootstrap, 0} in functions && - {:supported_systems, 0} in functions || + + ({:start_link, 1} in functions && + {:bootstrap, 0} in functions && + {:supported_systems, 0} in functions) || raise "illegal backend" rescue _ -> - Logger.error "You are using custom backend `#{inspect module}`, make sure it's a legal file_system backend module." + Logger.error( + "You are using custom backend `#{inspect(module)}`, make sure it's a legal file_system backend module." + ) + {:error, :illegal_backend} end defp validate_os(backend, module) do os_type = :os.type() + if os_type in module.supported_systems() do :ok else - Logger.error "The backend `#{backend}` you are using does NOT support your current system #{inspect os_type}." + Logger.error( + "The backend `#{backend}` you are using does NOT support your current system #{inspect(os_type)}." + ) + {:error, :unsupported_system} end end diff --git a/lib/file_system/backends/fs_inotify.ex b/lib/file_system/backends/fs_inotify.ex index 876c356..7f29495 100644 --- a/lib/file_system/backends/fs_inotify.ex +++ b/lib/file_system/backends/fs_inotify.ex @@ -39,8 +39,12 @@ defmodule FileSystem.Backends.FSInotify do def bootstrap do exec_file = executable_path() + if is_nil(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. If it's already installed but not be found, appoint executable file with `config.exs` or `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` env." + 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." + ) + {:error, :fs_inotify_bootstrap_error} else :ok @@ -74,32 +78,56 @@ defmodule FileSystem.Backends.FSInotify do def parse_options(options) do case Keyword.pop(options, :dirs) do {nil, _} -> - Logger.error "required argument `dirs` is missing" + Logger.error("required argument `dirs` is missing") {:error, :missing_dirs_argument} + {dirs, rest} -> format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist + args = [ - '-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from', - '-e', 'create', '-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-r' + ~c"-e", + ~c"modify", + ~c"-e", + ~c"close_write", + ~c"-e", + ~c"moved_to", + ~c"-e", + ~c"moved_from", + ~c"-e", + ~c"create", + ~c"-e", + ~c"delete", + ~c"-e", + ~c"attrib", + ~c"--format", + format, + ~c"--quiet", + ~c"-m", + ~c"-r" | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1) ] + parse_options(rest, args) end end defp parse_options([], result), do: {:ok, result} + defp parse_options([{:recursive, true} | t], result) do parse_options(t, result) end + defp parse_options([{:recursive, false} | t], result) do - parse_options(t, result -- ['-r']) + parse_options(t, result -- [~c"-r"]) end + defp parse_options([{:recursive, value} | t], result) do - Logger.error "unknown value `#{inspect value}` for recursive, ignore" + Logger.error("unknown value `#{inspect(value)}` for recursive, ignore") parse_options(t, result) end + defp parse_options([h | t], result) do - Logger.error "unknown option `#{inspect h}`, ignore" + Logger.error("unknown option `#{inspect(h)}`, ignore") parse_options(t, result) end @@ -112,21 +140,32 @@ defmodule FileSystem.Backends.FSInotify do case parse_options(rest) do {:ok, port_args} -> - bash_args = ['-c', '#{executable_path()} "$0" "$@" & PID=$!; read a; kill -KILL $PID'] + bash_args = [ + ~c"-c", + ~c"#{executable_path()} \"$0\" \"$@\" & PID=$!; read a; kill -KILL $PID" + ] all_args = case :os.type() do {:unix, :freebsd} -> - bash_args ++ ['--'] ++ port_args + bash_args ++ [~c"--"] ++ port_args _ -> bash_args ++ port_args end - port = Port.open( - {:spawn_executable, '/bin/sh'}, - [:binary, :stream, :exit_status, {:line, 16384}, {:args, all_args}, {:cd, System.tmp_dir!()}] - ) + port = + Port.open( + {:spawn_executable, ~c"/bin/sh"}, + [ + :binary, + :stream, + :exit_status, + {:line, 16384}, + {:args, all_args}, + {:cd, System.tmp_dir!()} + ] + ) Process.link(port) Process.flag(:trap_exit, true) @@ -138,18 +177,18 @@ defmodule FileSystem.Backends.FSInotify do end end - def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do + 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 + 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({:EXIT, port, _reason}, %{port: port}=state) do + def handle_info({:EXIT, port, _reason}, %{port: port} = state) do send(state.worker_pid, {:backend_file_event, self(), :stop}) {:stop, :normal, state} end @@ -162,19 +201,20 @@ defmodule FileSystem.Backends.FSInotify do {path, flags} = case String.split(line, @sep_char, trim: true) do [dir, flags, file] -> {Path.join(dir, file), flags} - [path, flags] -> {path, 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("MOVED_TO"), do: :moved_to - defp convert_flag("DELETE"), do: :deleted - defp convert_flag("MOVED_FROM"), do: :moved_from - defp convert_flag("ISDIR"), do: :isdir - defp convert_flag("MODIFY"), do: :modified + defp convert_flag("CREATE"), do: :created + defp convert_flag("MOVED_TO"), do: :moved_to + defp convert_flag("DELETE"), do: :deleted + defp convert_flag("MOVED_FROM"), do: :moved_from + 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("ATTRIB"), do: :attribute - defp convert_flag(_), do: :undefined + defp convert_flag("CLOSE"), do: :closed + 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 index f142fbd..1a0259b 100644 --- a/lib/file_system/backends/fs_mac.ex +++ b/lib/file_system/backends/fs_mac.ex @@ -57,10 +57,11 @@ defmodule FileSystem.Backends.FSMac do def bootstrap do exec_file = executable_path() + if not is_nil(exec_file) and File.exists?(exec_file) do :ok else - Logger.error "Can't find executable `mac_listener`" + Logger.error("Can't find executable `mac_listener`") {:error, :fs_mac_bootstrap_error} end end @@ -70,14 +71,33 @@ defmodule FileSystem.Backends.FSMac do 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, + [ + :mustscansubdirs, + :userdropped, + :kerneldropped, + :eventidswrapped, + :historydone, + :rootchanged, + :mount, + :unmount, + :created, + :removed, + :inodemetamod, + :renamed, + :modified, + :finderinfomod, + :changeowner, + :xattrmod, + :isfile, + :isdir, + :issymlink, + :ownevent ] end defp executable_path do - executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv) + executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || + executable_path(:priv) end defp executable_path(:config) do @@ -95,8 +115,12 @@ defmodule FileSystem.Backends.FSMac do defp executable_path(:priv) do case :code.priv_dir(:file_system) do {:error, _} -> - Logger.error "`priv` dir for `:file_system` application is not available in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSMAC_EXECUTABLE_FILE` env." + Logger.error( + "`priv` dir for `:file_system` application is not available in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSMAC_EXECUTABLE_FILE` env." + ) + nil + dir when is_list(dir) -> Path.join(dir, @default_exec_file) end @@ -105,47 +129,57 @@ defmodule FileSystem.Backends.FSMac do def parse_options(options) do case Keyword.pop(options, :dirs) do {nil, _} -> - Logger.error "required argument `dirs` is missing" + Logger.error("required argument `dirs` is missing") {:error, :missing_dirs_argument} + {dirs, rest} -> - args = ['-F' | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)] + args = [~c"-F" | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)] parse_options(rest, args) end end defp parse_options([], result), do: {:ok, result} + defp parse_options([{:latency, latency} | t], result) do result = if is_float(latency) or is_integer(latency) do - ['--latency=#{latency / 1}' | result] + [~c"--latency=#{latency / 1}" | result] else - Logger.error "latency should be integer or float, got `#{inspect latency}, ignore" + Logger.error("latency should be integer or float, got `#{inspect(latency)}, ignore") result end + parse_options(t, result) end + defp parse_options([{:no_defer, true} | t], result) do - parse_options(t, ['--no-defer' | result]) + parse_options(t, [~c"--no-defer" | result]) end + defp parse_options([{:no_defer, false} | t], result) do parse_options(t, result) end + defp parse_options([{:no_defer, value} | t], result) do - Logger.error "unknown value `#{inspect value}` for no_defer, ignore" + Logger.error("unknown value `#{inspect(value)}` for no_defer, ignore") parse_options(t, result) end + defp parse_options([{:with_root, true} | t], result) do - parse_options(t, ['--with-root' | result]) + parse_options(t, [~c"--with-root" | result]) end + defp parse_options([{:with_root, false} | t], result) do parse_options(t, result) end + defp parse_options([{:with_root, value} | t], result) do - Logger.error "unknown value `#{inspect value}` for with_root, ignore" + Logger.error("unknown value `#{inspect(value)}` for with_root, ignore") parse_options(t, result) end + defp parse_options([h | t], result) do - Logger.error "unknown option `#{inspect h}`, ignore" + Logger.error("unknown option `#{inspect(h)}`, ignore") parse_options(t, result) end @@ -155,32 +189,36 @@ defmodule FileSystem.Backends.FSMac do def init(args) do {worker_pid, rest} = Keyword.pop(args, :worker_pid) + case parse_options(rest) do {:ok, port_args} -> - port = Port.open( - {:spawn_executable, to_charlist(executable_path())}, - [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] - ) + port = + Port.open( + {:spawn_executable, to_charlist(executable_path())}, + [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] + ) + Process.link(port) Process.flag(:trap_exit, true) {:ok, %{port: port, worker_pid: worker_pid}} + {:error, _} -> :ignore end end - def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do + 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 + 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({:EXIT, port, _reason}, %{port: port}=state) do + def handle_info({:EXIT, port, _reason}, %{port: port} = state) do send(state.worker_pid, {:backend_file_event, self(), :stop}) {:stop, :normal, state} end @@ -191,7 +229,8 @@ defmodule FileSystem.Backends.FSMac do def parse_line(line) do [_, _, events, path] = line |> to_string |> String.split(["\t", "="], parts: 4) - {path, events |> String.split(["[", ",", "]"], trim: true) |> Enum.map(&String.to_existing_atom/1)} - end + {path, + events |> String.split(["[", ",", "]"], trim: true) |> Enum.map(&String.to_existing_atom/1)} + end end diff --git a/lib/file_system/backends/fs_poll.ex b/lib/file_system/backends/fs_poll.ex index fcacb3d..3d2ac27 100644 --- a/lib/file_system/backends/fs_poll.ex +++ b/lib/file_system/backends/fs_poll.ex @@ -53,7 +53,7 @@ defmodule FileSystem.Backends.FSPoll do fresh_mtimes = files_mtimes(dirs) diff(stale_mtimes, fresh_mtimes) - |> Tuple.to_list + |> Tuple.to_list() |> Enum.zip([:created, :deleted, :modified]) |> Enum.each(&report_change(&1, worker_pid)) @@ -70,11 +70,13 @@ defmodule FileSystem.Backends.FSPoll do case File.stat!(dir) do %{type: :regular, mtime: mtime} -> Map.put(map, dir, mtime) + %{type: :directory} -> dir |> Path.join("*") - |> Path.wildcard + |> Path.wildcard() |> files_mtimes(map) + %{type: _other} -> map end @@ -83,16 +85,19 @@ defmodule FileSystem.Backends.FSPoll do @doc false def diff(stale_mtimes, fresh_mtimes) do - fresh_file_paths = fresh_mtimes |> Map.keys |> MapSet.new - stale_file_paths = stale_mtimes |> Map.keys |> MapSet.new + fresh_file_paths = fresh_mtimes |> Map.keys() |> MapSet.new() + stale_file_paths = stale_mtimes |> Map.keys() |> MapSet.new() created_file_paths = - MapSet.difference(fresh_file_paths, stale_file_paths) |> MapSet.to_list + MapSet.difference(fresh_file_paths, stale_file_paths) |> MapSet.to_list() + deleted_file_paths = - MapSet.difference(stale_file_paths, fresh_file_paths) |> MapSet.to_list + MapSet.difference(stale_file_paths, fresh_file_paths) |> MapSet.to_list() + modified_file_paths = for file_path <- MapSet.intersection(stale_file_paths, fresh_file_paths), - stale_mtimes[file_path] != fresh_mtimes[file_path], do: file_path + stale_mtimes[file_path] != fresh_mtimes[file_path], + do: file_path {created_file_paths, deleted_file_paths, modified_file_paths} end diff --git a/lib/file_system/backends/fs_windows.ex b/lib/file_system/backends/fs_windows.ex index d2ed0da..c06f638 100644 --- a/lib/file_system/backends/fs_windows.ex +++ b/lib/file_system/backends/fs_windows.ex @@ -43,10 +43,11 @@ defmodule FileSystem.Backends.FSWindows do def bootstrap do exec_file = executable_path() + if not is_nil(exec_file) and File.exists?(exec_file) do :ok else - Logger.error "Can't find executable `inotifywait.exe`" + Logger.error("Can't find executable `inotifywait.exe`") {:error, :fs_windows_bootstrap_error} end end @@ -60,7 +61,8 @@ defmodule FileSystem.Backends.FSWindows do end defp executable_path do - executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv) + executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || + executable_path(:priv) end defp executable_path(:config) do @@ -78,8 +80,12 @@ defmodule FileSystem.Backends.FSWindows do defp executable_path(:priv) do case :code.priv_dir(:file_system) do {:error, _} -> - Logger.error "`priv` dir for `:file_system` application is not available in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` env." + Logger.error( + "`priv` dir for `:file_system` application is not available in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` env." + ) + nil + dir when is_list(dir) -> Path.join(dir, @default_exec_file) end @@ -88,31 +94,42 @@ defmodule FileSystem.Backends.FSWindows do def parse_options(options) do case Keyword.pop(options, :dirs) do {nil, _} -> - Logger.error "required argument `dirs` is missing" + Logger.error("required argument `dirs` is missing") {:error, :missing_dirs_argument} + {dirs, rest} -> format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist + args = [ - '--format', format, '--quiet', '-m', '-r' + ~c"--format", + format, + ~c"--quiet", + ~c"-m", + ~c"-r" | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1) ] + parse_options(rest, args) end end defp parse_options([], result), do: {:ok, result} + defp parse_options([{:recursive, true} | t], result) do parse_options(t, result) end + defp parse_options([{:recursive, false} | t], result) do - parse_options(t, result -- ['-r']) + parse_options(t, result -- [~c"-r"]) end + defp parse_options([{:recursive, value} | t], result) do - Logger.error "unknown value `#{inspect value}` for recursive, ignore" + Logger.error("unknown value `#{inspect(value)}` for recursive, ignore") parse_options(t, result) end + defp parse_options([h | t], result) do - Logger.error "unknown option `#{inspect h}`, ignore" + Logger.error("unknown option `#{inspect(h)}`, ignore") parse_options(t, result) end @@ -122,32 +139,36 @@ defmodule FileSystem.Backends.FSWindows do def init(args) do {worker_pid, rest} = Keyword.pop(args, :worker_pid) + case parse_options(rest) do {:ok, port_args} -> - port = Port.open( - {:spawn_executable, to_charlist(executable_path())}, - [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] - ) + port = + Port.open( + {:spawn_executable, to_charlist(executable_path())}, + [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] + ) + Process.link(port) Process.flag(:trap_exit, true) {:ok, %{port: port, worker_pid: worker_pid}} + {:error, _} -> :ignore end end - def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do + 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 + 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({:EXIT, port, _reason}, %{port: port}=state) do + def handle_info({:EXIT, port, _reason}, %{port: port} = state) do send(state.worker_pid, {:backend_file_event, self(), :stop}) {:stop, :normal, state} end @@ -160,14 +181,15 @@ defmodule FileSystem.Backends.FSWindows 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} + [path, flags] -> {path, flags} end + {path |> Path.split() |> Path.join(), 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("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 + defp convert_flag(_), do: :undefined end diff --git a/lib/file_system/worker.ex b/lib/file_system/worker.ex index e24b755..1166cc8 100644 --- a/lib/file_system/worker.ex +++ b/lib/file_system/worker.ex @@ -15,9 +15,9 @@ defmodule FileSystem.Worker do @doc false def init(args) do {backend, rest} = Keyword.pop(args, :backend) + with {:ok, backend} <- FileSystem.Backend.backend(backend), - {:ok, backend_pid} <- backend.start_link([{:worker_pid, self()} | rest]) - do + {:ok, backend_pid} <- backend.start_link([{:worker_pid, self()} | rest]) do {:ok, %{backend_pid: backend_pid, subscribers: %{}}} else _ -> :ignore @@ -32,10 +32,15 @@ defmodule FileSystem.Worker do end @doc false - def handle_info({:backend_file_event, backend_pid, file_event}, %{backend_pid: backend_pid}=state) do - state.subscribers |> Enum.each(fn {_ref, subscriber_pid} -> + 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 diff --git a/mix.exs b/mix.exs index b67eaef..36990ba 100644 --- a/mix.exs +++ b/mix.exs @@ -61,9 +61,7 @@ defmodule FileSystem.Mixfile do ldflags = System.get_env("LDFLAGS", "") cmd = - "clang #{cflags} #{ldflags} -framework CoreFoundation -framework CoreServices -Wno-deprecated-declarations #{ - source - } -o #{target}" + "clang #{cflags} #{ldflags} -framework CoreFoundation -framework CoreServices -Wno-deprecated-declarations #{source} -o #{target}" if Mix.shell().cmd(cmd) > 0 do Logger.error( diff --git a/test/backends/fs_inotify_test.exs b/test/backends/fs_inotify_test.exs index a499654..87b44f2 100644 --- a/test/backends/fs_inotify_test.exs +++ b/test/backends/fs_inotify_test.exs @@ -5,31 +5,109 @@ defmodule FileSystem.Backends.FSInotifyTest do describe "options parse test" do test "without :dirs" do assert {:error, _} = parse_options([]) - assert {:error, _} = parse_options([recursive: 1]) + assert {:error, _} = parse_options(recursive: 1) end test "supported options" do - assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from', '-e', 'create', - '-e', 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102], - '--quiet', '-m', '-r', '/tmp']} == - parse_options(dirs: ["/tmp"], recursive: true) - - assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from', '-e', 'create', - '-e', 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102], - '--quiet', '-m', '/tmp']} == - parse_options(dirs: ["/tmp"], recursive: false) + assert {:ok, + [ + ~c"-e", + ~c"modify", + ~c"-e", + ~c"close_write", + ~c"-e", + ~c"moved_to", + ~c"-e", + ~c"moved_from", + ~c"-e", + ~c"create", + ~c"-e", + ~c"delete", + ~c"-e", + ~c"attrib", + ~c"--format", + [37, 119, 1, 37, 101, 1, 37, 102], + ~c"--quiet", + ~c"-m", + ~c"-r", + ~c"/tmp" + ]} == + parse_options(dirs: ["/tmp"], recursive: true) + + assert {:ok, + [ + ~c"-e", + ~c"modify", + ~c"-e", + ~c"close_write", + ~c"-e", + ~c"moved_to", + ~c"-e", + ~c"moved_from", + ~c"-e", + ~c"create", + ~c"-e", + ~c"delete", + ~c"-e", + ~c"attrib", + ~c"--format", + [37, 119, 1, 37, 101, 1, 37, 102], + ~c"--quiet", + ~c"-m", + ~c"/tmp" + ]} == + parse_options(dirs: ["/tmp"], recursive: false) end test "ignore unsupported options" do - assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from', '-e', 'create', - '-e', 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102], - '--quiet', '-m', '/tmp']} == - parse_options(dirs: ["/tmp"], recursive: false, unsupported: :options) - - assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from', '-e', 'create', - '-e', 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102], - '--quiet', '-m', '-r', '/tmp']} == - parse_options(dirs: ["/tmp"], recursive: :unknown_value) + assert {:ok, + [ + ~c"-e", + ~c"modify", + ~c"-e", + ~c"close_write", + ~c"-e", + ~c"moved_to", + ~c"-e", + ~c"moved_from", + ~c"-e", + ~c"create", + ~c"-e", + ~c"delete", + ~c"-e", + ~c"attrib", + ~c"--format", + [37, 119, 1, 37, 101, 1, 37, 102], + ~c"--quiet", + ~c"-m", + ~c"/tmp" + ]} == + parse_options(dirs: ["/tmp"], recursive: false, unsupported: :options) + + assert {:ok, + [ + ~c"-e", + ~c"modify", + ~c"-e", + ~c"close_write", + ~c"-e", + ~c"moved_to", + ~c"-e", + ~c"moved_from", + ~c"-e", + ~c"create", + ~c"-e", + ~c"delete", + ~c"-e", + ~c"attrib", + ~c"--format", + [37, 119, 1, 37, 101, 1, 37, 102], + ~c"--quiet", + ~c"-m", + ~c"-r", + ~c"/tmp" + ]} == + parse_options(dirs: ["/tmp"], recursive: :unknown_value) end end @@ -38,49 +116,48 @@ defmodule FileSystem.Backends.FSInotifyTest do test "dir write close" do assert {"/one/two/file", [:modified, :closed]} == - ~w|/one/two/ CLOSE_WRITE,CLOSE file| |> to_port_line |> parse_line + ~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 + ~w|/one/two/ CREATE file| |> to_port_line |> parse_line end test "dir moved to" do assert {"/one/two/file", [:moved_to]} == - ~w|/one/two/ MOVED_TO file| |> to_port_line |> parse_line + ~w|/one/two/ MOVED_TO file| |> to_port_line |> parse_line end test "dir moved from" do assert {"/one/two/file", [:moved_from]} == - ~w|/one/two/ MOVED_FROM file| |> to_port_line |> parse_line + ~w|/one/two/ MOVED_FROM 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 + ~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 + ~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 + ~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 + ["/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 + ["/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 + ["/one two", "CLOSE_WRITE,CLOSE", "file 1"] |> to_port_line |> parse_line end end - end diff --git a/test/backends/fs_mac_test.exs b/test/backends/fs_mac_test.exs index 67a504a..0a64d66 100644 --- a/test/backends/fs_mac_test.exs +++ b/test/backends/fs_mac_test.exs @@ -5,37 +5,37 @@ defmodule FileSystem.Backends.FSMacTest do describe "options parse test" do test "without :dirs" do assert {:error, _} = parse_options([]) - assert {:error, _} = parse_options([latency: 1]) + assert {:error, _} = parse_options(latency: 1) end test "supported options" do - assert {:ok, ['--with-root', '--no-defer', '--latency=0.0', '-F', '/tmp']} == - parse_options(dirs: ["/tmp"], latency: 0, no_defer: true, with_root: true) + assert {:ok, [~c"--with-root", ~c"--no-defer", ~c"--latency=0.0", ~c"-F", ~c"/tmp"]} == + parse_options(dirs: ["/tmp"], latency: 0, no_defer: true, with_root: true) - assert {:ok, ['--no-defer', '--latency=1.1', '-F', '/tmp1', '/tmp2']} == - parse_options(dirs: ["/tmp1", "/tmp2"], latency: 1.1, no_defer: true) + assert {:ok, [~c"--no-defer", ~c"--latency=1.1", ~c"-F", ~c"/tmp1", ~c"/tmp2"]} == + parse_options(dirs: ["/tmp1", "/tmp2"], latency: 1.1, no_defer: true) end test "ignore unsupported options" do - assert {:ok, ['--latency=0.0', '-F', '/tmp']} == - parse_options(dirs: ["/tmp"], latency: 0, unsuppported: :options) + assert {:ok, [~c"--latency=0.0", ~c"-F", ~c"/tmp"]} == + parse_options(dirs: ["/tmp"], latency: 0, unsuppported: :options) end end describe "port line parse test" do test "file modified" do assert {"/one/two/file", [:inodemetamod, :modified]} == - parse_line('37425557\t0x00011400=[inodemetamod,modified]\t/one/two/file') + parse_line(~c"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') + parse_line(~c"37425557\t0x00011400=[inodemetamod,modified]\t/one two/file") end test "equal character in file" do assert {"/one two/file=2", [:inodemetamod, :modified]} == - parse_line('37425557\t0x00011400=[inodemetamod,modified]\t/one two/file=2') + parse_line(~c"37425557\t0x00011400=[inodemetamod,modified]\t/one two/file=2") end end end diff --git a/test/backends/fs_poll_test.exs b/test/backends/fs_poll_test.exs index e2587c2..39c1f23 100644 --- a/test/backends/fs_poll_test.exs +++ b/test/backends/fs_poll_test.exs @@ -7,7 +7,7 @@ defmodule FileSystem.Backends.FSPollTest do @stale %{ "modified" => @mtime1, - "deleted" => @mtime1, + "deleted" => @mtime1 } @fresh %{ diff --git a/test/file_system_test.exs b/test/file_system_test.exs index 2026933..7b59b6d 100644 --- a/test/file_system_test.exs +++ b/test/file_system_test.exs @@ -2,7 +2,7 @@ defmodule FileSystemTest do use ExUnit.Case, async: true test "file event api" do - tmp_dir = System.cmd("mktemp", ["-d"]) |> elem(0) |> String.trim + tmp_dir = System.cmd("mktemp", ["-d"]) |> elem(0) |> String.trim() {:ok, pid} = FileSystem.start_link(dirs: [tmp_dir]) FileSystem.subscribe(pid) @@ -10,10 +10,12 @@ defmodule FileSystemTest do File.touch("#{tmp_dir}/a") assert_receive {:file_event, ^pid, {_path, _events}}, 5000 - new_subscriber = spawn(fn -> - FileSystem.subscribe(pid) - :timer.sleep(10000) - end) + new_subscriber = + spawn(fn -> + FileSystem.subscribe(pid) + :timer.sleep(10000) + end) + assert Process.alive?(new_subscriber) Process.exit(new_subscriber, :kill) refute Process.alive?(new_subscriber) @@ -24,9 +26,10 @@ defmodule FileSystemTest do Port.list() |> Enum.reject(fn port -> - :undefined == port |> Port.info |> Access.get(:os_pid) + :undefined == port |> Port.info() |> Access.get(:os_pid) end) |> Enum.each(&Port.close/1) + assert_receive {:file_event, ^pid, :stop}, 5000 File.rm_rf!(tmp_dir)