diff --git a/lib/gen_lsp.ex b/lib/gen_lsp.ex index 6c0a361..fa2e0b4 100644 --- a/lib/gen_lsp.ex +++ b/lib/gen_lsp.ex @@ -174,6 +174,10 @@ defmodule GenLSP do type: {:or, [:pid, :atom]}, doc: "The `t:pid/0` or name of the `GenLSP.Assigns` process." ], + task_supervisor: [ + type: {:or, [:pid, :atom]}, + doc: "The Task.Supervisor for the task queue" + ], name: [ type: :atom, doc: @@ -192,7 +196,8 @@ defmodule GenLSP do opts = NimbleOptions.validate!(opts, @options_schema) :proc_lib.start_link(__MODULE__, :init, [ - {module, init_args, Keyword.take(opts, [:name, :buffer, :assigns]), self()} + {module, init_args, Keyword.take(opts, [:name, :buffer, :assigns, :task_supervisor]), + self()} ]) end @@ -201,7 +206,16 @@ defmodule GenLSP do me = self() buffer = opts[:buffer] assigns = opts[:assigns] - lsp = %LSP{mod: module, pid: me, buffer: buffer, assigns: assigns} + task_supervisor = opts[:task_supervisor] + + lsp = %LSP{ + mod: module, + pid: me, + buffer: buffer, + assigns: assigns, + task_supervisor: task_supervisor, + tasks: Map.new() + } case module.init(lsp, init_args) do {:ok, %LSP{} = lsp} -> @@ -318,146 +332,149 @@ defmodule GenLSP do start = System.system_time(:microsecond) :telemetry.execute([:gen_lsp, :request, :client, :start], %{}) - attempt( - lsp, - "Last message received: handle_request #{inspect(request)}", - [:gen_lsp, :request, :client], - fn - {:error, error} -> - {:ok, output} = - Schematic.dump( - GenLSP.ErrorResponse.schema(), - %GenLSP.ErrorResponse{ - code: GenLSP.Enumerations.ErrorCodes.internal_error(), - message: error - } - ) - - packet = %{ - "jsonrpc" => "2.0", - "id" => Process.get(:request_id), - "error" => output - } - - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) - - GenLSP.Buffer.outgoing(lsp.buffer, packet) - loop(lsp, parent, deb) - - _ -> - case GenLSP.Requests.new(request) do - {:ok, %{id: id} = req} -> - Process.put(:request_id, id) - - result = - :telemetry.span([:gen_lsp, :handle_request], %{method: req.method}, fn -> - {lsp.mod.handle_request(req, lsp), %{}} - end) - - case result do - {:reply, reply, %LSP{} = lsp} -> - response_key = - case reply do - %GenLSP.ErrorResponse{} -> "error" - _ -> "result" - end - - # if result is valid, continue, if not, we return an internal error - {response_key, response} = - case Schematic.dump(req.__struct__.result(), reply) do - {:ok, output} -> - {response_key, output} - - {:error, errors} -> - exception = InvalidResponse.exception({req.method, reply, errors}) - - Logger.error(Exception.format(:error, exception)) - - {:ok, output} = - Schematic.dump( - GenLSP.ErrorResponse.schema(), - %GenLSP.ErrorResponse{ - code: GenLSP.Enumerations.ErrorCodes.internal_error(), - message: exception.message - } - ) - - {"error", output} - end - - packet = %{ - "jsonrpc" => "2.0", - "id" => id, - response_key => response - } - - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) - - GenLSP.Buffer.outgoing(lsp.buffer, packet) - - duration = System.system_time(:microsecond) - start - - Logger.debug( - "handled request client -> server #{req.method} in #{format_time(duration)}", - id: req.id, - method: req.method - ) - - :telemetry.execute([:gen_lsp, :request, :client, :stop], %{ - duration: duration - }) - - loop(lsp, parent, deb) - - {:noreply, lsp} -> - duration = System.system_time(:microsecond) - start + me = self() + + {:ok, task} = + attempt( + lsp, + "Last message received: handle_request #{inspect(request)}", + [:gen_lsp, :request, :client], + fn + {:error, error} -> + {:ok, output} = + Schematic.dump( + GenLSP.ErrorResponse.schema(), + %GenLSP.ErrorResponse{ + code: GenLSP.Enumerations.ErrorCodes.internal_error(), + message: error + } + ) - Logger.debug( - "handled request client -> server #{req.method} in #{format_time(duration)}", - id: req.id, - method: req.method + packet = %{ + "jsonrpc" => "2.0", + "id" => Process.get(:request_id), + "error" => output + } + + # deb = + # :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) + + GenLSP.Buffer.outgoing(lsp.buffer, packet) + + _ -> + case GenLSP.Requests.new(request) do + {:ok, %{id: id} = req} -> + Process.put(:request_id, id) + send(me, {:request_id, id}) + + result = + :telemetry.span([:gen_lsp, :handle_request], %{method: req.method}, fn -> + {lsp.mod.handle_request(req, lsp), %{}} + end) + + case result do + {:reply, reply, %LSP{} = lsp} -> + response_key = + case reply do + %GenLSP.ErrorResponse{} -> "error" + _ -> "result" + end + + # if result is valid, continue, if not, we return an internal error + {response_key, response} = + case Schematic.dump(req.__struct__.result(), reply) do + {:ok, output} -> + {response_key, output} + + {:error, errors} -> + exception = InvalidResponse.exception({req.method, reply, errors}) + + Logger.error(Exception.format(:error, exception)) + + {:ok, output} = + Schematic.dump( + GenLSP.ErrorResponse.schema(), + %GenLSP.ErrorResponse{ + code: GenLSP.Enumerations.ErrorCodes.internal_error(), + message: exception.message + } + ) + + {"error", output} + end + + packet = %{ + "jsonrpc" => "2.0", + "id" => id, + response_key => response + } + + GenLSP.Buffer.outgoing(lsp.buffer, packet) + + duration = System.system_time(:microsecond) - start + + Logger.debug( + "handled request client -> server #{req.method} in #{format_time(duration)}", + id: req.id, + method: req.method + ) + + :telemetry.execute([:gen_lsp, :request, :client, :stop], %{ + duration: duration + }) + + {:noreply, _lsp} -> + duration = System.system_time(:microsecond) - start + + Logger.debug( + "handled request client -> server #{req.method} in #{format_time(duration)}", + id: req.id, + method: req.method + ) + + :telemetry.execute([:gen_lsp, :request, :client, :stop], %{ + duration: duration + }) + end + + {:error, errors} -> + # the payload is not parseable at all, other than being valid JSON and having + # an `id` property to signal its a request + exception = InvalidRequest.exception({request, errors}) + + Logger.error(Exception.format(:error, exception)) + + {:ok, output} = + Schematic.dump( + GenLSP.ErrorResponse.schema(), + %GenLSP.ErrorResponse{ + code: GenLSP.Enumerations.ErrorCodes.invalid_request(), + message: exception.message + } ) - :telemetry.execute([:gen_lsp, :request, :client, :stop], %{ - duration: duration - }) - - loop(lsp, parent, deb) - end - - {:error, errors} -> - # the payload is not parseable at all, other than being valid JSON and having - # an `id` property to signal its a request - exception = InvalidRequest.exception({request, errors}) - - Logger.error(Exception.format(:error, exception)) - - {:ok, output} = - Schematic.dump( - GenLSP.ErrorResponse.schema(), - %GenLSP.ErrorResponse{ - code: GenLSP.Enumerations.ErrorCodes.invalid_request(), - message: exception.message - } - ) - - packet = %{ - "jsonrpc" => "2.0", - "id" => request["id"], - "error" => output - } + packet = %{ + "jsonrpc" => "2.0", + "id" => request["id"], + "error" => output + } + + GenLSP.Buffer.outgoing(lsp.buffer, packet) + end + end + ) + + id = + receive do + {:request_id, id} -> + id + end - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) + tasks = Map.put(lsp.tasks, id, task) - GenLSP.Buffer.outgoing(lsp.buffer, packet) + lsp = put_in(lsp.tasks, tasks) - loop(lsp, parent, deb) - end - end - ) + loop(lsp, parent, deb) {:notification, from, notification} -> deb = :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:in, :notification, from}) @@ -470,10 +487,38 @@ defmodule GenLSP do [:gen_lsp, :notification, :client], fn {:error, _} -> - loop(lsp, parent, deb) + Logger.warning("client -> server notification crashed") _ -> case GenLSP.Notifications.new(notification) do + {:ok, %GenLSP.Notifications.DollarCancelRequest{} = note} -> + result = + :telemetry.span( + [:gen_lsp, :handle_notification], + %{method: note.method}, + fn -> + with pid when is_pid(pid) <- lsp.tasks[note.params.id] do + Task.Supervisor.terminate_child(lsp.task_supervisor, pid) + end + + {{:noreply, lsp}, %{}} + end + ) + + case result do + {:noreply, %LSP{}} -> + duration = System.system_time(:microsecond) - start + + Logger.debug( + "handled notification client -> server #{note.method} in #{format_time(duration)}", + method: note.method + ) + + :telemetry.execute([:gen_lsp, :notification, :client, :stop], %{ + duration: duration + }) + end + {:ok, note} -> result = :telemetry.span( @@ -485,7 +530,7 @@ defmodule GenLSP do ) case result do - {:noreply, %LSP{} = lsp} -> + {:noreply, %LSP{}} -> duration = System.system_time(:microsecond) - start Logger.debug( @@ -496,8 +541,6 @@ defmodule GenLSP do :telemetry.execute([:gen_lsp, :notification, :client, :stop], %{ duration: duration }) - - loop(lsp, parent, deb) end {:error, errors} -> @@ -505,15 +548,12 @@ defmodule GenLSP do exception = InvalidNotification.exception({notification, errors}) Logger.warning(Exception.format(:error, exception)) - - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) - - loop(lsp, parent, deb) end end ) + loop(lsp, parent, deb) + message -> deb = :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:in, :info, message}) start = System.system_time(:microsecond) @@ -525,7 +565,7 @@ defmodule GenLSP do [:gen_lsp, :info], fn {:error, _} -> - loop(lsp, parent, deb) + Logger.warning("handle_info #{inspect(message)} crashed") _ -> result = @@ -534,13 +574,14 @@ defmodule GenLSP do end) case result do - {:noreply, %LSP{} = lsp} -> + {:noreply, %LSP{} = _lsp} -> duration = System.system_time(:microsecond) - start :telemetry.execute([:gen_lsp, :info, :stop], %{duration: duration}) - loop(lsp, parent, deb) end end ) + + loop(lsp, parent, deb) end end @@ -553,18 +594,22 @@ defmodule GenLSP do end @spec attempt(LSP.t(), String.t(), list(atom()), (:try | {:error, String.t()} -> any())) :: - no_return() + DynamicSupervisor.on_start_child() defp attempt(lsp, message, prefix, callback) do - callback.(:try) - rescue - e -> - :telemetry.execute(prefix ++ [:exception], %{message: message}) - - message = Exception.format(:error, e, __STACKTRACE__) - Logger.error(message) - error(lsp, message) - - callback.({:error, message}) + Task.Supervisor.start_child(lsp.task_supervisor, fn -> + try do + callback.(:try) + rescue + e -> + :telemetry.execute(prefix ++ [:exception], %{message: message}) + + message = Exception.format(:error, e, __STACKTRACE__) + Logger.error(message) + error(lsp, message) + + callback.({:error, message}) + end + end) end defp dump!(schematic, structure) do diff --git a/lib/gen_lsp/lsp.ex b/lib/gen_lsp/lsp.ex index 100f549..de708cf 100644 --- a/lib/gen_lsp/lsp.ex +++ b/lib/gen_lsp/lsp.ex @@ -9,6 +9,8 @@ defmodule GenLSP.LSP do field :buffer, atom() | pid() field :assigns, atom() | pid() field :pid, pid() + field :tasks, %{integer() => pid()} + field :task_supervisor, atom() | pid() end @spec assign(t(), Keyword.t() | (map() -> keyword())) :: t() diff --git a/lib/gen_lsp/test.ex b/lib/gen_lsp/test.ex index a7f0b55..76078fb 100644 --- a/lib/gen_lsp/test.ex +++ b/lib/gen_lsp/test.ex @@ -16,6 +16,9 @@ defmodule GenLSP.Test do buffer: pid(), buffer_id: atom(), assigns: pid(), + assigns_id: atom(), + task_supervisor: pid(), + task_supervisor_id: atom(), port: integer() } @@ -39,6 +42,7 @@ defmodule GenLSP.Test do def server(mod, opts \\ []) do buffer_id = Keyword.get(opts, :buffer_id, :buffer) assigns_id = Keyword.get(opts, :assigns_id, :assigns) + task_supervisor_id = Keyword.get(opts, :task_supervisor_id, :task_supervisor) lsp_id = Keyword.get(opts, :lsp_id, :lsp) buffer = @@ -46,23 +50,30 @@ defmodule GenLSP.Test do id: buffer_id ) - assigns = - start_supervised!(GenLSP.Assigns, id: assigns_id) + assigns = start_supervised!(GenLSP.Assigns, id: assigns_id) + + task_supervisor = + start_supervised!(Supervisor.child_spec(Task.Supervisor, id: task_supervisor_id)) {:ok, port} = :inet.port(GenLSP.Buffer.comm_state(buffer).lsocket) lsp = - start_supervised!({mod, Keyword.merge([buffer: buffer, assigns: assigns], opts)}, + start_supervised!( + {mod, + Keyword.merge([buffer: buffer, assigns: assigns, task_supervisor: task_supervisor], opts)}, id: lsp_id ) %{ lsp: lsp, + lsp_id: lsp_id, buffer: buffer, - assigns: assigns, - port: port, buffer_id: buffer_id, - lsp_id: lsp_id + assigns: assigns, + assigns_id: assigns_id, + task_supervisor: task_supervisor, + task_supervisor_id: task_supervisor_id, + port: port } end @@ -128,6 +139,8 @@ defmodule GenLSP.Test do @spec shutdown_server!(server :: server()) :: :ok def shutdown_server!(server) do stop_supervised!(server.buffer_id) + stop_supervised!(server.assigns_id) + stop_supervised!(server.task_supervisor_id) stop_supervised!(server.lsp_id) :ok diff --git a/test/gen_lsp_test.exs b/test/gen_lsp_test.exs index 1a24cdf..63f7e8f 100644 --- a/test/gen_lsp_test.exs +++ b/test/gen_lsp_test.exs @@ -41,6 +41,49 @@ defmodule GenLSPTest do 500 end + # @tag :skip + test "cancels a request", %{client: client} do + id = System.unique_integer([:positive]) + + assert :ok == + request(client, %{ + "jsonrpc" => "2.0", + "method" => "initialize", + "params" => %{"capabilities" => %{}}, + "id" => id + }) + + assert_result ^id, _, 500 + + request_id = id + 1 + + assert :ok == + request(client, %{ + "jsonrpc" => "2.0", + "method" => "textDocument/formatting", + "params" => %{ + "textDocument" => %{"uri" => "/path/to/file.ex"}, + "options" => %{"insertSpaces" => true, "tabSize" => 2} + }, + "id" => request_id + }) + + assert_receive {:request_pid, request_pid} + + ref = Process.monitor(request_pid) + + assert :ok == + notify(client, %{ + "jsonrpc" => "2.0", + "method" => "$/cancelRequest", + "params" => %{ + "id" => request_id + } + }) + + assert_receive {:DOWN, ^ref, :process, _object, _reason} + end + test "can send a request from the server to the client", %{client: client} do id = System.unique_integer([:positive]) @@ -193,12 +236,10 @@ defmodule GenLSPTest do assert_error(2, %{ "code" => -32603, - "message" => - """ - ** (RuntimeError) boom - (gen_lsp 0.10.0) test/support/example_server.ex:35: GenLSPTest.ExampleServer.handle_request/2 - """ <> _ + "message" => message }) + + assert message =~ "** (RuntimeError) boom" end) assert log =~ "[error] ** (RuntimeError) boom" diff --git a/test/support/example_server.ex b/test/support/example_server.ex index f89744b..c6936bc 100644 --- a/test/support/example_server.ex +++ b/test/support/example_server.ex @@ -27,6 +27,16 @@ defmodule GenLSPTest.ExampleServer do }, lsp} end + def handle_request(%Requests.TextDocumentFormatting{}, lsp) do + send(assigns(lsp).test_pid, {:request_pid, self()}) + + receive do + :finish -> :ok + end + + {:reply, nil, lsp} + end + def handle_request(%Requests.TextDocumentDocumentSymbol{}, lsp) do {:reply, [nil, []], lsp} end