zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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