Skip to content

Commit 88dd761

Browse files
authored
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
1 parent 3680ba5 commit 88dd761

File tree

12 files changed

+164
-28
lines changed

12 files changed

+164
-28
lines changed

apps/elixir_ls_debugger/lib/debugger/variables.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ defmodule ElixirLS.Debugger.Variables do
6666
children =
6767
var
6868
|> Map.to_list()
69+
|> Enum.sort()
6970
|> Enum.slice(start || 0, count || map_size(var))
7071

7172
for {key, value} <- children do

apps/elixir_ls_utils/lib/output_device.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ defmodule ElixirLS.Utils.OutputDevice do
99
## Client API
1010

1111
def start_link(device, output_fn) do
12-
Task.start_link(fn -> loop({device, output_fn}) end)
12+
Task.start_link(fn ->
13+
# Trap exit to make sure the process completes :io_request handling before exiting
14+
Process.flag(:trap_exit, true)
15+
loop({device, output_fn})
16+
end)
1317
end
1418

1519
def child_spec(arguments) do
@@ -22,12 +26,13 @@ defmodule ElixirLS.Utils.OutputDevice do
2226
}
2327
end
2428

25-
def get_opts, do: @opts
26-
2729
## Implementation
2830

2931
defp loop(state) do
3032
receive do
33+
{:EXIT, _from, reason} ->
34+
exit(reason)
35+
3136
{:io_request, from, reply_as, request} ->
3237
result = io_request(request, state, reply_as)
3338
send(from, {:io_reply, reply_as, result})
@@ -82,6 +87,8 @@ defmodule ElixirLS.Utils.OutputDevice do
8287
end
8388

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

apps/elixir_ls_utils/lib/packet_stream.ex

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ defmodule ElixirLS.Utils.PacketStream do
33
Reads from an IO device and provides a stream of incoming packets
44
"""
55

6-
def stream(pid \\ Process.group_leader()) do
7-
if is_pid(pid) do
8-
:ok = :io.setopts(pid, binary: true, encoding: :latin1)
9-
end
6+
def stream(pid, halt_on_error? \\ false) when is_pid(pid) do
7+
stream_pid = self()
8+
9+
Task.start_link(fn ->
10+
ref = Process.monitor(pid)
11+
12+
receive do
13+
{:DOWN, ^ref, :process, _pid, reason} ->
14+
send(stream_pid, {:exit_reason, reason})
15+
end
16+
end)
1017

1118
Stream.resource(
1219
fn -> :ok end,
@@ -31,7 +38,31 @@ defmodule ElixirLS.Utils.PacketStream do
3138
:ok
3239

3340
{:error, reason} ->
34-
raise "Unable to read from device: #{inspect(reason)}"
41+
"Unable to read from input device: #{inspect(reason)}"
42+
43+
error_message =
44+
unless Process.alive?(pid) do
45+
receive do
46+
{:exit_reason, exit_reason} ->
47+
"Input device terminated: #{inspect(exit_reason)}"
48+
after
49+
500 -> "Input device terminated"
50+
end
51+
else
52+
"Unable to read from device: #{inspect(reason)}"
53+
end
54+
55+
if halt_on_error? do
56+
if ElixirLS.Utils.WireProtocol.io_intercepted?() do
57+
ElixirLS.Utils.WireProtocol.undo_intercept_output()
58+
end
59+
60+
IO.puts(:stderr, error_message)
61+
62+
System.halt(1)
63+
else
64+
raise error_message
65+
end
3566
end
3667
)
3768
end

apps/elixir_ls_utils/lib/wire_protocol.ex

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,93 @@ defmodule ElixirLS.Utils.WireProtocol do
2323
end
2424

2525
def io_intercepted? do
26-
!!Process.whereis(:raw_user)
26+
!!Process.whereis(:raw_standard_error)
2727
end
2828

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

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

35-
{:ok, user} = OutputDevice.start_link(raw_user, print_fn)
36-
{:ok, standard_error} = OutputDevice.start_link(raw_user, print_err_fn)
35+
{:ok, intercepted_user} = OutputDevice.start_link(raw_user, print_fn)
36+
{:ok, intercepted_standard_error} = OutputDevice.start_link(raw_user, print_err_fn)
3737

3838
Process.unregister(:user)
3939
Process.register(raw_user, :raw_user)
40-
Process.register(user, :user)
40+
Process.register(intercepted_user, :user)
4141

4242
Process.unregister(:standard_error)
4343
Process.register(raw_standard_error, :raw_standard_error)
44-
Process.register(standard_error, :standard_error)
44+
Process.register(intercepted_standard_error, :standard_error)
4545

46-
for process <- :erlang.processes(), process not in [raw_user, raw_standard_error] do
47-
Process.group_leader(process, user)
46+
for process <- :erlang.processes(),
47+
process not in [
48+
raw_user,
49+
raw_standard_error,
50+
intercepted_user,
51+
intercepted_standard_error
52+
] do
53+
Process.group_leader(process, intercepted_user)
4854
end
4955
end
5056

57+
def undo_intercept_output() do
58+
intercepted_user = Process.whereis(:user)
59+
intercepted_standard_error = Process.whereis(:standard_error)
60+
61+
Process.unregister(:user)
62+
63+
raw_user =
64+
try do
65+
raw_user = Process.whereis(:raw_user)
66+
Process.unregister(:raw_user)
67+
Process.register(raw_user, :user)
68+
raw_user
69+
rescue
70+
ArgumentError -> nil
71+
end
72+
73+
Process.unregister(:standard_error)
74+
75+
raw_standard_error =
76+
try do
77+
raw_standard_error = Process.whereis(:raw_standard_error)
78+
Process.unregister(:raw_standard_error)
79+
Process.register(raw_standard_error, :standard_error)
80+
raw_user
81+
rescue
82+
ArgumentError -> nil
83+
end
84+
85+
if raw_user do
86+
for process <- :erlang.processes(),
87+
process not in [
88+
raw_user,
89+
raw_standard_error,
90+
intercepted_user,
91+
intercepted_standard_error
92+
] do
93+
Process.group_leader(process, raw_user)
94+
end
95+
else
96+
init = :erlang.processes() |> hd
97+
98+
for process <- :erlang.processes(),
99+
process not in [raw_standard_error, intercepted_user, intercepted_standard_error] do
100+
Process.group_leader(process, init)
101+
end
102+
end
103+
104+
Process.unlink(intercepted_user)
105+
Process.unlink(intercepted_standard_error)
106+
107+
Process.exit(intercepted_user, :kill)
108+
Process.exit(intercepted_standard_error, :kill)
109+
end
110+
51111
def stream_packets(receive_packets_fn) do
52-
PacketStream.stream(Process.whereis(:raw_user))
112+
PacketStream.stream(Process.whereis(:raw_user), true)
53113
|> Stream.each(fn packet -> receive_packets_fn.(packet) end)
54114
|> Stream.run()
55115
end

apps/elixir_ls_utils/priv/debugger.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ IF EXIST "%APPDATA%\elixir_ls\setup.bat" (
66
)
77

88
SET ERL_LIBS=%~dp0;%ERL_LIBS%
9-
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.Debugger.CLI.main()"
9+
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.Debugger.CLI.main()"

apps/elixir_ls_utils/priv/language_server.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ IF EXIST "%APPDATA%\elixir_ls\setup.bat" (
66
)
77

88
SET ERL_LIBS=%~dp0;%ERL_LIBS%
9-
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.LanguageServer.CLI.main()"
9+
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" -e "ElixirLS.LanguageServer.CLI.main()"

apps/elixir_ls_utils/priv/launch.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,4 @@ fi
7979

8080
export ERL_LIBS="$SCRIPTPATH:$ERL_LIBS"
8181

82-
exec elixir $ELS_ELIXIR_OPTS --erl "+sbwt none +sbwtdcpu none +sbwtdio none $ELS_ERL_OPTS" -e "$ELS_SCRIPT"
82+
exec elixir $ELS_ELIXIR_OPTS --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none $ELS_ERL_OPTS" -e "$ELS_SCRIPT"

apps/language_server/lib/language_server/dialyzer/analyzer.ex

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,26 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
9696
solvers: :undefined
9797
)
9898

99+
Record.defrecordp(
100+
:analysis_26,
101+
:analysis,
102+
analysis_pid: :undefined,
103+
type: :succ_typings,
104+
defines: [],
105+
doc_plt: :undefined,
106+
files: [],
107+
include_dirs: [],
108+
start_from: :byte_code,
109+
plt: :undefined,
110+
use_contracts: true,
111+
behaviours_chk: false,
112+
timing: false,
113+
timing_server: :none,
114+
callgraph_file: [],
115+
mod_deps_file: [],
116+
solvers: :undefined
117+
)
118+
99119
def analyze(active_plt, []) do
100120
{active_plt, %{}, []}
101121
end
@@ -110,12 +130,19 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
110130
solvers: []
111131
)
112132

113-
_ ->
133+
25 ->
114134
analysis_25(
115135
plt: active_plt,
116136
files: files,
117137
solvers: []
118138
)
139+
140+
_ ->
141+
analysis_26(
142+
plt: active_plt,
143+
files: files,
144+
solvers: []
145+
)
119146
end
120147

121148
parent = self()

apps/language_server/lib/language_server/dialyzer/manifest.ex

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,15 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do
106106
exported_types_list
107107
} = File.read!(manifest_path) |> :erlang.binary_to_term()
108108

109-
# FIXME: matching against opaque type
109+
active_plt = :dialyzer_plt.new()
110+
110111
plt(
111112
info: info,
112113
types: types,
113114
contracts: contracts,
114115
callbacks: callbacks,
115116
exported_types: exported_types
116-
) = active_plt = apply(:dialyzer_plt, :new, [])
117+
) = active_plt
117118

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

129130
def load_elixir_plt() do
130-
apply(:dialyzer_plt, :from_file, [to_charlist(elixir_plt_path())])
131+
if String.to_integer(System.otp_release()) < 26 do
132+
:dialyzer_plt.from_file(to_charlist(elixir_plt_path()))
133+
else
134+
:dialyzer_cplt.from_file(to_charlist(elixir_plt_path()))
135+
end
131136
rescue
132137
_ -> build_elixir_plt()
133138
catch
@@ -175,7 +180,12 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do
175180
)
176181

177182
JsonRpc.show_message(:info, "Saved Elixir PLT to #{elixir_plt_path()}")
178-
:dialyzer_plt.from_file(to_charlist(elixir_plt_path()))
183+
184+
if String.to_integer(System.otp_release()) < 26 do
185+
:dialyzer_plt.from_file(to_charlist(elixir_plt_path()))
186+
else
187+
:dialyzer_cplt.from_file(to_charlist(elixir_plt_path()))
188+
end
179189
end
180190

181191
defp otp_vsn() do

scripts/debugger.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ SET MIX_ENV=prod
1313
@REM elixir is a batch script and needs to be called
1414
ECHO "" | CALL elixir "%~dp0quiet_install.exs" > nul
1515
IF %ERRORLEVEL% NEQ 0 EXIT 1
16-
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"
16+
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"

scripts/language_server.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ SET MIX_ENV=prod
1313
@REM elixir is a batch script and needs to be called
1414
ECHO "" | CALL elixir "%~dp0quiet_install.exs" >nul
1515
IF %ERRORLEVEL% NEQ 0 EXIT 1
16-
elixir %ELS_ELIXIR_OPTS% --erl "+sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"
16+
elixir %ELS_ELIXIR_OPTS% --erl "-kernel standard_io_encoding latin1 +sbwt none +sbwtdcpu none +sbwtdio none %ELS_ERL_OPTS%" "%~dp0launch.exs"

scripts/launch.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,4 @@ export MIX_ENV=prod
8282
# we need to make sure it doesn't interfere with LSP/DAP
8383
echo "" | elixir "$SCRIPTPATH/quiet_install.exs" >/dev/null || exit 1
8484

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

0 commit comments

Comments
 (0)