/src/distfs_attendant.erl
Erlang | 220 lines | 114 code | 24 blank | 82 comment | 0 complexity | 79cd2044d09e1f5b277e7253ea913fb0 MD5 | raw file
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %%% Client attendant. %%%
- %%% %%%
- %%% This module is part of the distributed file system server. %%%
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %%% Copyright © 2014, 2015 Mariano Street. %%%
- %%% %%%
- %%% This file is part of DistFS. %%%
- %%% %%%
- %%% DistFS is free software: you can redistribute it and/or modify %%%
- %%% it under the terms of the GNU General Public License as published by %%%
- %%% the Free Software Foundation, either version 3 of the License, or %%%
- %%% (at your option) any later version. %%%
- %%% %%%
- %%% DistFS is distributed in the hope that it will be useful, %%%
- %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%%
- %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%%
- %%% GNU General Public License for more details. %%%
- %%% %%%
- %%% You should have received a copy of the GNU General Public License %%%
- %%% along with DistFS. If not, see <http://www.gnu.org/licenses/>. %%%
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- -module(distfs_attendant).
- -behaviour(gen_server).
- -export([start_link/1]).
- -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
- code_change/3]).
- -include("logger.hrl").
- %% The state of the server once a connection with a client has been
- %% established. The session information.
- %%
- -record(session, {% The connection socket.
- %
- % It allows the server to receive messages from the client
- % and send messages to it.
- socket :: port(),
- % The worker assigned to the client.
- %
- % It is the one in charge of processing all operations on
- % the file system requested by the client. In case of
- % operations that require output from every worker, this
- % one acts as the interface between the attendant and the
- % rest of workers, mandating them to do work as needed.
- worker :: pid(),
- % Buffer for storing received strings until a whole line is
- % available.
- buffer = "" :: string(),
- % How many data bytes to receive in a raw manner (i.e. not
- % to be interpreted as a command line).
- raw = 0 :: non_neg_integer()}).
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% Types %%
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% Private.
- -type session() :: #session{}.
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% API function %%
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- -spec start_link(port()) -> _.
- start_link(ListenSocket) ->
- gen_server:start_link(?MODULE, ListenSocket, []).
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% Callback functions %%
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% At first, the state of the server is simply the listening socket. This is
- %% what `init` returns (wrapped in a pair).
- %%
- -spec init(P) -> {ok, P} when P :: port().
- init(ListenSocket) ->
- % Because accepting a connection is a blocking operation, it cannot be
- % done here, before initialization is done. Instead, forward the task to
- % the server loop by sending itself an asynchronous message. After the
- % initialization is done and the loop is begun, that message will be
- % attended.
- gen_server:cast(self(), accept),
- {ok, ListenSocket}.
- -spec handle_call(_, distfs_otp:from(), S) ->
- distfs_otp:stop_unexpected(S) when S :: session().
- handle_call(Request, From, Session) ->
- ?ERROR("unexpected call from ~p: ~p", [From, Request]),
- {stop, unexpected_message, Session}.
- %% Once a connection with a client has been established, the state is no
- %% longer a socket only, but a session record. This record contains a socket
- %% among other things, but not a listening one.
- %%
- -spec handle_cast(accept, port()) -> {noreply, session()};
- (_, S) -> distfs_otp:stop_unexpected(S)
- when S :: session().
- handle_cast(accept, ListenSocket) ->
- {ok, Socket} = gen_tcp:accept(ListenSocket),
- % Log who is connected.
- ?INFO("connection established with: ~s",
- [distfs_utils:get_peer_string(Socket)]),
- Worker = distfs_workersup:get_random_worker(),
- Session = #session{socket = Socket, worker = Worker},
- {noreply, Session};
- handle_cast(Request, Session) ->
- ?ERROR("unexpected cast: ~p", [Request]),
- {stop, unexpected_message, Session}.
- -spec handle_info({tcp, inet:port_number(), _}, session()) ->
- {noreply, session()};
- ({tcp_closed, port()}, S) ->
- distfs_otp:stop_normal(S) when S :: session();
- ({tcp_error, port(), _}, S) ->
- distfs_otp:stop_normal(S) when S :: session();
- (_, S) ->
- distfs_otp:stop_unexpected(S) when S :: session().
- handle_info({tcp, _Port, Data}, Session0) ->
- ?INFO("received: ~p", [Data]),
- Session1 = case Session0#session.raw of
- 0 -> handle_line(Data, Session0);
- _ -> handle_raw(Data, Session0)
- end,
- inet:setopts(Session1#session.socket, [{active, once}]),
- {noreply, Session1};
- handle_info({tcp_closed, Socket}, Session = #session{socket = Socket}) ->
- ?INFO("connection closed by client", []),
- {stop, normal, Session};
- handle_info({tcp_error, Socket, Reason}, Session = {socket = Socket}) ->
- ?ERROR("connection closed because of an error: ~p", [Reason]),
- {stop, normal, Session};
- handle_info({Ref, Reply}, Session) when is_reference(Ref) ->
- ?ERROR("ignoring timeouted reply: ~p", [Reply]),
- {noreply, Session};
- handle_info(Info, Session) ->
- ?ERROR("unexpected info: ~p", [Info]),
- {stop, unexpected_message, Session}.
- -spec terminate(_, session()) -> _.
- terminate(normal, Session) ->
- distfs_worker:close_files(Session#session.worker),
- gen_tcp:close(Session#session.socket);
- terminate(Reason, Session) ->
- distfs_worker:close_files(Session#session.worker),
- ?ERROR("terminated abnormally because of: ~p", [Reason]).
- -spec code_change({down, string()}, _, _) -> {error, unsupported};
- (string(), session(), _) -> {error, unsupported}.
- code_change(_OldVsn, _Session, _Extra) ->
- ?ERROR("code change not supported", []),
- {error, unsupported}.
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% Private functions %%
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
- %% Treat data as a command line chunk.
- %%
- handle_line(Data, Session0) ->
- %% TODO: is all this really needed?, can't we assume we are
- %% always getting whole command lines directly since after
- %% each of these, an answer is waited for?
- %% WARNING: this code does not solve the problem of getting
- %% multiple lines at once.
- Session1 = case distfs_parser:split(Data) of
- {[], _Data} ->
- NewBuffer = Session0#session.buffer ++ Data,
- Session0;
- {CurrentTail, NewBuffer} ->
- handle_full_line(Session0#session.buffer
- ++ CurrentTail, Session0)
- end,
- Session1#session{buffer = NewBuffer}.
- -spec handle_line(distfs_parser:commandline(), session()) -> session().
- handle_full_line(CommandLine, Session) ->
- Result = distfs_worker:execute_line(Session#session.worker, CommandLine),
- handle_result(Result, Session).
- %% Treat data in a raw manner.
- %%
- handle_raw(Data, Session0) ->
- try lists:split(Session0#session.raw, Data) of
- % All the expected raw data is now available. The remainder,
- % `NewBuffer`, is a command line.
- %% WARNING: this code does not solve the problem of getting
- %% multiple lines at once.
- {RawTail, NewBuffer} ->
- Session1 = handle_full_raw(Session0#session.buffer ++ RawTail,
- Session0),
- Session1#session{buffer = NewBuffer}
- catch
- % There are less than `Session0#session.raw` bytes in `Data`. This
- % means that not all the expected raw data has been received
- % yet.
- exit:badarg ->
- NewBuffer = Session0#session.buffer ++ Data,
- Session0#session{raw = Session0#session.raw - length(Data),
- buffer = NewBuffer}
- end.
- handle_full_raw(Data, Session) ->
- Result = distfs_worker:attend_raw(Session#session.worker, Data),
- handle_result(Result, Session#session{raw = 0}).
- -spec handle_result({raw, non_neg_integer()} | nonempty_string(),
- session()) -> session().
- handle_result({raw, Size}, Session) ->
- Session#session{raw = Size};
- handle_result(Answer, Session) ->
- % Send the result to the client.
- ?INFO("sending: ~p", [Answer]),
- gen_tcp:send(Session#session.socket, Answer),
- Session.