From 88dd761227e7f0a2353fbd211934ff2a4e15fb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sun, 25 Jun 2023 22:51:10 +0200 Subject: [PATCH] OTP 26 support (#923) * 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 --- .../lib/debugger/variables.ex | 1 + apps/elixir_ls_utils/lib/output_device.ex | 13 +++- apps/elixir_ls_utils/lib/packet_stream.ex | 41 ++++++++-- apps/elixir_ls_utils/lib/wire_protocol.ex | 78 ++++++++++++++++--- apps/elixir_ls_utils/priv/debugger.bat | 2 +- apps/elixir_ls_utils/priv/language_server.bat | 2 +- apps/elixir_ls_utils/priv/launch.sh | 2 +- .../lib/language_server/dialyzer/analyzer.ex | 29 ++++++- .../lib/language_server/dialyzer/manifest.ex | 18 ++++- scripts/debugger.bat | 2 +- scripts/language_server.bat | 2 +- scripts/launch.sh | 2 +- 12 files changed, 164 insertions(+), 28 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/variables.ex b/apps/elixir_ls_debugger/lib/debugger/variables.ex index f86a4309a..8aeec8931 100644 --- a/apps/elixir_ls_debugger/lib/debugger/variables.ex +++ b/apps/elixir_ls_debugger/lib/debugger/variables.ex @@ -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 diff --git a/apps/elixir_ls_utils/lib/output_device.ex b/apps/elixir_ls_utils/lib/output_device.ex index 2594bdddd..344319b7b 100644 --- a/apps/elixir_ls_utils/lib/output_device.ex +++ b/apps/elixir_ls_utils/lib/output_device.ex @@ -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 @@ -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}) @@ -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 diff --git a/apps/elixir_ls_utils/lib/packet_stream.ex b/apps/elixir_ls_utils/lib/packet_stream.ex index 1235321fb..4a9c1accd 100644 --- a/apps/elixir_ls_utils/lib/packet_stream.ex +++ b/apps/elixir_ls_utils/lib/packet_stream.ex @@ -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, @@ -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 diff --git a/apps/elixir_ls_utils/lib/wire_protocol.ex b/apps/elixir_ls_utils/lib/wire_protocol.ex index 8e0450319..502d1e7b8 100644 --- a/apps/elixir_ls_utils/lib/wire_protocol.ex +++ b/apps/elixir_ls_utils/lib/wire_protocol.ex @@ -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 diff --git a/apps/elixir_ls_utils/priv/debugger.bat b/apps/elixir_ls_utils/priv/debugger.bat index 04c00bf8a..57089cb3d 100755 --- a/apps/elixir_ls_utils/priv/debugger.bat +++ b/apps/elixir_ls_utils/priv/debugger.bat @@ -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()" diff --git a/apps/elixir_ls_utils/priv/language_server.bat b/apps/elixir_ls_utils/priv/language_server.bat index c14e3cf09..0de4b6349 100755 --- a/apps/elixir_ls_utils/priv/language_server.bat +++ b/apps/elixir_ls_utils/priv/language_server.bat @@ -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()" diff --git a/apps/elixir_ls_utils/priv/launch.sh b/apps/elixir_ls_utils/priv/launch.sh index 2364526d6..c434c5ccd 100755 --- a/apps/elixir_ls_utils/priv/launch.sh +++ b/apps/elixir_ls_utils/priv/launch.sh @@ -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" diff --git a/apps/language_server/lib/language_server/dialyzer/analyzer.ex b/apps/language_server/lib/language_server/dialyzer/analyzer.ex index 0d8e5dc79..ecee353f4 100644 --- a/apps/language_server/lib/language_server/dialyzer/analyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer/analyzer.ex @@ -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 @@ -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() diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index d86041c47..7d1677d34 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -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) @@ -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 @@ -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 diff --git a/scripts/debugger.bat b/scripts/debugger.bat index 4c87e1905..0fecba623 100755 --- a/scripts/debugger.bat +++ b/scripts/debugger.bat @@ -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" diff --git a/scripts/language_server.bat b/scripts/language_server.bat index e035ff412..6bfae24d9 100755 --- a/scripts/language_server.bat +++ b/scripts/language_server.bat @@ -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" diff --git a/scripts/launch.sh b/scripts/launch.sh index aa0acd5f4..f31d4bebd 100755 --- a/scripts/launch.sh +++ b/scripts/launch.sh @@ -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"