Skip to content
Snippets Groups Projects
Commit 15f29f76 authored by Xiangrong Hao's avatar Xiangrong Hao Committed by GitHub
Browse files

more friendly extra arguments (#32)

* more friendly extra arguments

* add unit test
parent 7cd97a32
Branches
No related tags found
No related merge requests found
...@@ -88,14 +88,14 @@ end ...@@ -88,14 +88,14 @@ end
``` ```
## Tweaking behaviour via listener extra arguments ## Tweaking behaviour via extra arguments
For each platform, you can pass extra arguments to the underlying listener process via the `listener_extra_args` option. For each platform, you can pass extra arguments to the underlying listener process.
Each backend support different extra arguments, check backend module documentation for more information.
Here is an example to get instant notifications on file changes for Mac OS X: Here is an example to get instant notifications on file changes for Mac OS X:
```elixir ```elixir
FileSystem.start_link(dirs: ["/path/to/some/files"], listener_extra_args: "--latency=0.0") FileSystem.start_link(dirs: ["/path/to/some/files"], latency: 0, watch_root: true)
``` ```
See the [fs source](https://github.com/synrc/fs/tree/master/c_src) for more details.
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config use Mix.Config
# This configuration is loaded before any dependency and is restricted if :test == Mix.env do
# to this project. If another project depends on this project, this config :logger, backends: []
# file won't be loaded nor affect the parent project. For this reason, end
# if you want to provide default values for your application for third-
# party users, it should be done in your mix.exs file.
# Sample configuration:
#
# config :logger, :console,
# level: :info,
# format: "$date $time [$level] $metadata$message\n",
# metadata: [:user_id]
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
...@@ -10,13 +10,13 @@ defmodule FileSystem do ...@@ -10,13 +10,13 @@ defmodule FileSystem do
for `macos`, `:fs_inotify` for `linux` and `freebsd`, for `macos`, `:fs_inotify` for `linux` and `freebsd`,
`:fs_windows` for `windows` `:fs_windows` for `windows`
* `:listener_extra_args` (string, optional), extra args for
port backend.
* `:name` (atom, optional), `name` can be used to subscribe as * `:name` (atom, optional), `name` can be used to subscribe as
the same as pid when the `name` is given. The `name` should the same as pid when the `name` is given. The `name` should
be the name of worker process. be the name of worker process.
* All rest options will treated as backend options. See backend
module documents for more details.
## Example ## Example
Simple usage: Simple usage:
...@@ -26,7 +26,7 @@ defmodule FileSystem do ...@@ -26,7 +26,7 @@ defmodule FileSystem do
Get instant notifications on file changes for Mac OS X: Get instant notifications on file changes for Mac OS X:
iex> FileSystem.start_link(dirs: ["/path/to/some/files"], listener_extra_args: "--latency=0.0") iex> FileSystem.start_link(dirs: ["/path/to/some/files"], latency: 0)
Named monitor with specified backend: Named monitor with specified backend:
......
require Logger require Logger
alias FileSystem.Utils
defmodule FileSystem.Backends.FSInotify do defmodule FileSystem.Backends.FSInotify do
@moduledoc """ @moduledoc """
...@@ -7,6 +6,10 @@ defmodule FileSystem.Backends.FSInotify do ...@@ -7,6 +6,10 @@ defmodule FileSystem.Backends.FSInotify do
FileSysetm backend for linux and freebsd, a GenServer receive data from Port, parse event FileSysetm backend for linux and freebsd, a GenServer receive data from Port, parse event
and send it to the worker process. and send it to the worker process.
Need `inotify-tools` installed to use this backend. Need `inotify-tools` installed to use this backend.
## Backend Options
* `:recursive` (bool, default: true), monitor directories and their contents recursively
""" """
use GenServer use GenServer
...@@ -35,24 +38,56 @@ defmodule FileSystem.Backends.FSInotify do ...@@ -35,24 +38,56 @@ defmodule FileSystem.Backends.FSInotify do
System.find_executable("inotifywait") System.find_executable("inotifywait")
end end
def parse_options(options) do
case Keyword.pop(options, :dirs) do
{nil, _} ->
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', 'create',
'-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-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'])
end
defp parse_options([{:recursive, value} | t], result) do
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"
parse_options(t, result)
end
def start_link(args) do def start_link(args) do
GenServer.start_link(__MODULE__, args, []) GenServer.start_link(__MODULE__, args, [])
end end
def init(args) do def init(args) do
port_path = Utils.format_path(args[:dirs]) {worker_pid, rest} = Keyword.pop(args, :worker_pid)
format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist case parse_options(rest) do
port_args = [ {:ok, port_args} ->
'-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'create', port = Port.open(
'-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-r' {:spawn_executable, to_charlist(find_executable())},
] ++ Utils.format_args(args[:listener_extra_args]) ++ port_path [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}]
port = Port.open( )
{:spawn_executable, to_charlist(find_executable())}, Process.link(port)
[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] Process.flag(:trap_exit, true)
) {:ok, %{port: port, worker_pid: worker_pid}}
Process.link(port) {:error, _} ->
Process.flag(:trap_exit, true) :ignore
{:ok, %{port: port, worker_pid: args[:worker_pid]}} end
end end
def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
......
require Logger require Logger
alias FileSystem.Utils
defmodule FileSystem.Backends.FSMac do defmodule FileSystem.Backends.FSMac do
@moduledoc """ @moduledoc """
This file is a fork from https://github.com/synrc/fs. This file is a fork from https://github.com/synrc/fs.
FileSysetm backend for macos, a GenServer receive data from Port, parse event FileSysetm backend for macos, a GenServer receive data from Port, parse event
and send it to the worker process. and send it to the worker process.
will compile executable the buildin executable file when file the first time it is used. Will compile executable the buildin executable file when file the first time it is used.
## Backend Options
* `:latency` (float, default: 0.5), latency period.
* `:no_defer` (bool, default: false), enable no-defer latency modifier.
Works with latency parameter, Also check apple `FSEvent` api documents
https://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagnodefer
* `:watch_root` (bool, default: false), watch for when the root path has changed.
Set the flag `true` to monitor events when watching `/tmp/fs/dir` and run
`mv /tmp/fs /tmp/fx`. Also check apple `FSEvent` api documents
https://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagwatchroot
* recursive is enabled by default, no option to disable it for now.
""" """
use GenServer use GenServer
...@@ -44,21 +58,68 @@ defmodule FileSystem.Backends.FSMac do ...@@ -44,21 +58,68 @@ defmodule FileSystem.Backends.FSMac do
(:code.priv_dir(:file_system) ++ '/mac_listener') |> to_string (:code.priv_dir(:file_system) ++ '/mac_listener') |> to_string
end end
def parse_options(options) do
case Keyword.pop(options, :dirs) do
{nil, _} ->
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)]
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]
else
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])
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"
parse_options(t, result)
end
defp parse_options([{:with_root, true} | t], result) do
parse_options(t, ['--with-root' | result])
end
defp parse_options([{:with_root, value} | t], result) do
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"
parse_options(t, result)
end
def start_link(args) do def start_link(args) do
GenServer.start_link(__MODULE__, args, []) GenServer.start_link(__MODULE__, args, [])
end end
def init(args) do def init(args) do
port_path = Utils.format_path(args[:dirs]) {worker_pid, rest} = Keyword.pop(args, :worker_pid)
port_args = Utils.format_args(args[:listener_extra_args]) ++ ['-F' | port_path] case parse_options(rest) do
port = Port.open( {:ok, port_args} ->
{:spawn_executable, to_charlist(find_executable())}, port = Port.open(
[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] {:spawn_executable, to_charlist(find_executable())},
) [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}]
Process.link(port) )
Process.flag(:trap_exit, true) Process.link(port)
{:ok, %{port: port, worker_pid: args[:worker_pid]}} Process.flag(:trap_exit, true)
{:ok, %{port: port, worker_pid: worker_pid}}
{:error, _} ->
:ignore
end
end end
def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
......
require Logger require Logger
alias FileSystem.Utils
defmodule FileSystem.Backends.FSWindows do defmodule FileSystem.Backends.FSWindows do
@moduledoc """ @moduledoc """
...@@ -7,6 +6,10 @@ defmodule FileSystem.Backends.FSWindows do ...@@ -7,6 +6,10 @@ defmodule FileSystem.Backends.FSWindows do
FileSysetm backend for windows, a GenServer receive data from Port, parse event FileSysetm backend for windows, a GenServer receive data from Port, parse event
and send it to the worker process. and send it to the worker process.
Need binary executable file packaged in to use this backend. Need binary executable file packaged in to use this backend.
## Backend Options
* `:recursive` (bool, default: true), monitor directories and their contents recursively
""" """
use GenServer use GenServer
...@@ -35,23 +38,55 @@ defmodule FileSystem.Backends.FSWindows do ...@@ -35,23 +38,55 @@ defmodule FileSystem.Backends.FSWindows do
(:code.priv_dir(:file_system) ++ '/inotifywait.exe') |> to_string (:code.priv_dir(:file_system) ++ '/inotifywait.exe') |> to_string
end end
def parse_options(options) do
case Keyword.pop(options, :dirs) do
{nil, _} ->
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'
| 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'])
end
defp parse_options([{:recursive, value} | t], result) do
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"
parse_options(t, result)
end
def start_link(args) do def start_link(args) do
GenServer.start_link(__MODULE__, args, []) GenServer.start_link(__MODULE__, args, [])
end end
def init(args) do def init(args) do
port_path = Utils.format_path(args[:dirs]) {worker_pid, rest} = Keyword.pop(args, :worker_pid)
format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist case parse_options(rest) do
port_args = Utils.format_args(args[:listener_extra_args]) ++ [ {:ok, port_args} ->
'--format', format, '--quiet', '-m', '-r' | port_path port = Port.open(
] {:spawn_executable, to_charlist(find_executable())},
port = Port.open( [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}]
{:spawn_executable, to_charlist(find_executable())}, )
[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] Process.link(port)
) Process.flag(:trap_exit, true)
Process.link(port) {:ok, %{port: port, worker_pid: worker_pid}}
Process.flag(:trap_exit, true) {:error, _} ->
{:ok, %{port: port, worker_pid: args[:worker_pid]}} :ignore
end
end end
def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
......
defmodule FileSystem.Utils do
@moduledoc false
@doc false
@spec format_path(String.t() | [String.t()]) :: [charlist()]
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
@doc false
@spec format_args(nil | String.t() | [String.t()]) :: [charlist()]
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
...@@ -8,18 +8,19 @@ defmodule FileSystem.Worker do ...@@ -8,18 +8,19 @@ defmodule FileSystem.Worker do
@doc false @doc false
def start_link(args) do def start_link(args) do
{args, opts} = Keyword.split(args, [:backend, :dirs, :listener_extra_args]) {opts, args} = Keyword.split(args, [:name])
GenServer.start_link(__MODULE__, args, opts) GenServer.start_link(__MODULE__, args, opts)
end end
@doc false @doc false
def init(args) do def init(args) do
case FileSystem.Backend.backend(args[:backend]) do {backend, rest} = Keyword.pop(args, :backend)
{:ok, backend} -> with {:ok, backend} <- FileSystem.Backend.backend(backend),
{:ok, backend_pid} = backend.start_link([{:worker_pid, self()} | Keyword.drop(args, [:backend])]) {:ok, backend_pid} <- backend.start_link([{:worker_pid, self()} | rest])
{:ok, %{backend_pid: backend_pid, subscribers: %{}}} do
{:error, _reason} -> {:ok, %{backend_pid: backend_pid, subscribers: %{}}}
:ignore else
_ -> :ignore
end end
end end
......
...@@ -2,47 +2,75 @@ defmodule FileSystem.Backends.FSInotifyTest do ...@@ -2,47 +2,75 @@ defmodule FileSystem.Backends.FSInotifyTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
import FileSystem.Backends.FSInotify import FileSystem.Backends.FSInotify
test "dir write close" do describe "options parse test" do
assert {"/one/two/file", [:modified, :closed]} == test "without :dirs" do
~w|/one/two/ CLOSE_WRITE,CLOSE file| |> to_port_line |> parse_line assert {:error, _} = parse_options([])
end assert {:error, _} = parse_options([recursive: 1])
end
test "dir create" do test "supported options" do
assert {"/one/two/file", [:created]} == assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'create', '-e',
~w|/one/two/ CREATE file| |> to_port_line |> parse_line 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102],
end '--quiet', '-m', '-r', '/tmp']} ==
parse_options(dirs: ["/tmp"], recursive: true)
test "dir moved to" do assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'create', '-e',
assert {"/one/two/file", [:renamed]} == 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102],
~w|/one/two/ MOVED_TO file| |> to_port_line |> parse_line '--quiet', '-m', '/tmp']} ==
end parse_options(dirs: ["/tmp"], recursive: false)
end
test "dir is_dir create" do test "ignore unsupported options" do
assert {"/one/two/dir", [:created, :isdir]} == assert {:ok, ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'create', '-e',
~w|/one/two/ CREATE,ISDIR dir| |> to_port_line |> parse_line 'delete', '-e', 'attrib', '--format', [37, 119, 1, 37, 101, 1, 37, 102],
'--quiet', '-m', '/tmp']} ==
parse_options(dirs: ["/tmp"], recursive: false, unsuppported: :options)
end
end end
test "file write close" do describe "port line parse test" do
assert {"/one/two/file", [:modified, :closed]} == defp to_port_line(list), do: list |> Enum.join(<<1>>) |> to_charlist
~w|/one/two/file CLOSE_WRITE,CLOSE| |> to_port_line |> parse_line
end
test "file delete_self" do test "dir write close" do
assert {"/one/two/file", [:undefined]} == assert {"/one/two/file", [:modified, :closed]} ==
~w|/one/two/file DELETE_SELF| |> to_port_line |> parse_line ~w|/one/two/ CLOSE_WRITE,CLOSE file| |> to_port_line |> parse_line
end end
test "whitespace in path" do test "dir create" do
assert {"/one two/file", [:modified, :closed]} == assert {"/one/two/file", [:created]} ==
["/one two", "CLOSE_WRITE,CLOSE", "file"] |> to_port_line |> parse_line ~w|/one/two/ CREATE file| |> to_port_line |> parse_line
end
assert {"/one/two/file 1", [:modified, :closed]} == test "dir moved to" do
["/one/two", "CLOSE_WRITE,CLOSE", "file 1"] |> to_port_line |> parse_line assert {"/one/two/file", [:renamed]} ==
~w|/one/two/ MOVED_TO file| |> to_port_line |> parse_line
end
assert {"/one two/file 1", [:modified, :closed]} == test "dir is_dir create" do
["/one two", "CLOSE_WRITE,CLOSE", "file 1"] |> to_port_line |> parse_line assert {"/one/two/dir", [:created, :isdir]} ==
end ~w|/one/two/ CREATE,ISDIR dir| |> to_port_line |> parse_line
end
defp to_port_line(list), do: list |> Enum.join(<<1>>) |> to_charlist 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
end
end end
...@@ -2,13 +2,35 @@ defmodule FileSystem.Backends.FSMacTest do ...@@ -2,13 +2,35 @@ defmodule FileSystem.Backends.FSMacTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
import FileSystem.Backends.FSMac import FileSystem.Backends.FSMac
test "file modified" do describe "options parse test" do
assert {"/one/two/file", [:inodemetamod, :modified]} == test "without :dirs" do
parse_line('37425557\t0x00011400=[inodemetamod,modified]\t/one/two/file') assert {:error, _} = parse_options([])
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, ['--no-defer', '--latency=1.1', '-F', '/tmp1', '/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)
end
end end
test "whitespace in path" do describe "port line parse test" do
assert {"/one two/file", [:inodemetamod, :modified]} == test "file modified" do
parse_line('37425557\t0x00011400=[inodemetamod,modified]\t/one two/file') 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 end
end end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment