PageRenderTime 60ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/src/distfs_attendant.erl

https://gitlab.com/mstreet_fceia/distfs
Erlang | 220 lines | 114 code | 24 blank | 82 comment | 0 complexity | 79cd2044d09e1f5b277e7253ea913fb0 MD5 | raw file
  1. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  2. %%% Client attendant. %%%
  3. %%% %%%
  4. %%% This module is part of the distributed file system server. %%%
  5. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  6. %%% Copyright © 2014, 2015 Mariano Street. %%%
  7. %%% %%%
  8. %%% This file is part of DistFS. %%%
  9. %%% %%%
  10. %%% DistFS is free software: you can redistribute it and/or modify %%%
  11. %%% it under the terms of the GNU General Public License as published by %%%
  12. %%% the Free Software Foundation, either version 3 of the License, or %%%
  13. %%% (at your option) any later version. %%%
  14. %%% %%%
  15. %%% DistFS is distributed in the hope that it will be useful, %%%
  16. %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%%
  17. %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%%
  18. %%% GNU General Public License for more details. %%%
  19. %%% %%%
  20. %%% You should have received a copy of the GNU General Public License %%%
  21. %%% along with DistFS. If not, see <http://www.gnu.org/licenses/>. %%%
  22. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  23. -module(distfs_attendant).
  24. -behaviour(gen_server).
  25. -export([start_link/1]).
  26. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
  27. code_change/3]).
  28. -include("logger.hrl").
  29. %% The state of the server once a connection with a client has been
  30. %% established. The session information.
  31. %%
  32. -record(session, {% The connection socket.
  33. %
  34. % It allows the server to receive messages from the client
  35. % and send messages to it.
  36. socket :: port(),
  37. % The worker assigned to the client.
  38. %
  39. % It is the one in charge of processing all operations on
  40. % the file system requested by the client. In case of
  41. % operations that require output from every worker, this
  42. % one acts as the interface between the attendant and the
  43. % rest of workers, mandating them to do work as needed.
  44. worker :: pid(),
  45. % Buffer for storing received strings until a whole line is
  46. % available.
  47. buffer = "" :: string(),
  48. % How many data bytes to receive in a raw manner (i.e. not
  49. % to be interpreted as a command line).
  50. raw = 0 :: non_neg_integer()}).
  51. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  52. %% Types %%
  53. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  54. %% Private.
  55. -type session() :: #session{}.
  56. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  57. %% API function %%
  58. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  59. -spec start_link(port()) -> _.
  60. start_link(ListenSocket) ->
  61. gen_server:start_link(?MODULE, ListenSocket, []).
  62. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  63. %% Callback functions %%
  64. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  65. %% At first, the state of the server is simply the listening socket. This is
  66. %% what `init` returns (wrapped in a pair).
  67. %%
  68. -spec init(P) -> {ok, P} when P :: port().
  69. init(ListenSocket) ->
  70. % Because accepting a connection is a blocking operation, it cannot be
  71. % done here, before initialization is done. Instead, forward the task to
  72. % the server loop by sending itself an asynchronous message. After the
  73. % initialization is done and the loop is begun, that message will be
  74. % attended.
  75. gen_server:cast(self(), accept),
  76. {ok, ListenSocket}.
  77. -spec handle_call(_, distfs_otp:from(), S) ->
  78. distfs_otp:stop_unexpected(S) when S :: session().
  79. handle_call(Request, From, Session) ->
  80. ?ERROR("unexpected call from ~p: ~p", [From, Request]),
  81. {stop, unexpected_message, Session}.
  82. %% Once a connection with a client has been established, the state is no
  83. %% longer a socket only, but a session record. This record contains a socket
  84. %% among other things, but not a listening one.
  85. %%
  86. -spec handle_cast(accept, port()) -> {noreply, session()};
  87. (_, S) -> distfs_otp:stop_unexpected(S)
  88. when S :: session().
  89. handle_cast(accept, ListenSocket) ->
  90. {ok, Socket} = gen_tcp:accept(ListenSocket),
  91. % Log who is connected.
  92. ?INFO("connection established with: ~s",
  93. [distfs_utils:get_peer_string(Socket)]),
  94. Worker = distfs_workersup:get_random_worker(),
  95. Session = #session{socket = Socket, worker = Worker},
  96. {noreply, Session};
  97. handle_cast(Request, Session) ->
  98. ?ERROR("unexpected cast: ~p", [Request]),
  99. {stop, unexpected_message, Session}.
  100. -spec handle_info({tcp, inet:port_number(), _}, session()) ->
  101. {noreply, session()};
  102. ({tcp_closed, port()}, S) ->
  103. distfs_otp:stop_normal(S) when S :: session();
  104. ({tcp_error, port(), _}, S) ->
  105. distfs_otp:stop_normal(S) when S :: session();
  106. (_, S) ->
  107. distfs_otp:stop_unexpected(S) when S :: session().
  108. handle_info({tcp, _Port, Data}, Session0) ->
  109. ?INFO("received: ~p", [Data]),
  110. Session1 = case Session0#session.raw of
  111. 0 -> handle_line(Data, Session0);
  112. _ -> handle_raw(Data, Session0)
  113. end,
  114. inet:setopts(Session1#session.socket, [{active, once}]),
  115. {noreply, Session1};
  116. handle_info({tcp_closed, Socket}, Session = #session{socket = Socket}) ->
  117. ?INFO("connection closed by client", []),
  118. {stop, normal, Session};
  119. handle_info({tcp_error, Socket, Reason}, Session = {socket = Socket}) ->
  120. ?ERROR("connection closed because of an error: ~p", [Reason]),
  121. {stop, normal, Session};
  122. handle_info({Ref, Reply}, Session) when is_reference(Ref) ->
  123. ?ERROR("ignoring timeouted reply: ~p", [Reply]),
  124. {noreply, Session};
  125. handle_info(Info, Session) ->
  126. ?ERROR("unexpected info: ~p", [Info]),
  127. {stop, unexpected_message, Session}.
  128. -spec terminate(_, session()) -> _.
  129. terminate(normal, Session) ->
  130. distfs_worker:close_files(Session#session.worker),
  131. gen_tcp:close(Session#session.socket);
  132. terminate(Reason, Session) ->
  133. distfs_worker:close_files(Session#session.worker),
  134. ?ERROR("terminated abnormally because of: ~p", [Reason]).
  135. -spec code_change({down, string()}, _, _) -> {error, unsupported};
  136. (string(), session(), _) -> {error, unsupported}.
  137. code_change(_OldVsn, _Session, _Extra) ->
  138. ?ERROR("code change not supported", []),
  139. {error, unsupported}.
  140. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  141. %% Private functions %%
  142. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  143. %% Treat data as a command line chunk.
  144. %%
  145. handle_line(Data, Session0) ->
  146. %% TODO: is all this really needed?, can't we assume we are
  147. %% always getting whole command lines directly since after
  148. %% each of these, an answer is waited for?
  149. %% WARNING: this code does not solve the problem of getting
  150. %% multiple lines at once.
  151. Session1 = case distfs_parser:split(Data) of
  152. {[], _Data} ->
  153. NewBuffer = Session0#session.buffer ++ Data,
  154. Session0;
  155. {CurrentTail, NewBuffer} ->
  156. handle_full_line(Session0#session.buffer
  157. ++ CurrentTail, Session0)
  158. end,
  159. Session1#session{buffer = NewBuffer}.
  160. -spec handle_line(distfs_parser:commandline(), session()) -> session().
  161. handle_full_line(CommandLine, Session) ->
  162. Result = distfs_worker:execute_line(Session#session.worker, CommandLine),
  163. handle_result(Result, Session).
  164. %% Treat data in a raw manner.
  165. %%
  166. handle_raw(Data, Session0) ->
  167. try lists:split(Session0#session.raw, Data) of
  168. % All the expected raw data is now available. The remainder,
  169. % `NewBuffer`, is a command line.
  170. %% WARNING: this code does not solve the problem of getting
  171. %% multiple lines at once.
  172. {RawTail, NewBuffer} ->
  173. Session1 = handle_full_raw(Session0#session.buffer ++ RawTail,
  174. Session0),
  175. Session1#session{buffer = NewBuffer}
  176. catch
  177. % There are less than `Session0#session.raw` bytes in `Data`. This
  178. % means that not all the expected raw data has been received
  179. % yet.
  180. exit:badarg ->
  181. NewBuffer = Session0#session.buffer ++ Data,
  182. Session0#session{raw = Session0#session.raw - length(Data),
  183. buffer = NewBuffer}
  184. end.
  185. handle_full_raw(Data, Session) ->
  186. Result = distfs_worker:attend_raw(Session#session.worker, Data),
  187. handle_result(Result, Session#session{raw = 0}).
  188. -spec handle_result({raw, non_neg_integer()} | nonempty_string(),
  189. session()) -> session().
  190. handle_result({raw, Size}, Session) ->
  191. Session#session{raw = Size};
  192. handle_result(Answer, Session) ->
  193. % Send the result to the client.
  194. ?INFO("sending: ~p", [Answer]),
  195. gen_tcp:send(Session#session.socket, Answer),
  196. Session.