fs_mac.ex (6080B)
1 require Logger 2 3 defmodule FileSystem.Backends.FSMac do 4 @moduledoc """ 5 This file is a fork from https://github.com/synrc/fs. 6 FileSysetm backend for macos, a GenServer receive data from Port, parse event 7 and send it to the worker process. 8 Will compile executable the buildin executable file when file the first time it is used. 9 10 ## Backend Options 11 12 * `:latency` (float, default: 0.5), latency period. 13 14 * `:no_defer` (bool, default: false), enable no-defer latency modifier. 15 Works with latency parameter, Also check apple `FSEvent` api documents 16 https://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagnodefer 17 18 * `:watch_root` (bool, default: false), watch for when the root path has changed. 19 Set the flag `true` to monitor events when watching `/tmp/fs/dir` and run 20 `mv /tmp/fs /tmp/fx`. Also check apple `FSEvent` api documents 21 https://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagwatchroot 22 23 * recursive is enabled by default, no option to disable it for now. 24 25 ## Executable File Path 26 27 The default executable file is `mac_listener` in `priv` dir of `:file_system` application, there're two ways to custom it, useful when run `:file_system` with escript. 28 29 * config with `config.exs` 30 `config :file_system, :fs_mac, executable_file: "YOUR_EXECUTABLE_FILE_PATH"` 31 32 * config with `FILESYSTEM_FSMAC_EXECUTABLE_FILE` os environment 33 FILESYSTEM_FSMAC_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH 34 """ 35 36 use GenServer 37 @behaviour FileSystem.Backend 38 39 @default_exec_file "mac_listener" 40 41 def bootstrap do 42 exec_file = executable_path() 43 if not is_nil(exec_file) and File.exists?(exec_file) do 44 :ok 45 else 46 Logger.error "Can't find executable `mac_listener`" 47 {:error, :fs_mac_bootstrap_error} 48 end 49 end 50 51 def supported_systems do 52 [{:unix, :darwin}] 53 end 54 55 def known_events do 56 [ :mustscansubdirs, :userdropped, :kerneldropped, :eventidswrapped, :historydone, 57 :rootchanged, :mount, :unmount, :created, :removed, :inodemetamod, :renamed, :modified, 58 :finderinfomod, :changeowner, :xattrmod, :isfile, :isdir, :issymlink, :ownevent, 59 ] 60 end 61 62 defp executable_path do 63 executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv) 64 end 65 66 defp executable_path(:config) do 67 Application.get_env(:file_system, :fs_mac)[:executable_file] 68 end 69 70 defp executable_path(:system_env) do 71 System.get_env("FILESYSTEM_FSMAC_EXECUTABLE_FILE") 72 end 73 74 defp executable_path(:system_path) do 75 System.find_executable(@default_exec_file) 76 end 77 78 defp executable_path(:priv) do 79 case :code.priv_dir(:file_system) do 80 {:error, _} -> 81 Logger.error "`priv` dir for `:file_system` application is not avalible in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSMAC_EXECUTABLE_FILE` env." 82 nil 83 dir when is_list(dir) -> 84 Path.join(dir, @default_exec_file) 85 end 86 end 87 88 def parse_options(options) do 89 case Keyword.pop(options, :dirs) do 90 {nil, _} -> 91 Logger.error "required argument `dirs` is missing" 92 {:error, :missing_dirs_argument} 93 {dirs, rest} -> 94 args = ['-F' | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)] 95 parse_options(rest, args) 96 end 97 end 98 99 defp parse_options([], result), do: {:ok, result} 100 defp parse_options([{:latency, latency} | t], result) do 101 result = 102 if is_float(latency) or is_integer(latency) do 103 ['--latency=#{latency / 1}' | result] 104 else 105 Logger.error "latency should be integer or float, got `#{inspect latency}, ignore" 106 result 107 end 108 parse_options(t, result) 109 end 110 defp parse_options([{:no_defer, true} | t], result) do 111 parse_options(t, ['--no-defer' | result]) 112 end 113 defp parse_options([{:no_defer, false} | t], result) do 114 parse_options(t, result) 115 end 116 defp parse_options([{:no_defer, value} | t], result) do 117 Logger.error "unknown value `#{inspect value}` for no_defer, ignore" 118 parse_options(t, result) 119 end 120 defp parse_options([{:with_root, true} | t], result) do 121 parse_options(t, ['--with-root' | result]) 122 end 123 defp parse_options([{:with_root, false} | t], result) do 124 parse_options(t, result) 125 end 126 defp parse_options([{:with_root, value} | t], result) do 127 Logger.error "unknown value `#{inspect value}` for with_root, ignore" 128 parse_options(t, result) 129 end 130 defp parse_options([h | t], result) do 131 Logger.error "unknown option `#{inspect h}`, ignore" 132 parse_options(t, result) 133 end 134 135 def start_link(args) do 136 GenServer.start_link(__MODULE__, args, []) 137 end 138 139 def init(args) do 140 {worker_pid, rest} = Keyword.pop(args, :worker_pid) 141 case parse_options(rest) do 142 {:ok, port_args} -> 143 port = Port.open( 144 {:spawn_executable, to_charlist(executable_path())}, 145 [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] 146 ) 147 Process.link(port) 148 Process.flag(:trap_exit, true) 149 {:ok, %{port: port, worker_pid: worker_pid}} 150 {:error, _} -> 151 :ignore 152 end 153 end 154 155 def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do 156 {file_path, events} = line |> parse_line 157 send(state.worker_pid, {:backend_file_event, self(), {file_path, events}}) 158 {:noreply, state} 159 end 160 161 def handle_info({port, {:exit_status, _}}, %{port: port}=state) do 162 send(state.worker_pid, {:backend_file_event, self(), :stop}) 163 {:stop, :normal, state} 164 end 165 166 def handle_info({:EXIT, port, _reason}, %{port: port}=state) do 167 send(state.worker_pid, {:backend_file_event, self(), :stop}) 168 {:stop, :normal, state} 169 end 170 171 def handle_info(_, state) do 172 {:noreply, state} 173 end 174 175 def parse_line(line) do 176 [_, _, events, path] = line |> to_string |> String.split(["\t", "="], parts: 4) 177 {path, events |> String.split(["[", ",", "]"], trim: true) |> Enum.map(&String.to_existing_atom/1)} 178 end 179 180 end