Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion erts/emulator/nifs/common/prim_tty_nif.c
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ static ERL_NIF_TERM tty_create_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM

static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {

ERL_NIF_TERM input;
ERL_NIF_TERM input, signals;
TTYResource *tty;

debug("tty_init_nif(%T,%T)\r\n", argv[0], argv[1]);
Expand All @@ -1088,6 +1088,14 @@ static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar
if (!enif_get_map_value(env, argv[1], enif_make_atom(env, "input"), &input))
return enif_make_badarg(env);

/* Whether signal and flow-control handling should stay enabled in
raw mode. Defaults to true to preserve the shell's ctrl+c break
behavior. When false, ISIG, IEXTEN and IXON are disabled so that
control bytes such as ctrl+o, ctrl+c and ctrl+s/ctrl+q reach the
application. */
if (!enif_get_map_value(env, argv[1], enif_make_atom(env, "signals"), &signals))
signals = atom_true;

if (tty->tty == unavailable) {
if (enif_is_identical(input, atom_raw))
return make_enotsup(env);
Expand Down Expand Up @@ -1126,6 +1134,15 @@ static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar
/* erts_fprintf(stderr,"echo %T\r\n", echo); */
tty->tty_smode.c_lflag &= ~ECHO;

if (enif_is_identical(signals, atom_false)) {
/* Also disable signal and flow-control handling so that
control bytes are passed through to the application.
This restores the sig => false behavior that the
prim_tty API had before the OTP 28 shell improvements. */
tty->tty_smode.c_iflag &= ~(BRKINT|IGNPAR|IXON|IXANY);
tty->tty_smode.c_lflag &= ~(ISIG|IEXTEN);
}

}

if (tcsetattr(tty->ofd, TCSANOW, &tty->tty_smode) < 0) {
Expand All @@ -1143,6 +1160,11 @@ static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar
if (tty->tty == enabled) {
dwOutMode |= DISABLE_NEWLINE_AUTO_RETURN;
dwInMode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT);
if (enif_is_identical(signals, atom_false)) {
/* Pass ctrl+c through to the application instead of
generating a console event for it. */
dwInMode &= ~ENABLE_PROCESSED_INPUT;
}
}

if (tty->ifd != INVALID_HANDLE_VALUE && !SetConsoleMode(tty->ifd, dwInMode))
Expand Down
6 changes: 4 additions & 2 deletions lib/kernel/src/prim_tty.erl
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@

-type options() :: #{ input := cooked | raw | disabled,
output := raw | cooked,
ofd => stdout | stderr }.
ofd => stdout | stderr,
signals => boolean() }.
-type request() ::
{putc_raw, binary()} |
{putc, unicode:unicode_binary()} |
Expand Down Expand Up @@ -326,7 +327,8 @@ reinit(State = #state{ options = OldOptions }, UserOptions) ->
end.

options(UserOptions) ->
maps:merge(#{ input => raw, output => cooked, ofd => stdout }, UserOptions).
maps:merge(#{ input => raw, output => cooked, ofd => stdout,
signals => true }, UserOptions).

init(State, ssh) ->
State#state{ xn = true };
Expand Down
9 changes: 6 additions & 3 deletions lib/kernel/src/user_drv.erl
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@
-type arguments() ::
#{ initial_shell => noshell | shell() |
{remote, unicode:charlist()} | {remote, unicode:charlist(), {module(), atom(), [term()]}},
input => cooked | raw | disabled }.
input => cooked | raw | disabled,
signals => boolean() }.

%% Default line editing shell
-spec start() -> pid().
Expand Down Expand Up @@ -197,7 +198,8 @@ init(Args) ->
{next_event, internal, TTYState}};
true ->
TTYState = prim_tty:init(
#{ input => maps:get(input, Args), output => raw }),
#{ input => maps:get(input, Args), output => raw,
signals => maps:get(signals, Args, true) }),
init_standard_error(TTYState, false),
{ok, init, {Args,#state{ terminal_mode = maps:get(input, Args), user = start_user() } },
{next_event, internal, TTYState}}
Expand Down Expand Up @@ -405,7 +407,8 @@ server({call, From}, {start_shell, Args},
State#state{
terminal_mode = Input,
tty = prim_tty:reinit(TTY, #{ input => Input,
output => raw }),
output => raw,
signals => maps:get(signals, Args, true) }),
shell_started = false }
end
end
Expand Down
84 changes: 83 additions & 1 deletion lib/kernel/test/interactive_shell_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
init_per_testcase/2, end_per_testcase/2,
get_columns_and_rows/1, exit_initial/1, job_control_local/1,
job_control_remote/1,stop_during_init/1,wrap/1,
noshell_raw/1,
noshell_raw/1, raw_signals/1,
shell_history/1, shell_history_resize/1, shell_history_eaccess/1,
shell_history_repair/1, shell_history_repair_corrupt/1,
shell_history_corrupt/1,
Expand Down Expand Up @@ -101,6 +101,7 @@ groups() ->
shell_invalid_ansi,
shell_get_password,
noshell_raw,
raw_signals,
{group, shell_history},
{group, remsh}]},
{shell_history, [],
Expand Down Expand Up @@ -2136,6 +2137,87 @@ noshell_raw(Config) ->
end,
ok.

%% Verify that the signals option of shell:start_interactive can be
%% switched on and off at runtime. ctrl+s is XOFF flow control: while
%% IXON is enabled the terminal driver swallows the byte, so with
%% signals => true the application never sees it. Uses a noshell peer
%% on the run_erl pty so that the line discipline is the only place
%% where the byte can be consumed.
raw_signals(Config) ->
case proplists:get_value(default_shell, Config) of
new ->
TCGl = group_leader(),
TC = self(),

TestcaseFun = fun() ->
link(TC),
group_leader(whereis(user), self()),

try
%% "\el" is an artifact from attaching to_erl and
%% is consumed while the terminal is still cooked.
"\el" ++ "hello\n" = io:get_line("1> "),

%% Raw mode with signals disabled: ctrl+s is
%% delivered to the application.
ok = shell:start_interactive(
{noshell, #{mode => raw, signals => false}}),
[$\^s] = io:get_chars("2> ", 1),

%% Switch signals back on: ctrl+s is now consumed
%% by the terminal driver as XOFF flow control
%% (ctrl+q restarts the output), so the next byte
%% typed is the first one the application sees.
ok = shell:start_interactive(
{noshell, #{mode => raw, signals => true}}),
Parent = self(),
spawn(fun() ->
Parent ! {res, io:get_chars("3> ", 1)}
end),
{res, "x"} =
receive {res, Res} -> {res, Res}
after 5000 -> timeout
end,

%% Switch signals off again: ctrl+s reaches the
%% application once more.
ok = shell:start_interactive(
{noshell, #{mode => raw, signals => false}}),
[$\^s] = io:get_chars("4> ", 1),

io:format("exit")
catch E:R:ST ->
io:format(TCGl, "~p", [{E, R, ST}])
end
end,

rtnode:run(
[{eval, fun() -> spawn(TestcaseFun), ok end},

%% Cooked mode: the to_erl attach artifact is drained
{expect, "1> $"},
{putline, "hello"},

%% Raw mode with signals disabled
{expect, "2> $"},
{putdata, [$\^s]},

%% Signals enabled: ctrl+s and ctrl+q are swallowed, "x" arrives
{expect, "3> $"},
{putdata, [$\^s]},
{putdata, [$\^q]},
{putdata, "x"},

%% Signals disabled again
{expect, "4> $"},
{putdata, [$\^s]},

{expect, "exit$"}
], [], [],
["-noshell","-pz",filename:dirname(code:which(?MODULE))]);
_ -> ok
end.

get_until(start, NewChars) ->
get_until([], NewChars);
get_until(State, NewChars) ->
Expand Down
45 changes: 33 additions & 12 deletions lib/stdlib/src/shell.erl
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,30 @@ or when [`erl`](`e:erts:erl_cmd.md`) is started with the
[`-noshell`](`e:erts:erl_cmd.md#noshell`) flags. The following options are
allowed:

- **noshell | {noshell, Mode}**{: #noshell_raw } - Starts the interactive shell
as if [`-noshell`](`e:erts:erl_cmd.md#noshell`) was given to
- **noshell | {noshell, Mode}**{: #noshell_raw } - Starts the interactive
shell as if [`-noshell`](`e:erts:erl_cmd.md#noshell`) was given to
[`erl`](`e:erts:erl_cmd.md`).

It is possible to give a `Mode` indicating if the input should be set
in `cooked` or `raw` mode. `Mode` only has en effect if `t:io:user/0` is a tty.
If no `Mode` is given, it defaults is `cooked`.
`Mode` is either the atom `cooked` or `raw`, or a map with the
following keys:

When in `raw` mode all key presses are passed to `t:io:user/0` as they are
typed when they are typed and the characters are not echoed to the terminal.
It is possible to set the `echo` to `true` using `io:setopts/2` to enabling
echoing again.
- **mode** - `cooked` or `raw`. `mode` only has an effect if
`t:io:user/0` is a tty. If no `mode` is given, it defaults to
`cooked`.

When in `cooked` mode the OS will handle the line editing and all data is
passed to `t:io:user/0` when a newline is entered.
When in `raw` mode all key presses are passed to `t:io:user/0` as
they are typed when they are typed and the characters are not echoed
to the terminal. It is possible to set the `echo` to `true` using
`io:setopts/2` to enabling echoing again.

When in `cooked` mode the OS will handle the line editing and all
data is passed to `t:io:user/0` when a newline is entered.

- **signals** - Whether the terminal driver should handle signals and
flow control. When `false`, control bytes such as `ctrl+c`, `ctrl+o`
and `ctrl+s`/`ctrl+q` are passed to `t:io:user/0` as data instead of
being intercepted by the terminal driver. Defaults to `true`. Only
has an effect in `raw` mode.

- **[mfa()](`t:erlang:mfa/0`)** - Starts the interactive shell using
[`mfa()`](`t:erlang:mfa/0`) as the default shell. The `t:mfa/0` should
Expand Down Expand Up @@ -136,7 +145,9 @@ On error this function will return:
description of the error reasons.
""".
-doc(#{since => <<"OTP 26.0">>}).
-spec start_interactive(noshell | {noshell, raw | cooked} | {module(), atom(), [term()]}) ->
-spec start_interactive(noshell | {noshell, raw | cooked |
#{mode => raw | cooked, signals => boolean()}} |
{module(), atom(), [term()]}) ->
ok | {error, already_started};
({remote, string()}) ->
ok | {error, already_started | noconnection};
Expand All @@ -148,6 +159,16 @@ start_interactive(noshell) ->
start_interactive({noshell, cooked});
start_interactive({noshell, Type}) when Type =:= raw; Type =:= cooked ->
user_drv:start_shell(#{ initial_shell => noshell, input => Type });
start_interactive({noshell, #{mode := Mode} = Options}) when Mode =:= raw; Mode =:= cooked ->
case maps:get(signals, Options, true) of
Signals when is_boolean(Signals) ->
user_drv:start_shell(#{ initial_shell => noshell, input => Mode,
signals => Signals });
_ ->
erlang:error(function_clause, [{noshell, Options}])
end;
start_interactive({noshell, Arg}) ->
erlang:error(function_clause, [{noshell, Arg}]);
start_interactive(InitialShell) ->
user_drv:start_shell(#{ initial_shell => InitialShell }).

Expand Down
Loading