151 lines
4.1 KiB
Elixir
151 lines
4.1 KiB
Elixir
defmodule CljElixir.NRepl.SessionManager do
|
|
@moduledoc "Manages nREPL sessions with independent REPL state."
|
|
|
|
use GenServer
|
|
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, %{}, opts)
|
|
end
|
|
|
|
@impl true
|
|
def init(_) do
|
|
{:ok, %{sessions: %{}}}
|
|
end
|
|
|
|
def create_session(manager) do
|
|
GenServer.call(manager, :create_session)
|
|
end
|
|
|
|
def close_session(manager, session_id) do
|
|
GenServer.call(manager, {:close_session, session_id})
|
|
end
|
|
|
|
def list_sessions(manager) do
|
|
GenServer.call(manager, :list_sessions)
|
|
end
|
|
|
|
def eval(manager, session_id, code) do
|
|
GenServer.call(manager, {:eval, session_id, code}, :infinity)
|
|
end
|
|
|
|
@doc "Eval with stdout capture. Returns {output, result} where result is {:ok, value} | {:error, error}."
|
|
def eval_with_capture(manager, session_id, code) do
|
|
GenServer.call(manager, {:eval_with_capture, session_id, code}, :infinity)
|
|
end
|
|
|
|
# --- Callbacks ---
|
|
|
|
@impl true
|
|
def handle_call(:create_session, _from, state) do
|
|
id = generate_uuid()
|
|
{:ok, pid} = Agent.start_link(fn -> CljElixir.REPL.new() end)
|
|
new_sessions = Map.put(state.sessions, id, pid)
|
|
{:reply, id, %{state | sessions: new_sessions}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:close_session, id}, _from, state) do
|
|
case Map.pop(state.sessions, id) do
|
|
{nil, _} ->
|
|
{:reply, :not_found, state}
|
|
|
|
{pid, new_sessions} ->
|
|
Agent.stop(pid)
|
|
{:reply, :ok, %{state | sessions: new_sessions}}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:list_sessions, _from, state) do
|
|
{:reply, Map.keys(state.sessions), state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:eval, id, code}, _from, state) do
|
|
case Map.get(state.sessions, id) do
|
|
nil ->
|
|
{:reply, {:error, "unknown session"}, state}
|
|
|
|
pid ->
|
|
result =
|
|
Agent.get_and_update(
|
|
pid,
|
|
fn repl_state ->
|
|
case CljElixir.REPL.eval(code, repl_state) do
|
|
{:ok, value, new_state} -> {{:ok, value}, new_state}
|
|
{:error, error, new_state} -> {{:error, error}, new_state}
|
|
end
|
|
end,
|
|
:infinity
|
|
)
|
|
|
|
{:reply, result, state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:eval_with_capture, id, code}, _from, state) do
|
|
case Map.get(state.sessions, id) do
|
|
nil ->
|
|
{:reply, {"", {:error, "unknown session"}, "user"}, state}
|
|
|
|
pid ->
|
|
{output, result, ns} =
|
|
Agent.get_and_update(
|
|
pid,
|
|
fn repl_state ->
|
|
{output, eval_result, new_state} = eval_capturing_output(code, repl_state)
|
|
ns = CljElixir.REPL.current_ns(new_state)
|
|
|
|
{{output, eval_result, ns}, new_state}
|
|
end,
|
|
:infinity
|
|
)
|
|
|
|
{:reply, {output, result, ns}, state}
|
|
end
|
|
end
|
|
|
|
# Evaluate code while capturing stdout within the current process
|
|
defp eval_capturing_output(code, repl_state) do
|
|
{:ok, string_io} = StringIO.open("")
|
|
old_gl = Process.group_leader()
|
|
Process.group_leader(self(), string_io)
|
|
|
|
try do
|
|
case CljElixir.REPL.eval(code, repl_state) do
|
|
{:ok, value, new_state} ->
|
|
Process.group_leader(self(), old_gl)
|
|
{_input, output} = StringIO.contents(string_io)
|
|
StringIO.close(string_io)
|
|
{output, {:ok, value}, new_state}
|
|
|
|
{:error, error, new_state} ->
|
|
Process.group_leader(self(), old_gl)
|
|
{_input, output} = StringIO.contents(string_io)
|
|
StringIO.close(string_io)
|
|
{output, {:error, error}, new_state}
|
|
end
|
|
rescue
|
|
e ->
|
|
Process.group_leader(self(), old_gl)
|
|
StringIO.close(string_io)
|
|
{"", {:error, Exception.message(e)}, repl_state}
|
|
end
|
|
end
|
|
|
|
# Simple UUID v4 generation using :crypto (OTP built-in, no deps)
|
|
defp generate_uuid do
|
|
<<a::32, b::16, c::16, d::16, e::48>> = :crypto.strong_rand_bytes(16)
|
|
|
|
:io_lib.format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b", [
|
|
a,
|
|
b,
|
|
Bitwise.band(c, 0x0FFF),
|
|
Bitwise.bor(Bitwise.band(d, 0x3FFF), 0x8000),
|
|
e
|
|
])
|
|
|> IO.iodata_to_binary()
|
|
end
|
|
end
|