Skip to content

Commit f6d030c

Browse files
committed
kernel,stdlib: Add signals option to shell:start_interactive
shell:start_interactive/1 can now take {noshell, #{mode => raw, signals => false}} to disable the terminal driver's signal and flow control handling in raw mode, so that control bytes such as ctrl+o, ctrl+c and ctrl+s/ctrl+q are passed to the application as data instead of being intercepted. The option is passed through user_drv to prim_tty and can be switched on and off at runtime. On Windows, ENABLE_PROCESSED_INPUT is cleared when signals is false. This restores the prim_tty sig option that was dropped in the OTP 28 shell improvements.
1 parent c2548e6 commit f6d030c

5 files changed

Lines changed: 147 additions & 19 deletions

File tree

erts/emulator/nifs/common/prim_tty_nif.c

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ static ERL_NIF_TERM tty_create_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM
10731073

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

1076-
ERL_NIF_TERM input;
1076+
ERL_NIF_TERM input, signals;
10771077
TTYResource *tty;
10781078

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

1091+
/* Whether signal and flow-control handling should stay enabled in
1092+
raw mode. Defaults to true to preserve the shell's ctrl+c break
1093+
behavior. When false, ISIG, IEXTEN and IXON are disabled so that
1094+
control bytes such as ctrl+o, ctrl+c and ctrl+s/ctrl+q reach the
1095+
application. */
1096+
if (!enif_get_map_value(env, argv[1], enif_make_atom(env, "signals"), &signals))
1097+
signals = atom_true;
1098+
10911099
if (tty->tty == unavailable) {
10921100
if (enif_is_identical(input, atom_raw))
10931101
return make_enotsup(env);
@@ -1126,6 +1134,15 @@ static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar
11261134
/* erts_fprintf(stderr,"echo %T\r\n", echo); */
11271135
tty->tty_smode.c_lflag &= ~ECHO;
11281136

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

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

11481170
if (tty->ifd != INVALID_HANDLE_VALUE && !SetConsoleMode(tty->ifd, dwInMode))

lib/kernel/src/prim_tty.erl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@
166166

167167
-type options() :: #{ input := cooked | raw | disabled,
168168
output := raw | cooked,
169-
ofd => stdout | stderr }.
169+
ofd => stdout | stderr,
170+
signals => boolean() }.
170171
-type request() ::
171172
{putc_raw, binary()} |
172173
{putc, unicode:unicode_binary()} |
@@ -326,7 +327,8 @@ reinit(State = #state{ options = OldOptions }, UserOptions) ->
326327
end.
327328

328329
options(UserOptions) ->
329-
maps:merge(#{ input => raw, output => cooked, ofd => stdout }, UserOptions).
330+
maps:merge(#{ input => raw, output => cooked, ofd => stdout,
331+
signals => true }, UserOptions).
330332

331333
init(State, ssh) ->
332334
State#state{ xn = true };

lib/kernel/src/user_drv.erl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@
125125
-type arguments() ::
126126
#{ initial_shell => noshell | shell() |
127127
{remote, unicode:charlist()} | {remote, unicode:charlist(), {module(), atom(), [term()]}},
128-
input => cooked | raw | disabled }.
128+
input => cooked | raw | disabled,
129+
signals => boolean() }.
129130

130131
%% Default line editing shell
131132
-spec start() -> pid().
@@ -197,7 +198,8 @@ init(Args) ->
197198
{next_event, internal, TTYState}};
198199
true ->
199200
TTYState = prim_tty:init(
200-
#{ input => maps:get(input, Args), output => raw }),
201+
#{ input => maps:get(input, Args), output => raw,
202+
signals => maps:get(signals, Args, true) }),
201203
init_standard_error(TTYState, false),
202204
{ok, init, {Args,#state{ terminal_mode = maps:get(input, Args), user = start_user() } },
203205
{next_event, internal, TTYState}}
@@ -405,7 +407,8 @@ server({call, From}, {start_shell, Args},
405407
State#state{
406408
terminal_mode = Input,
407409
tty = prim_tty:reinit(TTY, #{ input => Input,
408-
output => raw }),
410+
output => raw,
411+
signals => maps:get(signals, Args, true) }),
409412
shell_started = false }
410413
end
411414
end

lib/kernel/test/interactive_shell_SUITE.erl

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
init_per_testcase/2, end_per_testcase/2,
4444
get_columns_and_rows/1, exit_initial/1, job_control_local/1,
4545
job_control_remote/1,stop_during_init/1,wrap/1,
46-
noshell_raw/1,
46+
noshell_raw/1, raw_signals/1,
4747
shell_history/1, shell_history_resize/1, shell_history_eaccess/1,
4848
shell_history_repair/1, shell_history_repair_corrupt/1,
4949
shell_history_corrupt/1,
@@ -101,6 +101,7 @@ groups() ->
101101
shell_invalid_ansi,
102102
shell_get_password,
103103
noshell_raw,
104+
raw_signals,
104105
{group, shell_history},
105106
{group, remsh}]},
106107
{shell_history, [],
@@ -2136,6 +2137,85 @@ noshell_raw(Config) ->
21362137
end,
21372138
ok.
21382139

2140+
%% Verify that the signals option of shell:start_interactive can be
2141+
%% switched on and off at runtime. ctrl+o is VDISCARD on POSIX: while
2142+
%% IEXTEN is enabled the terminal driver swallows the byte, so with
2143+
%% signals => true the application never sees it. Uses a noshell peer
2144+
%% on the run_erl pty so that the line discipline is the only place
2145+
%% where the byte can be consumed.
2146+
raw_signals(Config) ->
2147+
case proplists:get_value(default_shell, Config) of
2148+
new ->
2149+
TCGl = group_leader(),
2150+
TC = self(),
2151+
2152+
TestcaseFun = fun() ->
2153+
link(TC),
2154+
group_leader(whereis(user), self()),
2155+
2156+
try
2157+
%% "\el" is an artifact from attaching to_erl and
2158+
%% is consumed while the terminal is still cooked.
2159+
"\el" ++ "hello\n" = io:get_line("1> "),
2160+
2161+
%% Raw mode with signals disabled: ctrl+o is
2162+
%% delivered to the application.
2163+
ok = shell:start_interactive(
2164+
{noshell, #{mode => raw, signals => false}}),
2165+
[$\^o] = io:get_chars("2> ", 1),
2166+
2167+
%% Switch signals back on: ctrl+o is now consumed
2168+
%% by the terminal driver, so the next byte typed
2169+
%% is the first one the application sees.
2170+
ok = shell:start_interactive(
2171+
{noshell, #{mode => raw, signals => true}}),
2172+
Parent = self(),
2173+
spawn(fun() ->
2174+
Parent ! {res, io:get_chars("3> ", 1)}
2175+
end),
2176+
{res, "x"} =
2177+
receive {res, Res} -> {res, Res}
2178+
after 5000 -> timeout
2179+
end,
2180+
2181+
%% Switch signals off again: ctrl+o reaches the
2182+
%% application once more.
2183+
ok = shell:start_interactive(
2184+
{noshell, #{mode => raw, signals => false}}),
2185+
[$\^o] = io:get_chars("4> ", 1),
2186+
2187+
io:format("exit")
2188+
catch E:R:ST ->
2189+
io:format(TCGl, "~p", [{E, R, ST}])
2190+
end
2191+
end,
2192+
2193+
rtnode:run(
2194+
[{eval, fun() -> spawn(TestcaseFun), ok end},
2195+
2196+
%% Cooked mode: the to_erl attach artifact is drained
2197+
{expect, "1> $"},
2198+
{putline, "hello"},
2199+
2200+
%% Raw mode with signals disabled
2201+
{expect, "2> $"},
2202+
{putdata, [$\^o]},
2203+
2204+
%% Signals enabled: ctrl+o is swallowed, "x" arrives
2205+
{expect, "3> $"},
2206+
{putdata, [$\^o]},
2207+
{putdata, "x"},
2208+
2209+
%% Signals disabled again
2210+
{expect, "4> $"},
2211+
{putdata, [$\^o]},
2212+
2213+
{expect, "exit$"}
2214+
], [], [],
2215+
["-noshell","-pz",filename:dirname(code:which(?MODULE))]);
2216+
_ -> ok
2217+
end.
2218+
21392219
get_until(start, NewChars) ->
21402220
get_until([], NewChars);
21412221
get_until(State, NewChars) ->

lib/stdlib/src/shell.erl

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,21 +89,30 @@ or when [`erl`](`e:erts:erl_cmd.md`) is started with the
8989
[`-noshell`](`e:erts:erl_cmd.md#noshell`) flags. The following options are
9090
allowed:
9191

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

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

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

105-
When in `cooked` mode the OS will handle the line editing and all data is
106-
passed to `t:io:user/0` when a newline is entered.
103+
When in `raw` mode all key presses are passed to `t:io:user/0` as
104+
they are typed when they are typed and the characters are not echoed
105+
to the terminal. It is possible to set the `echo` to `true` using
106+
`io:setopts/2` to enabling echoing again.
107+
108+
When in `cooked` mode the OS will handle the line editing and all
109+
data is passed to `t:io:user/0` when a newline is entered.
110+
111+
- **signals** - Whether the terminal driver should handle signals and
112+
flow control. When `false`, control bytes such as `ctrl+c`, `ctrl+o`
113+
and `ctrl+s`/`ctrl+q` are passed to `t:io:user/0` as data instead of
114+
being intercepted by the terminal driver. Defaults to `true`. Only
115+
has an effect in `raw` mode.
107116

108117
- **[mfa()](`t:erlang:mfa/0`)** - Starts the interactive shell using
109118
[`mfa()`](`t:erlang:mfa/0`) as the default shell. The `t:mfa/0` should
@@ -136,7 +145,9 @@ On error this function will return:
136145
description of the error reasons.
137146
""".
138147
-doc(#{since => <<"OTP 26.0">>}).
139-
-spec start_interactive(noshell | {noshell, raw | cooked} | {module(), atom(), [term()]}) ->
148+
-spec start_interactive(noshell | {noshell, raw | cooked |
149+
#{mode => raw | cooked, signals => boolean()}} |
150+
{module(), atom(), [term()]}) ->
140151
ok | {error, already_started};
141152
({remote, string()}) ->
142153
ok | {error, already_started | noconnection};
@@ -148,6 +159,16 @@ start_interactive(noshell) ->
148159
start_interactive({noshell, cooked});
149160
start_interactive({noshell, Type}) when Type =:= raw; Type =:= cooked ->
150161
user_drv:start_shell(#{ initial_shell => noshell, input => Type });
162+
start_interactive({noshell, #{mode := Mode} = Options}) when Mode =:= raw; Mode =:= cooked ->
163+
case maps:get(signals, Options, true) of
164+
Signals when is_boolean(Signals) ->
165+
user_drv:start_shell(#{ initial_shell => noshell, input => Mode,
166+
signals => Signals });
167+
_ ->
168+
erlang:error(function_clause, [{noshell, Options}])
169+
end;
170+
start_interactive({noshell, Arg}) ->
171+
erlang:error(function_clause, [{noshell, Arg}]);
151172
start_interactive(InitialShell) ->
152173
user_drv:start_shell(#{ initial_shell => InitialShell }).
153174

0 commit comments

Comments
 (0)