From 43fffc9b5f767b885200a78c5727004155e1f5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Niemier?= <~@hauleth.dev> Date: Thu, 22 Jan 2026 23:11:27 +0100 Subject: [PATCH] Implement sysfs implementation It is basic, read-only, implementation of ERTS system information filesystem. So far the implementation contains 3 directories: - `processes` with list of processes containing basic process information - `applications` with list of loaded applications as well as basic information about these - `system_info` with information about ERTS Future improvements should add `memory` and `statistics` paths as well and things like removing process directory as a way to kill process or pseudo-file that will allow sending messages to processes. --- src/e9p_fs.erl | 2 + src/e9p_server.erl | 8 +- src/e9p_sysfs.erl | 342 +++++++++++++++++++++++++++++++++++++++++++++ src/e9p_unfs.erl | 125 +++++++++-------- 4 files changed, 420 insertions(+), 57 deletions(-) create mode 100644 src/e9p_sysfs.erl diff --git a/src/e9p_fs.erl b/src/e9p_fs.erl index 0fef2e0..48c8f5d 100644 --- a/src/e9p_fs.erl +++ b/src/e9p_fs.erl @@ -124,6 +124,8 @@ walk({Mod, State0}, FID0, Paths) when is_atom(Mod) -> do_walk(_Mod, FID, [], State, Acc) -> {ok, {FID, lists:reverse(Acc)}, State}; +do_walk(_Mod, #fid{path = []}, [~".." | _Names], State, _Acc) -> + {error, "Cannot walt to root parent of root directory", State}; do_walk(Mod, #fid{qid = QID0, path = Path, state = FState0} = FID0, [P | Rest], State0, Acc) -> case e9p:is_type(QID0, directory) of true -> diff --git a/src/e9p_server.erl b/src/e9p_server.erl index 7b6f042..5608059 100644 --- a/src/e9p_server.erl +++ b/src/e9p_server.erl @@ -58,6 +58,7 @@ accept_loop(LSock, Handler) -> loop(#state{socket = Sock} = State) -> case e9p_transport:read(Sock) of {ok, Tag, Data} -> + ?LOG_DEBUG(#{message => Data, tag => Tag}), try handle_message(Data, State#state.fids, State#state.handler) of {ok, Reply, FIDs, Handler} -> e9p_transport:send(Sock, Tag, Reply), @@ -67,8 +68,13 @@ loop(#state{socket = Sock} = State) -> ?MODULE:loop(State#state{handler = RHandler}) catch C:E:S -> + ?LOG_ERROR(#{ + kind => C, + msg => E, + stacktrace => S + }), e9p_transport:send(Sock, Tag, #rerror{msg = io_lib:format("Caught ~p: ~p", [C, E])}), - erlang:raise(C, E, S) + ?MODULE:loop(State) end; {error, closed} -> ?LOG_INFO("Connection closed"), diff --git a/src/e9p_sysfs.erl b/src/e9p_sysfs.erl new file mode 100644 index 0000000..bdd6ccc --- /dev/null +++ b/src/e9p_sysfs.erl @@ -0,0 +1,342 @@ +-module(e9p_sysfs). + +-behaviour(e9p_fs). + +-include_lib("kernel/include/logger.hrl"). + +-export([init/1, + root/3, + walk/4, + open/4, + create/6, + read/5, + write/5, + remove/3, + stat/3, + wstat/4]). + +init(_State) -> {ok, []}. + +root(_Uname, _Aname, State) -> + {ok, #{qid := QID}} = stat_for([]), + {ok, {QID, []}, State}. + +walk(_FID, Path, Name, State) -> + ?LOG_DEBUG(#{path => Path, name => Name}), + case stat_for(Path ++ [Name]) of + {ok, #{qid := QID}} -> + {{QID, []}, State}; + {error, Reason} -> + {error, Reason, State} + end. + +open(_FID, [], _Mode, State) -> + {ok, {[~"applications", ~"processes", ~"system_info"], 0}, State}; +open(_FID, [~"system_info"], _Mode, State) -> + Keys = [ + ~"allocated_areas", + % ~"allocator", %% TBD + ~"alloc_util_allocators", + % ~"allocator_sizes", %% TBD + ~"cpu_topology", + ~"logocal_processors", + ~"logical_processors_available", + ~"logical_processors_online", + ~"cpu_quota", + ~"update_cpu_info", + ~"fullsweep_after", + ~"garbage_collection", + ~"heap_sizes", + ~"heap_type", + ~"max_heap_type", + ~"message_queue_data", + ~"min_heap_size", + ~"min_bin_vheap_size", + ~"procs", + ~"atom_count", + ~"atom_limit", + ~"ets_count", + ~"ets_limit", + ~"port_count", + ~"port_limit", + ~"process_count", + ~"process_limit", + ~"end_time", + ~"os_monotonic_time_source", + ~"os_time_source", + ~"start_time", + ~"time_correlation", + ~"time_offset", + ~"time_warp_mode", + ~"tolerant_timeofday", + ~"dirty_cpu_schedulers", + ~"dirty_cpu_schedulers_online", + ~"dirty_io_schedulers", + ~"dirty_io_schedulers_online", + ~"multi_scheduling", + ~"multi_scheduling_blockers", + ~"normal_multi_scheduling_blockers", + ~"scheduler_bind_type", + ~"scheduler_bindings", + % ~"scheduler_id", %% Intentionally omitted + ~"schedulers", + ~"schedulers_online", + ~"smp_support", + ~"threads", + ~"thread_pool_size", + ~"async_dist", + ~"creation", + ~"delayed_node_table_gc", + ~"dist", + ~"dist_buf_busy_limit", + ~"dist_ctrl", + ~"c_compiler_used", + ~"check_io", + ~"debug_compiled", + ~"driver_version", + ~"dynamic_trace", + ~"dynamic_trace_probes", + ~"emu_flavor", + ~"emu_type", + ~"halt_flush_timeout", + ~"info", + ~"kernel_poll", + ~"loaded", + ~"machine", + ~"modified_timing_level", + ~"nif_version", + ~"otp_release", + ~"outstanding_system_requests_limit", + ~"port_parallelism", + ~"system_architecture", + ~"system_logger", + ~"system_version", + ~"trace_control_word", + ~"version", + ~"wordsize" + ], + {ok, {Keys, 0}, State}; +open(_FID, [~"system_info", ~"wordsize"], _Mode, State) -> + {ok, {[~"internal", ~"external"], 0}, State}; +open(_FID, [~"system_info", ~"wordsize", TypeB], _Mode, State) -> + Type = binary_to_atom(TypeB), + Wordsize = erlang:system_info({wordsize, Type}), + Data = iolist_to_binary(io_lib:format("~B", [Wordsize])), + {ok, {Data, 0}, State}; +open(_FID, [~"system_info", KeyB], _Mode, State) -> + Key = binary_to_atom(KeyB), + Val = erlang:system_info(Key), + Data = iolist_to_binary(io_lib:format("~p", [Val])), + {ok, {Data, 0}, State}; +%% ===== Processes ===== +open(_FID, [~"processes"], _Mode, State) -> + Processes = lists:map(fun pid_to_list/1, erlang:processes()), + {ok, {Processes, 0}, State}; +open(_FID, [~"processes", _PID], _Mode, State) -> + Keys = [ + ~"current_function", + ~"initial_call", + ~"status", + ~"message_queue_len", + ~"links", + ~"dictionary", + ~"trap_exit", + ~"error_handler", + ~"priority", + ~"group_leader", + ~"total_heap_size", + ~"heap_size", + ~"stack_size", + ~"reductions", + ~"garbage_collection" + ], + {ok, {Keys, 0}, State}; +open(_FID, [~"processes", PIDB, KeyB], _Mode, State) -> + PIDL = binary_to_list(PIDB), + PID = list_to_pid(PIDL), + Key = binary_to_existing_atom(KeyB), + case erlang:process_info(PID, Key) of + {Key, Val} -> + Data = iolist_to_binary(io_lib:format("~p", [Val])), + {ok, {Data, 0}, State}; + [] -> {ok, {[], 0}, State}; + undefined -> + {error, "No such file", State} + end; +%% ===== Applications ===== +open(_FID, [~"applications"], _Mode, State) -> + AllApps = lists:map(fun({Name, _, _}) when is_atom(Name) -> erlang:atom_to_binary(Name) end, + application:loaded_applications()), + {ok, {AllApps, 0}, State}; +open(_FID, [~"applications", Name], _Mode, State) -> + Atom = binary_to_existing_atom(Name), + {ok, AppKeys} = application:get_all_key(Atom), + Keys = proplists:get_keys(AppKeys), + Files = lists:map(fun erlang:atom_to_binary/1, Keys), + {ok, {Files, 0}, State}; +open(_FID, [~"applications", Name, ~"env"], _Mode, State) -> + Atom = binary_to_existing_atom(Name), + AllEnv = application:get_all_env(Atom), + Data = iolist_to_binary(io_lib:format("~p", [AllEnv])), + {ok, {Data, 0}, State}; +open(_FID, [~"applications", NameB, KeyB], _Mode, State) -> + Name = binary_to_existing_atom(NameB), + Key = binary_to_existing_atom(KeyB), + {ok, Val} = application:get_key(Name, Key), + Data = iolist_to_binary(io_lib:format("~p", [Val])), + {ok, {Data, 0}, State}. + +create(_FID, _Path, _Name, _Perm, _Mode, State) -> + {error, "Not supported", State}. + +read({QID, Data}, Path, Offset, Length, State) -> + case e9p:is_type(QID, directory) of + true -> readdir(Data, Path, Offset, Length, State); + false -> readfile(Data, Path, Offset, Length, State) + end. + +readdir(Data, Path, Offset, Length, State) -> + Encoded = lists:map(fun(Entry) -> + {ok, Stat} = stat_for(Path ++ [Entry]), + e9p_msg:encode_stat(Stat) + end, Data), + Bin = iolist_to_binary(Encoded), + {ok, {Data, chunk(Bin, Offset, Length)}, State}. + +readfile(Data, _Path, Offset, Length, State) when is_integer(Length) -> + {ok, {Data, chunk(Data, Offset, Length)}, State}. + +chunk(Data, Offset, Length) + when is_binary(Data), is_integer(Offset), is_integer(Length) -> + if + Offset =< byte_size(Data) -> + Len = min(Length, byte_size(Data) - Offset), + binary_part(Data, Offset, Len); + true -> ~"" + end. + +write(_FID, _Path, _Offset, _Data, State) -> + {error, "Unimplemented", State}. + +remove(_FID, _Path, State) -> + {error, "Unimplemented", State}. + +stat(_FID, Path, State) -> + case stat_for(Path) of + {ok, Stat} -> {ok, Stat, State}; + {error, Reason} -> {error, Reason, State} + end. + +wstat(_FID, _Path, _Stat, State) -> + {error, "Not supported", State}. + +stat_for([]) -> + {ok, #{ + qid => e9p:make_qid(directory, 0, 0), + name => ~"/", + mode => 8#555, + length => 0 + }}; +stat_for([~"processes"]) -> + {ok, #{ + qid => e9p:make_qid(directory, 0, 1), + name => ~"processes", + mode => 8#555, + length => 0 + }}; +stat_for([~"processes", PID]) -> + Hash = erlang:phash2(PID, 16#FFFF), + <> = <<16#1, 0:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(directory, 0, Value), + name => PID, + mode => 8#555, + length => 0 + }}; +stat_for([~"processes", PID, Key]) -> + Hash = erlang:phash2(PID, 16#FFFF), + KeyH = erlang:phash2(Key, 16#FFF), + <> = <<16#1, KeyH:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(regular, 0, Value), + name => Key, + mode => 8#555, + length => 0 + }}; +stat_for([~"applications"]) -> + {ok, #{ + qid => e9p:make_qid(directory, 0, 2), + name => ~"applications", + mode => 8#555, + length => 0 + }}; +stat_for([~"applications", Name]) -> + Atom = binary_to_existing_atom(Name), + case application:get_key(Atom, vsn) of + undefined -> {error, "Not exist"}; + {ok, _Vsn} -> + Hash = erlang:phash2(Name, 16#FFFF), + <> = <<16#2, 0:24, Hash:32>>, + {ok, + #{ + qid => e9p:make_qid(directory, 0, Value), + name => Name, + mode => 8#555, + length => 0 + }} + end; +stat_for([~"applications", Name, Key]) -> + Hash = erlang:phash2(Name, 16#FFFF), + KeyH = erlang:phash2(Key, 16#FFF), + <> = <<16#2, KeyH:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(regular, 0, Value), + name => Key, + mode => 8#444, + length => 0 + }}; +stat_for([~"system_info"]) -> + {ok, #{ + qid => e9p:make_qid(directory, 0, 3), + name => ~"system_info", + mode => 8#555, + length => 0 + }}; +stat_for([~"system_info", ~"wordsize"]) -> + Hash = erlang:phash2(~"wordsize", 16#FFFF), + <> = <<16#2, 0:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(directory, 0, Value), + name => ~"wordsize", + mode => 8#555, + length => 0 + }}; +stat_for([~"system_info", ~"wordsize", ~"internal"]) -> + Hash = erlang:phash2(~"wordsize", 16#FFFF), + <> = <<16#2, 1:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(regular, 0, Value), + name => ~"internal", + mode => 8#444, + length => 0 + }}; +stat_for([~"system_info", ~"wordsize", ~"external"]) -> + Hash = erlang:phash2(~"wordsize", 16#FFFF), + <> = <<16#2, 2:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(regular, 0, Value), + name => ~"external", + mode => 8#444, + length => 0 + }}; +stat_for([~"system_info", Name]) -> + Hash = erlang:phash2(Name, 16#FFFF), + <> = <<16#2, 0:24, Hash:32>>, + {ok, #{ + qid => e9p:make_qid(regular, 0, Value), + name => Name, + mode => 8#444, + length => 0 + }}; +stat_for(_Path) -> + {error, "Not exist"}. diff --git a/src/e9p_unfs.erl b/src/e9p_unfs.erl index 44d97cb..da21019 100644 --- a/src/e9p_unfs.erl +++ b/src/e9p_unfs.erl @@ -4,6 +4,10 @@ -module(e9p_unfs). +-moduledoc """ +Expose Unix Filesystem as 9p2000 mount +""". + -behaviour(e9p_fs). -include_lib("kernel/include/logger.hrl"). @@ -12,9 +16,7 @@ -export([init/1, root/3, walk/4, stat/3, open/4, read/5, clunk/2, create/6, write/5, remove/3, wstat/4]). --doc """ -Create QID and Stat data for given path. -""". +% Create QID and Stat data for given path. qid(Root, Path) -> FullPath = filename:join([Root] ++ Path), case file:read_file_info(FullPath, [{time, posix}]) of @@ -25,9 +27,47 @@ qid(Root, Path) -> {error, _} = Error -> Error end. +file_info_to_stat( + Path, + QID, + #file_info{ + size = Len, + atime = Atime, + mtime = Mtime, + mode = Mode + }) -> + Name = if + Path == [] -> ~"/"; + true -> lists:last(Path) + end, + #{ + qid => QID, + mode => Mode, + atime => Atime, + mtime => Mtime, + length => Len, + name => Name + }. + +stat_to_file_info(Stat) -> + #{ + mode := Mode, + atime := Atime, + mtime := Mtime + } = Stat, + #file_info{ + mode = Mode, + atime = Atime, + mtime = Mtime + }. + +%% ====== Filesystem handlers ====== + +-doc false. init(#{path := Path}) -> {ok, #{root => unicode:characters_to_binary(Path)}}. +-doc false. root(UName, AName, #{root := Root} = State) -> ?LOG_INFO(#{uname => UName, aname => AName}), maybe @@ -35,8 +75,7 @@ root(UName, AName, #{root := Root} = State) -> {ok, {Qid, []}, State} end. -walk(_QID, [], ~"..", State) -> - {false, State}; +-doc false. walk(_QID, Path, ~"..", #{root := Root} = State) -> case qid(Root, lists:droplast(Path)) of {ok, NQid, _Stat} -> {{NQid, []}, State}; @@ -48,6 +87,7 @@ walk(_QID, Path, File, #{root := Root} = State) -> {error, _} -> {false, State} end. +-doc false. stat({QID, _}, Path, #{root := Root} = State) -> FullPath = filename:join([Root] ++ Path), case file:read_file_info(FullPath, [{time, posix}]) of @@ -58,6 +98,19 @@ stat({QID, _}, Path, #{root := Root} = State) -> {error, Error, State} end. +-doc false. +wstat(_QID, Path, Stat, #{root := Root} = State) -> + FileInfo = stat_to_file_info(Stat), + FullPath = filename:join([Root] ++ Path), + + case file:write_file_info(FullPath, FileInfo, [{time, posix}]) of + ok -> {ok, State}; + {error, Reason} -> + {error, io_lib:format("Couldn't write file stat: ~p", [Reason]), + State} + end. + +-doc false. open({QID, []}, Path, Mode, #{root := Root} = State) -> FullPath = filename:join([Root] ++ Path), QS = case e9p:is_type(QID, directory) of @@ -72,9 +125,18 @@ open({QID, []}, Path, Mode, #{root := Root} = State) -> end, {ok, {QS, 0}, State}. +-doc false. +clunk({_, {regular, FD}}, State) -> + ok = file:close(FD), + {ok, State}; +clunk(_QID, State) -> + {ok, State}. + +-doc false. create(_QID, _Path, _Name, _Perm, _Mode, State) -> {error, "Unsupported", State}. +-doc false. remove({QID, _} = FID, Path, #{root := Root} = State0) -> FullPath = filename:join([Root] ++ Path), {ok, State} = clunk(FID, State0), @@ -95,6 +157,7 @@ translate_mode([write]) -> {false, [read, write]}; translate_mode([append]) -> {false, [read, write]}; translate_mode([exec]) -> {false, [read]}. +-doc false. read({_QID, {regular, FD}}, _Path, Offset, Len, State) -> case file:pread(FD, Offset, Len) of {ok, Data} -> {ok, {{regular, FD}, Data}, State}; @@ -116,59 +179,9 @@ readdir(Root, Path, [Next | Rest], Len, Acc) -> true -> readdir(Root, Path, Rest, Len - Size, [Encoded | Acc]) end. +-doc false. write({_QID, {regular, FD}}, _Path, Offset, Data, State) -> case file:pwrite(FD, Offset, Data) of ok -> {ok, {{regular, FD}, iolist_size(Data)}, State}; {error, Err} -> {error, io_lib:format("Write error ~p", [Err]), State} end. - -clunk({_, {regular, FD}}, State) -> - ok = file:close(FD), - {ok, State}; -clunk(_QID, State) -> - {ok, State}. - -wstat(_QID, Path, Stat, #{root := Root} = State) -> - FileInfo = stat_to_file_info(Stat), - FullPath = filename:join([Root] ++ Path), - - case file:write_file_info(FullPath, FileInfo, [{time, posix}]) of - ok -> {ok, State}; - {error, Reason} -> - {error, io_lib:format("Couldn't write file stat: ~p", [Reason]), - State} - end. - -file_info_to_stat( - Path, - QID, - #file_info{ - size = Len, - atime = Atime, - mtime = Mtime, - mode = Mode - }) -> - Name = if - Path == [] -> ~"/"; - true -> lists:last(Path) - end, - #{ - qid => QID, - mode => Mode, - atime => Atime, - mtime => Mtime, - length => Len, - name => Name - }. - -stat_to_file_info(Stat) -> - #{ - mode := Mode, - atime := Atime, - mtime := Mtime - } = Stat, - #file_info{ - mode = Mode, - atime = Atime, - mtime = Mtime - }. -- 2.51.2