defmodule Exfuse.Fs.Elixir do
@moduledoc """
A UserFs filesystem implementation module which makes
available common data from the Elixir run time.
The implementation breaks the file path into leaves and the traverse/4
function works through them to find the end of the path.
The traverse/4 function verifies each element of the path as it is
called. This is necessary because it is no use continuing past the
pids/<0.63.0> point if PID <0.63.0> does not exist! The first two
parameters to traverse/4 are passed on and through so they are
available when the traversal finishes, the third parameter is the
context in which to evaluate the next path element (for example
traversing the "pids" directory results in the context being
'all_pids', and the element after that is {pid, Pid}, enabling, for
example, a read-directory event, if it is invoked at that
point, to enumerate all objects that are inside a PID.
See 'Exfuse.Fs.Example' for a simpler example of a filesystem, or the
simple hello-world `Exfuse.Fs.Hello`.
"""
use Exfuse.Fs
defstruct mount_point: nil
def exfuse_init(mount_point, _opts) do
{:ok, %__MODULE__{mount_point: mount_point}}
end
def handle_event(:readdir, %{path: path}, socket) do
dispatch(socket, :elixirfs_readdir, path)
end
def handle_event(:getattr, %{path: path}, socket) do
dispatch(socket, :elixirfs_getattr, path)
end
def handle_event(:readlink, %{path: path}, socket) do
dispatch(socket, :elixirfs_readlink, path)
end
def handle_event(:read, %{path: path, offset: offset, size: size}, socket) do
with {:reply, content, socket} <- dispatch(socket, :elixirfs_read, path) do
{:reply, slice(content, offset, size), socket}
end
end
def handle_event(_op, _event, socket), do: {:error, :enosys, socket}
defp dispatch(%Exfuse.Socket{state: state} = socket, request, "/" <> path) do
path_leaves = String.split(path, "/", parts: :infinity)
case traverse(state, request, :root, path_leaves) do
{:ok, reply, new_state} -> {:reply, reply, Exfuse.Socket.put_state(socket, new_state)}
{:error, reason, new_state} -> {:error, reason, Exfuse.Socket.put_state(socket, new_state)}
end
end
defp slice(content, offset, size) do
start = min(offset, byte_size(content))
count = min(size, byte_size(content) - start)
binary_part(content, start, count)
end
defp traverse(state, request, :root, [""]) do
apply(__MODULE__, request, [state, :root])
end
defp traverse(state, request, :root, ["pids" | more]) do
traverse(state, request, :all_pids, more)
end
defp traverse(state, request, :all_pids, []) do
apply(__MODULE__, request, [state, :all_pids])
end
defp traverse(state, request, :all_pids, ["#PID" <> pid | more]) do
traverse(state, request, {:pid, :erlang.list_to_pid(String.to_charlist(pid))}, more)
end
defp traverse(state, request, {:pid, pid}, []) do
apply(__MODULE__, request, [state, {:pid, pid}])
end
defp traverse(state, request, :root, ["names" | more]) do
traverse(state, request, :names, more)
end
defp traverse(state, request, :names, []) do
apply(__MODULE__, request, [state, :names])
end
defp traverse(state, request, :names, ["local" | more]) do
traverse(state, request, :local_names, more)
end
defp traverse(state, request, :local_names, []) do
apply(__MODULE__, request, [state, :local_names])
end
defp traverse(state, request, :local_names, [name | more]) do
traverse(state, request, {:local_name, String.to_atom(name)}, more)
end
defp traverse(state, request, {:local_name, name}, []) do
apply(__MODULE__, request, [state, {:local_name, name}])
end
defp traverse(state, request, :names, ["global" | more]) do
traverse(state, request, :global_names, more)
end
defp traverse(state, request, :global_names, []) do
apply(__MODULE__, request, [state, :global_names])
end
defp traverse(state, request, :global_names, [name | more]) do
traverse(state, request, {:global_name, String.to_atom(name)}, more)
end
defp traverse(state, request, {:global_name, name}, []) do
apply(__MODULE__, request, [state, {:global_name, name}])
end
defp traverse(state, request, {:pid, pid}, ["process_info" | more]) do
traverse(state, request, {:proc_info, pid}, more)
end
defp traverse(state, request, {:proc_info, pid}, []) do
apply(__MODULE__, request, [state, {:proc_info, pid}])
end
defp traverse(state, request, {:proc_info, pid}, [item_spec]) do
apply(__MODULE__, request, [state, {:proc_info, pid, String.to_atom(item_spec)}])
end
defp traverse(state, request, {:pid, pid}, ["linked" | more]) do
traverse(state, request, {:link_from, pid}, more)
end
defp traverse(state, request, {:link_from, pid}, []) do
apply(__MODULE__, request, [state, {:link_from, pid}])
end
defp traverse(state, request, {:link_from, _pid}, ["#PID" <> linked_pid]) do
apply(__MODULE__, request, [
state,
{:link_to, :erlang.list_to_pid(String.to_charlist(linked_pid))}
])
end
defp traverse(state, request, :root, ["nodes" | more]) do
traverse(state, request, :nodes, more)
end
defp traverse(state, request, :nodes, []) do
apply(__MODULE__, request, [state, :nodes])
end
defp traverse(state, request, :nodes, [node]) do
apply(__MODULE__, request, [state, {:node, String.to_atom(node)}])
end
defp traverse(state, request, :root, ["apps" | more]) do
traverse(state, request, :apps, more)
end
defp traverse(state, request, :apps, []) do
apply(__MODULE__, request, [state, :apps])
end
defp traverse(state, request, :apps, [name | more]) do
traverse(state, request, {:app, String.to_atom(name)}, more)
end
defp traverse(state, request, {:app, app}, []) do
apply(__MODULE__, request, [state, {:app, app}])
end
defp traverse(state, request, {:app, app}, [app_sub_dir | more]) do
traverse(state, request, {:app, app, app_sub_dir}, more)
end
defp traverse(state, request, {:app, app, app_sub_dir}, []) do
apply(__MODULE__, request, [state, {:app, app, app_sub_dir}])
end
defp traverse(state, request, {:app, app, "env"}, [opt]) do
apply(__MODULE__, request, [state, {:app_env, app, String.to_atom(opt)}])
end
defp traverse(state, request, :root, ["code" | more]) do
traverse(state, request, :code, more)
end
defp traverse(state, request, :code, []) do
apply(__MODULE__, request, [state, :code])
end
defp traverse(state, request, :code, ["modules" | more]) do
traverse(state, request, {:code, :modules}, more)
end
defp traverse(state, request, {:code, :modules}, []) do
apply(__MODULE__, request, [state, {:code, :modules}])
end
defp traverse(state, request, {:code, :modules}, [module | more]) do
traverse(state, request, {:code, :module, String.to_atom(module)}, more)
end
defp traverse(state, request, {code, :module, module}, []) do
apply(__MODULE__, request, [state, {code, :module, module}])
end
defp traverse(state, request, {code, :module, module}, ["file"]) do
apply(__MODULE__, request, [state, {code, :module, module, :file}])
end
defp traverse(state, _, _, _) do
{:error, @error_noent, state}
end
@doc false
def elixirfs_readdir(state, :root) do
{:ok, ["pids", "names", "nodes", "apps", "code"], state}
end
def elixirfs_readdir(state, :all_pids) do
readdir =
for p when is_pid(p) <- Process.list(),
do: "#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(p))
{:ok, readdir, state}
end
def elixirfs_readdir(state, :names) do
{:ok, ["local", "global"], state}
end
def elixirfs_readdir(state, :local_names) do
local_names = for n <- Process.registered(), do: "#{n}"
{:ok, local_names, state}
end
def elixirfs_readdir(state, :global_names) do
global_names = for n <- :global.registered_names(), do: "#{n}"
{:ok, global_names, state}
end
def elixirfs_readdir(state, {:pid, _pid}) do
{:ok, ["process_info", "linked"], state}
end
def elixirfs_readdir(state, {:proc_info, pid}) do
case Process.info(pid) do
nil ->
{:error, @error_noent, state}
info ->
proc_info = for {k, _} <- info, do: "#{k}"
{:ok, proc_info, state}
end
end
def elixirfs_readdir(state, {:link_from, pid}) do
{:links, linked} = Process.info(pid, :links)
readdir =
for p when is_pid(p) <- linked, do: "#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(p))
{:ok, readdir, state}
end
def elixirfs_readdir(state, :nodes) do
readdir = for n <- [node() | Node.list()], do: "#{n}"
{:ok, readdir, state}
end
def elixirfs_readdir(state, {:node, _node}) do
{:ok, [], state}
end
def elixirfs_readdir(state, :apps) do
{:ok, for({n, _} <- running_apps(), do: "#{n}"), state}
end
def elixirfs_readdir(state, {:app, app}) do
case running_app_pid(app) do
{:ok, :undefined} ->
{:ok, ["descr", "vsn", "env"], state}
{:ok, _pid} ->
{:ok, ["app_proc", "top_sup", "descr", "vsn", "env"], state}
:error ->
{:error, @error_noent, state}
end
end
def elixirfs_readdir(state, {:app, app, "env"}) do
{:ok, for({opt, _val} <- Application.get_all_env(app), do: "#{opt}"), state}
end
def elixirfs_readdir(state, :code) do
{:ok, ["modules"], state}
end
def elixirfs_readdir(state, {:code, :modules}) do
{:ok, for({m, _file} <- :code.all_loaded(), do: "#{m}"), state}
end
def elixirfs_readdir(state, {:code, :module, _module}) do
{:ok, ["file"], state}
end
@doc false
def elixirfs_getattr(state, :root) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, :all_pids) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, :names) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, :local_names) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, :global_names) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:local_name, name}) do
case Process.whereis(name) do
nil -> {:error, @error_noent, state}
_pid -> {:ok, {0o0755, @attr_symlink, 0}, state}
end
end
def elixirfs_getattr(state, {:global_name, name}) do
case global_name_pid(name) do
{:ok, _pid} -> {:ok, {0o0755, @attr_symlink, 0}, state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_getattr(state, {:pid, _pid}) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:proc_info, _pid}) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:proc_info, pid, item_spec}) do
case safe_process_info(pid, item_spec) do
{:ok, {_item_spec, item_data}} ->
{:ok, {0o0644, @attr_file, byte_size(proc_info_content(item_data))}, state}
:error ->
{:error, @error_noent, state}
end
end
def elixirfs_getattr(state, :nodes) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:node, _Node}) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, :apps) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:app, app}) do
case running_app_pid(app) do
{:ok, _pid} -> {:ok, {0o0755, @attr_dir, 0}, state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_getattr(state, {:app, app, app_sub_dir}) do
attrs =
case {app_sub_dir, running_app_pid(app), loaded_app(app)} do
{"app_proc", {:ok, pid}, _loaded} when pid !== :undefined ->
{:ok, {0o0755, @attr_symlink, 0}}
{"top_sup", {:ok, pid}, _loaded} when pid !== :undefined ->
{:ok, {0o0755, @attr_symlink, 0}}
{"descr", _running, {:ok, _descr, _vsn}} ->
{:ok, {0o0644, @attr_file, file_size(state, {:app, app, app_sub_dir})}}
{"vsn", _running, {:ok, _descr, _vsn}} ->
{:ok, {0o0644, @attr_file, file_size(state, {:app, app, app_sub_dir})}}
{"env", {:ok, _pid}, _loaded} ->
{:ok, {0o0755, @attr_dir, 0}}
_ ->
:error
end
case attrs do
{:ok, attrs} -> {:ok, attrs, state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_getattr(state, {:app_env, app, opt}) do
case app_env(app, opt) do
{:ok, _val} -> {:ok, {0o0644, @attr_file, file_size(state, {:app_env, app, opt})}, state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_getattr(state, {:link_from, _pid}) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:link_to, _linked_pid}) do
{:ok, {0o0755, @attr_symlink, 0}, state}
end
def elixirfs_getattr(state, :code) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:code, :modules}) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:code, :module, _Module}) do
{:ok, {0o0755, @attr_dir, 0}, state}
end
def elixirfs_getattr(state, {:code, :module, module, :file}) do
{:ok, {0o0644, @attr_file, file_size(state, {:code, :module, module, :file})}, state}
end
@doc false
def elixirfs_readlink(state, {:local_name, name}) do
case Process.whereis(name) do
nil ->
{:error, @error_noent, state}
pid ->
dest =
state.mount_point <> "/pids/#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(pid))
{:ok, dest, state}
end
end
def elixirfs_readlink(state, {:global_name, name}) do
case global_name_pid(name) do
{:ok, pid} ->
dest =
state.mount_point <> "/pids/#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(pid))
{:ok, dest, state}
:error ->
{:error, @error_noent, state}
end
end
def elixirfs_readlink(state, {:app, app, "app_proc"}) do
case running_app_pid(app) do
{:ok, pid} when pid !== :undefined ->
dest =
state.mount_point <> "/pids/#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(pid))
{:ok, dest, state}
_ ->
{:error, @error_noent, state}
end
end
def elixirfs_readlink(state, {:app, app, "top_sup"}) do
case running_app_pid(app) do
{:ok, pid} when pid !== :undefined ->
{sup_pid, _mod} = :application_master.get_child(pid)
dest =
state.mount_point <>
"/pids/#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(sup_pid))
{:ok, dest, state}
_ ->
{:error, @error_noent, state}
end
end
def elixirfs_readlink(state, {:link_to, linked_pid}) do
dest =
state.mount_point <> "/pids/#PID" <> :erlang.list_to_binary(:erlang.pid_to_list(linked_pid))
{:ok, dest, state}
end
@doc false
def elixirfs_read(state, {:proc_info, pid, item_spec}) do
case safe_process_info(pid, item_spec) do
{:ok, {^item_spec, item_data}} ->
{:ok, proc_info_content(item_data), state}
:error ->
{:error, @error_noent, state}
end
end
def elixirfs_read(state, {:app, app, "descr"}) do
case loaded_app(app) do
{:ok, descr, _vsn} -> {:ok, "#{descr}\n", state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_read(state, {:app, app, "vsn"}) do
case loaded_app(app) do
{:ok, _descr, vsn} -> {:ok, "#{vsn}\n", state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_read(state, {:app_env, app, opt}) do
case app_env(app, opt) do
{:ok, val} -> {:ok, inspect(val, pretty: true) <> "\n", state}
:error -> {:error, @error_noent, state}
end
end
def elixirfs_read(state, {:code, :module, module, :file}) do
{:ok, "#{:code.which(module)}\n", state}
end
defp file_size(state, context) do
{:ok, content, _state} = elixirfs_read(state, context)
byte_size(content)
end
defp proc_info_content(item_data) do
inspect(item_data, pretty: true) <> "\n"
end
defp safe_process_info(pid, item_spec) do
case Process.info(pid, item_spec) do
nil -> :error
item -> {:ok, item}
end
rescue
ArgumentError -> :error
end
defp loaded_apps() do
:application_controller.info()[:loaded]
end
defp running_apps() do
:application_controller.info()[:running]
end
defp running_app_pid(app) do
case for {name, pid} when name == app <- running_apps(), do: pid do
[pid] -> {:ok, pid}
[] -> :error
end
end
defp loaded_app(app) do
case for {name, descr, vsn} when name == app <- loaded_apps(), do: {descr, vsn} do
[{descr, vsn}] -> {:ok, descr, vsn}
[] -> :error
end
end
defp app_env(app, opt) do
env = Application.get_all_env(app)
if Keyword.has_key?(env, opt) do
{:ok, Keyword.fetch!(env, opt)}
else
:error
end
end
defp global_name_pid(name) do
case :global.whereis_name(name) do
pid when is_pid(pid) -> {:ok, pid}
_ -> :error
end
end
end