Skip to content

Commit

Permalink
Merge pull request erlang#7627 from frazze-jobb/frazze/ssh/dumb_terminal
Browse files Browse the repository at this point in the history
ssh: support dumb terminals in ssh client
OTP-18861
  • Loading branch information
frazze-jobb authored Nov 16, 2023
2 parents 902090b + d5a5199 commit a9d4045
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 39 deletions.
60 changes: 37 additions & 23 deletions lib/kernel/src/group.erl
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ server(Ancestors, Drv, Shell, Options) ->
put(user_drv, Drv),
ExpandFun = normalize_expand_fun(Options, fun edlin_expand:expand/2),
put(expand_fun, ExpandFun),
put(echo, proplists:get_value(echo, Options, true)),
Echo = proplists:get_value(echo, Options, true),
put(echo, Echo),
Dumb = proplists:get_value(dumb, Options, false),
put(dumb, Dumb),
put(expand_below, proplists:get_value(expand_below, Options, true)),

server_loop(Drv, start_shell(Shell), []).
Expand Down Expand Up @@ -490,7 +493,7 @@ get_chars_line(Prompt, M, F, Xa, Drv, Shell, Buf, Encoding) ->
get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, start, [], Encoding).

get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf0, State, LineCont0, Encoding) ->
Result = case get(echo) of
Result = case not(get(dumb)) andalso get(echo) of
true ->
get_line(Buf0, Pbs, LineCont0, Drv, Shell, Encoding);
false ->
Expand All @@ -513,7 +516,7 @@ get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf0, State, LineCont0, Encoding) ->
get_chars_apply(Pbs, M, F, Xa, Drv, Shell, Buf, State0, LineCont, Encoding) ->
%% multi line support means that we should not keep the state
%% but we need to keep it for oldshell mode
{State, Line} = case get(echo) of
{State, Line} = case not(get(dumb)) andalso get(echo) of
true -> {start, edlin:current_line(LineCont)};
false -> {State0, LineCont}
end,
Expand Down Expand Up @@ -836,19 +839,23 @@ get_line_echo_off(Chars, ToEnc, Pbs, Drv, Shell) ->
Res
end.

get_line_echo_off1({Chars,[]}, Drv, Shell) ->
get_line_echo_off1({Chars,[],Rs}, Drv, Shell) ->
case get(echo) of
true -> send_drv_reqs(Drv, Rs);
false -> skip
end,
receive
{Drv,{data,Cs}} ->
get_line_echo_off1(edit_line(cast(Cs, list), Chars), Drv, Shell);
{Drv,eof} ->
get_line_echo_off1(edit_line(eof, Chars), Drv, Shell);
{io_request,From,ReplyAs,Req} when is_pid(From) ->
io_request(Req, From, ReplyAs, Drv, Shell, []),
get_line_echo_off1({Chars,[]}, Drv, Shell);
get_line_echo_off1({Chars,[],[]}, Drv, Shell);
{reply,{From,ReplyAs},Reply} when From =/= undefined ->
%% We take care of replies from puts here as well
io_reply(From, ReplyAs, Reply),
get_line_echo_off1({Chars,[]},Drv, Shell);
get_line_echo_off1({Chars,[],[]},Drv, Shell);
{'EXIT',Drv,interrupt} ->
interrupted;
{'EXIT',Drv,_} ->
Expand All @@ -858,9 +865,12 @@ get_line_echo_off1({Chars,[]}, Drv, Shell) ->
end;
get_line_echo_off1(eof, _Drv, _Shell) ->
{done,eof,eof};
get_line_echo_off1({Chars,Rest}, _Drv, _Shell) ->
get_line_echo_off1({Chars,Rest,Rs}, Drv, _Shell) ->
case get(echo) of
true -> send_drv_reqs(Drv, Rs);
false -> skip
end,
{done,lists:reverse(Chars),case Rest of done -> []; _ -> Rest end}.

get_chars_echo_off(Pbs, Drv, Shell) ->
send_drv_reqs(Drv, [{insert_chars, unicode,Pbs}]),
get_chars_echo_off1(Drv, Shell).
Expand Down Expand Up @@ -895,22 +905,26 @@ get_chars_echo_off1(Drv, Shell) ->
%% - ^d in posix/icanon mode: eof, delete-forward in edlin
%% - ^r in posix/icanon mode: reprint (silly in echo-off mode :-))
%% - ^w in posix/icanon mode: word-erase (produces a beep in edlin)
edit_line(eof, []) ->
edit_line(Input, State) ->
edit_line(Input, State, []).
edit_line(eof, [], _) ->
eof;
edit_line(eof, Chars) ->
{Chars,eof};
edit_line([],Chars) ->
{Chars,[]};
edit_line([$\r,$\n|Cs],Chars) ->
{[$\n | Chars], remainder_after_nl(Cs)};
edit_line([NL|Cs],Chars) when NL =:= $\r; NL =:= $\n ->
{[$\n | Chars], remainder_after_nl(Cs)};
edit_line([Erase|Cs],[]) when Erase =:= $\177; Erase =:= $\^H ->
edit_line(Cs,[]);
edit_line([Erase|Cs],[_|Chars]) when Erase =:= $\177; Erase =:= $\^H ->
edit_line(Cs,Chars);
edit_line([Char|Cs],Chars) ->
edit_line(Cs,[Char|Chars]).
edit_line(eof, Chars, Rs) ->
{Chars,eof, lists:reverse(Rs)};
edit_line([],Chars, Rs) ->
{Chars,[],lists:reverse(Rs)};
edit_line([$\r,$\n|Cs],Chars, Rs) ->
{[$\n | Chars], remainder_after_nl(Cs), lists:reverse([{put_chars, unicode, "\n"}|Rs])};
edit_line([NL|Cs],Chars, Rs) when NL =:= $\r; NL =:= $\n ->
{[$\n | Chars], remainder_after_nl(Cs), lists:reverse([{put_chars, unicode, "\n"}|Rs])};
edit_line([Erase|Cs],[], Rs) when Erase =:= $\177; Erase =:= $\^H ->
edit_line(Cs,[], Rs);
edit_line([Erase|Cs],[_|Chars], Rs) when Erase =:= $\177; Erase =:= $\^H ->
edit_line(Cs,Chars, [{delete_chars, -1}|Rs]);
edit_line([CtrlChar|Cs],Chars, Rs) when CtrlChar < 32 ->
edit_line(Cs,Chars,Rs);
edit_line([Char|Cs],Chars, Rs) ->
edit_line(Cs,[Char|Chars], [{put_chars, unicode, [Char]}|Rs]).

remainder_after_nl("") -> done;
remainder_after_nl(Cs) -> Cs.
Expand Down
51 changes: 36 additions & 15 deletions lib/ssh/src/ssh_cli.erl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ handle_ssh_msg({ssh_cm, _ConnectionHandler,
#state{group = Group} = State0) ->
{Enc, State} = guess_encoding(Data, State0),
List = unicode:characters_to_list(Data, Enc),
to_group(List, Group),
to_group(List, Group, get_dumb(State#state.pty)),
{ok, State};

handle_ssh_msg({ssh_cm, ConnectionHandler,
Expand Down Expand Up @@ -393,21 +393,28 @@ out_enc(#state{encoding = PeerEnc,

%%--------------------------------------------------------------------

to_group([], _Group) ->
to_group([], _Group, _Dumb) ->
ok;
to_group([$\^C | Tail], Group) ->
to_group([$\^C | Tail], Group, Dumb) ->
exit(Group, interrupt),
to_group(Tail, Group);
to_group(Data, Group) ->
to_group(Tail, Group, Dumb);
to_group(Data, Group, Dumb) ->
Func = fun(C) -> C /= $\^C end,
Tail = case lists:splitwith(Func, Data) of
{[], Right} ->
Right;
{Left, Right} ->
Group ! {self(), {data, Left}},
%% Filter out escape sequences, only support Ctrl sequences
Left1 = if Dumb -> replace_escapes(Left); true -> Left end,
Group ! {self(), {data, Left1}},
Right
end,
to_group(Tail, Group).
to_group(Tail, Group, Dumb).
replace_escapes(Data) ->
lists:flatten([ if C =:= 27 ->
[$^,C+64];
true -> C
end || C <- Data]).

%%--------------------------------------------------------------------
%%% io_request, handle io requests from the user process,
Expand Down Expand Up @@ -511,10 +518,10 @@ get_tty_command(left, N, _TerminalType) ->
-define(TABWIDTH, 8).

%% convert input characters to buffer and to writeout
%% Note that the buf is reversed but the buftail is not
%% Note that Bef is reversed but Aft is not
%% (this is handy; the head is always next to the cursor)
conv_buf([], {LB, {Bef, Aft}, LA, Col}, AccWrite, _Tty) ->
{{LB, {Bef, Aft}, LA}, lists:reverse(AccWrite), Col};
{{LB, {Bef, Aft}, LA, Col}, lists:reverse(AccWrite)};
conv_buf([13, 10 | Rest], {LB, {Bef, Aft}, LA, Col}, AccWrite, Tty = #ssh_pty{width = W}) ->
conv_buf(Rest, {[lists:reverse(Bef)|LB], {[], tl2(Aft)}, LA, Col+(W-(Col rem W))}, [10, 13 | AccWrite], Tty);
conv_buf([13 | Rest], {LB, {Bef, Aft}, LA, Col}, AccWrite, Tty = #ssh_pty{width = W}) ->
Expand All @@ -532,21 +539,28 @@ conv_buf([C | Rest], {LB, {Bef, Aft}, LA, Col}, AccWrite, Tty) ->

%%% put characters before the prompt
put_chars(Chars, Buf, Tty) ->
Dumb = get_dumb(Tty),
case Buf of
{[],{[],[]},[],_} -> {_, WriteBuf, _} = conv_buf(Chars, Buf, [], Tty),
{[],{[],[]},[],_} -> {_, WriteBuf} = conv_buf(Chars, Buf, [], Tty),
{WriteBuf, Buf};
_ ->
_ when Dumb =:= false ->
{Delete, DeletedState} = io_request(delete_line, Buf, Tty, []),
{_, PutBuffer, _} = conv_buf(Chars, DeletedState, [], Tty),
{_, PutBuffer} = conv_buf(Chars, DeletedState, [], Tty),
{Redraw, _} = io_request(redraw_prompt_pre_deleted, Buf, Tty, []),
{[Delete, PutBuffer, Redraw], Buf}
{[Delete, PutBuffer, Redraw], Buf};
_ ->
%% When we have a dumb terminal, we get messages via put_chars requests
%% so state should be empty {[],{[],[]},[],_},
%% but if we end up here its not, so keep the state
{_, WriteBuf} = conv_buf(Chars, Buf, [], Tty),
{WriteBuf, Buf}
end.

%%% insert character at current position
insert_chars([], Buf, _Tty) ->
{[], Buf};
insert_chars(Chars, {_LB,{_Bef, Aft},LA, _Col}=Buf, Tty) ->
{{NewLB, {NewBef, _NewAft}, _NewLA}, WriteBuf, NewCol} = conv_buf(Chars, Buf, [], Tty),
{{NewLB, {NewBef, _NewAft}, _NewLA, NewCol}, WriteBuf} = conv_buf(Chars, Buf, [], Tty),
M = move_cursor(special_at_width(NewCol+length(Aft), Tty), NewCol, Tty),
{[WriteBuf, Aft | M], {NewLB,{NewBef, Aft},LA, NewCol}}.

Expand Down Expand Up @@ -725,7 +739,7 @@ start_shell(ConnectionHandler, State) ->
Shell
end,
State#state{group = group:start(self(), ShellSpawner,
[{expand_below, false},
[{dumb, get_dumb(State#state.pty)},{expand_below, false},
{echo, get_echo(State#state.pty)}]),
buf = empty_buf()}.

Expand Down Expand Up @@ -842,6 +856,13 @@ t2str(T) -> try io_lib:format("~s",[T])
end.

%%--------------------------------------------------------------------
get_dumb(Tty) ->
try
Tty#ssh_pty.term =:= "dumb"
catch
_:_ -> false
end.

% Pty can be undefined if the client never sets any pty options before
% starting the shell.
get_echo(Tty) ->
Expand Down
2 changes: 1 addition & 1 deletion lib/stdlib/src/edlin.erl
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ start(Pbs) ->

%% Only two modes used: 'none' and 'search'. Other modes can be
%% handled inline through specific character handling.
start(Pbs, {_,{_,_},[]}=Cont) ->
start(Pbs, {_,{_,[]},[]}=Cont) ->
%% Skip redraw if the cursor is at the end.
{more_chars,{line,Pbs,Cont,{normal,none}},[{insert_chars,unicode,multi_line_prompt(Pbs)}]};

Expand Down

0 comments on commit a9d4045

Please sign in to comment.