Skip to content

Commit

Permalink
Log errors in Server.Transport.StdIO. (#684)
Browse files Browse the repository at this point in the history
* Log errors in `Server.Transport.StdIO`.
* Added error log in read loop.
* Improved error values in header and body parsing.

* format files

* Stop server when stdio receives `:eof`, and log message.

* Replace `String.to_integer/1` with `Integer.parse/1` to prevent crashes in `StdIO`.
* Also fix redundant clause in with expression per credo.
  • Loading branch information
Moosieus authored and scohen committed Apr 9, 2024
1 parent d690726 commit c095618
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 12 deletions.
6 changes: 5 additions & 1 deletion apps/protocol/lib/lexical/protocol/json_rpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Lexical.Protocol.JsonRpc do
alias Lexical.Protocol.Notifications
alias Lexical.Protocol.Requests

require Logger

@crlf "\r\n"

def decode(message_string) do
Expand Down Expand Up @@ -30,8 +32,10 @@ defmodule Lexical.Protocol.JsonRpc do
{:ok, json_rpc}
end

# These messages appear to be empty Responses (per LSP spec) sent to
# aknowledge Requests sent from the language server to the client.
defp do_decode(%{"id" => _id, "result" => nil}) do
:error
{:error, :empty_response}
end

defp do_decode(%{"method" => method, "id" => _id} = request) do
Expand Down
47 changes: 36 additions & 11 deletions apps/server/lib/lexical/server/transport/std_io.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Lexical.Server.Transport.StdIO do
alias Lexical.Protocol.JsonRpc

require Logger

@behaviour Lexical.Server.Transport

def start_link(device, callback) do
Expand Down Expand Up @@ -59,44 +61,67 @@ defmodule Lexical.Server.Transport.StdIO do
"\n" ->
headers = parse_headers(buffer)

with {:ok, content_length} <-
header_value(headers, "content-length", &String.to_integer/1),
with {:ok, content_length} <- content_length(headers),
{:ok, data} <- read_body(device, content_length),
{:ok, message} <- JsonRpc.decode(data) do
callback.(message)
else
{:error, :empty_response} ->
:noop

{:error, reason} ->
Logger.critical("read protocol message: #{inspect(reason)}")
end

loop([], device, callback)

:eof ->
Logger.critical("stdio received :eof, server will stop.")
maybe_stop()

line ->
loop([line | buffer], device, callback)
end
end

defp parse_headers(headers) do
Enum.map(headers, &parse_header/1)
defp content_length(headers) do
with {:ok, len_str} <- find_header(headers, "content-length") do
parse_length(len_str)
end
end

defp find_header(headers, name) do
case List.keyfind(headers, name, 0) do
{_, len_str} -> {:ok, len_str}
nil -> {:error, {:header_not_found, name}}
end
end

defp header_value(headers, header_name, converter) do
case List.keyfind(headers, header_name, 0) do
nil -> :error
{_, value} -> {:ok, converter.(value)}
defp parse_length(len_str) when is_binary(len_str) do
case Integer.parse(len_str) do
{int, ""} -> {:ok, int}
:error -> {:error, {:cant_parse_length, len_str}}
end
end

defp read_body(device, byte_count) do
case IO.binread(device, byte_count) do
data when is_binary(data) or is_list(data) ->
data when is_binary(data) ->
{:ok, data}

other ->
other
:eof ->
Logger.critical("stdio received :eof, server will stop.")
maybe_stop()

{:error, reason} ->
{:error, reason}
end
end

defp parse_headers(headers) do
Enum.map(headers, &parse_header/1)
end

defp parse_header(line) do
[name, value] = String.split(line, ":")

Expand Down

0 comments on commit c095618

Please sign in to comment.