From 3e4baaee6543befa35e604b9d40391c719d0d752 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 12 May 2022 13:55:12 +0200 Subject: [PATCH 01/34] erts: Implement erlang:display_string/2 used for testing --- erts/emulator/beam/bif.c | 83 ++++++++++++++++++++----- erts/emulator/beam/bif.tab | 2 +- erts/emulator/beam/erl_dirty_bif.tab | 1 + erts/emulator/beam/sys.h | 2 +- erts/emulator/test/exception_SUITE.erl | 2 + erts/preloaded/ebin/erlang.beam | Bin 132536 -> 132836 bytes erts/preloaded/src/erlang.erl | 15 ++++- lib/kernel/src/erl_erts_errors.erl | 30 ++++++++- 8 files changed, 116 insertions(+), 19 deletions(-) diff --git a/erts/emulator/beam/bif.c b/erts/emulator/beam/bif.c index 1d0a145f3fc7..c8e7caac15f3 100644 --- a/erts/emulator/beam/bif.c +++ b/erts/emulator/beam/bif.c @@ -23,6 +23,10 @@ #endif #include /* offsetof() */ +#ifdef HAVE_SYS_IOCTL_H +#include +#endif +#define WANT_NONBLOCKING #include "sys.h" #include "erl_vm.h" #include "erl_sys_driver.h" @@ -4180,27 +4184,78 @@ BIF_RETTYPE erts_debug_display_1(BIF_ALIST_1) BIF_RET(res); } - -BIF_RETTYPE display_string_1(BIF_ALIST_1) +BIF_RETTYPE display_string_2(BIF_ALIST_2) { Process* p = BIF_P; - Eterm string = BIF_ARG_1; - Sint len = erts_unicode_list_to_buf_len(string); + Eterm string = BIF_ARG_2; + Sint len; Sint written; byte *str; - int res; + int res, fd; + byte *temp_alloc = NULL; + + if (ERTS_IS_ATOM_STR("stdout", BIF_ARG_1)) { + fd = fileno(stdout); + } else if (ERTS_IS_ATOM_STR("stderr", BIF_ARG_1)) { + fd = fileno(stderr); +#if defined(HAVE_SYS_IOCTL_H) && defined(TIOCSTI) + } else if (ERTS_IS_ATOM_STR("stdin", BIF_ARG_1)) { + fd = open("/proc/self/fd/0",0); +#endif + } else { + BIF_ERROR(p, BADARG); + } + if (is_list(string) || is_nil(string)) { + len = erts_unicode_list_to_buf_len(string); + if (len < 0) BIF_ERROR(p, BADARG); + str = temp_alloc = (byte *) erts_alloc(ERTS_ALC_T_TMP, sizeof(char)*len); + res = erts_unicode_list_to_buf(string, str, len, &written); + if (res != 0 || written != len) + erts_exit(ERTS_ERROR_EXIT, "%s:%d: Internal error (%d)\n", __FILE__, __LINE__, res); + } else if (is_binary(string)) { + Uint bitoffs, bitsize; + ERTS_GET_BINARY_BYTES(string, str, bitoffs, bitsize); + if (bitsize % 8 != 0) BIF_ERROR(p, BADARG); + len = binary_size(string); + if (bitoffs != 0) { + str = erts_get_aligned_binary_bytes(string, &temp_alloc); + } + } else { + BIF_ERROR(p, BADARG); + } - if (len < 0) { - BIF_ERROR(p, BADARG); +#if defined(HAVE_SYS_IOCTL_H) && defined(TIOCSTI) + if (ERTS_IS_ATOM_STR("stdin", BIF_ARG_1)) { + for (int i = 0; i < len; i++) { + if (ioctl(fd, TIOCSTI, str+i) < 0) { + fprintf(stderr,"failed to write to %s (%s)\r\n", "/proc/self/fd/0", + strerror(errno)); + close(fd); + goto error; + } + } + close(fd); + } else +#endif + { + written = 0; + do { + res = write(fd, str+written, len-written); + if (res < 0 && errno != ERRNO_BLOCK && errno != EINTR) + goto error; + written += res; + } while (written < len); } - str = (byte *) erts_alloc(ERTS_ALC_T_TMP, sizeof(char)*(len + 1)); - res = erts_unicode_list_to_buf(string, str, len, &written); - if (res != 0 || written != len) - erts_exit(ERTS_ERROR_EXIT, "%s:%d: Internal error (%d)\n", __FILE__, __LINE__, res); - str[len] = '\0'; - erts_fprintf(stderr, "%s", str); - erts_free(ERTS_ALC_T_TMP, (void *) str); + if (temp_alloc) + erts_free(ERTS_ALC_T_TMP, (void *) temp_alloc); BIF_RET(am_true); + +error: { + char *errnostr = erl_errno_id(errno); + BIF_P->fvalue = am_atom_put(errnostr, strlen(errnostr)); + erts_free(ERTS_ALC_T_TMP, (void *) str); + BIF_ERROR(p, BADARG | EXF_HAS_EXT_INFO); + } } BIF_RETTYPE display_nl_0(BIF_ALIST_0) diff --git a/erts/emulator/beam/bif.tab b/erts/emulator/beam/bif.tab index dffe3963b599..17f35a1bc775 100644 --- a/erts/emulator/beam/bif.tab +++ b/erts/emulator/beam/bif.tab @@ -56,7 +56,7 @@ bif erlang:crc32_combine/3 bif erlang:date/0 bif erlang:delete_module/1 bif erlang:display/1 -bif erlang:display_string/1 +bif erlang:display_string/2 bif erlang:display_nl/0 ubif erlang:element/2 bif erlang:erase/0 diff --git a/erts/emulator/beam/erl_dirty_bif.tab b/erts/emulator/beam/erl_dirty_bif.tab index 3f16f3e0f330..9245a19be611 100644 --- a/erts/emulator/beam/erl_dirty_bif.tab +++ b/erts/emulator/beam/erl_dirty_bif.tab @@ -50,6 +50,7 @@ dirty-io erts_debug:dirty_io/2 dirty-cpu erts_debug:lcnt_control/2 dirty-cpu erts_debug:lcnt_collect/0 dirty-cpu erts_debug:lcnt_clear/0 +dirty-cpu erlang:display_string/2 # --- TEST of Dirty BIF functionality --- # Functions below will execute on dirty schedulers when emulator has diff --git a/erts/emulator/beam/sys.h b/erts/emulator/beam/sys.h index 20b0571e43af..b57cfd6952a6 100644 --- a/erts/emulator/beam/sys.h +++ b/erts/emulator/beam/sys.h @@ -608,7 +608,7 @@ extern erts_tsd_key_t erts_is_crash_dumping_key; static unsigned long zero_value = 0, one_value = 1; # define SET_BLOCKING(fd) { if (ioctlsocket((fd), FIONBIO, &zero_value) != 0) fprintf(stderr, "Error setting socket to non-blocking: %d\n", WSAGetLastError()); } # define SET_NONBLOCKING(fd) ioctlsocket((fd), FIONBIO, &one_value) - +# define ERRNO_BLOCK EAGAIN /* We use the posix way for windows */ # else # ifdef NB_FIONBIO /* Old BSD */ # include diff --git a/erts/emulator/test/exception_SUITE.erl b/erts/emulator/test/exception_SUITE.erl index 5cce5d149168..dfec2a28dc05 100644 --- a/erts/emulator/test/exception_SUITE.erl +++ b/erts/emulator/test/exception_SUITE.erl @@ -851,6 +851,8 @@ error_info(_Config) -> {display, ["test erlang:display/1"], [no_fail]}, {display_string, [{a,b,c}]}, + {display_string, [standard_out,"test erlang:display/2"]}, + {display_string, [stdout,{a,b,c}]}, %% Internal undcoumented BIFs. {dist_ctrl_get_data, 1}, diff --git a/erts/preloaded/ebin/erlang.beam b/erts/preloaded/ebin/erlang.beam index 759d4de03f918bc8c4cd52436ba14d180fe19775..cd37b934767c8b2c77f64b2248c919cecbeb4339 100644 GIT binary patch literal 132836 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 S>xln%?(%t8AXlys5&eJRgAF$T literal 132536 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

FW!AY+gDV}#t?~kW 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 %% display_string/1 -spec erlang:display_string(P1) -> true when + P1 :: string() | binary(). +display_string(String) -> + try erlang:display_string(stderr, String) + catch error:badarg:ST -> + [{erlang, display_string, _, [ErrorInfo]}|_] = ST, + erlang:error(badarg, [String], [ErrorInfo]) + end. + +%% display_string/2 +-spec erlang:display_string(Device, P1) -> true when + Device :: stdin | stdout | stderr, P1 :: string(). -display_string(_P1) -> +display_string(_Stream,_P1) -> erlang:nif_error(undefined). %% dt_append_vm_tag_data/1 diff --git a/lib/kernel/src/erl_erts_errors.erl b/lib/kernel/src/erl_erts_errors.erl index 4d23112b83a0..fe7758dae0d4 100644 --- a/lib/kernel/src/erl_erts_errors.erl +++ b/lib/kernel/src/erl_erts_errors.erl @@ -352,8 +352,19 @@ format_erlang_error(demonitor, [_], _) -> format_erlang_error(demonitor, [Ref,Options], _) -> Arg1 = must_be_ref(Ref), [Arg1,maybe_option_list_error(Options, Arg1)]; -format_erlang_error(display_string, [_], _) -> +format_erlang_error(display_string, [_], none) -> [not_string]; +format_erlang_error(display_string, [_], Cause) -> + maybe_posix_message(Cause, false); +format_erlang_error(display_string, [Device, _], none) -> + case lists:member(Device,[stdin,stdout,stderr]) of + true -> + [[],not_string]; + false -> + [not_device,[]] + end; +format_erlang_error(display_string, [_, _], Cause) -> + maybe_posix_message(Cause, true); format_erlang_error(element, [Index, Tuple], _) -> [if not is_integer(Index) -> @@ -1430,8 +1441,23 @@ is_flat_char_list([H|T]) -> is_flat_char_list([]) -> true; is_flat_char_list(_) -> false. +maybe_posix_message(Cause, HasDevice) -> + case erl_posix_msg:message(Cause) of + "unknown POSIX error" -> + unknown; + PosixStr when HasDevice -> + [unicode:characters_to_binary( + io_lib:format("~ts (~tp)",[PosixStr, Cause]))]; + PosixStr when not HasDevice -> + [{general, + unicode:characters_to_binary( + io_lib:format("~ts (~tp)",[PosixStr, Cause]))}] + end. + format_error_map([""|Es], ArgNum, Map) -> format_error_map(Es, ArgNum + 1, Map); +format_error_map([{general, E}|Es], ArgNum, Map) -> + format_error_map(Es, ArgNum, Map#{ general => expand_error(E)}); format_error_map([E|Es], ArgNum, Map) -> format_error_map(Es, ArgNum + 1, Map#{ArgNum => expand_error(E)}); format_error_map([], _, Map) -> @@ -1519,6 +1545,8 @@ expand_error(not_ref) -> <<"not a reference">>; expand_error(not_string) -> <<"not a list of characters">>; +expand_error(not_device) -> + <<"not a valid device type">>; expand_error(not_tuple) -> <<"not a tuple">>; expand_error(range) -> From 0fc54754b4e2cac7c789b5a54d653de8bdd14cbf Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 25 Aug 2022 14:25:01 +0200 Subject: [PATCH 02/34] erts: Fix writing to std handler when detached When writing using erlang:display* when detached the write operation would return an error on windows. So we make sure that the handles are valid so that they can always be written to. --- erts/emulator/beam/bif.c | 28 +++++++++++++++++++++++++--- erts/etc/common/erlexec.c | 12 ++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/erts/emulator/beam/bif.c b/erts/emulator/beam/bif.c index c8e7caac15f3..b78942bd4533 100644 --- a/erts/emulator/beam/bif.c +++ b/erts/emulator/beam/bif.c @@ -4191,18 +4191,30 @@ BIF_RETTYPE display_string_2(BIF_ALIST_2) Sint len; Sint written; byte *str; - int res, fd; + int res; byte *temp_alloc = NULL; +#ifdef __WIN32__ + HANDLE fd; + if (ERTS_IS_ATOM_STR("stdout", BIF_ARG_1)) { + fd = GetStdHandle(STD_OUTPUT_HANDLE); + } else if (ERTS_IS_ATOM_STR("stderr", BIF_ARG_1)) { + fd = GetStdHandle(STD_ERROR_HANDLE); + } +#else + int fd; if (ERTS_IS_ATOM_STR("stdout", BIF_ARG_1)) { fd = fileno(stdout); } else if (ERTS_IS_ATOM_STR("stderr", BIF_ARG_1)) { fd = fileno(stderr); + } #if defined(HAVE_SYS_IOCTL_H) && defined(TIOCSTI) - } else if (ERTS_IS_ATOM_STR("stdin", BIF_ARG_1)) { + else if (ERTS_IS_ATOM_STR("stdin", BIF_ARG_1)) { fd = open("/proc/self/fd/0",0); + } #endif - } else { +#endif + else { BIF_ERROR(p, BADARG); } if (is_list(string) || is_nil(string)) { @@ -4238,6 +4250,11 @@ BIF_RETTYPE display_string_2(BIF_ALIST_2) } else #endif { +#ifdef __WIN32__ + if (!WriteFile(fd, str, len, &written, NULL)) { + goto error; + } +#else written = 0; do { res = write(fd, str+written, len-written); @@ -4245,13 +4262,18 @@ BIF_RETTYPE display_string_2(BIF_ALIST_2) goto error; written += res; } while (written < len); +#endif } if (temp_alloc) erts_free(ERTS_ALC_T_TMP, (void *) temp_alloc); BIF_RET(am_true); error: { +#ifdef __WIN32__ + char *errnostr = last_error(); +#else char *errnostr = erl_errno_id(errno); +#endif BIF_P->fvalue = am_atom_put(errnostr, strlen(errnostr)); erts_free(ERTS_ALC_T_TMP, (void *) str); BIF_ERROR(p, BADARG | EXF_HAS_EXT_INFO); diff --git a/erts/etc/common/erlexec.c b/erts/etc/common/erlexec.c index b1e2118b7af9..5d4432789be2 100644 --- a/erts/etc/common/erlexec.c +++ b/erts/etc/common/erlexec.c @@ -453,6 +453,18 @@ int main(int argc, char **argv) Eargsp[argc] = NULL; emu = argv[0]; start_emulator_program = strsave(argv[0]); + /* We set the stdandard handles to nul in order for prim_tty_nif + and erlang:display_string to work without returning ebadf for + detached emulators */ + SetStdHandle(STD_INPUT_HANDLE, + CreateFile("nul", GENERIC_READ, 0, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL)); + SetStdHandle(STD_OUTPUT_HANDLE, + CreateFile("nul", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL)); + SetStdHandle(STD_ERROR_HANDLE, + CreateFile("nul", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL)); goto skip_arg_massage; } free_env_val(s); From e53f0eaa23efadca06f3aaf179032ca7ef27bd73 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Sun, 15 May 2022 22:51:54 +0200 Subject: [PATCH 03/34] kernel: Add tty tests using tmux --- .github/dockerfiles/Dockerfile.ubuntu-base | 7 + lib/kernel/test/interactive_shell_SUITE.erl | 1133 ++++++++++++++++++- lib/kernel/test/rtnode.erl | 45 +- 3 files changed, 1135 insertions(+), 50 deletions(-) diff --git a/.github/dockerfiles/Dockerfile.ubuntu-base b/.github/dockerfiles/Dockerfile.ubuntu-base index dc8b28e1c8a5..76952b39a596 100644 --- a/.github/dockerfiles/Dockerfile.ubuntu-base +++ b/.github/dockerfiles/Dockerfile.ubuntu-base @@ -48,6 +48,13 @@ RUN apt-get install -y git && \ done && \ rm -rf ~/.kerl +## We use tmux to test terminals +RUN apt-get install -y libevent-dev libutf8proc-dev && \ + cd /tmp && wget https://github.com/tmux/tmux/releases/download/3.2a/tmux-3.2a.tar.gz && \ + tar xvzf tmux-3.2a.tar.gz && cd tmux-3.2a && \ + ./configure --enable-static --enable-utf8proc && \ + make && make install + ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 ARG USER=gitpod diff --git a/lib/kernel/test/interactive_shell_SUITE.erl b/lib/kernel/test/interactive_shell_SUITE.erl index d303d42edaaa..cba745264781 100644 --- a/lib/kernel/test/interactive_shell_SUITE.erl +++ b/lib/kernel/test/interactive_shell_SUITE.erl @@ -21,6 +21,20 @@ -include_lib("kernel/include/file.hrl"). -include_lib("common_test/include/ct.hrl"). +%% Things to add tests for: +%% - TERM=dumb +%% - Editing line > MAXSIZE (1 << 16) +%% - \t tests (use io:format("\t")) +%% - xn fix after Delete and Backspace +%% - octal_to_hex > 255 length (is this possible?) +%% 1222 0 : } else if (lastput == 0) { /* A multibyte UTF8 character */ +%% 1223 0 : for (i = 0; i < ubytes; ++i) { +%% 1224 0 : outc(ubuf[i]); +%% 1225 : } +%% 1226 : } else { +%% 1227 0 : outc(lastput); +%% - $TERM set to > 1024 long value + -export([all/0, suite/0, groups/0, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2, @@ -32,25 +46,39 @@ shell_history_custom/1, shell_history_custom_errors/1, job_control_remote_noshell/1,ctrl_keys/1, get_columns_and_rows_escript/1, + shell_navigation/1, shell_xnfix/1, shell_delete/1, + shell_transpose/1, shell_search/1, shell_insert/1, + shell_update_window/1, shell_huge_input/1, + shell_invalid_unicode/1, shell_support_ansi_input/1, + shell_invalid_ansi/1, shell_suspend/1, shell_full_queue/1, + shell_unicode_wrap/1, shell_delete_unicode_wrap/1, + shell_delete_unicode_not_at_cursor_wrap/1, + shell_update_window_unicode_wrap/1, remsh_basic/1, remsh_longnames/1, remsh_no_epmd/1]). %% Exports for custom shell history module -export([load/0, add/1]). +%% For custom prompt testing +-export([prompt/1]). suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,3}}]. all() -> - [get_columns_and_rows_escript,get_columns_and_rows, - exit_initial, job_control_local, - job_control_remote, job_control_remote_noshell, - ctrl_keys, stop_during_init, wrap, - {group, shell_history}, - {group, remsh}]. + [{group, to_erl}, + {group, tty}]. groups() -> - [{shell_history, [], + [{to_erl,[], + [get_columns_and_rows_escript,get_columns_and_rows, + exit_initial, job_control_local, + job_control_remote, job_control_remote_noshell, + ctrl_keys, stop_during_init, wrap, + shell_invalid_ansi, + {group, shell_history}, + {group, remsh}]}, + {shell_history, [], [shell_history, shell_history_resize, shell_history_eaccess, @@ -65,25 +93,48 @@ groups() -> {remsh, [], [remsh_basic, remsh_longnames, - remsh_no_epmd]} + remsh_no_epmd]}, + {tty,[], + [{group,tty_unicode}, + {group,tty_latin1}, + shell_suspend, + shell_full_queue + ]}, + {tty_unicode,[parallel], + [{group,tty_tests}, + shell_invalid_unicode + %% unicode wrapping does not work right yet + %% shell_unicode_wrap, + %% shell_delete_unicode_wrap, + %% shell_delete_unicode_not_at_cursor_wrap, + %% shell_update_window_unicode_wrap + ]}, + {tty_latin1,[],[{group,tty_tests}]}, + {tty_tests, [parallel], + [shell_navigation, shell_xnfix, shell_delete, + shell_transpose, shell_search, shell_insert, + shell_update_window, shell_huge_input, + shell_support_ansi_input]} ]. init_per_suite(Config) -> Term = os:getenv("TERM", "dumb"), os:putenv("TERM", "vt100"), - case rtnode:get_default_shell() of - noshell -> - os:putenv("TERM",Term), - {skip, "No run_erl"}; - DefShell -> - [{default_shell,DefShell},{term,Term}|Config] - end. + [{term,Term}|Config]. end_per_suite(Config) -> Term = proplists:get_value(term,Config), os:putenv("TERM",Term), ok. +init_per_group(to_erl, Config) -> + case rtnode:get_progs() of + {error, Error} -> + {skip, Error}; + _ -> + DefShell = rtnode:get_default_shell(), + [{default_shell,DefShell}|Config] + end; init_per_group(remsh, Config) -> case proplists:get_value(default_shell, Config) of old -> {skip, "Not supported in old shell"}; @@ -94,6 +145,33 @@ init_per_group(shell_history, Config) -> old -> {skip, "Not supported in old shell"}; new -> Config end; +init_per_group(tty, Config) -> + case string:split(tmux("-V")," ") of + ["tmux",[Num,$.|_]] when Num >= $3, Num =< $9 -> + tmux("kill-session"), + "" = tmux("-u new-session -x 50 -y 60 -d"), + ["" = tmux(["set-environment '",Name,"' '",Value,"'"]) + || {Name,Value} <- os:env()], + Config; + ["tmux", Vsn] -> + {skip, "invalid tmux version " ++ Vsn ++ ". Need vsn 3 or later"}; + Error -> + {skip, "tmux not installed " ++ Error} + end; +init_per_group(Group, Config) when Group =:= tty_unicode; + Group =:= tty_latin1 -> + [Lang,_] = + string:split( + os:getenv("LC_ALL", + os:getenv("LC_CTYPE", + os:getenv("LANG","en_US.UTF-8"))),"."), + case Group of + tty_unicode -> + [{encoding, unicode},{env,[{"LC_ALL",Lang++".UTF-8"}]}|Config]; + tty_latin1 -> + % [{encoding, latin1},{env,[{"LC_ALL",Lang++".ISO-8859-1"}]}|Config], + {skip, "latin1 tests not implemented yet"} + end; init_per_group(sh_custom, Config) -> %% Ensure that ERL_AFLAGS will not override the value of the shell_history variable. {ok, Peer, Node} = ?CT_PEER(["-noshell","-kernel","shell_history","not_overridden"]), @@ -113,18 +191,39 @@ init_per_group(sh_custom, Config) -> init_per_group(_GroupName, Config) -> Config. +end_per_group(tty, _Config) -> + Windows = string:split(tmux("list-windows"), "\n", all), + lists:foreach( + fun(W) -> + case string:split(W, " ", all) of + ["0:" | _] -> ok; + [No, _Name | _] -> + "" = os:cmd(["tmux select-window -t ", string:split(No,":")]), + ct:log("~ts~n~ts",[W, os:cmd(lists:concat(["tmux capture-pane -p -e"]))]) + end + end, Windows), +% "" = os:cmd("tmux kill-session") + ok; end_per_group(_GroupName, Config) -> Config. -init_per_testcase(_Func, Config) -> - Config. +init_per_testcase(Func, Config) -> + Path = [Func, + [proplists:get_value(name,P) || + P <- [proplists:get_value(tc_group_properties,Config,[])] ++ + proplists:get_value(tc_group_path,Config,[])]], + [{tc_path, lists:concat(lists:join("-",lists:flatten(Path)))} | Config]. -end_per_testcase(_Case, _Config) -> - %% Terminate any connected nodes. They may disturb test cases that follow. - lists:foreach(fun(Node) -> - catch erpc:call(Node, erlang, halt, []) - end, nodes()), - ok. +end_per_testcase(_Case, Config) -> + case proplists:get_value(name, proplists:get_value(tc_group_properties, Config)) of + tty_tests -> ok; + _ -> + %% Terminate any connected nodes. They may disturb test cases that follow. + lists:foreach(fun(Node) -> + catch erpc:call(Node, erlang, halt, []) + end, nodes()), + ok + end. %%-define(DEBUG,1). -ifdef(DEBUG). @@ -248,6 +347,976 @@ test_columns_and_rows(new, _Args) -> [], "stty rows 40; stty columns 90; "). +shell_navigation(Config) -> + + Term = start_tty(Config), + + try + [begin + send_tty(Term,"{aaa,'b"++U++"b',ccc}"), + check_location(Term, {0, 0}), %% Check that cursor jump backward + check_content(Term, "{aaa,'b"++U++"b',ccc}$"), + timer:sleep(1000), %% Wait for cursor to jump back + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc}")}), + send_tty(Term,"Home"), + check_location(Term, {0, 0}), + send_tty(Term,"End"), + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc}")}), + send_tty(Term,"Left"), + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{aaa,'b"++U++"b',")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{aaa,")}), + send_tty(Term,"C-Right"), + check_location(Term, {0, width("{aaa,'b"++U++"b'")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{aaa,")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, 0}), + send_tty(Term,"C-E"), + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc}")}), + send_tty(Term,"C-A"), + check_location(Term, {0, 0}), + send_tty(Term,"Enter") + end || U <- hard_unicode()], + ok + after + stop_tty(Term) + end. + +shell_xnfix(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + As = lists:duplicate(Cols - Col - 1,"a"), + + try + [begin + check_location(Term, {0, 0}), + send_tty(Term,As), + check_content(Term,[As,$$]), + check_location(Term, {0, Cols - Col - 1}), + send_tty(Term,"a"), + check_location(Term, {0, -Col}), + send_tty(Term,"aaa"), + check_location(Term, {0, -Col + 3}), + [send_tty(Term,"Left") || _ <- lists:seq(1,3 + width(U))], + send_tty(Term,U), + %% a{Cols-1}U\naaaaa + check_content(Term,[lists:duplicate(Cols - Col - 1 - width(U),$a), + U,"\n",lists:duplicate(3+width(U), $a),"$"]), + check_location(Term, {0, -Col}), + send_tty(Term,"Left"), + send_tty(Term,U), + %% a{Cols-1}U\nUaaaaa + check_content(Term,[lists:duplicate(Cols - Col - 1 - width(U),$a), + U,"\n",U,lists:duplicate(3+width(U), $a),"$"]), + check_location(Term, {0, -Col}), + %% send_tty(Term,"Left"), + %% send_tty(Term,"BSpace"), + %% a{Cols-2}U\nUaaaaa + %% check_content(Term,[lists:duplicate(Cols - Col - 2 - width(U),$a), + %% U,"\n",U,lists:duplicate(3+width(U), $a),"$"]), + %% send_tty(Term,"BSpace"), + %% check_content(Term,[lists:duplicate(Cols - Col - 1 - width(U),$a), + %% U,U,"\n",lists:duplicate(3+width(U), $a),"$"]), + %% send_tty(Term,"aa"), + %% check_content(Term,[lists:duplicate(Cols - Col - 2 - width(U),$a), + %% U,"a\n",U,lists:duplicate(3+width(U), $a),"$"]), + %% check_location(Term, {0, -Col}), + send_tty(Term,"C-K"), + check_location(Term, {0, -Col}), + send_tty(Term,"C-A"), + check_location(Term, {-1, 0}), + send_tty(Term,"C-E"), + check_location(Term, {0, -Col}), + send_tty(Term,"Enter"), + ok + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + + +%% Characters that are larger than 2 wide need special handling when they +%% are at the end of the current line. +shell_unicode_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + FirstLine = [U,lists:duplicate(Cols - Col - width(U)*2 + 1,"a")], + OtherLineA = [U,lists:duplicate(Cols - width(U) * 2+1,"a")], + OtherLineB = [U,lists:duplicate(Cols - width(U) * 2+1,"b")], + OtherLineC = [U,lists:duplicate(Cols - width(U) * 2+1,"c")], + OtherLineD = [U,lists:duplicate(Cols - width(U) * 2+1,"d")], + send_tty(Term,FirstLine), + check_content(Term, [FirstLine,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineA), + check_content(Term, [OtherLineA,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineB), + check_content(Term, [OtherLineB,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineC), + check_content(Term, [OtherLineC,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineD), + check_content(Term, [OtherLineD,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,"C-A"), + check_location(Term, {-4, 0}), %% Broken + send_tty(Term,"Right"), + check_location(Term, {-4, width(U)}), %% Broken + + send_tty(Term,"DC"), %% Broken + check_content(Term, ["a.*",U,"$"]), + check_content(Term, ["^b.*",U,"c$"]), + check_content(Term, ["^c.*",U,"dd$"]), + + send_tty(Term,"a"), + check_content(Term, [FirstLine,$$]), + check_content(Term, [OtherLineA,$$]), + check_content(Term, [OtherLineB,$$]), + check_content(Term, [OtherLineC,$$]), + check_content(Term, [OtherLineD,$$]), + + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +shell_delete(Config) -> + + Term = start_tty(Config), + + try + + [ begin + send_tty(Term,"a"), + check_content(Term, "> a$"), + check_location(Term, {0, 1}), + send_tty(Term,"BSpace"), + check_location(Term, {0, 0}), + check_content(Term, ">$"), + send_tty(Term,"a"), + send_tty(Term,U), + check_location(Term, {0, width([$a, U])}), + send_tty(Term,"a"), + send_tty(Term,U), + check_location(Term, {0, width([$a,U,$a,U])}), + check_content(Term, ["> a",U,$a,U,"$"]), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + send_tty(Term,"BSpace"), + check_location(Term, {0, width([$a])}), + check_content(Term, ["> aa",U,"$"]), + send_tty(Term,U), + check_location(Term, {0, width([$a,U])}), + send_tty(Term,"Left"), + send_tty(Term,"DC"), + check_location(Term, {0, width([$a])}), + check_content(Term, ["> aa",U,"$"]), + send_tty(Term,"DC"), + send_tty(Term,"DC"), + check_content(Term, ["> a$"]), + send_tty(Term,"C-E"), + check_location(Term, {0, width([$a])}), + send_tty(Term,"BSpace"), + check_location(Term, {0, width([])}) + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +%% When deleting characters at the edge of the screen that are "large", +%% we need to take special care. +shell_delete_unicode_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + send_tty(Term,lists:duplicate(Cols - Col,"a")), + check_content(Term,"> a*$"), + send_tty(Term,[U,U,"aaaaa"]), + check_content(Term,["\n",U,U,"aaaaa$"]), + [send_tty(Term,"Left") || _ <- lists:seq(1,5+2)], + check_location(Term,{0,-Col}), + send_tty(Term,"BSpace"), + check_content(Term,"> a* \n"), + check_location(Term,{-1,Cols - Col - 1}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U,"\n"]), + check_location(Term,{-1,Cols - Col - 2}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U," \n"]), + check_location(Term,{-1,Cols - Col - 3}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U,U,"\n"]), + check_content(Term,["\naaaaa$"]), + check_location(Term,{-1,Cols - Col - 4}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U,U,"a\n"]), + check_content(Term,["\naaaa$"]), + check_location(Term,{-1,Cols - Col - 5}), + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +%% When deleting characters and a "large" characters is changing line we need +%% to take extra care +shell_delete_unicode_not_at_cursor_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + send_tty(Term,lists:duplicate(Cols - Col,"a")), + check_content(Term,"> a*$"), + send_tty(Term,["a",U,"aaaaa"]), + check_content(Term,["\na",U,"aaaaa$"]), + send_tty(Term,"C-A"), + send_tty(Term,"DC"), + check_content(Term,["\n",U,"aaaaa$"]), + send_tty(Term,"DC"), + check_content(Term,["\n",U,"aaaaa$"]), + check_content(Term,["> a* \n"]), + send_tty(Term,"DC"), + check_content(Term,["\naaaaa$"]), + check_content(Term,["> a*",U,"\n"]), + send_tty(Term,"DC"), + check_content(Term,["\naaaa$"]), + check_content(Term,["> a*",U,"a\n"]), + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +%% When deleting characters and a "large" characters is changing line we need +%% to take extra care +shell_update_window_unicode_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + send_tty(Term,lists:duplicate(Cols - Col - width(U) + 1,"a")), + check_content(Term,"> a*$"), + send_tty(Term,[U,"aaaaa"]), + check_content(Term,["> a* ?\n",U,"aaaaa$"]), + tmux(["resize-window -t ",tty_name(Term)," -x ",Cols+1]), + check_content(Term,["> a*",U,"\naaaaa$"]), + tmux(["resize-window -t ",tty_name(Term)," -x ",Cols]), + check_content(Term,["> a* ?\n",U,"aaaaa$"]), + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +shell_transpose(Config) -> + + Term = start_tty(Config), + + Unicode = [[$a]] ++ hard_unicode(), + + try + [ + begin + send_tty(Term,"a"), + [send_tty(Term,[CP]) || CP <- U], + send_tty(Term,"b"), + [[send_tty(Term,[CP]) || CP <- U2] || U2 <- Unicode], + send_tty(Term,"cde"), + check_content(Term, ["a",U,"b",Unicode,"cde$"]), + check_location(Term, {0, width(["a",U,"b",Unicode,"cde"])}), + send_tty(Term,"Home"), + check_location(Term, {0, 0}), + send_tty(Term,"Right"), + send_tty(Term,"Right"), + check_location(Term, {0, 1+width([U])}), + send_tty(Term,"C-T"), + check_content(Term, ["ab",U,Unicode,"cde$"]), + send_tty(Term,"C-T"), + check_content(Term, ["ab",hd(Unicode),U,tl(Unicode),"cde$"]), + [send_tty(Term,"C-T") || _ <- lists:seq(1,length(Unicode)-1)], + check_content(Term, ["ab",Unicode,U,"cde$"]), + send_tty(Term,"C-T"), + check_content(Term, ["ab",Unicode,"c",U,"de$"]), + check_location(Term, {0, width(["ab",Unicode,"c",U])}), + send_tty(Term,"End"), + check_location(Term, {0, width(["ab",Unicode,"c",U,"de"])}), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + send_tty(Term,"BSpace"), + check_content(Term, ["ab",Unicode,"cde$"]), + send_tty(Term,"End"), + send_tty(Term,"Enter") + end || U <- Unicode], + ok + after + stop_tty(Term), + ok + end. + +shell_search(C) -> + + Term = start_tty(C), + {_Row, Cols} = get_location(Term), + + try + send_tty(Term,"a"), + send_tty(Term,"."), + send_tty(Term,"Enter"), + send_tty(Term,"'"), + send_tty(Term,"a"), + send_tty(Term,[16#1f600]), + send_tty(Term,"'"), + send_tty(Term,"."), + send_tty(Term,"Enter"), + check_location(Term, {0, 0}), + send_tty(Term,"C-r"), + check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }), + send_tty(Term,"C-a"), + check_location(Term, {0, width(C, "'a😀'.")}), + send_tty(Term,"Enter"), + send_tty(Term,"C-r"), + check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }), + send_tty(Term,"a"), + check_location(Term, {0, - Cols + width(C, "(search)`a': 'a😀'.") }), + send_tty(Term,"C-r"), + check_location(Term, {0, - Cols + width(C, "(search)`a': a.") }), + send_tty(Term,"BSpace"), + check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }), + send_tty(Term,"BSpace"), + check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }), + ok + after + stop_tty(Term), + ok + end. + +shell_insert(Config) -> + Term = start_tty(Config), + + try + send_tty(Term,"abcdefghijklm"), + check_content(Term, "abcdefghijklm$"), + check_location(Term, {0, 13}), + send_tty(Term,"Home"), + send_tty(Term,"Right"), + send_tty(Term,"C-T"), + send_tty(Term,"C-T"), + send_tty(Term,"C-T"), + send_tty(Term,"C-T"), + check_content(Term, "bcdeafghijklm$"), + send_tty(Term,"End"), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + send_tty(Term,"BSpace"), + check_content(Term, "bcdeafghijlm$"), + ok + after + stop_tty(Term) + end. + +shell_update_window(Config) -> + Term = start_tty(Config), + + Text = lists:flatten(["abcdefghijklmabcdefghijklm"]), + {_Row, Col} = get_location(Term), + + try + send_tty(Term,Text), + check_content(Term,Text), + check_location(Term, {0, width(Text)}), + tmux(["resize-window -t ",tty_name(Term)," -x ",width(Text)+Col+1]), + send_tty(Term,"a"), + check_location(Term, {0, -Col}), + send_tty(Term,"BSpace"), + tmux(["resize-window -t ",tty_name(Term)," -x ",width(Text)+Col]), + %% xnfix bug! at least in tmux... seems to work in iTerm as it does not + %% need xnfix when resizing + check_location(Term, {0, -Col}), + tmux(["resize-window -t ",tty_name(Term)," -x ",width(Text) div 2 + Col]), + check_location(Term, {0, -Col + width(Text) div 2}), + ok + after + stop_tty(Term) + end. + +shell_huge_input(Config) -> + Term = start_tty(Config), + + ManyUnicode = lists:duplicate(100,hard_unicode()), + + try + send_tty(Term,ManyUnicode), + check_content(Term, hard_unicode_match(Config) ++ "$", + #{ replace => {"\n",""} }), + send_tty(Term,"Enter"), + ok + after + stop_tty(Term) + end. + +%% Test that the shell works when invalid utf-8 (aka latin1) is sent to it +shell_invalid_unicode(Config) -> + Term = start_tty(Config), + + InvalidUnicode = <<$å,$ä,$ö>>, %% åäö in latin1 + + try + send_tty(Term,hard_unicode()), + check_content(Term, hard_unicode() ++ "$"), + send_tty(Term,"Enter"), + check_content(Term, "illegal character"), + %% Send invalid utf-8 + send_stdin(Term,InvalidUnicode), + %% Check that the utf-8 was echoed + check_content(Term, "\\\\345\\\\344\\\\366$"), + send_tty(Term,"Enter"), + %% Check that the terminal entered "latin1" mode + send_tty(Term,"😀한."), + check_content(Term, "\\Q\\360\\237\\230\\200\\355\\225\\234.\\E$"), + send_tty(Term,"Enter"), + %% Check that we can reset the encoding to unicode + send_tty(Term,"io:setopts([{encoding,unicode}])."), + send_tty(Term,"Enter"), + check_content(Term, "\nok\n"), + send_tty(Term,"😀한"), + check_content(Term, "😀한$"), + ok + after + stop_tty(Term), + ok + end. + + +%% Test the we can handle ansi insert, navigation and delete +%% We currently can not so skip this test +shell_support_ansi_input(Config) -> + + Term = start_tty(Config), + + BoldText = "\e[;1m", + ClearText = "\e[0m", + + try + send_stdin(Term,["{",BoldText,"a😀b",ClearText,"}"]), + timer:sleep(1000), + try check_location(Term, {0, width("{1ma😀bm}")}) of + _ -> + throw({skip, "Do not support ansi input"}) + catch _:_ -> + ok + end, + check_location(Term, {0, width("{a😀b}")}), + check_content(fun() -> get_content(Term,"-e") end, + ["{", BoldText, "a😀b", ClearText, "}"]), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + check_location(Term, {0, width("{a😀")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{")}), + send_tty(Term,"End"), + send_tty(Term,"BSpace"), + send_tty(Term,"BSpace"), + check_content(Term, ["{", BoldText, "a😀"]), + ok + after + stop_tty(Term), + ok + end. + +%% Test the we can handle invalid ansi escape chars. +%% tmux cannot handle this... so we test this using to_erl +shell_invalid_ansi(_Config) -> + + InvalidAnsiPrompt = ["\e]94m",54620,44397,50612,47,51312,49440,47568,"\e]0m"], + + rtnode:run( + [{eval, fun() -> application:set_env( + stdlib, shell_prompt_func_test, + fun() -> InvalidAnsiPrompt end) + end }, + {putline,"a."}, + {expect, "a[.]"}, + {expect, ["\\Q",InvalidAnsiPrompt,"\\E"]}], + "", "", + ["-pz",filename:dirname(code:which(?MODULE)), + "-connect_all","false", + "-kernel","logger_level","all", + "-kernel","shell_history","disabled", + "-kernel","prevent_overlapping_partitions","false", + "-eval","shell:prompt_func({interactive_shell_SUITE,prompt})." + ]). + + +%% We test that suspending of `erl` and then resuming restores the shell +shell_suspend(Config) -> + + Name = peer:random_name(proplists:get_value(tc_path,Config)), + %% In order to suspend `erl` we need it to run in a shell that has job control + %% so we start the peer within a tmux window instead of having it be the original + %% process. + os:cmd("tmux new-window -n " ++ Name ++ " -d -- bash --norc"), + + Peer = #{ name => Name, + post_process_args => + fun(["new-window","-n",_,"-d","--"|CmdAndArgs]) -> + FlatCmdAndArgs = + lists:join( + " ",[[$',A,$'] || A <- CmdAndArgs]), + ["send","-t",Name,lists:flatten(FlatCmdAndArgs),"Enter"] + end + }, + + + Term = start_tty([{peer, Peer}|Config]), + + try + send_tty(Term, hard_unicode()), + check_content(Term,["2> ",hard_unicode(),"$"]), + send_tty(Term, "C-Z"), + check_content(Term,"\\Q[1]+\\E\\s*Stopped"), + send_tty(Term, "fg"), + send_tty(Term, "Enter"), + send_tty(Term, "C-L"), + check_content(Term,["2> ",hard_unicode(),"$"]), + check_location(Term,{0,width(hard_unicode())}), + ok + after + stop_tty(Term), + ok + end. + +%% We test that suspending of `erl` and then resuming restores the shell +shell_full_queue(Config) -> + + %% In order to fill the read buffer of the terminal we need to get a + %% bit creative. We first need to start erl in bash in order to be + %% able to get access to job control for suspended processes. + %% We then also wrap `erl` in `unbuffer -p` so that we can suspend + %% that program in order to block writing to stdout for a while. + + Name = peer:random_name(proplists:get_value(tc_path,Config)), + os:cmd("tmux new-window -n " ++ Name ++ " -d -- bash --norc"), + + Peer = #{ name => Name, + post_process_args => + fun(["new-window","-n",_,"-d","--"|CmdAndArgs]) -> + FlatCmdAndArgs = ["unbuffer -p "] ++ + lists:join( + " ",[[$',A,$'] || A <- CmdAndArgs]), + ["send","-t",Name,lists:flatten(FlatCmdAndArgs),"Enter"] + end + }, + + + Term = start_tty([{peer, Peer}|Config]), + + UnbufferedPid = os:cmd("ps -o ppid= -p " ++ rpc(Term,os,getpid,[])), + + WriteUntilStopped = + fun F(Char) -> + rpc(Term,io,format,[user,[Char],[]]), + put(bytes,get(bytes,0)+1), + receive + stop -> + rpc(Term,io,format,[user,[Char+1],[]]) + after 0 -> F(Char) + end + end, + + WaitUntilBlocked = + fun(Pid, Ref) -> + (fun F(Cnt) -> + receive + {'DOWN',Ref,_,_,_} = Down -> + ct:fail({io_format_did_not_block, Down}) + after 1000 -> + ok + end, + case process_info(Pid,dictionary) of + {dictionary,[{bytes,Cnt}]} -> + ct:log("Bytes until blocked: ~p~n",[Cnt]), + %% Add one extra byte as for + %% the current blocking call + Cnt + 1; + {dictionary,[{bytes,NewCnt}]} -> + F(NewCnt) + end + end)(0) + end, + + try + %% First test that we can suspend and then resume + os:cmd("kill -TSTP " ++ UnbufferedPid), + check_content(Term,"\\Q[1]+\\E\\s*Stopped"), + {Pid, Ref} = spawn_monitor(fun() -> WriteUntilStopped($a) end), + WaitUntilBlocked(Pid, Ref), + send_tty(Term, "fg"), + send_tty(Term, "Enter"), + Pid ! stop, + check_content(Term,"b$"), + + send_tty(Term, "."), + send_tty(Term, "Enter"), + + %% Then we test that all characters are written when system + %% is terminated just after writing + {ok,Cols} = rpc(Term,io,columns,[user]), + send_tty(Term, "Enter"), + os:cmd("kill -TSTP " ++ UnbufferedPid), + check_content(Term,"\\Q[1]+\\E\\s*Stopped"), + {Pid2, Ref2} = spawn_monitor(fun() -> WriteUntilStopped($c) end), + Bytes = WaitUntilBlocked(Pid2, Ref2) - 1, + stop_tty(Term), + send_tty(Term, "fg"), + send_tty(Term, "Enter"), + check_content( + fun() -> + tmux(["capture-pane -p -S - -E - -t ",tty_name(Term)]) + end, lists:flatten([lists:duplicate(Cols,$c) ++ "\n" || + _ <- lists:seq(1,(Bytes) div Cols)] + ++ [lists:duplicate((Bytes) rem Cols,$c)])), + ct:log("~ts",[tmux(["capture-pane -p -S - -E - -t ",tty_name(Term)])]), + ok + after + stop_tty(Term), + ok + end. + +get(Key,Default) -> + case get(Key) of + undefined -> + Default; + Value -> + Value + end. + +%% A list of unicode graphemes that are notoriously hard to render +hard_unicode() -> + ZWJ = + case os:type() of + %% macOS has very good rendering of ZWJ, + %% but the cursor does not agree with it.. + {unix, darwin} -> []; + _ -> [[16#1F91A,16#1F3FC]] % Hand with skintone 🤚🏼 + end, + [[16#1f600], % Smilie 😀 + "한", % Hangul + "Z̤͔ͧ̑̓","ä͖̭̈̇","lͮ̒ͫ","ǧ̗͚̚","o̙̔ͮ̇͐̇" %% Vertically stacked chars + %%"👩‍👩", % Zero width joiner + %%"👩‍👩‍👧‍👦" % Zero width joiner + | ZWJ]. + +hard_unicode_match(Config) -> + ["\\Q",[unicode_to_octet(Config, U) || U <- hard_unicode()],"\\E"]. + +unicode_to_octet(Config, U) -> + case ?config(encoding,Config) of + unicode -> U; + latin1 -> unicode_to_octet(U) + end. + +unicode_to_octet(U) -> + [if Byte >= 128 -> [$\\,integer_to_list(Byte,8)]; + true -> Byte + end || <> <= unicode:characters_to_binary(U)]. + +unicode_to_hex(Config, U) -> + case ?config(encoding,Config) of + unicode -> U; + latin1 -> unicode_to_hex(U) + end. + +unicode_to_hex(U) when is_integer(U) -> + unicode_to_hex([U]); +unicode_to_hex(Us) -> + [if U < 128 -> U; + U < 512 -> ["\\",integer_to_list(U,8)]; + true -> ["\\x{",integer_to_list(U,16),"}"] + end || U <- Us]. + +width(C, Str) -> + case ?config(encoding, C) of + unicode -> width(Str); + latin1 -> width(unicode_to_octet(Str)) + end. +width(Str) -> + lists:sum( + [npwcwidth(CP) || CP <- lists:flatten(Str)]). + +%% Poor mans character width +npwcwidth(16#D55C) -> + 2; %% 한 +npwcwidth(16#1f91A) -> + 2; %% hand +npwcwidth(16#1F3Fc) -> + 2; %% Skintone +npwcwidth(16#1f600) -> + 2; %% smilie +npwcwidth(C) -> + case lists:member(C, [775,776,780,785,786,787,788,791,793,794, + 804,813,848,852,854,858,871,875,878]) of + true -> + 0; + false -> + 1 + end. + +-record(tmux, {peer, node, name, orig_location }). + +tmux([Cmd|_] = Command) when is_list(Cmd) -> + tmux(lists:concat(Command)); +tmux(Command) -> + string:trim(os:cmd(["tmux ",Command])). + +rpc(#tmux{ node = N }, M, F, A) -> + erpc:call(N, M, F, A). + +start_tty(Config) -> + + %% Start an node in an xterm + %% {ok, XPeer, _XNode} = ?CT_PEER(#{ exec => + %% {os:find_executable("xterm"), + %% ["-hold","-e",os:find_executable("erl")]}, + %% detached => false }), + + Name = maps:get(name,proplists:get_value(peer, Config, #{}), + peer:random_name(proplists:get_value(tc_path, Config))), + + Envs = lists:flatmap(fun({Key,Value}) -> + ["-env",Key,Value] + end, proplists:get_value(env,Config,[])), + + ExecArgs = case os:getenv("TMUX_DEBUG") of + "strace" -> + STraceLog = filename:join(proplists:get_value(priv_dir,Config), + Name++".strace"), + ct:pal("Link to strace: file://~ts", [STraceLog]), + [os:find_executable("strace"),"-f", + "-o",STraceLog, + "-e","trace=all", + "-e","read=0,1,2", + "-e","write=0,1,2" + ] ++ string:split(ct:get_progname()," ",all); + "rr" -> + [os:find_executable("cerl"),"-rr"]; + _ -> + string:split(ct:get_progname()," ",all) + end, + DefaultPeerArgs = #{ name => Name, + exec => + {os:find_executable("tmux"), + ["new-window","-n",Name,"-d","--"] ++ ExecArgs }, + + args => ["-pz",filename:dirname(code:which(?MODULE)), + "-connect_all","false", + "-kernel","logger_level","all", + "-kernel","shell_history","disabled", + "-kernel","prevent_overlapping_partitions","false", + "-eval","shell:prompt_func({interactive_shell_SUITE,prompt})." + ] ++ Envs, + detached => false + }, + + {ok, Peer, Node} = + ?CT_PEER(maps:merge(proplists:get_value(peer,Config,#{}), + DefaultPeerArgs)), + + Self = self(), + + %% By default peer links with the starter. For these TCs we however only + %% want the peer to die if we die, so we create a "unidirection link" using + %% monitors. + spawn(fun() -> + TCRef = erlang:monitor(process, Self), + PeerRef = erlang:monitor(process, Peer), + receive + {'DOWN',TCRef,_,_,Reason} -> + exit(Peer, Reason); + {'DOWN',PeerRef,_,_,_} -> + ok + end + end), + unlink(Peer), + + Prompt = fun() -> ["\e[94m",54620,44397,50612,47,51312,49440,47568,"\e[0m"] end, + erpc:call(Node, application, set_env, + [stdlib, shell_prompt_func_test, + proplists:get_value(shell_prompt_func_test, Config, Prompt)]), + + "" = tmux(["set-option -t ",Name," remain-on-exit on"]), + Term = #tmux{ peer = Peer, node = Node, name = Name }, + {Rows, _} = get_window_size(Term), + + %% We send a lot of newlines here in order for the number of rows + %% in the window to be max so that we can predict what the cursor + %% position is. + [send_tty(Term,"\n") || _ <- lists:seq(1, Rows)], + + %% We start tracing on the remote node in order to help debugging + TraceLog = filename:join(proplists:get_value(priv_dir,Config),Name++".trace"), + ct:log("Link to trace: file://~ts",[TraceLog]), + + spawn(Node, + fun() -> + {ok, _} = dbg:tracer(file,TraceLog), + %% dbg:p(whereis(user_drv),[c,m]), + %% dbg:p(whereis(user_drv_writer),[c,m]), + %% dbg:p(whereis(user_drv_reader),[c,m]), + %% dbg:tp(user_drv,x), + %% dbg:tp(prim_tty,x), + %% dbg:tpl(prim_tty,write_nif,x), + %% dbg:tpl(prim_tty,read_nif,x), + monitor(process, Self), + receive _ -> ok end + end), + + %% We enter an 'a' here so that we can get the correct orig position + %% with an alternative prompt. + send_tty(Term,"a.\n"), + check_content(Term,"2>$"), + OrigLocation = get_location(Term), + Term#tmux{ orig_location = OrigLocation }. + +prompt(L) -> + N = proplists:get_value(history, L, 0), + Fun = application:get_env(stdlib, shell_prompt_func_test, + fun() -> atom_to_list(node()) end), + io_lib:format("(~ts)~w> ",[Fun(),N]). + +stop_tty(Term) -> + catch peer:stop(Term#tmux.peer), + ct:log("~ts",[get_content(Term, "-e")]), +% "" = tmux("kill-window -t " ++ Term#tmux.name), + ok. + +tty_name(Term) -> + Term#tmux.name. + +send_tty(Term, "Home") -> + %% https://stackoverflow.com/a/55616731 + send_tty(Term,"Escape"), + send_tty(Term,"OH"); +send_tty(Term, "End") -> + send_tty(Term,"Escape"), + send_tty(Term,"OF"); +send_tty(#tmux{ name = Name } = _Term,Value) -> + [Head | Quotes] = string:split(Value, "'", all), + "" = tmux("send -t " ++ Name ++ " '" ++ Head ++ "'"), + [begin + "" = tmux("send -t " ++ Name ++ " \"'\""), + "" = tmux("send -t " ++ Name ++ " '" ++ V ++ "'") + end || V <- Quotes]. + +%% We use send_stdin for testing of things that we cannot sent via +%% the tmux send command, such as invalid unicode +send_stdin(Term, Chars) when is_binary(Chars) -> + rpc(Term,erlang,display_string,[stdin,Chars]); +send_stdin(Term, Chars) -> + send_stdin(Term, iolist_to_binary(unicode:characters_to_binary(Chars))). + +check_location(Term, Where) -> + check_location(Term, Where, 5). +check_location(#tmux{ orig_location = {OrigRow, OrigCol} = Orig } = Term, + {AdjRow, AdjCol} = Where, Attempt) -> + NewLocation = get_location(Term), + case {OrigRow+AdjRow,OrigCol+AdjCol} of + NewLocation -> NewLocation; + _ when Attempt =:= 0 -> + {NewRow, NewCol} = NewLocation, + ct:fail({wrong_location, {expected,{AdjRow, AdjCol}}, + {got,{NewRow - OrigRow, NewCol - OrigCol}, + {NewLocation, Orig}}}); + _ -> + timer:sleep(50), + check_location(Term, Where, Attempt -1) + end. + +get_location(Term) -> + RowAndCol = tmux("display -pF '#{cursor_y} #{cursor_x}' -t "++Term#tmux.name), + [Row, Col] = string:lexemes(string:trim(RowAndCol,both)," "), + {list_to_integer(Row), list_to_integer(Col)}. + +get_window_size(Term) -> + RowAndCol = tmux("display -pF '#{window_height} #{window_width}' -t "++Term#tmux.name), + [Row, Col] = string:lexemes(string:trim(RowAndCol,both)," "), + {list_to_integer(Row), list_to_integer(Col)}. + +check_content(Term, Match) -> + check_content(Term, Match, #{}). +check_content(Term, Match, Opts) when is_map(Opts) -> + check_content(Term, Match, Opts, 5). +check_content(Term, Match, Opts, Attempt) -> + OrigContent = case Term of + #tmux{} -> get_content(Term); + Fun when is_function(Fun,0) -> Fun() + end, + Content = case maps:find(replace, Opts) of + {ok, {RE,Repl} } -> + re:replace(OrigContent, RE, Repl, [global]); + error -> + OrigContent + end, + case re:run(string:trim(Content, both), lists:flatten(Match), [unicode]) of + {match,_} -> + ok; + _ when Attempt =:= 0 -> + io:format("Failed to find '~ts' in ~n'~ts'~n", + [unicode:characters_to_binary(Match), Content]), + io:format("Failed to find '~w' in ~n'~w'~n", + [unicode:characters_to_binary(Match), Content]), + ct:fail(nomatch); + _ -> + timer:sleep(500), + check_content(Term, Match, Opts, Attempt - 1) + end. + +get_content(Term) -> + get_content(Term, ""). +get_content(#tmux{ name = Name }, Args) -> + Content = unicode:characters_to_binary(tmux("capture-pane -p " ++ Args ++ " -t " ++ Name)), + case string:split(Content,"a.\na") of + [_Ignore,C] -> + C; + [C] -> + C + end. + %% Tests that exit of initial shell restarts shell. exit_initial(Config) when is_list(Config) -> case proplists:get_value(default_shell, Config) of @@ -868,25 +1937,26 @@ remsh_longnames(Config) when is_list(Config) -> "@127.0.0.1"; _ -> "" end, - case rtnode:start(" -name " ++ atom_to_list(?FUNCTION_NAME)++Domain) of + Name = peer:random_name(?FUNCTION_NAME), + case rtnode:start(" -name " ++ Name ++ Domain) of {ok, _SRPid, STPid, SNode, SState} -> try {ok, _CRPid, CTPid, CNode, CState} = rtnode:start("-name undefined" ++ Domain ++ - " -remsh " ++ atom_to_list(?FUNCTION_NAME)), + " -remsh " ++ Name), try ok = rtnode:send_commands( SNode, STPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"}], 1), + {expect, "\\Q" ++ Name ++ "\\E"}], 1), ok = rtnode:send_commands( CNode, CTPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"} | quit_hosting_node()], 1) + {expect, "\\Q" ++ Name ++ "\\E"} | quit_hosting_node()], 1) after rtnode:dump_logs(rtnode:stop(CState)) end @@ -900,8 +1970,9 @@ remsh_longnames(Config) when is_list(Config) -> %% Test that -remsh works without epmd. remsh_no_epmd(Config) when is_list(Config) -> EPMD_ARGS = "-start_epmd false -erl_epmd_port 12345 ", + Name = ?CT_PEER_NAME(), case rtnode:start([],"ERL_EPMD_PORT=12345 ", - EPMD_ARGS ++ " -sname " ++ atom_to_list(?FUNCTION_NAME)) of + EPMD_ARGS ++ " -sname " ++ Name) of {ok, _SRPid, STPid, SNode, SState} -> try ok = rtnode:send_commands( @@ -909,17 +1980,17 @@ remsh_no_epmd(Config) when is_list(Config) -> STPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"}], 1), + {expect, "\\Q" ++ Name ++ "\\E"}], 1), {ok, _CRPid, CTPid, CNode, CState} = rtnode:start([],"ERL_EPMD_PORT=12345 ", - EPMD_ARGS ++ " -remsh "++atom_to_list(?FUNCTION_NAME)), + EPMD_ARGS ++ " -remsh "++Name), try ok = rtnode:send_commands( CNode, CTPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"} | quit_hosting_node()], 1) + {expect, "\\Q" ++ Name ++ "\\E"} | quit_hosting_node()], 1) after rtnode:stop(CState) end diff --git a/lib/kernel/test/rtnode.erl b/lib/kernel/test/rtnode.erl index af818557de1e..cee494e2b8d6 100644 --- a/lib/kernel/test/rtnode.erl +++ b/lib/kernel/test/rtnode.erl @@ -19,7 +19,7 @@ %% -module(rtnode). --export([run/1, run/2, run/3, run/4, start/1, start/3, send_commands/3, stop/1, +-export([run/1, run/2, run/3, run/4, start/1, start/3, send_commands/4, stop/1, start_runerl_command/3, check_logs/3, check_logs/4, read_logs/1, dump_logs/1, get_default_shell/0, get_progs/0, create_tempdir/0, timeout/1]). @@ -50,8 +50,8 @@ run(Commands, Nodename, ErlPrefix) -> run(Commands, Nodename, ErlPrefix, Args) -> case start(Nodename, ErlPrefix, Args) of - {ok, _SPid, CPid, RTState} -> - Res = catch send_commands(CPid, Commands, 1), + {ok, _SPid, CPid, Node, RTState} -> + Res = catch send_commands(Node, CPid, Commands, 1), Logs = stop(RTState), case Res of ok -> @@ -84,11 +84,11 @@ start(Nodename, ErlPrefix, Args) -> lists:join($\s, ErlArgs), Tempdir,Nodename,Args), CPid = start_toerl_server(ToErl,Tempdir,undefined), - {ok, SPid, CPid, {CPid, SPid, ToErl, Tempdir}}; + {ok, SPid, CPid, undefined, {CPid, SPid, ToErl, Tempdir}}; Tempdir -> - SPid = start_peer_runerl_node(RunErl,ErlWArgs,Tempdir,Nodename,Args), + {SPid, Node} = start_peer_runerl_node(RunErl,ErlWArgs,Tempdir,Nodename,Args), CPid = start_toerl_server(ToErl,Tempdir,SPid), - {ok, SPid, CPid, {CPid, SPid, ToErl, Tempdir}} + {ok, SPid, CPid, Node, {CPid, SPid, ToErl, Tempdir}} end end. @@ -108,7 +108,7 @@ stop({CPid, SPid, ToErl, Tempdir}) -> stop_try_harder(ToErl, Tempdir, SPid) -> CPid = start_toerl_server(ToErl, Tempdir, SPid), - ok = send_commands(CPid, + ok = send_commands(undefined, CPid, [{putline,[7]}, {expect, " --> $"}, {putline, "s"}, @@ -125,36 +125,43 @@ timeout(short) -> timeout(normal) -> 10000 * test_server:timetrap_scale_factor(). -send_commands(CPid, [{sleep, X}|T], N) -> +send_commands(Node, CPid, [{sleep, X}|T], N) -> ?dbg({sleep, X}), receive after X -> - send_commands(CPid, T, N+1) + send_commands(Node, CPid, T, N+1) end; -send_commands(CPid, [{expect, Expect}|T], N) when is_list(Expect) -> - send_commands(CPid, [{expect, unicode, Expect}|T], N); -send_commands(CPid, [{expect, Encoding, Expect}|T], N) when is_list(Expect) -> +send_commands(Node, CPid, [{expect, Expect}|T], N) when is_list(Expect) -> + send_commands(Node, CPid, [{expect, unicode, Expect}|T], N); +send_commands(Node, CPid, [{expect, Encoding, Expect}|T], N) when is_list(Expect) -> ?dbg({expect, Expect}), case command(CPid, {expect, Encoding, [Expect], timeout(normal)}) of ok -> - send_commands(CPid, T, N + 1); + send_commands(Node, CPid, T, N + 1); {expect_timeout, Got} -> ct:pal("expect timed out waiting for ~p\ngot: ~p\n", [Expect,Got]), {error, timeout}; Other -> Other end; -send_commands(CPid, [{putline, Line}|T], N) -> - send_commands(CPid, [{putdata, Line ++ "\n"}|T], N); -send_commands(CPid, [{putdata, Data}|T], N) -> +send_commands(Node, CPid, [{putline, Line}|T], N) -> + send_commands(Node, CPid, [{putdata, Line ++ "\n"}|T], N); +send_commands(Node, CPid, [{putdata, Data}|T], N) -> ?dbg({putdata, Data}), case command(CPid, {send_data, Data}) of ok -> - send_commands(CPid, T, N+1); + send_commands(Node, CPid, T, N+1); Error -> Error end; -send_commands(_CPid, [], _) -> +send_commands(Node, CPid, [{eval, Fun}|T], N) -> + case erpc:call(Node, Fun) of + ok -> + send_commands(Node, CPid, T, N+1); + Error -> + Error + end; +send_commands(_Node, _CPid, [], _) -> ok. command(Pid, Req) -> @@ -300,7 +307,7 @@ start_peer_runerl_node(RunErl,Erl,Tempdir,Nodename,Args) -> erlang:raise(E,R,ST) end end), - Peer. + {Peer, Node}. start_toerl_server(ToErl,Tempdir,SPid) -> Pid = spawn(?MODULE,toerl_server,[self(),ToErl,Tempdir,SPid]), From d289ca2695069d782b62e8a58a566a8aa3c5c769 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 11 May 2022 08:04:18 +0200 Subject: [PATCH 04/34] kernel: Remove shell:whereis_evaluator shell:whereis_evaluator was only used by the pman application but was not removed when that application was removed. --- lib/kernel/src/group.erl | 47 ++++++++------------------------ lib/kernel/src/user.erl | 17 ------------ lib/kernel/src/user_drv.erl | 25 ----------------- lib/stdlib/src/shell.erl | 54 ------------------------------------- 4 files changed, 11 insertions(+), 132 deletions(-) diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index 8410c1a4b5a4..f8ce13e947b3 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -22,7 +22,6 @@ %% A group leader process for user io. -export([start/2, start/3, server/3]). --export([interfaces/1]). start(Drv, Shell) -> start(Drv, Shell, []). @@ -40,31 +39,8 @@ server(Drv, Shell, Options) -> proplists:get_value(expand_fun, Options, fun(B) -> edlin_expand:expand(B) end)), put(echo, proplists:get_value(echo, Options, true)), - - start_shell(Shell), - server_loop(Drv, get(shell), []). - -%% Return the pid of user_drv and the shell process. -%% Note: We can't ask the group process for this info since it -%% may be busy waiting for data from the driver. -interfaces(Group) -> - case process_info(Group, dictionary) of - {dictionary,Dict} -> - get_pids(Dict, [], false); - _ -> - [] - end. -get_pids([Drv = {user_drv,_} | Rest], Found, _) -> - get_pids(Rest, [Drv | Found], true); -get_pids([Sh = {shell,_} | Rest], Found, Active) -> - get_pids(Rest, [Sh | Found], Active); -get_pids([_ | Rest], Found, Active) -> - get_pids(Rest, Found, Active); -get_pids([], Found, true) -> - Found; -get_pids([], _Found, false) -> - []. + server_loop(Drv, start_shell(Shell), []). %% start_shell(Shell) %% Spawn a shell with its group_leader from the beginning set to ourselves. @@ -81,9 +57,9 @@ start_shell(Shell) when is_function(Shell) -> start_shell(Shell) when is_pid(Shell) -> group_leader(self(), Shell), % we are the shells group leader link(Shell), % we're linked to it. - put(shell, Shell); + Shell; start_shell(_Shell) -> - ok. + undefined. start_shell1(M, F, Args) -> G = group_leader(), @@ -92,7 +68,7 @@ start_shell1(M, F, Args) -> Shell when is_pid(Shell) -> group_leader(G, self()), link(Shell), % we're linked to it. - put(shell, Shell); + Shell; Error -> % start failure exit(Error) % let the group process crash end. @@ -104,7 +80,7 @@ start_shell1(Fun) -> Shell when is_pid(Shell) -> group_leader(G, self()), link(Shell), % we're linked to it. - put(shell, Shell); + Shell; Error -> % start failure exit(Error) % let the group process crash end. @@ -127,7 +103,7 @@ server_loop(Drv, Shell, Buf0) -> server_loop(Drv, Shell, Buf0); {'EXIT',Drv,interrupt} -> %% Send interrupt to the shell. - exit_shell(interrupt), + exit_shell(Shell, interrupt), server_loop(Drv, Shell, Buf0); {'EXIT',Drv,R} -> exit(R); @@ -143,11 +119,10 @@ server_loop(Drv, Shell, Buf0) -> server_loop(Drv, Shell, Buf0) end. -exit_shell(Reason) -> - case get(shell) of - undefined -> true; - Pid -> exit(Pid, Reason) - end. +exit_shell(undefined, _Reason) -> + true; +exit_shell(Pid, Reason) -> + exit(Pid, Reason). get_tty_geometry(Drv) -> Drv ! {self(),tty_geometry}, @@ -192,7 +167,7 @@ io_request(Req, From, ReplyAs, Drv, Shell, Buf0) -> %% 'kill' instead of R, since the shell is not always in %% a state where it is ready to handle a termination %% message. - exit_shell(kill), + exit_shell(Shell, kill), exit(R) end. diff --git a/lib/kernel/src/user.erl b/lib/kernel/src/user.erl index 67c2eafdbee5..81048922dfdf 100644 --- a/lib/kernel/src/user.erl +++ b/lib/kernel/src/user.erl @@ -23,7 +23,6 @@ %% Basic standard i/o server for user interface port. -export([start/0, start/1, start_out/0]). --export([interfaces/1]). -define(NAME, user). @@ -55,22 +54,6 @@ start_port(PortSettings) -> register(?NAME, Id), Id. -%% Return the pid of the shell process. -%% Note: We can't ask the user process for this info since it -%% may be busy waiting for data from the port. -interfaces(User) -> - case process_info(User, dictionary) of - {dictionary,Dict} -> - case lists:keysearch(shell, 1, Dict) of - {value,Sh={shell,Shell}} when is_pid(Shell) -> - [Sh]; - _ -> - [] - end; - _ -> - [] - end. - server(Pid) when is_pid(Pid) -> process_flag(trap_exit, true), link(Pid), diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index fa7687bf2ae3..520dba9419dc 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -23,8 +23,6 @@ -export([start/0,start/1,start/2,start/3,server/2,server/3]). --export([interfaces/1]). - -include_lib("kernel/include/logger.hrl"). -define(OP_PUTC,0). @@ -65,26 +63,6 @@ start(Pname, Shell) -> start(Iname, Oname, Shell) -> spawn(user_drv, server, [Iname,Oname,Shell]). - -%% 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. - --spec interfaces(pid()) -> [{'current_group', pid()}]. - -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; - _ -> - [] - end. - %% server(Pid, Shell) %% server(Pname, Shell) %% server(Iname, Oname, Shell) @@ -147,7 +125,6 @@ server1(Iport, Oport, Shell) -> {group:start(self(), Shell),Shell} end, - put(current_group, Curr), Gr = gr_add_cur(Gr1, Curr, Shell1), %% Print some information. io_request({put_chars, unicode, @@ -191,7 +168,6 @@ start_user() -> 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) -> @@ -254,7 +230,6 @@ server_loop(Iport, Oport, Curr, User, Gr, {Resp, IOQ} = IOQueue) -> 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"}], diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index 7de78758b03b..c3d9c83a66ba 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -20,7 +20,6 @@ -module(shell). -export([start/0, start/1, start/2, server/1, server/2, history/1, results/1]). --export([whereis_evaluator/0, whereis_evaluator/1]). -export([start_restricted/1, stop_restricted/0]). -export([local_allowed/3, non_local_allowed/3]). -export([catch_exception/1, prompt_func/1, strings/1]). @@ -62,59 +61,6 @@ start(NoCtrlG, StartSync) -> _ = code:ensure_loaded(user_default), spawn(fun() -> server(NoCtrlG, StartSync) end). -%% Find the pid of the current evaluator process. --spec whereis_evaluator() -> 'undefined' | pid(). - -whereis_evaluator() -> - %% locate top group leader, always registered as user - %% can be implemented by group (normally) or user - %% (if oldshell or noshell) - case whereis(user) of - undefined -> - undefined; - User -> - %% get user_drv pid from group, or shell pid from user - case group:interfaces(User) of - [] -> % old- or noshell - case user:interfaces(User) of - [] -> - undefined; - [{shell,Shell}] -> - whereis_evaluator(Shell) - end; - [{user_drv,UserDrv}] -> - %% get current group pid from user_drv - case user_drv:interfaces(UserDrv) of - [] -> - undefined; - [{current_group,Group}] -> - %% get shell pid from group - GrIfs = group:interfaces(Group), - case lists:keyfind(shell, 1, GrIfs) of - {shell, Shell} -> - whereis_evaluator(Shell); - false -> - undefined - end - end - end - end. - --spec whereis_evaluator(pid()) -> 'undefined' | pid(). - -whereis_evaluator(Shell) -> - case process_info(Shell, dictionary) of - {dictionary,Dict} -> - case lists:keyfind(evaluator, 1, Dict) of - {_, Eval} when is_pid(Eval) -> - Eval; - _ -> - undefined - end; - _ -> - undefined - end. - %% Call this function to start a user restricted shell %% from a normal shell session. -spec start_restricted(Module) -> {'error', Reason} when From 516b3bfebb16116f93856c134d0c19fba0492f9f Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 8 Jun 2022 21:28:04 +0200 Subject: [PATCH 05/34] stdlib: Fix ancestors for group and shell This makes observer better at drawing the application tree --- lib/kernel/src/group.erl | 11 ++++++++--- lib/stdlib/src/shell.erl | 9 ++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index f8ce13e947b3..73434984f171 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -21,16 +21,21 @@ %% A group leader process for user io. --export([start/2, start/3, server/3]). +-export([start/2, start/3, server/4]). start(Drv, Shell) -> start(Drv, Shell, []). start(Drv, Shell, Options) -> - spawn_link(group, server, [Drv, Shell, Options]). + Ancestors = [self() | case get('$ancestors') of + undefined -> []; + Anc -> Anc + end], + spawn_link(group, server, [Ancestors, Drv, Shell, Options]). -server(Drv, Shell, Options) -> +server(Ancestors, Drv, Shell, Options) -> process_flag(trap_exit, true), + _ = [put('$ancestors', Ancestors) || Shell =/= {}], edlin:init(), put(line_buffer, proplists:get_value(line_buffer, Options, group_history:load())), put(read_mode, list), diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index c3d9c83a66ba..c62d5a45d250 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -59,7 +59,14 @@ start(NoCtrlG) -> start(NoCtrlG, StartSync) -> _ = code:ensure_loaded(user_default), - spawn(fun() -> server(NoCtrlG, StartSync) end). + Ancestors = [self() | case get('$ancestors') of + undefined -> []; + Anc -> Anc + end], + spawn(fun() -> + put('$ancestors', Ancestors), + server(NoCtrlG, StartSync) + end). %% Call this function to start a user restricted shell %% from a normal shell session. From 707c8aa582fb55fb6a6bc3bcecbfbf86191541c5 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 12 May 2022 13:59:03 +0200 Subject: [PATCH 06/34] stdlib: Allow any Key/Value in io:setopts The protocol replies enotsup for any unkown, so we can send unknown terms. This is useful when we want to be able to extend the options allowed to be sent. --- lib/stdlib/src/io.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/stdlib/src/io.erl b/lib/stdlib/src/io.erl index 18f6ef3ddee9..b0c2a2e586af 100644 --- a/lib/stdlib/src/io.erl +++ b/lib/stdlib/src/io.erl @@ -213,7 +213,8 @@ get_password(Io) -> -type opt_pair() :: {'binary', boolean()} | {'echo', boolean()} | {'expand_fun', expand_fun()} - | {'encoding', encoding()}. + | {'encoding', encoding()} + | {atom(), term()}. -spec getopts() -> [opt_pair()] | {'error', Reason} when Reason :: term(). From f586260347081b6cd81e17894287c934b7f02e5d Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 30 Mar 2022 17:26:32 +0200 Subject: [PATCH 07/34] stdlib: Fix backspace for empty search Closes #4225 --- lib/stdlib/src/edlin.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/stdlib/src/edlin.erl b/lib/stdlib/src/edlin.erl index 6078c5e67bd1..cbd10ae3bde3 100644 --- a/lib/stdlib/src/edlin.erl +++ b/lib/stdlib/src/edlin.erl @@ -191,9 +191,9 @@ key_map($\^E, none) -> end_of_line; key_map($\^F, none) -> forward_char; key_map($\^H, none) -> backward_delete_char; key_map($\t, none) -> tab_expand; +key_map($\^K, none) -> kill_line; key_map($\^L, none) -> redraw_line; key_map($\n, none) -> new_line; -key_map($\^K, none) -> kill_line; key_map($\r, none) -> new_line; key_map($\^T, none) -> transpose_char; key_map($\^U, none) -> ctlu; @@ -320,9 +320,9 @@ do_op({search, backward_delete_char}, [_|Bef], Aft, Rs) -> {{Bef,NAft}, [{insert_chars, unicode, NAft}, {delete_chars,-Offset}|Rs], search}; -do_op({search, backward_delete_char}, [], _Aft, Rs) -> - Aft="': ", - {{[],Aft}, Rs, search}; +do_op({search, backward_delete_char}, [], Aft, Rs) -> + NAft="': ", + {{[],NAft}, [{insert_chars, unicode, NAft}, {delete_chars,-cp_len(Aft)}|Rs], search}; do_op({search, skip_up}, Bef, Aft, Rs) -> Offset= cp_len(Aft), NAft = "': ", From e8849e6746f76aa5babc18b04cde21d398f09ee4 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 11 May 2022 08:05:11 +0200 Subject: [PATCH 08/34] kernel: Make `user` configure correct standard_error --- lib/kernel/src/standard_error.erl | 41 +++++++++++++----------- lib/kernel/src/user_drv.erl | 10 ++++++ lib/kernel/test/standard_error_SUITE.erl | 4 ++- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/lib/kernel/src/standard_error.erl b/lib/kernel/src/standard_error.erl index 1aad064392b9..58831f0ba71b 100644 --- a/lib/kernel/src/standard_error.erl +++ b/lib/kernel/src/standard_error.erl @@ -66,6 +66,7 @@ server(PortName,PortSettings) -> run(P) -> put(encoding, latin1), + put(onlcr, false), server_loop(P). server_loop(Port) -> @@ -161,7 +162,7 @@ io_request({get_geometry,rows},Port) -> io_request(getopts, _Port) -> getopts(); io_request({setopts,Opts}, _Port) when is_list(Opts) -> - setopts(Opts); + do_setopts(Opts); io_request({requests,Reqs}, Port) -> io_requests(Reqs, {ok,ok}, Port); io_request(R, _Port) -> %Unknown request @@ -203,19 +204,27 @@ put_chars(Chars, Port) when is_binary(Chars) -> {ok,ok}. %% setopts -setopts(Opts0) -> +do_setopts(Opts0) -> Opts = expand_encoding(Opts0), case check_valid_opts(Opts) of true -> - do_setopts(Opts); + lists:foreach( + fun({encoding, Enc}) -> + put(encoding, Enc); + ({onlcr, Bool}) -> + put(onlcr, Bool) + end, Opts), + {ok, ok}; false -> {error,{error,enotsup}} end. check_valid_opts([]) -> true; -check_valid_opts([{encoding,Valid}|T]) when Valid =:= unicode; - Valid =:= utf8; Valid =:= latin1 -> +check_valid_opts([{encoding,Valid}|T]) when Valid =:= unicode; Valid =:= utf8; + Valid =:= latin1 -> + check_valid_opts(T); +check_valid_opts([{onlcr,Bool}|T]) when is_boolean(Bool) -> check_valid_opts(T); check_valid_opts(_) -> false. @@ -226,27 +235,21 @@ expand_encoding([latin1 | T]) -> [{encoding,latin1} | expand_encoding(T)]; expand_encoding([unicode | T]) -> [{encoding,unicode} | expand_encoding(T)]; +expand_encoding([utf8 | T]) -> + [{encoding,unicode} | expand_encoding(T)]; +expand_encoding([{encoding,utf8} | T]) -> + [{encoding,unicode} | expand_encoding(T)]; expand_encoding([H|T]) -> [H|expand_encoding(T)]. -do_setopts(Opts) -> - case proplists:get_value(encoding, Opts) of - Valid when Valid =:= unicode; Valid =:= utf8 -> - put(encoding, unicode); - latin1 -> - put(encoding, latin1); - undefined -> - ok - end, - {ok,ok}. - getopts() -> Uni = {encoding,get(encoding)}, - {ok,[Uni]}. + Onlcr = {onlcr, get(onlcr)}, + {ok,[Uni, Onlcr]}. wrap_characters_to_binary(Chars,From,To) -> - TrNl = (whereis(user_drv) =/= undefined), - Limit = case To of + TrNl = get(onlcr), + Limit = case To of latin1 -> 255; _Else -> diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 520dba9419dc..aed09f6125bc 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -92,6 +92,16 @@ server(Iname, Oname, Shell) -> end. server1(Iport, Oport, Shell) -> + + Encoding = + case get_unicode_state(Iport) of + true -> unicode; + false -> latin1 + end, + + %% Initialize standard_error + ok = io:setopts(standard_error, [{encoding, Encoding}, {onlcr,true}]), + put(eof, false), %% Start user and initial shell. User = start_user(), diff --git a/lib/kernel/test/standard_error_SUITE.erl b/lib/kernel/test/standard_error_SUITE.erl index 1d9026dc58ec..34bb880db8ed 100644 --- a/lib/kernel/test/standard_error_SUITE.erl +++ b/lib/kernel/test/standard_error_SUITE.erl @@ -34,8 +34,10 @@ badarg(Config) when is_list(Config) -> true = erlang:is_process_alive(whereis(standard_error)), ok. +%% Check that standard_out and standard_error have the same encoding getopts(Config) when is_list(Config) -> - [{encoding,latin1}] = io:getopts(standard_error), + Encoding = proplists:get_value(encoding, io:getopts(user)), + Encoding = proplists:get_value(encoding, io:getopts(standard_error)), ok. %% Test that writing a lot of output to standard_error does not cause the From 9fff925758f86c2c35590e5de55f3faf5182f74e Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Fri, 4 Feb 2022 09:33:13 +0100 Subject: [PATCH 09/34] stdlib: Delete dead edlin code --- lib/stdlib/src/edlin.erl | 145 --------------------------------------- 1 file changed, 145 deletions(-) diff --git a/lib/stdlib/src/edlin.erl b/lib/stdlib/src/edlin.erl index cbd10ae3bde3..97088dd36147 100644 --- a/lib/stdlib/src/edlin.erl +++ b/lib/stdlib/src/edlin.erl @@ -621,148 +621,3 @@ cp_len(Str) -> cp_len([GC|R], Len) -> cp_len(R, Len + gc_len(GC)); cp_len([], Len) -> Len. - -%% %% expand(CurrentBefore) -> -%% %% {yes,Expansion} | no -%% %% Try to expand the word before as either a module name or a function -%% %% name. We can handle white space around the seperating ':' but the -%% %% function name must be on the same line. CurrentBefore is reversed -%% %% and over_word/3 reverses the characters it finds. In certain cases -%% %% possible expansions are printed. - -%% expand(Bef0) -> -%% {Bef1,Word,_} = over_word(Bef0, [], 0), -%% case over_white(Bef1, [], 0) of -%% {[$:|Bef2],_White,_Nwh} -> -%% {Bef3,_White1,_Nwh1} = over_white(Bef2, [], 0), -%% {_,Mod,_Nm} = over_word(Bef3, [], 0), -%% expand_function_name(Mod, Word); -%% {_,_,_} -> -%% expand_module_name(Word) -%% end. - -%% expand_module_name(Prefix) -> -%% match(Prefix, code:all_loaded(), ":"). - -%% expand_function_name(ModStr, FuncPrefix) -> -%% Mod = list_to_atom(ModStr), -%% case erlang:module_loaded(Mod) of -%% true -> -%% L = apply(Mod, module_info, []), -%% case lists:keyfind(exports, 1, L) of -%% {_, Exports} -> -%% match(FuncPrefix, Exports, "("); -%% _ -> -%% no -%% end; -%% false -> -%% no -%% end. - -%% match(Prefix, Alts, Extra) -> -%% Matches = match1(Prefix, Alts), -%% case longest_common_head([N || {N,_} <- Matches]) of -%% {partial, []} -> -%% print_matches(Matches), -%% no; -%% {partial, Str} -> -%% case lists:nthtail(length(Prefix), Str) of -%% [] -> -%% print_matches(Matches), -%% {yes, []}; -%% Remain -> -%% {yes, Remain} -%% end; -%% {complete, Str} -> -%% {yes, lists:nthtail(length(Prefix), Str) ++ Extra}; -%% no -> -%% no -%% end. - -%% %% Print the list of names L in multiple columns. -%% print_matches(L) -> -%% io:nl(), -%% col_print(lists:sort(L)), -%% ok. - -%% col_print([]) -> ok; -%% col_print(L) -> col_print(L, field_width(L), 0). - -%% col_print(X, Width, Len) when Width + Len > 79 -> -%% io:nl(), -%% col_print(X, Width, 0); -%% col_print([{H0,A}|T], Width, Len) -> -%% H = if -%% %% If the second element is an integer, we assume it's an -%% %% arity, and meant to be printed. -%% integer(A) -> -%% H0 ++ "/" ++ integer_to_list(A); -%% true -> -%% H0 -%% end, -%% io:format("~-*s",[Width,H]), -%% col_print(T, Width, Len+Width); -%% col_print([], _, _) -> -%% io:nl(). - -%% field_width([{H,_}|T]) -> field_width(T, length(H)). - -%% field_width([{H,_}|T], W) -> -%% case length(H) of -%% L when L > W -> field_width(T, L); -%% _ -> field_width(T, W) -%% end; -%% field_width([], W) when W < 40 -> -%% W + 4; -%% field_width([], _) -> -%% 40. - -%% match1(Prefix, Alts) -> -%% match1(Prefix, Alts, []). - -%% match1(Prefix, [{H,A}|T], L) -> -%% case prefix(Prefix, Str = atom_to_list(H)) of -%% true -> -%% match1(Prefix, T, [{Str,A}|L]); -%% false -> -%% match1(Prefix, T, L) -%% end; -%% match1(_, [], L) -> -%% L. - -%% longest_common_head([]) -> -%% no; -%% longest_common_head(LL) -> -%% longest_common_head(LL, []). - -%% longest_common_head([[]|_], L) -> -%% {partial, reverse(L)}; -%% longest_common_head(LL, L) -> -%% case same_head(LL) of -%% true -> -%% [[H|_]|_] = LL, -%% LL1 = all_tails(LL), -%% case all_nil(LL1) of -%% false -> -%% longest_common_head(LL1, [H|L]); -%% true -> -%% {complete, reverse([H|L])} -%% end; -%% false -> -%% {partial, reverse(L)} -%% end. - -%% same_head([[H|_]|T1]) -> same_head(H, T1). - -%% same_head(H, [[H|_]|T]) -> same_head(H, T); -%% same_head(_, []) -> true; -%% same_head(_, _) -> false. - -%% all_tails(LL) -> all_tails(LL, []). - -%% all_tails([[_|T]|T1], L) -> all_tails(T1, [T|L]); -%% all_tails([], L) -> L. - -%% all_nil([]) -> true; -%% all_nil([[] | Rest]) -> all_nil(Rest); -%% all_nil(_) -> false. From 6209f143e8db007c5e98b3ffc8306c27b61098ed Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Tue, 28 Jun 2022 09:05:14 +0200 Subject: [PATCH 10/34] gh: Unify building of macOS and iOS --- .github/scripts/build-macos.sh | 21 ++++++++++++++------- .github/workflows/main.yaml | 10 +++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/scripts/build-macos.sh b/.github/scripts/build-macos.sh index 82b07bac7b7c..73c35a6a22d3 100755 --- a/.github/scripts/build-macos.sh +++ b/.github/scripts/build-macos.sh @@ -1,12 +1,19 @@ #!/bin/sh -export MAKEFLAGS=-j$(getconf _NPROCESSORS_ONLN) -export ERL_TOP=`pwd` -export RELEASE_ROOT=$ERL_TOP/release +export MAKEFLAGS="-j$(getconf _NPROCESSORS_ONLN)" +export ERL_TOP="$(pwd)" export ERLC_USE_SERVER=true +export RELEASE_ROOT="$ERL_TOP/release" +BUILD_DOCS=false -./otp_build configure \ - --disable-dynamic-ssl-lib +if [ "$1" = "build_docs" ]; then + BUILD_DOCS=true + shift +fi + +./otp_build configure $* ./otp_build boot -a -./otp_build release -a $RELEASE_ROOT -make release_docs DOC_TARGETS=chunks +./otp_build release -a "$RELEASE_ROOT" +if $BUILD_DOCS; then + make release_docs DOC_TARGETS=chunks +fi diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7bdda4c618e6..e24f94eec5de 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -127,7 +127,7 @@ jobs: tar -xzf ./otp_src.tar.gz export PATH=$PWD/wxWidgets/release/bin:$PATH cd otp - $GITHUB_WORKSPACE/.github/scripts/build-macos.sh + $GITHUB_WORKSPACE/.github/scripts/build-macos.sh build_docs --disable-dynamic-ssl-lib tar -czf otp_macos_$(cat OTP_VERSION)_x86-64.tar.gz -C release . - name: Test Erlang @@ -152,6 +152,7 @@ jobs: runs-on: macos-12 needs: pack steps: + - uses: actions/checkout@v2 - name: Download source archive uses: actions/download-artifact@v2 with: @@ -161,12 +162,7 @@ jobs: run: | tar -xzf ./otp_src.tar.gz cd otp - export ERL_TOP=`pwd` - export MAKEFLAGS="-j$(($(nproc) + 2)) -O" - export ERLC_USE_SERVER=true - ./otp_build configure --xcomp-conf=./xcomp/erl-xcomp-arm64-ios.conf --without-ssl - ./otp_build boot -a - ./otp_build release -a + $GITHUB_WORKSPACE/.github/scripts/build-macos.sh --xcomp-conf=./xcomp/erl-xcomp-arm64-ios.conf --without-ssl - name: Package .xcframework run: | From 4042a96b5290b98d16e060cb0cae4b409d8dceea Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Tue, 28 Jun 2022 09:55:16 +0200 Subject: [PATCH 11/34] otp: Use noinput in test runs for driver_SUITE to work better --- make/test_target_script.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/make/test_target_script.sh b/make/test_target_script.sh index a837533c7a86..7fc9d5b89d3d 100755 --- a/make/test_target_script.sh +++ b/make/test_target_script.sh @@ -316,7 +316,7 @@ then -pz "$ERL_TOP/lib/common_test/test_server" \ -pz "." \ -ct_test_vars "{net_dir,\"\"}" \ - -noshell \ + -noinput \ -sname test_server \ -rsh ssh \ ${ERL_ARGS} @@ -337,7 +337,7 @@ else -pz "$WIN_ERL_TOP/lib/common_test/test_server"\ -pz "."\ -ct_test_vars "{net_dir,\"\"}"\ - -noshell\ + -noinput\ -sname test_server\ -rsh ssh\ ${ERL_ARGS} From 1da39b4ec40aa43222d2b57bf2f3b7edd3a172ce Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Fri, 28 Jan 2022 17:38:59 +0100 Subject: [PATCH 12/34] erts: Fix printing of non-printable characters --- erts/emulator/drivers/unix/ttsl_drv.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erts/emulator/drivers/unix/ttsl_drv.c b/erts/emulator/drivers/unix/ttsl_drv.c index 08ab714153ba..3fb5bdb8fb4d 100644 --- a/erts/emulator/drivers/unix/ttsl_drv.c +++ b/erts/emulator/drivers/unix/ttsl_drv.c @@ -1166,8 +1166,9 @@ static int write_buf(Uint32 *s, int n, int next_char_is_crnl) --n; ++s; } else if (*s & CONTROL_TAG) { + byte c = (byte)*s; outc('^'); - outc(lastput = ((byte) ((*s == 0177) ? '?' : *s | 0x40))); + outc(lastput = ((byte) ((c == 0177 ? '?' : c | 0x40)))); n -= 2; s += 2; } else if (*s & ESCAPED_TAG) { From bf4d035ca1d5d43dba8c0c582e5bb03031b35109 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 11 May 2022 14:40:02 +0200 Subject: [PATCH 13/34] kernel: Refactor user_drv to use gen_statem --- lib/kernel/src/user_drv.erl | 766 ++++++++++++++++++------------------ 1 file changed, 378 insertions(+), 388 deletions(-) diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index aed09f6125bc..af72dc51ead8 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -20,8 +20,59 @@ -module(user_drv). %% Basic interface to a port. - --export([start/0,start/1,start/2,start/3,server/2,server/3]). +%% +%% This is responsible for a couple of things: +%% - Dispatching I/O messages when erl is running as a terminal. +%% 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, UnicodeBinary :: binary()}}` +%% - 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()}} | + %% Move the cursor X characters left or right (negative is left) + {move_rel, -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} | + %% Trigger a terminal "bell" + beep | + %% Execute multiple request() actions + {requests, [request()]}. + +-export_type([message/0]). +-export([start/0, start/1]). + +%% gen_statem state callbacks +-export([init/3,server/3,switch_loop/3]). + +%% gen_statem callbacks +-export([init/1, callback_mode/0]). -include_lib("kernel/include/logger.hrl"). @@ -37,77 +88,57 @@ -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)). +-record(state, { port, user, current_group, groups, queue }). + %% 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. -spec start() -> pid(). start() -> %Default line editing shell - spawn(user_drv, server, ['tty_sl -c -e',{shell,start,[init]}]). - -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]}]). - -start(Pname, Shell) -> - spawn(user_drv, server, [Pname,Shell]). - -start(Iname, Oname, Shell) -> - spawn(user_drv, server, [Iname,Oname,Shell]). - -%% 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) + start(#{}). + +%% Backwards compatibility with pre OTP-26 for Elixir/LFE etc +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} -> + spawn(fun() -> + process_flag(trap_exit, true), + user:start() + end) end. -server(Iname, Oname, Shell) -> +callback_mode() -> state_functions. + +init(Args) -> 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) + case catch open_port({spawn,"tty_sl -c -e"}, [eof]) of + {'EXIT', _Reason} -> + {stop, normal}; + Port -> + {ok, init, {Args, #state{ } }, + {next_event, internal, Port}} end. -server1(Iport, Oport, Shell) -> +init(internal, Port, {Args, State}) -> + + User = start_user(), + + %% Cleanup ancestors so that observer looks nice + put('$ancestors',[User|get('$ancestors')]), + %% Initialize standard_error Encoding = - case get_unicode_state(Iport) of + case get_unicode_state(Port) of true -> unicode; false -> latin1 end, - - %% Initialize standard_error ok = io:setopts(standard_error, [{encoding, Encoding}, {onlcr,true}]), - put(eof, false), - %% Start user and initial shell. - User = start_user(), - Gr1 = gr_add_cur(gr_new(), User, {}), - - {Curr,Shell1} = + %% Initialize the starting shell + {Curr,Shell} = case init:get_argument(remsh) of {ok,[[Node]]} -> ANode = @@ -129,21 +160,31 @@ server1(Iport, Oport, Shell) -> end, RShell = {ANode,shell,start,[]}, - RGr = group:start(self(), RShell, rem_sh_opts(ANode)), - {RGr,RShell}; + {group:start(self(), RShell, rem_sh_opts(ANode)), RShell}; E when E =:= error ; E =:= {ok,[[]]} -> - {group:start(self(), Shell),Shell} + LShell = maps:get(initial_shell, Args, {shell,start,[init]}), + {group:start(self(), LShell), LShell} end, - 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), + Gr1 = gr_add_cur(gr_new(), User, {}), + Gr = gr_add_cur(Gr1, Curr, Shell), - %% Enter the server loop. - server_loop(Iport, Oport, Curr, User, Gr, {false, queue:new()}). + NewState = State#state{ port = Port, current_group = Curr, user = User, + groups = Gr, queue = {false, queue:new()} + }, + + %% Print some information. + 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, + + {next_state, server, NewState, + {next_event, info, + {Curr, {put_chars, unicode, lists:flatten(io_lib:format("~ts\n", [Slogan]))}}}}. append_hostname(Node, LocalNode) -> case string:find(Node,"@") of @@ -161,12 +202,6 @@ rem_sh_opts(Node) -> %% 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(), {}), @@ -175,305 +210,302 @@ start_user() -> User -> User end. - -server_loop(Iport, Oport, User, Gr, IOQueue) -> - Curr = gr_cur_pid(Gr), - 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), - 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) -> +server(info, {Port,{data,Bs}}, State = #state{ port = Port }) -> + UTF8Binary = list_to_binary(Bs), + 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; +server(info, {Port,eof}, State = #state{ port = Port }) -> + State#state.current_group ! {self(),eof}, + keep_state_and_data; +server(info, {Requester,tty_geometry}, #state{ port = Port }) -> + Requester ! {self(),tty_geometry,get_tty_geometry(Port)}, + keep_state_and_data; +server(info, {Requester,get_unicode_state}, #state{ port = Port }) -> + Requester ! {self(),get_unicode_state,get_unicode_state(Port)}, + keep_state_and_data; +server(info, {Requester,set_unicode_state,Bool}, #state{ port = Port }) -> + Requester ! {self(),set_unicode_state,set_unicode_state(Port, Bool)}, + keep_state_and_data; +server(info, Req, State = #state{ user = User, current_group = Curr }) + 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,_,_} + {keep_state, State#state{ queue = handle_req(Req, State#state.port, State#state.queue) }}; +server(info, {Port, ok}, State = #state{ port = Port, queue = {{Origin, Reply}, IOQ} }) -> + %% We get this ok from the port, in io_request we store + %% info about where to send reply at head of queue + Origin ! {reply,Reply}, + {keep_state, State#state{ queue = handle_req(next, Port, {false, IOQ}) }}; +server(info,{'EXIT',Port, _Reason}, #state{ port = Port }) -> + 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', 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 + if Reason =/= normal -> + io_requests([{put_chars,unicode,"*** ERROR: "}], State#state.port); + true -> % exit not caused by error + io_requests([{put_chars,unicode,"*** "}], State#state.port) + end, + io_requests([{put_chars,unicode,"Shell process terminated! "}], State#state.port), + 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 + io_requests([{put_chars,unicode,"***\n"}], State#state.port), + %% 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{ current_group = NewGroup, groups = Gr2 }}; + _ -> % remote shell + io_requests([{put_chars,unicode,"(^G to start new job) ***\n"}], + State#state.port), + {keep_state, State#state{ groups = Gr1 }} + end; + _ -> % not current, just remove it + {keep_state, State#state{ groups = gr_del_pid(State#state.groups, Group) }} + end; +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}, + keep_state_and_data; +server(_, _, _) -> + %% Ignore unknown messages. + keep_state_and_data. + +handle_req(next,Port,{false,IOQ}=IOQueue) -> case queue:out(IOQ) of {empty,_} -> IOQueue; {{value,{Origin,Req}},ExecQ} -> - case io_request(Req, Iport, Oport) of + case io_request(Req,Port) of ok -> - handle_req(next,Iport,Oport,{false,ExecQ}); + handle_req(next,Port,{false,ExecQ}); Reply -> - {{Origin,Reply}, ExecQ} + {{Origin,Reply},ExecQ} end end; -handle_req(Msg,Iport,Oport,{false,IOQ}=IOQueue) -> +handle_req(Msg,Port,{false,IOQ}=IOQueue) -> empty = queue:peek(IOQ), {Origin,Req} = Msg, - case io_request(Req, Iport, Oport) of + case io_request(Req, Port) of ok -> IOQueue; Reply -> {{Origin,Reply}, IOQ} end; -handle_req(Msg,_Iport,_Oport,{Resp, IOQ}) -> +handle_req(Msg,_Port,{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) - 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), +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,[]}), + io_request({put_chars,unicode,"\n"}, State#state.port), + {next_state, server, + State#state{ groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}}; + jcl -> + io_request({put_chars,unicode,"\nUser switch command\n"}, State#state.port), %% 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_and_data, + {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(" --> "), + io_requests(Rs, State#state.port), + {keep_state, {Cont, State}}; +switch_loop(internal, {line, Line}, State) -> + case erl_scan:string(Line) of + {ok, Tokens, _} -> + case switch_cmd(Tokens, State#state.port, State#state.groups) of + {ok, Groups} -> + {next_state, server, + State#state{ current_group = gr_cur_pid(Groups), groups = Groups } }; + retry -> + {keep_state_and_data, + {next_event, internal, line}}; + {retry, Groups} -> + {keep_state, State#state{ current_group = gr_cur_pid(Groups), + groups = Groups }, + {next_event, internal, line}} + end; + {error, _, _} -> + io_request({put_chars,unicode,"Illegal input\n"}, State#state.port), + {keep_state_and_data, + {next_event, internal, line}} + end; +switch_loop(info,{Port,{data,Cs}}, {Cont, State}) -> + case edlin:edit_line(Cs, Cont) of + {done,Line,_Rest, Rs} -> + io_requests(Rs, State#state.port), + {keep_state, State, {next_event, internal, {line, Line}}}; + {undefined,_Char,MoreCs,NewCont,Rs} -> + io_requests(Rs, State#state.port), + io_request(beep, State#state.port), + {keep_state, {NewCont, State}, + {next_event, info, {Port,{data,MoreCs}}}}; + {more_chars,NewCont,Rs} -> + io_requests(Rs, State#state.port), + {keep_state, {NewCont, State}}; + {blink,NewCont,Rs} -> + io_requests(Rs, State#state.port), + {keep_state, {NewCont, State}, 1000} end; -switch_cmd({ok,[{atom,_,i},{integer,_,I}],_}, Iport, Oport, Gr) -> +switch_loop(timeout, _, State) -> + {keep_state_and_data, + {next_state, info,{State#state.port,{data,[]}}}}; +switch_loop(info, _Unknown, _State) -> + {keep_state_and_data, postpone}. + +switch_cmd([{atom,_,Key},{Type,_,Value}], Port, Gr) + when Type =:= atom; Type =:= integer -> + switch_cmd({Key, Value}, Port, Gr); +switch_cmd([{atom,_,Key},{atom,_,V1},{atom,_,V2}], Port, Gr) -> + switch_cmd({Key, V1, V2}, Port, Gr); +switch_cmd([{atom,_,Key}], Port, Gr) -> + switch_cmd(Key, Port, Gr); +switch_cmd([{'?',_}], Port, Gr) -> + switch_cmd(h, Port, Gr); + +switch_cmd(Cmd, Port, Gr) when Cmd =:= c; Cmd =:= i; Cmd =:= k -> + Pid = gr_cur_pid(Gr), + CurrIndex = + case gr_get_info(Gr, Pid) of + undefined -> undefined; + {Ix, _} -> Ix + end, + switch_cmd({Cmd, CurrIndex}, Port, Gr); +switch_cmd({c, I}, Port, Gr0) -> + case gr_set_cur(Gr0, I) of + {ok,Gr} -> {ok, Gr}; + undefined -> unknown_group(Port) + end; +switch_cmd({i, I}, Port, 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) - 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) + unknown_group(Port) end; -switch_cmd({ok,[{atom,_,k},{integer,_,I}],_}, Iport, Oport, Gr) -> +switch_cmd({k, I}, Port, 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(Port) 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, Port, Gr) -> + io_requests(gr_list(Gr), Port), + retry; +switch_cmd({s, Shell}, _Port, 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, Port, Gr) -> + switch_cmd({s, shell}, Port, Gr); +switch_cmd(r, Port, Gr0) -> case is_alive() of true -> Node = pool:get_node(), Pid = group:start(self(), {Node,shell,start,[]}), 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) + io_request({put_chars,unicode,"Node is not alive\n"}, Port), + retry + end; +switch_cmd({r, Node}, Port, Gr) when is_atom(Node)-> + switch_cmd({r, Node, shell}, Port, Gr); +switch_cmd({r,Node,Shell}, Port, Gr0) when is_atom(Node), + is_atom(Shell) -> + case is_alive() of + true -> + Pid = group:start(self(), {Node,Shell,start,[]}), + Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}), + {retry, Gr}; + false -> + io_request({put_chars,unicode,"Node is not alive\n"}, Port), + retry 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, Port, _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); + io_request({put_chars,unicode,"Unknown command\n"}, Port), + retry; 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, Port, _Gr) -> + list_commands(Port), + retry; +switch_cmd([], _Port, _Gr) -> + retry; +switch_cmd(_Ts, Port, _Gr) -> + io_request({put_chars,unicode,"Unknown command\n"}, Port), + retry. + +unknown_group(Port) -> + io_request({put_chars,unicode,"Unknown job\n"}, Port), + retry. + + +list_commands(Port) -> QuitReq = case erlang:system_info(break_ignored) of true -> []; @@ -488,42 +520,20 @@ list_commands(Iport, Oport) -> {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. + Port). % 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 +get_tty_geometry(Port) -> + case (catch port_control(Port,?CTRL_OP_GET_WINSIZE,[])) of List when length(List) =:= 8 -> <> = list_to_binary(List), {W,H}; _ -> error end. -get_unicode_state(Iport) -> - case (catch port_control(Iport,?CTRL_OP_GET_UNICODE_STATE,[])) of +get_unicode_state(Port) -> + case (catch port_control(Port,?CTRL_OP_GET_UNICODE_STATE,[])) of [Int] when Int > 0 -> true; [Int] when Int =:= 0 -> @@ -532,16 +542,16 @@ get_unicode_state(Iport) -> error end. -set_unicode_state(Iport, Bool) -> +set_unicode_state(Port, Bool) -> Data = case Bool of true -> [1]; false -> [0] end, - case (catch port_control(Iport,?CTRL_OP_SET_UNICODE_STATE,Data)) of + case (catch port_control(Port,?CTRL_OP_SET_UNICODE_STATE,Data)) of [Int] when Int > 0 -> - {unicode, utf8}; + true; [Int] when Int =:= 0 -> - {unicode, false}; + false; _ -> error end. @@ -549,21 +559,21 @@ set_unicode_state(Iport, Bool) -> %% 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) -> +io_request({requests,Rs}, Port) -> + io_requests(Rs, Port); +io_request(Request, Port) -> case io_command(Request) of {Data, Reply} -> - true = port_command(Oport, Data), + true = port_command(Port, Data), Reply; unhandled -> ok end. -io_requests([R|Rs], Iport, Oport) -> - io_request(R, Iport, Oport), - io_requests(Rs, Iport, Oport); -io_requests([], _Iport, _Oport) -> +io_requests([R|Rs], Port) -> + io_request(R, Port), + io_requests(Rs, Port); +io_requests([], _Port) -> ok. put_int16(N, Tail) -> @@ -575,15 +585,15 @@ put_int16(N, Tail) -> %% 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}) -> +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}) -> +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}; @@ -629,7 +639,7 @@ gr_get_info1([], _I) -> undefined. gr_add_cur({Next,_CurI,_CurP,Gs}, Pid, Shell) -> - {Next+1,Next,Pid,append(Gs, [{Next,Pid,Shell}])}. + {Next+1,Next,Pid,lists:append(Gs, [{Next,Pid,Shell}])}. gr_set_cur({Next,_CurI,_CurP,Gs}, I) -> case gr_get_num1(Gs, I) of @@ -666,30 +676,10 @@ gr_list({_Next,CurI,_CurP,Gs}) -> 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(Gs, Cur, [{put_chars, unicode, + lists: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(Gs, Cur, [{put_chars, unicode, + lists: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. From c259e3934792ba795f98556181e74cbb7b961785 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 11 May 2022 16:07:50 +0200 Subject: [PATCH 14/34] kernel: Make sure `user` is registered when start returns In order to avoid spinning in `user_sup` we start user during the synchronous phase of `user_drv` startup. See #4895 for more details --- lib/kernel/src/user_drv.erl | 11 +++-------- lib/kernel/src/user_sup.erl | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index af72dc51ead8..0c316a100e0a 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -104,10 +104,7 @@ start(Args) when is_map(Args) -> case gen_statem:start({local, ?MODULE}, ?MODULE, Args, []) of {ok, Pid} -> Pid; {error, _Reason} -> - spawn(fun() -> - process_flag(trap_exit, true), - user:start() - end) + user:start() end. callback_mode() -> state_functions. @@ -118,13 +115,11 @@ init(Args) -> {'EXIT', _Reason} -> {stop, normal}; Port -> - {ok, init, {Args, #state{ } }, + {ok, init, {Args, #state{ user = start_user() } }, {next_event, internal, Port}} end. -init(internal, Port, {Args, State}) -> - - User = start_user(), +init(internal, Port, {Args, State = #state{ user = User }}) -> %% Cleanup ancestors so that observer looks nice put('$ancestors',[User|get('$ancestors')]), diff --git a/lib/kernel/src/user_sup.erl b/lib/kernel/src/user_sup.erl index 6206a8d0ab50..038d359564dd 100644 --- a/lib/kernel/src/user_sup.erl +++ b/lib/kernel/src/user_sup.erl @@ -45,7 +45,7 @@ init(Flags) -> nouser -> ignore; {master, Master} -> - Pid = start_slave(Master), + Pid = start_relay(Master), {ok, Pid, Pid}; {M, F, A} -> case start_user(M, F, A) of @@ -56,7 +56,7 @@ init(Flags) -> end end. -start_slave(Master) -> +start_relay(Master) -> case rpc:call(Master, erlang, whereis, [user]) of User when is_pid(User) -> spawn(?MODULE, relay, [User]); From bcb7b0c1214e737a62bdcfc0fd1d7ac54a0f52e7 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 12 May 2022 09:03:08 +0200 Subject: [PATCH 15/34] kernel: Rewrite user_drv group handling to use records --- lib/kernel/src/user_drv.erl | 121 ++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 0c316a100e0a..4ae0498a8c9f 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -601,80 +601,67 @@ 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,lists: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) -> + #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} -> {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, - lists: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, - lists:flatten(io_lib:format("~4w ~w\n", [I,Shell]))}|Jobs]); -gr_list([], _Cur, Jobs) -> - lists:reverse(Jobs). +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, + lists:flatten(io_lib:format("~4w~.1ts ~w\n", [I,Marker,S]))}] + end, Groups). From bd0865ff86f06bba13a99e38db5d1f0a124e5cb3 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Fri, 28 Jan 2022 17:37:49 +0100 Subject: [PATCH 16/34] erts: Re-Implement shell using nif This commit re-implements the entire tty driver for both Unix and Windows to use a common nif instead of two seperate drivers. The Unix implementation works pretty much as it did before only that a lot more of the terminal logic has been moved from Erlang to C. The windows implementation now uses Windows Terminal Sequences, i.e. the same sequences as most Unixes to control the terminal. This means that werl.exe is no longer needed and erl.exe will have the "newshell" with all the features normally only found on Unix. The new implementation also uses dirty I/O threads for all I/O which means that it can leave the FDs in blocking mode. This fixes problems when the Erlang tty is interacting with other systems such as bash. Closes #3150 Closes #3390 Closes #4343 --- HOWTO/INSTALL-WIN32.md | 4 +- erts/Makefile | 4 +- erts/doc/src/erlsrv_cmd.xml | 3 +- erts/emulator/Makefile.in | 8 +- erts/emulator/beam/break.c | 2 +- erts/emulator/drivers/unix/ttsl_drv.c | 1607 --------------- erts/emulator/drivers/win32/ttsl_drv.c | 786 -------- erts/emulator/drivers/win32/win_con.c | 2355 ---------------------- erts/emulator/drivers/win32/win_con.h | 40 - erts/emulator/nifs/common/prim_tty_nif.c | 1012 ++++++++++ erts/emulator/sys/win32/sys.c | 62 +- erts/emulator/sys/win32/sys_interrupt.c | 18 +- erts/emulator/test/statistics_SUITE.erl | 4 +- erts/etc/common/Makefile.in | 11 +- erts/etc/common/erlexec.c | 38 +- erts/etc/common/etc_common.h | 1 + erts/etc/win32/Install.c | 31 +- erts/etc/win32/Makefile | 1 - erts/etc/win32/erl.c | 22 +- erts/etc/win32/nsis/erlang20.nsi | 2 +- erts/etc/win32/win_erlexec.c | 121 +- lib/kernel/src/Makefile | 1 + lib/kernel/src/kernel.app.src | 3 +- lib/kernel/src/prim_tty.erl | 837 ++++++++ lib/kernel/src/user_drv.erl | 664 +++--- lib/sasl/test/systools_SUITE.erl | 4 +- lib/stdlib/src/stdlib.app.src | 2 +- 27 files changed, 2311 insertions(+), 5332 deletions(-) delete mode 100644 erts/emulator/drivers/unix/ttsl_drv.c delete mode 100644 erts/emulator/drivers/win32/ttsl_drv.c delete mode 100644 erts/emulator/drivers/win32/win_con.c delete mode 100644 erts/emulator/drivers/win32/win_con.h create mode 100644 erts/emulator/nifs/common/prim_tty_nif.c create mode 100644 lib/kernel/src/prim_tty.erl diff --git a/HOWTO/INSTALL-WIN32.md b/HOWTO/INSTALL-WIN32.md index 4ad0159bcd25..bd8387aaae65 100644 --- a/HOWTO/INSTALL-WIN32.md +++ b/HOWTO/INSTALL-WIN32.md @@ -68,7 +68,7 @@ This is the short story though, for the experienced and impatient: ) and unpack with `tar` to the windows disk for example to: /mnt/c/src/ - * Install mingw-gcc, and make: `sudo apt install g++-mingw-w64 gcc-mingw-w64 make` + * Install mingw-gcc, and make: `sudo apt update && sudo apt install g++-mingw-w64 gcc-mingw-w64 make` * `$ cd UNPACK_DIR` @@ -150,7 +150,7 @@ the different tools: Install into `C:/OpenSSL-Win64` (or `C:/OpenSSL-Win32`) * wxWidgets (optional) - You need this to build wx and use gui's in debugger and observer. + You need this to build wx to use gui's in debugger and observer. We recommend v3.1.4 or later. Unpack into `c:/opt/local64/pgm/wxWidgets-3.1.4` diff --git a/erts/Makefile b/erts/Makefile index d6d9dee40d08..a0f0dcfdb3ab 100644 --- a/erts/Makefile +++ b/erts/Makefile @@ -86,10 +86,8 @@ local_setup: cp $(ERL_TOP)/bin/$(TARGET)/erlc.exe $(ERL_TOP)/bin/erlc.exe; \ cp $(ERL_TOP)/bin/$(TARGET)/erl.exe $(ERL_TOP)/bin/erl.exe; \ cp $(ERL_TOP)/bin/$(TARGET)/erl_call.exe $(ERL_TOP)/bin/erl_call.exe; \ - cp $(ERL_TOP)/bin/$(TARGET)/werl.exe $(ERL_TOP)/bin/werl.exe; \ cp $(ERL_TOP)/bin/$(TARGET)/escript.exe $(ERL_TOP)/bin/escript.exe; \ - chmod 755 $(ERL_TOP)/bin/erl.exe $(ERL_TOP)/bin/erlc.exe \ - $(ERL_TOP)/bin/werl.exe; \ + chmod 755 $(ERL_TOP)/bin/erl.exe $(ERL_TOP)/bin/erlc.exe; \ make_local_ini.sh $(ERL_TOP); \ cp $(ERL_TOP)/bin/erl.ini $(ERL_TOP)/bin/$(TARGET)/erl.ini; \ else \ diff --git a/erts/doc/src/erlsrv_cmd.xml b/erts/doc/src/erlsrv_cmd.xml index e8f066b21b5d..fe952f690e66 100644 --- a/erts/doc/src/erlsrv_cmd.xml +++ b/erts/doc/src/erlsrv_cmd.xml @@ -112,8 +112,7 @@

The location of the Erlang emulator. The default is the located in the same - directory as erlsrv.exe. Do not specify - as this emulator, it will not work.

+ directory as erlsrv.exe.

If the system uses release handling, this is to be set to a program similar to .

diff --git a/erts/emulator/Makefile.in b/erts/emulator/Makefile.in index 42f7a6bb1c03..3791d2caa24c 100644 --- a/erts/emulator/Makefile.in +++ b/erts/emulator/Makefile.in @@ -1129,6 +1129,7 @@ RUN_OBJS += \ LTTNG_OBJS = $(OBJDIR)/erlang_lttng.o NIF_OBJS = \ + $(OBJDIR)/prim_tty_nif.o \ $(OBJDIR)/erl_tracer_nif.o \ $(OBJDIR)/prim_buffer_nif.o \ $(OBJDIR)/prim_file_nif.o \ @@ -1139,10 +1140,8 @@ ifeq ($(TARGET),win32) DRV_OBJS = \ $(OBJDIR)/registry_drv.o \ $(OBJDIR)/inet_drv.o \ - $(OBJDIR)/ram_file_drv.o \ - $(OBJDIR)/ttsl_drv.o + $(OBJDIR)/ram_file_drv.o OS_OBJS = \ - $(OBJDIR)/win_con.o \ $(OBJDIR)/dll_sys.o \ $(OBJDIR)/driver_tab.o \ $(OBJDIR)/sys_float.o \ @@ -1166,8 +1165,7 @@ OS_OBJS = \ DRV_OBJS = \ $(OBJDIR)/inet_drv.o \ - $(OBJDIR)/ram_file_drv.o \ - $(OBJDIR)/ttsl_drv.o + $(OBJDIR)/ram_file_drv.o endif ifneq ($(STATIC_NIFS),no) diff --git a/erts/emulator/beam/break.c b/erts/emulator/beam/break.c index 3b6de45587a7..dff270c60f61 100644 --- a/erts/emulator/beam/break.c +++ b/erts/emulator/beam/break.c @@ -581,7 +581,7 @@ do_break(void) /* check if we're in console mode and, if so, halt immediately if break is called */ mode = erts_read_env("ERL_CONSOLE_MODE"); - if (mode && sys_strcmp(mode, "window") != 0) + if (mode && sys_strcmp(mode, "detached") == 0) erts_exit(0, ""); erts_free_read_env(mode); #endif /* __WIN32__ */ diff --git a/erts/emulator/drivers/unix/ttsl_drv.c b/erts/emulator/drivers/unix/ttsl_drv.c deleted file mode 100644 index 3fb5bdb8fb4d..000000000000 --- a/erts/emulator/drivers/unix/ttsl_drv.c +++ /dev/null @@ -1,1607 +0,0 @@ -/* - * %CopyrightBegin% - * - * Copyright Ericsson AB 1996-2022. 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * %CopyrightEnd% - */ -/* - * Tty driver that reads one character at the time and provides a - * smart line for output. - */ - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif - -#include "erl_driver.h" - -static int ttysl_init(void); -static ErlDrvData ttysl_start(ErlDrvPort, char*); - -#ifdef HAVE_TERMCAP /* else make an empty driver that cannot be opened */ - -#ifndef WANT_NONBLOCKING -#define WANT_NONBLOCKING -#endif - -#include "sys.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef HAVE_WCWIDTH -#include -#endif -#ifdef HAVE_FCNTL_H -#include -#endif -#ifdef HAVE_SYS_IOCTL_H -#include -#endif -#if !defined(HAVE_SETLOCALE) || !defined(HAVE_NL_LANGINFO) || !defined(HAVE_LANGINFO_H) -#define PRIMITIVE_UTF8_CHECK 1 -#else -#include -#endif - -#if defined IOV_MAX -#define MAXIOV IOV_MAX -#elif defined UIO_MAXIOV -#define MAXIOV UIO_MAXIOV -#else -#define MAXIOV 16 -#endif - -#define TRUE 1 -#define FALSE 0 - -/* Termcap functions. */ -int tgetent(char* bp, char *name); -int tgetnum(char* cap); -int tgetflag(char* cap); -char *tgetstr(char* cap, char** buf); -char *tgoto(char* cm, int col, int line); -int tputs(char* cp, int affcnt, int (*outc)(int c)); - -/* Terminal capabilities in which we are interested. */ -static char *capbuf; -static char *up, *down, *left, *right; -static int cols, xn; -static volatile int cols_needs_update = FALSE; - -/* The various opcodes. */ -#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 CTRL_OP_GET_WINSIZE 100 -#define CTRL_OP_GET_UNICODE_STATE 101 -#define CTRL_OP_SET_UNICODE_STATE 102 - -/* We use 1024 as the buf size as that was the default buf size of FILE streams - on all platforms that I checked. */ -#define TTY_BUFFSIZE 1024 - -static int lbuf_size = BUFSIZ; -static Uint32 *lbuf; /* The current line buffer */ -static int llen; /* The current line length */ -static int lpos; /* The current "cursor position" in the line buffer */ - /* NOTE: not the same as column position a char may not take a" - * column to display or it might take many columns - */ -/* - * Tags used in line buffer to show that these bytes represent special characters, - * Max unicode is 0x0010ffff, so we have lots of place for meta tags... - */ -#define CONTROL_TAG 0x10000000U /* Control character, value in first position */ -#define ESCAPED_TAG 0x01000000U /* Escaped character, value in first position */ -#define TAG_MASK 0xFF000000U - -#define MAXSIZE (1 << 16) - -#define COL(_l) ((_l) % cols) -#define LINE(_l) ((_l) / cols) - -#define NL '\n' - -/* Main interface functions. */ -static void ttysl_stop(ErlDrvData); -static void ttysl_from_erlang(ErlDrvData, char*, ErlDrvSizeT); -static void ttysl_to_tty(ErlDrvData, ErlDrvEvent); -static void ttysl_flush_tty(ErlDrvData); -static void ttysl_from_tty(ErlDrvData, ErlDrvEvent); -static void ttysl_stop_select(ErlDrvEvent, void*); -static Sint16 get_sint16(char*); - -static ErlDrvPort ttysl_port; -static int ttysl_fd; -static int ttysl_terminate = 0; -static int ttysl_send_ok = 0; -static ErlDrvBinary *putcbuf; -static int putcpos; -static int putclen; - -/* Functions that work on the line buffer. */ -static int start_lbuf(void); -static int stop_lbuf(void); -static int put_chars(byte*,int); -static int move_rel(int); -static int ins_chars(byte *,int); -static int del_chars(int); -static int step_over_chars(int); -static int insert_buf(byte*,int); -static int write_buf(Uint32 *,int,int); -static int outc(int c); -static int move_cursor(int,int); -static int cp_pos_to_col(int cp_pos); - - -/* Termcap functions. */ -static int start_termcap(void); -static int stop_termcap(void); -static int move_left(int); -static int move_right(int); -static int move_up(int); -static int move_down(int); -static void update_cols(void); - -/* Terminal setting functions. */ -static int tty_init(int,int,int,int); -static int tty_set(int); -static int tty_reset(int); -static ErlDrvSSizeT ttysl_control(ErlDrvData, unsigned int, - char *, ErlDrvSizeT, char **, ErlDrvSizeT); -#ifdef ERTS_NOT_USED -static RETSIGTYPE suspend(int); -#endif -static RETSIGTYPE cont(int); -static RETSIGTYPE winch(int); - -/*#define LOG_DEBUG*/ - -#ifdef LOG_DEBUG -FILE *debuglog = NULL; - -#define DEBUGLOG(X) \ -do { \ - if (debuglog != NULL) { \ - my_debug_printf X; \ - } \ -} while (0) - -static void my_debug_printf(char *fmt, ...) -{ - char buffer[1024]; - va_list args; - - va_start(args, fmt); - erts_vsnprintf(buffer,1024,fmt,args); - va_end(args); - erts_fprintf(debuglog,"%s\n",buffer); - /*erts_printf("Debuglog = %s\n",buffer);*/ -} - -#else - -#define DEBUGLOG(X) - -#endif - -static int utf8_mode = 0; -static byte utf8buf[4]; /* for incomplete input */ -static int utf8buf_size; /* size of incomplete input */ - -# define IF_IMPL(x) x -#else -# define IF_IMPL(x) NULL -#endif /* HAVE_TERMCAP */ - -/* Define the driver table entry. */ -struct erl_drv_entry ttsl_driver_entry = { - ttysl_init, - ttysl_start, - IF_IMPL(ttysl_stop), - IF_IMPL(ttysl_from_erlang), - IF_IMPL(ttysl_from_tty), - IF_IMPL(ttysl_to_tty), - "tty_sl", /* driver_name */ - NULL, /* finish */ - NULL, /* handle */ - IF_IMPL(ttysl_control), - NULL, /* timeout */ - NULL, /* outputv */ - NULL, /* ready_async */ - IF_IMPL(ttysl_flush_tty), - NULL, /* call */ - NULL, /* event */ - ERL_DRV_EXTENDED_MARKER, - ERL_DRV_EXTENDED_MAJOR_VERSION, - ERL_DRV_EXTENDED_MINOR_VERSION, - 0, /* ERL_DRV_FLAGs */ - NULL, /* handle2 */ - NULL, /* process_exit */ - IF_IMPL(ttysl_stop_select) -}; - - -static int ttysl_init(void) -{ -#ifdef HAVE_TERMCAP - ttysl_port = (ErlDrvPort)-1; - ttysl_fd = -1; - lbuf = NULL; /* For line buffer handling */ - capbuf = NULL; /* For termcap handling */ -#endif -#ifdef LOG_DEBUG - { - char *dl; - if ((dl = getenv("TTYSL_DEBUG_LOG")) != NULL && *dl) { - debuglog = fopen(dl,"w+"); - if (debuglog != NULL) - setbuf(debuglog,NULL); - } - DEBUGLOG(("ttysl_init: Debuglog = %s(0x%ld)\n",dl,(long) debuglog)); - } -#endif - return 0; -} - -static ErlDrvData ttysl_start(ErlDrvPort port, char* buf) -{ -#ifndef HAVE_TERMCAP - return ERL_DRV_ERROR_GENERAL; -#else - char *s, *t, *l; - int canon, echo, sig; /* Terminal characteristics */ - int flag; - extern int using_oldshell; /* set this to let the rest of erts know */ - - DEBUGLOG(("ttysl_start: driver input \"%s\", ttysl_port = %d (-1 expected)", buf, ttysl_port)); - utf8buf_size = 0; - if (ttysl_port != (ErlDrvPort)-1) { - DEBUGLOG(("ttysl_start: failure with ttysl_port = %d, not initialized properly?\n", ttysl_port)); - return ERL_DRV_ERROR_GENERAL; - } - - DEBUGLOG(("ttysl_start: isatty(0) = %d (1 expected), isatty(1) = %d (1 expected)", isatty(0), isatty(1))); - if (!isatty(0) || !isatty(1)) { - DEBUGLOG(("ttysl_start: failure in isatty, isatty(0) = %d, isatty(1) = %d", isatty(0), isatty(1))); - return ERL_DRV_ERROR_GENERAL; - } - - /* Set the terminal modes to default leave as is. */ - canon = echo = sig = 0; - - /* Parse the input parameters. */ - for (s = strchr(buf, ' '); s; s = t) { - s++; - /* Find end of this argument (start of next) and insert NUL. */ - if ((t = strchr(s, ' '))) { - *t = '\0'; - } - if ((flag = ((*s == '+') ? 1 : ((*s == '-') ? -1 : 0)))) { - if (s[1] == 'c') canon = flag; - if (s[1] == 'e') echo = flag; - if (s[1] == 's') sig = flag; - } - else if ((ttysl_fd = open(s, O_RDWR, 0)) < 0) { - DEBUGLOG(("ttysl_start: failed to open ttysl_fd, open(%s, O_RDWR, 0)) = %d\n", s, ttysl_fd)); - return ERL_DRV_ERROR_GENERAL; - } - } - - if (ttysl_fd < 0) - ttysl_fd = 0; - - if (tty_init(ttysl_fd, canon, echo, sig) < 0 || - tty_set(ttysl_fd) < 0) { - DEBUGLOG(("ttysl_start: failed init tty or set tty\n")); - ttysl_port = (ErlDrvPort)-1; - tty_reset(ttysl_fd); - return ERL_DRV_ERROR_GENERAL; - } - - /* Set up smart line and termcap stuff. */ - if (!start_lbuf() || !start_termcap()) { - DEBUGLOG(("ttysl_start: failed to start_lbuf or start_termcap\n")); - stop_lbuf(); /* Must free this */ - tty_reset(ttysl_fd); - return ERL_DRV_ERROR_GENERAL; - } - - SET_NONBLOCKING(ttysl_fd); - -#ifdef PRIMITIVE_UTF8_CHECK - setlocale(LC_CTYPE, ""); /* Set international environment, - ignore result */ - if (((l = getenv("LC_ALL")) && *l) || - ((l = getenv("LC_CTYPE")) && *l) || - ((l = getenv("LANG")) && *l)) { - if (strstr(l, "UTF-8")) - utf8_mode = 1; - } - -#else - l = setlocale(LC_CTYPE, ""); /* Set international environment */ - if (l != NULL) { - utf8_mode = (strcmp(nl_langinfo(CODESET), "UTF-8") == 0); - DEBUGLOG(("ttysl_start: setlocale: %s",l)); - } -#endif - DEBUGLOG(("ttysl_start: utf8_mode is %s",(utf8_mode) ? "on" : "off")); - sys_signal(SIGCONT, cont); - sys_signal(SIGWINCH, winch); - - driver_select(port, (ErlDrvEvent)(UWord)ttysl_fd, ERL_DRV_READ|ERL_DRV_USE, 1); - ttysl_port = port; - - /* we need to know this when we enter the break handler */ - using_oldshell = 0; - - DEBUGLOG(("ttysl_start: successful start\n")); - return (ErlDrvData)ttysl_port; /* Nothing important to return */ -#endif /* HAVE_TERMCAP */ -} - -#ifdef HAVE_TERMCAP - -#define DEF_HEIGHT 24 -#define DEF_WIDTH 80 -static void ttysl_get_window_size(Uint32 *width, Uint32 *height) -{ -#ifdef TIOCGWINSZ - struct winsize ws; - if (ioctl(ttysl_fd,TIOCGWINSZ,&ws) == 0) { - *width = (Uint32) ws.ws_col; - *height = (Uint32) ws.ws_row; - if (*width <= 0) - *width = DEF_WIDTH; - if (*height <= 0) - *height = DEF_HEIGHT; - return; - } -#endif - *width = DEF_WIDTH; - *height = DEF_HEIGHT; -} - -static ErlDrvSSizeT ttysl_control(ErlDrvData drv_data, - unsigned int command, - char *buf, ErlDrvSizeT len, - char **rbuf, ErlDrvSizeT rlen) -{ - char resbuff[2*sizeof(Uint32)]; - ErlDrvSizeT res_size; - - command -= ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER; - switch (command) { - case CTRL_OP_GET_WINSIZE: - { - Uint32 w,h; - ttysl_get_window_size(&w,&h); - memcpy(resbuff,&w,sizeof(Uint32)); - memcpy(resbuff+sizeof(Uint32),&h,sizeof(Uint32)); - res_size = 2*sizeof(Uint32); - } - break; - case CTRL_OP_GET_UNICODE_STATE: - *resbuff = (utf8_mode) ? 1 : 0; - res_size = 1; - break; - case CTRL_OP_SET_UNICODE_STATE: - if (len > 0) { - int m = (int) *buf; - *resbuff = (utf8_mode) ? 1 : 0; - res_size = 1; - utf8_mode = (m) ? 1 : 0; - } else { - return 0; - } - break; - default: - return -1; - } - if (rlen < res_size) { - *rbuf = driver_alloc(res_size); - } - memcpy(*rbuf,resbuff,res_size); - return res_size; -} - - -static void ttysl_stop(ErlDrvData ttysl_data) -{ - DEBUGLOG(("ttysl_stop: ttysl_port = %d\n",ttysl_port)); - if (ttysl_port != (ErlDrvPort)-1) { - stop_lbuf(); - stop_termcap(); - tty_reset(ttysl_fd); - driver_select(ttysl_port, (ErlDrvEvent)(UWord)ttysl_fd, - ERL_DRV_WRITE|ERL_DRV_READ|ERL_DRV_USE, 0); - sys_signal(SIGCONT, SIG_DFL); - sys_signal(SIGWINCH, SIG_DFL); - } - ttysl_port = (ErlDrvPort)-1; - ttysl_fd = -1; - ttysl_terminate = 0; - /* return TRUE; */ -} - -static int put_utf8(int ch, byte *target, int sz, int *pos) -{ - Uint x = (Uint) ch; - if (x < 0x80) { - if (*pos >= sz) { - return -1; - } - target[(*pos)++] = (byte) x; - } - else if (x < 0x800) { - if (((*pos) + 1) >= sz) { - return -1; - } - target[(*pos)++] = (((byte) (x >> 6)) | - ((byte) 0xC0)); - target[(*pos)++] = (((byte) (x & 0x3F)) | - ((byte) 0x80)); - } else if (x < 0x10000) { - if ((x >= 0xD800 && x <= 0xDFFF) || - (x == 0xFFFE) || - (x == 0xFFFF)) { /* Invalid unicode range */ - return -1; - } - if (((*pos) + 2) >= sz) { - return -1; - } - - target[(*pos)++] = (((byte) (x >> 12)) | - ((byte) 0xE0)); - target[(*pos)++] = ((((byte) (x >> 6)) & 0x3F) | - ((byte) 0x80)); - target[(*pos)++] = (((byte) (x & 0x3F)) | - ((byte) 0x80)); - } else if (x < 0x110000) { /* Standard imposed max */ - if (((*pos) + 3) >= sz) { - return -1; - } - target[(*pos)++] = (((byte) (x >> 18)) | - ((byte) 0xF0)); - target[(*pos)++] = ((((byte) (x >> 12)) & 0x3F) | - ((byte) 0x80)); - target[(*pos)++] = ((((byte) (x >> 6)) & 0x3F) | - ((byte) 0x80)); - target[(*pos)++] = (((byte) (x & 0x3F)) | - ((byte) 0x80)); - } else { - return -1; - } - return 0; -} - - -static int pick_utf8(byte *s, int sz, int *pos) -{ - int size = sz - (*pos); - byte *source; - Uint unipoint; - - if (size > 0) { - source = s + (*pos); - if (((*source) & ((byte) 0x80)) == 0) { - unipoint = (int) *source; - ++(*pos); - return (int) unipoint; - } else if (((*source) & ((byte) 0xE0)) == 0xC0) { - if (size < 2) { - return -2; - } - if (((source[1] & ((byte) 0xC0)) != 0x80) || - ((*source) < 0xC2) /* overlong */) { - return -1; - } - (*pos) += 2; - unipoint = - (((Uint) ((*source) & ((byte) 0x1F))) << 6) | - ((Uint) (source[1] & ((byte) 0x3F))); - return (int) unipoint; - } else if (((*source) & ((byte) 0xF0)) == 0xE0) { - if (size < 3) { - return -2; - } - if (((source[1] & ((byte) 0xC0)) != 0x80) || - ((source[2] & ((byte) 0xC0)) != 0x80) || - (((*source) == 0xE0) && (source[1] < 0xA0)) /* overlong */ ) { - return -1; - } - if ((((*source) & ((byte) 0xF)) == 0xD) && - ((source[1] & 0x20) != 0)) { - return -1; - } - if (((*source) == 0xEF) && (source[1] == 0xBF) && - ((source[2] == 0xBE) || (source[2] == 0xBF))) { - return -1; - } - (*pos) += 3; - unipoint = - (((Uint) ((*source) & ((byte) 0xF))) << 12) | - (((Uint) (source[1] & ((byte) 0x3F))) << 6) | - ((Uint) (source[2] & ((byte) 0x3F))); - return (int) unipoint; - } else if (((*source) & ((byte) 0xF8)) == 0xF0) { - if (size < 4) { - return -2 ; - } - if (((source[1] & ((byte) 0xC0)) != 0x80) || - ((source[2] & ((byte) 0xC0)) != 0x80) || - ((source[3] & ((byte) 0xC0)) != 0x80) || - (((*source) == 0xF0) && (source[1] < 0x90)) /* overlong */) { - return -1; - } - if ((((*source) & ((byte)0x7)) > 0x4U) || - ((((*source) & ((byte)0x7)) == 0x4U) && - ((source[1] & ((byte)0x3F)) > 0xFU))) { - return -1; - } - (*pos) += 4; - unipoint = - (((Uint) ((*source) & ((byte) 0x7))) << 18) | - (((Uint) (source[1] & ((byte) 0x3F))) << 12) | - (((Uint) (source[2] & ((byte) 0x3F))) << 6) | - ((Uint) (source[3] & ((byte) 0x3F))); - return (int) unipoint; - } else { - return -1; - } - } else { - return -1; - } -} - -static int octal_or_hex_positions(Uint c) -{ - int x = 0; - Uint ch = c; - if (!ch) { - return 1; - } - while(ch) { - ++x; - ch >>= 3; - } - if (x <= 3) { - return 3; - } - /* \x{H ...} format when larger than \777 */ - x = 0; - ch = c; - while(ch) { - ++x; - ch >>= 4; - } - return x+3; -} - -static void octal_or_hex_format(Uint ch, byte *buf, int *pos) -{ - static byte hex_chars[] = { '0','1','2','3','4','5','6','7','8','9', - 'A','B','C','D','E','F'}; - int num = octal_or_hex_positions(ch); - if (num != 3) { - ASSERT(num > 3); - buf[(*pos)++] = 'x'; - buf[(*pos)++] = '{'; - num -= 3; - while(num--) { - buf[(*pos)++] = hex_chars[((ch >> (4*num)) & 0xFU)]; - } - buf[(*pos)++] = '}'; - } else { - while(num--) { - buf[(*pos)++] = ((byte) ((ch >> (3*num)) & 0x7U) + '0'); - } - } -} - -/* - * Check that there is enough room in all buffers to copy all pad chars - * and stiff we need If not, realloc lbuf. - */ -static int check_buf_size(byte *s, int n) -{ - int pos = 0; - int ch; - int size = 10; - - DEBUGLOG(("check_buf_size: n = %d",n)); - while(pos < n) { - /* Indata is always UTF-8 */ - if ((ch = pick_utf8(s,n,&pos)) < 0) { - /* XXX temporary allow invalid chars */ - ch = (int) s[pos]; - DEBUGLOG(("check_buf_size: Invalid UTF8:%d",ch)); - ++pos; - } - if (utf8_mode) { /* That is, terminal is UTF8 compliant */ - if (ch >= 128 || isprint(ch)) { -#ifdef HAVE_WCWIDTH - int width; -#endif - DEBUGLOG(("check_buf_size: Printable(UTF-8:%d):%d",pos,ch)); - size++; -#ifdef HAVE_WCWIDTH - if ((width = wcwidth(ch)) > 1) { - size += width - 1; - } -#endif - } else if (ch == '\t') { - size += 8; - } else { - DEBUGLOG(("check_buf_size: Magic(UTF-8:%d):%d",pos,ch)); - size += 2; - } - } else { - if (ch <= 255 && isprint(ch)) { - DEBUGLOG(("check_buf_size: Printable:%d",ch)); - size++; - } else if (ch == '\t') - size += 8; - else if (ch >= 128) { - DEBUGLOG(("check_buf_size: Non printable:%d",ch)); - size += (octal_or_hex_positions(ch) + 1); - } - else { - DEBUGLOG(("check_buf_size: Magic:%d",ch)); - size += 2; - } - } - } - - if (size + lpos >= lbuf_size) { - - lbuf_size = size + lpos + BUFSIZ; - if ((lbuf = driver_realloc(lbuf, lbuf_size * sizeof(Uint32))) == NULL) { - DEBUGLOG(("check_buf_size: alloc failure of %d bytes", lbuf_size * sizeof(Uint32))); - driver_failure(ttysl_port, -1); - return(0); - } - } - DEBUGLOG(("check_buf_size: success\n")); - return(1); -} - - -static void ttysl_from_erlang(ErlDrvData ttysl_data, char* buf, ErlDrvSizeT count) -{ - ErlDrvSizeT sz; - - sz = driver_sizeq(ttysl_port); - - putclen = count > TTY_BUFFSIZE ? TTY_BUFFSIZE : count; - putcbuf = driver_alloc_binary(putclen); - putcpos = 0; - - if (lpos > MAXSIZE) - put_chars((byte*)"\n", 1); - - DEBUGLOG(("ttysl_from_erlang: OP = %d", buf[0])); - - switch (buf[0]) { - case OP_PUTC_SYNC: - /* Using sync means that we have to send an ok to the - controlling process for each command call. We delay - sending ok if the driver queue exceeds a certain size. - We do not set ourselves as a busy port, as this - could be very bad for user_drv, if it gets blocked on - the port_command. */ - /* fall through */ - case OP_PUTC: - DEBUGLOG(("ttysl_from_erlang: OP: Putc(%lu)",(unsigned long) count-1)); - if (check_buf_size((byte*)buf+1, count-1) == 0) - return; - put_chars((byte*)buf+1, count-1); - break; - case OP_MOVE: - move_rel(get_sint16(buf+1)); - break; - case OP_INSC: - if (check_buf_size((byte*)buf+1, count-1) == 0) - return; - ins_chars((byte*)buf+1, count-1); - break; - case OP_DELC: - del_chars(get_sint16(buf+1)); - break; - case OP_BEEP: - outc('\007'); - break; - default: - /* Unknown op, just ignore. */ - break; - } - - driver_enq_bin(ttysl_port,putcbuf,0,putcpos); - driver_free_binary(putcbuf); - - if (sz == 0) { - for (;;) { - int written, qlen; - SysIOVec *iov; - - iov = driver_peekq(ttysl_port,&qlen); - if (iov) - written = writev(ttysl_fd, iov, qlen > MAXIOV ? MAXIOV : qlen); - else - written = 0; - if (written < 0) { - if (errno == ERRNO_BLOCK || errno == EINTR) { - driver_select(ttysl_port,(ErlDrvEvent)(long)ttysl_fd, - ERL_DRV_USE|ERL_DRV_WRITE,1); - break; - } else { - DEBUGLOG(("ttysl_from_erlang: driver failure in writev(%d,..) = %d (errno = %d)\n", ttysl_fd, written, errno)); - driver_failure_posix(ttysl_port, errno); - return; - } - } else { - if (driver_deq(ttysl_port, written) == 0) - break; - } - } - } - - if (buf[0] == OP_PUTC_SYNC) { - if (driver_sizeq(ttysl_port) > TTY_BUFFSIZE && !ttysl_terminate) { - /* We delay sending the ack until the buffer has been consumed */ - ttysl_send_ok = 1; - } else { - ErlDrvTermData spec[] = { - ERL_DRV_PORT, driver_mk_port(ttysl_port), - ERL_DRV_ATOM, driver_mk_atom("ok"), - ERL_DRV_TUPLE, 2 - }; - ASSERT(ttysl_send_ok == 0); - erl_drv_output_term(driver_mk_port(ttysl_port), spec, - sizeof(spec) / sizeof(spec[0])); - } - } - - return; /* TRUE; */ -} - -static void ttysl_to_tty(ErlDrvData ttysl_data, ErlDrvEvent fd) { - for (;;) { - int written, qlen; - SysIOVec *iov; - ErlDrvSizeT sz; - - iov = driver_peekq(ttysl_port,&qlen); - - DEBUGLOG(("ttysl_to_tty: qlen = %d", qlen)); - - if (iov) - written = writev(ttysl_fd, iov, qlen > MAXIOV ? MAXIOV : qlen); - else - written = 0; - if (written < 0) { - if (errno == EINTR) { - continue; - } else if (errno != ERRNO_BLOCK){ - DEBUGLOG(("ttysl_to_tty: driver failure in writev(%d,..) = %d (errno = %d)\n", ttysl_fd, written, errno)); - driver_failure_posix(ttysl_port, errno); - } - break; - } else { - sz = driver_deq(ttysl_port, written); - if (sz < TTY_BUFFSIZE && ttysl_send_ok) { - ErlDrvTermData spec[] = { - ERL_DRV_PORT, driver_mk_port(ttysl_port), - ERL_DRV_ATOM, driver_mk_atom("ok"), - ERL_DRV_TUPLE, 2 - }; - ttysl_send_ok = 0; - erl_drv_output_term(driver_mk_port(ttysl_port), spec, - sizeof(spec) / sizeof(spec[0])); - } - if (sz == 0) { - driver_select(ttysl_port,(ErlDrvEvent)(long)ttysl_fd, - ERL_DRV_WRITE,0); - if (ttysl_terminate) { - /* flush has been called, which means we should terminate - when queue is empty. This will not send any exit - message */ - DEBUGLOG(("ttysl_to_tty: ttysl_terminate normal\n")); - driver_failure_atom(ttysl_port, "normal"); - } - break; - } - } - } - - return; -} - -static void ttysl_flush_tty(ErlDrvData ttysl_data) { - DEBUGLOG(("ttysl_flush_tty: ..")); - ttysl_terminate = 1; - return; -} - -static void ttysl_from_tty(ErlDrvData ttysl_data, ErlDrvEvent fd) -{ - byte b[1024]; - ssize_t i; - int ch = 0, pos = 0; - int left = 1024; - byte *p = b; - byte t[1024]; - int tpos; - - if (utf8buf_size > 0) { - memcpy(b,utf8buf,utf8buf_size); - left -= utf8buf_size; - p += utf8buf_size; - utf8buf_size = 0; - } - - DEBUGLOG(("ttysl_from_tty: remainder = %d", left)); - - if ((i = read((int)(SWord)fd, (char *) p, left)) >= 0) { - if (p != b) { - i += (p - b); - } - if (utf8_mode) { /* Hopefully an UTF8 terminal */ - while(pos < i && (ch = pick_utf8(b,i,&pos)) >= 0) - ; - if (ch == -2 && i - pos <= 4) { - /* bytes left to care for */ - utf8buf_size = i -pos; - memcpy(utf8buf,b+pos,utf8buf_size); - } else if (ch == -1) { - DEBUGLOG(("ttysl_from_tty: Giving up on UTF8 mode, invalid character")); - utf8_mode = 0; - goto latin_terminal; - } - driver_output(ttysl_port, (char *) b, pos); - } else { - latin_terminal: - tpos = 0; - while (pos < i) { - while (tpos < 1020 && pos < i) { /* Max 4 bytes for UTF8 */ - put_utf8((int) b[pos++], t, 1024, &tpos); - } - driver_output(ttysl_port, (char *) t, tpos); - tpos = 0; - } - } - } else if (errno != EAGAIN && errno != EWOULDBLOCK) { - DEBUGLOG(("ttysl_from_tty: driver failure in read(%d,..) = %d (errno = %d)\n", (int)(SWord)fd, i, errno)); - driver_failure(ttysl_port, -1); - } -} - -static void ttysl_stop_select(ErlDrvEvent e, void* _) -{ - int fd = (int)(long)e; - if (fd != 0) { - close(fd); - } -} - -/* Procedures for putting and getting integers to/from strings. */ -static Sint16 get_sint16(char *s) -{ - return ((*s << 8) | ((byte*)s)[1]); -} - -static int start_lbuf(void) -{ - if (!lbuf && !(lbuf = ( Uint32*) driver_alloc(lbuf_size * sizeof(Uint32)))) - return FALSE; - llen = 0; - lpos = 0; - return TRUE; -} - -static int stop_lbuf(void) -{ - if (lbuf) { - driver_free(lbuf); - lbuf = NULL; - } - return TRUE; -} - -/* Put l bytes (in UTF8) from s into the buffer and output them. */ -static int put_chars(byte *s, int l) -{ - int n; - - n = insert_buf(s, l); - if (lpos > llen) - llen = lpos; - if (n > 0) - write_buf(lbuf + lpos - n, n, 0); - return TRUE; -} - -/* - * Move the current position forwards or backwards within the current - * line. We know about padding. - */ -static int move_rel(int n) -{ - int npos; /* The new position */ - - /* Step forwards or backwards over the buffer. */ - npos = step_over_chars(n); - - /* Calculate move, updates pointers and move the cursor. */ - move_cursor(lpos, npos); - lpos = npos; - return TRUE; -} - -/* Insert characters into the buffer at the current position. */ -static int ins_chars(byte *s, int l) -{ - int n, tl; - Uint32 *tbuf = NULL; /* Suppress warning about use-before-set */ - - /* Move tail of buffer to make space. */ - if ((tl = llen - lpos) > 0) { - if ((tbuf = driver_alloc(tl * sizeof(Uint32))) == NULL) - return FALSE; - memcpy(tbuf, lbuf + lpos, tl * sizeof(Uint32)); - } - n = insert_buf(s, l); - if (tl > 0) { - memcpy(lbuf + lpos, tbuf, tl * sizeof(Uint32)); - driver_free(tbuf); - } - llen += n; - write_buf(lbuf + (lpos - n), llen - (lpos - n), 0); - move_cursor(llen, lpos); - return TRUE; -} - -/* - * Delete characters in the buffer. Can delete characters before (n < 0) - * and after (n > 0) the current position. Cursor left at beginning of - * deleted block. - */ -static int del_chars(int n) -{ - int i, l, r; - int pos; - int gcs; /* deleted grapheme characters */ - - update_cols(); - - /* Step forward or backwards over n logical characters. */ - pos = step_over_chars(n); - DEBUGLOG(("del_chars: %d from %d %d %d\n", n, lpos, pos, llen)); - if (pos > lpos) { - l = pos - lpos; /* Buffer characters to delete */ - r = llen - lpos - l; /* Characters after deleted */ - gcs = cp_pos_to_col(pos) - cp_pos_to_col(lpos); - /* Fix up buffer and buffer pointers. */ - if (r > 0) - memmove(lbuf + lpos, lbuf + pos, r * sizeof(Uint32)); - llen -= l; - /* Write out characters after, blank the tail and jump back to lpos. */ - write_buf(lbuf + lpos, r, 0); - for (i = gcs ; i > 0; --i) - outc(' '); - if (xn && COL(cp_pos_to_col(llen)+gcs) == 0) - { - outc(' '); - move_left(1); - } - move_cursor(llen + gcs, lpos); - } - else if (pos < lpos) { - l = lpos - pos; /* Buffer characters */ - r = llen - lpos; /* Characters after deleted */ - gcs = -move_cursor(lpos, lpos-l); /* Move back */ - /* Fix up buffer and buffer pointers. */ - if (r > 0) - memmove(lbuf + pos, lbuf + lpos, r * sizeof(Uint32)); - lpos -= l; - llen -= l; - /* Write out characters after, blank the tail and jump back to lpos. */ - write_buf(lbuf + lpos, r, 0); - for (i = gcs ; i > 0; --i) - outc(' '); - if (xn && COL(cp_pos_to_col(llen)+gcs) == 0) - { - outc(' '); - move_left(1); - } - move_cursor(llen + gcs, lpos); - } - return TRUE; -} - -/* Step over n logical characters, check for overflow. */ -static int step_over_chars(int n) -{ - Uint32 *c, *beg, *end; - - beg = lbuf; - end = lbuf + llen; - c = lbuf + lpos; - for ( ; n > 0 && c < end; --n) { - c++; - while (c < end && (*c & TAG_MASK) && ((*c & ~TAG_MASK) == 0)) - c++; - } - for ( ; n < 0 && c > beg; n++) { - --c; - while (c > beg && (*c & TAG_MASK) && ((*c & ~TAG_MASK) == 0)) - --c; - } - return c - lbuf; -} - -/* - * Insert n characters into the buffer at lpos. - * Know about pad characters and treat \n specially. - */ - -static int insert_buf(byte *s, int n) -{ - int pos = 0; - int buffpos = lpos; - int ch; - - while (pos < n) { - if ((ch = pick_utf8(s,n,&pos)) < 0) { - /* XXX temporary allow invalid chars */ - ch = (int) s[pos]; - DEBUGLOG(("insert_buf: Invalid UTF8:%d",ch)); - ++pos; - } - if ((utf8_mode && (ch >= 128 || isprint(ch))) || (ch <= 255 && isprint(ch))) { - DEBUGLOG(("insert_buf: Printable(UTF-8):%d",ch)); - lbuf[lpos++] = (Uint32) ch; - } else if (ch >= 128) { /* not utf8 mode */ - int nc = octal_or_hex_positions(ch); - lbuf[lpos++] = ((Uint32) ch) | ESCAPED_TAG; - while (nc--) { - lbuf[lpos++] = ESCAPED_TAG; - } - } else if (ch == '\t') { - do { - lbuf[lpos++] = (CONTROL_TAG | ((Uint32) ch)); - ch = 0; - } while (lpos % 8); - } else if (ch == '\e') { - DEBUGLOG(("insert_buf: ANSI Escape: \\e")); - lbuf[lpos++] = (CONTROL_TAG | ((Uint32) ch)); - } else if (ch == '\n' || ch == '\r') { - write_buf(lbuf + buffpos, lpos - buffpos, 1); - outc('\r'); - if (ch == '\n') - outc('\n'); - if (llen > lpos) { - memmove(lbuf, lbuf + lpos, llen - lpos); - } - llen -= lpos; - lpos = buffpos = 0; - } else { - DEBUGLOG(("insert_buf: Magic(UTF-8):%d",ch)); - lbuf[lpos++] = ch | CONTROL_TAG; - lbuf[lpos++] = CONTROL_TAG; - } - } - return lpos - buffpos; /* characters "written" into - current buffer (may be less due to newline) */ -} - - - -/* - * Write n characters in line buffer starting at s. Be smart about - * non-printables. Know about pad characters and that \n can never - * occur normally. - */ - -static int write_buf(Uint32 *s, int n, int next_char_is_crnl) -{ - byte ubuf[4]; - int ubytes = 0, i; - byte lastput = ' '; - - update_cols(); - - DEBUGLOG(("write_buf(%d, %d)",n,next_char_is_crnl)); - - while (n > 0) { - if (!(*s & TAG_MASK) ) { - if (utf8_mode) { - ubytes = 0; - if (put_utf8((int) *s, ubuf, 4, &ubytes) == 0) { - for (i = 0; i < ubytes; ++i) { - outc(ubuf[i]); - } - lastput = 0; /* Means the last written character was multibyte UTF8 */ - } - } else { - outc((byte) *s); - lastput = (byte) *s; - } - --n; - ++s; - } else if (*s == (CONTROL_TAG | ((Uint32) '\t'))) { - outc(lastput = ' '); - --n; s++; - while (n > 0 && *s == CONTROL_TAG) { - outc(lastput = ' '); - --n; s++; - } - } else if (*s == (CONTROL_TAG | ((Uint32) '\e'))) { - outc(lastput = '\e'); - --n; - ++s; - } else if (*s & CONTROL_TAG) { - byte c = (byte)*s; - outc('^'); - outc(lastput = ((byte) ((c == 0177 ? '?' : c | 0x40)))); - n -= 2; - s += 2; - } else if (*s & ESCAPED_TAG) { - Uint32 ch = *s & ~(TAG_MASK); - byte *octbuff; - byte octtmp[256]; - int octbytes; - DEBUGLOG(("write_buf: Escaped: %d", ch)); - octbytes = octal_or_hex_positions(ch); - if (octbytes > 256) { - octbuff = driver_alloc(octbytes); - } else { - octbuff = octtmp; - } - octbytes = 0; - octal_or_hex_format(ch, octbuff, &octbytes); - DEBUGLOG(("write_buf: octbytes: %d", octbytes)); - outc('\\'); - for (i = 0; i < octbytes; ++i) { - outc(lastput = octbuff[i]); - DEBUGLOG(("write_buf: outc: %d", (int) lastput)); - } - n -= octbytes+1; - s += octbytes+1; - if (octbuff != octtmp) { - driver_free(octbuff); - } - } else { - DEBUGLOG(("write_buf: Very unexpected character %d",(int) *s)); - ++n; - --s; - } - } - /* Check landed in first column of new line and have 'xn' bug. - * https://www.gnu.org/software/termutils/manual/termcap-1.3/html_node/termcap_27.html - * - * The 'xn' bugs (from what I understand) is that the terminal cursor does - * not wrap to the next line when the current line is full. For example: - * - * If the terminal column size is 20 and we output 20 'a' the cursor will be - * on row 1, column 21. While we actually want it at row 2 column 0. So to - * achieve this the code below emits " \b", which will move the cursor to the - * correct place. - * - * We should not apply this 'xn' workaround if we know that the next character - * to be emitted is a cr|nl as that will wrap by itself. - */ - n = s - lbuf; - if (!next_char_is_crnl && xn && n != 0 && COL(cp_pos_to_col(n)) == 0) { - if (n >= llen) { - outc(' '); - } else if (lastput == 0) { /* A multibyte UTF8 character */ - for (i = 0; i < ubytes; ++i) { - outc(ubuf[i]); - } - } else { - outc(lastput); - } - move_left(1); - } - return TRUE; -} - - -/* The basic procedure for outputting one character. */ -static int outc(int c) -{ - putcbuf->orig_bytes[putcpos++] = c; - if (putcpos == putclen) { - driver_enq_bin(ttysl_port,putcbuf,0,putclen); - driver_free_binary(putcbuf); - putcpos = 0; - putclen = TTY_BUFFSIZE; - putcbuf = driver_alloc_binary(BUFSIZ); - } - return 1; -} - -static int move_cursor(int from_pos, int to_pos) -{ - int from_col, to_col; - int dc, dl; - update_cols(); - - from_col = cp_pos_to_col(from_pos); - to_col = cp_pos_to_col(to_pos); - - dc = COL(to_col) - COL(from_col); - dl = LINE(to_col) - LINE(from_col); - DEBUGLOG(("move_cursor: from %d %d to %d %d => %d %d\n", - from_pos, from_col, to_pos, to_col, dl, dc)); - if (dl > 0) - move_down(dl); - else if (dl < 0) - move_up(-dl); - if (dc > 0) - move_right(dc); - else if (dc < 0) - move_left(-dc); - return to_col-from_col; -} - -/* - * Returns the length of an ANSI escape code in a buffer, this function only consider - * color escape sequences like `\e[33m` or `\e[21;33m`. If a sequence has no valid - * terminator, the length is equal the number of characters between `\e` and the first - * invalid character, inclusive. - */ - -static int ansi_escape_width(Uint32 *s, int max_length) -{ - int i; - - if (*s != (CONTROL_TAG | ((Uint32) '\e'))) { - return 0; - } else if (max_length <= 1) { - return 1; - } else if (s[1] != '[') { - return 2; - } - - for (i = 2; i < max_length && (s[i] == ';' || (s[i] >= '0' && s[i] <= '9')); i++); - - return i + 1; -} - -static int cp_pos_to_col(int cp_pos) -{ - /* - * If we don't have any character width information. Assume that - * code points are one column wide - */ - int w = 1; - int col = 0; - int i = 0; - int j; - - if (cp_pos > llen) { - col += cp_pos - llen; - cp_pos = llen; - } - - while (i < cp_pos) { - j = ansi_escape_width(lbuf + i, llen - i); - - if (j > 0) { - i += j; - } else { -#ifdef HAVE_WCWIDTH - w = wcwidth(lbuf[i]); -#endif - if (w > 0) { - col += w; - } - i++; - } - } - - return col; -} - -static int start_termcap(void) -{ - int eres; - size_t envsz = 1024; - char *env = NULL; - char *c; - int tres; - - DEBUGLOG(("start_termcap: ..")); - - capbuf = driver_alloc(1024); - if (!capbuf) - goto termcap_false; - eres = erl_drv_getenv("TERM", capbuf, &envsz); - if (eres == 0) - env = capbuf; - else if (eres < 0) { - DEBUGLOG(("start_termcap: failure in erl_drv_getenv(\"TERM\", ..) = %d\n", eres)); - goto termcap_false; - } else /* if (eres > 1) */ { - char *envbuf = driver_alloc(envsz); - if (!envbuf) - goto termcap_false; - while (1) { - char *newenvbuf; - eres = erl_drv_getenv("TERM", envbuf, &envsz); - if (eres == 0) - break; - newenvbuf = driver_realloc(envbuf, envsz); - if (eres < 0 || !newenvbuf) { - DEBUGLOG(("start_termcap: failure in erl_drv_getenv(\"TERM\", ..) = %d or realloc buf == %p\n", eres, newenvbuf)); - env = newenvbuf ? newenvbuf : envbuf; - goto termcap_false; - } - envbuf = newenvbuf; - } - env = envbuf; - } - if ((tres = tgetent((char*)lbuf, env)) <= 0) { - DEBUGLOG(("start_termcap: failure in tgetent(..) = %d\n", tres)); - goto termcap_false; - } - if (env != capbuf) { - env = NULL; - driver_free(env); - } - c = capbuf; - cols = tgetnum("co"); - if (cols <= 0) - cols = DEF_WIDTH; - xn = tgetflag("xn"); - up = tgetstr("up", &c); - if (!(down = tgetstr("do", &c))) - down = "\n"; - if (!(left = tgetflag("bs") ? "\b" : tgetstr("bc", &c))) - left = "\b"; /* Can't happen - but does on Solaris 2 */ - right = tgetstr("nd", &c); - if (up && down && left && right) { - DEBUGLOG(("start_termcap: successful start\n")); - return TRUE; - } - DEBUGLOG(("start_termcap: failed start\n")); - termcap_false: - if (env && env != capbuf) - driver_free(env); - if (capbuf) - driver_free(capbuf); - capbuf = NULL; - return FALSE; -} - -static int stop_termcap(void) -{ - if (capbuf) driver_free(capbuf); - capbuf = NULL; - return TRUE; -} - -static int move_left(int n) -{ - while (n-- > 0) - tputs(left, 1, outc); - return TRUE; -} - -static int move_right(int n) -{ - while (n-- > 0) - tputs(right, 1, outc); - return TRUE; -} - -static int move_up(int n) -{ - while (n-- > 0) - tputs(up, 1, outc); - return TRUE; -} - -static int move_down(int n) -{ - while (n-- > 0) - tputs(down, 1, outc); - return TRUE; -} - - -/* - * Updates cols if terminal has resized (SIGWINCH). Should be called - * at the start of any function that uses the COL or LINE macros. If - * the terminal is resized after calling this function but before use - * of the macros, then we may write to the wrong screen location. - * - * We cannot call this from the SIGWINCH handler because it uses - * ioctl() which is not a safe function as listed in the signal(7) - * man page. - */ -static void update_cols(void) -{ - Uint32 width, height; - - if (cols_needs_update) { - cols_needs_update = FALSE; - ttysl_get_window_size(&width, &height); - cols = width; - } -} - - -/* - * Put a terminal device into non-canonical mode with ECHO off. - * Before doing so we first save the terminal's current mode, - * assuming the caller will call the tty_reset() function - * (also in this file) when it's done with raw mode. - */ - -static struct termios tty_smode, tty_rmode; - -static int tty_init(int fd, int canon, int echo, int sig) { - int tres; - DEBUGLOG(("tty_init: fd = %d, canon = %d, echo = %d, sig = %d", fd, canon, echo, sig)); - if ((tres = tcgetattr(fd, &tty_rmode)) < 0) { - DEBUGLOG(("tty_init: failure in tcgetattr(%d,..) = %d\n", fd, tres)); - return -1; - } - tty_smode = tty_rmode; - - /* Default characteristics for all usage including termcap output. */ - tty_smode.c_iflag &= ~ISTRIP; - - /* Turn canonical (line mode) on off. */ - if (canon > 0) { - tty_smode.c_iflag |= ICRNL; - tty_smode.c_lflag |= ICANON; - tty_smode.c_oflag |= OPOST; - tty_smode.c_cc[VEOF] = tty_rmode.c_cc[VEOF]; -#ifdef VDSUSP - tty_smode.c_cc[VDSUSP] = tty_rmode.c_cc[VDSUSP]; -#endif - } - if (canon < 0) { - tty_smode.c_iflag &= ~ICRNL; - tty_smode.c_lflag &= ~ICANON; - tty_smode.c_oflag &= ~OPOST; - /* Must get these really right or funny effects can occur. */ - tty_smode.c_cc[VMIN] = 1; - tty_smode.c_cc[VTIME] = 0; -#ifdef VDSUSP - tty_smode.c_cc[VDSUSP] = 0; -#endif - } - - /* Turn echo on or off. */ - if (echo > 0) - tty_smode.c_lflag |= ECHO; - if (echo < 0) - tty_smode.c_lflag &= ~ECHO; - - /* Set extra characteristics for "RAW" mode, no signals. */ - if (sig > 0) { - /* Ignore IMAXBEL as not POSIX. */ -#ifndef QNX - tty_smode.c_iflag |= (BRKINT|IGNPAR|ICRNL|IXON|IXANY); -#else - tty_smode.c_iflag |= (BRKINT|IGNPAR|ICRNL|IXON); -#endif - tty_smode.c_lflag |= (ISIG|IEXTEN); - } - if (sig < 0) { - /* Ignore IMAXBEL as not POSIX. */ -#ifndef QNX - tty_smode.c_iflag &= ~(BRKINT|IGNPAR|ICRNL|IXON|IXANY); -#else - tty_smode.c_iflag &= ~(BRKINT|IGNPAR|ICRNL|IXON); -#endif - tty_smode.c_lflag &= ~(ISIG|IEXTEN); - } - DEBUGLOG(("tty_init: successful init\n")); - return 0; -} - -/* - * Set/restore a terminal's mode to whatever it was on the most - * recent call to the tty_init() function above. - */ - -static int tty_set(int fd) -{ - int tres; - DEBUGF(("tty_set: Setting tty...\n")); - - if ((tres = tcsetattr(fd, TCSANOW, &tty_smode)) < 0) { - DEBUGLOG(("tty_set: failure in tcgetattr(%d,..) = %d\n", fd, tres)); - return(-1); - } - return(0); -} - -static int tty_reset(int fd) /* of terminal device */ -{ - int tres; - DEBUGF(("tty_reset: Resetting tty...\n")); - - if ((tres = tcsetattr(fd, TCSANOW, &tty_rmode)) < 0) { - DEBUGLOG(("tty_reset: failure in tcsetattr(%d,..) = %d\n", fd, tres)); - return(-1); - } - return(0); -} - -/* - * Signal handler to cope with signals so that we can reset the tty - * to the original settings - */ - -#ifdef ERTS_NOT_USED -/* XXX: A mistake that it isn't used, or should it be removed? */ - -static RETSIGTYPE suspend(int sig) -{ - if (tty_reset(ttysl_fd) < 0) { - DEBUGLOG(("signal: failure in suspend(%d), can't reset tty %d\n", sig, ttysl_fd)); - fprintf(stderr,"Can't reset tty \n"); - exit(1); - } - - sys_signal(sig, SIG_DFL); /* Set signal handler to default */ - sys_sigrelease(sig); /* Allow 'sig' to come through */ - kill(getpid(), sig); /* Send ourselves the signal */ - sys_sigblock(sig); /* Reset to old mask */ - sys_signal(sig, suspend); /* Reset signal handler */ - - if (tty_set(ttysl_fd) < 0) { - DEBUGLOG(("signal: failure in suspend(%d), can't set tty %d\n", sig, ttysl_fd)); - fprintf(stderr,"Can't set tty raw \n"); - exit(1); - } -} - -#endif - -static RETSIGTYPE cont(int sig) -{ - if (tty_set(ttysl_fd) < 0) { - DEBUGLOG(("signal: failure in cont(%d), can't set tty raw %d\n", sig, ttysl_fd)); - fprintf(stderr,"Can't set tty raw\n"); - exit(1); - } -} - -static RETSIGTYPE winch(int sig) -{ - cols_needs_update = TRUE; -} -#endif /* HAVE_TERMCAP */ diff --git a/erts/emulator/drivers/win32/ttsl_drv.c b/erts/emulator/drivers/win32/ttsl_drv.c deleted file mode 100644 index 8917e48919f6..000000000000 --- a/erts/emulator/drivers/win32/ttsl_drv.c +++ /dev/null @@ -1,786 +0,0 @@ -/* - * %CopyrightBegin% - * - * Copyright Ericsson AB 1996-2021. 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * %CopyrightEnd% - */ -/* - * Tty driver that reads one character at the time and provides a - * smart line for output. - */ - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif -#include "sys.h" -#include -#include -#include -#include -#include - -#include "erl_driver.h" -#include "win_con.h" - -#define TRUE 1 -#define FALSE 0 - -static int cols; /* Number of columns available. */ -static int rows; /* Number of rows available. */ - -/* The various opcodes. */ -#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 CTRL_OP_GET_WINSIZE 100 -#define CTRL_OP_GET_UNICODE_STATE 101 -#define CTRL_OP_SET_UNICODE_STATE 102 - -static int lbuf_size = BUFSIZ; -Uint32 *lbuf; /* The current line buffer */ -int llen; /* The current line length */ -int lpos; /* The current "cursor position" in the line buffer */ - -/* - * Tags used in line buffer to show that these bytes represent special characters, - * Max unicode is 0x0010ffff, so we have lots of place for meta tags... - */ -#define CONTROL_TAG 0x10000000U /* Control character, value in first position */ -#define ESCAPED_TAG 0x01000000U /* Escaped character, value in first position */ -#define TAG_MASK 0xFF000000U - -#define MAXSIZE (1 << 16) - -#define ISPRINT(c) (isprint(c) || (128+32 <= (c) && (c) < 256)) - -#define DEBUGLOG(X) /* nothing */ - -/* - * XXX These are used by win_con.c (for command history). - * Should be cleaned up. - */ - - -#define NL '\n' - -/* Main interface functions. */ -static int ttysl_init(void); -static ErlDrvData ttysl_start(ErlDrvPort, char*); -static void ttysl_stop(ErlDrvData); -static ErlDrvSSizeT ttysl_control(ErlDrvData, unsigned int, - char *, ErlDrvSizeT, char **, ErlDrvSizeT); -static void ttysl_from_erlang(ErlDrvData, char*, ErlDrvSizeT); -static void ttysl_from_tty(ErlDrvData, ErlDrvEvent); -static Sint16 get_sint16(char *s); - -static ErlDrvPort ttysl_port; - -extern ErlDrvEvent console_input_event; -extern HANDLE console_thread; - -static HANDLE ttysl_in = INVALID_HANDLE_VALUE; /* Handle for console input. */ -static HANDLE ttysl_out = INVALID_HANDLE_VALUE; /* Handle for console output */ - -/* Functions that work on the line buffer. */ -static int start_lbuf(); -static int stop_lbuf(); -static int put_chars(); -static int move_rel(); -static int ins_chars(); -static int del_chars(); -static int step_over_chars(int n); -static int insert_buf(); -static int write_buf(); -static void move_cursor(int, int); - -/* Define the driver table entry. */ -struct erl_drv_entry ttsl_driver_entry = { - ttysl_init, - ttysl_start, - ttysl_stop, - ttysl_from_erlang, - ttysl_from_tty, - NULL, - "tty_sl", - NULL, - NULL, - ttysl_control, - NULL, /* timeout */ - NULL, /* outputv */ - NULL, /* ready_async */ - NULL, /* flush */ - NULL, /* call */ - NULL, /* event */ - ERL_DRV_EXTENDED_MARKER, - ERL_DRV_EXTENDED_MAJOR_VERSION, - ERL_DRV_EXTENDED_MINOR_VERSION, - 0, - NULL, - NULL, - NULL, -}; - -static int utf8_mode = 0; - -static int ttysl_init() -{ - lbuf = NULL; /* For line buffer handling */ - ttysl_port = (ErlDrvPort)-1; - return 0; -} - -static ErlDrvData ttysl_start(ErlDrvPort port, char* buf) -{ - if ((SWord)ttysl_port != -1 || console_thread == NULL) { - return ERL_DRV_ERROR_GENERAL; - } - start_lbuf(); - utf8_mode = 1; - driver_select(port, console_input_event, ERL_DRV_READ, 1); - ttysl_port = port; - return (ErlDrvData)ttysl_port;/* Nothing important to return */ -} - -#define DEF_HEIGHT 24 -#define DEF_WIDTH 80 - -static void ttysl_get_window_size(Uint32 *width, Uint32 *height) -{ - *width = ConGetColumns(); - *height = ConGetRows(); -} - - -static ErlDrvSSizeT ttysl_control(ErlDrvData drv_data, - unsigned int command, - char *buf, ErlDrvSizeT len, - char **rbuf, ErlDrvSizeT rlen) -{ - char resbuff[2*sizeof(Uint32)]; - ErlDrvSizeT res_size; - - command -= ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER; - switch (command) { - case CTRL_OP_GET_WINSIZE: - { - Uint32 w,h; - ttysl_get_window_size(&w,&h); - memcpy(resbuff,&w,sizeof(Uint32)); - memcpy(resbuff+sizeof(Uint32),&h,sizeof(Uint32)); - res_size = 2*sizeof(Uint32); - } - break; - case CTRL_OP_GET_UNICODE_STATE: - *resbuff = (utf8_mode) ? 1 : 0; - res_size = 1; - break; - case CTRL_OP_SET_UNICODE_STATE: - if (len != 0) { - int m = (int) *buf; - *resbuff = (utf8_mode) ? 1 : 0; - res_size = 1; - utf8_mode = (m) ? 1 : 0; - } else { - return 0; - } - break; - default: - return -1; - } - if (rlen < res_size) { - *rbuf = driver_alloc(res_size); - } - memcpy(*rbuf,resbuff,res_size); - return res_size; -} - - -static void ttysl_stop(ErlDrvData ttysl_data) -{ - if ((SWord)ttysl_port != -1) { - driver_select(ttysl_port, console_input_event, ERL_DRV_READ, 0); - } - - ttysl_in = ttysl_out = INVALID_HANDLE_VALUE; - stop_lbuf(); - ttysl_port = (ErlDrvPort)-1; -} - -static int put_utf8(int ch, byte *target, int sz, int *pos) -{ - Uint x = (Uint) ch; - if (x < 0x80) { - if (*pos >= sz) { - return -1; - } - target[(*pos)++] = (byte) x; - } - else if (x < 0x800) { - if (((*pos) + 1) >= sz) { - return -1; - } - target[(*pos)++] = (((byte) (x >> 6)) | - ((byte) 0xC0)); - target[(*pos)++] = (((byte) (x & 0x3F)) | - ((byte) 0x80)); - } else if (x < 0x10000) { - if ((x >= 0xD800 && x <= 0xDFFF) || - (x == 0xFFFE) || - (x == 0xFFFF)) { /* Invalid unicode range */ - return -1; - } - if (((*pos) + 2) >= sz) { - return -1; - } - - target[(*pos)++] = (((byte) (x >> 12)) | - ((byte) 0xE0)); - target[(*pos)++] = ((((byte) (x >> 6)) & 0x3F) | - ((byte) 0x80)); - target[(*pos)++] = (((byte) (x & 0x3F)) | - ((byte) 0x80)); - } else if (x < 0x110000) { /* Standard imposed max */ - if (((*pos) + 3) >= sz) { - return -1; - } - target[(*pos)++] = (((byte) (x >> 18)) | - ((byte) 0xF0)); - target[(*pos)++] = ((((byte) (x >> 12)) & 0x3F) | - ((byte) 0x80)); - target[(*pos)++] = ((((byte) (x >> 6)) & 0x3F) | - ((byte) 0x80)); - target[(*pos)++] = (((byte) (x & 0x3F)) | - ((byte) 0x80)); - } else { - return -1; - } - return 0; -} - - -static int pick_utf8(byte *s, int sz, int *pos) -{ - int size = sz - (*pos); - byte *source; - Uint unipoint; - - if (size > 0) { - source = s + (*pos); - if (((*source) & ((byte) 0x80)) == 0) { - unipoint = (int) *source; - ++(*pos); - return (int) unipoint; - } else if (((*source) & ((byte) 0xE0)) == 0xC0) { - if (size < 2) { - return -2; - } - if (((source[1] & ((byte) 0xC0)) != 0x80) || - ((*source) < 0xC2) /* overlong */) { - return -1; - } - (*pos) += 2; - unipoint = - (((Uint) ((*source) & ((byte) 0x1F))) << 6) | - ((Uint) (source[1] & ((byte) 0x3F))); - return (int) unipoint; - } else if (((*source) & ((byte) 0xF0)) == 0xE0) { - if (size < 3) { - return -2; - } - if (((source[1] & ((byte) 0xC0)) != 0x80) || - ((source[2] & ((byte) 0xC0)) != 0x80) || - (((*source) == 0xE0) && (source[1] < 0xA0)) /* overlong */ ) { - return -1; - } - if ((((*source) & ((byte) 0xF)) == 0xD) && - ((source[1] & 0x20) != 0)) { - return -1; - } - if (((*source) == 0xEF) && (source[1] == 0xBF) && - ((source[2] == 0xBE) || (source[2] == 0xBF))) { - return -1; - } - (*pos) += 3; - unipoint = - (((Uint) ((*source) & ((byte) 0xF))) << 12) | - (((Uint) (source[1] & ((byte) 0x3F))) << 6) | - ((Uint) (source[2] & ((byte) 0x3F))); - return (int) unipoint; - } else if (((*source) & ((byte) 0xF8)) == 0xF0) { - if (size < 4) { - return -2 ; - } - if (((source[1] & ((byte) 0xC0)) != 0x80) || - ((source[2] & ((byte) 0xC0)) != 0x80) || - ((source[3] & ((byte) 0xC0)) != 0x80) || - (((*source) == 0xF0) && (source[1] < 0x90)) /* overlong */) { - return -1; - } - if ((((*source) & ((byte)0x7)) > 0x4U) || - ((((*source) & ((byte)0x7)) == 0x4U) && - ((source[1] & ((byte)0x3F)) > 0xFU))) { - return -1; - } - (*pos) += 4; - unipoint = - (((Uint) ((*source) & ((byte) 0x7))) << 18) | - (((Uint) (source[1] & ((byte) 0x3F))) << 12) | - (((Uint) (source[2] & ((byte) 0x3F))) << 6) | - ((Uint) (source[3] & ((byte) 0x3F))); - return (int) unipoint; - } else { - return -1; - } - } else { - return -1; - } -} - -static int octal_or_hex_positions(Uint c) -{ - int x = 0; - Uint ch = c; - if (!ch) { - return 1; - } - while(ch) { - ++x; - ch >>= 3; - } - if (x <= 3) { - return 3; - } - /* \x{H ...} format when larger than \777 */ - x = 0; - ch = c; - while(ch) { - ++x; - ch >>= 4; - } - return x+3; -} - -static void octal_or_hex_format(Uint ch, byte *buf, int *pos) -{ - static byte hex_chars[] = { '0','1','2','3','4','5','6','7','8','9', - 'A','B','C','D','E','F'}; - int num = octal_or_hex_positions(ch); - if (num != 3) { - buf[(*pos)++] = 'x'; - buf[(*pos)++] = '{'; - num -= 3; - while(num--) { - buf[(*pos)++] = hex_chars[((ch >> (4*num)) & 0xFU)]; - } - buf[(*pos)++] = '}'; - } else { - while(num--) { - buf[(*pos)++] = ((byte) ((ch >> (3*num)) & 0x7U) + '0'); - } - } -} - -/* - * Check that there is enough room in all buffers to copy all pad chars - * and stiff we need If not, realloc lbuf. - */ -static int check_buf_size(byte *s, int n) -{ - int pos = 0; - int ch; - int size = 10; - - while(pos < n) { - /* Indata is always UTF-8 */ - if ((ch = pick_utf8(s,n,&pos)) < 0) { - /* XXX temporary allow invalid chars */ - ch = (int) s[pos]; - DEBUGLOG(("Invalid UTF8:%d",ch)); - ++pos; - } - if (utf8_mode) { /* That is, terminal is UTF8 compliant */ - if (ch >= 128 || isprint(ch)) { - DEBUGLOG(("Printable(UTF-8:%d):%d",pos,ch)); - size++; /* Buffer contains wide characters... */ - } else if (ch == '\t') { - size += 8; - } else { - DEBUGLOG(("Magic(UTF-8:%d):%d",pos,ch)); - size += 2; - } - } else { - if (ch <= 255 && isprint(ch)) { - DEBUGLOG(("Printable:%d",ch)); - size++; - } else if (ch == '\t') - size += 8; - else if (ch >= 128) { - DEBUGLOG(("Non printable:%d",ch)); - size += (octal_or_hex_positions(ch) + 1); - } - else { - DEBUGLOG(("Magic:%d",ch)); - size += 2; - } - } - } - - if (size + lpos >= lbuf_size) { - - lbuf_size = size + lpos + BUFSIZ; - if ((lbuf = driver_realloc(lbuf, lbuf_size * sizeof(Uint32))) == NULL) { - driver_failure(ttysl_port, -1); - return(0); - } - } - return(1); -} - - -static void ttysl_from_erlang(ErlDrvData ttysl_data, char* buf, ErlDrvSizeT count) -{ - if (lpos > MAXSIZE) - put_chars((byte*)"\n", 1); - - switch (buf[0]) { - case OP_PUTC: - case OP_PUTC_SYNC: - DEBUGLOG(("OP: Putc(%I64u)",(unsigned long long)count-1)); - if (check_buf_size((byte*)buf+1, count-1) == 0) - return; - put_chars((byte*)buf+1, count-1); - break; - case OP_MOVE: - move_rel(get_sint16(buf+1)); - break; - case OP_INSC: - if (check_buf_size((byte*)buf+1, count-1) == 0) - return; - ins_chars((byte*)buf+1, count-1); - break; - case OP_DELC: - del_chars(get_sint16(buf+1)); - break; - case OP_BEEP: - ConBeep(); - break; - default: - /* Unknown op, just ignore. */ - break; - } - - if (buf[0] == OP_PUTC_SYNC) { - /* On windows we do a blocking write to the tty so we just - send the ack immediately. If at some point in the future - someone has a problem with tty output being blocking - this has to be changed. */ - ErlDrvTermData spec[] = { - ERL_DRV_PORT, driver_mk_port(ttysl_port), - ERL_DRV_ATOM, driver_mk_atom("ok"), - ERL_DRV_TUPLE, 2 - }; - erl_drv_output_term(driver_mk_port(ttysl_port), spec, - sizeof(spec) / sizeof(spec[0])); - } - return; -} - -extern int read_inbuf(char *data, int n); - -static void ttysl_from_tty(ErlDrvData ttysl_data, ErlDrvEvent fd) -{ - Uint32 inbuf[64]; - byte t[1024]; - int i,pos,tpos; - - i = ConReadInput(inbuf,1); - - pos = 0; - tpos = 0; - - while (pos < i) { - while (tpos < 1020 && pos < i) { /* Max 4 bytes for UTF8 */ - put_utf8((int) inbuf[pos++], t, 1024, &tpos); - } - driver_output(ttysl_port, (char *) t, tpos); - tpos = 0; - } -} - -/* - * Gets signed 16 bit integer from binary buffer. - */ -static Sint16 -get_sint16(char *s) -{ - return ((*s << 8) | ((byte*)s)[1]); -} - - -static int start_lbuf(void) -{ - if (!lbuf && !(lbuf = ( Uint32*) driver_alloc(lbuf_size * sizeof(Uint32)))) - return FALSE; - llen = 0; - lpos = 0; - return TRUE; -} - -static int stop_lbuf(void) -{ - if (lbuf) { - driver_free(lbuf); - lbuf = NULL; - } - llen = 0; /* To avoid access error in win_con:AddToCmdHistory during exit*/ - return TRUE; -} - -/* Put l bytes (in UTF8) from s into the buffer and output them. */ -static int put_chars(byte *s, int l) -{ - int n; - - n = insert_buf(s, l); - if (n > 0) - write_buf(lbuf + lpos - n, n); - if (lpos > llen) - llen = lpos; - return TRUE; -} - -/* - * Move the current position forwards or backwards within the current - * line. We know about padding. - */ -static int move_rel(int n) -{ - int npos; /* The new position */ - - /* Step forwards or backwards over the buffer. */ - npos = step_over_chars(n); - - /* Calculate move, updates pointers and move the cursor. */ - move_cursor(lpos, npos); - lpos = npos; - return TRUE; -} - -/* Insert characters into the buffer at the current position. */ -static int ins_chars(byte *s, int l) -{ - int n, tl; - Uint32 *tbuf = NULL; /* Suppress warning about use-before-set */ - - /* Move tail of buffer to make space. */ - if ((tl = llen - lpos) > 0) { - if ((tbuf = driver_alloc(tl * sizeof(Uint32))) == NULL) - return FALSE; - memcpy(tbuf, lbuf + lpos, tl * sizeof(Uint32)); - } - n = insert_buf(s, l); - if (tl > 0) { - memcpy(lbuf + lpos, tbuf, tl * sizeof(Uint32)); - driver_free(tbuf); - } - llen += n; - write_buf(lbuf + (lpos - n), llen - (lpos - n)); - move_cursor(llen, lpos); - return TRUE; -} - -/* - * Delete characters in the buffer. Can delete characters before (n < 0) - * and after (n > 0) the current position. Cursor left at beginning of - * deleted block. - */ -static int del_chars(int n) -{ - int i, l, r; - int pos; - - /*update_cols();*/ - - /* Step forward or backwards over n logical characters. */ - pos = step_over_chars(n); - - if (pos > lpos) { - l = pos - lpos; /* Buffer characters to delete */ - r = llen - lpos - l; /* Characters after deleted */ - /* Fix up buffer and buffer pointers. */ - if (r > 0) - memcpy(lbuf + lpos, lbuf + pos, r * sizeof(Uint32)); - llen -= l; - /* Write out characters after, blank the tail and jump back to lpos. */ - write_buf(lbuf + lpos, r); - for (i = l ; i > 0; --i) - ConPutChar(' '); - move_cursor(llen + l, lpos); - } - else if (pos < lpos) { - l = lpos - pos; /* Buffer characters */ - r = llen - lpos; /* Characters after deleted */ - move_cursor(lpos, lpos-l); /* Move back */ - /* Fix up buffer and buffer pointers. */ - if (r > 0) - memcpy(lbuf + pos, lbuf + lpos, r * sizeof(Uint32)); - lpos -= l; - llen -= l; - /* Write out characters after, blank the tail and jump back to lpos. */ - write_buf(lbuf + lpos, r); - for (i = l ; i > 0; --i) - ConPutChar(' '); - move_cursor(llen + l, lpos); - } - return TRUE; -} - - -/* Step over n logical characters, check for overflow. */ -static int step_over_chars(int n) -{ - Uint32 *c, *beg, *end; - - beg = lbuf; - end = lbuf + llen; - c = lbuf + lpos; - for ( ; n > 0 && c < end; --n) { - c++; - while (c < end && (*c & TAG_MASK) && ((*c & ~TAG_MASK) == 0)) - c++; - } - for ( ; n < 0 && c > beg; n++) { - --c; - while (c > beg && (*c & TAG_MASK) && ((*c & ~TAG_MASK) == 0)) - --c; - } - return c - lbuf; -} - -static int insert_buf(byte *s, int n) -{ - int pos = 0; - int buffpos = lpos; - int ch; - - while (pos < n) { - if ((ch = pick_utf8(s,n,&pos)) < 0) { - /* XXX temporary allow invalid chars */ - ch = (int) s[pos]; - DEBUGLOG(("insert_buf: Invalid UTF8:%d",ch)); - ++pos; - } - if ((utf8_mode && (ch >= 128 || isprint(ch))) || (ch <= 255 && isprint(ch))) { - DEBUGLOG(("insert_buf: Printable(UTF-8):%d",ch)); - lbuf[lpos++] = (Uint32) ch; - } else if (ch >= 128) { /* not utf8 mode */ - int nc = octal_or_hex_positions(ch); - lbuf[lpos++] = ((Uint32) ch) | ESCAPED_TAG; - while (nc--) { - lbuf[lpos++] = ESCAPED_TAG; - } - } else if (ch == '\t') { - do { - lbuf[lpos++] = (CONTROL_TAG | ((Uint32) ch)); - ch = 0; - } while (lpos % 8); - } else if (ch == '\n' || ch == '\r') { - write_buf(lbuf + buffpos, lpos - buffpos); - ConPutChar('\r'); - if (ch == '\n') - ConPutChar('\n'); - if (llen > lpos) { - memcpy(lbuf, lbuf + lpos, llen - lpos); - } - llen -= lpos; - lpos = buffpos = 0; - } else { - DEBUGLOG(("insert_buf: Magic(UTF-8):%d",ch)); - lbuf[lpos++] = ch | CONTROL_TAG; - lbuf[lpos++] = CONTROL_TAG; - } - } - return lpos - buffpos; /* characters "written" into - current buffer (may be less due to newline) */ -} -static int write_buf(Uint32 *s, int n) -{ - int i; - - /*update_cols();*/ - - while (n > 0) { - if (!(*s & TAG_MASK) ) { - ConPutChar(*s); - --n; - ++s; - } - else if (*s == (CONTROL_TAG | ((Uint32) '\t'))) { - ConPutChar(' '); - --n; s++; - while (n > 0 && *s == CONTROL_TAG) { - ConPutChar(' '); - --n; s++; - } - } else if (*s & CONTROL_TAG) { - ConPutChar('^'); - ConPutChar((*s == 0177) ? '?' : *s | 0x40); - n -= 2; - s += 2; - } else if (*s & ESCAPED_TAG) { - Uint32 ch = *s & ~(TAG_MASK); - byte *octbuff; - byte octtmp[256]; - int octbytes; - DEBUGLOG(("Escaped: %d", ch)); - octbytes = octal_or_hex_positions(ch); - if (octbytes > 256) { - octbuff = driver_alloc(octbytes); - } else { - octbuff = octtmp; - } - octbytes = 0; - octal_or_hex_format(ch, octbuff, &octbytes); - DEBUGLOG(("octbytes: %d", octbytes)); - ConPutChar('\\'); - for (i = 0; i < octbytes; ++i) { - ConPutChar(octbuff[i]); - } - n -= octbytes+1; - s += octbytes+1; - if (octbuff != octtmp) { - driver_free(octbuff); - } - } else { - DEBUGLOG(("Very unexpected character %d",(int) *s)); - ++n; - --s; - } - } - return TRUE; -} - - -static void -move_cursor(int from, int to) -{ - ConSetCursor(from,to); -} diff --git a/erts/emulator/drivers/win32/win_con.c b/erts/emulator/drivers/win32/win_con.c deleted file mode 100644 index 2e3c12dc58d1..000000000000 --- a/erts/emulator/drivers/win32/win_con.c +++ /dev/null @@ -1,2355 +0,0 @@ -/* - * %CopyrightBegin% - * - * Copyright Ericsson AB 1997-2021. 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * %CopyrightEnd% - */ - -#define UNICODE 1 -#define _UNICODE 1 -#include -#include -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif -#include "sys.h" -#include -#include "resource.h" -#include "erl_version.h" -#include -#include -#include "erl_driver.h" -#include "win_con.h" - -#define ALLOC(X) malloc(X) -#define REALLOC(X,Y) realloc(X,Y) -#define FREE(X) free(X) - -#if SIZEOF_VOID_P == 8 -#define WIN64 1 -#ifndef GCL_HBRBACKGROUND -#define GCL_HBRBACKGROUND GCLP_HBRBACKGROUND -#endif -#define DIALOG_PROC_RET INT_PTR -#define CF_HOOK_RET INT_PTR -#define CC_HOOK_RET INT_PTR -#define OFN_HOOK_RET INT_PTR -#else -#define DIALOG_PROC_RET BOOL -#define CF_HOOK_RET UINT -#define CC_HOOK_RET UINT -#define OFN_HOOK_RET UINT -#endif - - -#ifndef STATE_SYSTEM_INVISIBLE -/* Mingw problem with oleacc.h and WIN32_LEAN_AND_MEAN */ -#define STATE_SYSTEM_INVISIBLE 0x00008000 -#endif - -#define WM_CONTEXT (0x0401) -#define WM_CONBEEP (0x0402) -#define WM_SAVE_PREFS (0x0403) - -#define USER_KEY TEXT("Software\\Ericsson\\Erlang\\") TEXT(ERLANG_VERSION) - -#define FRAME_HEIGHT ((2*GetSystemMetrics(SM_CYEDGE))+(2*GetSystemMetrics(SM_CYFRAME))+GetSystemMetrics(SM_CYCAPTION)) -#define FRAME_WIDTH (2*GetSystemMetrics(SM_CXFRAME)+(2*GetSystemMetrics(SM_CXFRAME))+GetSystemMetrics(SM_CXVSCROLL)) - -#define LINE_LENGTH canvasColumns -#define COL(_l) ((_l) % LINE_LENGTH) -#define LINE(_l) ((_l) / LINE_LENGTH) - -#ifdef UNICODE -/* - * We use a character in the invalid unicode range - */ -#define SET_CURSOR (0xD8FF) -#else -/* - * XXX There is no escape to send a character 0x80. Fortunately, - * the ttsl driver currently replaces 0x80 with an octal sequence. - */ -#define SET_CURSOR (0x80) -#endif - -#define SCAN_CODE_BREAK 0x46 /* scan code for Ctrl-Break */ - - -typedef struct ScreenLine_s { - struct ScreenLine_s* next; - struct ScreenLine_s* prev; - int width; -#ifdef HARDDEBUG - int allocated; -#endif - int newline; /* Ends with hard newline: 1, wrapped at end: 0 */ - TCHAR *text; -} ScreenLine_t; - -extern Uint32 *lbuf; /* The current line buffer */ -extern int llen; /* The current line length */ -extern int lpos; - -HANDLE console_input_event; -HANDLE console_thread = NULL; - -#define DEF_CANVAS_COLUMNS 80 -#define DEF_CANVAS_ROWS 26 - -#define BUFSIZE 4096 -#define MAXBUFSIZE 32768 -typedef struct { - TCHAR *data; - int size; - int wrPos; - int rdPos; -} buffer_t; - -static buffer_t inbuf; -static buffer_t outbuf; - -static CHOOSEFONT cf; - -static TCHAR szFrameClass[] = TEXT("FrameClass"); -static TCHAR szClientClass[] = TEXT("ClientClass"); -static HWND hFrameWnd; -static HWND hClientWnd; -static HWND hTBWnd; -static HWND hComboWnd; -static HANDLE console_input; -static HANDLE console_output; -static int cxChar,cyChar, cxCharMax; -static int cxClient,cyClient; -static int cyToolBar; -static int iVscrollPos,iHscrollPos; -static int iVscrollMax,iHscrollMax; -static int nBufLines; -static int cur_x; -static int cur_y; -static int canvasColumns = DEF_CANVAS_COLUMNS; -static int canvasRows = DEF_CANVAS_ROWS; -static ScreenLine_t *buffer_top,*buffer_bottom; -static ScreenLine_t* cur_line; -static POINT editBeg,editEnd; -static BOOL fSelecting = FALSE; -static BOOL fTextSelected = FALSE; -static HKEY key; -static BOOL has_key = FALSE; -static LOGFONT logfont; -static DWORD fgColor; -static DWORD bkgColor; -static FILE *logfile = NULL; -static RECT winPos; -static BOOL toolbarVisible; -static BOOL destroyed = FALSE; - -static int lines_to_save = 10000; /* Maximum number of screen lines to save. */ - -#define TITLE_BUF_SZ 256 - -struct title_buf { - TCHAR *name; - TCHAR buf[TITLE_BUF_SZ]; -}; - -static TCHAR *erlang_window_title = TEXT("Erlang"); - -static unsigned __stdcall ConThreadInit(LPVOID param); -static LRESULT CALLBACK ClientWndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam); -static LRESULT CALLBACK FrameWndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam); -static DIALOG_PROC_RET CALLBACK AboutDlgProc(HWND hDlg, UINT iMsg, WPARAM wParam, LPARAM lParam); -static ScreenLine_t *ConNewLine(void); -static void DeleteTopLine(void); -static void ensure_line_below(void); -static ScreenLine_t *GetLineFromY(int y); -static void LoadUserPreferences(void); -static void SaveUserPreferences(void); -static void set_scroll_info(HWND hwnd); -static void ConCarriageFeed(int); -static void ConScrollScreen(void); -static BOOL ConChooseFont(HWND hwnd); -static void ConFontInitialize(HWND hwnd); -static void ConSetFont(HWND hwnd); -static void ConChooseColor(HWND hwnd); -static void DrawSelection(HWND hwnd, POINT pt1, POINT pt2); -static void InvertSelectionArea(HWND hwnd); -static void OnEditCopy(HWND hwnd); -static void OnEditPaste(HWND hwnd); -static void OnEditSelAll(HWND hwnd); -static void GetFileName(HWND hwnd, TCHAR *pFile); -static void OpenLogFile(HWND hwnd); -static void CloseLogFile(HWND hwnd); -static void LogFileWrite(TCHAR *buf, int n); -static int write_inbuf(TCHAR *data, int n); -static void init_buffers(void); -static void AddToCmdHistory(void); -static int write_outbuf(TCHAR *data, int num_chars); -static void ConDrawText(HWND hwnd); -static BOOL (WINAPI *ctrl_handler)(DWORD); -static HWND InitToolBar(HWND hwndParent); -static void window_title(struct title_buf *); -static void free_window_title(struct title_buf *); -static void Client_OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags); - -#ifdef HARDDEBUG -/* For really hard GUI startup debugging, place DEBUGBOX() macros in code - and get modal message boxes with the line number. */ -static void debug_box(int line) { - TCHAR buff[1024]; - swprintf(buff,1024,TEXT("DBG:%d"),line); - MessageBox(NULL,buff,TEXT("DBG"),MB_OK|MB_APPLMODAL); -} - -#define DEBUGBOX() debug_box(__LINE__) -#endif - -#define CON_VPRINTF_BUF_INC_SIZE 1024 - -static erts_dsprintf_buf_t * -grow_con_vprintf_buf(erts_dsprintf_buf_t *dsbufp, size_t need) -{ - char *buf; - size_t size; - - ASSERT(dsbufp); - - if (!dsbufp->str) { - size = (((need + CON_VPRINTF_BUF_INC_SIZE - 1) - / CON_VPRINTF_BUF_INC_SIZE) - * CON_VPRINTF_BUF_INC_SIZE); - buf = (char *) ALLOC(size * sizeof(char)); - } - else { - size_t free_size = dsbufp->size - dsbufp->str_len; - - if (need <= free_size) - return dsbufp; - - size = need - free_size + CON_VPRINTF_BUF_INC_SIZE; - size = (((size + CON_VPRINTF_BUF_INC_SIZE - 1) - / CON_VPRINTF_BUF_INC_SIZE) - * CON_VPRINTF_BUF_INC_SIZE); - size += dsbufp->size; - buf = (char *) REALLOC((void *) dsbufp->str, - size * sizeof(char)); - } - if (!buf) - return NULL; - if (buf != dsbufp->str) - dsbufp->str = buf; - dsbufp->size = size; - return dsbufp; -} - -static int con_vprintf(char *format, va_list arg_list) -{ - int res,i; - erts_dsprintf_buf_t dsbuf = ERTS_DSPRINTF_BUF_INITER(grow_con_vprintf_buf); - res = erts_vdsprintf(&dsbuf, format, arg_list); - if (res >= 0) { - TCHAR *tmp = ALLOC(dsbuf.str_len*sizeof(TCHAR)); - for (i=0;iwidth < xpos) { - return (canvasColumns-hscroll)*cxChar; - } - /* Not needed (?): SelectObject(hdc,CreateFontIndirect(&logfont)); */ - if (GetTextExtentPoint32(hdc,pLine->text,xpos,&size)) { -#ifdef HARDDEBUG - fprintf(stderr,"size.cx:%d\n",(int)size.cx); - fflush(stderr); -#endif - if (hscrollPix >= size.cx) { - return 0; - } - return ((int) size.cx) - hscrollPix; - } else { - return (xpos-hscroll)*cxChar; - } -} - -static int GetXFromCurrentY(HDC hdc, int hscroll, int xpos) { - return GetXFromLine(hdc, hscroll, xpos, GetLineFromY(cur_y)); -} - -void ConSetCursor(int from, int to) -{ TCHAR cmd[9]; - int *p; - //DebugBreak(); - cmd[0] = SET_CURSOR; - /* - * XXX Expect trouble on CPUs which don't allow misaligned read and writes. - */ - p = (int *)&cmd[1]; - *p++ = from; - *p = to; - write_outbuf(cmd, 1 + (2*sizeof(int)/sizeof(TCHAR))); -} - -void ConPrintf(char *format, ...) -{ - va_list va; - - va_start(va, format); - (void) con_vprintf(format, va); - va_end(va); -} - -void ConBeep(void) -{ - SendMessage(hClientWnd, WM_CONBEEP, 0L, 0L); -} - -int ConReadInput(Uint32 *data, int num_chars) -{ - TCHAR *buf; - int nread; - WaitForSingleObject(console_input,INFINITE); - nread = num_chars = min(num_chars,inbuf.wrPos-inbuf.rdPos); - buf = &inbuf.data[inbuf.rdPos]; - inbuf.rdPos += nread; - while (nread--) - *data++ = *buf++; - if (inbuf.rdPos >= inbuf.wrPos) { - inbuf.rdPos = 0; - inbuf.wrPos = 0; - ResetEvent(console_input_event); - } - ReleaseSemaphore(console_input,1,NULL); - return num_chars; -} - -int ConGetKey(void) -{ - Uint32 c; - WaitForSingleObject(console_input,INFINITE); - ResetEvent(console_input_event); - inbuf.rdPos = inbuf.wrPos = 0; - ReleaseSemaphore(console_input,1,NULL); - WaitForSingleObject(console_input_event,INFINITE); - ConReadInput(&c, 1); - return (int) c; -} - -int ConGetColumns(void) -{ - return (int) canvasColumns; /* 32bit atomic on windows */ -} - -int ConGetRows(void) { - return (int) canvasRows; -} - - -static HINSTANCE hInstance; -extern HMODULE beam_module; - -static unsigned __stdcall -ConThreadInit(LPVOID param) -{ - MSG msg; - WNDCLASSEX wndclass; - int iCmdShow; - STARTUPINFO StartupInfo; - HACCEL hAccel; - int x, y, w, h; - struct title_buf title; - - /*DebugBreak();*/ -#ifdef HARDDEBUG - if(AttachConsole(ATTACH_PARENT_PROCESS) || AllocConsole()) { - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); - } -#endif - - hInstance = GetModuleHandle(NULL); - StartupInfo.dwFlags = 0; - GetStartupInfo(&StartupInfo); - iCmdShow = StartupInfo.dwFlags & STARTF_USESHOWWINDOW ? - StartupInfo.wShowWindow : SW_SHOWDEFAULT; - - LoadUserPreferences(); - - /* frame window class */ - wndclass.cbSize = sizeof (wndclass); - wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_BYTEALIGNCLIENT; - wndclass.lpfnWndProc = FrameWndProc; - wndclass.cbClsExtra = 0; - wndclass.cbWndExtra = 0; - wndclass.hInstance = hInstance; - wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE(1)); - wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); - wndclass.hbrBackground = NULL; - wndclass.lpszMenuName = NULL; - wndclass.lpszClassName = szFrameClass; - wndclass.hIconSm = LoadIcon (hInstance, MAKEINTRESOURCE(1)); - RegisterClassExW (&wndclass); - - /* client window class */ - wndclass.cbSize = sizeof (wndclass); - wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; - wndclass.lpfnWndProc = ClientWndProc; - wndclass.cbClsExtra = 0; - wndclass.cbWndExtra = 0; - wndclass.hInstance = hInstance; - wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE(1)); - wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); - wndclass.hbrBackground = CreateSolidBrush(bkgColor); - wndclass.lpszMenuName = NULL; - wndclass.lpszClassName = szClientClass; - wndclass.hIconSm = LoadIcon (hInstance, MAKEINTRESOURCE(1)); - RegisterClassExW (&wndclass); - - InitCommonControls(); - init_buffers(); - - nBufLines = 0; - buffer_top = cur_line = ConNewLine(); - cur_line->next = buffer_bottom = ConNewLine(); - buffer_bottom->prev = cur_line; - - /* Create Frame Window */ - window_title(&title); - hFrameWnd = CreateWindowEx(0, szFrameClass, title.name, - WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, - CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT, - NULL,LoadMenu(beam_module,MAKEINTRESOURCE(1)), - hInstance,NULL); - free_window_title(&title); - - /* XXX OTP-5522: - The window position is not saved correctly and if the window - is closed when minimized, it's not possible to start werl again - with the window open. Temporary fix so far is to ignore saved values - and always start with initial settings. */ - /* Original: if (winPos.left == -1) { */ - /* Temporary: if (1) { */ - if (1) { - - /* initial window position */ - x = 0; - y = 0; - w = cxChar*LINE_LENGTH+FRAME_WIDTH+GetSystemMetrics(SM_CXVSCROLL); - h = cyChar*30+FRAME_HEIGHT; - } else { - /* saved window position */ - x = winPos.left; - y = winPos.top; - w = winPos.right - x; - h = winPos.bottom - y; - } - SetWindowPos(hFrameWnd, NULL, x, y, w, h, SWP_NOZORDER); - - ShowWindow(hFrameWnd, iCmdShow); - UpdateWindow(hFrameWnd); - - hAccel = LoadAccelerators(beam_module,MAKEINTRESOURCE(1)); - - ReleaseSemaphore(console_input, 1, NULL); - ReleaseSemaphore(console_output, 1, NULL); - - - /* Main message loop */ - while (GetMessage (&msg, NULL, 0, 0)) - { - if (!TranslateAccelerator(hFrameWnd,hAccel,&msg)) - { - TranslateMessage (&msg); - DispatchMessage (&msg); - } - } - /* - PostQuitMessage() results in WM_QUIT which makes GetMessage() - return 0 (which stops the main loop). Before we return from - the console thread, the ctrl_handler is called to do erts_exit. - */ - (*ctrl_handler)(CTRL_CLOSE_EVENT); - return msg.wParam; -} - -static LRESULT CALLBACK -FrameWndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) -{ - RECT r; - int cy,i,bufsize; - TCHAR c; - unsigned long l; - TCHAR buf[128]; - struct title_buf title; - - switch (iMsg) { - case WM_CREATE: - /* client window creation */ - window_title(&title); - hClientWnd = CreateWindowEx(0, szClientClass, title.name, - WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_HSCROLL, - CW_USEDEFAULT, CW_USEDEFAULT, - CW_USEDEFAULT, CW_USEDEFAULT, - hwnd, (HMENU)0, hInstance, NULL); - free_window_title(&title); - hTBWnd = InitToolBar(hwnd); - UpdateWindow (hClientWnd); - return 0; - case WM_SIZE : - if (IsWindowVisible(hTBWnd)) { - SendMessage(hTBWnd,TB_AUTOSIZE,0,0L); - GetWindowRect(hTBWnd,&r); - cy = r.bottom-r.top; - } else cy = 0; - MoveWindow(hClientWnd,0,cy,LOWORD(lParam),HIWORD(lParam)-cy,TRUE); - return 0; - case WM_ERASEBKGND: - return 1; - case WM_SETFOCUS : - CreateCaret(hClientWnd, NULL, cxChar, cyChar); - SetCaretPos(GetXFromCurrentY(GetDC(hClientWnd),iHscrollPos,cur_x), (cur_y-iVscrollPos)*cyChar); - ShowCaret(hClientWnd); - return 0; - case WM_KILLFOCUS: - HideCaret(hClientWnd); - DestroyCaret(); - return 0; - case WM_INITMENUPOPUP : - if (lParam == 0) /* File popup menu */ - { - EnableMenuItem((HMENU)wParam, IDMENU_STARTLOG, - logfile ? MF_GRAYED : MF_ENABLED); - EnableMenuItem((HMENU)wParam, IDMENU_STOPLOG, - logfile ? MF_ENABLED : MF_GRAYED); - return 0; - } - else if (lParam == 1) /* Edit popup menu */ - { - EnableMenuItem((HMENU)wParam, IDMENU_COPY, - fTextSelected ? MF_ENABLED : MF_GRAYED); - EnableMenuItem((HMENU)wParam, IDMENU_PASTE, - IsClipboardFormatAvailable(CF_TEXT) ? MF_ENABLED : MF_GRAYED); - return 0; - } - else if (lParam == 3) /* View popup menu */ - { - CheckMenuItem((HMENU)wParam,IDMENU_TOOLBAR, - IsWindowVisible(hTBWnd) ? MF_CHECKED : MF_UNCHECKED); - return 0; - } - break; - case WM_NOTIFY: - switch (((LPNMHDR) lParam)->code) { - case TTN_NEEDTEXT: - { - LPTOOLTIPTEXT lpttt; - lpttt = (LPTOOLTIPTEXT) lParam; - lpttt->hinst = hInstance; - /* check for combobox handle */ - if (lpttt->uFlags&TTF_IDISHWND) { - if ((lpttt->hdr.idFrom == (UINT_PTR) hComboWnd)) { - lstrcpy(lpttt->lpszText,TEXT("Command History")); - break; - } - } - /* check for toolbar buttons */ - switch (lpttt->hdr.idFrom) { - case IDMENU_COPY: - lstrcpy(lpttt->lpszText,TEXT("Copy (Ctrl+C)")); - break; - case IDMENU_PASTE: - lstrcpy(lpttt->lpszText,TEXT("Paste (Ctrl+V)")); - break; - case IDMENU_FONT: - lstrcpy(lpttt->lpszText,TEXT("Fonts")); - break; - case IDMENU_ABOUT: - lstrcpy(lpttt->lpszText,TEXT("Help")); - break; - } - } - } - break; - case WM_COMMAND: - switch(LOWORD(wParam)) - { - case IDMENU_STARTLOG: - OpenLogFile(hwnd); - return 0; - case IDMENU_STOPLOG: - CloseLogFile(hwnd); - return 0; - case IDMENU_EXIT: - SendMessage(hwnd, WM_CLOSE, 0, 0L); - return 0; - case IDMENU_COPY: - if (fTextSelected) - OnEditCopy(hClientWnd); - return 0; - case IDMENU_PASTE: - OnEditPaste(hClientWnd); - return 0; - case IDMENU_SELALL: - OnEditSelAll(hClientWnd); - return 0; - case IDMENU_FONT: - if (ConChooseFont(hClientWnd)) { - ConSetFont(hClientWnd); - } - SaveUserPreferences(); - return 0; - case IDMENU_SELECTBKG: - ConChooseColor(hClientWnd); - SaveUserPreferences(); - return 0; - case IDMENU_TOOLBAR: - if (toolbarVisible) { - ShowWindow(hTBWnd,SW_HIDE); - toolbarVisible = FALSE; - } else { - ShowWindow(hTBWnd,SW_SHOW); - toolbarVisible = TRUE; - } - GetClientRect(hwnd,&r); - PostMessage(hwnd,WM_SIZE,0,MAKELPARAM(r.right,r.bottom)); - return 0; - case IDMENU_ABOUT: - DialogBox(beam_module,TEXT("AboutBox"),hwnd,AboutDlgProc); - return 0; - case ID_COMBOBOX: - switch (HIWORD(wParam)) { - case CBN_SELENDOK: - i = SendMessage(hComboWnd,CB_GETCURSEL,0,0); - if (i != CB_ERR) { - buf[0] = 0x01; /* CTRL+A */ - buf[1] = 0x0B; /* CTRL+K */ - bufsize = SendMessage(hComboWnd,CB_GETLBTEXT,i,(LPARAM)&buf[2]); - if (bufsize != CB_ERR) - write_inbuf(buf,bufsize+2); - SetFocus(hwnd); - } - break; - case CBN_SELENDCANCEL: - break; - } - break; - case ID_BREAK: /* CTRL+BRK */ - /* pass on break char if the ctrl_handler is disabled */ - if ((*ctrl_handler)(CTRL_C_EVENT) == FALSE) { - c = 0x03; - write_inbuf(&c,1); - } - return 0; - } - break; - case WM_KEYDOWN : - switch (wParam) { - case VK_UP: c = 'P'-'@'; break; - case VK_DOWN : c = 'N'-'@'; break; - case VK_RIGHT : c = 'F'-'@'; break; - case VK_LEFT : c = 'B'-'@'; break; - case VK_DELETE : c = 'D' -'@'; break; - case VK_HOME : c = 'A'-'@'; break; - case VK_END : c = 'E'-'@'; break; - case VK_RETURN : AddToCmdHistory(); return 0; - case VK_PRIOR : /* PageUp */ - PostMessage(hClientWnd, WM_VSCROLL, SB_PAGEUP, 0); - return 0; - case VK_NEXT : /* PageDown */ - PostMessage(hClientWnd, WM_VSCROLL, SB_PAGEDOWN, 0); - return 0; - default: return 0; - } - write_inbuf(&c, 1); - return 0; - case WM_MOUSEWHEEL: - { - int delta = GET_WHEEL_DELTA_WPARAM(wParam); - if (delta < 0) { - PostMessage(hClientWnd, WM_VSCROLL, MAKELONG(SB_THUMBTRACK, - (iVscrollPos + 5)),0); - } else { - WORD pos = ((iVscrollPos - 5) < 0) ? 0 : (iVscrollPos - 5); - PostMessage(hClientWnd, WM_VSCROLL, MAKELONG(SB_THUMBTRACK,pos),0); - } - return 0; - } - case WM_CHAR: - c = (TCHAR)wParam; - write_inbuf(&c,1); - return 0; - case WM_CLOSE : - break; - case WM_DESTROY : - SaveUserPreferences(); - destroyed = TRUE; - PostQuitMessage(0); - return 0; - case WM_SAVE_PREFS : - SaveUserPreferences(); - return 0; - } - return DefWindowProc(hwnd, iMsg, wParam, lParam); -} - -static BOOL -Client_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) -{ - ConFontInitialize(hwnd); - cur_x = cur_y = 0; - iVscrollPos = 0; - iHscrollPos = 0; - return TRUE; -} - -static void -Client_OnPaint(HWND hwnd) -{ - ScreenLine_t *pLine; - int x,y,i,iTop,iBot; - PAINTSTRUCT ps; - RECT rcInvalid; - HDC hdc; - - hdc = BeginPaint(hwnd, &ps); - rcInvalid = ps.rcPaint; - hdc = ps.hdc; - iTop = max(0, iVscrollPos + rcInvalid.top/cyChar); - iBot = min(nBufLines, iVscrollPos + rcInvalid.bottom/cyChar+1); - pLine = GetLineFromY(iTop); - for (i = iTop; i < iBot && pLine != NULL; i++) { - y = cyChar*(i-iVscrollPos); - x = -cxChar*iHscrollPos; - TextOut(hdc, x, y, &pLine->text[0], pLine->width); - pLine = pLine->next; - } - if (fTextSelected || fSelecting) { - InvertSelectionArea(hwnd); - } - SetCaretPos(GetXFromCurrentY(hdc,iHscrollPos,cur_x), (cur_y-iVscrollPos)*cyChar); - EndPaint(hwnd, &ps); -} -#ifdef HARDDEBUG -static void dump_linebufs(void) { - char *buff; - ScreenLine_t *s = buffer_top; - fprintf(stderr,"LinebufDump------------------------\n"); - while(s) { - if (s == buffer_top) fprintf(stderr,"BT-> "); - if (s == buffer_bottom) fprintf(stderr,"BB-> "); - if (s == cur_line) fprintf(stderr,"CL-> "); - - buff = (char *) ALLOC(s->width+1); - memcpy(buff,s->text,s->width); - buff[s->width] = '\0'; - fprintf(stderr,"{\"%s\",%d,%d}\n",buff,s->newline,s->allocated); - FREE(buff); - s = s->next; - } - fprintf(stderr,"LinebufDumpEnd---------------------\n"); - fflush(stderr); -} -#endif - -static void reorganize_linebufs(HWND hwnd) { - ScreenLine_t *otop = buffer_top; - ScreenLine_t *obot = buffer_bottom; - ScreenLine_t *next; - int i,cpos; - - cpos = 0; - i = nBufLines - cur_y; - while (i > 1) { - cpos += obot->width; - obot = obot->prev; - i--; - } - cpos += (obot->width - cur_x); -#ifdef HARDDEBUG - fprintf(stderr,"nBufLines = %d, cur_x = %d, cur_y = %d, cpos = %d\n", - nBufLines,cur_x,cur_y,cpos); - fflush(stderr); -#endif - - - nBufLines = 0; - buffer_top = cur_line = ConNewLine(); - cur_line->next = buffer_bottom = ConNewLine(); - buffer_bottom->prev = cur_line; - - cur_x = cur_y = 0; - iVscrollPos = 0; - iHscrollPos = 0; - - while(otop) { - for(i=0;iwidth;++i) { - cur_line->text[cur_x] = otop->text[i]; - cur_x++; - if (cur_x > cur_line->width) - cur_line->width = cur_x; - if (GetXFromCurrentY(GetDC(hwnd),0,cur_x) + cxChar > - (LINE_LENGTH * cxChar)) { - ConCarriageFeed(0); - } - } - if (otop->newline) { - ConCarriageFeed(1); - /*ConScrollScreen();*/ - } - next = otop->next; - FREE(otop->text); - FREE(otop); - otop = next; - } - while (cpos) { - cur_x--; - if (cur_x < 0) { - cur_y--; - cur_line = cur_line->prev; - cur_x = cur_line->width-1; - } - cpos--; - } - SetCaretPos(GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x), (cur_y-iVscrollPos)*cyChar); -#ifdef HARDDEBUG - fprintf(stderr,"canvasColumns = %d,nBufLines = %d, cur_x = %d, cur_y = %d\n", - canvasColumns,nBufLines,cur_x,cur_y); - fflush(stderr); -#endif -} - - -static void -Client_OnSize(HWND hwnd, UINT state, int cx, int cy) -{ - RECT r; - SCROLLBARINFO sbi; - int w,h,columns; - int scrollheight; - cxClient = cx; - cyClient = cy; - set_scroll_info(hwnd); - GetClientRect(hwnd,&r); - w = r.right - r.left; - h = r.bottom - r.top; - sbi.cbSize = sizeof(SCROLLBARINFO); - if (!GetScrollBarInfo(hwnd, OBJID_HSCROLL,&sbi) || - (sbi.rgstate[0] & STATE_SYSTEM_INVISIBLE)) { - scrollheight = 0; - } else { - scrollheight = sbi.rcScrollBar.bottom - sbi.rcScrollBar.top; - } - canvasRows = (h - scrollheight) / cyChar; - if (canvasRows < DEF_CANVAS_ROWS) { - canvasRows = DEF_CANVAS_ROWS; - } - columns = (w - GetSystemMetrics(SM_CXVSCROLL)) /cxChar; - if (columns < DEF_CANVAS_COLUMNS) - columns = DEF_CANVAS_COLUMNS; - if (columns != canvasColumns) { - canvasColumns = columns; - /*dump_linebufs();*/ - reorganize_linebufs(hwnd); - fSelecting = fTextSelected = FALSE; - InvalidateRect(hwnd, NULL, TRUE); -#ifdef HARDDEBUG - fprintf(stderr,"Paint: cols = %d, rows = %d\n",canvasColumns,canvasRows); - fflush(stderr); -#endif - } - - SetCaretPos(GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x), (cur_y-iVscrollPos)*cyChar); -} - -static void calc_charpoint_from_point(HDC dc, int x, int y, int y_offset, POINT *pt) -{ - int r; - int hscrollPix = iHscrollPos * cxChar; - - pt->y = y/cyChar + iVscrollPos + y_offset; - - if (x > (LINE_LENGTH-iHscrollPos) * cxChar) { - x = (LINE_LENGTH-iHscrollPos) * cxChar; - } - if (pt->y - y_offset > 0 && GetLineFromY(pt->y - y_offset) == NULL) { - pt->y = nBufLines - 1 + y_offset; - pt->x = GetLineFromY(pt->y - y_offset)->width; - } else { - for (pt->x = 1; - (r = GetXFromLine(dc, 0, pt->x, GetLineFromY(pt->y - y_offset))) != 0 && - (r - hscrollPix) < x; - ++(pt->x)) - ; - if ((r - hscrollPix) > x) - --(pt->x); -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"pt->x = %d, iHscrollPos = %d\n",(int) pt->x, iHscrollPos); - fflush(stderr); -#endif - if (pt->x <= 0) { - pt->x = x/cxChar + iHscrollPos; - } - } -} - - -static void -Client_OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags) -{ - int r; - SetFocus(GetParent(hwnd)); /* In case combobox steals the focus */ -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"OnLButtonDown fSelecting = %d, fTextSelected = %d:\n", - fSelecting,fTextSelected); - fflush(stderr); -#endif - if (fTextSelected) { - InvertSelectionArea(hwnd); - } - fTextSelected = FALSE; - - calc_charpoint_from_point(GetDC(hwnd), x, y, 0, &editBeg); - - editEnd.x = editBeg.x; - editEnd.y = editBeg.y + 1; - fSelecting = TRUE; - SetCapture(hwnd); -} - -static void -Client_OnRButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags) -{ - if (fTextSelected) { - fSelecting = TRUE; - Client_OnMouseMove(hwnd,x,y,keyFlags); - fSelecting = FALSE; - } -} - -static void -Client_OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags) -{ -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"OnLButtonUp fSelecting = %d, fTextSelected = %d:\n", - fSelecting,fTextSelected); - fprintf(stderr,"(Beg.x = %d, Beg.y = %d, " - "End.x = %d, End.y = %d)\n",editBeg.x,editBeg.y, - editEnd.x,editEnd.y); -#endif - if (fSelecting && - !(editBeg.x == editEnd.x && editBeg.y == (editEnd.y - 1))) { - fTextSelected = TRUE; - } -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"OnLButtonUp fTextSelected = %d:\n", - fTextSelected); - fflush(stderr); -#endif - fSelecting = FALSE; - ReleaseCapture(); -} - -#define EMPTY_RECT(R) \ -(((R).bottom - (R).top == 0) || ((R).right - (R).left == 0)) -#define ABS(X) (((X)< 0) ? -1 * (X) : X) -#define DIFF(A,B) ABS(((int)(A)) - ((int)(B))) - -static int diff_sel_area(RECT old[3], RECT new[3], RECT result[6]) -{ - int absposold = old[0].left + old[0].top * canvasColumns; - int absposnew = new[0].left + new[0].top * canvasColumns; - int absendold = absposold, absendnew = absposnew; - int i, x, ret = 0; - int abspos[2],absend[2]; - for(i = 0; i < 3; ++i) { - if (!EMPTY_RECT(old[i])) { - absendold += (old[i].right - old[i].left) * - (old[i].bottom - old[i].top); - } - if (!EMPTY_RECT(new[i])) { - absendnew += (new[i].right - new[i].left) * - (new[i].bottom - new[i].top); - } - } - abspos[0] = min(absposold, absposnew); - absend[0] = DIFF(absposold, absposnew) + abspos[0]; - abspos[1] = min(absendold, absendnew); - absend[1] = DIFF(absendold, absendnew) + abspos[1]; -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"abspos[0] = %d, absend[0] = %d, abspos[1] = %d, absend[1] = %d\n",abspos[0],absend[0],abspos[1],absend[1]); - fflush(stderr); -#endif - i = 0; - for (x = 0; x < 2; ++x) { - if (abspos[x] != absend[x]) { - int consumed = 0; - result[i].left = abspos[x] % canvasColumns; - result[i].top = abspos[x] / canvasColumns; - result[i].bottom = result[i].top + 1; - if ((absend[x] - abspos[x]) + result[i].left < canvasColumns) { -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"Nowrap, %d < canvasColumns\n", - (absend[x] - abspos[x]) + result[i].left); - fflush(stderr); -#endif - result[i].right = (absend[x] - abspos[x]) + result[i].left; - consumed += result[i].right - result[i].left; - } else { -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"Wrap, %d >= canvasColumns\n", - (absend[x] - abspos[x]) + result[i].left); - fflush(stderr); -#endif - result[i].right = canvasColumns; - consumed += result[i].right - result[i].left; - if (absend[x] - abspos[x] - consumed >= canvasColumns) { - ++i; - result[i].top = result[i-1].bottom; - result[i].left = 0; - result[i].right = canvasColumns; - result[i].bottom = (absend[x] - abspos[x] - consumed) / canvasColumns + result[i].top; - consumed += (result[i].bottom - result[i].top) * canvasColumns; - } - if (absend[x] - abspos[x] - consumed > 0) { - ++i; - result[i].top = result[i-1].bottom; - result[i].bottom = result[i].top + 1; - result[i].left = 0; - result[i].right = absend[x] - abspos[x] - consumed; - } - } - ++i; - } - } -#ifdef HARD_SEL_DEBUG - if (i > 2) { - int x; - fprintf(stderr,"i = %d\n",i); - fflush(stderr); - for (x = 0; x < i; ++x) { - fprintf(stderr, "result[%d]: top = %d, left = %d, " - "bottom = %d. right = %d\n", - x, result[x].top, result[x].left, - result[x].bottom, result[x].right); - } - } -#endif - return i; -} - - - -static void calc_sel_area(RECT rects[3], POINT beg, POINT end) -{ - /* These are not really rects and points, these are character - based positions, need to be multiplied by cxChar and cyChar to - make up canvas coordinates */ - memset(rects,0,3*sizeof(RECT)); - rects[0].left = beg.x; - rects[0].top = beg.y; - rects[0].bottom = beg.y+1; - if (end.y - beg.y == 1) { /* Only one row */ - rects[0].right = end.x; - goto out; - } - rects[0].right = canvasColumns; - if (end.y - beg.y > 2) { - rects[1].left = 0; - rects[1].top = rects[0].bottom; - rects[1].right = canvasColumns; - rects[1].bottom = end.y - 1; - } - rects[2].left = 0; - rects[2].top = end.y - 1; - rects[2].bottom = end.y; - rects[2].right = end.x; - - out: -#ifdef HARD_SEL_DEBUG - { - int i; - fprintf(stderr,"beg.x = %d, beg.y = %d, end.x = %d, end.y = %d\n", - beg.x,beg.y,end.x,end.y); - for (i = 0; i < 3; ++i) { - fprintf(stderr,"[%d] left = %d, top = %d, " - "right = %d, bottom = %d\n", - i, rects[i].left, rects[i].top, - rects[i].right, rects[i].bottom); - } - fflush(stderr); - } -#endif - return; -} - -static void calc_sel_area_turned(RECT rects[3], POINT eBeg, POINT eEnd) { - POINT from,to; - if (eBeg.y >= eEnd.y || - (eBeg.y == eEnd.y - 1 && eBeg.x > eEnd.x)) { -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"Reverting (Beg.x = %d, Beg.y = %d, " - "End.x = %d, End.y = %d)\n",eBeg.x,eBeg.y, - eEnd.x,eEnd.y); - fflush(stderr); -#endif - from.x = eEnd.x; - from.y = eEnd.y - 1; - to.x = eBeg.x; - to.y = eBeg.y + 1; - calc_sel_area(rects,from,to); - } else { - calc_sel_area(rects,eBeg,eEnd); - } -} - - -static void InvertSelectionArea(HWND hwnd) -{ - RECT rects[3]; - POINT from,to; - int i; - calc_sel_area_turned(rects,editBeg,editEnd); - for (i = 0; i < 3; ++i) { - if (!EMPTY_RECT(rects[i])) { - from.x = rects[i].left; - to.x = rects[i].right; - from.y = rects[i].top; - to.y = rects[i].bottom; - DrawSelection(hwnd,from,to); - } - } -} - -static void -Client_OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags) -{ - if (fSelecting) { - RECT rold[3], rnew[3], rupdate[6]; - int num_updates,i,r; - POINT from,to; - calc_sel_area_turned(rold,editBeg,editEnd); - - calc_charpoint_from_point(GetDC(hwnd), x, y, 1, &editEnd); - - calc_sel_area_turned(rnew,editBeg,editEnd); - num_updates = diff_sel_area(rold,rnew,rupdate); - for (i = 0; i < num_updates;++i) { - from.x = rupdate[i].left; - to.x = rupdate[i].right; - from.y = rupdate[i].top; - to.y = rupdate[i].bottom; -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"from: x=%d,y=%d, to: x=%d, y=%d\n", - from.x, from.y,to.x,to.y); - fflush(stderr); -#endif - DrawSelection(hwnd,from,to); - } - } -} - -static void -Client_OnVScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos) -{ - int iVscroll; - - switch(code) { - case SB_LINEDOWN: - iVscroll = 1; - break; - case SB_LINEUP: - iVscroll = -1; - break; - case SB_PAGEDOWN: - iVscroll = max(1, cyClient/cyChar); - break; - case SB_PAGEUP: - iVscroll = min(-1, -cyClient/cyChar); - break; - case SB_THUMBTRACK: - iVscroll = pos - iVscrollPos; - break; - default: - iVscroll = 0; - } - iVscroll = max(-iVscrollPos, min(iVscroll, iVscrollMax-iVscrollPos)); - if (iVscroll != 0) { - iVscrollPos += iVscroll; - ScrollWindowEx(hwnd, 0, -cyChar*iVscroll, NULL, NULL, - NULL, NULL, SW_ERASE | SW_INVALIDATE); - SetScrollPos(hwnd, SB_VERT, iVscrollPos, TRUE); - iVscroll = GetScrollPos(hwnd, SB_VERT); - UpdateWindow(hwnd); - } -} - -static void -Client_OnHScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos) -{ - int iHscroll, curCharWidth = cxClient/cxChar; - - switch(code) { - case SB_LINEDOWN: - iHscroll = 1; - break; - case SB_LINEUP: - iHscroll = -1; - break; - case SB_PAGEDOWN: - iHscroll = max(1,curCharWidth-1); - break; - case SB_PAGEUP: - iHscroll = min(-1,-(curCharWidth-1)); - break; - case SB_THUMBTRACK: - iHscroll = pos - iHscrollPos; - break; - default: - iHscroll = 0; - } - iHscroll = max(-iHscrollPos, min(iHscroll, iHscrollMax-iHscrollPos-(curCharWidth-1))); - if (iHscroll != 0) { - iHscrollPos += iHscroll; - ScrollWindow(hwnd, -cxChar*iHscroll, 0, NULL, NULL); - SetScrollPos(hwnd, SB_HORZ, iHscrollPos, TRUE); - UpdateWindow(hwnd); - } -} - -static LRESULT CALLBACK -ClientWndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) -{ - switch (iMsg) { - HANDLE_MSG(hwnd, WM_CREATE, Client_OnCreate); - HANDLE_MSG(hwnd, WM_SIZE, Client_OnSize); - HANDLE_MSG(hwnd, WM_PAINT, Client_OnPaint); - HANDLE_MSG(hwnd, WM_LBUTTONDOWN, Client_OnLButtonDown); - HANDLE_MSG(hwnd, WM_RBUTTONDOWN, Client_OnRButtonDown); - HANDLE_MSG(hwnd, WM_LBUTTONUP, Client_OnLButtonUp); - HANDLE_MSG(hwnd, WM_MOUSEMOVE, Client_OnMouseMove); - HANDLE_MSG(hwnd, WM_VSCROLL, Client_OnVScroll); - HANDLE_MSG(hwnd, WM_HSCROLL, Client_OnHScroll); - case WM_CONBEEP: - if (0) Beep(440, 400); - return 0; - case WM_CONTEXT: - ConDrawText(hwnd); - return 0; - case WM_CLOSE: - break; - case WM_DESTROY: - PostQuitMessage(0); - return 0; - } - return DefWindowProc (hwnd, iMsg, wParam, lParam); -} - -static void -LoadUserPreferences(void) -{ - DWORD size; - DWORD res; - DWORD type; - HFONT hfont; - /* default prefs */ - hfont = CreateFont(0,0, 0,0, 0, FALSE,FALSE,FALSE, - ANSI_CHARSET, OUT_TT_ONLY_PRECIS, CLIP_DEFAULT_PRECIS, - CLEARTYPE_QUALITY, FIXED_PITCH, TEXT("Consolas")); - if(hfont) { - GetObject(hfont, sizeof(LOGFONT), (PSTR)&logfont); - DeleteObject(hfont); - } else { - GetObject(GetStockObject(SYSTEM_FIXED_FONT),sizeof(LOGFONT),(PSTR)&logfont); - } - fgColor = GetSysColor(COLOR_WINDOWTEXT); - bkgColor = GetSysColor(COLOR_WINDOW); - winPos.left = -1; - toolbarVisible = FALSE; - - if (RegCreateKeyEx(HKEY_CURRENT_USER, USER_KEY, 0, 0, - REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, - &key, &res) != ERROR_SUCCESS) - return; - has_key = TRUE; - if (res == REG_CREATED_NEW_KEY) - return; - size = sizeof(logfont); - res = RegQueryValueEx(key,TEXT("Font"),NULL,&type,(LPBYTE)&logfont,&size); - size = sizeof(fgColor); - res = RegQueryValueEx(key,TEXT("FgColor"),NULL,&type,(LPBYTE)&fgColor,&size); - size = sizeof(bkgColor); - res = RegQueryValueEx(key,TEXT("BkColor"),NULL,&type,(LPBYTE)&bkgColor,&size); - size = sizeof(winPos); - res = RegQueryValueEx(key,TEXT("Pos"),NULL,&type,(LPBYTE)&winPos,&size); - size = sizeof(toolbarVisible); - res = RegQueryValueEx(key,TEXT("Toolbar"),NULL,&type,(LPBYTE)&toolbarVisible,&size); -} - -static void -SaveUserPreferences(void) -{ - WINDOWPLACEMENT wndPlace; - - if (has_key == TRUE) { - RegSetValueEx(key,TEXT("Font"),0,REG_BINARY,(CONST BYTE *)&logfont,sizeof(LOGFONT)); - RegSetValueEx(key,TEXT("FgColor"),0,REG_DWORD,(CONST BYTE *)&fgColor,sizeof(fgColor)); - RegSetValueEx(key,TEXT("BkColor"),0,REG_DWORD,(CONST BYTE *)&bkgColor,sizeof(bkgColor)); - RegSetValueEx(key,TEXT("Toolbar"),0,REG_DWORD,(CONST BYTE *)&toolbarVisible,sizeof(toolbarVisible)); - - wndPlace.length = sizeof(WINDOWPLACEMENT); - GetWindowPlacement(hFrameWnd,&wndPlace); - /* If wndPlace.showCmd == SW_MINIMIZE, then the window is minimized. - We don't care, wndPlace.rcNormalPosition always holds the last known position. */ - winPos = wndPlace.rcNormalPosition; - RegSetValueEx(key,TEXT("Pos"),0,REG_BINARY,(CONST BYTE *)&winPos,sizeof(winPos)); - } -} - - -static void -set_scroll_info(HWND hwnd) -{ - SCROLLINFO info; - int hScrollBy; - /* - * Set vertical scrolling range and scroll box position. - */ - - iVscrollMax = nBufLines-1; - iVscrollPos = min(iVscrollPos, iVscrollMax); - info.cbSize = sizeof(info); - info.fMask = SIF_PAGE|SIF_RANGE|SIF_POS; - info.nMin = 0; - info.nPos = iVscrollPos; - info.nPage = min(cyClient/cyChar, iVscrollMax); - info.nMax = iVscrollMax; - SetScrollInfo(hwnd, SB_VERT, &info, TRUE); - - /* - * Set horizontal scrolling range and scroll box position. - */ - - iHscrollMax = LINE_LENGTH-1; - hScrollBy = max(0, (iHscrollPos - (iHscrollMax-cxClient/cxChar))*cxChar); - iHscrollPos = min(iHscrollPos, iHscrollMax); - info.nPos = iHscrollPos; - info.nPage = cxClient/cxChar; - info.nMax = iHscrollMax; - SetScrollInfo(hwnd, SB_HORZ, &info, TRUE); - /*ScrollWindow(hwnd, hScrollBy, 0, NULL, NULL);*/ -} - - -static void -ensure_line_below(void) -{ - if (cur_line->next == NULL) { - if (nBufLines >= lines_to_save) { - ScreenLine_t* pLine = buffer_top->next; - FREE(buffer_top->text); - FREE(buffer_top); - buffer_top = pLine; - buffer_top->prev = NULL; - nBufLines--; - } - cur_line->next = ConNewLine(); - cur_line->next->prev = cur_line; - buffer_bottom = cur_line->next; - set_scroll_info(hClientWnd); - } -} - -static ScreenLine_t* -ConNewLine(void) -{ - ScreenLine_t *pLine; - - pLine = (ScreenLine_t *)ALLOC(sizeof(ScreenLine_t)); - if (!pLine) - return NULL; - pLine->text = (TCHAR *) ALLOC(canvasColumns * sizeof(TCHAR)); -#ifdef HARDDEBUG - pLine->allocated = canvasColumns; -#endif - pLine->width = 0; - pLine->prev = pLine->next = NULL; - pLine->newline = 0; - nBufLines++; - return pLine; -} - -static ScreenLine_t* -GetLineFromY(int y) -{ - ScreenLine_t *pLine = buffer_top; - int i; - - for (i = 0; i < nBufLines && pLine != NULL; i++) { - if (i == y) - return pLine; - pLine = pLine->next; - } - return NULL; -} - -void ConCarriageFeed(int hard_newline) -{ - cur_x = 0; - ensure_line_below(); - cur_line->newline = hard_newline; - cur_line = cur_line->next; - if (cur_y < nBufLines-1) { - cur_y++; - } else if (iVscrollPos > 0) { - iVscrollPos--; - } -} - -/* - * Scroll screen if cursor is not visible. - */ -static void -ConScrollScreen(void) -{ - if (cur_y >= iVscrollPos + cyClient/cyChar) { - int iVscroll; - - iVscroll = cur_y - iVscrollPos - cyClient/cyChar + 1; - iVscrollPos += iVscroll; - ScrollWindowEx(hClientWnd, 0, -cyChar*iVscroll, NULL, NULL, - NULL, NULL, SW_ERASE | SW_INVALIDATE); - SetScrollPos(hClientWnd, SB_VERT, iVscrollPos, TRUE); - UpdateWindow(hClientWnd); - } -} - -static void -DrawSelection(HWND hwnd, POINT pt1, POINT pt2) -{ - HDC hdc; - int width,height; -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"pt1.x = %d, pt1.y = %d, pt2.x = %d, pt2.y = %d\n", - (int) pt1.x, (int) pt1.y, (int) pt2.x, (int) pt2.y); -#endif - pt1.x = GetXFromLine(GetDC(hwnd),iHscrollPos,pt1.x,GetLineFromY(pt1.y)); - pt2.x = GetXFromLine(GetDC(hwnd),iHscrollPos,pt2.x,GetLineFromY(pt2.y-1)); - pt1.y -= iVscrollPos; - pt2.y -= iVscrollPos; - pt1.y *= cyChar; - pt2.y *= cyChar; -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"pt1.x = %d, pt1.y = %d, pt2.x = %d, pt2.y = %d\n", - (int) pt1.x, (int) pt1.y, (int) pt2.x, (int) pt2.y); - fflush(stderr); -#endif - width = pt2.x-pt1.x; - height = pt2.y - pt1.y; - hdc = GetDC(hwnd); - PatBlt(hdc,pt1.x,pt1.y,width,height,DSTINVERT); - ReleaseDC(hwnd,hdc); -} - -static void -OnEditCopy(HWND hwnd) -{ - HGLOBAL hMem; - TCHAR *pMem; - ScreenLine_t *pLine; - RECT rects[3]; - POINT from,to; - int i,j,sum,len; - if (editBeg.y >= editEnd.y || - (editBeg.y == editEnd.y - 1 && editBeg.x > editEnd.x)) { -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"CopyReverting (Beg.x = %d, Beg.y = %d, " - "End.x = %d, End.y = %d)\n",editBeg.x,editBeg.y, - editEnd.x,editEnd.y); - fflush(stderr); -#endif - from.x = editEnd.x; - from.y = editEnd.y - 1; - to.x = editBeg.x; - to.y = editBeg.y + 1; - calc_sel_area(rects,from,to); - } else { - calc_sel_area(rects,editBeg,editEnd); - } - sum = 1; - for (i = 0; i < 3; ++i) { - if (!EMPTY_RECT(rects[i])) { - pLine = GetLineFromY(rects[i].top); - for (j = rects[i].top; j < rects[i].bottom ;++j) { - if (pLine == NULL) { - sum += 2; - break; - } - if (pLine->width > rects[i].left) { - sum += (pLine->width < rects[i].right) ? - pLine->width - rects[i].left : - rects[i].right - rects[i].left; - } - if(pLine->newline && rects[i].right >= pLine->width) { - sum += 2; - } - pLine = pLine->next; - } - } - } -#ifdef HARD_SEL_DEBUG - fprintf(stderr,"sum = %d\n",sum); - fflush(stderr); -#endif - hMem = GlobalAlloc(GHND, sum * sizeof(TCHAR)); - pMem = GlobalLock(hMem); - for (i = 0; i < 3; ++i) { - if (!EMPTY_RECT(rects[i])) { - pLine = GetLineFromY(rects[i].top); - for (j = rects[i].top; j < rects[i].bottom; ++j) { - if (pLine == NULL) { - memcpy(pMem,TEXT("\r\n"),2 * sizeof(TCHAR)); - pMem += 2; - break; - } - if (pLine->width > rects[i].left) { - len = (pLine->width < rects[i].right) ? - pLine->width - rects[i].left : - rects[i].right - rects[i].left; - memcpy(pMem,pLine->text + rects[i].left,len * sizeof(TCHAR)); - pMem +=len; - } - if(pLine->newline && rects[i].right >= pLine->width) { - memcpy(pMem,TEXT("\r\n"),2 * sizeof(TCHAR)); - pMem += 2; - } - pLine = pLine->next; - } - } - } - *pMem = TEXT('\0'); - /* Flash de selection area to give user feedback about copying */ - InvertSelectionArea(hwnd); - Sleep(100); - InvertSelectionArea(hwnd); - - OpenClipboard(hwnd); - EmptyClipboard(); - GlobalUnlock(hMem); - SetClipboardData(CF_UNICODETEXT,hMem); - CloseClipboard(); -} - -/* XXX:PaN Tchar or char? */ -static void -OnEditPaste(HWND hwnd) -{ - HANDLE hClipMem; - TCHAR *pClipMem,*pMem,*pMem2; - if (!OpenClipboard(hwnd)) - return; - if ((hClipMem = GetClipboardData(CF_UNICODETEXT)) != NULL) { - pClipMem = GlobalLock(hClipMem); - pMem = (TCHAR *)ALLOC(GlobalSize(hClipMem) * sizeof(TCHAR)); - pMem2 = pMem; - while ((*pMem2 = *pClipMem) != TEXT('\0')) { - if (*pClipMem == TEXT('\r')) - *pMem2 = TEXT('\n'); - ++pMem2; - ++pClipMem; - } - GlobalUnlock(hClipMem); - write_inbuf(pMem, _tcsclen(pMem)); - } - CloseClipboard(); -} - -static void -OnEditSelAll(HWND hwnd) -{ - editBeg.x = 0; - editBeg.y = 0; - editEnd.x = LINE_LENGTH-1; - editEnd.y = cur_y; - fTextSelected = TRUE; - InvalidateRect(hwnd, NULL, TRUE); -} - -CF_HOOK_RET APIENTRY CFHookProc(HWND hDlg,UINT iMsg,WPARAM wParam,LPARAM lParam) -{ - /* Hook procedure for font dialog box */ - HWND hOwner; - RECT rc,rcOwner,rcDlg; - switch (iMsg) { - case WM_INITDIALOG: - /* center dialogbox within its owner window */ - if ((hOwner = GetParent(hDlg)) == NULL) - hOwner = GetDesktopWindow(); - GetWindowRect(hOwner, &rcOwner); - GetWindowRect(hDlg, &rcDlg); - CopyRect(&rc, &rcOwner); - OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); - OffsetRect(&rc, -rc.left, -rc.top); - OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); - SetWindowPos(hDlg,HWND_TOP,rcOwner.left + (rc.right / 2), - rcOwner.top + (rc.bottom / 2),0,0,SWP_NOSIZE); - return (CF_HOOK_RET) 1; - default: - break; - } - return (CF_HOOK_RET) 0; /* Let the default procedure process the message */ -} - -static BOOL -ConChooseFont(HWND hwnd) -{ - HDC hdc; - hdc = GetDC(hwnd); - cf.lStructSize = sizeof(CHOOSEFONT); - cf.hwndOwner = hwnd; - cf.hDC = NULL; - cf.lpLogFont = &logfont; - cf.iPointSize = 0; - cf.Flags = CF_INITTOLOGFONTSTRUCT|CF_SCREENFONTS|CF_FIXEDPITCHONLY|CF_EFFECTS|CF_ENABLEHOOK; - cf.rgbColors = GetTextColor(hdc); - cf.lCustData = 0L; - cf.lpfnHook = CFHookProc; - cf.lpTemplateName = NULL; - cf.hInstance = NULL; - cf.lpszStyle = NULL; - cf.nFontType = 0; - cf.nSizeMin = 0; - cf.nSizeMax = 0; - ReleaseDC(hwnd,hdc); - return ChooseFont(&cf); -} - -static void -ConFontInitialize(HWND hwnd) -{ - HDC hdc; - TEXTMETRIC tm; - HFONT hFont; - - hFont = CreateFontIndirect(&logfont); - hdc = GetDC(hwnd); - SelectObject(hdc, hFont); - SetTextColor(hdc,fgColor); - SetBkColor(hdc,bkgColor); - GetTextMetrics(hdc, &tm); - cxChar = tm.tmAveCharWidth; - cxCharMax = tm.tmMaxCharWidth; - cyChar = tm.tmHeight + tm.tmExternalLeading; - ReleaseDC(hwnd, hdc); -} - -static void -ConSetFont(HWND hwnd) -{ - HDC hdc; - TEXTMETRIC tm; - HFONT hFontNew; - - hFontNew = CreateFontIndirect(&logfont); - SendMessage(hComboWnd,WM_SETFONT,(WPARAM)hFontNew, - MAKELPARAM(1,0)); - hdc = GetDC(hwnd); - DeleteObject(SelectObject(hdc, hFontNew)); - GetTextMetrics(hdc, &tm); - cxChar = tm.tmAveCharWidth; - cxCharMax = tm.tmMaxCharWidth; - cyChar = tm.tmHeight + tm.tmExternalLeading; - fgColor = cf.rgbColors; - SetTextColor(hdc,fgColor); - ReleaseDC(hwnd, hdc); - set_scroll_info(hwnd); - HideCaret(hwnd); - if (DestroyCaret()) { - CreateCaret(hwnd, NULL, cxChar, cyChar); - SetCaretPos(GetXFromCurrentY(hdc,iHscrollPos,cur_x), (cur_y-iVscrollPos)*cyChar); - } - ShowCaret(hwnd); - InvalidateRect(hwnd, NULL, TRUE); -} - -CC_HOOK_RET APIENTRY -CCHookProc(HWND hDlg,UINT iMsg,WPARAM wParam,LPARAM lParam) -{ - /* Hook procedure for choose color dialog box */ - HWND hOwner; - RECT rc,rcOwner,rcDlg; - switch (iMsg) { - case WM_INITDIALOG: - /* center dialogbox within its owner window */ - if ((hOwner = GetParent(hDlg)) == NULL) - hOwner = GetDesktopWindow(); - GetWindowRect(hOwner, &rcOwner); - GetWindowRect(hDlg, &rcDlg); - CopyRect(&rc, &rcOwner); - OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); - OffsetRect(&rc, -rc.left, -rc.top); - OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); - SetWindowPos(hDlg,HWND_TOP,rcOwner.left + (rc.right / 2), - rcOwner.top + (rc.bottom / 2),0,0,SWP_NOSIZE); - return (CC_HOOK_RET) 1; - default: - break; - } - return (CC_HOOK_RET) 0; /* Let the default procedure process the message */ -} - -void ConChooseColor(HWND hwnd) -{ - CHOOSECOLOR cc; - static COLORREF acrCustClr[16]; - HBRUSH hbrush; - HDC hdc; - - /* Initialize CHOOSECOLOR */ - ZeroMemory(&cc, sizeof(CHOOSECOLOR)); - cc.lStructSize = sizeof(CHOOSECOLOR); - cc.hwndOwner = hwnd; - cc.lpCustColors = (LPDWORD) acrCustClr; - cc.rgbResult = bkgColor; - cc.lpfnHook = CCHookProc; - cc.Flags = CC_FULLOPEN|CC_RGBINIT|CC_SOLIDCOLOR|CC_ENABLEHOOK; - - if (ChooseColor(&cc)==TRUE) { - bkgColor = cc.rgbResult; - hdc = GetDC(hwnd); - SetBkColor(hdc,bkgColor); - ReleaseDC(hwnd,hdc); - hbrush = CreateSolidBrush(bkgColor); - DeleteObject((HBRUSH)SetClassLongPtr(hClientWnd,GCL_HBRBACKGROUND,(LONG_PTR)hbrush)); - InvalidateRect(hwnd,NULL,TRUE); - } -} - -OFN_HOOK_RET APIENTRY OFNHookProc(HWND hwndDlg,UINT iMsg, - WPARAM wParam,LPARAM lParam) -{ - /* Hook procedure for open file dialog box */ - HWND hOwner,hDlg; - RECT rc,rcOwner,rcDlg; - hDlg = GetParent(hwndDlg); - switch (iMsg) { - case WM_INITDIALOG: - /* center dialogbox within its owner window */ - if ((hOwner = GetParent(hDlg)) == NULL) - hOwner = GetDesktopWindow(); - GetWindowRect(hOwner, &rcOwner); - GetWindowRect(hDlg, &rcDlg); - CopyRect(&rc, &rcOwner); - OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); - OffsetRect(&rc, -rc.left, -rc.top); - OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); - SetWindowPos(hDlg,HWND_TOP,rcOwner.left + (rc.right / 2), - rcOwner.top + (rc.bottom / 2),0,0,SWP_NOSIZE); - return (OFN_HOOK_RET) 1; - default: - break; - } - return (OFN_HOOK_RET) 0; /* the let default procedure process the message */ -} - -static void -GetFileName(HWND hwnd, TCHAR *pFile) -{ - /* Open the File Open dialog box and */ - /* retrieve the file name */ - OPENFILENAME ofn; - TCHAR szFilterSpec [128] = TEXT("logfiles (*.log)\0*.log\0All files (*.*)\0*.*\0\0"); - #define MAXFILENAME 256 - TCHAR szFileName[MAXFILENAME]; - TCHAR szFileTitle[MAXFILENAME]; - - /* these need to be filled in */ - _tcscpy(szFileName, TEXT("erlshell.log")); - _tcscpy(szFileTitle, TEXT("")); /* must be NULL */ - - ofn.lStructSize = sizeof(OPENFILENAME); - ofn.hwndOwner = NULL; - ofn.lpstrFilter = szFilterSpec; - ofn.lpstrCustomFilter = NULL; - ofn.nMaxCustFilter = 0; - ofn.nFilterIndex = 0; - ofn.lpstrFile = szFileName; - ofn.nMaxFile = MAXFILENAME; - ofn.lpstrInitialDir = NULL; - ofn.lpstrFileTitle = szFileTitle; - ofn.nMaxFileTitle = MAXFILENAME; - ofn.lpstrTitle = TEXT("Open logfile"); - ofn.lpstrDefExt = TEXT("log"); - ofn.Flags = OFN_CREATEPROMPT|OFN_HIDEREADONLY|OFN_EXPLORER|OFN_ENABLEHOOK|OFN_NOCHANGEDIR; /* OFN_NOCHANGEDIR only works in Vista :( */ - ofn.lpfnHook = OFNHookProc; - - if (!GetOpenFileName ((LPOPENFILENAME)&ofn)){ - *pFile = TEXT('\0'); - } else { - _tcscpy(pFile, ofn.lpstrFile); - } -} - -void OpenLogFile(HWND hwnd) -{ - /* open a file for logging */ - TCHAR filename[_MAX_PATH]; - - GetFileName(hwnd, filename); - if (filename[0] == '\0') - return; - if (NULL == (logfile = _tfopen(filename,TEXT("w,ccs=UNICODE")))) - return; -} - -void CloseLogFile(HWND hwnd) -{ - /* close log file */ - fclose(logfile); - logfile = NULL; -} - -void LogFileWrite(TCHAR *buf, int num_chars) -{ - /* write to logfile */ - int from,to; - while (num_chars-- > 0) { - switch (*buf) { - case SET_CURSOR: - buf++; - from = *((int *)buf); - buf += sizeof(int)/sizeof(TCHAR); - to = *((int *)buf); - buf += (sizeof(int)/sizeof(TCHAR))-1; - num_chars -= 2 * (sizeof(int)/sizeof(TCHAR)); - // Won't seek in Unicode file, sorry... - // fseek(logfile,to-from *sizeof(TCHAR),SEEK_CUR); - break; - default: - _fputtc(*buf,logfile); - break; - } - buf++; - } -} - -static void -init_buffers(void) -{ - inbuf.data = (TCHAR *) ALLOC(BUFSIZE * sizeof(TCHAR)); - outbuf.data = (TCHAR *) ALLOC(BUFSIZE * sizeof(TCHAR)); - inbuf.size = BUFSIZE; - inbuf.rdPos = inbuf.wrPos = 0; - outbuf.size = BUFSIZE; - outbuf.rdPos = outbuf.wrPos = 0; -} - -static int -check_realloc(buffer_t *buf, int num_chars) -{ - if (buf->wrPos + num_chars >= buf->size) { - if (buf->size > MAXBUFSIZE) - return 0; - buf->size += num_chars + BUFSIZE; - if (!(buf->data = (TCHAR *)REALLOC(buf->data, buf->size * sizeof(TCHAR)))) { - buf->size = buf->rdPos = buf->wrPos = 0; - return 0; - } - } - return 1; -} - -static int -write_inbuf(TCHAR *data, int num_chars) -{ - TCHAR *buf; - int nwrite; - WaitForSingleObject(console_input,INFINITE); - if (!check_realloc(&inbuf,num_chars)) { - ReleaseSemaphore(console_input,1,NULL); - return -1; - } - buf = &inbuf.data[inbuf.wrPos]; - inbuf.wrPos += num_chars; - nwrite = num_chars; - while (nwrite--) - *buf++ = *data++; - SetEvent(console_input_event); - ReleaseSemaphore(console_input,1,NULL); - return num_chars; -} - -static int -write_outbuf(TCHAR *data, int num_chars) -{ - TCHAR *buf; - int nwrite; - - WaitForSingleObject(console_output,INFINITE); - if (!check_realloc(&outbuf, num_chars)) { - ReleaseSemaphore(console_output,1,NULL); - return -1; - } - if (outbuf.rdPos == outbuf.wrPos) - PostMessage(hClientWnd, WM_CONTEXT, 0L, 0L); - buf = &outbuf.data[outbuf.wrPos]; - outbuf.wrPos += num_chars; - nwrite = num_chars; - while (nwrite--) - *buf++ = *data++; - ReleaseSemaphore(console_output,1,NULL); - return num_chars; -} - -DIALOG_PROC_RET CALLBACK AboutDlgProc(HWND hDlg, UINT iMsg, WPARAM wParam, LPARAM lParam) -{ - HWND hOwner; - RECT rc,rcOwner,rcDlg; - - switch (iMsg) { - case WM_INITDIALOG: - /* center dialogbox within its owner window */ - if ((hOwner = GetParent(hDlg)) == NULL) - hOwner = GetDesktopWindow(); - GetWindowRect(hOwner, &rcOwner); - GetWindowRect(hDlg, &rcDlg); - CopyRect(&rc, &rcOwner); - OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); - OffsetRect(&rc, -rc.left, -rc.top); - OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); - SetWindowPos(hDlg,HWND_TOP,rcOwner.left + (rc.right / 2), - rcOwner.top + (rc.bottom / 2),0,0,SWP_NOSIZE); - SetDlgItemText(hDlg, ID_OTP_VERSIONSTRING, - TEXT("OTP version ") TEXT(ERLANG_OTP_VERSION)); - SetDlgItemText(hDlg, ID_ERTS_VERSIONSTRING, - TEXT("Erlang emulator version ") TEXT(ERLANG_VERSION)); - return (DIALOG_PROC_RET) TRUE; - case WM_COMMAND: - switch (LOWORD(wParam)) { - case IDOK: - case IDCANCEL: - EndDialog(hDlg,0); - return (DIALOG_PROC_RET) TRUE; - } - break; - } - return (DIALOG_PROC_RET) FALSE; -} - -static void -ConDrawText(HWND hwnd) -{ - int num_chars; - int nchars; - TCHAR *buf; - int from, to; - int dl; - int dc; - RECT rc; - - WaitForSingleObject(console_output, INFINITE); - nchars = 0; - num_chars = outbuf.wrPos - outbuf.rdPos; - buf = &outbuf.data[outbuf.rdPos]; - if (logfile != NULL) - LogFileWrite(buf, num_chars); - - -#ifdef HARDDEBUG - { - TCHAR *bu = (TCHAR *) ALLOC((num_chars+1) * sizeof(TCHAR)); - memcpy(bu,buf,num_chars * sizeof(TCHAR)); - bu[num_chars]='\0'; - fprintf(stderr,"ConDrawText\"%S\"\n",bu); - FREE(bu); - fflush(stderr); - } -#endif - /* - * Don't draw any text in the window; just update the line buffers - * and invalidate the appropriate part of the window. The window - * will be updated on the next WM_PAINT message. - */ - - while (num_chars-- > 0) { - switch (*buf) { - case '\r': - break; - case '\n': - if (nchars > 0) { - rc.left = GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x - nchars); - rc.right = rc.left + cxCharMax*nchars; - rc.top = cyChar * (cur_y-iVscrollPos); - rc.bottom = rc.top + cyChar; - InvalidateRect(hwnd, &rc, TRUE); - nchars = 0; - } - ConCarriageFeed(1); - ConScrollScreen(); - break; - case SET_CURSOR: - if (nchars > 0) { - rc.left = GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x - nchars); - rc.right = rc.left + cxCharMax*nchars; - rc.top = cyChar * (cur_y-iVscrollPos); - rc.bottom = rc.top + cyChar; - InvalidateRect(hwnd, &rc, TRUE); - nchars = 0; - } - buf++; - from = *((int *)buf); - buf += sizeof(int)/sizeof(TCHAR); - to = *((int *)buf); - buf += (sizeof(int)/sizeof(TCHAR))-1; - num_chars -= 2 * (sizeof(int)/sizeof(TCHAR)); - while (to > from) { - cur_x++; - if (GetXFromCurrentY(GetDC(hwnd),0,cur_x)+cxChar > - (LINE_LENGTH * cxChar)) { - cur_x = 0; - cur_y++; - ensure_line_below(); - cur_line = cur_line->next; - } - from++; - } - while (to < from) { - cur_x--; - if (cur_x < 0) { - cur_y--; - cur_line = cur_line->prev; - cur_x = cur_line->width-1; - } - from--; - } - - break; - default: - nchars++; - cur_line->text[cur_x] = *buf; - cur_x++; - if (cur_x > cur_line->width) - cur_line->width = cur_x; - if (GetXFromCurrentY(GetDC(hwnd),0,cur_x)+cxChar > - (LINE_LENGTH * cxChar)) { - if (nchars > 0) { - rc.left = GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x - nchars); - rc.right = rc.left + cxCharMax*nchars; - rc.top = cyChar * (cur_y-iVscrollPos); - rc.bottom = rc.top + cyChar; - InvalidateRect(hwnd, &rc, TRUE); - } - ConCarriageFeed(0); - nchars = 0; - } - } - buf++; - } - if (nchars > 0) { - rc.left = GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x - nchars); - rc.right = rc.left + cxCharMax*nchars; - rc.top = cyChar * (cur_y-iVscrollPos); - rc.bottom = rc.top + cyChar; - InvalidateRect(hwnd, &rc, TRUE); - } - ConScrollScreen(); - SetCaretPos(GetXFromCurrentY(GetDC(hwnd),iHscrollPos,cur_x), (cur_y-iVscrollPos)*cyChar); - outbuf.wrPos = outbuf.rdPos = 0; - ReleaseSemaphore(console_output, 1, NULL); -} - -static void -AddToCmdHistory(void) -{ - int i; - int size; - Uint32 *buf; - wchar_t cmdBuf[128]; - - if (llen != 0) { - for (i = 0, size = 0; i < llen-1; i++) { - /* - * Find end of prompt. - */ - if ((lbuf[i] == '>') && lbuf[i+1] == ' ') { - buf = &lbuf[i+2]; - size = llen-i-2; - break; - } - } - if (size > 0 && size < 128) { - for (i = 0;i < size; ++i) { - cmdBuf[i] = (wchar_t) buf[i]; - } - cmdBuf[size] = 0; - SendMessage(hComboWnd,CB_INSERTSTRING,0,(LPARAM)cmdBuf); - } - } -} - -/*static TBBUTTON tbb[] = -{ - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - 0, IDMENU_COPY, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE, 0, 0, 0, 0, - 1, IDMENU_PASTE, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE, 0, 0, 0, 0, - 2, IDMENU_FONT, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE, 0, 0, 0, 0, - 3, IDMENU_ABOUT, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE, 0, 0, 0, 0, - 0, 0, TBSTATE_ENABLED, TBSTYLE_SEP, 0, 0, 0, 0, - };*/ -static TBBUTTON tbb[] = -{ - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP}, - {0, IDMENU_COPY, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE}, - {1, IDMENU_PASTE, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE}, - {2, IDMENU_FONT, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE}, - {3, IDMENU_ABOUT, TBSTATE_ENABLED, TBSTYLE_AUTOSIZE}, - {0, 0, TBSTATE_ENABLED, TBSTYLE_SEP} -}; - -static TBADDBITMAP tbbitmap = -{ - HINST_COMMCTRL, IDB_STD_SMALL_COLOR, -}; - - -static HWND -InitToolBar(HWND hwndParent) -{ - int x,y,cx; - HWND hwndTB,hwndTT; - RECT r; - TOOLINFO ti; - HFONT hFontNew; - DWORD backgroundColor = GetSysColor(COLOR_BTNFACE); - COLORMAP colorMap; - colorMap.from = RGB(192, 192, 192); - colorMap.to = backgroundColor; - /* Create toolbar window with tooltips */ - hwndTB = CreateWindowEx(0,TOOLBARCLASSNAME,(TCHAR *)NULL, - WS_CHILD|CCS_TOP|WS_CLIPSIBLINGS|TBSTYLE_TOOLTIPS, - 0,0,0,0,hwndParent, - (HMENU)2,hInstance,NULL); - SendMessage(hwndTB,TB_BUTTONSTRUCTSIZE, - (WPARAM) sizeof(TBBUTTON),0); - tbbitmap.hInst = NULL; - tbbitmap.nID = (UINT_PTR) CreateMappedBitmap(beam_module, 1,0, &colorMap, 1); - SendMessage(hwndTB, TB_ADDBITMAP, (WPARAM) 4, - (LPARAM) &tbbitmap); - - SendMessage(hwndTB,TB_ADDBUTTONS, (WPARAM) 30, - (LPARAM) tbb); - if (toolbarVisible) - ShowWindow(hwndTB, SW_SHOW); - - /* Create combobox window */ - SendMessage(hwndTB,TB_GETITEMRECT,0,(LPARAM)&r); - x = r.left; y = r.top; - SendMessage(hwndTB,TB_GETITEMRECT,23,(LPARAM)&r); - cx = r.right - x + 1; - hComboWnd = CreateWindow(TEXT("combobox"),NULL,WS_VSCROLL|WS_CHILD|WS_VISIBLE|CBS_DROPDOWNLIST, - x,y,cx,100,hwndParent,(HMENU)ID_COMBOBOX, hInstance,NULL); - SetParent(hComboWnd,hwndTB); - hFontNew = CreateFontIndirect(&logfont); - SendMessage(hComboWnd,WM_SETFONT,(WPARAM)hFontNew, - MAKELPARAM(1,0)); - - /* Add tooltip for combo box */ - ZeroMemory(&ti,sizeof(TOOLINFO)); - ti.cbSize = sizeof(TOOLINFO); - ti.uFlags = TTF_IDISHWND|TTF_CENTERTIP|TTF_SUBCLASS; - ti.hwnd = hwndTB;; - ti.uId = (UINT_PTR)hComboWnd; - ti.lpszText = LPSTR_TEXTCALLBACK; - hwndTT = (HWND)SendMessage(hwndTB,TB_GETTOOLTIPS,0,0); - SendMessage(hwndTT,TTM_ADDTOOL,0,(LPARAM)&ti); - - return hwndTB; -} - -static void -window_title(struct title_buf *tbuf) -{ - int res, i; - size_t bufsz = TITLE_BUF_SZ; - unsigned char charbuff[TITLE_BUF_SZ]; - - res = erl_drv_getenv("ERL_WINDOW_TITLE", charbuff, &bufsz); - if (res < 0) - tbuf->name = erlang_window_title; - else if (res == 0) { - for (i = 0; i < bufsz; ++i) { - tbuf->buf[i] = charbuff[i]; - } - tbuf->buf[bufsz - 1] = 0; - tbuf->name = &tbuf->buf[0]; - } else { - char *buf = ALLOC(bufsz); - if (!buf) - tbuf->name = erlang_window_title; - else { - while (1) { - char *newbuf; - res = erl_drv_getenv("ERL_WINDOW_TITLE", buf, &bufsz); - if (res <= 0) { - if (res == 0) { - TCHAR *wbuf = ALLOC(bufsz *sizeof(TCHAR)); - for (i = 0; i < bufsz ; ++i) { - wbuf[i] = buf[i]; - } - wbuf[bufsz - 1] = 0; - FREE(buf); - tbuf->name = wbuf; - } else { - tbuf->name = erlang_window_title; - FREE(buf); - } - break; - } - newbuf = REALLOC(buf, bufsz); - if (newbuf) - buf = newbuf; - else { - tbuf->name = erlang_window_title; - FREE(buf); - break; - } - } - } - } -} - -static void -free_window_title(struct title_buf *tbuf) -{ - if (tbuf->name != erlang_window_title && tbuf->name != &tbuf->buf[0]) - FREE(tbuf->name); -} diff --git a/erts/emulator/drivers/win32/win_con.h b/erts/emulator/drivers/win32/win_con.h deleted file mode 100644 index 7a642cd7edb7..000000000000 --- a/erts/emulator/drivers/win32/win_con.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * %CopyrightBegin% - * - * Copyright Ericsson AB 2007-2016. 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * %CopyrightEnd% - */ - -/* - * External API for the windows console (aka werl window) - * used by ttsl_drv.c - */ -#ifndef _WIN_CON_H_VISITED -#define _WIN_CON_H_VISITED 1 -void ConNormalExit(void); -void ConWaitForExit(void); -void ConSetCtrlHandler(BOOL (WINAPI *handler)(DWORD)); -int ConPutChar(Uint32 c); -void ConSetCursor(int from, int to); -void ConPrintf(char *format, ...); -void ConVprintf(char *format, va_list va); -void ConBeep(void); -int ConReadInput(Uint32 *data, int nbytes); -int ConGetKey(void); -int ConGetColumns(void); -int ConGetRows(void); -void ConInit(void); -#endif /* _WIN_CON_H_VISITED */ diff --git a/erts/emulator/nifs/common/prim_tty_nif.c b/erts/emulator/nifs/common/prim_tty_nif.c new file mode 100644 index 000000000000..309549bbd9cc --- /dev/null +++ b/erts/emulator/nifs/common/prim_tty_nif.c @@ -0,0 +1,1012 @@ +/* + * %CopyrightBegin% + * + * Copyright Ericsson 2015-2021. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * %CopyrightEnd% + */ + +/* + * Purpose: NIF library for interacting with the tty + * + */ + +#define STATIC_ERLANG_NIF 1 + +#ifndef WANT_NONBLOCKING +#define WANT_NONBLOCKING +#endif + +#include "config.h" +#include "sys.h" +#include "erl_nif.h" +#include "erl_driver.h" + +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_TERMCAP + #include + #include + #include +#endif +#ifndef __WIN32__ + #include + #include +#endif +#ifdef HAVE_SYS_UIO_H + #include +#endif + +#if defined IOV_MAX +#define MAXIOV IOV_MAX +#elif defined UIO_MAXIOV +#define MAXIOV UIO_MAXIOV +#else +#define MAXIOV 16 +#endif + +#if !defined(HAVE_SETLOCALE) || !defined(HAVE_NL_LANGINFO) || !defined(HAVE_LANGINFO_H) +#define PRIMITIVE_UTF8_CHECK 1 +#else +#include +#endif + +#ifdef VALGRIND +# include +#endif + +#define DEF_HEIGHT 24 +#define DEF_WIDTH 80 + +typedef struct { +#ifdef __WIN32__ + HANDLE ofd; + HANDLE ifd; +#else + int ofd; /* stdout */ + int ifd; /* stdin */ +#endif + ErlNifPid self; + ErlNifPid reader; + int tty; /* if the tty is initialized */ +#ifdef THREADED_READER + ErlNifTid reader_tid; +#endif +#ifndef __WIN32__ + int signal[2]; /* Pipe used for signal (winch + cont) notifications */ +#endif +#ifdef HAVE_TERMCAP + struct termios tty_smode; + struct termios tty_rmode; +#endif +} TTYResource; + +static ErlNifResourceType *tty_rt; + +/* The NIFs: */ +static ERL_NIF_TERM isatty_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_create_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_set_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM setlocale_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_select_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_read_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM isprint_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM wcwidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM wcswidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM sizeof_wchar_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_window_size_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tgetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_read_signal_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); + +static ErlNifFunc nif_funcs[] = { + {"isatty", 1, isatty_nif}, + {"tty_create", 0, tty_create_nif}, + {"tty_init", 3, tty_init_nif}, + {"tty_set", 1, tty_set_nif}, + {"tty_read_signal", 2, tty_read_signal_nif}, + {"setlocale", 0, setlocale_nif}, + {"tty_select", 3, tty_select_nif}, + {"tty_window_size", 1, tty_window_size_nif}, + {"write_nif", 2, tty_write_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"read_nif", 2, tty_read_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"isprint", 1, isprint_nif}, + {"wcwidth", 1, wcwidth_nif}, + {"wcswidth", 1, wcswidth_nif}, + {"sizeof_wchar", 0, sizeof_wchar_nif}, + {"tgetent_nif", 1, tty_tgetent_nif}, + {"tgetnum_nif", 1, tty_tgetnum_nif}, + {"tgetflag_nif", 1, tty_tgetflag_nif}, + {"tgetstr_nif", 1, tty_tgetstr_nif}, + {"tgoto_nif", 2, tty_tgoto_nif}, + {"tgoto_nif", 3, tty_tgoto_nif} +}; + +/* NIF interface declarations */ +static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info); +static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info); +static void unload(ErlNifEnv* env, void* priv_data); + +ERL_NIF_INIT(prim_tty, nif_funcs, load, NULL, upgrade, unload) + +#define ATOMS \ + ATOM_DECL(canon); \ + ATOM_DECL(echo); \ + ATOM_DECL(ebadf); \ + ATOM_DECL(undefined); \ + ATOM_DECL(error); \ + ATOM_DECL(true); \ + ATOM_DECL(ok); \ + ATOM_DECL(input); \ + ATOM_DECL(false); \ + ATOM_DECL(stdin); \ + ATOM_DECL(stdout); \ + ATOM_DECL(stderr); \ + ATOM_DECL(sig); + + +#define ATOM_DECL(A) static ERL_NIF_TERM atom_##A +ATOMS +#undef ATOM_DECL + +static ERL_NIF_TERM make_error(ErlNifEnv *env, ERL_NIF_TERM reason) { + return enif_make_tuple2(env, atom_error, reason); +} + +static ERL_NIF_TERM make_enotsup(ErlNifEnv *env) { + return make_error(env, enif_make_atom(env, "enotsup")); +} + +static ERL_NIF_TERM make_errno_error(ErlNifEnv *env, const char *function) { + ERL_NIF_TERM errorInfo; +#ifdef __WIN32__ + errorInfo = enif_make_atom(env, last_error()); +#else + errorInfo = enif_make_atom(env, erl_errno_id(errno)); +#endif + return make_error( + env, enif_make_tuple2( + env, enif_make_atom(env, function), errorInfo)); +} + +static int tty_get_fd(ErlNifEnv *env, ERL_NIF_TERM atom, int *fd) { + if (enif_is_identical(atom, atom_stdout)) { + *fd = fileno(stdout); + } else if (enif_is_identical(atom, atom_stdin)) { + *fd = fileno(stdin); + } else if (enif_is_identical(atom, atom_stderr)) { + *fd = fileno(stderr); + } else { + return 0; + } + return 1; +} + +static ERL_NIF_TERM isatty_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + int fd; + if (tty_get_fd(env, argv[0], &fd)) { + if (isatty(fd)) { + return atom_true; + } else if (errno == EINVAL || errno == ENOTTY) { + return atom_false; + } else { + return atom_ebadf; + } + } + return enif_make_badarg(env); +} + +static ERL_NIF_TERM isprint_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + int i; + if (enif_get_int(env, argv[0], &i)) { + ASSERT(i > 0 && i < 256); + return isprint((char)i) ? atom_true : atom_false; + } + return enif_make_badarg(env); +} + +static ERL_NIF_TERM wcwidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + int i; + if (enif_get_int(env, argv[0], &i)) { +#ifndef __WIN32__ + int width; + ASSERT(i > 0 && i < (1l << 21)); + width = wcwidth((wchar_t)i); + if (width == -1) { + return make_error(env, enif_make_atom(env, "not_printable")); + } + return enif_make_int(env, width); +#else + return make_enotsup(env); +#endif + } + return enif_make_badarg(env); +} + +static ERL_NIF_TERM wcswidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + ErlNifBinary bin; + if (enif_inspect_iolist_as_binary(env, argv[0], &bin)) { + wchar_t *chars = (wchar_t*)bin.data; + int width; +#ifdef DEBUG + for (int i = 0; i < bin.size / sizeof(wchar_t); i++) { + ASSERT(chars[i] >= 0 && chars[i] < (1l << 21)); + } +#endif +#ifndef __WIN32__ + width = wcswidth(chars, bin.size / sizeof(wchar_t)); +#else + width = bin.size / sizeof(wchar_t); +#endif + if (width == -1) { + return make_error(env, enif_make_atom(env, "not_printable")); + } + return enif_make_tuple2(env, atom_ok, enif_make_int(env, width)); + } + return enif_make_badarg(env); +} + +static ERL_NIF_TERM sizeof_wchar_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + return enif_make_int(env, sizeof(wchar_t)); +} + +static ERL_NIF_TERM tty_write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + ERL_NIF_TERM head = argv[1], tail; + ErlNifIOQueue *q = NULL; + ErlNifIOVec vec, *iovec = &vec; + SysIOVec *iov; + int iovcnt; + TTYResource *tty; + ssize_t res = 0; + size_t size; + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); + + while (!enif_is_identical(head, enif_make_list(env, 0))) { + if (!enif_inspect_iovec(env, MAXIOV, head, &tail, &iovec)) + return enif_make_badarg(env); + + head = tail; + + iov = iovec->iov; + size = iovec->size; + iovcnt = iovec->iovcnt; + + do { +#ifndef __WIN32__ + do { + res = writev(tty->ofd, iov, iovcnt); + } while(res < 0 && (errno == EINTR || errno == EAGAIN)); +#else + for (int i = 0; i < iovec->iovcnt; i++) { + ssize_t written; + BOOL r = WriteFile(tty->ofd, iovec->iov[i].iov_base, iovec->iov[i].iov_len, &written, NULL); + if (!r) { + res = -1; + break; + } + res += written; + } +#endif + if (res < 0) { + if (q) enif_ioq_destroy(q); + return make_errno_error(env, "writev"); + } + if (res != size) { + if (!q) { + q = enif_ioq_create(ERL_NIF_IOQ_NORMAL); + enif_ioq_enqv(q, iovec, 0); + } + } + + if (q) { + enif_ioq_deq(q, res, &size); + if (size == 0) { + enif_ioq_destroy(q); + q = NULL; + } else { + iov = enif_ioq_peek(q, &iovcnt); + } + } + } while(q); + + }; + return atom_ok; +} + +static ERL_NIF_TERM tty_read_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + TTYResource *tty; + ErlNifBinary bin; + ERL_NIF_TERM res_term; + ssize_t res = 0; + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); +#ifdef __WIN32__ + { + ssize_t inputs_read, num_characters = 0; + wchar_t *characters = NULL; + INPUT_RECORD inputs[128]; + if (!ReadConsoleInputW(tty->ifd, inputs, sizeof(inputs)/sizeof(*inputs), + &inputs_read)) { + return make_errno_error(env, "ReadConsoleInput"); + } + for (int i = 0; i < inputs_read; i++) { + if (inputs[i].EventType == KEY_EVENT) { + if (inputs[i].Event.KeyEvent.bKeyDown && + inputs[i].Event.KeyEvent.uChar.UnicodeChar < 256 && + inputs[i].Event.KeyEvent.uChar.UnicodeChar != 0) { + num_characters++; + } + if (!inputs[i].Event.KeyEvent.bKeyDown && + inputs[i].Event.KeyEvent.uChar.UnicodeChar > 255 && + inputs[i].Event.KeyEvent.uChar.UnicodeChar != 0) { + num_characters++; + } + } + } + enif_alloc_binary(num_characters * sizeof(wchar_t), &bin); + characters = (wchar_t*)bin.data; + for (int i = 0; i < inputs_read; i++) { + switch (inputs[i].EventType) + { + case KEY_EVENT: + if (inputs[i].Event.KeyEvent.bKeyDown && + inputs[i].Event.KeyEvent.uChar.UnicodeChar < 256 && + inputs[i].Event.KeyEvent.uChar.UnicodeChar != 0) { + characters[res++] = inputs[i].Event.KeyEvent.uChar.UnicodeChar; + } + if (!inputs[i].Event.KeyEvent.bKeyDown && + inputs[i].Event.KeyEvent.uChar.UnicodeChar > 255 && + inputs[i].Event.KeyEvent.uChar.UnicodeChar != 0) { + characters[res++] = inputs[i].Event.KeyEvent.uChar.UnicodeChar; + } + break; + case WINDOW_BUFFER_SIZE_EVENT: + enif_send(env, &tty->self, NULL, + enif_make_tuple2(env, enif_make_atom(env, "resize"), + enif_make_tuple2(env, + enif_make_int(env, inputs[i].Event.WindowBufferSizeEvent.dwSize.Y), + enif_make_int(env, inputs[i].Event.WindowBufferSizeEvent.dwSize.X)))); + break; + case MENU_EVENT: + case FOCUS_EVENT: + /* Should be ignored according to + https://docs.microsoft.com/en-us/windows/console/input-record-str */ + break; + default: + fprintf(stderr,"Unknown event: %d\r\n", inputs[i].EventType); + break; + } + } + res *= sizeof(wchar_t); + } +#else + enif_alloc_binary(1024, &bin); + res = read(tty->ifd, bin.data, bin.size); + if (res < 0) { + if (errno != EAGAIN && errno != EINTR) { + enif_release_binary(&bin); + return make_errno_error(env, "read"); + } + res = 0; + } else if (res == 0) { + enif_release_binary(&bin); + return make_error(env, enif_make_atom(env, "closed")); + } +#endif + enif_select(env, tty->ifd, ERL_NIF_SELECT_READ, tty, NULL, argv[1]); + if (res == bin.size) { + res_term = enif_make_binary(env, &bin); + } else if (res < bin.size / 2) { + unsigned char *buff = enif_make_new_binary(env, res, &res_term); + if (res > 0) { + memcpy(buff, bin.data, res); + } + enif_release_binary(&bin); + } else { + enif_realloc_binary(&bin, res); + res_term = enif_make_binary(env, &bin); + } + + return enif_make_tuple2(env, atom_ok, res_term); +} + +static ERL_NIF_TERM setlocale_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef __WIN32__ + if (!SetConsoleOutputCP(CP_UTF8)) { + return make_errno_error(env, "SetConsoleOutputCP"); + } + return atom_true; +#elif defined(PRIMITIVE_UTF8_CHECK) + setlocale(LC_CTYPE, ""); /* Set international environment, + ignore result */ + return enif_make_atom(env, "primitive"); +#else + char *l = setlocale(LC_CTYPE, ""); /* Set international environment */ + if (l != NULL) { + if (strcmp(nl_langinfo(CODESET), "UTF-8") == 0) + return atom_true; + } + return atom_false; +#endif +} + +static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef HAVE_TERMCAP + ErlNifBinary TERM; + if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + return enif_make_badarg(env); + if (tgetent((char *)NULL /* ignored */, (char *)TERM.data) <= 0) { + return make_errno_error(env, "tgetent"); + } + return atom_ok; +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_tgetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef HAVE_TERMCAP + ErlNifBinary TERM; + if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + return enif_make_badarg(env); + return enif_make_int(env, tgetnum((char*)TERM.data)); +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef HAVE_TERMCAP + ErlNifBinary TERM; + if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + return enif_make_badarg(env); + if (tgetflag((char*)TERM.data)) + return atom_true; + return atom_false; +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef HAVE_TERMCAP + ErlNifBinary TERM, ret; + /* tgetstr seems to use a lot of stack buffer space, + so buff needs to be relatively "small" */ + char *str = NULL; + char buff[BUFSIZ] = {0}; + + if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + return enif_make_badarg(env); + str = tgetstr((char*)TERM.data, (char**)&buff); + if (!str) return atom_false; + enif_alloc_binary(strlen(str), &ret); + memcpy(ret.data, str, strlen(str)); + return enif_make_tuple2( + env, atom_ok, enif_make_binary(env, &ret)); +#else + return make_enotsup(env); +#endif +} + +#ifdef HAVE_TERMCAP +static int tputs_buffer_index; +static unsigned char tputs_buffer[1024]; + +#if defined(__sun) && defined(__SVR4) /* Solaris */ +static int tty_puts_putc(char c) { +#else +static int tty_puts_putc(int c) { +#endif + tputs_buffer[tputs_buffer_index++] = (unsigned char)c; + return 0; +} +#endif + +static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef HAVE_TERMCAP + ErlNifBinary TERM; + char *ent; + int value1, value2 = 0; + if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM) || + !enif_get_int(env, argv[1], &value1)) + return enif_make_badarg(env); + if (argc == 2) { + ent = tgoto((char*)TERM.data, 0, value1); + } else { + ASSERT(argc == 3); + ent = tgoto((char*)TERM.data, value1, value2); + } + if (!ent) return make_errno_error(env, "tgoto"); + + tputs_buffer_index = 0; + if (tputs(ent, 1, tty_puts_putc)) { + return make_errno_error(env, "tputs"); + } else { + ERL_NIF_TERM ret; + unsigned char *buff = enif_make_new_binary(env, tputs_buffer_index, &ret); + memcpy(buff, tputs_buffer, tputs_buffer_index); + return enif_make_tuple2(env, atom_ok, ret); + } +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_create_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + + TTYResource *tty = enif_alloc_resource(tty_rt, sizeof(TTYResource)); + ERL_NIF_TERM tty_term; + memset(tty, 0, sizeof(*tty)); +#ifndef __WIN32__ + tty->ifd = 0; + tty->ofd = 1; +#else + tty->ifd = GetStdHandle(STD_INPUT_HANDLE); + tty->ofd = GetStdHandle(STD_OUTPUT_HANDLE); +#endif + + tty_term = enif_make_resource(env, tty); + enif_release_resource(tty); + + enif_set_pid_undefined(&tty->self); + enif_set_pid_undefined(&tty->reader); + + return enif_make_tuple2(env, atom_ok, tty_term); +} + +static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + +#if defined(HAVE_TERMCAP) || defined(__WIN32__) + ERL_NIF_TERM canon, echo, sig; + TTYResource *tty; + int fd; + + if (argc != 3 || + !tty_get_fd(env, argv[1], &fd) || + !enif_is_map(env, argv[2])) { + return enif_make_badarg(env); + } + + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); + + if (!enif_get_map_value(env, argv[2], enif_make_atom(env,"canon"), &canon)) + canon = enif_make_atom(env, "undefined"); + if (!enif_get_map_value(env, argv[2], enif_make_atom(env,"echo"), &echo)) + echo = enif_make_atom(env, "undefined"); + if (!enif_get_map_value(env, argv[2], enif_make_atom(env,"sig"), &sig)) + sig = enif_make_atom(env, "undefined"); + +#ifndef __WIN32__ + if (tcgetattr(fd, &tty->tty_rmode) < 0) { + return make_errno_error(env, "tcgetattr"); + } + + tty->tty_smode = tty->tty_rmode; + + /* Default characteristics for all usage including termcap output. */ + tty->tty_smode.c_iflag &= ~ISTRIP; + + /* erts_fprintf(stderr,"canon %T\r\n", canon); */ + /* Turn canonical (line mode) on off. */ + if (enif_is_identical(canon, atom_true)) { + tty->tty_smode.c_iflag |= ICRNL; + tty->tty_smode.c_lflag |= ICANON; + tty->tty_smode.c_oflag |= OPOST; + tty->tty_smode.c_cc[VEOF] = tty->tty_rmode.c_cc[VEOF]; +#ifdef VDSUSP + tty->tty_smode.c_cc[VDSUSP] = tty->tty_rmode.c_cc[VDSUSP]; +#endif + } + if (enif_is_identical(canon, atom_false)) { + tty->tty_smode.c_iflag &= ~ICRNL; + tty->tty_smode.c_lflag &= ~ICANON; + tty->tty_smode.c_oflag &= ~OPOST; + + tty->tty_smode.c_cc[VMIN] = 1; + tty->tty_smode.c_cc[VTIME] = 0; +#ifdef VDSUSP + tty->tty_smode.c_cc[VDSUSP] = 0; +#endif + } + + /* Turn echo on or off. */ + /* erts_fprintf(stderr,"echo %T\r\n", echo); */ + if (enif_is_identical(echo, atom_true)) + tty->tty_smode.c_lflag |= ECHO; + if (enif_is_identical(echo, atom_false)) + tty->tty_smode.c_lflag &= ~ECHO; + + /* erts_fprintf(stderr,"sig %T\r\n", sig); */ + /* Set extra characteristics for "RAW" mode, no signals. */ + if (enif_is_identical(sig, atom_true)) { + /* Ignore IMAXBEL as not POSIX. */ +#ifndef QNX + tty->tty_smode.c_iflag |= (BRKINT|IGNPAR|ICRNL|IXON|IXANY); +#else + tty->tty_smode.c_iflag |= (BRKINT|IGNPAR|ICRNL|IXON); +#endif + tty->tty_smode.c_lflag |= (ISIG|IEXTEN); + } + if (enif_is_identical(sig, atom_false)) { + /* Ignore IMAXBEL as not POSIX. */ +#ifndef QNX + tty->tty_smode.c_iflag &= ~(BRKINT|IGNPAR|ICRNL|IXON|IXANY); +#else + tty->tty_smode.c_iflag &= ~(BRKINT|IGNPAR|ICRNL|IXON); +#endif + tty->tty_smode.c_lflag &= ~(ISIG|IEXTEN); + } + +#else + /* Set output mode to handle virtual terminal sequences */ + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut == INVALID_HANDLE_VALUE) + { + return make_errno_error(env, "GetStdHandle"); + } + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if (hIn == INVALID_HANDLE_VALUE) + { + return make_errno_error(env, "GetStdHandle"); + } + + DWORD dwOriginalOutMode = 0; + DWORD dwOriginalInMode = 0; + if (!GetConsoleMode(hOut, &dwOriginalOutMode)) + { + return make_errno_error(env, "GetConsoleMode"); + } + if (!GetConsoleMode(hIn, &dwOriginalInMode)) + { + return make_errno_error(env, "GetConsoleMode"); + } + + /* fprintf(stderr, "origOutMode: %x origInMode: %x\r\n", */ + /* dwOriginalOutMode, dwOriginalInMode); */ + + DWORD dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; + DWORD dwRequestedInModes = ENABLE_VIRTUAL_TERMINAL_INPUT; + DWORD dwDisabledInModes = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT; + + DWORD dwOutMode = dwOriginalOutMode | dwRequestedOutModes; + if (!SetConsoleMode(hOut, dwOutMode)) + { + /* we failed to set both modes, try to step down mode gracefully. */ + dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING; + dwOutMode = dwOriginalOutMode | dwRequestedOutModes; + if (!SetConsoleMode(hOut, dwOutMode)) + { + /* Failed to set any VT mode, can't do anything here. */ + return make_errno_error(env, "SetConsoleMode"); + } + } + + DWORD dwInMode = (dwOriginalInMode | dwRequestedInModes) & ~dwDisabledInModes; + if (!SetConsoleMode(hIn, dwInMode)) + { + /* Failed to set VT input mode, can't do anything here. */ + return make_errno_error(env, "SetConsoleMode"); + } + +#endif /* __WIN32__ */ + + tty->tty = 1; + + return atom_ok; +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_set_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#if defined(HAVE_TERMCAP) || defined(__WIN32__) + TTYResource *tty; + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); +#ifdef HAVE_TERMCAP + if (tty->tty && tcsetattr(tty->ifd, TCSANOW, &tty->tty_smode) < 0) { + return make_errno_error(env, "tcsetattr"); + } +#endif + enif_self(env, &tty->self); + enif_monitor_process(env, tty, &tty->self, NULL); + return atom_ok; +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_window_size_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + TTYResource *tty; + int width = -1, height = -1; + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); + { +#ifdef TIOCGWINSZ + struct winsize ws; + if (ioctl(tty->ifd,TIOCGWINSZ,&ws) == 0) { + if (ws.ws_col > 0) + width = ws.ws_col; + if (ws.ws_row > 0) + height = ws.ws_row; + } else if (ioctl(tty->ofd,TIOCGWINSZ,&ws) == 0) { + if (ws.ws_col > 0) + width = ws.ws_col; + if (ws.ws_row > 0) + height = ws.ws_row; + } +#elif defined(__WIN32__) + CONSOLE_SCREEN_BUFFER_INFOEX buffer_info; + buffer_info.cbSize = sizeof(buffer_info); + if (GetConsoleScreenBufferInfoEx(tty->ofd, &buffer_info)) { + height = buffer_info.dwSize.Y; + width = buffer_info.dwSize.X; + } else { + return make_errno_error(env,"GetConsoleScreenBufferInfoEx"); + } +#endif + } + if (width == -1 && height == -1) { + return make_enotsup(env); + } + return enif_make_tuple2( + env, atom_ok, + enif_make_tuple2( + env, + enif_make_int(env, width), + enif_make_int(env, height) + )); +} + +#ifndef __WIN32__ + +static int tty_signal_fd = -1; + +static RETSIGTYPE tty_cont(int sig) +{ + if (tty_signal_fd != 1) { + while (write(tty_signal_fd, "c", 1) < 0 && errno == EINTR) { }; + } +} + + +static RETSIGTYPE tty_winch(int sig) +{ + if (tty_signal_fd != 1) { + while (write(tty_signal_fd, "w", 1) < 0 && errno == EINTR) { }; + } +} + +#endif + +static ERL_NIF_TERM tty_read_signal_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + TTYResource *tty; + char buff[1]; + ssize_t ret; + ERL_NIF_TERM res; + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); +#ifndef __WIN32__ + do { + ret = read(tty->signal[0], buff, 1); + } while (ret < 0 && errno == EAGAIN); + + if (ret < 0) { + return make_errno_error(env, "read"); + } else if (ret == 0) { + return make_error(env, enif_make_atom(env,"empty")); + } + + enif_select(env, tty->signal[0], ERL_NIF_SELECT_READ, tty, NULL, argv[1]); + + if (buff[0] == 'w') { + res = enif_make_atom(env, "winch"); + } else if (buff[0] == 'c') { + res = enif_make_atom(env, "cont"); + } else { + res = enif_make_string_len(env, buff, 1, ERL_NIF_LATIN1); + } + return enif_make_tuple2(env, atom_ok, res); +#else + return make_enotsup(env); +#endif +} + +#ifdef THREADED_READED +struct tty_reader_init { + ErlNifEnv *env; + ERL_NIF_TERM tty; +}; + +#define TTY_READER_BUF_SIZE 1024 + +static void *tty_reader_thread(void *args) { + struct tty_reader_init *tty_reader_init = (struct tty_reader_init*)args; + TTYResource *tty; + ErlNifBinary binary; + ErlNifEnv *env = NULL; + ERL_NIF_TERM data[10]; + int cnt = 0; + + enif_alloc_binary(TTY_READER_BUF_SIZE, &binary); + + enif_get_resource(tty_reader_init->env, tty_reader_init->tty, tty_rt, (void **)&tty); + + SET_BLOCKING(tty->ifd); + + while(true) { + ssize_t i = read(tty->ifd, binary.data, TTY_READER_BUF_SIZE); + /* fprintf(stderr,"Read: %ld bytes from %d\r\n", i, tty->ifd); */ + if (i < 0) { + int saved_errno = errno; + if (env) { + ERL_NIF_TERM msg = enif_make_list_from_array(env, data, cnt); + enif_send(env, &tty->self, NULL, enif_make_tuple2(env, atom_input, msg)); + cnt = 0; + env = NULL; + } + if (saved_errno != EAGAIN) { + env = enif_alloc_env(); + errno = saved_errno; + enif_send(env, &tty->self, NULL, make_errno_error(env, "read")); + break; + } + } else { + if (!env) { + env = enif_alloc_env(); + } + enif_realloc_binary(&binary, i); + data[cnt++] = enif_make_binary(env, &binary); + if (cnt == 10 || i != TTY_READER_BUF_SIZE) { + ERL_NIF_TERM msg = enif_make_list_from_array(env, data, cnt); + enif_send(env, &tty->self, NULL, enif_make_tuple2(env, atom_input, msg)); + cnt = 0; + env = NULL; + } + enif_alloc_binary(TTY_READER_BUF_SIZE, &binary); + } + } + + enif_free_env(tty_reader_init->env); + enif_free(tty_reader_init); + return (void*)0; +} + +#endif + +static ERL_NIF_TERM tty_select_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { + TTYResource *tty; +#ifdef THREADED_READER + struct tty_reader_init *tty_reader_init; +#endif +#ifndef __WIN32__ + extern int using_oldshell; /* set this to let the rest of erts know */ +#endif + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); + +#ifndef __WIN32__ + if (pipe(tty->signal) == -1) { + return make_errno_error(env, "pipe"); + } + SET_NONBLOCKING(tty->signal[0]); + enif_select(env, tty->signal[0], ERL_NIF_SELECT_READ, tty, NULL, argv[1]); + tty_signal_fd = tty->signal[1]; + + sys_signal(SIGCONT, tty_cont); + sys_signal(SIGWINCH, tty_winch); + + using_oldshell = 0; +#endif + + enif_select(env, tty->ifd, ERL_NIF_SELECT_READ, tty, NULL, argv[2]); + + enif_self(env, &tty->reader); + enif_monitor_process(env, tty, &tty->reader, NULL); + +#ifdef THREADED_READER + + tty_reader_init = enif_alloc(sizeof(struct tty_reader_init)); + tty_reader_init->env = enif_alloc_env(); + tty_reader_init->tty = enif_make_copy(tty_reader_init->env, argv[0]); + + if (enif_thread_create( + "stdin_reader", + &tty->reader_tid, + tty_reader_thread, tty_reader_init, NULL)) { + enif_free(tty_reader_init); + return make_errno_error(env, "enif_thread_create"); + } +#endif + return atom_ok; +} + +static void tty_monitor_down(ErlNifEnv* caller_env, void* obj, ErlNifPid* pid, ErlNifMonitor* mon) { + TTYResource *tty = obj; +#ifdef HAVE_TERMCAP + if (enif_compare_pids(pid, &tty->self) == 0) { + tcsetattr(tty->ifd, TCSANOW, &tty->tty_rmode); + } +#endif + if (enif_compare_pids(pid, &tty->reader) == 0) { + enif_select(caller_env, tty->ifd, ERL_NIF_SELECT_STOP, tty, NULL, atom_undefined); +#ifndef __WIN32__ + enif_select(caller_env, tty->signal[0], ERL_NIF_SELECT_STOP, tty, NULL, atom_undefined); + close(tty->signal[1]); + sys_signal(SIGCONT, SIG_DFL); + sys_signal(SIGWINCH, SIG_DFL); +#endif + } +} + +static void tty_select_stop(ErlNifEnv* caller_env, void* obj, ErlNifEvent event, int is_direct_call) { +/* Only used to close the signal pipe on unix */ +#ifndef __WIN32__ + if (event != 0) + close(event); +#endif +} + +static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) +{ + + ErlNifResourceTypeInit rt = { + NULL /* dtor */, + tty_select_stop, + tty_monitor_down}; + +#define ATOM_DECL(A) atom_##A = enif_make_atom(env, #A) +ATOMS +#undef ATOM_DECL + + *priv_data = NULL; + + tty_rt = enif_open_resource_type_x(env, "tty", &rt, ERL_NIF_RT_CREATE, NULL); + + return 0; +} + +static void unload(ErlNifEnv* env, void* priv_data) +{ + +} + +static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, + ERL_NIF_TERM load_info) +{ + if (*old_priv_data != NULL) { + return -1; /* Don't know how to do that */ + } + if (*priv_data != NULL) { + return -1; /* Don't know how to do that */ + } + if (load(env, priv_data, load_info)) { + return -1; + } + return 0; +} diff --git a/erts/emulator/sys/win32/sys.c b/erts/emulator/sys/win32/sys.c index c3505eddc54d..5f269ea938b1 100644 --- a/erts/emulator/sys/win32/sys.c +++ b/erts/emulator/sys/win32/sys.c @@ -32,7 +32,6 @@ #include "erl_sys_driver.h" #include "global.h" #include "erl_threads.h" -#include "../../drivers/win32/win_con.h" #include "erl_cpu_topology.h" #include @@ -125,8 +124,6 @@ BOOL WINAPI ctrl_handler(DWORD dwCtrlType); static int max_files = 1024; static BOOL use_named_pipes; -static BOOL win_console = FALSE; - static OSVERSIONINFO int_os_version; /* Version information for Win32. */ @@ -205,10 +202,6 @@ erts_sys_misc_mem_sz(void) */ void sys_tty_reset(int exit_code) { - if (exit_code == ERTS_ERROR_EXIT) - ConWaitForExit(); - else - ConNormalExit(); } void erl_sys_args(int* argc, char** argv) @@ -304,25 +297,16 @@ int erts_set_signal(Eterm signal, Eterm type) { return 0; } +static DWORD dwOriginalOutMode = 0; +static DWORD dwOriginalInMode = 0; + static void init_console(void) { - char* mode = erts_read_env("ERL_CONSOLE_MODE"); - - if (!mode || strcmp(mode, "window") == 0) { - win_console = TRUE; - ConInit(); - /*nohup = 0;*/ - } else if (strncmp(mode, "tty:", 4) == 0) { - if (mode[5] == 'c') { - setvbuf(stdout, NULL, _IONBF, 0); - } - if (mode[6] == 'c') { - setvbuf(stderr, NULL, _IONBF, 0); - } - } - - erts_free_read_env(mode); + GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwOriginalOutMode); + GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &dwOriginalInMode); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); } int sys_max_files(void) @@ -2194,7 +2178,6 @@ fd_start(ErlDrvPort port_num, char* name, SysDriverOpts* opts) return ERL_DRV_ERROR_GENERAL; } - fd_driver_input = &(dp->in); dp->in.flags = DF_XLAT_CR; if (is_std_error) { dp->out.flags |= DF_DROP_IF_INVH; /* Just drop messages if stderror @@ -2202,6 +2185,7 @@ fd_start(ErlDrvPort port_num, char* name, SysDriverOpts* opts) } if ( in == 0 && out == 1) { + fd_driver_input = &(dp->in); save_01_port = dp; } else if (in == 2 && out == 2) { save_22_port = dp; @@ -2945,10 +2929,6 @@ sys_get_key(int fd) { ASSERT(fd == 0); - if (win_console) { - return ConGetKey(); - } - /* * Black magic follows. (Code stolen from get_overlapped_result()) */ @@ -2974,6 +2954,32 @@ sys_get_key(int fd) } } } + else { + char c[64]; + DWORD dwBytesRead, dwCurrentOutMode = 0, dwCurrentInMode = 0; + + /* Get current console information */ + GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwCurrentOutMode); + GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &dwCurrentInMode); + + /* Set the a "oldstyle" terminal with line input that we can use ReadFile on */ + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), dwOriginalOutMode); + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), + ENABLE_PROCESSED_INPUT | + ENABLE_LINE_INPUT | + ENABLE_ECHO_INPUT | + ENABLE_INSERT_MODE | + ENABLE_QUICK_EDIT_MODE | + ENABLE_AUTO_POSITION + ); + + if (ReadFile(GetStdHandle(STD_INPUT_HANDLE), &c, sizeof(c), &dwBytesRead, NULL) && dwBytesRead > 0) { + /* Restore original console information */ + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), dwCurrentOutMode); + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), dwCurrentInMode); + return c[0]; + } + } return '*'; /* Error! */ } diff --git a/erts/emulator/sys/win32/sys_interrupt.c b/erts/emulator/sys/win32/sys_interrupt.c index cee269eed4b1..b8821e47aa61 100644 --- a/erts/emulator/sys/win32/sys_interrupt.c +++ b/erts/emulator/sys/win32/sys_interrupt.c @@ -23,11 +23,13 @@ #ifdef HAVE_CONFIG_H # include "config.h" #endif + +#define ERTS_WANT_BREAK_HANDLING + #include "sys.h" #include "erl_alloc.h" #include "erl_thr_progress.h" #include "erl_driver.h" -#include "../../drivers/win32/win_con.h" #if defined(__GNUC__) # define WIN_SYS_INLINE __inline__ @@ -82,7 +84,6 @@ BOOL WINAPI ctrl_handler_ignore_break(DWORD dwCtrlType) } void erts_set_ignore_break(void) { - ConSetCtrlHandler(ctrl_handler_ignore_break); SetConsoleCtrlHandler(ctrl_handler_ignore_break, TRUE); } @@ -92,6 +93,9 @@ BOOL WINAPI ctrl_handler_replace_intr(DWORD dwCtrlType) case CTRL_C_EVENT: return FALSE; case CTRL_BREAK_EVENT: + if (ERTS_BREAK_REQUESTED) { + erts_exit(ERTS_INTR_EXIT, ""); + } SetEvent(erts_sys_break_event); break; case CTRL_LOGOFF_EVENT: @@ -110,7 +114,11 @@ BOOL WINAPI ctrl_handler_replace_intr(DWORD dwCtrlType) /* Don't use ctrl-c for break handler but let it be used by the shell instead (see user_drv.erl) */ void erts_replace_intr(void) { - ConSetCtrlHandler(ctrl_handler_replace_intr); + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + DWORD dwOriginalInMode = 0; + if (GetConsoleMode(hIn, &dwOriginalInMode)) { + SetConsoleMode(hIn, dwOriginalInMode & ~ENABLE_PROCESSED_INPUT); + } SetConsoleCtrlHandler(ctrl_handler_replace_intr, TRUE); } @@ -119,6 +127,9 @@ BOOL WINAPI ctrl_handler(DWORD dwCtrlType) switch (dwCtrlType) { case CTRL_C_EVENT: case CTRL_BREAK_EVENT: + if (ERTS_BREAK_REQUESTED) { + erts_exit(ERTS_INTR_EXIT, ""); + } SetEvent(erts_sys_break_event); break; case CTRL_LOGOFF_EVENT: @@ -135,7 +146,6 @@ BOOL WINAPI ctrl_handler(DWORD dwCtrlType) void init_break_handler() { - ConSetCtrlHandler(ctrl_handler); SetConsoleCtrlHandler(ctrl_handler, TRUE); } diff --git a/erts/emulator/test/statistics_SUITE.erl b/erts/emulator/test/statistics_SUITE.erl index c230aa9f194c..014c0a114e3d 100644 --- a/erts/emulator/test/statistics_SUITE.erl +++ b/erts/emulator/test/statistics_SUITE.erl @@ -382,7 +382,7 @@ run_scheduler_wall_time_test(Type) -> Pid end, StartDirtyHog = fun(Func) -> - F = fun () -> + F = fun() -> erts_debug:Func(alive_waitexiting, MeMySelfAndI) end, @@ -470,7 +470,7 @@ online_statistics(Stats) -> DirtyCPUSchedulersOnline = erlang:system_info(dirty_cpu_schedulers_online), DirtyIOSchedulersOnline = erlang:system_info(dirty_io_schedulers), SortedStats = lists:sort(Stats), - ct:pal("Stats: ~p~n", [SortedStats]), + ct:log("Stats: ~p~n", [SortedStats]), SchedulersStats = lists:sublist(SortedStats, 1, SchedulersOnline), DirtyCPUSchedulersStats = diff --git a/erts/etc/common/Makefile.in b/erts/etc/common/Makefile.in index 42d4395eb246..37f77d294b5f 100644 --- a/erts/etc/common/Makefile.in +++ b/erts/etc/common/Makefile.in @@ -168,7 +168,6 @@ INSTALL_PROGS = \ $(BINDIR)/erlsrv.exe \ $(BINDIR)/erl.exe \ $(BINDIR)/erl_log.exe\ - $(BINDIR)/werl.exe \ $(BINDIR)/$(ERLEXEC) \ $(INSTALL_EMBEDDED_PROGS) @@ -267,13 +266,12 @@ endif rm -f $(ERL_TOP)/erts/obj*/$(TARGET)/vxcall.o rm -f $(ERL_TOP)/erts/obj*/$(TARGET)/erl.o rm -f $(ERL_TOP)/erts/obj*/$(TARGET)/erl_log.o - rm -f $(ERL_TOP)/erts/obj*/$(TARGET)/werl.o rm -f $(TEXTFILES) rm -f *~ core #------------------------------------------------------------------------ # Windows specific targets -# The windows platform is quite different from the others. erl/werl are small C programs +# The windows platform is quite different from the others. erl are small C programs # loading a DLL. INI files are used instead of environment variables and the Install # script is actually a program, also Install has an INI file which tells of emulator # versions etc. @@ -287,9 +285,6 @@ $(BINDIR)/$(ERLEXEC): $(OBJDIR)/erlexec.o $(OBJDIR)/win_erlexec.o $(OBJDIR)/init $(BINDIR)/erl@EXEEXT@: $(OBJDIR)/erl.o $(OBJDIR)/init_file.o $(OBJDIR)/$(ERLRES_OBJ) $(V_LD) $(LDFLAGS) -o $@ $(OBJDIR)/erl.o $(OBJDIR)/init_file.o $(OBJDIR)/$(ERLRES_OBJ) -$(BINDIR)/werl@EXEEXT@: $(OBJDIR)/werl.o $(OBJDIR)/init_file.o $(OBJDIR)/$(ERLRES_OBJ) - $(V_LD) $(LDFLAGS) -o $@ $(OBJDIR)/werl.o $(OBJDIR)/init_file.o $(OBJDIR)/$(ERLRES_OBJ) - $(BINDIR)/erl_log@EXEEXT@: $(OBJDIR)/erl_log.o $(V_LD) $(LDFLAGS) -o $@ $(OBJDIR)/erl_log.o @@ -367,10 +362,6 @@ $(OBJDIR)/erlsrv_util.o: $(WINETC)/erlsrv/erlsrv_util.c $(ERLSRV_HEADERS) \ $(OBJDIR)/erlsrv_logmess.h $(RC_GENERATED) $(V_CC) $(CFLAGS) -I$(OBJDIR) $(MT_FLAG) -o $@ -c $< -$(OBJDIR)/werl.o: $(WINETC)/erl.c $(WINETC)/init_file.h $(RC_GENERATED) - $(V_CC) $(CFLAGS) -DBUILD_TYPE=\"-$(TYPE)\" -DERL_RUN_SHARED_LIB=1 \ - -DWIN32_WERL -o $@ -c $(WINETC)/erl.c - $(OBJDIR)/erl_log.o: $(WINETC)/erl_log.c $(RC_GENERATED) $(V_CC) $(CFLAGS) -DBUILD_TYPE=\"-$(TYPE)\" -DERL_RUN_SHARED_LIB=1 \ -o $@ -c $(WINETC)/erl_log.c diff --git a/erts/etc/common/erlexec.c b/erts/etc/common/erlexec.c index 5d4432789be2..b666b4adec17 100644 --- a/erts/etc/common/erlexec.c +++ b/erts/etc/common/erlexec.c @@ -39,14 +39,12 @@ #define DIRSEP "\\" #define PATHSEP ";" #define NULL_DEVICE "nul" -#define BINARY_EXT "" #define DLL_EXT ".dll" #define EMULATOR_EXECUTABLE "beam.dll" #else #define PATHSEP ":" #define DIRSEP "/" #define NULL_DEVICE "/dev/null" -#define BINARY_EXT "" #define EMULATOR_EXECUTABLE "beam" #endif @@ -218,7 +216,6 @@ static char* possibly_quote(char* arg); /* * Functions from win_erlexec.c */ -int start_win_emulator(char* emu, char *startprog,char** argv, int start_detached); int start_emulator(char* emu, char*start_prog, char** argv, int start_detached); #endif @@ -246,7 +243,7 @@ static const char* emu_flavor = DEFAULT_SUFFIX; /* Flavor of emulator (smp, jit #ifdef __WIN32__ static char *start_emulator_program = NULL; /* For detached mode - - erl.exe/werl.exe */ + erl.exe */ static char* key_val_name = ERLANG_VERSION; /* Used by the registry * access functions. */ @@ -256,7 +253,6 @@ static int config_script_cnt = 0; static int got_start_erl = 0; static HANDLE this_module_handle; -static int run_werl; static WCHAR *utf8_to_utf16(unsigned char *bytes); static char *utf16_to_utf8(WCHAR *wstr); static WCHAR *latin1_to_utf16(char *str); @@ -414,7 +410,7 @@ static void add_boot_config(void) #define NEXT_ARG_CHECK() NEXT_ARG_CHECK_NAMED(argv[i]) #ifdef __WIN32__ -__declspec(dllexport) int win_erlexec(int argc, char **argv, HANDLE module, int windowed) +__declspec(dllexport) int win_erlexec(int argc, char **argv, HANDLE module) #else int main(int argc, char **argv) #endif @@ -435,7 +431,6 @@ int main(int argc, char **argv) #ifdef __WIN32__ this_module_handle = module; - run_werl = windowed; /* if we started this erl just to get a detached emulator, * the arguments are already prepared for beam, so we skip * directly to start_emulator */ @@ -534,7 +529,7 @@ int main(int argc, char **argv) emu = add_extra_suffixes(emu); emu_name = strsave(emu); - erts_snprintf(tmpStr, sizeof(tmpStr), "%s" DIRSEP "%s" BINARY_EXT, bindir, emu); + erts_snprintf(tmpStr, sizeof(tmpStr), "%s" DIRSEP "%s", bindir, emu); emu = strsave(tmpStr); s = get_env("ESCRIPT_NAME"); @@ -1127,24 +1122,7 @@ int main(int argc, char **argv) skip_arg_massage: /*DebugBreak();*/ - if (run_werl) { - if (start_detached) { - char *p; - /* transform werl to erl */ - p = start_emulator_program+strlen(start_emulator_program); - while (--p >= start_emulator_program && *p != '/' && *p != '\\' && - *p != 'W' && *p != 'w') - ; - if (p >= start_emulator_program && (*p == 'W' || *p == 'w') && - (p[1] == 'E' || p[1] == 'e') && (p[2] == 'R' || p[2] == 'r') && - (p[3] == 'L' || p[3] == 'l')) { - memmove(p,p+1,strlen(p)); - } - } - return start_win_emulator(emu, start_emulator_program, Eargsp, start_detached); - } else { - return start_emulator(emu, start_emulator_program, Eargsp, start_detached); - } + return start_emulator(emu, start_emulator_program, Eargsp, start_detached); #else @@ -1610,6 +1588,14 @@ static void get_parameters(int argc, char** argv) emu = EMULATOR_EXECUTABLE; start_emulator_program = strsave(argv[0]); + /* in wsl argv[0] is given as "erl.exe", but start_emulator_program should be + an absolute path, so we prepend BINDIR to it */ + if (strcmp(start_emulator_program, "erl.exe") == 0) { + erts_snprintf(tmpStr, sizeof(tmpStr), "%s" DIRSEP "%s", bindir, + start_emulator_program); + start_emulator_program = strsave(tmpStr); + } + free(ini_filename); } diff --git a/erts/etc/common/etc_common.h b/erts/etc/common/etc_common.h index 289a33b42a82..865cb6a6c6ff 100644 --- a/erts/etc/common/etc_common.h +++ b/erts/etc/common/etc_common.h @@ -35,6 +35,7 @@ # include # include # include +# include // _getcwd #endif #include diff --git a/erts/etc/win32/Install.c b/erts/etc/win32/Install.c index 1b8f894dc946..497dd537fd2a 100644 --- a/erts/etc/win32/Install.c +++ b/erts/etc/win32/Install.c @@ -24,6 +24,7 @@ */ #include +#include #include #include #include "init_file.h" @@ -47,11 +48,12 @@ int wmain(int argc, wchar_t **argv) InitFile *ini_file; InitSection *ini_section; HANDLE module = GetModuleHandle(NULL); - wchar_t *binaries[] = { L"erl.exe", L"werl.exe", L"erlc.exe", L"erl_call.exe", + wchar_t *binaries[] = { L"erl.exe", L"erlc.exe", L"erl_call.exe", L"dialyzer.exe", L"typer.exe", L"escript.exe", L"ct_run.exe", NULL }; wchar_t *scripts[] = { L"start_clean.boot", L"start_sasl.boot", L"no_dot_erlang.boot", NULL }; + wchar_t *links[][2] = { { L"erl.exe", L"werl.exe" }, NULL }; wchar_t fromname[MAX_PATH]; wchar_t toname[MAX_PATH]; size_t converted; @@ -175,7 +177,32 @@ int wmain(int argc, wchar_t **argv) fprintf(stderr,"Continuing installation anyway...\n"); } } - + + for (i = 0; links[i][0] != NULL; ++i) { + swprintf(toname,MAX_PATH,L"%s\\%s",bin_dir,links[i][1]); + if (!CreateSymbolicLinkW(toname,links[i][0],0)) { + DWORD err = GetLastError(); + if (err == ERROR_PRIVILEGE_NOT_HELD) { + fprintf(stderr,"Must be administrator to create link, copying %S instead.\n", + links[i][0]); + swprintf(fromname,MAX_PATH,L"%s\\%s",bin_dir,links[i][0]); + if (!CopyFileW(fromname,toname,FALSE)) { + fprintf(stderr,"Could not copy file %S to %S\n", + fromname,toname); + fprintf(stderr,"Continuing installation anyway...\n"); + } + } else { + wchar_t buf[256]; + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + buf, (sizeof(buf) / sizeof(wchar_t)), NULL); + fprintf(stderr,"Could not create links from %S to %S %d: %S\n", + fromname, toname, err, buf); + fprintf(stderr,"Continuing installation anyway...\n"); + } + } + } + for (i = 0; scripts[i] != NULL; ++i) { swprintf(fromname,MAX_PATH,L"%s\\%s",release_dir,scripts[i]); swprintf(toname,MAX_PATH,L"%s\\%s",bin_dir,scripts[i]); diff --git a/erts/etc/win32/Makefile b/erts/etc/win32/Makefile index c6376ebe7405..f553f83e9220 100644 --- a/erts/etc/win32/Makefile +++ b/erts/etc/win32/Makefile @@ -39,7 +39,6 @@ ROOTDIR = $(ERL_TOP)/erts INSTALL_PROGS = \ $(BINDIR)/inet_gethost.exe \ $(BINDIR)/erl.exe \ - $(BINDIR)/werl.exe \ $(BINDIR)/heart.exe \ $(BINDIR)/erlc.exe \ $(BINDIR)/erlsrv.exe \ diff --git a/erts/etc/win32/erl.c b/erts/etc/win32/erl.c index 99a41b99e5f9..31650de83198 100644 --- a/erts/etc/win32/erl.c +++ b/erts/etc/win32/erl.c @@ -23,7 +23,7 @@ #include #include "init_file.h" -typedef int ErlexecFunction(int, char **, HANDLE, int); +typedef int ErlexecFunction(int, char **, HANDLE); #define INI_FILENAME L"erl.ini" #define INI_SECTION "erlang" @@ -35,18 +35,8 @@ static void error(char* format, ...); static wchar_t *erlexec_name; static wchar_t *erlexec_dir; -#ifdef WIN32_WERL -#define WERL 1 -int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, - PWSTR szCmdLine, int iCmdShow) -{ - int argc = __argc; - wchar_t **argv = __wargv; -#else -#define WERL 0 int wmain(int argc, wchar_t **argv) { -#endif HANDLE erlexec_handle; /* Instance */ ErlexecFunction *win_erlexec; wchar_t *path = malloc(100*sizeof(wchar_t)); @@ -120,7 +110,7 @@ int wmain(int argc, wchar_t **argv) } #endif - return (*win_erlexec)(argc,utf8argv,erlexec_handle,WERL); + return (*win_erlexec)(argc,utf8argv,erlexec_handle); } @@ -316,7 +306,6 @@ static void get_parameters(void) free(ini_filename); } - static void error(char* format, ...) { char sbuf[2048]; @@ -326,11 +315,6 @@ static void error(char* format, ...) vsprintf(sbuf, format, ap); va_end(ap); -#ifndef WIN32_WERL - fprintf(stderr, "%s\n", sbuf); -#else - MessageBox(NULL, sbuf, "Werl", MB_OK|MB_ICONERROR); -#endif + fprintf(stderr, "%s\n", sbuf); exit(1); } - diff --git a/erts/etc/win32/nsis/erlang20.nsi b/erts/etc/win32/nsis/erlang20.nsi index ae933f59af97..2e317fd3629a 100644 --- a/erts/etc/win32/nsis/erlang20.nsi +++ b/erts/etc/win32/nsis/erlang20.nsi @@ -190,7 +190,7 @@ cp_files: CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" continue_create: CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Erlang.lnk" \ - "$INSTDIR\bin\werl.exe" + "$INSTDIR\bin\erl.exe" !insertmacro MUI_STARTMENU_WRITE_END ; And once again, the verbosity... diff --git a/erts/etc/win32/win_erlexec.c b/erts/etc/win32/win_erlexec.c index 7b21ed37850c..53b1ac92b0a8 100644 --- a/erts/etc/win32/win_erlexec.c +++ b/erts/etc/win32/win_erlexec.c @@ -48,7 +48,6 @@ static char* win32_errorstr(int error); static int has_console(void); static char** fnuttify_argv(char **argv); static void free_fnuttified(char **v); -static int windowed = 0; #ifdef LOAD_BEAM_DYNAMICALLY typedef int SysGetKeyFunction(int); @@ -133,103 +132,6 @@ free_env_val(char *value) free(value); } - -int -start_win_emulator(char* utf8emu, char *utf8start_prog, char** utf8argv, int start_detached) -{ - int len; - int argc = 0; - - windowed = 1; - while (utf8argv[argc] != NULL) { - ++argc; - } - - if (start_detached) { - wchar_t *start_prog=NULL; - int result; - int i; - wchar_t **argv; - close(0); - close(1); - close(2); - - set_env("ERL_CONSOLE_MODE", "detached"); - set_env(DLL_ENV, utf8emu); - - utf8argv[0] = utf8start_prog; - utf8argv = fnuttify_argv(utf8argv); - - len = MultiByteToWideChar(CP_UTF8, 0, utf8start_prog, -1, NULL, 0); - start_prog = malloc(len*sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, utf8start_prog, -1, start_prog, len); - - /* Convert utf8argv to multibyte argv */ - argv = malloc((argc+1) * sizeof(wchar_t*)); - for (i=0; i>, + down = <<"\n">>, + left = <<"\b">>, + right = <<"\e[C">>, + %% Tab to next 8 column windows is "\e[1I", for unix "ta" termcap + tab = <<"\e[1I">>, + insert = false, + delete = false, + position = <<"\e[6n">>, %% "u7" on my Linux + position_reply = <<"\e\\[([0-9]+);([0-9]+)R">>, + %% Copied from https://github.com/chalk/ansi-regex/blob/main/index.js + ansi_regexp = <<"^[\e",194,155,"][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?",7,")|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))">>, + %% The SGR (Select Graphic Rendition) parameters https://en.wikipedia.org/wiki/ANSI_escape_code#SGR + ansi_sgr = <<"^[\e",194,155,"]\\[[0-9;:]*m">> + }). + +-type options() :: #{ tty => boolean(), + canon => boolean(), + echo => boolean(), + sig => boolean() + }. +-type request() :: + {putc, unicode:unicode_binary()} | + {insert, unicode:unicode_binary()} | + {delete, integer()} | + {move, integer()} | + beep. +-opaque state() :: #state{}. +-export_type([state/0]). + +-spec on_load() -> ok. +on_load() -> + on_load(#{}). + +-spec on_load(Extra) -> ok when + Extra :: map(). +on_load(Extra) -> + case erlang:load_nif(atom_to_list(?MODULE), Extra) of + ok -> ok; + {error,{reload,_}} -> + ok + end. + +window_size(State = #state{ tty = TTY }) -> + case tty_window_size(TTY) of + {error, enotsup} when map_get(tty, State#state.options) -> + %% When the TTY is enabled, we should return a "dummy" row and column + %% when we cannot find the proper size. + {ok, {State#state.cols, State#state.rows}}; + WinSz -> + WinSz + end. + +-spec init(options()) -> state(). +init(UserOptions) when is_map(UserOptions) -> + + Options = options(UserOptions), + {ok, TTY} = tty_create(), + + %% Initialize the locale to see if we support utf-8 or not + UnicodeMode = + case setlocale() of + primitive -> + lists:any( + fun(Key) -> + string:find(os:getenv(Key,""),"UTF-8") =/= nomatch + end, ["LC_ALL", "LC_CTYPE", "LANG"]); + UnicodeLocale when is_boolean(UnicodeLocale) -> + UnicodeLocale + end, + + init_term(#state{ tty = TTY, unicode = UnicodeMode, options = Options }). +init_term(State = #state{ tty = TTY, options = Options }) -> + TTYState = + case maps:get(tty, Options) of + true -> + ok = tty_init(TTY, stdout, Options), + ok = tty_set(TTY), + init(State, os:type()); + false -> + State + end, + + {ok, Writer} = proc_lib:start_link(?MODULE, writer, [State#state.tty]), + {ok, Reader} = proc_lib:start_link(?MODULE, reader, [[State#state.tty, self()]]), + + update_geometry(TTYState#state{ reader = Reader, writer = Writer }). + +-spec reinit(state(), options()) -> state(). +reinit(State, UserOptions) -> + init_term(State#state{ options = options(UserOptions) }). + +options(UserOptions) -> + maps:merge( + #{ input => true, + tty => true, + canon => false, + echo => false }, UserOptions). + +init(State, {unix,_}) -> + ok = tgetent(os:getenv("TERM")), + + %% See https://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html#SEC23 + %% for a list of all possible termcap capabilities + Cols = case tgetnum("co") of + {ok, Cs} -> Cs; + _ -> (#state{})#state.cols + end, + Up = case tgetstr("up") of + {ok, U} -> U; + false -> error(enotsup) + end, + Down = case tgetstr("do") of + false -> (#state{})#state.down; + {ok, D} -> D + end, + Left = case {tgetflag("bs"),tgetstr("bc")} of + {true,_} -> (#state{})#state.left; + {_,false} -> (#state{})#state.left; + {_,{ok, L}} -> L + end, + + Right = case tgetstr("nd") of + {ok, R} -> R; + false -> error(enotsup) + end, + Insert = + case tgetstr("IC") of + {ok, IC} -> IC; + false -> (#state{})#state.insert + end, + + Tab = case tgetstr("ta") of + {ok, TA} -> TA; + false -> (#state{})#state.tab + end, + + Delete = case tgetstr("DC") of + {ok, DC} -> DC; + false -> (#state{})#state.delete + end, + + Position = case tgetstr("u7") of + {ok, <<"\e[6n">> = U7} -> + %% User 7 should contain the codes for getting + %% cursor position. + % User 6 should contain how to parse the reply + {ok, <<"\e[%i%d;%dR">>} = tgetstr("u6"), + <<"\e[6n">> = U7; + false -> (#state{})#state.position + end, + + State#state{ + cols = Cols, + xn = tgetflag("xn"), + up = Up, + down = Down, + left = Left, + right = Right, + insert = Insert, + delete = Delete, + tab = Tab, + position = Position + }; +init(State, {win32, _}) -> + State#state{ + %% position = false, + xn = true }. + +-spec handles(state()) -> #{ read := undefined | reference(), + write := reference() }. +handles(#state{ reader = undefined, + writer = {_WriterPid, WriterRef}}) -> + #{ read => undefined, write => WriterRef }; +handles(#state{ reader = {_ReaderPid, ReaderRef}, + writer = {_WriterPid, WriterRef}}) -> + #{ read => ReaderRef, write => WriterRef }. + +-spec unicode(state()) -> boolean(). +unicode(State) -> + State#state.unicode. + +-spec unicode(state(), boolean()) -> state(). +unicode(#state{ reader = {ReaderPid, _} } = State, Bool) -> + MonRef = erlang:monitor(process, ReaderPid), + ReaderPid ! {self(), set_unicode_state, Bool}, + receive + {ReaderPid, set_unicode_state, _} -> ok; + {'DOWN',MonRef,_,_,_} -> ok + end, + State#state{ unicode = Bool }. + +-spec handle_signal(state(), winch | cont) -> state(). +handle_signal(State, winch) -> + update_geometry(State); +handle_signal(State, cont) -> + tty_set(State#state.tty), + State. + +reader([TTY, Parent]) -> + register(user_drv_reader, self()), + ReaderRef = make_ref(), + SignalRef = make_ref(), + ok = tty_select(TTY, SignalRef, ReaderRef), + proc_lib:init_ack({ok, {self(), ReaderRef}}), + FromEnc = case os:type() of + {unix, _} -> utf8; + {win32, _} -> {utf16, little} + end, + reader_loop(TTY, Parent, SignalRef, ReaderRef, FromEnc, <<>>). + +reader_loop(TTY, Parent, SignalRef, ReaderRef, FromEnc, Acc) -> + receive + {select, TTY, SignalRef, ready_input} -> + {ok, Signal} = tty_read_signal(TTY, SignalRef), + Parent ! {ReaderRef,{signal,Signal}}, + reader_loop(TTY, Parent, SignalRef, ReaderRef, FromEnc, Acc); + {Parent, set_unicode_state, _} when FromEnc =:= {utf16, little} -> + %% Ignore requests on windows + Parent ! {self(), set_unicode_state, true}, + reader_loop(TTY, Parent, SignalRef, ReaderRef, FromEnc, Acc); + {Parent, set_unicode_state, Bool} -> + Parent ! {self(), set_unicode_state, FromEnc =/= latin1}, + NewFromEnc = if Bool -> utf8; not Bool -> latin1 end, + reader_loop(TTY, Parent, SignalRef, ReaderRef, NewFromEnc, Acc); + {select, TTY, ReaderRef, ready_input} -> + case read_nif(TTY, ReaderRef) of + {error, closed} -> + Parent ! {ReaderRef, eof}, + ok; + {ok, <<>>} -> + %% EAGAIN or EINTR + reader_loop(TTY, Parent, SignalRef, ReaderRef, FromEnc, Acc); + {ok, UtfXBytes} -> + + {Bytes, NewAcc, NewFromEnc} = + case unicode:characters_to_binary([Acc, UtfXBytes], FromEnc, utf8) of + {error, B, Error} -> + %% We should only be able to get incorrect encoded data when + %% using utf8 (i.e. we are on unix) + FromEnc = utf8, + Parent ! {self(), set_unicode_state, false}, + receive + {Parent, set_unicode_state, false} -> + Parent ! {self(), set_unicode_state, true} + end, + receive + {Parent, set_unicode_state, true} -> ok + end, + Latin1Chars = unicode:characters_to_binary(Error, latin1, utf8), + {<>, <<>>, latin1}; + {incomplete, B, Inc} -> + {B, Inc, FromEnc}; + B when is_binary(B) -> + {B, <<>>, FromEnc} + end, + Parent ! {ReaderRef, {data, Bytes}}, + reader_loop(TTY, Parent, SignalRef, ReaderRef, NewFromEnc, NewAcc) + end + end. + +writer(TTY) -> + register(user_drv_writer, self()), + WriterRef = make_ref(), + proc_lib:init_ack({ok, {self(), WriterRef}}), + writer_loop(TTY, WriterRef). + +-spec write(state(), unicode:chardata()) -> ok. +write(#state{ writer = {WriterPid, _}}, Chars) -> + WriterPid ! {write, erlang:iolist_to_iovec(Chars)}, ok. +-spec write(state(), unicode:chardata(), From :: pid()) -> ok. +write(#state{ writer = {WriterPid, _}}, Chars, From) -> + WriterPid ! {write, From, erlang:iolist_to_iovec(Chars)}, ok. + +writer_loop(TTY, WriterRef) -> + receive + {write, []} -> + writer_loop(TTY, WriterRef); + {write, Chars} -> + ok = write_nif(TTY, Chars), + writer_loop(TTY, WriterRef); + {write, From, []} -> + From ! {WriterRef, ok}, + writer_loop(TTY, WriterRef); + {write, From, Chars} -> + case write_nif(TTY, Chars) of + ok -> + From ! {WriterRef, ok}, + writer_loop(TTY, WriterRef); + Else -> + From ! {WriterRef, Else}, + writer_loop(TTY, WriterRef) + end + end. + +-spec handle_request(state(), request()) -> {erlang:iovec(), state()}. +handle_request(State = #state{ options = #{ tty := false } }, Request) -> + case Request of + {putc, Binary} -> + {encode(Binary, State#state.unicode), State}; + beep -> + {<<7>>, State}; + _Ignore -> + {<<>>, State} + end; +%% putc prints Binary and overwrites any existing characters +handle_request(State = #state{ unicode = U }, {putc, Binary}) -> + %% Todo should handle invalid unicode? + {PutBuffer, NewState} = insert_buf(State, Binary), + if NewState#state.buffer_after =:= [] -> + {encode(PutBuffer, U), NewState}; + true -> + %% Delete any overwritten characters after current the cursor + OldLength = logical(State#state.buffer_before), + NewLength = logical(NewState#state.buffer_before), + {_, _, _, NewBA} = split(NewLength - OldLength, NewState#state.buffer_after, U), + {encode(PutBuffer, U), NewState#state{ buffer_after = NewBA }} + end; +handle_request(State = #state{ unicode = U }, {delete, N}) when N > 0 -> + {_DelNum, DelCols, _, NewBA} = split(N, State#state.buffer_after, U), + BBCols = cols(State#state.buffer_before, U), + NewBACols = cols(NewBA, U), + {[encode(NewBA, U), + lists:duplicate(DelCols, $\s), + xnfix(State, BBCols + NewBACols + DelCols), + move_cursor(State, + BBCols + NewBACols + DelCols, + BBCols)], + State#state{ buffer_after = NewBA }}; +handle_request(State = #state{ unicode = U }, {delete, N}) when N < 0 -> + {_DelNum, DelCols, _, NewBB} = split(-N, State#state.buffer_before, U), + NewBBCols = cols(NewBB, U), + BACols = cols(State#state.buffer_after, U), + {[move_cursor(State, NewBBCols + DelCols, NewBBCols), + encode(State#state.buffer_after,U), + lists:duplicate(DelCols, $\s), + xnfix(State, NewBBCols + BACols + DelCols), + move_cursor(State, NewBBCols + BACols + DelCols, NewBBCols)], + State#state{ buffer_before = NewBB } }; +handle_request(State, {delete, 0}) -> + {"",State}; +handle_request(State = #state{ unicode = U }, {move, N}) when N < 0 -> + {_DelNum, DelCols, NewBA, NewBB} = split(-N, State#state.buffer_before, U), + NewBBCols = cols(NewBB, U), + Moves = move_cursor(State, NewBBCols + DelCols, NewBBCols), + {Moves, State#state{ buffer_before = NewBB, + buffer_after = NewBA ++ State#state.buffer_after} }; +handle_request(State = #state{ unicode = U }, {move, N}) when N > 0 -> + {_DelNum, DelCols, NewBB, NewBA} = split(N, State#state.buffer_after, U), + BBCols = cols(State#state.buffer_before, U), + {move_cursor(State, BBCols, BBCols + DelCols), + State#state{ buffer_after = NewBA, + buffer_before = NewBB ++ State#state.buffer_before} }; +handle_request(State, {move, 0}) -> + {"",State}; +handle_request(State = #state{ xn = OrigXn, unicode = U }, {insert, Chars}) -> + {InsertBuffer, NewState0} = insert_buf(State#state{ xn = false }, Chars), + NewState = NewState0#state{ xn = OrigXn }, + BBCols = cols(NewState#state.buffer_before, U), + BACols = cols(NewState#state.buffer_after, U), + {[ encode(InsertBuffer, U), + encode(NewState#state.buffer_after, U), + xnfix(State, BBCols + BACols), + move_cursor(State, BBCols + BACols, BBCols) ], + NewState}; +handle_request(State, beep) -> + {<<7>>, State}; +handle_request(State, Req) -> + erlang:display({unhandled_request, Req}), + {"", State}. + +%% Split the buffer after N logical characters returning +%% the number of real characters deleted and the column length +%% of those characters +split(N, Buff, Unicode) -> + ?dbg({?FUNCTION_NAME, N, Buff, Unicode}), + split(N, Buff, [], 0, 0, Unicode). +split(0, Buff, Acc, Chars, Cols, _Unicode) -> + ?dbg({?FUNCTION_NAME, {Chars, Cols, Acc, Buff}}), + {Chars, Cols, Acc, Buff}; +split(N, _Buff, _Acc, _Chars, _Cols, _Unicode) when N < 0 -> + ok = N; +split(_N, [], Acc, Chars, Cols, _Unicode) -> + {Chars, Cols, Acc, []}; +split(N, [Char | T], Acc, Cnt, Cols, Unicode) when is_integer(Char) -> + split(N - 1, T, [Char | Acc], Cnt + 1, Cols + npwcwidth(Char, Unicode), Unicode); +split(N, [Chars | T], Acc, Cnt, Cols, Unicode) when is_list(Chars) -> + split(N - length(Chars), T, [Chars | Acc], + Cnt + length(Chars), Cols + cols(Chars, Unicode), Unicode); +split(N, [SkipChars | T], Acc, Cnt, Cols, Unicode) when is_binary(SkipChars) -> + split(N, T, [SkipChars | Acc], Cnt, Cols, Unicode). + +logical([]) -> + 0; +logical([Char | T]) when is_integer(Char) -> + 1 + logical(T); +logical([Chars | T]) when is_list(Chars) -> + length(Chars) + logical(T); +logical([SkipChars | T]) when is_binary(SkipChars) -> + logical(T). + +move_cursor(#state{ cols = W } = State, FromCol, ToCol) -> + ?dbg({?FUNCTION_NAME, FromCol, ToCol}), + [case (ToCol div W) - (FromCol div W) of + 0 -> ""; + N when N < 0 -> + ?dbg({move, up, -N}), + move(up, State, -N); + N -> + ?dbg({move, down, N}), + move(down, State, N) + end, + case (ToCol rem W) - (FromCol rem W) of + 0 -> ""; + N when N < 0 -> + ?dbg({down, left, -N}), + move(left, State, -N); + N -> + ?dbg({down, right, N}), + move(right, State, N) + end]. + +move(up, #state{ up = Up }, N) -> + lists:duplicate(N, Up); +move(down, #state{ down = Down }, N) -> + lists:duplicate(N, Down); +move(left, #state{ left = Left }, N) -> + lists:duplicate(N, Left); +move(right, #state{ right = Right }, N) -> + lists:duplicate(N, Right). + +cols([],_Unicode) -> + 0; +cols([Char | T], Unicode) when is_integer(Char) -> + npwcwidth(Char, Unicode) + cols(T, Unicode); +cols([Chars | T], Unicode) when is_list(Chars) -> + cols(Chars, Unicode) + cols(T, Unicode); +cols([SkipSeq | T], Unicode) when is_binary(SkipSeq) -> + %% Any binary should be an ANSI escape sequence + %% so we skip that + cols(T, Unicode). + +update_geometry(State) -> + case tty_window_size(State#state.tty) of + {ok, {Cols, Rows}} when Cols > 0 -> + ?dbg({?FUNCTION_NAME, Cols}), + State#state{ cols = Cols, rows = Rows }; + _Error -> + ?dbg({?FUNCTION_NAME, _Error}), + State + end. + +npwcwidth(Char) -> + npwcwidth(Char, true). +npwcwidth(Char, true) -> + case wcwidth(Char) of + {error, not_printable} -> 0; + {error, enotsup} -> + case unicode_util:is_wide(Char) of + true -> 2; + false -> 1 + end; + C -> C + end; +npwcwidth(Char, false) -> + byte_size(char_to_latin1(Char)). + + +%% Return the xn fix for the current cursor position. +%% We use get_position to figure out if we need to calculate the current columns +%% or not. +%% +%% We need to know the actual column because get_position will return the last +%% column number when the cursor is: +%% * in the last column +%% * off screen +%% +%% and it is when the cursor is off screen that we should do the xnfix. +xnfix(#state{ position = _, unicode = U } = State) -> + xnfix(State, cols(State#state.buffer_before, U)). +%% Return the xn fix for CurrCols location. +xnfix(#state{ xn = true, cols = Cols } = State, CurrCols) + when CurrCols =/= 0, CurrCols rem Cols == 0 -> + [<<"\s">>,move(left, State, 1)]; +xnfix(_, _CurrCols) -> + ?dbg({xnfix, _CurrCols}), + []. + +characters_to_output(Chars) -> + try unicode:characters_to_binary(Chars) of + Binary -> + Binary + catch error:badarg -> + unicode:characters_to_binary( + lists:map( + fun({ansi, Ansi}) -> + Ansi; + (Char) -> + Char + end, Chars) + ) + end. +characters_to_buffer(Chars) -> + lists:flatmap( + fun({ansi, _Ansi}) -> + ""; + (Char) -> + [Char] + end, Chars). + +insert_buf(State, Binary) when is_binary(Binary) -> + insert_buf(State, Binary, [], []). +insert_buf(State, Bin, LineAcc, Acc) -> + case string:next_grapheme(Bin) of + [] -> + NewBB = characters_to_buffer(LineAcc) ++ State#state.buffer_before, + NewState = State#state{ buffer_before = NewBB }, + {[Acc, characters_to_output(lists:reverse(LineAcc)), xnfix(NewState)], + NewState}; + [$\t | Rest] -> + insert_buf(State, Rest, [State#state.tab | LineAcc], Acc); + [$\e | Rest] -> + case re:run(Bin, State#state.ansi_regexp, [unicode]) of + {match, [{0, N}]} -> + <> = Bin, + case re:run(Bin, State#state.ansi_sgr, [unicode]) of + {match, [{0, N}]} -> + %% We include the graphics ansi sequences in the + %% buffer that we step over + insert_buf(State, AnsiRest, [Ansi | LineAcc], Acc); + _ -> + %% Any other ansi sequences are just printed and + %% then dropped as they "should" not effect rendering + insert_buf(State, AnsiRest, [{ansi, Ansi} | LineAcc], Acc) + end; + _ -> + insert_buf(State, Rest, [$\e | LineAcc], Acc) + end; + [NLCR | Rest] when NLCR =:= $\n; NLCR =:= $\r -> + Tail = + if NLCR =:= $\n -> + <<$\r,$\n>>; + true -> + <<$\r>> + end, + insert_buf(State#state{ buffer_before = [], buffer_after = [] }, Rest, [], + [Acc, [characters_to_output(lists:reverse(LineAcc)), Tail]]); + [Cluster | Rest] when is_list(Cluster) -> + insert_buf(State, Rest, [Cluster | LineAcc], Acc); + %% We have gotten a code point that may be part of the previous grapheme cluster. + [Char | Rest] when Char >= 128, LineAcc =:= [], State#state.buffer_before =/= [] -> + [PrevChar | BB] = State#state.buffer_before, + case string:next_grapheme([PrevChar | Bin]) of + [PrevChar | _] -> + %% It was not part of the previous cluster, so just insert + %% it as a normal character + insert_buf(State, Rest, [Char | LineAcc], Acc); + [Cluster | ClusterRest] -> + %% It was part of the previous grapheme cluster, so we output + %% it and insert it into the before_buffer + %% TODO: If an xnfix was done on PrevChar, + %% then we should rewrite the entire grapheme cluster. + {_, ToWrite} = lists:split(length(lists:flatten([PrevChar])), Cluster), + insert_buf(State#state{ buffer_before = [Cluster | BB] }, + ClusterRest, LineAcc, + [Acc, unicode:characters_to_binary(ToWrite)]) + end; + [Char | Rest] when Char >= 128 -> + insert_buf(State, Rest, [Char | LineAcc], Acc); + [Char | Rest] -> + case {isprint(Char), Char} of + {true,_} -> + insert_buf(State, Rest, [Char | LineAcc], Acc); + {false, 8#177} -> %% DEL + insert_buf(State, Rest, ["^?" | LineAcc], Acc); + {false, _} -> + insert_buf(State, Rest, ["^" ++ [Char bor 8#40] | LineAcc], Acc) + end + end. + +-spec to_latin1(erlang:binary()) -> erlang:iovec(). +to_latin1(Bin) -> + case is_usascii(Bin) of + true -> [Bin]; + false -> lists:flatten([binary_to_latin1(Bin)]) + end. + +is_usascii(<>) when Char < 128 -> + is_usascii(T); +is_usascii(<<>>) -> + true; +is_usascii(_) -> + false. + +binary_to_latin1(Buffer) -> + [char_to_latin1(CP) || CP <- unicode:characters_to_list(Buffer)]. +char_to_latin1(UnicodeChar) when UnicodeChar >= 512 -> + <<"\\x{",(integer_to_binary(UnicodeChar, 16))/binary,"}">>; +char_to_latin1(UnicodeChar) when UnicodeChar >= 128 -> + <<"\\",(integer_to_binary(UnicodeChar, 8))/binary>>; +char_to_latin1(UnicodeChar) -> + <>. + +encode(UnicodeChars, true) -> + unicode:characters_to_binary(UnicodeChars); +encode(UnicodeChars, false) -> + to_latin1(unicode:characters_to_binary(UnicodeChars)). + +%% Using get_position adds about 10ms of latency +%% get_position(#state{ position = false }) -> +%% unknown; +%% get_position(State) -> +%% [] = write(State, State#state.position), +%% get_position(State, <<>>). +%% get_position(State, Acc) -> +%% receive +%% {select,TTY,Ref,ready_input} +%% when TTY =:= State#state.tty, +%% Ref =:= State#state.read -> +%% {Bytes, <<>>} = read_input(State#state{ acc = Acc }), +%% case re:run(Bytes, State#state.position_reply, [unicode]) of +%% {match,[{Start,Length},Row,Col]} -> +%% <> = Bytes, +%% %% This should be put in State in order to not screw up the +%% %% message order... +%% [State#state.parent ! {{self(), State#state.tty}, {data, <>}} +%% || Before =/= <<>>, After =/= <<>>], +%% {binary_to_integer(binary:part(Bytes,Row)), +%% binary_to_integer(binary:part(Bytes,Col))}; +%% nomatch -> +%% get_position(State, Bytes) +%% end +%% after 1000 -> +%% unknown +%% end. + +-ifdef(debug). +dbg(_) -> + ok. +-endif + +%% Nif functions +-spec isatty(stdin | stdout | stderr) -> boolean() | ebadf. +isatty(_Fd) -> + erlang:nif_error(undef). +tty_create() -> + erlang:nif_error(undef). +tty_init(_TTY, _Fd, _Options) -> + erlang:nif_error(undef). +tty_set(_TTY) -> + erlang:nif_error(undef). +setlocale() -> + erlang:nif_error(undef). +tty_select(_TTY, _SignalRef, _ReadRef) -> + erlang:nif_error(undef). +write_nif(_TTY, _IOVec) -> + erlang:nif_error(undef). +read_nif(_TTY, _Ref) -> + erlang:nif_error(undef). +tty_window_size(_TTY) -> + erlang:nif_error(undef). +isprint(_Char) -> + erlang:nif_error(undef). +wcwidth(_Char) -> + erlang:nif_error(undef). +sizeof_wchar() -> + erlang:nif_error(undef). +wcswidth(_Char) -> + erlang:nif_error(undef). +tgetent(Char) -> + tgetent_nif([Char,0]). +tgetnum(Char) -> + tgetnum_nif([Char,0]). +tgetflag(Char) -> + tgetflag_nif([Char,0]). +tgetstr(Char) -> + tgetstr_nif([Char,0]). +tgoto(Char, Arg) -> + tgoto_nif([Char,0], Arg). +tgoto(Char, Arg1, Arg2) -> + tgoto_nif([Char,0], Arg1, Arg2). +tgetent_nif(_Char) -> + erlang:nif_error(undef). +tgetnum_nif(_Char) -> + erlang:nif_error(undef). +tgetflag_nif(_Char) -> + erlang:nif_error(undef). +tgetstr_nif(_Char) -> + erlang:nif_error(undef). +tgoto_nif(_Ent, _Arg) -> + erlang:nif_error(undef). +tgoto_nif(_Ent, _Arg1, _Arg2) -> + erlang:nif_error(undef). +tty_read_signal(_TTY, _Ref) -> + erlang:nif_error(undef). + diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 4ae0498a8c9f..6c6a4b7024fc 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -19,13 +19,14 @@ %% -module(user_drv). -%% Basic interface to a port. +%% Basic interface to stdin/stdout. %% %% This is responsible for a couple of things: -%% - Dispatching I/O messages when erl is running as a terminal. +%% - Dispatching I/O messages when erl is running +%% * as a terminal. %% 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, UnicodeBinary :: binary()}}` +%% `{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 %% @@ -76,28 +77,24 @@ -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)). +-record(state, { tty, write, read, shell_started = true, user, current_group, groups, queue }). --record(state, { port, user, current_group, groups, queue }). - -%% start() +-type shell() :: {module(), atom(), arity()} | {node(), module(), atom(), arity()}. +-type arguments() :: #{ initial_shell => shell() | {remote, unicode:charlist()} }. +%% Default line editing shell -spec start() -> pid(). - -start() -> %Default line editing shell - start(#{}). +start() -> + case init:get_argument(remsh) of + {ok,[[Node]]} -> + start(#{ initial_shell => {remote, Node} }); + E when E =:= error ; E =:= {ok,[[]]} -> + start(#{ }) + end. %% 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) -> @@ -109,93 +106,143 @@ start(Args) when is_map(Args) -> callback_mode() -> state_functions. +-spec init(arguments()) -> gen_statem:init_result(init). init(Args) -> process_flag(trap_exit, true), - case catch open_port({spawn,"tty_sl -c -e"}, [eof]) of - {'EXIT', _Reason} -> - {stop, normal}; - Port -> - {ok, init, {Args, #state{ user = start_user() } }, - {next_event, internal, Port}} + prim_tty:on_load(), + IsTTY = prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true, + if IsTTY -> + try prim_tty:init(#{}) of + TTYState -> + init_standard_error(TTYState, true), + {ok, init, {Args, #state{ user = start_user() } }, + {next_event, internal, TTYState}} + catch error:enotsup -> + %% This is thrown by prim_tty:init when + %% it could not start the terminal, + %% probably because TERM=dumb was set. + {stop, normal} + end; + not IsTTY -> + {stop, normal} end. -init(internal, Port, {Args, State = #state{ user = User }}) -> +%% 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')]), - %% Initialize standard_error - Encoding = - case get_unicode_state(Port) of - true -> unicode; - false -> latin1 - end, - ok = io:setopts(standard_error, [{encoding, Encoding}, {onlcr,true}]), - - %% Initialize the starting shell - {Curr,Shell} = - 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, + #{ 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, {}) + }, - RShell = {ANode,shell,start,[]}, - {group:start(self(), RShell, rem_sh_opts(ANode)), RShell}; - E when E =:= error ; E =:= {ok,[[]]} -> - LShell = maps:get(initial_shell, Args, {shell,start,[init]}), - {group:start(self(), LShell), LShell} - end, + case Args of + #{ initial_shell := {remote, Node} } -> + init_remote_shell(NewState, Node); + #{ initial_shell := InitialShell } -> + init_local_shell(NewState, InitialShell); + _ -> + init_local_shell(NewState, {shell,start,[init]}) + end. - Gr1 = gr_add_cur(gr_new(), User, {}), - Gr = gr_add_cur(Gr1, Curr, Shell), +init_remote_shell(State, Node) -> - NewState = State#state{ port = Port, current_group = Curr, user = User, - groups = Gr, queue = {false, queue:new()} - }, + StartedDist = + case net_kernel:get_state() of + #{ started := no } -> + {ok, _} = net_kernel:start([undefined, shortnames]), + true; + _ -> + false + end, - %% Print some information. - 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, + LocalNode = + case net_kernel:get_state() of + #{ name_type := dynamic } -> + net_kernel:nodename(); + #{ name_type := static } -> + node() + end, - {next_state, server, NewState, - {next_event, info, - {Curr, {put_chars, unicode, lists:flatten(io_lib:format("~ts\n", [Slogan]))}}}}. + RemoteNode = + case string:find(Node,"@") of + nomatch -> + list_to_atom(Node ++ string:find(atom_to_list(LocalNode),"@")); + _ -> + list_to_atom(Node) + end, -append_hostname(Node, LocalNode) -> - case string:find(Node,"@") of - nomatch -> - list_to_atom(Node ++ string:find(atom_to_list(LocalNode),"@")); - _ -> - list_to_atom(Node) + case net_kernel:connect_node(RemoteNode) of + true -> + %% 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, + + RShellOpts = [{expand_fun,fun(B)-> rpc:call(RemoteNode,edlin_expand,expand,[B]) end}], + + RShell = {RemoteNode,shell,start,[]}, + Gr = gr_add_cur(State#state.groups, + group:start(self(), RShell, RShellOpts), + RShell), + + init_shell(State#state{ groups = Gr }, [Slogan,$\n]); + false -> + ?LOG_ERROR("Could not connect to ~p, starting local shell",[RemoteNode]), + _ = [net_kernel:stop() || StartedDist], + init_local_shell(State, {shell, start, []}) end. -rem_sh_opts(Node) -> - [{expand_fun,fun(B)-> rpc:call(Node,edlin_expand,expand,[B]) 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, + + Gr = gr_add_cur(State#state.groups, + group:start(self(), InitialShell), + InitialShell), + + init_shell(State#state{ groups = Gr }, [Slogan,$\n]). + +init_shell(State, Slogan) -> + + init_standard_error(State#state.tty, State#state.shell_started), + + {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) of undefined -> @@ -206,8 +253,12 @@ start_user() -> User end. -server(info, {Port,{data,Bs}}, State = #state{ port = Port }) -> - UTF8Binary = list_to_binary(Bs), +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 -> @@ -221,30 +272,51 @@ server(info, {Port,{data,Bs}}, State = #state{ port = Port }) -> {self(), {data, unicode:characters_to_list(UTF8Binary, utf8)}}, keep_state_and_data end; -server(info, {Port,eof}, State = #state{ port = Port }) -> - State#state.current_group ! {self(),eof}, +server(info, {ReadHandle,eof}, State = #state{ read = ReadHandle }) -> + State#state.current_group ! {self(), eof}, keep_state_and_data; -server(info, {Requester,tty_geometry}, #state{ port = Port }) -> - Requester ! {self(),tty_geometry,get_tty_geometry(Port)}, - keep_state_and_data; -server(info, {Requester,get_unicode_state}, #state{ port = Port }) -> - Requester ! {self(),get_unicode_state,get_unicode_state(Port)}, +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, keep_state_and_data; -server(info, {Requester,set_unicode_state,Bool}, #state{ port = Port }) -> - Requester ! {self(),set_unicode_state,set_unicode_state(Port, Bool)}, +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, Req, State = #state{ user = User, current_group = Curr }) 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,_,_} - {keep_state, State#state{ queue = handle_req(Req, State#state.port, State#state.queue) }}; -server(info, {Port, ok}, State = #state{ port = Port, queue = {{Origin, Reply}, IOQ} }) -> + {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, Reply}, IOQ} }) -> %% We get this ok from the port, in io_request we store %% info about where to send reply at head of queue - Origin ! {reply,Reply}, - {keep_state, State#state{ queue = handle_req(next, Port, {false, IOQ}) }}; -server(info,{'EXIT',Port, _Reason}, #state{ port = Port }) -> + Origin ! {reply, Reply}, + {NewTTYState, NewQueue} = handle_req(next, State#state.tty, {false, IOQ}), + {keep_state, State#state{ tty = NewTTYState, queue = NewQueue }}; +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}, 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 }) -> @@ -253,66 +325,39 @@ server(info,{'EXIT',User, _Reason}, State = #state{ user = User }) -> groups = gr_set_num(State#state.groups, 1, NewUser, {})}}; 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 - if Reason =/= normal -> - io_requests([{put_chars,unicode,"*** ERROR: "}], State#state.port); - true -> % exit not caused by error - io_requests([{put_chars,unicode,"*** "}], State#state.port) - end, - io_requests([{put_chars,unicode,"Shell process terminated! "}], State#state.port), + 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 - io_requests([{put_chars,unicode,"***\n"}], State#state.port), + 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{ current_group = NewGroup, groups = Gr2 }}; + {keep_state, State#state{ tty = NewTTyState, + current_group = NewGroup, + groups = Gr2 }}; _ -> % remote shell - io_requests([{put_chars,unicode,"(^G to start new job) ***\n"}], - State#state.port), - {keep_state, State#state{ groups = Gr1 }} + 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(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}, - keep_state_and_data; server(_, _, _) -> %% Ignore unknown messages. keep_state_and_data. -handle_req(next,Port,{false,IOQ}=IOQueue) -> - case queue:out(IOQ) of - {empty,_} -> - IOQueue; - {{value,{Origin,Req}},ExecQ} -> - case io_request(Req,Port) of - ok -> - handle_req(next,Port,{false,ExecQ}); - Reply -> - {{Origin,Reply},ExecQ} - end - end; -handle_req(Msg,Port,{false,IOQ}=IOQueue) -> - empty = queue:peek(IOQ), - {Origin,Req} = Msg, - case io_request(Req, Port) of - ok -> - IOQueue; - Reply -> - {{Origin,Reply}, IOQ} - end; -handle_req(Msg,_Port,{Resp, IOQ}) -> - %% All requests are queued when we have outstanding sync put_chars - {Resp, queue:in(Msg,IOQ)}. - contains_ctrl_g_or_ctrl_c(<<$\^G,_/binary>>) -> ctrl_g; contains_ctrl_g_or_ctrl_c(<<$\^C,_/binary>>) -> @@ -339,96 +384,95 @@ switch_loop(internal, init, State) -> end end, NewGroup = group:start(self(), {shell,start,[]}), - io_request({put_chars,unicode,"\n"}, State#state.port), + NewTTYState = io_requests([{put_chars,unicode,<<"\n">>}], State#state.tty), {next_state, server, - State#state{ groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}}; + State#state{ tty = NewTTYState, + groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}}; jcl -> - io_request({put_chars,unicode,"\nUser switch command\n"}, State#state.port), + NewTTYState = + io_requests([{put_chars,unicode,<<"\nUser switch command\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(State#state.groups)), - {keep_state_and_data, + {keep_state, State#state{ tty = NewTTYState }, {next_event, internal, line}} end; switch_loop(internal, line, State) -> {more_chars, Cont, Rs} = edlin:start(" --> "), - io_requests(Rs, State#state.port), - {keep_state, {Cont, State}}; + {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.port, State#state.groups) of + case switch_cmd(Tokens, State#state.groups) of {ok, Groups} -> {next_state, server, State#state{ current_group = gr_cur_pid(Groups), groups = Groups } }; - retry -> - {keep_state_and_data, + {retry, Requests} -> + {keep_state, State#state{ tty = io_requests(Requests, State#state.tty) }, {next_event, internal, line}}; - {retry, Groups} -> - {keep_state, State#state{ current_group = gr_cur_pid(Groups), - groups = Groups }, + {retry, Requests, Groups} -> + {keep_state, State#state{ + tty = io_requests(Requests, State#state.tty), + current_group = gr_cur_pid(Groups), + groups = Groups }, {next_event, internal, line}} end; {error, _, _} -> - io_request({put_chars,unicode,"Illegal input\n"}, State#state.port), - {keep_state_and_data, + NewTTYState = + io_requests([{put_chars,unicode,<<"Illegal input\n">>}], State#state.tty), + {keep_state, State#state{ tty = NewTTYState }, {next_event, internal, line}} end; -switch_loop(info,{Port,{data,Cs}}, {Cont, State}) -> - case edlin:edit_line(Cs, Cont) of +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} -> - io_requests(Rs, State#state.port), - {keep_state, State, {next_event, internal, {line, Line}}}; + {keep_state, State#state{ tty = io_requests(Rs, State#state.tty) }, + {next_event, internal, {line, Line}}}; {undefined,_Char,MoreCs,NewCont,Rs} -> - io_requests(Rs, State#state.port), - io_request(beep, State#state.port), - {keep_state, {NewCont, State}, - {next_event, info, {Port,{data,MoreCs}}}}; + {keep_state, + {NewCont, State#state{ tty = io_requests(Rs ++ [beep], State#state.tty)}}, + {next_event, info, {ReadHandle,{data,MoreCs}}}}; {more_chars,NewCont,Rs} -> - io_requests(Rs, State#state.port), - {keep_state, {NewCont, State}}; + {keep_state, + {NewCont, State#state{ tty = io_requests(Rs, State#state.tty)}}}; {blink,NewCont,Rs} -> - io_requests(Rs, State#state.port), - {keep_state, {NewCont, State}, 1000} + {keep_state, + {NewCont, State#state{ tty = io_requests(Rs, State#state.tty)}}, + 1000} end; -switch_loop(timeout, _, State) -> +switch_loop(timeout, _, {_Cont, State}) -> {keep_state_and_data, - {next_state, info,{State#state.port,{data,[]}}}}; + {next_event, info, {State#state.read,{data,[]}}}}; switch_loop(info, _Unknown, _State) -> {keep_state_and_data, postpone}. -switch_cmd([{atom,_,Key},{Type,_,Value}], Port, Gr) +switch_cmd([{atom,_,Key},{Type,_,Value}], Gr) when Type =:= atom; Type =:= integer -> - switch_cmd({Key, Value}, Port, Gr); -switch_cmd([{atom,_,Key},{atom,_,V1},{atom,_,V2}], Port, Gr) -> - switch_cmd({Key, V1, V2}, Port, Gr); -switch_cmd([{atom,_,Key}], Port, Gr) -> - switch_cmd(Key, Port, Gr); -switch_cmd([{'?',_}], Port, Gr) -> - switch_cmd(h, Port, Gr); - -switch_cmd(Cmd, Port, Gr) when Cmd =:= c; Cmd =:= i; Cmd =:= k -> - Pid = gr_cur_pid(Gr), - CurrIndex = - case gr_get_info(Gr, Pid) of - undefined -> undefined; - {Ix, _} -> Ix - end, - switch_cmd({Cmd, CurrIndex}, Port, Gr); -switch_cmd({c, I}, Port, Gr0) -> + 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(Port) + undefined -> unknown_group() end; -switch_cmd({i, I}, Port, Gr) -> +switch_cmd({i, I}, Gr) -> case gr_get_num(Gr, I) of {pid,Pid} -> exit(Pid, interrupt), - retry; + {retry, []}; undefined -> - unknown_group(Port) + unknown_group() end; -switch_cmd({k, I}, Port, Gr) -> +switch_cmd({k, I}, Gr) -> case gr_get_num(Gr, I) of {pid,Pid} -> exit(Pid, die), @@ -437,163 +481,132 @@ switch_cmd({k, I}, Port, Gr) -> retry; _ -> receive {'EXIT',Pid,_} -> - {retry,gr_del_pid(Gr, Pid)} + {retry,[],gr_del_pid(Gr, Pid)} after 1000 -> - {retry,Gr} + {retry,[],Gr} end end; undefined -> - unknown_group(Port) + unknown_group() end; -switch_cmd(j, Port, Gr) -> - io_requests(gr_list(Gr), Port), - retry; -switch_cmd({s, Shell}, _Port, Gr0) when is_atom(Shell) -> +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,[]}), - {retry, Gr}; -switch_cmd(s, Port, Gr) -> - switch_cmd({s, shell}, Port, Gr); -switch_cmd(r, Port, 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,[]}), Gr = gr_add_cur(Gr0, Pid, {Node,shell,start,[]}), - {retry, Gr}; + {retry, [], Gr}; false -> - io_request({put_chars,unicode,"Node is not alive\n"}, Port), - retry + {retry, [{put_chars,unicode,<<"Node is not alive\n">>}]} end; -switch_cmd({r, Node}, Port, Gr) when is_atom(Node)-> - switch_cmd({r, Node, shell}, Port, Gr); -switch_cmd({r,Node,Shell}, Port, Gr0) when is_atom(Node), - is_atom(Shell) -> +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,[]}), Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}), - {retry, Gr}; + {retry, [], Gr}; false -> - io_request({put_chars,unicode,"Node is not alive\n"}, Port), - retry + {retry, [{put_chars,unicode,"Node is not alive\n"}]} end; -switch_cmd(q, Port, _Gr) -> +switch_cmd(q, _Gr) -> case erlang:system_info(break_ignored) of true -> % noop - io_request({put_chars,unicode,"Unknown command\n"}, Port), - retry; + {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}; false -> halt() end; -switch_cmd(h, Port, _Gr) -> - list_commands(Port), - retry; -switch_cmd([], _Port, _Gr) -> - retry; -switch_cmd(_Ts, Port, _Gr) -> - io_request({put_chars,unicode,"Unknown command\n"}, Port), - retry. +switch_cmd(h, _Gr) -> + {retry, list_commands()}; +switch_cmd([], _Gr) -> + {retry,[]}; +switch_cmd(_Ts, _Gr) -> + {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}. -unknown_group(Port) -> - io_request({put_chars,unicode,"Unknown job\n"}, Port), - retry. +unknown_group() -> + {retry,[{put_chars,unicode,<<"Unknown job\n">>}]}. - -list_commands(Port) -> +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"}], - Port). - -% Let driver report window geometry, -% definitely outside of the common interface -get_tty_geometry(Port) -> - case (catch port_control(Port,?CTRL_OP_GET_WINSIZE,[])) of - List when length(List) =:= 8 -> - <> = list_to_binary(List), - {W,H}; - _ -> - error - end. -get_unicode_state(Port) -> - case (catch port_control(Port,?CTRL_OP_GET_UNICODE_STATE,[])) of - [Int] when Int > 0 -> - true; - [Int] when Int =:= 0 -> - false; - _ -> - error - end. - -set_unicode_state(Port, Bool) -> - Data = case Bool of - true -> [1]; - false -> [0] - end, - case (catch port_control(Port,?CTRL_OP_SET_UNICODE_STATE,Data)) of - [Int] when Int > 0 -> - true; - [Int] when Int =:= 0 -> - false; - _ -> - error - end. - -%% io_request(Request, InPort, OutPort) -%% io_requests(Requests, InPort, OutPort) -%% Note: InPort is unused. -io_request({requests,Rs}, Port) -> - io_requests(Rs, Port); -io_request(Request, Port) -> - case io_command(Request) of - {Data, Reply} -> - true = port_command(Port, Data), - Reply; - unhandled -> - ok - end. - -io_requests([R|Rs], Port) -> - io_request(R, Port), - io_requests(Rs, Port); -io_requests([], _Port) -> - 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. + [{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">>}]. + +-spec io_request(request(), prim_tty:state()) -> {noreply | term(), prim_tty:state()}. +io_request({requests,Rs}, TTY) -> + {noreply, io_requests(Rs, TTY)}; +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 = prim_tty:write(NewTTY, Output, self()), + {Reply, NewTTY}; +io_request({move_rel, N}, TTY) -> + write(prim_tty:handle_request(TTY, {move, N})); +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(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([R|Rs], TTY) -> + {noreply, NewTTY} = io_request(R, TTY), + io_requests(Rs, NewTTY); +io_requests([], TTY) -> + TTY. + +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, NewTTYState} -> + {NewTTYState, {{Origin,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, NewTTYState} -> + {NewTTYState, {{Origin,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) @@ -663,5 +676,6 @@ gr_list(#gr{ current = Current, groups = Groups}) -> (#group{ index = I, shell = S }) -> Marker = ["*" || Current =:= I], [{put_chars, unicode, - lists:flatten(io_lib:format("~4w~.1ts ~w\n", [I,Marker,S]))}] + unicode:characters_to_binary( + io_lib:format("~4w~.1ts ~w\n", [I,Marker,S]))}] end, Groups). diff --git a/lib/sasl/test/systools_SUITE.erl b/lib/sasl/test/systools_SUITE.erl index 801660ddacf4..6065c099fa9e 100644 --- a/lib/sasl/test/systools_SUITE.erl +++ b/lib/sasl/test/systools_SUITE.erl @@ -1109,9 +1109,9 @@ erts_tar(Config) -> {win32, _} -> {["beam.smp.pdb","erl.exe", "erl.pdb","erl_log.exe","erlexec.dll","erlsrv.exe","heart.exe", - "start_erl.exe","werl.exe","beam.smp.dll", + "start_erl.exe","beam.smp.dll", "epmd.exe","erl.ini","erl_call.exe", - "erlexec.pdb","escript.exe","inet_gethost.exe","werl.pdb"], + "erlexec.pdb","escript.exe","inet_gethost.exe"], ["dialyzer.exe","erlc.exe","yielding_c_fun.exe","ct_run.exe","typer.exe"]} end, diff --git a/lib/stdlib/src/stdlib.app.src b/lib/stdlib/src/stdlib.app.src index 90c19b2cade4..653e92812784 100644 --- a/lib/stdlib/src/stdlib.app.src +++ b/lib/stdlib/src/stdlib.app.src @@ -112,6 +112,6 @@ dets]}, {applications, [kernel]}, {env, []}, - {runtime_dependencies, ["sasl-3.0","kernel-8.4","erts-@OTP-17934@","crypto-4.5", + {runtime_dependencies, ["sasl-3.0","kernel-@OTP-17932@","erts-@OTP-17934@","crypto-4.5", "compiler-5.0"]} ]}. From 7159bb8d4ec292bff86c96c58a374a3729cc07c6 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 15 Jun 2022 08:30:51 +0200 Subject: [PATCH 17/34] kernel: Use prim_tty wcwidth for tty testing --- lib/kernel/src/prim_tty.erl | 2 +- lib/kernel/test/interactive_shell_SUITE.erl | 36 ++++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index 3e0b52a1d55b..cf078f462bf7 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -105,7 +105,7 @@ %% to previous line automatically. -export([init/1, reinit/2, isatty/1, handles/1, unicode/1, unicode/2, handle_signal/2, - window_size/1, handle_request/2, write/2, write/3]). + window_size/1, handle_request/2, write/2, write/3, npwcwidth/1]). -nifs([isatty/1, tty_create/0, tty_init/3, tty_set/1, setlocale/0, tty_select/3, tty_window_size/1, write_nif/2, read_nif/2, isprint/1, diff --git a/lib/kernel/test/interactive_shell_SUITE.erl b/lib/kernel/test/interactive_shell_SUITE.erl index cba745264781..48b0ef89e51a 100644 --- a/lib/kernel/test/interactive_shell_SUITE.erl +++ b/lib/kernel/test/interactive_shell_SUITE.erl @@ -1079,22 +1079,26 @@ width(Str) -> lists:sum( [npwcwidth(CP) || CP <- lists:flatten(Str)]). -%% Poor mans character width -npwcwidth(16#D55C) -> - 2; %% 한 -npwcwidth(16#1f91A) -> - 2; %% hand -npwcwidth(16#1F3Fc) -> - 2; %% Skintone -npwcwidth(16#1f600) -> - 2; %% smilie -npwcwidth(C) -> - case lists:member(C, [775,776,780,785,786,787,788,791,793,794, - 804,813,848,852,854,858,871,875,878]) of - true -> - 0; - false -> - 1 +npwcwidth(CP) -> + try prim_tty:npwcwidth(CP) + catch error:undef -> + if CP =:= 16#D55C -> + 2; %% 한 + CP =:= 16#1f91A -> + 2; %% hand + CP =:= 16#1F3Fc -> + 2; %% Skintone + CP =:= 16#1f600 -> + 2; %% smilie + true -> + case lists:member(CP, [775,776,780,785,786,787,788,791,793,794, + 804,813,848,852,854,858,871,875,878]) of + true -> + 0; + false -> + 1 + end + end end. -record(tmux, {peer, node, name, orig_location }). From 669ad5e49000aff81dd8b0a635eab8a194338621 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Tue, 21 Jun 2022 17:01:11 +0200 Subject: [PATCH 18/34] kernel: Change -noshell and -noinput to use user_drv We want as much of the I/O work as possible to go through the user_drv as it will use dirty I/O schedulers to schedule the work. Also this makes the unicode detection work even for -noshell/-noinput type systems. This commit also adds user_drv:start_shell/0 to allow the user to start the shell after the fact. For instance when rebar3 starts as en escript it may want to start a shell depending on what arguments are given to it. --- erts/emulator/nifs/common/prim_tty_nif.c | 130 ++++++++++++++--------- lib/kernel/src/group.erl | 24 ++--- lib/kernel/src/prim_tty.erl | 51 ++++++--- lib/kernel/src/user_drv.erl | 93 ++++++++++------ lib/kernel/src/user_sup.erl | 7 +- 5 files changed, 192 insertions(+), 113 deletions(-) diff --git a/erts/emulator/nifs/common/prim_tty_nif.c b/erts/emulator/nifs/common/prim_tty_nif.c index 309549bbd9cc..09f762eb392e 100644 --- a/erts/emulator/nifs/common/prim_tty_nif.c +++ b/erts/emulator/nifs/common/prim_tty_nif.c @@ -79,6 +79,10 @@ typedef struct { #ifdef __WIN32__ HANDLE ofd; HANDLE ifd; + DWORD dwOriginalOutMode; + DWORD dwOriginalInMode; + DWORD dwOutMode; + DWORD dwInMode; #else int ofd; /* stdout */ int ifd; /* stdin */ @@ -127,7 +131,7 @@ static ErlNifFunc nif_funcs[] = { {"tty_init", 3, tty_init_nif}, {"tty_set", 1, tty_set_nif}, {"tty_read_signal", 2, tty_read_signal_nif}, - {"setlocale", 0, setlocale_nif}, + {"setlocale", 1, setlocale_nif}, {"tty_select", 3, tty_select_nif}, {"tty_window_size", 1, tty_window_size_nif}, {"write_nif", 2, tty_write_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, @@ -179,16 +183,18 @@ static ERL_NIF_TERM make_enotsup(ErlNifEnv *env) { return make_error(env, enif_make_atom(env, "enotsup")); } -static ERL_NIF_TERM make_errno_error(ErlNifEnv *env, const char *function) { - ERL_NIF_TERM errorInfo; +static ERL_NIF_TERM make_errno(ErlNifEnv *env) { #ifdef __WIN32__ - errorInfo = enif_make_atom(env, last_error()); + return enif_make_atom(env, last_error()); #else - errorInfo = enif_make_atom(env, erl_errno_id(errno)); + return enif_make_atom(env, erl_errno_id(errno)); #endif +} + +static ERL_NIF_TERM make_errno_error(ErlNifEnv *env, const char *function) { return make_error( env, enif_make_tuple2( - env, enif_make_atom(env, function), errorInfo)); + env, enif_make_atom(env, function), make_errno(env))); } static int tty_get_fd(ErlNifEnv *env, ERL_NIF_TERM atom, int *fd) { @@ -302,7 +308,8 @@ static ERL_NIF_TERM tty_write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM a #else for (int i = 0; i < iovec->iovcnt; i++) { ssize_t written; - BOOL r = WriteFile(tty->ofd, iovec->iov[i].iov_base, iovec->iov[i].iov_len, &written, NULL); + BOOL r = WriteFile(tty->ofd, iovec->iov[i].iov_base, + iovec->iov[i].iov_len, &written, NULL); if (!r) { res = -1; break; @@ -312,7 +319,7 @@ static ERL_NIF_TERM tty_write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM a #endif if (res < 0) { if (q) enif_ioq_destroy(q); - return make_errno_error(env, "writev"); + return make_error(env, make_errno(env)); } if (res != size) { if (!q) { @@ -341,10 +348,12 @@ static ERL_NIF_TERM tty_read_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar ErlNifBinary bin; ERL_NIF_TERM res_term; ssize_t res = 0; + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) return enif_make_badarg(env); + #ifdef __WIN32__ - { + if (tty->dwInMode) { ssize_t inputs_read, num_characters = 0; wchar_t *characters = NULL; INPUT_RECORD inputs[128]; @@ -401,6 +410,23 @@ static ERL_NIF_TERM tty_read_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar } } res *= sizeof(wchar_t); + } else { + DWORD bytesTransferred; + enif_alloc_binary(1024, &bin); + if (ReadFile(tty->ifd, bin.data, bin.size, + &bytesTransferred, NULL)) { + res = bytesTransferred; + if (res == 0) { + enif_release_binary(&bin); + return make_error(env, enif_make_atom(env, "closed")); + } + } else { + DWORD error = GetLastError(); + enif_release_binary(&bin); + if (error == ERROR_BROKEN_PIPE) + return make_error(env, enif_make_atom(env, "closed")); + return make_errno_error(env, "ReadFile"); + } } #else enif_alloc_binary(1024, &bin); @@ -435,8 +461,16 @@ static ERL_NIF_TERM tty_read_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar static ERL_NIF_TERM setlocale_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { #ifdef __WIN32__ - if (!SetConsoleOutputCP(CP_UTF8)) { - return make_errno_error(env, "SetConsoleOutputCP"); + TTYResource *tty; + + if (!enif_get_resource(env, argv[0], tty_rt, (void **)&tty)) + return enif_make_badarg(env); + + if (tty->dwOutMode) + { + if (!SetConsoleOutputCP(CP_UTF8)) { + return make_errno_error(env, "SetConsoleOutputCP"); + } } return atom_true; #elif defined(PRIMITIVE_UTF8_CHECK) @@ -566,7 +600,31 @@ static ERL_NIF_TERM tty_create_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM tty->ofd = 1; #else tty->ifd = GetStdHandle(STD_INPUT_HANDLE); + if (tty->ifd == INVALID_HANDLE_VALUE || tty->ifd == NULL) { + tty->ifd = CreateFile("nul", GENERIC_READ, 0, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + } tty->ofd = GetStdHandle(STD_OUTPUT_HANDLE); + if (tty->ofd == INVALID_HANDLE_VALUE || tty->ofd == NULL) { + tty->ofd = CreateFile("nul", GENERIC_WRITE, 0, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + } + if (GetConsoleMode(tty->ofd, &tty->dwOriginalOutMode)) + { + tty->dwOutMode = ENABLE_VIRTUAL_TERMINAL_PROCESSING | tty->dwOriginalOutMode; + if (!SetConsoleMode(tty->ofd, tty->dwOutMode)) { + /* Failed to set any VT mode, can't do anything here. */ + return make_errno_error(env, "SetConsoleMode"); + } + } + if (GetConsoleMode(tty->ifd, &tty->dwOriginalInMode)) + { + tty->dwInMode = ENABLE_VIRTUAL_TERMINAL_INPUT | tty->dwOriginalInMode; + if (!SetConsoleMode(tty->ifd, tty->dwInMode)) { + /* Failed to set any VT mode, can't do anything here. */ + return make_errno_error(env, "SetConsoleMode"); + } + } #endif tty_term = enif_make_resource(env, tty); @@ -663,53 +721,18 @@ static ERL_NIF_TERM tty_init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM ar } #else - /* Set output mode to handle virtual terminal sequences */ - HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (hOut == INVALID_HANDLE_VALUE) - { - return make_errno_error(env, "GetStdHandle"); - } - HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); - if (hIn == INVALID_HANDLE_VALUE) - { - return make_errno_error(env, "GetStdHandle"); - } - - DWORD dwOriginalOutMode = 0; - DWORD dwOriginalInMode = 0; - if (!GetConsoleMode(hOut, &dwOriginalOutMode)) - { - return make_errno_error(env, "GetConsoleMode"); - } - if (!GetConsoleMode(hIn, &dwOriginalInMode)) - { - return make_errno_error(env, "GetConsoleMode"); - } - /* fprintf(stderr, "origOutMode: %x origInMode: %x\r\n", */ - /* dwOriginalOutMode, dwOriginalInMode); */ - - DWORD dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; - DWORD dwRequestedInModes = ENABLE_VIRTUAL_TERMINAL_INPUT; - DWORD dwDisabledInModes = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT; + /* tty->dwOriginalOutMode, tty->dwOriginalInMode); */ - DWORD dwOutMode = dwOriginalOutMode | dwRequestedOutModes; - if (!SetConsoleMode(hOut, dwOutMode)) - { - /* we failed to set both modes, try to step down mode gracefully. */ - dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING; - dwOutMode = dwOriginalOutMode | dwRequestedOutModes; - if (!SetConsoleMode(hOut, dwOutMode)) - { - /* Failed to set any VT mode, can't do anything here. */ - return make_errno_error(env, "SetConsoleMode"); - } + /* If we cannot disable NEWLINE_AUTO_RETURN we continue anyway as things work */ + if (SetConsoleMode(tty->ofd, tty->dwOutMode | DISABLE_NEWLINE_AUTO_RETURN)) { + tty->dwOutMode |= DISABLE_NEWLINE_AUTO_RETURN; } - DWORD dwInMode = (dwOriginalInMode | dwRequestedInModes) & ~dwDisabledInModes; - if (!SetConsoleMode(hIn, dwInMode)) + tty->dwInMode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT); + if (!SetConsoleMode(tty->ifd, tty->dwInMode)) { - /* Failed to set VT input mode, can't do anything here. */ + /* Failed to set disable echo or line input mode */ return make_errno_error(env, "SetConsoleMode"); } @@ -922,6 +945,7 @@ static ERL_NIF_TERM tty_select_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM sys_signal(SIGWINCH, tty_winch); using_oldshell = 0; + #endif enif_select(env, tty->ifd, ERL_NIF_SELECT_READ, tty, NULL, argv[2]); diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index 73434984f171..d55a0bb69cae 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -97,7 +97,7 @@ server_loop(Drv, Shell, Buf0) -> %% selective receive loops elsewhere in this module. Buf = io_request(Req, From, ReplyAs, Drv, Shell, Buf0), server_loop(Drv, Shell, Buf); - {reply,{{From,ReplyAs},Reply}} -> + {reply,{From,ReplyAs},Reply} -> io_reply(From, ReplyAs, Reply), server_loop(Drv, Shell, Buf0); {driver_id,ReplyTo} -> @@ -191,7 +191,7 @@ io_request(Req, From, ReplyAs, Drv, Shell, Buf0) -> io_request({put_chars,unicode,Chars}, Drv, _Shell, From, Buf) -> case catch unicode:characters_to_binary(Chars,utf8) of Binary when is_binary(Binary) -> - send_drv(Drv, {put_chars_sync, unicode, Binary, {From,ok}}), + send_drv(Drv, {put_chars_sync, unicode, Binary, From}), {noreply,Buf}; _ -> {error,{error,{put_chars, unicode,Chars}},Buf} @@ -199,12 +199,12 @@ io_request({put_chars,unicode,Chars}, Drv, _Shell, From, Buf) -> io_request({put_chars,unicode,M,F,As}, Drv, _Shell, From, Buf) -> case catch apply(M, F, As) of Binary when is_binary(Binary) -> - send_drv(Drv, {put_chars_sync, unicode, Binary, {From,ok}}), + send_drv(Drv, {put_chars_sync, unicode, Binary, From}), {noreply,Buf}; Chars -> case catch unicode:characters_to_binary(Chars,utf8) of B when is_binary(B) -> - send_drv(Drv, {put_chars_sync, unicode, B, {From,ok}}), + send_drv(Drv, {put_chars_sync, unicode, B, From}), {noreply,Buf}; _ -> {error,{error,F},Buf} @@ -213,12 +213,12 @@ io_request({put_chars,unicode,M,F,As}, Drv, _Shell, From, Buf) -> io_request({put_chars,latin1,Binary}, Drv, _Shell, From, Buf) when is_binary(Binary) -> send_drv(Drv, {put_chars_sync, unicode, unicode:characters_to_binary(Binary,latin1), - {From,ok}}), + From}), {noreply,Buf}; io_request({put_chars,latin1,Chars}, Drv, _Shell, From, Buf) -> case catch unicode:characters_to_binary(Chars,latin1) of Binary when is_binary(Binary) -> - send_drv(Drv, {put_chars_sync, unicode, Binary, {From,ok}}), + send_drv(Drv, {put_chars_sync, unicode, Binary, From}), {noreply,Buf}; _ -> {error,{error,{put_chars,latin1,Chars}},Buf} @@ -228,12 +228,12 @@ io_request({put_chars,latin1,M,F,As}, Drv, _Shell, From, Buf) -> Binary when is_binary(Binary) -> send_drv(Drv, {put_chars_sync, unicode, unicode:characters_to_binary(Binary,latin1), - {From,ok}}), + From}), {noreply,Buf}; Chars -> case catch unicode:characters_to_binary(Chars,latin1) of B when is_binary(B) -> - send_drv(Drv, {put_chars_sync, unicode, B, {From,ok}}), + send_drv(Drv, {put_chars_sync, unicode, B, From}), {noreply,Buf}; _ -> {error,{error,F},Buf} @@ -654,7 +654,7 @@ more_data(What, Cont0, Drv, Shell, Ls, Encoding) -> io_request(Req, From, ReplyAs, Drv, Shell, []), %WRONG!!! send_drv_reqs(Drv, edlin:redraw_line(Cont)), get_line1({more_chars,Cont,[]}, Drv, Shell, Ls, Encoding); - {reply,{{From,ReplyAs},Reply}} -> + {reply,{From,ReplyAs},Reply} -> %% We take care of replies from puts here as well io_reply(From, ReplyAs, Reply), more_data(What, Cont0, Drv, Shell, Ls, Encoding); @@ -682,7 +682,7 @@ get_line_echo_off1({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); - {reply,{{From,ReplyAs},Reply}} when From =/= undefined -> + {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); @@ -709,7 +709,7 @@ get_chars_echo_off1(Drv, Shell) -> {io_request,From,ReplyAs,Req} when is_pid(From) -> io_request(Req, From, ReplyAs, Drv, Shell, []), get_chars_echo_off1(Drv, Shell); - {reply,{{From,ReplyAs},Reply}} when From =/= undefined -> + {reply,{From,ReplyAs},Reply} when From =/= undefined -> %% We take care of replies from puts here as well io_reply(From, ReplyAs, Reply), get_chars_echo_off1(Drv, Shell); @@ -857,7 +857,7 @@ get_password1({Chars,[]}, Drv, Shell) -> %% set to []. But do we expect anything but plain output? get_password1({Chars, []}, Drv, Shell); - {reply,{{From,ReplyAs},Reply}} -> + {reply,{From,ReplyAs},Reply} -> %% We take care of replies from puts here as well io_reply(From, ReplyAs, Reply), get_password1({Chars, []},Drv, Shell); diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index cf078f462bf7..c0933cf6857e 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -107,7 +107,7 @@ -export([init/1, reinit/2, isatty/1, handles/1, unicode/1, unicode/2, handle_signal/2, window_size/1, handle_request/2, write/2, write/3, npwcwidth/1]). --nifs([isatty/1, tty_create/0, tty_init/3, tty_set/1, setlocale/0, +-nifs([isatty/1, tty_create/0, tty_init/3, tty_set/1, setlocale/1, tty_select/3, tty_window_size/1, write_nif/2, read_nif/2, isprint/1, wcwidth/1, wcswidth/1, sizeof_wchar/0, tgetent_nif/1, tgetnum_nif/1, tgetflag_nif/1, tgetstr_nif/1, @@ -156,6 +156,7 @@ }). -type options() :: #{ tty => boolean(), + input => boolean(), canon => boolean(), echo => boolean(), sig => boolean() @@ -200,7 +201,7 @@ init(UserOptions) when is_map(UserOptions) -> %% Initialize the locale to see if we support utf-8 or not UnicodeMode = - case setlocale() of + case setlocale(TTY) of primitive -> lists:any( fun(Key) -> @@ -222,10 +223,23 @@ init_term(State = #state{ tty = TTY, options = Options }) -> State end, - {ok, Writer} = proc_lib:start_link(?MODULE, writer, [State#state.tty]), - {ok, Reader} = proc_lib:start_link(?MODULE, reader, [[State#state.tty, self()]]), + WriterState = + if TTYState#state.writer =:= undefined -> + {ok, Writer} = proc_lib:start_link(?MODULE, writer, [State#state.tty]), + TTYState#state{ writer = Writer }; + true -> + TTYState + end, + ReaderState = + case {maps:get(input, Options), TTYState#state.reader} of + {true, undefined} -> + {ok, Reader} = proc_lib:start_link(?MODULE, reader, [[State#state.tty, self()]]), + WriterState#state{ reader = Reader }; + {false, undefined} -> + WriterState + end, - update_geometry(TTYState#state{ reader = Reader, writer = Writer }). + update_geometry(ReaderState). -spec reinit(state(), options()) -> state(). reinit(State, UserOptions) -> @@ -346,7 +360,15 @@ reader([TTY, Parent]) -> proc_lib:init_ack({ok, {self(), ReaderRef}}), FromEnc = case os:type() of {unix, _} -> utf8; - {win32, _} -> {utf16, little} + {win32, _} -> + case isatty(stdin) of + true -> + {utf16, little}; + _ -> + %% When not reading from a console + %% the data read is utf8 encoded + utf8 + end end, reader_loop(TTY, Parent, SignalRef, ReaderRef, FromEnc, <<>>). @@ -409,16 +431,18 @@ writer(TTY) -> -spec write(state(), unicode:chardata()) -> ok. write(#state{ writer = {WriterPid, _}}, Chars) -> WriterPid ! {write, erlang:iolist_to_iovec(Chars)}, ok. --spec write(state(), unicode:chardata(), From :: pid()) -> ok. -write(#state{ writer = {WriterPid, _}}, Chars, From) -> - WriterPid ! {write, From, erlang:iolist_to_iovec(Chars)}, ok. +-spec write(state(), unicode:chardata(), From :: pid()) -> {ok, reference()}. +write(#state{ writer = {WriterPid, _WriterRef}}, Chars, From) -> + Ref = erlang:monitor(process, WriterPid), + WriterPid ! {write, From, erlang:iolist_to_iovec(Chars)}, + {ok, Ref}. writer_loop(TTY, WriterRef) -> receive {write, []} -> writer_loop(TTY, WriterRef); {write, Chars} -> - ok = write_nif(TTY, Chars), + _ = write_nif(TTY, Chars), writer_loop(TTY, WriterRef); {write, From, []} -> From ! {WriterRef, ok}, @@ -428,9 +452,8 @@ writer_loop(TTY, WriterRef) -> ok -> From ! {WriterRef, ok}, writer_loop(TTY, WriterRef); - Else -> - From ! {WriterRef, Else}, - writer_loop(TTY, WriterRef) + {error, Reason} -> + exit(self(), Reason) end end. @@ -790,7 +813,7 @@ tty_init(_TTY, _Fd, _Options) -> erlang:nif_error(undef). tty_set(_TTY) -> erlang:nif_error(undef). -setlocale() -> +setlocale(_TTY) -> erlang:nif_error(undef). tty_select(_TTY, _SignalRef, _ReadRef) -> erlang:nif_error(undef). diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 6c6a4b7024fc..7bb29d647999 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -24,6 +24,8 @@ %% This is responsible for a couple of things: %% - Dispatching I/O messages when erl is running %% * as a terminal. +%% * with -noshell +%% * with -noinput %% 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()}}` @@ -80,7 +82,8 @@ -record(state, { tty, write, read, shell_started = true, user, current_group, groups, queue }). -type shell() :: {module(), atom(), arity()} | {node(), module(), atom(), arity()}. --type arguments() :: #{ initial_shell => shell() | {remote, unicode:charlist()} }. +-type arguments() :: #{ initial_shell => noshell | shell() | {remote, unicode:charlist()}, + input => boolean() }. %% Default line editing shell -spec start() -> pid(). @@ -111,19 +114,28 @@ init(Args) -> process_flag(trap_exit, true), prim_tty:on_load(), IsTTY = prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true, - if IsTTY -> - try prim_tty:init(#{}) of - TTYState -> - init_standard_error(TTYState, true), - {ok, init, {Args, #state{ user = start_user() } }, - {next_event, internal, TTYState}} - catch error:enotsup -> - %% This is thrown by prim_tty:init when - %% it could not start the terminal, - %% probably because TERM=dumb was set. - {stop, normal} - end; - not IsTTY -> + StartShell = maps:get(initial_shell, Args, undefined) =/= noshell, + try + if IsTTY, StartShell -> + TTYState = prim_tty:init(#{}), + init_standard_error(TTYState, true), + {ok, init, {Args, #state{ user = start_user() } }, + {next_event, internal, TTYState}}; + not IsTTY, StartShell -> + %% We start an oldshell if stdout or stdin are not a TTY + %% and we have been told to start a shell. + {stop, normal}; + 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. {stop, normal} end. @@ -150,6 +162,8 @@ init(internal, TTYState, {Args, State = #state{ user = User }}) -> }, case Args of + #{ initial_shell := noshell } -> + init_noshell(NewState); #{ initial_shell := {remote, Node} } -> init_remote_shell(NewState, Node); #{ initial_shell := InitialShell } -> @@ -158,6 +172,11 @@ init(internal, TTYState, {Args, State = #state{ user = User }}) -> init_local_shell(NewState, {shell,start,[init]}) end. +%% 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) -> StartedDist = @@ -246,7 +265,7 @@ init_shell(State, Slogan) -> start_user() -> case whereis(user) of undefined -> - User = group:start(self(), {}), + User = group:start(self(), {}, [{echo,false}]), register(user, User), User; User -> @@ -304,17 +323,26 @@ server(info, Req, State = #state{ user = User, current_group = 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, Reply}, IOQ} }) -> - %% We get this ok from the port, in io_request we store + 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}, + 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}, + Requester ! {reply, Reply, ok}, keep_state_and_data; server(info,{'EXIT',User, shutdown}, #state{ user = User }) -> @@ -551,15 +579,16 @@ list_commands() -> QuitReq ++ [{put_chars, unicode,<<" ? | h - this message\n">>}]. --spec io_request(request(), prim_tty:state()) -> {noreply | term(), prim_tty:state()}. +-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({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 = prim_tty:write(NewTTY, Output, self()), - {Reply, NewTTY}; + {ok, MonitorRef} = prim_tty:write(NewTTY, Output, self()), + {Reply, MonitorRef, NewTTY}; io_request({move_rel, N}, TTY) -> write(prim_tty:handle_request(TTY, {move, N})); io_request({insert_chars, unicode, Chars}, TTY) -> @@ -583,26 +612,26 @@ io_requests([R|Rs], TTY) -> io_requests([], TTY) -> TTY. -handle_req(next,TTYState,{false,IOQ}=IOQueue) -> +handle_req(next, TTYState, {false, IOQ} = IOQueue) -> case queue:out(IOQ) of - {empty,_} -> + {empty, _} -> {TTYState, IOQueue}; - {{value,{Origin,Req}},ExecQ} -> - case io_request(Req,TTYState) of + {{value, {Origin, Req}}, ExecQ} -> + case io_request(Req, TTYState) of {noreply, NewTTYState} -> - handle_req(next,NewTTYState,{false,ExecQ}); - {Reply, NewTTYState} -> - {NewTTYState, {{Origin,Reply},ExecQ}} + handle_req(next, NewTTYState, {false, ExecQ}); + {Reply, MonitorRef, NewTTYState} -> + {NewTTYState, {{Origin, MonitorRef, Reply}, ExecQ}} end end; -handle_req(Msg,TTYState,{false,IOQ}=IOQueue) -> +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, NewTTYState} -> - {NewTTYState, {{Origin,Reply}, IOQ}} + {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 diff --git a/lib/kernel/src/user_sup.erl b/lib/kernel/src/user_sup.erl index 038d359564dd..a7cb1d906541 100644 --- a/lib/kernel/src/user_sup.erl +++ b/lib/kernel/src/user_sup.erl @@ -122,9 +122,12 @@ get_user(Flags) -> check_flags([{nouser, []} |T], _) -> check_flags(T, nouser); check_flags([{user, [User]} | T], _) -> check_flags(T, {list_to_atom(User), start, []}); -check_flags([{noshell, []} | T], _) -> check_flags(T, {user, start, []}); +check_flags([{noshell, []} | T], _) -> + check_flags(T,{user_drv, start, [#{ initial_shell => noshell }]}); check_flags([{oldshell, []} | T], _) -> check_flags(T, {user, start, []}); -check_flags([{noinput, []} | T], _) -> check_flags(T, {user, start_out, []}); +check_flags([{noinput, []} | T], _) -> + check_flags(T, {user_drv, start, [#{ initial_shell => noshell, + input => false }]}); check_flags([{master, [Node]} | T], _) -> check_flags(T, {master, list_to_atom(Node)}); check_flags([_H | T], User) -> check_flags(T, User); From d342869de8e0429d36c951a4448be34cbd454bf8 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 29 Jun 2022 12:26:42 +0200 Subject: [PATCH 19/34] kernel: Introduce terminal io:getopts option This is useful to have when checking if the target of output is a terminal. --- lib/kernel/src/group.erl | 15 ++++++++++++--- lib/kernel/src/user_drv.erl | 3 +++ lib/stdlib/doc/src/io.xml | 10 +++++++++- lib/stdlib/src/io.erl | 6 ++++-- lib/stdlib/src/shell_docs.erl | 12 ++++++------ lib/stdlib/test/shell_docs_SUITE.erl | 5 +++-- 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index d55a0bb69cae..f5b1a182f784 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -155,7 +155,16 @@ set_unicode_state(Drv,Bool) -> after 2000 -> timeout end. - +get_terminal_state(Drv) -> + Drv ! {self(),get_terminal_state}, + receive + {Drv,get_terminal_state,UniState} -> + UniState; + {Drv,get_terminal_state,error} -> + {error, internal} + after 2000 -> + {error,timeout} + end. io_request(Req, From, ReplyAs, Drv, Shell, Buf0) -> case io_request(Req, Drv, Shell, {From,ReplyAs}, Buf0) of @@ -411,8 +420,8 @@ getopts(Drv,Buf) -> true -> unicode; _ -> latin1 end}, - {ok,[Exp,Echo,Bin,Uni],Buf}. - + Tty = {terminal, get_terminal_state(Drv)}, + {ok,[Exp,Echo,Bin,Uni,Tty],Buf}. %% get_chars_*(Prompt, Module, Function, XtraArgument, Drv, Buffer) %% Gets characters from the input Drv until as the applied function diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 7bb29d647999..ebb501e32ad8 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -316,6 +316,9 @@ server(info, {Requester, set_unicode_state, Bool}, #state{ tty = TTYState } = St 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, Req, State = #state{ user = User, current_group = Curr }) when element(1,Req) =:= User orelse element(1,Req) =:= Curr, tuple_size(Req) =:= 2 orelse tuple_size(Req) =:= 3 -> diff --git a/lib/stdlib/doc/src/io.xml b/lib/stdlib/doc/src/io.xml index a400d2af2334..fd44dffb60ee 100644 --- a/lib/stdlib/doc/src/io.xml +++ b/lib/stdlib/doc/src/io.xml @@ -82,6 +82,9 @@ + + + @@ -779,9 +782,14 @@ enter>: alan : joe + {encoding,unicode}, + {terminal,true}]

This example is, as can be seen, run in an environment where the terminal supports Unicode input and output.

+

The terminal option is read only and indicates whether + the output stream is a terminal or not. + See setopts/1 for a description + of the other options.

diff --git a/lib/stdlib/src/io.erl b/lib/stdlib/src/io.erl index b0c2a2e586af..46d5cd4ae82f 100644 --- a/lib/stdlib/src/io.erl +++ b/lib/stdlib/src/io.erl @@ -215,14 +215,16 @@ get_password(Io) -> | {'expand_fun', expand_fun()} | {'encoding', encoding()} | {atom(), term()}. +-type get_opt_pair() :: opt_pair() + | {'terminal', boolean()}. --spec getopts() -> [opt_pair()] | {'error', Reason} when +-spec getopts() -> [get_opt_pair()] | {'error', Reason} when Reason :: term(). getopts() -> getopts(default_input()). --spec getopts(IoDevice) -> [opt_pair()] | {'error', Reason} when +-spec getopts(IoDevice) -> [get_opt_pair()] | {'error', Reason} when IoDevice :: device(), Reason :: term(). diff --git a/lib/stdlib/src/shell_docs.erl b/lib/stdlib/src/shell_docs.erl index e42b5bb5b864..201e842cb1e7 100644 --- a/lib/stdlib/src/shell_docs.erl +++ b/lib/stdlib/src/shell_docs.erl @@ -1005,18 +1005,18 @@ nl(Chars) -> init_ansi(#config{ ansi = undefined, io_opts = Opts }) -> %% We use this as our heuristic to see if we should print ansi or not case {application:get_env(kernel, shell_docs_ansi), + proplists:get_value(tty, Opts, false), proplists:is_defined(echo, Opts) andalso - proplists:is_defined(expand_fun, Opts), - os:type()} of + proplists:is_defined(expand_fun, Opts)} of {{ok,false}, _, _} -> put(ansi, noansi); {{ok,true}, _, _} -> put(ansi, []); - {_, _, {win32,_}} -> - put(ansi, noansi); - {_, true,_} -> + {_, true, _} -> + put(ansi, []); + {_, _, true} -> put(ansi, []); - {_, false,_} -> + {_, _, false} -> put(ansi, noansi) end; init_ansi(#config{ ansi = true }) -> diff --git a/lib/stdlib/test/shell_docs_SUITE.erl b/lib/stdlib/test/shell_docs_SUITE.erl index 028e2c0aba64..b7d85204d84b 100644 --- a/lib/stdlib/test/shell_docs_SUITE.erl +++ b/lib/stdlib/test/shell_docs_SUITE.erl @@ -255,14 +255,15 @@ render_non_native(_Config) -> beam_language = not_erlang, format = <<"text/asciidoc">>, module_doc = #{<<"en">> => <<"This is\n\npure text">>}, - docs= [] + docs = [] }, <<"\n\tnot_an_erlang_module\n\n" " This is\n" " \n" " pure text\n">> = - unicode:characters_to_binary(shell_docs:render(not_an_erlang_module, Docs, #{})), + unicode:characters_to_binary( + shell_docs:render(not_an_erlang_module, Docs, #{ ansi => false })), ok. From d4d5eff6ac6b0a3ba2094ff355a6f40840c2ab09 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 7 Jul 2022 16:47:44 +0200 Subject: [PATCH 20/34] stdlib: Fix group handling of eof for -noshell --- lib/kernel/src/group.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index f5b1a182f784..ac841feeac17 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -474,6 +474,8 @@ get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf0, State, Encoding) -> get_chars_apply(Pbs, M, F, Xa, Drv, Shell, Buf, State0, Line, Encoding) -> case catch M:F(State0, cast(Line,get(read_mode), Encoding), Encoding, Xa) of + {stop,Result,eof} -> + {ok,Result,eof}; {stop,Result,Rest} -> {ok,Result,append(Rest, Buf, Encoding)}; {'EXIT',_} -> @@ -702,6 +704,8 @@ get_line_echo_off1({Chars,[]}, Drv, Shell) -> {'EXIT',Shell,R} -> exit(R) end; +get_line_echo_off1(eof, _Drv, _Shell) -> + {done,eof,eof}; get_line_echo_off1({Chars,Rest}, _Drv, _Shell) -> {done,lists:reverse(Chars),case Rest of done -> []; _ -> Rest end}. @@ -739,8 +743,10 @@ 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, []) -> + eof; edit_line(eof, Chars) -> - {Chars,done}; + {Chars,eof}; edit_line([],Chars) -> {Chars,[]}; edit_line([$\r,$\n|Cs],Chars) -> From 5bb388471f293677e518c9de5ff803200a8957f8 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Tue, 9 Aug 2022 13:58:26 +0200 Subject: [PATCH 21/34] erts: Reset tty when child_setup exits If the cleanup code in sys_tty_reset is never run it can leave the terminal in a broken state. The cleanup code is not executed if erts receives a SIGKILL or if Ctrl-C is pressed when +B is started. Closes #3150 --- erts/emulator/sys/unix/erl_child_setup.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erts/emulator/sys/unix/erl_child_setup.c b/erts/emulator/sys/unix/erl_child_setup.c index 0a2ac9abf707..ae269292e5c5 100644 --- a/erts/emulator/sys/unix/erl_child_setup.c +++ b/erts/emulator/sys/unix/erl_child_setup.c @@ -58,6 +58,7 @@ #include #include #include +#include #define WANT_NONBLOCKING @@ -432,6 +433,17 @@ static int system_properties_fd(void) } #endif /* __ANDROID__ */ +/* + If beam is terminated using kill -9 or Ctrl-C when +B is set it may not + cleanup the terminal properly. So to clean it up we save the initial state in + erl_child_setup and then reset the terminal if we detect that beam terminated. + + Not all shells and OSs have this issue, but we do it on all unixes anyway as + it is hard for us to know where the bug exists or not and there is no hard in + doing it. + */ +static struct termios initial_tty_mode; + int main(int argc, char *argv[]) { @@ -447,6 +459,10 @@ main(int argc, char *argv[]) ABORT("Invalid arguments to child_setup"); } + if (isatty(0)) { + tcgetattr(0,&initial_tty_mode); + } + /* We close all fds except the uds from beam. All other fds from now on will have the CLOEXEC flags set on them. This means that we @@ -541,12 +557,18 @@ main(int argc, char *argv[]) pipes, 3, MSG_DONTWAIT)) < 0) { if (errno == EINTR) continue; + if (isatty(0)) { + tcsetattr(0,TCSANOW,&initial_tty_mode); + } DEBUG_PRINT("erl_child_setup failed to read from uds: %d, %d", res, errno); _exit(0); } if (res == 0) { DEBUG_PRINT("uds was closed!"); + if (isatty(0)) { + tcsetattr(0,TCSANOW,&initial_tty_mode); + } _exit(0); } /* Since we use unix domain sockets and send the entire data in From e276c69e793f1b6b06b86a67ce508e2265079094 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 10 Aug 2022 10:06:00 +0200 Subject: [PATCH 22/34] stdlib: Add shell_slogan stdlib configuration --- lib/kernel/test/rtnode.erl | 4 +++- lib/stdlib/doc/src/stdlib_app.xml | 12 ++++++++++++ lib/stdlib/test/io_proto_SUITE.erl | 20 ++++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/kernel/test/rtnode.erl b/lib/kernel/test/rtnode.erl index cee494e2b8d6..a7331e8e55f5 100644 --- a/lib/kernel/test/rtnode.erl +++ b/lib/kernel/test/rtnode.erl @@ -343,7 +343,9 @@ toerl_server(Parent, ToErl, TempDir, SPid) -> exit(Other) end, - State = #{port => Port, acc => [], spid => SPid}, + {ok, InitialData} = file:read_file(filename:join(TempDir,"erlang.log.1")), + + State = #{port => Port, acc => unicode:characters_to_list(InitialData), spid => SPid}, case toerl_loop(State) of normal -> ok; diff --git a/lib/stdlib/doc/src/stdlib_app.xml b/lib/stdlib/doc/src/stdlib_app.xml index 243853140bd0..1686f5cefb74 100644 --- a/lib/stdlib/doc/src/stdlib_app.xml +++ b/lib/stdlib/doc/src/stdlib_app.xml @@ -76,6 +76,18 @@

Can be used to determine how many results are saved by the Erlang shell.

+ shell_slogan = string() | fun(() -> string()) + +

The slogan printed when starting the Erlang shell subsystem. Example:

+ +$ erl -stdlib shell_slogan '"Test slogan"' +Test slogan +Eshell V13.0.2 (abort with ^G) +1> + +

The default is the return value of + erlang:system_info(system_version).

+
shell_strings = boolean()

Can be used to determine how the Erlang shell outputs lists of diff --git a/lib/stdlib/test/io_proto_SUITE.erl b/lib/stdlib/test/io_proto_SUITE.erl index 482e233493b5..8257e5191b17 100644 --- a/lib/stdlib/test/io_proto_SUITE.erl +++ b/lib/stdlib/test/io_proto_SUITE.erl @@ -24,7 +24,8 @@ -export([setopts_getopts/1,unicode_options/1,unicode_options_gen/1, binary_options/1, read_modes_gl/1, - read_modes_ogl/1, broken_unicode/1,eof_on_pipe/1,unicode_prompt/1]). + read_modes_ogl/1, broken_unicode/1,eof_on_pipe/1, + unicode_prompt/1, shell_slogan/1]). -export([io_server_proxy/1,start_io_server_proxy/0, proxy_getall/1, @@ -49,7 +50,8 @@ suite() -> all() -> [setopts_getopts, unicode_options, unicode_options_gen, binary_options, read_modes_gl, read_modes_ogl, - broken_unicode, eof_on_pipe, unicode_prompt]. + broken_unicode, eof_on_pipe, unicode_prompt, + shell_slogan]. groups() -> []. @@ -123,6 +125,20 @@ unicode_prompt(Config) when is_list(Config) -> ],[],"",["-oldshell","-pa",PA]), ok. +%% Test that an Unicode prompt does not crash the shell. +shell_slogan(Config) when is_list(Config) -> + case proplists:get_value(default_shell,Config) of + new -> + rtnode:run( + [{expect, "\\Q"++string:trim(erlang:system_info(system_version))++"\\E"} + ],[],"",[]), + rtnode:run( + [{expect, "\nTest slogan"} + ],[],"",["-stdlib","shell_slogan","\"Test slogan\""]); + _ -> + ok + end. + %% Check io:setopts and io:getopts functions. setopts_getopts(Config) when is_list(Config) -> From cb47b4d99a5918365e6c6a72bb4568ea6360cf5c Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 10 Aug 2022 11:06:30 +0200 Subject: [PATCH 23/34] stdlib: Add shell session configuration --- lib/kernel/src/user_drv.erl | 2 +- lib/stdlib/doc/src/stdlib_app.xml | 11 +++++++++++ lib/stdlib/src/shell.erl | 24 ++++++++++++++++++------ lib/stdlib/test/io_proto_SUITE.erl | 22 ++++++++++++++++++---- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index ebb501e32ad8..f94c0b48348a 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -421,7 +421,7 @@ switch_loop(internal, init, State) -> groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}}; jcl -> NewTTYState = - io_requests([{put_chars,unicode,<<"\nUser switch command\n">>}], + 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 diff --git a/lib/stdlib/doc/src/stdlib_app.xml b/lib/stdlib/doc/src/stdlib_app.xml index 1686f5cefb74..b4d0c81c96b4 100644 --- a/lib/stdlib/doc/src/stdlib_app.xml +++ b/lib/stdlib/doc/src/stdlib_app.xml @@ -76,6 +76,17 @@

Can be used to determine how many results are saved by the Erlang shell.

+ shell_session_slogan = string() | fun() -> string()) + +

The slogan printed when starting an Erlang shell. Example:

+ +$ erl -stdlib shell_session_slogan '"Test slogan"' +Erlang/OTP 26 [DEVELOPMENT] [erts-13.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] + +Test slogan +1> + +
shell_slogan = string() | fun(() -> string())

The slogan printed when starting the Erlang shell subsystem. Example:

diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index c62d5a45d250..6e11b435a79c 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -154,12 +154,24 @@ server(StartSync) -> undefined end, - case get(no_control_g) of - true -> - io:fwrite(<<"Eshell V~s\n">>, [erlang:system_info(version)]); - _undefined_or_false -> - io:fwrite(<<"Eshell V~s (abort with ^G)\n">>, - [erlang:system_info(version)]) + JCL = + case get(no_control_g) of + true -> " (type help(). for help)"; + _ -> " (press Ctrl+G to abort, type help(). for help)" + end, + DefaultSessionSlogan = + io_lib:format(<<"Eshell V~s">>, [erlang:system_info(version)]), + SessionSlogan = + case application:get_env(stdlib, shell_session_slogan, DefaultSessionSlogan) of + SloganFun when is_function(SloganFun, 0) -> + SloganFun(); + Slogan -> + Slogan + end, + try + io:fwrite("~ts~ts\n",[unicode:characters_to_list(SessionSlogan),JCL]) + catch _:_ -> + io:fwrite("Warning! The slogan \"~p\" could not be printed.\n",[SessionSlogan]) end, erase(no_control_g), diff --git a/lib/stdlib/test/io_proto_SUITE.erl b/lib/stdlib/test/io_proto_SUITE.erl index 8257e5191b17..f09c71312703 100644 --- a/lib/stdlib/test/io_proto_SUITE.erl +++ b/lib/stdlib/test/io_proto_SUITE.erl @@ -33,7 +33,7 @@ %% For spawn -export([answering_machine1/3, answering_machine2/3]). --export([uprompt/1]). +-export([uprompt/1, slogan/0, session_slogan/0]). %%-define(debug, true). @@ -127,18 +127,32 @@ unicode_prompt(Config) when is_list(Config) -> %% Test that an Unicode prompt does not crash the shell. shell_slogan(Config) when is_list(Config) -> + PA = filename:dirname(code:which(?MODULE)), case proplists:get_value(default_shell,Config) of new -> rtnode:run( - [{expect, "\\Q"++string:trim(erlang:system_info(system_version))++"\\E"} + [{expect, "\\Q"++string:trim(erlang:system_info(system_version))++"\\E"}, + {expect, "\\Q"++io_lib:format("Eshell V~s (press Ctrl+G to abort, type help(). for help)",[erlang:system_info(version)])++"\\E"} ],[],"",[]), rtnode:run( - [{expect, "\nTest slogan"} - ],[],"",["-stdlib","shell_slogan","\"Test slogan\""]); + [{expect, "\nTest slogan"}, + {expect, "\nTest session slogan \\("} + ],[],"",["-stdlib","shell_slogan","\"Test slogan\"", + "-stdlib","shell_session_slogan","\"Test session slogan\""]), + rtnode:run( + [{expect, "\nTest slogan"}, + {expect, "\\Q\nTest session slogan (\\E"} + ],[],"",["-stdlib","shell_slogan","fun io_proto_SUITE:slogan/0", + "-stdlib","shell_session_slogan","fun io_proto_SUITE:session_slogan/0", + "-pa",PA]); _ -> ok end. +slogan() -> + "Test slogan". +session_slogan() -> + "Test session slogan". %% Check io:setopts and io:getopts functions. setopts_getopts(Config) when is_list(Config) -> From e8e632c4510298863af8c7613fb7581f9cf08b33 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Wed, 10 Aug 2022 15:19:46 +0200 Subject: [PATCH 24/34] stdlib: Add shell:start_interactive/0,1 --- erts/doc/src/erl_cmd.xml | 4 +-- lib/kernel/src/prim_tty.erl | 19 ++++++++---- lib/kernel/src/user_drv.erl | 41 ++++++++++++++++++++++++- lib/stdlib/doc/src/shell.xml | 54 +++++++++++++++++++++++++++++++++ lib/stdlib/src/shell.erl | 11 +++++++ lib/stdlib/test/shell_SUITE.erl | 53 ++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 9 deletions(-) diff --git a/erts/doc/src/erl_cmd.xml b/erts/doc/src/erl_cmd.xml index 9f764d2b4e4d..74b4d39df006 100644 --- a/erts/doc/src/erl_cmd.xml +++ b/erts/doc/src/erl_cmd.xml @@ -540,12 +540,12 @@ $ erl \ to implement an Alternative Carrier for the Erlang Distribution.

- +

Ensures that the Erlang runtime system never tries to read any input. Implies .

- +

Starts an Erlang runtime system with no shell. This flag makes it possible to have the Erlang runtime system as a diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index c0933cf6857e..bd4c73132103 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -235,6 +235,8 @@ init_term(State = #state{ tty = TTY, options = Options }) -> {true, undefined} -> {ok, Reader} = proc_lib:start_link(?MODULE, reader, [[State#state.tty, self()]]), WriterState#state{ reader = Reader }; + {true, _} -> + WriterState; {false, undefined} -> WriterState end, @@ -336,12 +338,17 @@ unicode(State) -> State#state.unicode. -spec unicode(state(), boolean()) -> state(). -unicode(#state{ reader = {ReaderPid, _} } = State, Bool) -> - MonRef = erlang:monitor(process, ReaderPid), - ReaderPid ! {self(), set_unicode_state, Bool}, - receive - {ReaderPid, set_unicode_state, _} -> ok; - {'DOWN',MonRef,_,_,_} -> ok +unicode(#state{ reader = Reader } = State, Bool) -> + case Reader of + {ReaderPid, _} -> + MonRef = erlang:monitor(process, ReaderPid), + ReaderPid ! {self(), set_unicode_state, Bool}, + receive + {ReaderPid, set_unicode_state, _} -> ok; + {'DOWN',MonRef,_,_,_} -> ok + end; + undefined -> + ok end, State#state{ unicode = Bool }. diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index f94c0b48348a..98a042716be0 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -69,7 +69,7 @@ {requests, [request()]}. -export_type([message/0]). --export([start/0, start/1]). +-export([start/0, start/1, start_shell/0, start_shell/1]). %% gen_statem state callbacks -export([init/3,server/3,switch_loop/3]). @@ -95,6 +95,13 @@ start() -> start(#{ }) end. +-spec start_shell() -> ok | {error, Reason :: term()}. +start_shell() -> + start_shell(#{ }). +-spec start_shell(arguments()) -> ok | {error, enottty | already_started}. +start_shell(Args) -> + gen_statem:call(?MODULE, {start_shell, Args}). + %% Backwards compatibility with pre OTP-26 for Elixir/LFE etc -spec start(['tty_sl -c -e'| shell()]) -> pid(); (arguments()) -> pid(). @@ -272,6 +279,38 @@ start_user() -> User end. +server({call, From}, {start_shell, Args}, + State = #state{ tty = TTY, shell_started = false }) -> + case prim_tty:isatty(stdin) andalso prim_tty:isatty(stdout) of + true -> + try prim_tty:reinit(TTY, #{input => maps:get(input, Args, true) }) of + NewTTY -> + #{ read := ReadHandle, write := WriteHandle } = prim_tty:handles(NewTTY), + gen_statem:reply(From, ok), + NewState = State#state{ tty = NewTTY, + read = ReadHandle, + write = WriteHandle }, + case Args of + #{ initial_shell := noshell } -> + init_noshell(NewState); + #{ initial_shell := {remote, Node} } -> + init_remote_shell(NewState, Node); + #{ initial_shell := InitialShell } -> + init_local_shell(NewState, InitialShell); + _ -> + init_local_shell(NewState, {shell,start,[init]}) + end + catch error:enotsup -> + gen_statem:reply(From, {error, enotsup}), + keep_state_and_data + end; + false -> + gen_statem:reply(From, {error, enottty}), + keep_state_and_data + end; +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 ! diff --git a/lib/stdlib/doc/src/shell.xml b/lib/stdlib/doc/src/shell.xml index 928d2686b641..3d6f5065acf8 100644 --- a/lib/stdlib/doc/src/shell.xml +++ b/lib/stdlib/doc/src/shell.xml @@ -956,6 +956,60 @@ q - quit erlang + + + Start the interactive shell + +

Starts the interactive shell if it has not already been started. + It can be used to programatically start the shell from an escript + or when erl is started with the -noinput or -noshell flags.

+ + + + + + Start the interactive shell + +

Starts the interactive shell if it has not already been started. + It can be used to programatically start the shell from an + escript or when + erl is started with the + -noinput or + -noshell flags. + The following options are allowed:

+ + noshell + +

Starts the interactive shell as if + -noshell was given to erl. + This is only useful when erl is started with + -noinput and the + system want to read input data. +

+
+ mfa() + +

Starts the interactive shell using + mfa() + as the default shell.

+
+ {node(), + mfa()} + +

Starts the interactive shell using + mfa() on + node() as the default shell.

+
+ {remote, string()} + +

Starts the interactive shell using as if + -remsh + was given to erl.

+
+
+
+
+ Exit a normal shell and starts a restricted shell. diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index 6e11b435a79c..8d48ff7b2a03 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -23,6 +23,7 @@ -export([start_restricted/1, stop_restricted/0]). -export([local_allowed/3, non_local_allowed/3]). -export([catch_exception/1, prompt_func/1, strings/1]). +-export([start_interactive/0, start_interactive/1]). -define(LINEMAX, 30). -define(CHAR_MAX, 60). @@ -47,6 +48,16 @@ non_local_allowed({init,stop},[],State) -> non_local_allowed(_,_,State) -> {false,State}. +-spec start_interactive() -> ok | {error, already_started | enottty}. +start_interactive() -> + user_drv:start_shell(). +-spec start_interactive(noshell | mfa() | {node(), mfa()} | {remote, string()}) -> + ok | {error, already_started | enottty}. +start_interactive({Node, {M, F, A}}) -> + user_drv:start_shell(#{ initial_shell => {Node, M, F ,A} }); +start_interactive(InitialShell) -> + user_drv:start_shell(#{ initial_shell => InitialShell }). + -spec start() -> pid(). start() -> diff --git a/lib/stdlib/test/shell_SUITE.erl b/lib/stdlib/test/shell_SUITE.erl index b38dee47e7e1..6aedfb94a272 100644 --- a/lib/stdlib/test/shell_SUITE.erl +++ b/lib/stdlib/test/shell_SUITE.erl @@ -36,6 +36,8 @@ -export([ start_restricted_from_shell/1, start_restricted_on_command_line/1,restricted_local/1]). +-export([ start_interactive/1 ]). + %% Internal export. -export([otp_5435_2/0, prompt1/1, prompt2/1, prompt3/1, prompt4/1, prompt5/1]). @@ -3014,6 +3016,57 @@ otp_14296(Config) when is_list(Config) -> {error, {_,_,"bad term"}} = TF("1, 2"), ok. +start_interactive(_Config) -> + rtnode:run( + [{expect, "test"}, + {putline, "test."}, + {eval, fun() -> shell:start_interactive() end}, + {expect, "1>"}, + {expect, "2>"} + ],[],"",["-noinput","-eval","io:format(\"test~n\")"]), + + rtnode:run( + [{expect, "test"}, + {putline, "test."}, + {eval, fun() -> shell:start_interactive({shell,start,[]}) end}, + {expect, "1>"}, + {expect, "2>"} + ],[],"",["-noinput","-eval","io:format(\"test~n\")"]), + + rtnode:run( + [{expect, "test"}, + {putline, "test."}, + {eval, fun() -> shell:start_interactive(noshell) end}, + {eval, fun() -> io:format(user,"~ts",[io:get_line(user, "")]) end}, + {expect, "test\\."}, + {putline, "test."}, + {eval, fun() -> shell:start_interactive() end}, + {expect, "1>"}, + {expect, "2>"} + ],[],"",["-noinput","-eval","io:format(\"test~n\")"]), + + {ok, RPeer, RNode} = ?CT_PEER(), + unlink(RPeer), + SRNode = atom_to_list(RNode), + rtnode:run( + [{expect, "test"}, + {putline, "test."}, + {eval, fun() -> shell:start_interactive({remote, SRNode}) end}, + {expect, "\\Q("++SRNode++")\\E2>"} + ],[],"",["-noinput","-eval","io:format(\"test~n\")"]), + + {ok, Peer, Node} = ?CT_PEER(), + unlink(Peer), + SNode = atom_to_list(Node), + rtnode:run( + [{expect, "test"}, + {putline, "test."}, + {eval, fun() -> shell:start_interactive({Node, {shell,start,[]}}) end}, + {expect, "\\Q("++SNode++")\\E2>"} + ],[],"",["-noinput","-eval","io:format(\"test~n\")"]), + + ok. + term_to_string(T) -> lists:flatten(io_lib:format("~w", [T])). From 590aea1479be12806ce029c0de916d9966ab6c35 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 11 Aug 2022 09:22:58 +0200 Subject: [PATCH 25/34] erts: Fix building of debuginfo on windows --- make/configure.ac | 4 ---- otp_build | 7 +++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/make/configure.ac b/make/configure.ac index e0d4103b6edb..1276caecbcac 100644 --- a/make/configure.ac +++ b/make/configure.ac @@ -212,10 +212,6 @@ AS_HELP_STRING([--disable-parallel-configure], [disable parallel execution of co AC_ARG_ENABLE(dirty-schedulers, AS_HELP_STRING([--enable-dirty-schedulers], [enable dirty scheduler support])) -AC_ARG_ENABLE(plain-emulator, -AS_HELP_STRING([--enable-plain-emulator], [enable threaded non-smp emulator]) -AS_HELP_STRING([--disable-plain-emulator], [disable threaded non-smp emulator])) - AC_ARG_WITH(termcap, AS_HELP_STRING([--with-termcap], [use termcap (default)]) AS_HELP_STRING([--without-termcap], diff --git a/otp_build b/otp_build index 7b35b39fc2c5..284da6389320 100755 --- a/otp_build +++ b/otp_build @@ -1001,8 +1001,7 @@ do_tests () do_debuginfo_win32 () { setup_make - (cd erts/emulator && $MAKE MAKE="$MAKE" TARGET=$TARGET FLAVOR=smp debug &&\ - $MAKE MAKE="$MAKE" TARGET=$TARGET FLAVOR=plain debug) || exit 1 + (cd erts/emulator && $MAKE MAKE="$MAKE" TARGET=$TARGET debug) || exit 1 if [ -z "$1" ]; then RELDIR="$ERL_TOP/release/$TARGET" else @@ -1010,7 +1009,7 @@ do_debuginfo_win32 () fi BINDIR="$ERL_TOP/bin/$TARGET" EVSN=`grep '^VSN' erts/vsn.mk | sed 's,^VSN.*=[^0-9]*\([0-9].*\)$,@\1,g;s,^[^@].*,,g;s,^@,,g'` - for f in beam.debug.smp.dll beam.smp.pdb beam.debug.smp.dll.pdb erl.pdb werl.pdb erlexec.pdb; do + for f in beam.debug.smp.dll beam.smp.pdb beam.debug.smp.dll.pdb erl.pdb erlexec.pdb; do if [ -f $BINDIR/$f ]; then rm -f $RELDIR/erts-$EVSN/bin/$f cp $BINDIR/$f $RELDIR/erts-$EVSN/bin/$f @@ -1218,7 +1217,7 @@ case "$1" in do_configure "$@";; opt) do_boot;; - plain|smp) + smp) if [ $minus_x_flag = false ]; then TYPE=opt fi; From e413dc2be8818f95c4fe2a43339e589d2e5f9238 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Fri, 12 Aug 2022 08:53:51 +0200 Subject: [PATCH 26/34] erts: Clear fallback flag when stopping select Not clearing it triggered the assert below when stdin was deselected. --- erts/emulator/sys/common/erl_check_io.c | 1 + 1 file changed, 1 insertion(+) diff --git a/erts/emulator/sys/common/erl_check_io.c b/erts/emulator/sys/common/erl_check_io.c index 1b12ed32934d..fe35dbd21102 100644 --- a/erts/emulator/sys/common/erl_check_io.c +++ b/erts/emulator/sys/common/erl_check_io.c @@ -1918,6 +1918,7 @@ erts_check_io(ErtsPollThread *psi, ErtsMonotonicTime timeout_time, int poll_only /* fallthrough */ case ERTS_EV_TYPE_NONE: /* Deselected ... */ case_ERTS_EV_TYPE_NONE: + state->flags &= ~ERTS_EV_FLAG_FALLBACK; ASSERT(!state->events && !state->active_events && !state->flags); check_fd_cleanup(state, &free_select, &free_nif); break; From e30cc88b843e5afcc4a3cb6fd8d305df22af4caa Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Sun, 14 Aug 2022 13:02:11 +0200 Subject: [PATCH 27/34] erts: Handle can be null for statically linked nifs --- erts/emulator/beam/erl_nif.c | 1 - 1 file changed, 1 deletion(-) diff --git a/erts/emulator/beam/erl_nif.c b/erts/emulator/beam/erl_nif.c index c17921d9d4eb..77358f7812dc 100644 --- a/erts/emulator/beam/erl_nif.c +++ b/erts/emulator/beam/erl_nif.c @@ -2274,7 +2274,6 @@ static void close_dynlib(struct erl_module_nif* lib) { ASSERT(lib != NULL); ASSERT(lib->mod == NULL); - ASSERT(lib->handle != NULL); ASSERT(erts_refc_read(&lib->dynlib_refc,0) == 0); if (lib->entry.unload != NULL) { From f306c4842009a60940b06ae36ee8496de14ea52f Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Mon, 15 Aug 2022 16:29:18 +0200 Subject: [PATCH 28/34] ssh: Fix to be compatible with new group interface --- lib/ssh/src/ssh.app.src | 6 +++--- lib/ssh/src/ssh_cli.erl | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/ssh/src/ssh.app.src b/lib/ssh/src/ssh.app.src index 2cb8d8048882..aded3fc06ed1 100644 --- a/lib/ssh/src/ssh.app.src +++ b/lib/ssh/src/ssh.app.src @@ -59,9 +59,9 @@ {mod, {ssh_app, []}}, {runtime_dependencies, [ "crypto-5.0", - "erts-11.0", - "kernel-6.0", + "erts-@OTP-17932@", + "kernel-@OTP-17932@", "public_key-1.6.1", - "stdlib-3.15", + "stdlib-@OTP-17932@", "runtime_tools-1.15.1" ]}]}. diff --git a/lib/ssh/src/ssh_cli.erl b/lib/ssh/src/ssh_cli.erl index 13a44beea3c8..43237b6141cb 100644 --- a/lib/ssh/src/ssh_cli.erl +++ b/lib/ssh/src/ssh_cli.erl @@ -281,6 +281,10 @@ handle_msg({Group, get_unicode_state}, State) -> Group ! {self(), get_unicode_state, false}, {ok, State}; +handle_msg({Group, get_terminal_state}, State) -> + Group ! {self(), get_terminal_state, true}, + {ok, State}; + handle_msg({Group, tty_geometry}, #state{group = Group, pty = Pty } = State) -> @@ -447,7 +451,7 @@ io_request(tty_geometry, Buf, Tty, Group) -> io_request({put_chars_sync, Class, Cs, Reply}, Buf, Tty, Group) -> %% We handle these asynchronous for now, if we need output guarantees %% we have to handle these synchronously - Group ! {reply, Reply}, + Group ! {reply, Reply, ok}, io_request({put_chars, Class, Cs}, Buf, Tty, Group); io_request(_R, Buf, _Tty, _Group) -> From 36ec67ba9db91a32b41a085638866fe2137886b6 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Mon, 15 Aug 2022 16:57:49 +0200 Subject: [PATCH 29/34] stdlib: Polish testcase failures --- lib/kernel/test/Makefile | 1 + lib/kernel/test/application_SUITE.erl | 19 +++++++++---------- lib/kernel/test/interactive_shell_SUITE.erl | 4 ++-- lib/stdlib/test/Makefile | 1 + lib/stdlib/test/io_proto_SUITE.erl | 4 ++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/kernel/test/Makefile b/lib/kernel/test/Makefile index 413349d98a20..e3103220076b 100644 --- a/lib/kernel/test/Makefile +++ b/lib/kernel/test/Makefile @@ -214,6 +214,7 @@ release_tests_spec: make_emakefile $(EMAKEFILE) $(COVERFILE) "$(RELSYSDIR)" chmod -R u+w "$(RELSYSDIR)" @tar cf - *_SUITE_data | (cd "$(RELSYSDIR)"; tar xf -) + $(INSTALL_DIR) "$(RELSYSDIR)/kernel_SUITE_data" $(INSTALL_DATA) $(ERL_TOP)/make/otp_version_tickets "$(RELSYSDIR)/kernel_SUITE_data" release_docs_spec: diff --git a/lib/kernel/test/application_SUITE.erl b/lib/kernel/test/application_SUITE.erl index 019ca3e7f1b1..4bfa07e2d69f 100644 --- a/lib/kernel/test/application_SUITE.erl +++ b/lib/kernel/test/application_SUITE.erl @@ -2191,17 +2191,16 @@ do_configfd_test_bash() -> ok -> case proplists:get_value(system_total_memory, memsup:get_system_memory_data()) of Memory when is_integer(Memory), - Memory > 16*1024*1024*1024 -> + Memory > 8*1024*1024*1024 -> application:stop(os_mon), - true = - ("magic42" =/= - RunInBash( - "erl " - "-noshell " - "-configfd 3 " - "-eval " - "'io:format(\"magic42\"),erlang:halt()' " - "3< <(erl -noshell -eval '(fun W(D) -> io:put_chars(D), W([D,D]) end)(<<\"00000000000000000\">>)') ")); + Res = RunInBash( + "erl " + "-noshell " + "-configfd 3 " + "-eval " + "'io:format(\"magic42\"),erlang:halt()' " + "3< <(erl -noshell -eval '(fun W(D) -> io:put_chars(D), W([D,<<\"00000000000000000\">>]) end)([])') "), + {match, _} = re:run(Res,"Max size 134217728 bytes exceeded"); _ -> io:format("Skipped huge file check to avoid flaky test on machine with less than 8GB of memory") end; diff --git a/lib/kernel/test/interactive_shell_SUITE.erl b/lib/kernel/test/interactive_shell_SUITE.erl index 48b0ef89e51a..bd002e907ff4 100644 --- a/lib/kernel/test/interactive_shell_SUITE.erl +++ b/lib/kernel/test/interactive_shell_SUITE.erl @@ -1967,7 +1967,7 @@ remsh_longnames(Config) when is_list(Config) -> after rtnode:dump_logs(rtnode:stop(SState)) end; - Else -> + {skip, _} = Else -> Else end. @@ -2001,7 +2001,7 @@ remsh_no_epmd(Config) when is_list(Config) -> after rtnode:stop(SState) end; - Else -> + {skip, _} = Else -> Else end. diff --git a/lib/stdlib/test/Makefile b/lib/stdlib/test/Makefile index 0ee9ee6f6d79..0503db2c62ae 100644 --- a/lib/stdlib/test/Makefile +++ b/lib/stdlib/test/Makefile @@ -164,6 +164,7 @@ release_tests_spec: make_emakefile $(ERL_FILES) $(COVERFILE) $(EXTRA_FILES) "$(RELSYSDIR)" chmod -R u+w "$(RELSYSDIR)" @tar cf - *_SUITE_data property_test | (cd "$(RELSYSDIR)"; tar xf -) + $(INSTALL_DIR) "$(RELSYSDIR)/stdlib_SUITE_data" $(INSTALL_DATA) $(ERL_TOP)/make/otp_version_tickets "$(RELSYSDIR)/stdlib_SUITE_data" release_docs_spec: diff --git a/lib/stdlib/test/io_proto_SUITE.erl b/lib/stdlib/test/io_proto_SUITE.erl index f09c71312703..bc96992ce2a3 100644 --- a/lib/stdlib/test/io_proto_SUITE.erl +++ b/lib/stdlib/test/io_proto_SUITE.erl @@ -117,10 +117,10 @@ unicode_prompt(Config) when is_list(Config) -> {putline, "hej"}, {expect, "\\Q\"hej\\n\"\\E"}, {putline, "io:setopts([{binary,true}])."}, - {expect, "[\n ]ok"}, + {expect, "[\n ]\\?*ok"}, {putline, "io:get_line('')."}, {putline, "hej"}, - {expect,"[\n ]hej"}, + {expect,"[\n ]\\?*hej"}, {expect, "\\Q<<\"hej\\n\">>\\E"} ],[],"",["-oldshell","-pa",PA]), ok. From 77835e0743de35a8742e242914f7cb9db3362c56 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Thu, 18 Aug 2022 11:42:37 +0200 Subject: [PATCH 30/34] stdlib: Fix escript SUITE Argument parsing by init is a bit naive. In the escript testcase "-noshell xxxx...." was interpreted as the "-noshell" flag being given the value "xxxx....". So to fix this, we push the "-boot file" argument last and thus "xxxxx" will be seen as a separate argument as it should. --- erts/etc/common/escript.c | 2 +- lib/stdlib/test/escript_SUITE_data/arg_overflow | 2 +- lib/stdlib/test/escript_SUITE_data/linebuf_overflow | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erts/etc/common/escript.c b/erts/etc/common/escript.c index 078937e67698..e418daf43091 100644 --- a/erts/etc/common/escript.c +++ b/erts/etc/common/escript.c @@ -510,8 +510,8 @@ main(int argc, char** argv) PUSH(emulator); PUSH("+B"); - PUSH2("-boot", "no_dot_erlang"); PUSH("-noshell"); + PUSH2("-boot", "no_dot_erlang"); /* * Read options from the %%! row in the script and add them as args diff --git a/lib/stdlib/test/escript_SUITE_data/arg_overflow b/lib/stdlib/test/escript_SUITE_data/arg_overflow index dd5accc05184..e3138cabbdaf 100755 --- a/lib/stdlib/test/escript_SUITE_data/arg_overflow +++ b/lib/stdlib/test/escript_SUITE_data/arg_overflow @@ -1,5 +1,5 @@ #! /usr/bin/env escript %% -*- erlang -*- -%%!x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x +%%!-x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x -x main(_) -> halt(0). diff --git a/lib/stdlib/test/escript_SUITE_data/linebuf_overflow b/lib/stdlib/test/escript_SUITE_data/linebuf_overflow index 33133c1ce903..018be1f26d0a 100755 --- a/lib/stdlib/test/escript_SUITE_data/linebuf_overflow +++ b/lib/stdlib/test/escript_SUITE_data/linebuf_overflow @@ -1,5 +1,5 @@ #! /usr/bin/env escript %% -*- erlang -*- -%%!xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +%%!-v xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx main(_) -> halt(0). From 8895c48498e06533a596cb4bd33a1a01f08175f2 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Tue, 16 Aug 2022 15:39:33 +0200 Subject: [PATCH 31/34] kernel: Fix remote shell edlin expand for jcl remote shells --- lib/kernel/src/user_drv.erl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 98a042716be0..d4ee34d25d61 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -224,11 +224,9 @@ init_remote_shell(State, Node) -> SloganEnv end, - RShellOpts = [{expand_fun,fun(B)-> rpc:call(RemoteNode,edlin_expand,expand,[B]) end}], - RShell = {RemoteNode,shell,start,[]}, Gr = gr_add_cur(State#state.groups, - group:start(self(), RShell, RShellOpts), + group:start(self(), RShell, remsh_opts(RemoteNode)), RShell), init_shell(State#state{ groups = Gr }, [Slogan,$\n]); @@ -571,7 +569,7 @@ 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,[]}, remsh_opts(Node)), Gr = gr_add_cur(Gr0, Pid, {Node,shell,start,[]}), {retry, [], Gr}; false -> @@ -582,7 +580,7 @@ switch_cmd({r, Node}, Gr) when is_atom(Node)-> 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,[]}), + Pid = group:start(self(), {Node,Shell,start,[]}, remsh_opts(Node)), Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}), {retry, [], Gr}; false -> @@ -621,6 +619,9 @@ list_commands() -> QuitReq ++ [{put_chars, unicode,<<" ? | h - this message\n">>}]. +remsh_opts(Node) -> + [{expand_fun,fun(B)-> rpc:call(Node,edlin_expand,expand,[B]) end}]. + -spec io_request(request(), prim_tty:state()) -> {noreply, prim_tty:state()} | {term(), reference(), prim_tty:state()}. io_request({requests,Rs}, TTY) -> From fbc7ff327058a38799abd3258ff793f2b20b9490 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Fri, 26 Aug 2022 13:49:35 +0200 Subject: [PATCH 32/34] ct: Fix peer compilation from unicode path --- lib/common_test/src/test_server.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/common_test/src/test_server.erl b/lib/common_test/src/test_server.erl index 9d893847c2ab..a46394385d85 100644 --- a/lib/common_test/src/test_server.erl +++ b/lib/common_test/src/test_server.erl @@ -2860,7 +2860,7 @@ peer_compile(Erl, cover_compiled, OutDir) -> peer_compile(Erl, ModPath, OutDir) -> {ok, ModSrc} = filelib:find_source(ModPath), Erlc = filename:join(filename:dirname(Erl), "erlc"), - cmd(Erlc, ["-o", OutDir, ModSrc]), + cmd(Erlc, ["-o", OutDir, unicode:characters_to_binary(ModSrc)]), OutDir. %% This should really be implemented as os:cmd. From 75bfdc45786005533f466fa29e25f72489c8599c Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Mon, 29 Aug 2022 11:01:59 +0200 Subject: [PATCH 33/34] erts: Fix so that prim_tty can be reloaded This is needed for when prim_tty is cover compiled. --- erts/emulator/nifs/common/prim_tty_nif.c | 18 +++++++++--------- lib/kernel/src/prim_tty.erl | 3 +-- lib/kernel/src/user_drv.erl | 1 - 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/erts/emulator/nifs/common/prim_tty_nif.c b/erts/emulator/nifs/common/prim_tty_nif.c index 09f762eb392e..83279c832f68 100644 --- a/erts/emulator/nifs/common/prim_tty_nif.c +++ b/erts/emulator/nifs/common/prim_tty_nif.c @@ -996,9 +996,7 @@ static void tty_select_stop(ErlNifEnv* caller_env, void* obj, ErlNifEvent event, #endif } -static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) -{ - +static void load_resources(ErlNifEnv* env, ErlNifResourceFlags rt_flags) { ErlNifResourceTypeInit rt = { NULL /* dtor */, tty_select_stop, @@ -1008,10 +1006,13 @@ static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) ATOMS #undef ATOM_DECL - *priv_data = NULL; - - tty_rt = enif_open_resource_type_x(env, "tty", &rt, ERL_NIF_RT_CREATE, NULL); + tty_rt = enif_open_resource_type_x(env, "tty", &rt, rt_flags, NULL); +} +static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) +{ + *priv_data = NULL; + load_resources(env, ERL_NIF_RT_CREATE); return 0; } @@ -1029,8 +1030,7 @@ static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, if (*priv_data != NULL) { return -1; /* Don't know how to do that */ } - if (load(env, priv_data, load_info)) { - return -1; - } + *priv_data = NULL; + load_resources(env, ERL_NIF_RT_TAKEOVER); return 0; } diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index bd4c73132103..4b522b22395c 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -119,8 +119,7 @@ %% proc_lib exports -export([reader/1, writer/1]). --export([on_load/0]). - +-on_load(on_load/0). %%-define(debug, true). -ifdef(debug). diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index d4ee34d25d61..d9cc8736156a 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -119,7 +119,6 @@ callback_mode() -> state_functions. -spec init(arguments()) -> gen_statem:init_result(init). init(Args) -> process_flag(trap_exit, true), - prim_tty:on_load(), IsTTY = prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true, StartShell = maps:get(initial_shell, Args, undefined) =/= noshell, try From 945b37f2efa00b8dc07b2e2204b79f586b31c9c8 Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Tue, 30 Aug 2022 09:49:10 +0200 Subject: [PATCH 34/34] kernel: Fix loading of prim_tty nif in embedded mode When running in embedded mode the on_load function is called by the init process after the user processes are started. So we add a way for user to call the on_load function earlier in the boot sequence so that the shell can be started when it should. We cannot move the on_load calls for all modules to an earlier place in the kernel boot sequence as the code in on_load may depend on kernel services being started. --- erts/preloaded/ebin/init.beam | Bin 61568 -> 62408 bytes erts/preloaded/src/init.erl | 20 +++++++++++++++----- lib/kernel/src/user_drv.erl | 3 +++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erts/preloaded/ebin/init.beam b/erts/preloaded/ebin/init.beam index 98fa6e42b06193c6c9934f312f1006a250241b16..14d0c8eac2857690c471a479c1f48facf035987d 100644 GIT binary patch delta 39747 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 delta 38947 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 %%% run_on_load_handlers() -> + run_on_load_handlers(all). + +run_on_load_handlers(Mods) when is_list(Mods); Mods =:= all -> Ref = monitor(process, ?ON_LOAD_HANDLER), - catch ?ON_LOAD_HANDLER ! run_on_load, + catch ?ON_LOAD_HANDLER ! {run_on_load, self(), Ref, Mods}, receive {'DOWN',Ref,process,_,noproc} -> %% There is no on_load handler process, @@ -1477,7 +1480,9 @@ run_on_load_handlers() -> {'DOWN',Ref,process,_,Res} -> %% Failure to run an on_load handler. %% This is fatal during start-up. - exit(Res) + exit(Res); + {reply, Ref, on_load_done} -> + ok end. start_on_load_handler_process() -> @@ -1493,9 +1498,14 @@ on_load_loop(Mods, Debug0) -> on_load_loop(Mods, Debug); {loaded,Mod} -> on_load_loop([Mod|Mods], Debug0); - run_on_load -> + {run_on_load, _, _, all} -> run_on_load_handlers(Mods, Debug0), - exit(on_load_done) + exit(on_load_done); + {run_on_load, From, Ref, ModsToRun} -> + [run_on_load_handlers([Mod], Debug0) + || Mod <- ModsToRun, lists:member(Mod, Mods)], + From ! {reply, Ref, on_load_done}, + on_load_loop(Mods -- ModsToRun, Debug0) end. run_on_load_handlers([M|Ms], Debug) -> diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index d9cc8736156a..1a7575755758 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -119,6 +119,9 @@ callback_mode() -> state_functions. -spec init(arguments()) -> gen_statem:init_result(init). init(Args) -> process_flag(trap_exit, true), + %% When running in embedded mode we need to call prim_tty:on_load manually here + %% as the automatic call happens after user is started. + ok = init:run_on_load_handlers([prim_tty]), IsTTY = prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true, StartShell = maps:get(initial_shell, Args, undefined) =/= noshell, try