Pure Erlang implementation of 9p2000 protocol
filesystem
fs
9p2000
erlang
9p
1% SPDX-FileCopyrightText: 2026 Łukasz Niemier <~@hauleth.dev>
2%
3% SPDX-License-Identifier: Apache-2.0
4
5-module(e9p_sysfs).
6
7-behaviour(e9p_fs).
8
9-include_lib("kernel/include/logger.hrl").
10
11-export([init/1,
12 root/3,
13 walk/4,
14 open/4,
15 create/6,
16 read/5,
17 write/5,
18 remove/3,
19 stat/3,
20 wstat/4]).
21
22init(_State) -> {ok, []}.
23
24root(_Uname, _Aname, State) ->
25 {ok, #{qid := QID}} = stat_for([]),
26 {ok, {QID, []}, State}.
27
28walk(_FID, Path, Name, State) ->
29 ?LOG_DEBUG(#{path => Path, name => Name}),
30 case stat_for(Path ++ [Name]) of
31 {ok, #{qid := QID}} ->
32 {{QID, []}, State};
33 {error, Reason} ->
34 {error, Reason, State}
35 end.
36
37open(_FID, [], _Mode, State) ->
38 {ok, {[~"applications", ~"processes", ~"system_info"], 0}, State};
39open(_FID, [~"system_info"], _Mode, State) ->
40 Keys = [
41 ~"allocated_areas",
42 % ~"allocator", %% TBD
43 ~"alloc_util_allocators",
44 % ~"allocator_sizes", %% TBD
45 ~"cpu_topology",
46 ~"logical_processors",
47 ~"logical_processors_available",
48 ~"logical_processors_online",
49 ~"cpu_quota",
50 ~"update_cpu_info",
51 ~"fullsweep_after",
52 ~"garbage_collection",
53 ~"heap_sizes",
54 ~"heap_type",
55 ~"max_heap_size",
56 ~"message_queue_data",
57 ~"min_heap_size",
58 ~"min_bin_vheap_size",
59 ~"procs",
60 ~"atom_count",
61 ~"atom_limit",
62 ~"ets_count",
63 ~"ets_limit",
64 ~"port_count",
65 ~"port_limit",
66 ~"process_count",
67 ~"process_limit",
68 ~"end_time",
69 ~"os_monotonic_time_source",
70 ~"os_system_time_source",
71 ~"start_time",
72 ~"time_correction",
73 ~"time_offset",
74 ~"time_warp_mode",
75 ~"tolerant_timeofday",
76 ~"dirty_cpu_schedulers",
77 ~"dirty_cpu_schedulers_online",
78 ~"dirty_io_schedulers",
79 ~"multi_scheduling",
80 ~"multi_scheduling_blockers",
81 ~"normal_multi_scheduling_blockers",
82 ~"scheduler_bind_type",
83 ~"scheduler_bindings",
84 % ~"scheduler_id", %% Intentionally omitted
85 ~"schedulers",
86 ~"schedulers_online",
87 ~"smp_support",
88 ~"threads",
89 ~"thread_pool_size",
90 ~"async_dist",
91 ~"creation",
92 ~"delayed_node_table_gc",
93 ~"dist",
94 ~"dist_buf_busy_limit",
95 ~"dist_ctrl",
96 ~"c_compiler_used",
97 ~"check_io",
98 ~"debug_compiled",
99 ~"driver_version",
100 ~"dynamic_trace",
101 ~"dynamic_trace_probes",
102 ~"emu_flavor",
103 ~"emu_type",
104 ~"halt_flush_timeout",
105 ~"info",
106 ~"kernel_poll",
107 ~"loaded",
108 ~"machine",
109 ~"modified_timing_level",
110 ~"nif_version",
111 ~"otp_release",
112 ~"outstanding_system_requests_limit",
113 ~"port_parallelism",
114 ~"system_architecture",
115 ~"system_logger",
116 ~"system_version",
117 ~"trace_control_word",
118 ~"version",
119 ~"wordsize"
120 ],
121 {ok, {Keys, 0}, State};
122open(_FID, [~"system_info", ~"wordsize"], _Mode, State) ->
123 {ok, {[~"internal", ~"external"], 0}, State};
124open(_FID, [~"system_info", ~"wordsize", TypeB], _Mode, State) ->
125 Type = binary_to_atom(TypeB),
126 Wordsize = erlang:system_info({wordsize, Type}),
127 Data = iolist_to_binary(io_lib:format("~B", [Wordsize])),
128 {ok, {Data, 0}, State};
129open(_FID, [~"system_info", KeyB], _Mode, State) ->
130 Key = binary_to_atom(KeyB),
131 Data = case erlang:system_info(Key) of
132 Val when is_binary(Val) -> Val;
133 Val ->
134 case io_lib:printable_list(Val) of
135 true -> unicode:characters_to_binary(Val);
136 false -> iolist_to_binary(io_lib:format("~p", [Val]))
137 end
138 end,
139 {ok, {Data, 0}, State};
140%% ===== Processes =====
141open(_FID, [~"processes"], _Mode, State) ->
142 Processes = lists:map(fun pid_to_list/1, erlang:processes()),
143 {ok, {Processes, 0}, State};
144open(_FID, [~"processes", _PID], _Mode, State) ->
145 Keys = [
146 ~"current_function",
147 ~"initial_call",
148 ~"status",
149 ~"message_queue_len",
150 ~"links",
151 ~"dictionary",
152 ~"trap_exit",
153 ~"error_handler",
154 ~"priority",
155 ~"group_leader",
156 ~"total_heap_size",
157 ~"heap_size",
158 ~"stack_size",
159 ~"reductions",
160 ~"garbage_collection"
161 ],
162 {ok, {Keys, 0}, State};
163open(_FID, [~"processes", PIDB, KeyB], _Mode, State) ->
164 PIDL = binary_to_list(PIDB),
165 PID = list_to_pid(PIDL),
166 Key = binary_to_existing_atom(KeyB),
167 case erlang:process_info(PID, Key) of
168 {Key, Val} ->
169 Data = iolist_to_binary(io_lib:format("~p", [Val])),
170 {ok, {Data, 0}, State};
171 [] -> {ok, {[], 0}, State};
172 undefined ->
173 {error, "No such file", State}
174 end;
175%% ===== Applications =====
176open(_FID, [~"applications"], _Mode, State) ->
177 AllApps = lists:map(fun({Name, _, _}) when is_atom(Name) -> erlang:atom_to_binary(Name) end,
178 application:loaded_applications()),
179 {ok, {AllApps, 0}, State};
180open(_FID, [~"applications", Name], _Mode, State) ->
181 Atom = binary_to_existing_atom(Name),
182 {ok, AppKeys} = application:get_all_key(Atom),
183 Keys = proplists:get_keys(AppKeys),
184 Files = lists:map(fun erlang:atom_to_binary/1, Keys),
185 {ok, {Files, 0}, State};
186open(_FID, [~"applications", Name, ~"env"], _Mode, State) ->
187 Atom = binary_to_existing_atom(Name),
188 AllEnv = application:get_all_env(Atom),
189 Data = iolist_to_binary(io_lib:format("%% coding: utf-8~n~n~p.", [AllEnv])),
190 {ok, {Data, 0}, State};
191open(_FID, [~"applications", NameB, KeyB], _Mode, State) ->
192 Name = binary_to_existing_atom(NameB),
193 Key = binary_to_existing_atom(KeyB),
194 {ok, Val} = application:get_key(Name, Key),
195 Data = iolist_to_binary(io_lib:format("~p", [Val])),
196 {ok, {Data, 0}, State}.
197
198create(_FID, _Path, _Name, _Perm, _Mode, State) ->
199 {error, "Not supported", State}.
200
201read({QID, Data}, Path, Offset, Length, State) ->
202 case e9p:is_type(QID, directory) of
203 true -> readdir(Data, Path, Offset, Length, State);
204 false -> readfile(Data, Path, Offset, Length, State)
205 end.
206
207readdir(Data, Path, Offset, Length, State) ->
208 Encoded = lists:map(fun(Entry) ->
209 {ok, Stat} = stat_for(Path ++ [Entry]),
210 e9p_msg:encode_stat(Stat)
211 end, Data),
212 Bin = iolist_to_binary(Encoded),
213 {ok, {Data, chunk(Bin, Offset, Length)}, State}.
214
215readfile(Data, _Path, Offset, Length, State) when is_integer(Length) ->
216 {ok, {Data, chunk(Data, Offset, Length)}, State}.
217
218chunk(Data, Offset, Length)
219 when is_binary(Data), is_integer(Offset), is_integer(Length) ->
220 if
221 Offset =< byte_size(Data) ->
222 Len = min(Length, byte_size(Data) - Offset),
223 binary_part(Data, Offset, Len);
224 true -> ~""
225 end.
226
227write(_FID, _Path, _Offset, _Data, State) ->
228 {error, "Unimplemented", State}.
229
230remove(_FID, _Path, State) ->
231 {error, "Unimplemented", State}.
232
233stat(_FID, Path, State) ->
234 case stat_for(Path) of
235 {ok, Stat} -> {ok, Stat, State};
236 {error, Reason} -> {error, Reason, State}
237 end.
238
239wstat(_FID, _Path, _Stat, State) ->
240 {error, "Not supported", State}.
241
242stat_for([]) ->
243 {ok, #{
244 qid => e9p:make_qid(directory, 0, 0),
245 name => ~"/",
246 mode => 8#555,
247 length => 0
248 }};
249stat_for([~"processes"]) ->
250 {ok, #{
251 qid => e9p:make_qid(directory, 0, 1),
252 name => ~"processes",
253 mode => 8#555,
254 length => 0
255 }};
256stat_for([~"processes", PID]) ->
257 Hash = erlang:phash2(PID, 16#FFFF),
258 <<Value:64>> = <<16#1, 0:24, Hash:32>>,
259 {ok, #{
260 qid => e9p:make_qid(directory, 0, Value),
261 name => PID,
262 mode => 8#555,
263 length => 0
264 }};
265stat_for([~"processes", PID, Key]) ->
266 Hash = erlang:phash2(PID, 16#FFFF),
267 KeyH = erlang:phash2(Key, 16#FFF),
268 <<Value:64>> = <<16#1, KeyH:24, Hash:32>>,
269 {ok, #{
270 qid => e9p:make_qid(regular, 0, Value),
271 name => Key,
272 mode => 8#555,
273 length => 0
274 }};
275stat_for([~"applications"]) ->
276 {ok, #{
277 qid => e9p:make_qid(directory, 0, 2),
278 name => ~"applications",
279 mode => 8#555,
280 length => 0
281 }};
282stat_for([~"applications", Name]) ->
283 Atom = binary_to_existing_atom(Name),
284 case application:get_key(Atom, vsn) of
285 undefined -> {error, "Not exist"};
286 {ok, _Vsn} ->
287 Hash = erlang:phash2(Name, 16#FFFF),
288 <<Value:64>> = <<16#2, 0:24, Hash:32>>,
289 {ok,
290 #{
291 qid => e9p:make_qid(directory, 0, Value),
292 name => Name,
293 mode => 8#555,
294 length => 0
295 }}
296 end;
297stat_for([~"applications", Name, Key]) ->
298 Hash = erlang:phash2(Name, 16#FFFF),
299 KeyH = erlang:phash2(Key, 16#FFF),
300 <<Value:64>> = <<16#2, KeyH:24, Hash:32>>,
301 {ok, #{
302 qid => e9p:make_qid(regular, 0, Value),
303 name => Key,
304 mode => 8#444,
305 length => 0
306 }};
307stat_for([~"system_info"]) ->
308 {ok, #{
309 qid => e9p:make_qid(directory, 0, 3),
310 name => ~"system_info",
311 mode => 8#555,
312 length => 0
313 }};
314stat_for([~"system_info", ~"wordsize"]) ->
315 Hash = erlang:phash2(~"wordsize", 16#FFFF),
316 <<Value:64>> = <<16#2, 0:24, Hash:32>>,
317 {ok, #{
318 qid => e9p:make_qid(directory, 0, Value),
319 name => ~"wordsize",
320 mode => 8#555,
321 length => 0
322 }};
323stat_for([~"system_info", ~"wordsize", ~"internal"]) ->
324 Hash = erlang:phash2(~"wordsize", 16#FFFF),
325 <<Value:64>> = <<16#2, 1:24, Hash:32>>,
326 {ok, #{
327 qid => e9p:make_qid(regular, 0, Value),
328 name => ~"internal",
329 mode => 8#444,
330 length => 0
331 }};
332stat_for([~"system_info", ~"wordsize", ~"external"]) ->
333 Hash = erlang:phash2(~"wordsize", 16#FFFF),
334 <<Value:64>> = <<16#2, 2:24, Hash:32>>,
335 {ok, #{
336 qid => e9p:make_qid(regular, 0, Value),
337 name => ~"external",
338 mode => 8#444,
339 length => 0
340 }};
341stat_for([~"system_info", Name]) ->
342 Hash = erlang:phash2(Name, 16#FFFF),
343 <<Value:64>> = <<16#2, 0:24, Hash:32>>,
344 {ok, #{
345 qid => e9p:make_qid(regular, 0, Value),
346 name => Name,
347 mode => 8#444,
348 length => 0
349 }};
350stat_for(_Path) ->
351 {error, "Not exist"}.