diff options
Diffstat (limited to 'lib/kernel/src/user_drv.erl')
-rw-r--r-- | lib/kernel/src/user_drv.erl | 1458 |
1 files changed, 864 insertions, 594 deletions
diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index fa7687bf2a..25ebcbdd68 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2021. All Rights Reserved. +%% Copyright Ericsson AB 1996-2023. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -19,591 +19,890 @@ %% -module(user_drv). -%% Basic interface to a port. - --export([start/0,start/1,start/2,start/3,server/2,server/3]). - --export([interfaces/1]). +%% Basic interface to stdin/stdout. +%% +%% This is responsible for a couple of things: +%% - Dispatching I/O messages when erl is running +%% The messages are listed in the type message/0. +%% - Any data received from the terminal is sent to the current group like this: +%% `{DrvPid :: pid(), {data, UnicodeCharacters :: list()}}` +%% - It serves as the job control manager (i.e. what happens when you type ^G) +%% - Starts potential -remsh sessions to other nodes +%% +-type message() :: + %% I/O requests that modify the terminal + {Sender :: pid(), request()} | + %% Query the server of the current dimensions of the terminal. + %% `Sender` will be sent the message: + %% `{DrvPid :: pid(), tty_geometry, {Width :: integer(), Height :: integer()}}` + {Sender :: pid(), tty_geometry} | + %% Query the server if it supports unicode characters + %% `Sender` will be sent the message: + %% `{DrvPid :: pid(), get_unicode_state, SupportUnicode :: boolean()}` + {Sender :: pid(), get_unicode_state} | + %% Change whether the server supports unicode characters or not. The reply + %% contains the previous unicode state. + %% `Sender` will be sent the message: + %% `{DrvPid :: pid(), set_unicode_state, SupportedUnicode :: boolean()}` + {Sender :: pid(), set_unicode_state, boolean()}. +-type request() :: + %% Put characters at current cursor position, + %% overwriting any characters it encounters. + {put_chars, unicode, binary()} | + %% Same as put_chars/3, but sends Reply to From when the characters are + %% guaranteed to have been written to the terminal + {put_chars_sync, unicode, binary(), {From :: pid(), Reply :: term()}} | + %% Put text in expansion area + {put_expand} | + {put_expand_no_trim} | + %% Move the cursor X characters left or right (negative is left) + {move_rel, -32768..32767} | + %% Move the cursor Y rows up or down (negative is up) + {move_line, -32768..32767} | + %% Move combo, helper to simplify some move operations + {move_combo, -32768..32767, -32768..32767, -32768..32767} | + %% Insert characters at current cursor position moving any + %% characters after the cursor. + {insert_chars, unicode, binary()} | + %% Delete X chars before or after the cursor adjusting any test remaining + %% to the right of the cursor. + {delete_chars, -32768..32767} | + %% Deletes the current prompt and expression + delete_line | + %% Delete after the cursor + delete_after_cursor | + %% Trigger a terminal "bell" + beep | + %% Clears the screen + clear | + %% Execute multiple request() actions + {requests, [request()]} | + %% Open external editor + {open_editor, string()} | + %% Redraws the current prompt and expression + redraw_prompt | + {redraw_prompt, string(), string(), tuple()} | + %% Clears the state, not touching the characters + new_prompt. + +-export_type([message/0]). +-export([start/0, start/1, start_shell/0, start_shell/1, whereis_group/0]). + +%% gen_statem state callbacks +-behaviour(gen_statem). +-export([init/3,server/3,switch_loop/3]). + +%% gen_statem callbacks +-export([init/1, callback_mode/0]). -include_lib("kernel/include/logger.hrl"). --define(OP_PUTC,0). --define(OP_MOVE,1). --define(OP_INSC,2). --define(OP_DELC,3). --define(OP_BEEP,4). --define(OP_PUTC_SYNC,5). -% Control op --define(ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER, 16#018b0900). --define(CTRL_OP_GET_WINSIZE, (100 + ?ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER)). --define(CTRL_OP_GET_UNICODE_STATE, (101 + ?ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER)). --define(CTRL_OP_SET_UNICODE_STATE, (102 + ?ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER)). - -%% start() -%% start(ArgumentList) -%% start(PortName, Shell) -%% start(InPortName, OutPortName, Shell) -%% Start the user driver server. The arguments to start/1 are slightly -%% strange as this may be called both at start up from the command line -%% and explicitly from other code. - +-record(editor, { port :: port(), file :: file:name(), requester :: pid() }). +-record(state, { tty :: prim_tty:state() | undefined, + write :: reference() | undefined, + read :: reference() | undefined, + shell_started = new :: new | old | false, + editor :: #editor{} | undefined, + user :: pid(), + current_group :: pid() | undefined, + groups, queue }). + +-type shell() :: {module(), atom(), [term()]} | {node(), module(), atom(), [term()]}. +-type arguments() :: #{ initial_shell => noshell | shell() | + {remote, unicode:charlist()} | {remote, unicode:charlist(), {module(), atom(), [term()]}}, + input => boolean() }. + +%% Default line editing shell -spec start() -> pid(). +start() -> + case init:get_argument(remsh) of + {ok,[[Node]]} -> + start(#{ initial_shell => {remote, Node} }); + {ok,[[Node]|_]} -> + ?LOG_WARNING("Multiple -remsh given to erl, using the first, ~p", [Node]), + start(#{ initial_shell => {remote, Node} }); + E when E =:= error ; E =:= {ok,[[]]} -> + start(#{ }) + end. -start() -> %Default line editing shell - spawn(user_drv, server, ['tty_sl -c -e',{shell,start,[init]}]). +-spec start_shell() -> ok | {error, Reason :: term()}. +start_shell() -> + start_shell(#{ }). +-spec start_shell(arguments()) -> ok | {error, already_started}. +start_shell(Args) -> + gen_statem:call(?MODULE, {start_shell, Args}). + +-spec whereis_group() -> pid() | undefined. +whereis_group() -> + {dictionary, Dict} = + erlang:process_info(whereis(?MODULE), dictionary), + proplists:get_value(current_group, Dict). + +%% Backwards compatibility with pre OTP-26 for Elixir/LFE etc +-spec start(['tty_sl -c -e'| shell()]) -> pid(); + (arguments()) -> pid(). +start(['tty_sl -c -e', Shell]) -> + start(#{ initial_shell => Shell }); +start(Args) when is_map(Args) -> + case gen_statem:start({local, ?MODULE}, ?MODULE, Args, []) of + {ok, Pid} -> Pid; + {error, Reason} -> + {error, Reason} + end. -start([Pname]) -> - spawn(user_drv, server, [Pname,{shell,start,[init]}]); -start([Pname|Args]) -> - spawn(user_drv, server, [Pname|Args]); -start(Pname) -> - spawn(user_drv, server, [Pname,{shell,start,[init]}]). +callback_mode() -> state_functions. -start(Pname, Shell) -> - spawn(user_drv, server, [Pname,Shell]). +-spec init(arguments()) -> gen_statem:init_result(init). +init(Args) -> + process_flag(trap_exit, true), -start(Iname, Oname, Shell) -> - spawn(user_drv, server, [Iname,Oname,Shell]). + IsTTY = prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true, + StartShell = maps:get(initial_shell, Args, undefined) =/= noshell, + OldShell = maps:get(initial_shell, Args, undefined) =:= oldshell, + try + if + not IsTTY andalso StartShell; OldShell -> + error(enotsup); + IsTTY, StartShell -> + TTYState = prim_tty:init(#{}), + init_standard_error(TTYState, true), + {ok, init, {Args, #state{ user = start_user() } }, + {next_event, internal, TTYState}}; + true -> + TTYState = prim_tty:init(#{input => maps:get(input, Args, true), + tty => false}), + init_standard_error(TTYState, false), + {ok, init, {Args,#state{ user = start_user() } }, + {next_event, internal, TTYState}} + end + catch error:enotsup -> + %% This is thrown by prim_tty:init when + %% it could not start the terminal, + %% probably because TERM=dumb was set. + %% + %% The oldshell mode is important as it is + %% the mode used when running erlang in an + %% emacs buffer. + CatchTTYState = prim_tty:init(#{tty => false}), + init_standard_error(CatchTTYState, false), + {ok, init, {Args,#state{ shell_started = old, user = start_user() } }, + {next_event, internal, CatchTTYState}} + end. +%% Initialize standard_error +init_standard_error(TTY, NewlineCarriageReturn) -> + Encoding = case prim_tty:unicode(TTY) of + true -> unicode; + false -> latin1 + end, + ok = io:setopts(standard_error, [{encoding, Encoding}, + {onlcr, NewlineCarriageReturn}]). + +init(internal, TTYState, {Args, State = #state{ user = User }}) -> + + %% Cleanup ancestors so that observer looks nice + put('$ancestors',[User|get('$ancestors')]), + + #{ read := ReadHandle, write := WriteHandle } = prim_tty:handles(TTYState), + + NewState = State#state{ tty = TTYState, + read = ReadHandle, write = WriteHandle, + user = User, queue = {false, queue:new()}, + groups = gr_add_cur(gr_new(), User, {}) + }, + + case Args of + #{ initial_shell := noshell } -> + init_noshell(NewState); + #{ initial_shell := {remote, Node} } -> + InitialShell = {shell,start,[]}, + exit_on_remote_shell_error( + Node, InitialShell, init_remote_shell(NewState, Node, InitialShell)); + #{ initial_shell := {remote, Node, InitialShell} } -> + exit_on_remote_shell_error( + Node, InitialShell, init_remote_shell(NewState, Node, InitialShell)); + #{ initial_shell := oldshell } -> + old = State#state.shell_started, + init_local_shell(NewState, {shell,start,[]}); + #{ initial_shell := InitialShell } -> + init_local_shell(NewState, InitialShell); + _ -> + init_local_shell(NewState, {shell,start,[init]}) + end. -%% Return the pid of the active group process. -%% Note: We can't ask the user_drv process for this info since it -%% may be busy waiting for data from the port. +exit_on_remote_shell_error(RemoteNode, _, {error, noconnection}) -> + io:format(standard_error, "Could not connect to ~p\n", [RemoteNode]), + erlang:halt(1); +exit_on_remote_shell_error(RemoteNode, {M, _, _}, {error, Reason}) -> + io:format(standard_error, "Could not load ~p on ~p (~p)\n", [RemoteNode, M, Reason]), + erlang:halt(1); +exit_on_remote_shell_error(_, _, Result) -> + Result. + +%% We have been started with -noshell. In this mode the current_group is +%% the `user` group process. +init_noshell(State) -> + init_shell(State#state{ shell_started = false }, ""). + +init_remote_shell(State, Node, {M, F, A}) -> + + case net_kernel:get_state() of + #{ started := no } -> + {ok, _} = net_kernel:start([undefined, shortnames]), + ok; + _ -> + ok + end, --spec interfaces(pid()) -> [{'current_group', pid()}]. + LocalNode = + case net_kernel:get_state() of + #{ name_type := dynamic } -> + net_kernel:nodename(); + #{ name_type := static } -> + node() + end, + + RemoteNode = + case string:find(Node,"@") of + nomatch -> + list_to_atom(Node ++ string:find(atom_to_list(LocalNode),"@")); + _ -> + list_to_atom(Node) + end, + + case net_kernel:connect_node(RemoteNode) of + true -> + + case erpc:call(RemoteNode, code, ensure_loaded, [M]) of + {error, Reason} when Reason =/= embedded -> + {error, Reason}; + _ -> + + %% Setup correct net tick times + case erpc:call(RemoteNode, net_kernel, get_net_ticktime, []) of + {ongoing_change_to, NetTickTime} -> + _ = net_kernel:set_net_ticktime(NetTickTime), + ok; + NetTickTime -> + _ = net_kernel:set_net_ticktime(NetTickTime), + ok + end, -interfaces(UserDrv) -> - case process_info(UserDrv, dictionary) of - {dictionary,Dict} -> - case lists:keysearch(current_group, 1, Dict) of - {value,Gr={_,Group}} when is_pid(Group) -> - [Gr]; - _ -> - [] - end; - _ -> - [] + RShell = {RemoteNode, M, F, A}, + + %% We fetch the shell slogan from the remote node + Slogan = + case erpc:call(RemoteNode, application, get_env, + [stdlib, shell_slogan, + erpc:call(RemoteNode, erlang, system_info, [system_version])]) of + Fun when is_function(Fun, 0) -> + erpc:call(RemoteNode, Fun); + SloganEnv -> + SloganEnv + end, + + Group = group:start(self(), RShell, + [{echo,State#state.shell_started =:= new}] ++ + group_opts(RemoteNode)), + + Gr = gr_add_cur(State#state.groups, Group, RShell), + + init_shell(State#state{ groups = Gr }, [Slogan,$\n]) + end; + false -> + {error, noconnection} end. -%% server(Pid, Shell) -%% server(Pname, Shell) -%% server(Iname, Oname, Shell) -%% The initial calls to run the user driver. These start the port(s) -%% then call server1/3 to set everything else up. - -server(Pid, Shell) when is_pid(Pid) -> - server1(Pid, Pid, Shell); -server(Pname, Shell) -> - process_flag(trap_exit, true), - case catch open_port({spawn,Pname}, [eof]) of - {'EXIT', _} -> - %% Let's try a dumb user instead - user:start(); - Port -> - server1(Port, Port, Shell) - end. +init_local_shell(State, InitialShell) -> + Slogan = + case application:get_env( + stdlib, shell_slogan, + fun() -> erlang:system_info(system_version) end) of + Fun when is_function(Fun, 0) -> + Fun(); + SloganEnv -> + SloganEnv + end, -server(Iname, Oname, Shell) -> - process_flag(trap_exit, true), - case catch open_port({spawn,Iname}, [eof]) of - {'EXIT', _} -> %% It might be a dumb terminal lets start dumb user - user:start(); - Iport -> - Oport = open_port({spawn,Oname}, [eof]), - server1(Iport, Oport, Shell) - end. + Gr = gr_add_cur(State#state.groups, + group:start(self(), InitialShell, + group_opts() ++ [{echo,State#state.shell_started =:= new}]), + InitialShell), -server1(Iport, Oport, Shell) -> - put(eof, false), - %% Start user and initial shell. - User = start_user(), - Gr1 = gr_add_cur(gr_new(), User, {}), - - {Curr,Shell1} = - case init:get_argument(remsh) of - {ok,[[Node]]} -> - ANode = - if - node() =:= nonode@nohost -> - %% We try to connect to the node if the current node is not - %% a distributed node yet. If this succeeds it means that we - %% are running using "-sname undefined". - _ = net_kernel:start([undefined, shortnames]), - NodeName = append_hostname(Node, net_kernel:nodename()), - case net_kernel:connect_node(NodeName) of - true -> - NodeName; - _Else -> - ?LOG_ERROR("Could not connect to ~p",[Node]) - end; - true -> - append_hostname(Node, node()) - end, + init_shell(State#state{ groups = Gr }, [Slogan,$\n]). - RShell = {ANode,shell,start,[]}, - RGr = group:start(self(), RShell, rem_sh_opts(ANode)), - {RGr,RShell}; - E when E =:= error ; E =:= {ok,[[]]} -> - {group:start(self(), Shell),Shell} - end, +init_shell(State, Slogan) -> + init_standard_error(State#state.tty, State#state.shell_started =:= new), + Curr = gr_cur_pid(State#state.groups), put(current_group, Curr), - Gr = gr_add_cur(Gr1, Curr, Shell1), - %% Print some information. - io_request({put_chars, unicode, - flatten(io_lib:format("~ts\n", - [erlang:system_info(system_version)]))}, - Iport, Oport), - - %% Enter the server loop. - server_loop(Iport, Oport, Curr, User, Gr, {false, queue:new()}). - -append_hostname(Node, LocalNode) -> - case string:find(Node,"@") of - nomatch -> - list_to_atom(Node ++ string:find(atom_to_list(LocalNode),"@")); - _ -> - list_to_atom(Node) - end. - -rem_sh_opts(Node) -> - [{expand_fun,fun(B)-> rpc:call(Node,edlin_expand,expand,[B]) end}]. + {next_state, server, State#state{ current_group = gr_cur_pid(State#state.groups) }, + {next_event, info, + {gr_cur_pid(State#state.groups), + {put_chars, unicode, + unicode:characters_to_binary(io_lib:format("~ts", [Slogan]))}}}}. %% start_user() %% Start a group leader process and register it as 'user', unless, %% of course, a 'user' already exists. - start_user() -> - case whereis(user_drv) of - undefined -> - register(user_drv, self()); - _ -> - ok - end, case whereis(user) of undefined -> - User = group:start(self(), {}), + User = group:start(self(), {}, [{echo,false}]), register(user, User), User; User -> User end. - -server_loop(Iport, Oport, User, Gr, IOQueue) -> - Curr = gr_cur_pid(Gr), - put(current_group, Curr), - server_loop(Iport, Oport, Curr, User, Gr, IOQueue). - -server_loop(Iport, Oport, Curr, User, Gr, {Resp, IOQ} = IOQueue) -> - receive - {Iport,{data,Bs}} -> - BsBin = list_to_binary(Bs), - Unicode = unicode:characters_to_list(BsBin,utf8), - port_bytes(Unicode, Iport, Oport, Curr, User, Gr, IOQueue); - {Iport,eof} -> - Curr ! {self(),eof}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - - %% We always handle geometry and unicode requests - {Requester,tty_geometry} -> - Requester ! {self(),tty_geometry,get_tty_geometry(Iport)}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - {Requester,get_unicode_state} -> - Requester ! {self(),get_unicode_state,get_unicode_state(Iport)}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - {Requester,set_unicode_state, Bool} -> - Requester ! {self(),set_unicode_state,set_unicode_state(Iport,Bool)}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - - Req when element(1,Req) =:= User orelse element(1,Req) =:= Curr, - tuple_size(Req) =:= 2 orelse tuple_size(Req) =:= 3 -> - %% We match {User|Curr,_}|{User|Curr,_,_} - NewQ = handle_req(Req, Iport, Oport, IOQueue), - server_loop(Iport, Oport, Curr, User, Gr, NewQ); - {Oport,ok} -> - %% We get this ok from the port, in io_request we store - %% info about where to send reply at head of queue - {Origin,Reply} = Resp, - Origin ! {reply,Reply}, - NewQ = handle_req(next, Iport, Oport, {false, IOQ}), - server_loop(Iport, Oport, Curr, User, Gr, NewQ); - {'EXIT',Iport,_R} -> - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - {'EXIT',Oport,_R} -> - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - {'EXIT',User,shutdown} -> % force data to port - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - {'EXIT',User,_R} -> % keep 'user' alive - NewU = start_user(), - server_loop(Iport, Oport, Curr, NewU, gr_set_num(Gr, 1, NewU, {}), IOQueue); - {'EXIT',Pid,R} -> % shell and group leader exit - case gr_cur_pid(Gr) of - Pid when R =/= die , - R =/= terminated -> % current shell exited - if R =/= normal -> - io_requests([{put_chars,unicode,"*** ERROR: "}], Iport, Oport); - true -> % exit not caused by error - io_requests([{put_chars,unicode,"*** "}], Iport, Oport) - end, - io_requests([{put_chars,unicode,"Shell process terminated! "}], Iport, Oport), - Gr1 = gr_del_pid(Gr, Pid), - case gr_get_info(Gr, Pid) of - {Ix,{shell,start,Params}} -> % 3-tuple == local shell - io_requests([{put_chars,unicode,"***\n"}], Iport, Oport), - %% restart group leader and shell, same index - Pid1 = group:start(self(), {shell,start,Params}), - {ok,Gr2} = gr_set_cur(gr_set_num(Gr1, Ix, Pid1, - {shell,start,Params}), Ix), - put(current_group, Pid1), - server_loop(Iport, Oport, Pid1, User, Gr2, IOQueue); - _ -> % remote shell - io_requests([{put_chars,unicode,"(^G to start new job) ***\n"}], - Iport, Oport), - server_loop(Iport, Oport, Curr, User, Gr1, IOQueue) - end; - _ -> % not current, just remove it - server_loop(Iport, Oport, Curr, User, gr_del_pid(Gr, Pid), IOQueue) - end; - {Requester, {put_chars_sync, _, _, Reply}} -> - %% We need to ack the Req otherwise originating process will hang forever - %% Do discard the output to non visible shells (as was done previously) - Requester ! {reply, Reply}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); - _X -> - %% Ignore unknown messages. - server_loop(Iport, Oport, Curr, User, Gr, IOQueue) - end. -handle_req(next,Iport,Oport,{false,IOQ}=IOQueue) -> - case queue:out(IOQ) of - {empty,_} -> - IOQueue; - {{value,{Origin,Req}},ExecQ} -> - case io_request(Req, Iport, Oport) of - ok -> - handle_req(next,Iport,Oport,{false,ExecQ}); - Reply -> - {{Origin,Reply}, ExecQ} +server({call, From}, {start_shell, Args}, + State = #state{ tty = TTY, shell_started = false }) -> + IsTTY = prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true, + StartShell = maps:get(initial_shell, Args, undefined) =/= noshell, + OldShell = maps:get(initial_shell, Args, undefined) =:= oldshell, + NewState = + try + if + not IsTTY andalso StartShell; OldShell -> + error(enotsup); + IsTTY, StartShell -> + NewTTY = prim_tty:reinit(TTY, #{ }), + State#state{ tty = NewTTY, + shell_started = new }; + true -> + NewTTY = prim_tty:reinit(TTY, #{ tty => false }), + State#state{ tty = NewTTY, shell_started = false } end + catch error:enotsup -> + NewTTYState = prim_tty:reinit(TTY, #{ tty => false }), + State#state{ tty = NewTTYState, shell_started = old } + end, + #{ read := ReadHandle, write := WriteHandle } = prim_tty:handles(NewState#state.tty), + NewHandleState = NewState#state { + read = ReadHandle, + write = WriteHandle + }, + {Result, Reply} + = case maps:get(initial_shell, Args, undefined) of + noshell -> + {init_noshell(NewHandleState), ok}; + {remote, Node} -> + case init_remote_shell(NewHandleState, Node, {shell, start, []}) of + {error, _} = Error -> + {init_noshell(NewHandleState), Error}; + R -> + {R, ok} + end; + {remote, Node, InitialShell} -> + case init_remote_shell(NewHandleState, Node, InitialShell) of + {error, _} = Error -> + {init_noshell(NewHandleState), Error}; + R -> + {R, ok} + end; + undefined -> + case NewHandleState#state.shell_started of + old -> + {init_local_shell(NewHandleState, {shell,start,[]}), ok}; + new -> + {init_local_shell(NewHandleState, {shell,start,[init]}), ok}; + false -> + %% This can never happen, but dialyzer complains so we add + %% this clause. + {keep_state_and_data, ok} + end; + InitialShell -> + {init_local_shell(NewHandleState, InitialShell), ok} + end, + gen_statem:reply(From, Reply), + Result; +server({call, From}, {start_shell, _Args}, _State) -> + gen_statem:reply(From, {error, already_started}), + keep_state_and_data; +server(info, {ReadHandle,{data,UTF8Binary}}, State = #state{ read = ReadHandle }) + when State#state.current_group =:= State#state.user -> + State#state.current_group ! + {self(), {data, unicode:characters_to_list(UTF8Binary, utf8)}}, + keep_state_and_data; +server(info, {ReadHandle,{data,UTF8Binary}}, State = #state{ read = ReadHandle }) -> + case contains_ctrl_g_or_ctrl_c(UTF8Binary) of + ctrl_g -> {next_state, switch_loop, State, {next_event, internal, init}}; + ctrl_c -> + case gr_get_info(State#state.groups, State#state.current_group) of + undefined -> ok; + _ -> exit(State#state.current_group, interrupt) + end, + keep_state_and_data; + none -> + State#state.current_group ! + {self(), {data, unicode:characters_to_list(UTF8Binary, utf8)}}, + keep_state_and_data end; -handle_req(Msg,Iport,Oport,{false,IOQ}=IOQueue) -> - empty = queue:peek(IOQ), - {Origin,Req} = Msg, - case io_request(Req, Iport, Oport) of - ok -> - IOQueue; - Reply -> - {{Origin,Reply}, IOQ} - end; -handle_req(Msg,_Iport,_Oport,{Resp, IOQ}) -> - %% All requests are queued when we have outstanding sync put_chars - {Resp, queue:in(Msg,IOQ)}. - -%% port_bytes(Bytes, InPort, OutPort, CurrentProcess, UserProcess, Group) -%% Check the Bytes from the port to see if it contains a ^G. If so, -%% either escape to switch_loop or restart the shell. Otherwise send -%% the bytes to Curr. - -port_bytes([$\^G|_Bs], Iport, Oport, _Curr, User, Gr, IOQueue) -> - handle_escape(Iport, Oport, User, Gr, IOQueue); - -port_bytes([$\^C|_Bs], Iport, Oport, Curr, User, Gr, IOQueue) -> - interrupt_shell(Iport, Oport, Curr, User, Gr, IOQueue); - -port_bytes([B], Iport, Oport, Curr, User, Gr, IOQueue) -> - Curr ! {self(),{data,[B]}}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue); -port_bytes(Bs, Iport, Oport, Curr, User, Gr, IOQueue) -> - case member($\^G, Bs) of - true -> - handle_escape(Iport, Oport, User, Gr, IOQueue); - false -> - Curr ! {self(),{data,Bs}}, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue) - end. - -interrupt_shell(Iport, Oport, Curr, User, Gr, IOQueue) -> - case gr_get_info(Gr, Curr) of - undefined -> - ok; % unknown - _ -> - exit(Curr, interrupt) +server(info, {ReadHandle,eof}, State = #state{ read = ReadHandle }) -> + State#state.current_group ! {self(), eof}, + keep_state_and_data; +server(info,{ReadHandle,{signal,Signal}}, State = #state{ tty = TTYState, read = ReadHandle }) -> + {keep_state, State#state{ tty = prim_tty:handle_signal(TTYState, Signal) }}; + +server(info, {Requester, tty_geometry}, #state{ tty = TTYState }) -> + case prim_tty:window_size(TTYState) of + {ok, Geometry} -> + Requester ! {self(), tty_geometry, Geometry}, + ok; + Error -> + Requester ! {self(), tty_geometry, Error}, + ok end, - server_loop(Iport, Oport, Curr, User, Gr, IOQueue). - -handle_escape(Iport, Oport, User, Gr, IOQueue) -> - case application:get_env(stdlib, shell_esc) of - {ok,abort} -> - Pid = gr_cur_pid(Gr), - exit(Pid, die), + keep_state_and_data; +server(info, {Requester, get_unicode_state}, #state{ tty = TTYState }) -> + Requester ! {self(), get_unicode_state, prim_tty:unicode(TTYState) }, + keep_state_and_data; +server(info, {Requester, set_unicode_state, Bool}, #state{ tty = TTYState } = State) -> + OldUnicode = prim_tty:unicode(TTYState), + NewTTYState = prim_tty:unicode(TTYState, Bool), + ok = io:setopts(standard_error,[{encoding, if Bool -> unicode; true -> latin1 end}]), + Requester ! {self(), set_unicode_state, OldUnicode}, + {keep_state, State#state{ tty = NewTTYState }}; +server(info, {Requester, get_terminal_state}, _State) -> + Requester ! {self(), get_terminal_state, prim_tty:isatty(stdout) }, + keep_state_and_data; +server(info, {Requester, {open_editor, Buffer}}, #state{tty = TTYState } = State) -> + case open_editor(TTYState, Buffer) of + false -> + Requester ! {self(), {editor_data, Buffer}}, + keep_state_and_data; + {EditorPort, TmpPath} -> + {keep_state, State#state{ editor = #editor{ port = EditorPort, + file = TmpPath, + requester = Requester }}} + end; +server(info, Req, State = #state{ user = User, current_group = Curr, editor = undefined }) + when element(1,Req) =:= User orelse element(1,Req) =:= Curr, + tuple_size(Req) =:= 2 orelse tuple_size(Req) =:= 3 -> + %% We match {User|Curr,_}|{User|Curr,_,_} + {NewTTYState, NewQueue} = handle_req(Req, State#state.tty, State#state.queue), + {keep_state, State#state{ tty = NewTTYState, queue = NewQueue }}; +server(info, {WriteRef, ok}, State = #state{ write = WriteRef, + queue = {{Origin, MonitorRef, Reply}, IOQ} }) -> + %% We get this ok from the user_drv_writer, in io_request we store + %% info about where to send reply at head of queue + Origin ! {reply, Reply, ok}, + erlang:demonitor(MonitorRef, [flush]), + {NewTTYState, NewQueue} = handle_req(next, State#state.tty, {false, IOQ}), + {keep_state, State#state{ tty = NewTTYState, queue = NewQueue }}; +server(info, {'DOWN', MonitorRef, _, _, Reason}, + #state{ queue = {{Origin, MonitorRef, Reply}, _IOQ} }) -> + %% The writer process died, we send the correct error to the caller and + %% then stop this process. This will bring down all linked groups (including 'user'). + %% All writes from now on will throw badarg terminated. + Origin ! {reply, Reply, {error, Reason}}, + ?LOG_INFO("Failed to write to standard out (~p)", [Reason]), + stop; +server(info,{Requester, {put_chars_sync, _, _, Reply}}, _State) -> + %% This is a sync request from an unknown or inactive group. + %% We need to ack the Req otherwise originating process will hang forever. + %% We discard the output to non visible shells + Requester ! {reply, Reply, ok}, + keep_state_and_data; + +server(info,{'EXIT',User, shutdown}, #state{ user = User }) -> + keep_state_and_data; +server(info,{'EXIT',User, _Reason}, State = #state{ user = User }) -> + NewUser = start_user(), + {keep_state, State#state{ user = NewUser, + groups = gr_set_num(State#state.groups, 1, NewUser, {})}}; +server(info, {'EXIT', EditorPort, _R}, + State = #state{tty = TTYState, + editor = #editor{ requester = Requester, + port = EditorPort, + file = PathTmp}}) -> + {ok, Content} = file:read_file(PathTmp), + _ = file:del_dir_r(PathTmp), + Unicode = case unicode:characters_to_list(Content,unicode) of + {error, _, _} -> unicode:characters_to_list( + unicode:characters_to_list(Content,latin1), unicode); + U -> U + end, + Requester ! {self(), {editor_data, string:chomp(Unicode)}}, + ok = prim_tty:enable_reader(TTYState), + {keep_state, State#state{editor = undefined}}; +server(info,{'EXIT', Group, Reason}, State) -> % shell and group leader exit + case gr_cur_pid(State#state.groups) of + Group when Reason =/= die, Reason =/= terminated -> % current shell exited + Reqs = [if + Reason =/= normal -> + {put_chars,unicode,<<"*** ERROR: ">>}; + true -> % exit not caused by error + {put_chars,unicode,<<"*** ">>} + end, + {put_chars,unicode,<<"Shell process terminated! ">>}], + Gr1 = gr_del_pid(State#state.groups, Group), + case gr_get_info(State#state.groups, Group) of + {Ix,{shell,start,Params}} -> % 3-tuple == local shell + NewTTyState = io_requests(Reqs ++ [{put_chars,unicode,<<"***\n">>}], + State#state.tty), + %% restart group leader and shell, same index + NewGroup = group:start(self(), {shell,start,Params}), + {ok,Gr2} = gr_set_cur(gr_set_num(Gr1, Ix, NewGroup, + {shell,start,Params}), Ix), + {keep_state, State#state{ tty = NewTTyState, + current_group = NewGroup, + groups = Gr2 }}; + _ -> % remote shell + NewTTYState = io_requests( + Reqs ++ [{put_chars,unicode,<<"(^G to start new job) ***\n">>}], + State#state.tty), + {keep_state, State#state{ tty = NewTTYState, groups = Gr1 }} + end; + _ -> % not current, just remove it + {keep_state, State#state{ groups = gr_del_pid(State#state.groups, Group) }} + end; +server(_, _, _) -> + keep_state_and_data. + +contains_ctrl_g_or_ctrl_c(<<$\^G,_/binary>>) -> + ctrl_g; +contains_ctrl_g_or_ctrl_c(<<$\^C,_/binary>>) -> + ctrl_c; +contains_ctrl_g_or_ctrl_c(<<_/utf8,T/binary>>) -> + contains_ctrl_g_or_ctrl_c(T); +contains_ctrl_g_or_ctrl_c(<<>>) -> + none. + +switch_loop(internal, init, State) -> + case application:get_env(stdlib, shell_esc, jcl) of + abort -> + CurrGroup = gr_cur_pid(State#state.groups), + exit(CurrGroup, die), Gr1 = - case gr_get_info(Gr, Pid) of - {_Ix,{}} -> % no shell - Gr; + case gr_get_info(State#state.groups, CurrGroup) of + {_Ix,{}} -> % no shell + State#state.groups; _ -> - receive {'EXIT',Pid,_} -> - gr_del_pid(Gr, Pid) + receive {'EXIT',CurrGroup,_} -> + gr_del_pid(State#state.groups, CurrGroup) after 1000 -> - Gr + State#state.groups end end, - Pid1 = group:start(self(), {shell,start,[]}), - io_request({put_chars,unicode,"\n"}, Iport, Oport), - server_loop(Iport, Oport, User, - gr_add_cur(Gr1, Pid1, {shell,start,[]}), IOQueue); - - _ -> % {ok,jcl} | undefined - io_request({put_chars,unicode,"\nUser switch command\n"}, Iport, Oport), + NewGroup = group:start(self(), {shell,start,[]}), + NewTTYState = io_requests([{put_chars,unicode,<<"\n">>}], State#state.tty), + {next_state, server, + State#state{ tty = NewTTYState, + groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}}; + jcl -> + NewTTYState = + io_requests([{put_chars,unicode,<<"\nUser switch command (type h for help)\n">>}], + State#state.tty), %% init edlin used by switch command and have it copy the %% text buffer from current group process - edlin:init(gr_cur_pid(Gr)), - server_loop(Iport, Oport, User, switch_loop(Iport, Oport, Gr), IOQueue) - end. - -switch_loop(Iport, Oport, Gr) -> - Line = get_line(edlin:start(" --> "), Iport, Oport), - switch_cmd(erl_scan:string(Line), Iport, Oport, Gr). - -switch_cmd({ok,[{atom,_,c},{integer,_,I}],_}, Iport, Oport, Gr0) -> - case gr_set_cur(Gr0, I) of - {ok,Gr} -> Gr; - undefined -> unknown_group(Iport, Oport, Gr0) + edlin:init(gr_cur_pid(State#state.groups)), + {keep_state, State#state{ tty = NewTTYState }, + {next_event, internal, line}} end; -switch_cmd({ok,[{atom,_,c}],_}, Iport, Oport, Gr) -> - case gr_get_info(Gr, gr_cur_pid(Gr)) of - undefined -> - unknown_group(Iport, Oport, Gr); - _ -> - Gr +switch_loop(internal, line, State) -> + {more_chars, Cont, Rs} = edlin:start(" --> "), + {keep_state, {Cont, State#state{ tty = io_requests(Rs, State#state.tty) }}}; +switch_loop(internal, {line, Line}, State) -> + case erl_scan:string(Line) of + {ok, Tokens, _} -> + case switch_cmd(Tokens, State#state.groups) of + {ok, Groups} -> + Curr = gr_cur_pid(Groups), + put(current_group, Curr), + Curr ! {self(), activate}, + {next_state, server, + State#state{ current_group = Curr, groups = Groups }}; + {retry, Requests} -> + {keep_state, State#state{ tty = io_requests(Requests, State#state.tty) }, + {next_event, internal, line}}; + {retry, Requests, Groups} -> + Curr = gr_cur_pid(Groups), + put(current_group, Curr), + {keep_state, State#state{ + tty = io_requests(Requests, State#state.tty), + current_group = Curr, + groups = Groups }, + {next_event, internal, line}} + end; + {error, _, _} -> + NewTTYState = + io_requests([{put_chars,unicode,<<"Illegal input\n">>}], State#state.tty), + {keep_state, State#state{ tty = NewTTYState }, + {next_event, internal, line}} end; -switch_cmd({ok,[{atom,_,i},{integer,_,I}],_}, Iport, Oport, Gr) -> +switch_loop(info,{ReadHandle,{data,Cs}}, {Cont, #state{ read = ReadHandle } = State}) -> + case edlin:edit_line(unicode:characters_to_list(Cs), Cont) of + {done,{[Line],_,_},_Rest, Rs} -> + {keep_state, State#state{ tty = io_requests(Rs, State#state.tty) }, + {next_event, internal, {line, Line}}}; + {undefined,_Char,MoreCs,NewCont,Rs} -> + {keep_state, + {NewCont, State#state{ tty = io_requests(Rs ++ [beep], State#state.tty)}}, + {next_event, info, {ReadHandle,{data,MoreCs}}}}; + {more_chars,NewCont,Rs} -> + {keep_state, + {NewCont, State#state{ tty = io_requests(Rs, State#state.tty)}}}; + {blink,NewCont,Rs} -> + {keep_state, + {NewCont, State#state{ tty = io_requests(Rs, State#state.tty)}}, + 1000} + end; +switch_loop(timeout, _, {_Cont, State}) -> + {keep_state_and_data, + {next_event, info, {State#state.read,{data,[]}}}}; +switch_loop(info, _Unknown, _State) -> + {keep_state_and_data, postpone}. + +switch_cmd([{atom,_,Key},{Type,_,Value}], Gr) + when Type =:= atom; Type =:= integer -> + switch_cmd({Key, Value}, Gr); +switch_cmd([{atom,_,Key},{atom,_,V1},{atom,_,V2}], Gr) -> + switch_cmd({Key, V1, V2}, Gr); +switch_cmd([{atom,_,Key}], Gr) -> + switch_cmd(Key, Gr); +switch_cmd([{'?',_}], Gr) -> + switch_cmd(h, Gr); + +switch_cmd(Cmd, Gr) when Cmd =:= c; Cmd =:= i; Cmd =:= k -> + switch_cmd({Cmd, gr_cur_index(Gr)}, Gr); +switch_cmd({c, I}, Gr0) -> + case gr_set_cur(Gr0, I) of + {ok,Gr} -> {ok, Gr}; + undefined -> unknown_group() + end; +switch_cmd({i, I}, Gr) -> case gr_get_num(Gr, I) of {pid,Pid} -> exit(Pid, interrupt), - switch_loop(Iport, Oport, Gr); + {retry, []}; undefined -> - unknown_group(Iport, Oport, Gr) + unknown_group() end; -switch_cmd({ok,[{atom,_,i}],_}, Iport, Oport, Gr) -> - Pid = gr_cur_pid(Gr), - case gr_get_info(Gr, Pid) of - undefined -> - unknown_group(Iport, Oport, Gr); - _ -> - exit(Pid, interrupt), - switch_loop(Iport, Oport, Gr) - end; -switch_cmd({ok,[{atom,_,k},{integer,_,I}],_}, Iport, Oport, Gr) -> +switch_cmd({k, I}, Gr) -> case gr_get_num(Gr, I) of {pid,Pid} -> exit(Pid, die), case gr_get_info(Gr, Pid) of {_Ix,{}} -> % no shell - switch_loop(Iport, Oport, Gr); + retry; _ -> - Gr1 = - receive {'EXIT',Pid,_} -> - gr_del_pid(Gr, Pid) - after 1000 -> - Gr - end, - switch_loop(Iport, Oport, Gr1) + receive {'EXIT',Pid,_} -> + {retry,[],gr_del_pid(Gr, Pid)} + after 1000 -> + {retry,[],Gr} + end end; undefined -> - unknown_group(Iport, Oport, Gr) + unknown_group() end; -switch_cmd({ok,[{atom,_,k}],_}, Iport, Oport, Gr) -> - Pid = gr_cur_pid(Gr), - Info = gr_get_info(Gr, Pid), - case Info of - undefined -> - unknown_group(Iport, Oport, Gr); - {_Ix,{}} -> % no shell - switch_loop(Iport, Oport, Gr); - _ -> - exit(Pid, die), - Gr1 = - receive {'EXIT',Pid,_} -> - gr_del_pid(Gr, Pid) - after 1000 -> - Gr - end, - switch_loop(Iport, Oport, Gr1) - end; -switch_cmd({ok,[{atom,_,j}],_}, Iport, Oport, Gr) -> - io_requests(gr_list(Gr), Iport, Oport), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[{atom,_,s},{atom,_,Shell}],_}, Iport, Oport, Gr0) -> +switch_cmd(j, Gr) -> + {retry, gr_list(Gr)}; +switch_cmd({s, Shell}, Gr0) when is_atom(Shell) -> Pid = group:start(self(), {Shell,start,[]}), Gr = gr_add_cur(Gr0, Pid, {Shell,start,[]}), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[{atom,_,s}],_}, Iport, Oport, Gr0) -> - Pid = group:start(self(), {shell,start,[]}), - Gr = gr_add_cur(Gr0, Pid, {shell,start,[]}), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[{atom,_,r}],_}, Iport, Oport, Gr0) -> + {retry, [], Gr}; +switch_cmd(s, Gr) -> + switch_cmd({s, shell}, Gr); +switch_cmd(r, Gr0) -> case is_alive() of true -> Node = pool:get_node(), - Pid = group:start(self(), {Node,shell,start,[]}), + Pid = group:start(self(), {Node,shell,start,[]}, group_opts(Node)), Gr = gr_add_cur(Gr0, Pid, {Node,shell,start,[]}), - switch_loop(Iport, Oport, Gr); + {retry, [], Gr}; false -> - io_request({put_chars,unicode,"Not alive\n"}, Iport, Oport), - switch_loop(Iport, Oport, Gr0) + {retry, [{put_chars,unicode,<<"Node is not alive\n">>}]} + end; +switch_cmd({r, Node}, Gr) when is_atom(Node)-> + switch_cmd({r, Node, shell}, Gr); +switch_cmd({r,Node,Shell}, Gr0) when is_atom(Node), is_atom(Shell) -> + case is_alive() of + true -> + Pid = group:start(self(), {Node,Shell,start,[]}, group_opts(Node)), + Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}), + {retry, [], Gr}; + false -> + {retry, [{put_chars,unicode,"Node is not alive\n"}]} end; -switch_cmd({ok,[{atom,_,r},{atom,_,Node}],_}, Iport, Oport, Gr0) -> - Pid = group:start(self(), {Node,shell,start,[]}), - Gr = gr_add_cur(Gr0, Pid, {Node,shell,start,[]}), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[{atom,_,r},{atom,_,Node},{atom,_,Shell}],_}, - Iport, Oport, Gr0) -> - Pid = group:start(self(), {Node,Shell,start,[]}), - Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[{atom,_,q}],_}, Iport, Oport, Gr) -> + +switch_cmd(q, _Gr) -> case erlang:system_info(break_ignored) of true -> % noop - io_request({put_chars,unicode,"Unknown command\n"}, Iport, Oport), - switch_loop(Iport, Oport, Gr); + {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}; false -> halt() end; -switch_cmd({ok,[{atom,_,h}],_}, Iport, Oport, Gr) -> - list_commands(Iport, Oport), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[{'?',_}],_}, Iport, Oport, Gr) -> - list_commands(Iport, Oport), - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,[],_}, Iport, Oport, Gr) -> - switch_loop(Iport, Oport, Gr); -switch_cmd({ok,_Ts,_}, Iport, Oport, Gr) -> - io_request({put_chars,unicode,"Unknown command\n"}, Iport, Oport), - switch_loop(Iport, Oport, Gr); -switch_cmd(_Ts, Iport, Oport, Gr) -> - io_request({put_chars,unicode,"Illegal input\n"}, Iport, Oport), - switch_loop(Iport, Oport, Gr). - -unknown_group(Iport, Oport, Gr) -> - io_request({put_chars,unicode,"Unknown job\n"}, Iport, Oport), - switch_loop(Iport, Oport, Gr). - -list_commands(Iport, Oport) -> +switch_cmd(h, _Gr) -> + {retry, list_commands()}; +switch_cmd([], _Gr) -> + {retry,[]}; +switch_cmd(_Ts, _Gr) -> + {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}. + +unknown_group() -> + {retry,[{put_chars,unicode,<<"Unknown job\n">>}]}. + +list_commands() -> QuitReq = case erlang:system_info(break_ignored) of - true -> + true -> []; false -> - [{put_chars, unicode," q - quit erlang\n"}] + [{put_chars, unicode,<<" q - quit erlang\n">>}] end, - io_requests([{put_chars, unicode," c [nn] - connect to job\n"}, - {put_chars, unicode," i [nn] - interrupt job\n"}, - {put_chars, unicode," k [nn] - kill job\n"}, - {put_chars, unicode," j - list all jobs\n"}, - {put_chars, unicode," s [shell] - start local shell\n"}, - {put_chars, unicode," r [node [shell]] - start remote shell\n"}] ++ - QuitReq ++ - [{put_chars, unicode," ? | h - this message\n"}], - Iport, Oport). - -get_line({done,Line,_Rest,Rs}, Iport, Oport) -> - io_requests(Rs, Iport, Oport), - Line; -get_line({undefined,_Char,Cs,Cont,Rs}, Iport, Oport) -> - io_requests(Rs, Iport, Oport), - io_request(beep, Iport, Oport), - get_line(edlin:edit_line(Cs, Cont), Iport, Oport); -get_line({What,Cont0,Rs}, Iport, Oport) -> - io_requests(Rs, Iport, Oport), - receive - {Iport,{data,Cs}} -> - get_line(edlin:edit_line(Cs, Cont0), Iport, Oport); - {Iport,eof} -> - get_line(edlin:edit_line(eof, Cont0), Iport, Oport) - after - get_line_timeout(What) -> - get_line(edlin:edit_line([], Cont0), Iport, Oport) - end. - -get_line_timeout(blink) -> 1000; -get_line_timeout(more_chars) -> infinity. - -% Let driver report window geometry, -% definitely outside of the common interface -get_tty_geometry(Iport) -> - case (catch port_control(Iport,?CTRL_OP_GET_WINSIZE,[])) of - List when length(List) =:= 8 -> - <<W:32/native,H:32/native>> = list_to_binary(List), - {W,H}; - _ -> - error - end. -get_unicode_state(Iport) -> - case (catch port_control(Iport,?CTRL_OP_GET_UNICODE_STATE,[])) of - [Int] when Int > 0 -> - true; - [Int] when Int =:= 0 -> - false; - _ -> - error - end. - -set_unicode_state(Iport, Bool) -> - Data = case Bool of - true -> [1]; - false -> [0] - end, - case (catch port_control(Iport,?CTRL_OP_SET_UNICODE_STATE,Data)) of - [Int] when Int > 0 -> - {unicode, utf8}; - [Int] when Int =:= 0 -> - {unicode, false}; - _ -> - error + [{put_chars, unicode,<<" c [nn] - connect to job\n">>}, + {put_chars, unicode,<<" i [nn] - interrupt job\n">>}, + {put_chars, unicode,<<" k [nn] - kill job\n">>}, + {put_chars, unicode,<<" j - list all jobs\n">>}, + {put_chars, unicode,<<" s [shell] - start local shell\n">>}, + {put_chars, unicode,<<" r [node [shell]] - start remote shell\n">>}] ++ + QuitReq ++ + [{put_chars, unicode,<<" ? | h - this message\n">>}]. + +group_opts(Node) -> + VersionString = erpc:call(Node, erlang, system_info, [otp_release]), + Version = list_to_integer(VersionString), + ExpandFun = + case Version > 25 of + true -> [{expand_fun,fun(B, Opts)-> erpc:call(Node,edlin_expand,expand,[B, Opts]) end}]; + false -> [{expand_fun,fun(B, _)-> erpc:call(Node,edlin_expand,expand,[B]) end}] + end, + group_opts() ++ ExpandFun. +group_opts() -> + [{expand_below, application:get_env(stdlib, shell_expand_location, below) =:= below}]. + +-spec io_request(request(), prim_tty:state()) -> {noreply, prim_tty:state()} | + {term(), reference(), prim_tty:state()}. +io_request({requests,Rs}, TTY) -> + {noreply, io_requests(Rs, TTY)}; +io_request(redraw_prompt, TTY) -> + write(prim_tty:handle_request(TTY, redraw_prompt)); +io_request({redraw_prompt, Pbs, Pbs2, LineState}, TTY) -> + write(prim_tty:handle_request(TTY, {redraw_prompt, Pbs, Pbs2, LineState})); +io_request(new_prompt, TTY) -> + write(prim_tty:handle_request(TTY, new_prompt)); +io_request(delete_after_cursor, TTY) -> + write(prim_tty:handle_request(TTY, delete_after_cursor)); +io_request(delete_line, TTY) -> + write(prim_tty:handle_request(TTY, delete_line)); +io_request({put_chars_keep_state, unicode, Chars}, TTY) -> + write(prim_tty:handle_request(TTY, {putc_keep_state, unicode:characters_to_binary(Chars)})); +io_request({put_chars, unicode, Chars}, TTY) -> + write(prim_tty:handle_request(TTY, {putc, unicode:characters_to_binary(Chars)})); +io_request({put_chars_sync, unicode, Chars, Reply}, TTY) -> + {Output, NewTTY} = prim_tty:handle_request(TTY, {putc, unicode:characters_to_binary(Chars)}), + {ok, MonitorRef} = prim_tty:write(NewTTY, Output, self()), + {Reply, MonitorRef, NewTTY}; +io_request({put_chars_sync, latin1, Chars, Reply}, TTY) -> + {Output, NewTTY} = prim_tty:handle_request(TTY, {putc_raw, Chars}), + {ok, MonitorRef} = prim_tty:write(NewTTY, Output, self()), + {Reply, MonitorRef, NewTTY}; +io_request({put_expand, unicode, Chars}, TTY) -> + write(prim_tty:handle_request(TTY, {expand_with_trim, unicode:characters_to_binary(Chars)})); +io_request({put_expand_no_trim, unicode, Chars}, TTY) -> + write(prim_tty:handle_request(TTY, {expand, unicode:characters_to_binary(Chars)})); +io_request({move_rel, N}, TTY) -> + write(prim_tty:handle_request(TTY, {move, N})); +io_request({move_line, R}, TTY) -> + write(prim_tty:handle_request(TTY, {move_line, R})); +io_request({move_combo, V1, R, V2}, TTY) -> + write(prim_tty:handle_request(TTY, {move_combo, V1, R, V2})); +io_request({insert_chars, unicode, Chars}, TTY) -> + write(prim_tty:handle_request(TTY, {insert, unicode:characters_to_binary(Chars)})); +io_request({delete_chars, N}, TTY) -> + write(prim_tty:handle_request(TTY, {delete, N})); +io_request(clear, TTY) -> + write(prim_tty:handle_request(TTY, clear)); +io_request(beep, TTY) -> + write(prim_tty:handle_request(TTY, beep)). + +write({Output, TTY}) -> + ok = prim_tty:write(TTY, Output), + {noreply, TTY}. + +io_requests([{insert_chars, unicode, C1},{insert_chars, unicode, C2}|Rs], TTY) -> + io_requests([{insert_chars, unicode, [C1,C2]}|Rs], TTY); +io_requests([{put_chars, unicode, C1},{put_chars, unicode, C2}|Rs], TTY) -> + io_requests([{put_chars, unicode, [C1,C2]}|Rs], TTY); +io_requests([{move_rel, N}, {move_line, R}, {move_rel, M}|Rs], TTY) -> + io_requests([{move_combo, N, R, M}|Rs], TTY); +io_requests([{move_rel, N}, {move_line, R}|Rs], TTY) -> + io_requests([{move_combo, N, R, 0}|Rs], TTY); +io_requests([{move_line, R}, {move_rel, M}|Rs], TTY) -> + io_requests([{move_combo, 0, R, M}|Rs], TTY); +io_requests([R|Rs], TTY) -> + {noreply, NewTTY} = io_request(R, TTY), + io_requests(Rs, NewTTY); +io_requests([], TTY) -> + TTY. + +open_editor(TTY, Buffer) -> + DefaultEditor = + case os:type() of + {win32, _} -> "notepad"; + {unix, _} -> "nano" + end, + Editor = os:getenv("VISUAL", os:getenv("EDITOR", DefaultEditor)), + TmpFile = string:chomp(mktemp()) ++ ".erl", + _ = file:write_file(TmpFile, unicode:characters_to_binary(Buffer, unicode)), + case filelib:is_file(TmpFile) of + true -> + ok = prim_tty:disable_reader(TTY), + try + EditorPort = + case os:type() of + {win32, _} -> + [Cmd | Args] = string:split(Editor," ", all), + open_port({spawn_executable, os:find_executable(Cmd)}, + [{args,Args ++ [TmpFile]}, nouse_stdio]); + {unix, _ } -> + open_port({spawn, Editor ++ " " ++ TmpFile}, [nouse_stdio]) + end, + {EditorPort, TmpFile} + catch error:enoent -> + ok = prim_tty:enable_reader(TTY), + io:format(standard_error, "Could not find EDITOR '~ts'.~n", [Editor]), + false + end; + false -> + io:format(standard_error, + "Could not find create temp file '~ts'.~n", + [TmpFile]), + false end. -%% io_request(Request, InPort, OutPort) -%% io_requests(Requests, InPort, OutPort) -%% Note: InPort is unused. -io_request({requests,Rs}, Iport, Oport) -> - io_requests(Rs, Iport, Oport); -io_request(Request, _Iport, Oport) -> - case io_command(Request) of - {Data, Reply} -> - true = port_command(Oport, Data), - Reply; - unhandled -> - ok +mktemp() -> + case os:type() of + {win32, _} -> + os:cmd("powershell \"write-host (& New-TemporaryFile | Select-Object -ExpandProperty FullName)\""); + {unix,_} -> + os:cmd("mktemp") end. -io_requests([R|Rs], Iport, Oport) -> - io_request(R, Iport, Oport), - io_requests(Rs, Iport, Oport); -io_requests([], _Iport, _Oport) -> - ok. - -put_int16(N, Tail) -> - [(N bsr 8)band 255,N band 255|Tail]. - -%% When a put_chars_sync command is used, user_drv guarantees that -%% the bytes have been put in the buffer of the port before an acknowledgement -%% is sent back to the process sending the request. This command was added in -%% OTP 18 to make sure that data sent from io:format is actually printed -%% to the console before the vm stops when calling erlang:halt(integer()). --dialyzer({no_improper_lists, io_command/1}). -io_command({put_chars_sync, unicode,Cs,Reply}) -> - {[?OP_PUTC_SYNC|unicode:characters_to_binary(Cs,utf8)], Reply}; -io_command({put_chars, unicode,Cs}) -> - {[?OP_PUTC|unicode:characters_to_binary(Cs,utf8)], ok}; -io_command({move_rel,N}) -> - {[?OP_MOVE|put_int16(N, [])], ok}; -io_command({insert_chars,unicode,Cs}) -> - {[?OP_INSC|unicode:characters_to_binary(Cs,utf8)], ok}; -io_command({delete_chars,N}) -> - {[?OP_DELC|put_int16(N, [])], ok}; -io_command(beep) -> - {[?OP_BEEP], ok}; -io_command(_) -> - unhandled. +handle_req(next, TTYState, {false, IOQ} = IOQueue) -> + case queue:out(IOQ) of + {empty, _} -> + {TTYState, IOQueue}; + {{value, {Origin, Req}}, ExecQ} -> + case io_request(Req, TTYState) of + {noreply, NewTTYState} -> + handle_req(next, NewTTYState, {false, ExecQ}); + {Reply, MonitorRef, NewTTYState} -> + {NewTTYState, {{Origin, MonitorRef, Reply}, ExecQ}} + end + end; +handle_req(Msg, TTYState, {false, IOQ} = IOQueue) -> + empty = queue:peek(IOQ), + {Origin, Req} = Msg, + case io_request(Req, TTYState) of + {noreply, NewTTYState} -> + {NewTTYState, IOQueue}; + {Reply, MonitorRef, NewTTYState} -> + {NewTTYState, {{Origin, MonitorRef, Reply}, IOQ}} + end; +handle_req(Msg,TTYState,{Resp, IOQ}) -> + %% All requests are queued when we have outstanding sync put_chars + {TTYState, {Resp, queue:in(Msg,IOQ)}}. %% gr_new() %% gr_get_num(Group, Index) @@ -611,100 +910,71 @@ io_command(_) -> %% gr_add_cur(Group, Pid, Shell) %% gr_set_cur(Group, Index) %% gr_cur_pid(Group) +%% gr_cur_index(Group) %% gr_del_pid(Group, Pid) %% Manage the group list. The group structure has the form: %% {NextIndex,CurrIndex,CurrPid,GroupList} %% %% where each element in the group list is: %% {Index,GroupPid,Shell} - +-record(group, { index, pid, shell }). +-record(gr, { next = 0, current = 0, pid = none, groups = []}). gr_new() -> - {0,0,none,[]}. - -gr_get_num({_Next,_CurI,_CurP,Gs}, I) -> - gr_get_num1(Gs, I). - -gr_get_num1([{I,_Pid,{}}|_Gs], I) -> - undefined; -gr_get_num1([{I,Pid,_S}|_Gs], I) -> - {pid,Pid}; -gr_get_num1([_G|Gs], I) -> - gr_get_num1(Gs, I); -gr_get_num1([], _I) -> - undefined. - -gr_get_info({_Next,_CurI,_CurP,Gs}, Pid) -> - gr_get_info1(Gs, Pid). - -gr_get_info1([{I,Pid,S}|_Gs], Pid) -> - {I,S}; -gr_get_info1([_G|Gs], I) -> - gr_get_info1(Gs, I); -gr_get_info1([], _I) -> - undefined. - -gr_add_cur({Next,_CurI,_CurP,Gs}, Pid, Shell) -> - {Next+1,Next,Pid,append(Gs, [{Next,Pid,Shell}])}. - -gr_set_cur({Next,_CurI,_CurP,Gs}, I) -> - case gr_get_num1(Gs, I) of - {pid,Pid} -> {ok,{Next,I,Pid,Gs}}; + #gr{}. +gr_new_group(I, P, S) -> + #group{ index = I, pid = P, shell = S }. + +gr_get_num(#gr{ groups = Gs }, I) -> + case lists:keyfind(I, #group.index, Gs) of + false -> undefined; + #group{ shell = {} } -> + undefined; + #group{ pid = Pid } -> + {pid, Pid} + end. + +gr_get_info(#gr{ groups = Gs }, Pid) -> + case lists:keyfind(Pid, #group.pid, Gs) of + false -> undefined; + #group{ index = I, shell = S } -> + {I, S} + end. + +gr_add_cur(#gr{ next = Next, groups = Gs}, Pid, Shell) -> + put(current_group, Pid), + #gr{ next = Next + 1, current = Next, pid = Pid, + groups = Gs ++ [gr_new_group(Next, Pid, Shell)] + }. + +gr_set_cur(Gr, I) -> + case gr_get_num(Gr, I) of + {pid,Pid} -> + put(current_group, Pid), + {ok, Gr#gr{ current = I, pid = Pid }}; undefined -> undefined end. -gr_set_num({Next,CurI,CurP,Gs}, I, Pid, Shell) -> - {Next,CurI,CurP,gr_set_num1(Gs, I, Pid, Shell)}. - -gr_set_num1([{I,_Pid,_Shell}|Gs], I, NewPid, NewShell) -> - [{I,NewPid,NewShell}|Gs]; -gr_set_num1([{I,Pid,Shell}|Gs], NewI, NewPid, NewShell) when NewI > I -> - [{I,Pid,Shell}|gr_set_num1(Gs, NewI, NewPid, NewShell)]; -gr_set_num1(Gs, NewI, NewPid, NewShell) -> - [{NewI,NewPid,NewShell}|Gs]. - -gr_del_pid({Next,CurI,CurP,Gs}, Pid) -> - {Next,CurI,CurP,gr_del_pid1(Gs, Pid)}. - -gr_del_pid1([{_I,Pid,_S}|Gs], Pid) -> - Gs; -gr_del_pid1([G|Gs], Pid) -> - [G|gr_del_pid1(Gs, Pid)]; -gr_del_pid1([], _Pid) -> - []. - -gr_cur_pid({_Next,_CurI,CurP,_Gs}) -> - CurP. - -gr_list({_Next,CurI,_CurP,Gs}) -> - gr_list(Gs, CurI, []). - -gr_list([{_I,_Pid,{}}|Gs], Cur, Jobs) -> - gr_list(Gs, Cur, Jobs); -gr_list([{Cur,_Pid,Shell}|Gs], Cur, Jobs) -> - gr_list(Gs, Cur, [{put_chars, unicode,flatten(io_lib:format("~4w* ~w\n", [Cur,Shell]))}|Jobs]); -gr_list([{I,_Pid,Shell}|Gs], Cur, Jobs) -> - gr_list(Gs, Cur, [{put_chars, unicode,flatten(io_lib:format("~4w ~w\n", [I,Shell]))}|Jobs]); -gr_list([], _Cur, Jobs) -> - lists:reverse(Jobs). - -append([H|T], X) -> - [H|append(T, X)]; -append([], X) -> - X. - -member(X, [X|_Rest]) -> true; -member(X, [_H|Rest]) -> - member(X, Rest); -member(_X, []) -> false. - -flatten(List) -> - flatten(List, [], []). - -flatten([H|T], Cont, Tail) when is_list(H) -> - flatten(H, [T|Cont], Tail); -flatten([H|T], Cont, Tail) -> - [H|flatten(T, Cont, Tail)]; -flatten([], [H|Cont], Tail) -> - flatten(H, Cont, Tail); -flatten([], [], Tail) -> - Tail. +gr_set_num(Gr = #gr{ groups = Groups }, I, Pid, Shell) -> + NewGroups = lists:keystore(I, #group.index, Groups, gr_new_group(I,Pid,Shell)), + Gr#gr{ groups = NewGroups }. + + +gr_del_pid(Gr = #gr{ groups = Groups }, Pid) -> + Gr#gr{ groups = lists:keydelete(Pid, #group.pid, Groups) }. + + +gr_cur_pid(#gr{ pid = Pid }) -> + Pid. +gr_cur_index(#gr{ current = Index }) -> + Index. + +gr_list(#gr{ current = Current, groups = Groups}) -> + lists:flatmap( + fun(#group{ shell = {} }) -> + []; + (#group{ index = I, shell = S }) -> + Marker = ["*" || Current =:= I], + [{put_chars, unicode, + unicode:characters_to_binary( + io_lib:format("~4w~.1ts ~w\n", [I,Marker,S]))}] + end, Groups). |