Skip to content

Commit c28b596

Browse files
committed
Fix prompt handling in Erlang/OTP 26, closes #12584
1 parent a1e0726 commit c28b596

File tree

8 files changed

+175
-107
lines changed

8 files changed

+175
-107
lines changed

bin/iex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ readlink_f () {
3030

3131
SELF=$(readlink_f "$0")
3232
SCRIPT_PATH=$(dirname "$SELF")
33-
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-user elixir" +iex "$@"
33+
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-user elixir" -e ":elixir.start_iex()" +iex "$@"

bin/iex.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ goto end
2424

2525
:run
2626
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
27-
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
27+
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" -e ":elixir.start_iex()" +iex %__ELIXIR_IEX_FLAGS% %*
2828
:end
2929
endlocal

lib/elixir/src/elixir.erl

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
%% private to the Elixir compiler and reserved to be used by Elixir only.
33
-module(elixir).
44
-behaviour(application).
5-
-export([start_cli/0, start/0]).
5+
-export([start_cli/0, start/0, start_iex/0]).
66
-export([start/2, stop/1, config_change/3]).
77
-export([
88
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
@@ -192,12 +192,29 @@ start_cli() ->
192192
'Elixir.Kernel.CLI':main(init:get_plain_arguments()),
193193
elixir_config:booted().
194194

195+
%% TODO: Delete prim_tty branches and -user on Erlang/OTP 26.
195196
start() ->
196-
case init:get_argument(elixir_root) of
197-
{ok, [[Root]]} -> code:add_patha(Root ++ "/iex/ebin");
198-
_ -> ok
199-
end,
200-
'Elixir.IEx.CLI':main().
197+
case code:ensure_loaded(prim_tty) of
198+
{module, _} ->
199+
user_drv:start(#{initial_shell => noshell});
200+
{error, _} ->
201+
case init:get_argument(elixir_root) of
202+
{ok, [[Root]]} -> code:add_patha(Root ++ "/iex/ebin");
203+
_ -> ok
204+
end,
205+
'Elixir.IEx.CLI':deprecated()
206+
end.
207+
start_iex() ->
208+
case code:ensure_loaded(prim_tty) of
209+
{module, _} ->
210+
spawn(fun() ->
211+
elixir_config:wait_until_booted(),
212+
(shell:whereis() =:= undefined) andalso 'Elixir.IEx':cli()
213+
end);
214+
215+
{error, _} ->
216+
ok
217+
end.
201218

202219
%% EVAL HOOKS
203220

lib/iex/lib/iex.ex

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -848,22 +848,101 @@ defmodule IEx do
848848
@spec break!(module, atom, arity, non_neg_integer) :: IEx.Pry.id()
849849
defdelegate break!(module, function, arity, stops \\ 1), to: IEx.Pry
850850

851-
## Callbacks
851+
@doc false
852+
def dont_display_result, do: :"do not show this result in output"
853+
854+
## CLI
855+
856+
@compile {:no_warn_undefined, {:shell, :start_interactive, 1}}
852857

853858
# This is a callback invoked by Erlang shell utilities
854-
# when someone presses Ctrl+G and adds 's Elixir.IEx'.
859+
# when someone presses Ctrl+G and adds `s 'Elixir.IEx'`.
855860
@doc false
856861
def start(opts \\ [], mfa \\ {IEx, :dont_display_result, []}) do
857-
spawn(fn ->
858-
{:ok, _} = Application.ensure_all_started(:elixir)
859-
System.wait_until_booted()
860-
:ok = :io.setopts(binary: true, encoding: :unicode)
861-
{:ok, _} = Application.ensure_all_started(:iex)
862-
_ = for fun <- Enum.reverse(after_spawn()), do: fun.()
863-
IEx.Server.run_from_shell(opts, mfa)
864-
end)
862+
# TODO: Keep only this branch, delete optional args and mfa,
863+
# and delete IEx.Server.run_from_shell/2 on Erlang/OTP 26+
864+
if Code.ensure_loaded?(:prim_tty) do
865+
spawn(fn ->
866+
{:ok, _} = Application.ensure_all_started(:iex)
867+
_ = for fun <- Enum.reverse(after_spawn()), do: fun.()
868+
IEx.Server.run([register: false] ++ opts)
869+
end)
870+
else
871+
spawn(fn ->
872+
{:ok, _} = Application.ensure_all_started(:elixir)
873+
System.wait_until_booted()
874+
:ok = :io.setopts(binary: true, encoding: :unicode)
875+
{:ok, _} = Application.ensure_all_started(:iex)
876+
_ = for fun <- Enum.reverse(after_spawn()), do: fun.()
877+
IEx.Server.run_from_shell(opts, mfa)
878+
end)
879+
end
865880
end
866881

882+
# Manual tests for changing the CLI boot.
883+
#
884+
# 1. In some situations, we cannot read inputs as IEx boots:
885+
#
886+
# $ iex -e ":io.get_line(:foo)"
887+
#
888+
# 2. In some situations, connecting to a remote node via --remsh
889+
# is not possible. This can be tested by starting two IEx nodes:
890+
#
891+
# $ iex --sname foo
892+
# $ iex --sname bar --remsh foo
893+
#
894+
# 3. When still using --remsh, we need to guarantee the arguments
895+
# are processed on the local node and not the remote one. For such,
896+
# one can replace the last line above by:
897+
#
898+
# $ iex --sname bar --remsh foo -e 'IO.inspect node()'
899+
#
900+
# And verify that the local node name is printed.
901+
#
902+
# 4. Finally, in some other circumstances, printing messages may become
903+
# borked. This can be verified with:
904+
#
905+
# $ iex -e ":logger.info('foo~nbar', [])"
906+
#
907+
# By the time those instructions have been written, all tests above pass.
867908
@doc false
868-
def dont_display_result, do: :"do not show this result in output"
909+
def cli(opts \\ []) do
910+
args = :init.get_plain_arguments()
911+
opts = opts ++ [dot_iex_path: find_dot_iex(args), on_eof: :halt]
912+
913+
ref = make_ref()
914+
mfa = {__MODULE__, :__cli__, [self(), ref, opts]}
915+
916+
shell =
917+
if remote = get_remsh(args) do
918+
{:remote, remote, mfa}
919+
else
920+
mfa
921+
end
922+
923+
:ok = :shell.start_interactive(shell)
924+
925+
receive do
926+
{^ref, shell} -> shell
927+
after
928+
15_000 ->
929+
IO.puts(:stderr, "Could not start the shell after 15 seconds, aborting...")
930+
System.halt(1)
931+
end
932+
end
933+
934+
@doc false
935+
def __cli__(parent, ref, opts) do
936+
pid = start(opts)
937+
send(parent, {ref, pid})
938+
pid
939+
end
940+
941+
defp find_dot_iex([~c"--dot-iex", h | _]), do: List.to_string(h)
942+
defp find_dot_iex([_ | t]), do: find_dot_iex(t)
943+
defp find_dot_iex([]), do: nil
944+
945+
defp get_remsh([~c"--remsh", h | _]), do: h
946+
defp get_remsh([_ | t]), do: get_remsh(t)
947+
defp get_remsh([]), do: nil
869948
end

lib/iex/lib/iex/broker.ex

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule IEx.Broker do
1313
Finds the IEx server.
1414
"""
1515
@spec shell :: shell()
16-
# TODO: Use shell:whereis_shell() from Erlang/OTP 26+.
16+
# TODO: Use shell:whereis() from Erlang/OTP 26+.
1717
def shell() do
1818
if user = Process.whereis(:user) do
1919
if user_drv = get_from_dict(user, :user_drv) do
@@ -75,19 +75,50 @@ defmodule IEx.Broker do
7575
GenServer.call(broker_pid, {:refuse, take_ref})
7676
end
7777

78+
@doc """
79+
Asks to IO if we want to take over.
80+
"""
81+
def take_over?(location, whereami, opts) do
82+
evaluator = opts[:evaluator] || self()
83+
message = "Request to pry #{inspect(evaluator)} at #{location}#{whereami}"
84+
interrupt = IEx.color(:eval_interrupt, "#{message}\nAllow? [Yn] ")
85+
yes?(IO.gets(:stdio, interrupt))
86+
end
87+
88+
defp yes?(string) do
89+
is_binary(string) and String.trim(string) in ["", "y", "Y", "yes", "YES", "Yes"]
90+
end
91+
7892
@doc """
7993
Client requests a takeover.
8094
"""
8195
@spec take_over(binary, iodata, keyword) ::
8296
{:ok, server :: pid, group_leader :: pid, counter :: integer}
8397
| {:error, :no_iex | :refused}
8498
def take_over(location, whereami, opts) do
85-
case GenServer.whereis(@name) do
86-
nil ->
87-
{:error, :no_iex}
99+
case take_over_existing(location, whereami, opts) do
100+
{:error, :no_iex} ->
101+
cond do
102+
# TODO: Remove this check on Erlang/OTP 26+ and {:error, :no_iex}
103+
not Code.ensure_loaded?(:prim_tty) ->
104+
{:error, :no_iex}
105+
106+
take_over?(location, whereami, opts) ->
107+
{:ok, IEx.cli(opts), Process.group_leader(), 1}
108+
109+
true ->
110+
{:error, :refused}
111+
end
112+
113+
other ->
114+
other
115+
end
116+
end
88117

89-
_pid ->
90-
GenServer.call(@name, {:take_over, location, whereami, opts}, :infinity)
118+
defp take_over_existing(location, whereami, opts) do
119+
case GenServer.whereis(@name) do
120+
nil -> {:error, :no_iex}
121+
_pid -> GenServer.call(@name, {:take_over, location, whereami, opts}, :infinity)
91122
end
92123
end
93124

lib/iex/lib/iex/cli.ex

Lines changed: 16 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,26 @@
1-
# IEx sets the Erlang user to be IEx.CLI via the command line.
2-
# While this works most of the time, there are some problems
3-
# that may happen depending on how the booting process goes.
4-
# Those problems need to be manually tested in case changes are
5-
# done to this file.
6-
#
7-
# 1. In some situations, printing something before the shell
8-
# starts becomes very slow. To verify this feature, just
9-
# get an existing project and run:
10-
#
11-
# $ mix clean
12-
# $ iex -S mix
13-
#
14-
# If the printing of data is slower than usual. This particular
15-
# bug has arisen;
16-
#
17-
# 2. In some situations, connecting to a remote node via --remsh
18-
# is not possible. This can be tested by starting two IEx nodes:
19-
#
20-
# $ iex --sname foo
21-
# $ iex --sname bar --remsh foo@localhost
22-
#
23-
# 3. When still using --remsh, we need to guarantee the arguments
24-
# are processed on the local node and not the remote one. For such,
25-
# one can replace the last line above by:
26-
#
27-
# $ iex --sname bar --remsh foo@localhost -e 'IO.inspect node()'
28-
#
29-
# And verify that the local node name is printed.
30-
#
31-
# 4. Finally, in some other circumstances, printing messages may become
32-
# borked. This can be verified with:
33-
#
34-
# $ iex -e ":logger.info('foo~nbar', [])"
35-
#
36-
# By the time those instructions have been written, all tests above pass.
1+
# Remove this whole module on Erlang/OTP 26+.
372
defmodule IEx.CLI do
383
@moduledoc false
394

405
@compile {:no_warn_undefined, {:user, :start, 0}}
416

42-
def main do
43-
# TODO: Keep only the first branch and remove the usage
44-
# of -user callback altogether on Erlang/OTP 26+ by using
45-
# --eval and :shell.start_interactive(new_tty_args())
46-
cond do
47-
Code.ensure_loaded?(:prim_tty) ->
48-
:user_drv.start(%{initial_shell: new_tty_args()})
49-
50-
tty_works?() ->
51-
:user_drv.start([:"tty_sl -c -e", old_tty_args()])
52-
53-
true ->
54-
if get_remsh(:init.get_plain_arguments()) do
55-
IO.puts(
56-
:stderr,
57-
"warning: the --remsh option will be ignored because IEx is running on limited shell"
58-
)
59-
end
7+
def deprecated do
8+
if tty_works?() do
9+
:user_drv.start([:"tty_sl -c -e", old_tty_args()])
10+
else
11+
if get_remsh(:init.get_plain_arguments()) do
12+
IO.puts(
13+
:stderr,
14+
"warning: the --remsh option will be ignored because IEx is running on limited shell"
15+
)
16+
end
6017

61-
:user.start()
18+
:user.start()
6219

63-
# IEx.Broker is capable of considering all groups under user_drv but
64-
# when we use :user.start(), we need to explicitly register it instead.
65-
# If we don't register, pry doesn't work.
66-
IEx.start([register: true] ++ options())
20+
# IEx.Broker is capable of considering all groups under user_drv but
21+
# when we use :user.start(), we need to explicitly register it instead.
22+
# If we don't register, pry doesn't work.
23+
IEx.start([register: true] ++ options())
6724
end
6825
end
6926

@@ -84,14 +41,6 @@ defmodule IEx.CLI do
8441
end
8542
end
8643

87-
defp new_tty_args do
88-
if remote = get_remsh(:init.get_plain_arguments()) do
89-
{:remote, remote, remote_start_mfa()}
90-
else
91-
local_start_mfa()
92-
end
93-
end
94-
9544
defp old_tty_args do
9645
if remote = get_remsh(:init.get_plain_arguments()) do
9746
remote = List.to_atom(append_hostname(remote))

lib/iex/lib/iex/server.ex

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ defmodule IEx.Server do
2727
* `:env` - the `Macro.Env` used for the evaluator
2828
* `:binding` - an initial set of variables for the evaluator
2929
* `:on_eof` - if it should `:stop_evaluator` (default) or `:halt` the system
30+
* `:register` - if this shell should be registered in the broker (default is `true`)
3031
3132
"""
3233
@doc since: "1.8.0"
3334
@spec run(keyword) :: :ok
3435
def run(opts) when is_list(opts) do
35-
IEx.Broker.register(self())
36+
if Keyword.get(opts, :register, true) do
37+
IEx.Broker.register(self())
38+
end
39+
3640
run_without_registration(init_state(opts), opts, nil)
3741
end
3842

@@ -289,10 +293,8 @@ defmodule IEx.Server do
289293
end
290294

291295
defp take_over?(take_pid, take_ref, take_location, take_whereami, take_opts, counter) do
292-
evaluator = take_opts[:evaluator]
293-
message = "Request to pry #{inspect(evaluator)} at #{take_location}#{take_whereami}"
294-
interrupt = IEx.color(:eval_interrupt, "#{message}\nAllow? [Yn] ")
295-
take_over?(take_pid, take_ref, counter, yes?(IO.gets(:stdio, interrupt)))
296+
answer = IEx.Broker.take_over?(take_location, take_whereami, take_opts)
297+
take_over?(take_pid, take_ref, counter, answer)
296298
end
297299

298300
defp take_over?(take_pid, take_ref, counter, response) when is_boolean(response) do
@@ -309,10 +311,6 @@ defmodule IEx.Server do
309311
end
310312
end
311313

312-
defp yes?(string) do
313-
is_binary(string) and String.trim(string) in ["", "y", "Y", "yes", "YES", "Yes"]
314-
end
315-
316314
## State
317315

318316
defp init_state(opts) do

lib/iex/test/iex/server_test.exs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@ defmodule IEx.ServerTest do
1818
end
1919

2020
describe "pry" do
21-
test "no sessions" do
22-
assert capture_io(fn ->
23-
assert IEx.pry() == {:error, :no_iex}
24-
end) =~ "Is an IEx shell running?"
25-
end
26-
2721
test "inside evaluator itself" do
2822
assert capture_iex("require IEx; IEx.pry()") =~ "Break reached"
2923
end

0 commit comments

Comments
 (0)