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_server).
6
7-include("e9p_internal.hrl").
8
9-include_lib("kernel/include/logger.hrl").
10
11-export([start/2,
12 start_link/2]).
13
14-export([setup_acceptor/3,
15 accept_loop/2,
16 loop/1
17 ]).
18
19-record(state, {
20 socket,
21 % trans_mod = gen_tcp,
22 % ver,
23 fids = #{},
24 handler
25 }).
26
27start(Port, Handler) ->
28 proc_lib:start(?MODULE, setup_acceptor, [self(), Port, Handler]).
29
30start_link(Port, Handler) ->
31 proc_lib:start_link(?MODULE, setup_acceptor, [self(), Port, Handler]).
32
33setup_acceptor(Parent, Port, Handler0) ->
34 maybe
35 {ok, LSock} ?= gen_tcp:listen(Port, [binary, {active, false}]),
36 {ok, Handler} ?= e9p_fs:init(Handler0),
37
38 proc_lib:init_ack(Parent, {ok, self()}),
39
40 ?MODULE:accept_loop(LSock, Handler)
41 else
42 {error, _} = Error ->
43 proc_lib:init_fail(Parent, Error)
44 end.
45
46accept_loop(LSock, Handler) ->
47 case gen_tcp:accept(LSock, 5000) of
48 {ok, Sock} ->
49 % TODO: Handle connected clients in separate process
50
51 State = #state{
52 socket = Sock,
53 handler = Handler
54 },
55 ok = ?MODULE:loop(State),
56 ?MODULE:accept_loop(LSock, Handler);
57 {error, timeout} ->
58 ?MODULE:accept_loop(LSock, Handler);
59 {error, closed} ->
60 ok
61 end.
62
63loop(#state{socket = Sock} = State) ->
64 case e9p_transport:read(Sock) of
65 {ok, Tag, Data} ->
66 ?LOG_DEBUG(#{message => Data, tag => Tag}),
67 try handle_message(Data, State#state.fids, State#state.handler) of
68 {ok, Reply, FIDs, Handler} ->
69 e9p_transport:send(Sock, Tag, Reply),
70 ?MODULE:loop(State#state{fids = FIDs, handler = Handler});
71 {error, Err, RHandler} ->
72 e9p_transport:send(Sock, Tag, #rerror{msg = Err}),
73 ?MODULE:loop(State#state{handler = RHandler})
74 catch
75 C:E:S ->
76 ?LOG_ERROR(#{
77 kind => C,
78 msg => E,
79 stacktrace => S
80 }),
81 e9p_transport:send(Sock, Tag, #rerror{msg = io_lib:format("Caught ~p: ~p", [C, E])}),
82 ?MODULE:loop(State)
83 end;
84 {error, closed} ->
85 ?LOG_INFO("Connection closed"),
86 ok
87 end.
88
89handle_message(#tversion{version = <<"9P2000", _/binary>>, max_packet_size = MPS}, FIDs, Handler) ->
90 % Currently only "basic" 9p2000 version is supported, without any extensions
91 % like `.u` or `.L`
92 {ok, #rversion{version = ~"9P2000", max_packet_size = MPS}, FIDs, Handler};
93
94handle_message(#tflush{}, FIDs, Handler) ->
95 % Currently there is no support for parallel messages, so this does simply
96 % nothing
97 {ok, #rflush{}, FIDs, Handler};
98
99handle_message(#tattach{fid = FID, uname = UName, aname = AName}, FIDs, Handler0) ->
100 maybe
101 {ok, QID, Handler} ?= e9p_fs:root(Handler0, UName, AName),
102 NFIDs = FIDs#{FID => QID},
103 {ok, #rattach{qid = QID#fid.qid}, NFIDs, Handler}
104 end;
105
106handle_message(#twalk{fid = FID, new_fid = NewFID, names = Paths}, FIDs, Handler0) ->
107 maybe
108 {ok, QID} ?= get_qid(FIDs, FID),
109 {ok, {NewQID, QIDs}, Handler} ?= e9p_fs:walk(Handler0, QID, Paths),
110 {ok, #rwalk{qids = QIDs}, FIDs#{NewFID => NewQID}, Handler}
111 end;
112
113handle_message(#topen{fid = FID, mode = Mode}, FIDs, Handler0) ->
114 maybe
115 {ok, QID} ?= get_qid(FIDs, FID),
116 {ok, {NewQID, IOUnit}, Handler} ?= e9p_fs:open(Handler0, QID, Mode),
117 {ok, #ropen{qid = QID#fid.qid, io_unit = IOUnit}, FIDs#{FID => NewQID}, Handler}
118 end;
119handle_message(#tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}, FIDs, Handler0) ->
120 maybe
121 {ok, QID} ?= get_qid(FIDs, FID),
122 {ok, {NewQID, IOUnit}, Handler} ?= e9p_fs:create(Handler0, QID, Name, Perm, Mode),
123 {ok, #rcreate{qid = NewQID#fid.qid, io_unit = IOUnit}, FIDs, Handler}
124 end;
125
126handle_message(#tread{fid = FID, offset = Offset, len = Len}, FIDs, Handler0) ->
127 maybe
128 {ok, QID} ?= get_qid(FIDs, FID),
129 {ok, {NQID, Data}, Handler} ?= e9p_fs:read(Handler0, QID, Offset, Len),
130 {ok, #rread{data = Data}, FIDs#{FID => NQID}, Handler}
131 end;
132handle_message(#twrite{fid = FID, offset = Offset, data = Data}, FIDs, Handler0) ->
133 maybe
134 {ok, QID} ?= get_qid(FIDs, FID),
135 {ok, {NQID, Len}, Handler} ?= e9p_fs:write(Handler0, QID, Offset, Data),
136 {ok, #rwrite{len = Len}, FIDs#{FID => NQID}, Handler}
137 end;
138
139handle_message(#tclunk{fid = FID}, FIDs, Handler0) ->
140 maybe
141 {ok, QID} ?= get_qid(FIDs, FID),
142 {ok, Handler} ?= e9p_fs:clunk(Handler0, QID),
143 NFIDs = maps:remove(FID, FIDs),
144 {ok, #rclunk{}, NFIDs, Handler}
145 end;
146
147handle_message(#tremove{fid = FID}, FIDs, Handler0) ->
148 maybe
149 {ok, QID} ?= get_qid(FIDs, FID),
150 {ok, Handler} ?= e9p_fs:remove(Handler0, QID),
151 {ok, #rremove{}, FIDs, Handler}
152 end;
153
154handle_message(#tstat{fid = FID}, FIDs, Handler0) ->
155 maybe
156 {ok, QID} ?= get_qid(FIDs, FID),
157 {ok, Stat, Handler} ?= e9p_fs:stat(Handler0, QID),
158 {ok, #rstat{stat = Stat}, FIDs, Handler}
159 end;
160
161handle_message(#twstat{fid = FID, stat = Stat}, FIDs, Handler0) ->
162 maybe
163 {ok, QID} ?= get_qid(FIDs, FID),
164 {ok, Handler} ?= e9p_fs:wstat(Handler0, QID, Stat),
165 {ok, #rwstat{}, FIDs, Handler}
166 end;
167
168handle_message(_Msg, _FIDs, Handler) ->
169 {error, ~"Unknown request type", Handler}.
170
171get_qid(FIDs, FID) ->
172 case FIDs of
173 #{FID := QID} -> {ok, QID};
174 _ -> {error, io_lib:fwrite(~"Unknown FID: ~B", [FID])}
175 end.