Skip to content

Commit

Permalink
OTP 26 support (#923)
Browse files Browse the repository at this point in the history
* fix map stable sort issue on OTP 26

* make dialyzer work with OTP 26

* trap exits in output device

make sure that io_request is handled before exiting

* make sure that we log to stderr if user device dies

* do not call setopt

on OTP 26 somehow this results in setopt request being sent to OutputDevice with not supported options causing it to crash

* run formatter

* failing case

* start erl with latin1 stdin

* cleanup
  • Loading branch information
lukaszsamson committed Jun 25, 2023
1 parent 3680ba5 commit 88dd761
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 28 deletions.
1 change: 1 addition & 0 deletions apps/elixir_ls_debugger/lib/debugger/variables.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ defmodule ElixirLS.Debugger.Variables do
children =
var
|> Map.to_list()
|> Enum.sort()
|> Enum.slice(start || 0, count || map_size(var))

for {key, value} <- children do
Expand Down
13 changes: 10 additions & 3 deletions apps/elixir_ls_utils/lib/output_device.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ defmodule ElixirLS.Utils.OutputDevice do
## Client API

def start_link(device, output_fn) do
Task.start_link(fn -> loop({device, output_fn}) end)
Task.start_link(fn ->
# Trap exit to make sure the process completes :io_request handling before exiting
Process.flag(:trap_exit, true)
loop({device, output_fn})
end)
end

def child_spec(arguments) do
Expand All @@ -22,12 +26,13 @@ defmodule ElixirLS.Utils.OutputDevice do
}
end

def get_opts, do: @opts

## Implementation

defp loop(state) do
receive do
{:EXIT, _from, reason} ->
exit(reason)

{:io_request, from, reply_as, request} ->
result = io_request(request, state, reply_as)
send(from, {:io_reply, reply_as, result})
Expand Down Expand Up @@ -82,6 +87,8 @@ defmodule ElixirLS.Utils.OutputDevice do
end

defp io_request({:setopts, new_opts}, _state, _reply_as) do
# we do not support changing opts
# only validate that the passed ones match defaults
validate_otps(new_opts, {:ok, 0})
end

Expand Down
41 changes: 36 additions & 5 deletions apps/elixir_ls_utils/lib/packet_stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ defmodule ElixirLS.Utils.PacketStream do
Reads from an IO device and provides a stream of incoming packets
"""

def stream(pid \\ Process.group_leader()) do
if is_pid(pid) do
:ok = :io.setopts(pid, binary: true, encoding: :latin1)
end
def stream(pid, halt_on_error? \\ false) when is_pid(pid) do
stream_pid = self()

Task.start_link(fn ->
ref = Process.monitor(pid)

receive do
{:DOWN, ^ref, :process, _pid, reason} ->
send(stream_pid, {:exit_reason, reason})
end
end)

Stream.resource(
fn -> :ok end,
Expand All @@ -31,7 +38,31 @@ defmodule ElixirLS.Utils.PacketStream do
:ok

{:error, reason} ->
raise "Unable to read from device: #{inspect(reason)}"
"Unable to read from input device: #{inspect(reason)}"

error_message =
unless Process.alive?(pid) do
receive do
{:exit_reason, exit_reason} ->
"Input device terminated: #{inspect(exit_reason)}"
after
500 -> "Input device terminated"
end
else
"Unable to read from device: #{inspect(reason)}"
end

if halt_on_error? do
if ElixirLS.Utils.WireProtocol.io_intercepted?() do
ElixirLS.Utils.WireProtocol.undo_intercept_output()
end

IO.puts(:stderr, error_message)

System.halt(1)
else
raise error_message
end
end
)
end
Expand Down
78 changes: 69 additions & 9 deletions apps/elixir_ls_utils/lib/wire_protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,93 @@ defmodule ElixirLS.Utils.WireProtocol do
end

def io_intercepted? do
!!Process.whereis(:raw_user)
!!Process.whereis(:raw_standard_error)
end

def intercept_output(print_fn, print_err_fn) do
raw_user = Process.whereis(:user)
raw_standard_error = Process.whereis(:standard_error)

:ok = :io.setopts(raw_user, OutputDevice.get_opts())
:ok = :io.setopts(raw_user, binary: true, encoding: :latin1)

{:ok, user} = OutputDevice.start_link(raw_user, print_fn)
{:ok, standard_error} = OutputDevice.start_link(raw_user, print_err_fn)
{:ok, intercepted_user} = OutputDevice.start_link(raw_user, print_fn)
{:ok, intercepted_standard_error} = OutputDevice.start_link(raw_user, print_err_fn)

Process.unregister(:user)
Process.register(raw_user, :raw_user)
Process.register(user, :user)
Process.register(intercepted_user, :user)

Process.unregister(:standard_error)
Process.register(raw_standard_error, :raw_standard_error)
Process.register(standard_error, :standard_error)
Process.register(intercepted_standard_error, :standard_error)

for process <- :erlang.processes(), process not in [raw_user, raw_standard_error] do
Process.group_leader(process, user)
for process <- :erlang.processes(),
process not in [
raw_user,
raw_standard_error,
intercepted_user,
intercepted_standard_error
] do
Process.group_leader(process, intercepted_user)
end
end

def undo_intercept_output() do
intercepted_user = Process.whereis(:user)
intercepted_standard_error = Process.whereis(:standard_error)

Process.unregister(:user)

raw_user =
try do
raw_user = Process.whereis(:raw_user)
Process.unregister(:raw_user)
Process.register(raw_user, :user)
raw_user
rescue
ArgumentError -> nil
end

Process.unregister(:standard_error)

raw_standard_error =
try do
raw_standard_error = Process.whereis(:raw_standard_error)
Process.unregister(:raw_standard_error)
Process.register(raw_standard_error, :standard_error)
raw_user
rescue
ArgumentError -> nil
end

if raw_user do
for process <- :erlang.processes(),
process not in [
raw_user,
raw_standard_error,
intercepted_user,
intercepted_standard_error
] do
Process.group_leader(process, raw_user)
end
else
init = :erlang.processes() |> hd

for process <- :erlang.processes(),
process not in [raw_standard_error, intercepted_user, intercepted_standard_error] do
Process.group_leader(process, init)
end
end

Process.unlink(intercepted_user)
Process.unlink(intercepted_standard_error)

Process.exit(intercepted_user, :kill)
Process.exit(intercepted_standard_error, :kill)
end

def stream_packets(receive_packets_fn) do
PacketStream.stream(Process.whereis(:raw_user))
PacketStream.stream(Process.whereis(:raw_user), true)
|> Stream.each(fn packet -> receive_packets_fn.(packet) end)
|> Stream.run()
end
Expand Down
2 changes: 1 addition & 1 deletion apps/elixir_ls_utils/priv/debugger.bat
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ IF EXIST "%APPDATA%\elixir_ls\setup.bat" (
)

SET ERL_LIBS=%~dp0;%ERL_LIBS%
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.Debugger.CLI.main()"
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.Debugger.CLI.main()"
2 changes: 1 addition & 1 deletion apps/elixir_ls_utils/priv/language_server.bat
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ IF EXIST "%APPDATA%\elixir_ls\setup.bat" (
)

SET ERL_LIBS=%~dp0;%ERL_LIBS%
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.LanguageServer.CLI.main()"
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.LanguageServer.CLI.main()"
2 changes: 1 addition & 1 deletion apps/elixir_ls_utils/priv/launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ fi

export ERL_LIBS="$SCRIPTPATH:$ERL_LIBS"

exec elixir $ELS_ELIXIR_OPTS --erl "+sbwt none +sbwtdcpu none +sbwtdio none $ELS_ERL_OPTS" -e "$ELS_SCRIPT"
exec elixir $ELS_ELIXIR_OPTS --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none $ELS_ERL_OPTS" -e "$ELS_SCRIPT"
29 changes: 28 additions & 1 deletion apps/language_server/lib/language_server/dialyzer/analyzer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
solvers: :undefined
)

Record.defrecordp(
:analysis_26,
:analysis,
analysis_pid: :undefined,
type: :succ_typings,
defines: [],
doc_plt: :undefined,
files: [],
include_dirs: [],
start_from: :byte_code,
plt: :undefined,
use_contracts: true,
behaviours_chk: false,
timing: false,
timing_server: :none,
callgraph_file: [],
mod_deps_file: [],
solvers: :undefined
)

def analyze(active_plt, []) do
{active_plt, %{}, []}
end
Expand All @@ -110,12 +130,19 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
solvers: []
)

_ ->
25 ->
analysis_25(
plt: active_plt,
files: files,
solvers: []
)

_ ->
analysis_26(
plt: active_plt,
files: files,
solvers: []
)
end

parent = self()
Expand Down
18 changes: 14 additions & 4 deletions apps/language_server/lib/language_server/dialyzer/manifest.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,15 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do
exported_types_list
} = File.read!(manifest_path) |> :erlang.binary_to_term()

# FIXME: matching against opaque type
active_plt = :dialyzer_plt.new()

plt(
info: info,
types: types,
contracts: contracts,
callbacks: callbacks,
exported_types: exported_types
) = active_plt = apply(:dialyzer_plt, :new, [])
) = active_plt

for item <- info_list, do: :ets.insert(info, item)
for item <- types_list, do: :ets.insert(types, item)
Expand All @@ -127,7 +128,11 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do
end

def load_elixir_plt() do
apply(:dialyzer_plt, :from_file, [to_charlist(elixir_plt_path())])
if String.to_integer(System.otp_release()) < 26 do
:dialyzer_plt.from_file(to_charlist(elixir_plt_path()))
else
:dialyzer_cplt.from_file(to_charlist(elixir_plt_path()))
end
rescue
_ -> build_elixir_plt()
catch
Expand Down Expand Up @@ -175,7 +180,12 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do
)

JsonRpc.show_message(:info, "Saved Elixir PLT to #{elixir_plt_path()}")
:dialyzer_plt.from_file(to_charlist(elixir_plt_path()))

if String.to_integer(System.otp_release()) < 26 do
:dialyzer_plt.from_file(to_charlist(elixir_plt_path()))
else
:dialyzer_cplt.from_file(to_charlist(elixir_plt_path()))
end
end

defp otp_vsn() do
Expand Down
2 changes: 1 addition & 1 deletion scripts/debugger.bat
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ SET MIX_ENV=prod
@REM elixir is a batch script and needs to be called
ECHO "" | CALL elixir "%~dp0quiet_install.exs" > nul
IF %ERRORLEVEL% NEQ 0 EXIT 1
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"
2 changes: 1 addition & 1 deletion scripts/language_server.bat
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ SET MIX_ENV=prod
@REM elixir is a batch script and needs to be called
ECHO "" | CALL elixir "%~dp0quiet_install.exs" >nul
IF %ERRORLEVEL% NEQ 0 EXIT 1
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"
2 changes: 1 addition & 1 deletion scripts/launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ export MIX_ENV=prod
# we need to make sure it doesn't interfere with LSP/DAP
echo "" | elixir "$SCRIPTPATH/quiet_install.exs" >/dev/null || exit 1

exec elixir $ELS_ELIXIR_OPTS --erl "+sbwt none +sbwtdcpu none +sbwtdio none $ELS_ERL_OPTS" "$SCRIPTPATH/launch.exs"
exec elixir $ELS_ELIXIR_OPTS --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none $ELS_ERL_OPTS" "$SCRIPTPATH/launch.exs"

0 comments on commit 88dd761

Please sign in to comment.