defmodule CljElixir.NRepl.Server do @moduledoc """ nREPL TCP server for CljElixir. Listens on a configurable port and handles nREPL protocol messages over TCP using Bencode encoding. """ use GenServer require Logger alias CljElixir.NRepl.{Bencode, Handler, SessionManager} defstruct [:listen_socket, :port, :session_manager] def start_link(opts \\ []) do port = Keyword.get(opts, :port, 0) GenServer.start_link(__MODULE__, port, opts) end def port(server) do GenServer.call(server, :port) end # --- Callbacks --- @impl true def init(port) do {:ok, manager} = SessionManager.start_link() # Open TCP listener opts = [:binary, packet: :raw, active: false, reuseaddr: true] case :gen_tcp.listen(port, opts) do {:ok, listen_socket} -> # Get actual port (important when port=0) {:ok, actual_port} = :inet.port(listen_socket) state = %__MODULE__{ listen_socket: listen_socket, port: actual_port, session_manager: manager } # Start accepting connections in a separate process spawn_link(fn -> accept_loop(listen_socket, manager) end) Logger.info("nREPL server started on port #{actual_port}") {:ok, state} {:error, reason} -> {:stop, reason} end end @impl true def handle_call(:port, _from, state) do {:reply, state.port, state} end @impl true def terminate(_reason, state) do if state.listen_socket do :gen_tcp.close(state.listen_socket) end :ok end # --- Connection handling --- defp accept_loop(listen_socket, manager) do case :gen_tcp.accept(listen_socket) do {:ok, client_socket} -> spawn(fn -> handle_connection(client_socket, manager) end) accept_loop(listen_socket, manager) {:error, :closed} -> :ok {:error, reason} -> Logger.error("Accept error: #{inspect(reason)}") :ok end end defp handle_connection(socket, manager) do case read_message(socket, "") do {:ok, msg, rest} -> responses = Handler.handle(msg, manager) Enum.each(responses, fn response -> encoded = Bencode.encode(response) :gen_tcp.send(socket, encoded) end) handle_connection_with_buffer(socket, manager, rest) {:error, _reason} -> :gen_tcp.close(socket) end end defp handle_connection_with_buffer(socket, manager, buffer) do case try_decode(buffer) do {:ok, msg, rest} -> responses = Handler.handle(msg, manager) Enum.each(responses, fn response -> encoded = Bencode.encode(response) :gen_tcp.send(socket, encoded) end) handle_connection_with_buffer(socket, manager, rest) :incomplete -> case :gen_tcp.recv(socket, 0) do {:ok, data} -> handle_connection_with_buffer(socket, manager, buffer <> data) {:error, _} -> :gen_tcp.close(socket) end end end defp read_message(socket, buffer) do case try_decode(buffer) do {:ok, msg, rest} -> {:ok, msg, rest} :incomplete -> case :gen_tcp.recv(socket, 0) do {:ok, data} -> read_message(socket, buffer <> data) {:error, reason} -> {:error, reason} end end end defp try_decode(data) when byte_size(data) == 0, do: :incomplete defp try_decode(data) do try do {msg, rest} = Bencode.decode_one(data) {:ok, msg, rest} rescue _ -> :incomplete end end end