% SPDX-FileCopyrightText: 2026 Ɓukasz Niemier <~@hauleth.dev> % % SPDX-License-Identifier: Apache-2.0 -module(e9p_server). -include("e9p_internal.hrl"). -include_lib("kernel/include/logger.hrl"). -export([start/2, start_link/2]). -export([setup_acceptor/3, accept_loop/2, loop/1 ]). -record(state, { socket, % trans_mod = gen_tcp, % ver, fids = #{}, handler }). start(Port, Handler) -> proc_lib:start(?MODULE, setup_acceptor, [self(), Port, Handler]). start_link(Port, Handler) -> proc_lib:start_link(?MODULE, setup_acceptor, [self(), Port, Handler]). setup_acceptor(Parent, Port, Handler0) -> maybe {ok, LSock} ?= gen_tcp:listen(Port, [binary, {active, false}]), {ok, Handler} ?= e9p_fs:init(Handler0), proc_lib:init_ack(Parent, {ok, self()}), ?MODULE:accept_loop(LSock, Handler) else {error, _} = Error -> proc_lib:init_fail(Parent, Error) end. accept_loop(LSock, Handler) -> case gen_tcp:accept(LSock, 5000) of {ok, Sock} -> % TODO: Handle connected clients in separate process State = #state{ socket = Sock, handler = Handler }, ok = ?MODULE:loop(State), ?MODULE:accept_loop(LSock, Handler); {error, timeout} -> ?MODULE:accept_loop(LSock, Handler); {error, closed} -> ok end. 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), ?MODULE:loop(State#state{fids = FIDs, handler = Handler}); {error, Err, RHandler} -> e9p_transport:send(Sock, Tag, #rerror{msg = Err}), ?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])}), ?MODULE:loop(State) end; {error, closed} -> ?LOG_INFO("Connection closed"), ok end. handle_message(#tversion{version = <<"9P2000", _/binary>>, max_packet_size = MPS}, FIDs, Handler) -> % Currently only "basic" 9p2000 version is supported, without any extensions % like `.u` or `.L` {ok, #rversion{version = ~"9P2000", max_packet_size = MPS}, FIDs, Handler}; handle_message(#tflush{}, FIDs, Handler) -> % Currently there is no support for parallel messages, so this does simply % nothing {ok, #rflush{}, FIDs, Handler}; handle_message(#tattach{fid = FID, uname = UName, aname = AName}, FIDs, Handler0) -> maybe {ok, QID, Handler} ?= e9p_fs:root(Handler0, UName, AName), NFIDs = FIDs#{FID => QID}, {ok, #rattach{qid = QID#fid.qid}, NFIDs, Handler} end; handle_message(#twalk{fid = FID, new_fid = NewFID, names = Paths}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, {NewQID, QIDs}, Handler} ?= e9p_fs:walk(Handler0, QID, Paths), {ok, #rwalk{qids = QIDs}, FIDs#{NewFID => NewQID}, Handler} end; handle_message(#topen{fid = FID, mode = Mode}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, {NewQID, IOUnit}, Handler} ?= e9p_fs:open(Handler0, QID, Mode), {ok, #ropen{qid = QID#fid.qid, io_unit = IOUnit}, FIDs#{FID => NewQID}, Handler} end; handle_message(#tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, {NewQID, IOUnit}, Handler} ?= e9p_fs:create(Handler0, QID, Name, Perm, Mode), {ok, #rcreate{qid = NewQID#fid.qid, io_unit = IOUnit}, FIDs, Handler} end; handle_message(#tread{fid = FID, offset = Offset, len = Len}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, {NQID, Data}, Handler} ?= e9p_fs:read(Handler0, QID, Offset, Len), {ok, #rread{data = Data}, FIDs#{FID => NQID}, Handler} end; handle_message(#twrite{fid = FID, offset = Offset, data = Data}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, {NQID, Len}, Handler} ?= e9p_fs:write(Handler0, QID, Offset, Data), {ok, #rwrite{len = Len}, FIDs#{FID => NQID}, Handler} end; handle_message(#tclunk{fid = FID}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, Handler} ?= e9p_fs:clunk(Handler0, QID), NFIDs = maps:remove(FID, FIDs), {ok, #rclunk{}, NFIDs, Handler} end; handle_message(#tremove{fid = FID}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, Handler} ?= e9p_fs:remove(Handler0, QID), {ok, #rremove{}, FIDs, Handler} end; handle_message(#tstat{fid = FID}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, Stat, Handler} ?= e9p_fs:stat(Handler0, QID), {ok, #rstat{stat = Stat}, FIDs, Handler} end; handle_message(#twstat{fid = FID, stat = Stat}, FIDs, Handler0) -> maybe {ok, QID} ?= get_qid(FIDs, FID), {ok, Handler} ?= e9p_fs:wstat(Handler0, QID, Stat), {ok, #rwstat{}, FIDs, Handler} end; handle_message(_Msg, _FIDs, Handler) -> {error, ~"Unknown request type", Handler}. get_qid(FIDs, FID) -> case FIDs of #{FID := QID} -> {ok, QID}; _ -> {error, io_lib:fwrite(~"Unknown FID: ~B", [FID])} end.