From ac9c093f49cddbd8ecdf0877577a1b5641b65a2d Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 11 Jan 2025 17:42:50 +0100 Subject: [PATCH] Distribution: add support for autoconnect Signed-off-by: Paul Guyot --- libs/estdlib/src/dist_util.erl | 94 ++++++++++- libs/estdlib/src/net_kernel.erl | 26 ++- src/libAtomVM/defaultatoms.def | 7 +- src/libAtomVM/dist_nifs.c | 152 +++++++++++++++--- src/libAtomVM/dist_nifs.h | 12 +- src/libAtomVM/opcodesswitch.h | 9 +- .../lib/platform_defaultatoms.def | 1 - tests/libs/estdlib/test_net_kernel.erl | 32 ++++ 8 files changed, 292 insertions(+), 41 deletions(-) diff --git a/libs/estdlib/src/dist_util.erl b/libs/estdlib/src/dist_util.erl index 087e46b8f..52e59d7ba 100644 --- a/libs/estdlib/src/dist_util.erl +++ b/libs/estdlib/src/dist_util.erl @@ -229,7 +229,27 @@ handshake_other_started(#hs_data{socket = Socket, f_recv = Recv} = HSData0) -> end. -spec handshake_we_started(#hs_data{}) -> no_return(). -handshake_we_started(#hs_data{}) -> ok. +handshake_we_started(#hs_data{} = HSData0) -> + HSData1 = HSData0#hs_data{ + other_started = false, + this_flags = ?MANDATORY_DFLAGS + }, + send_name(HSData1), + case recv_status(HSData1) of + <<"ok">> -> ok; + <<"ok_simultaneous">> -> ok; + <<"nok">> -> ?shutdown({HSData1#hs_data.other_node, simultaneous}); + <<"alive">> -> send_status(<<"true">>, HSData1); + Other -> ?shutdown({HSData1#hs_data.other_node, {unexpected, Other}}) + end, + Cookie = net_kernel:get_cookie(HSData1#hs_data.other_node), + {OtherChallenge, OtherFlags, Creation} = recv_challenge(HSData1), + check_flags(OtherFlags, HSData1), + <> = crypto:strong_rand_bytes(4), + send_challenge_reply(Cookie, OtherChallenge, MyChallenge, HSData1), + OtherDigest = recv_challenge_ack(HSData1), + check_challenge(Cookie, MyChallenge, OtherDigest, HSData1), + connection(HSData1, Creation). % We are connected -spec connection(#hs_data{}, non_neg_integer()) -> no_return(). @@ -359,6 +379,20 @@ check_flags(Flags0, HSData) -> ?shutdown(Reason) end. +% send name +send_name( + #hs_data{socket = Socket, f_send = Send, this_node = ThisNode, this_flags = ThisFlags} = HSData +) -> + Creation = atomvm:get_creation(), + NodeName = atom_to_binary(ThisNode, latin1), + NameLen = byte_size(NodeName), + case Send(Socket, <<$N, ThisFlags:64, Creation:32, NameLen:16, NodeName/binary>>) of + {error, _} = Error -> + ?shutdown2({HSData#hs_data.other_node, Socket}, {send_name_failed, Error}); + ok -> + ok + end. + % Ensure name is somewhat valid -spec check_name(binary()) -> ok. check_name(Name) -> @@ -378,13 +412,13 @@ send_status(Status, #hs_data{socket = Socket, f_send = Send} = HSData) -> ok end. --spec recv_status_reply(#hs_data{}) -> binary(). -recv_status_reply(#hs_data{socket = Socket, f_recv = Recv} = HSData) -> +-spec recv_status(#hs_data{}) -> binary(). +recv_status(#hs_data{socket = Socket, f_recv = Recv} = HSData) -> case Recv(Socket, 0, infinity) of {ok, <<$s, Result/binary>>} -> Result; {ok, Other} -> - ?shutdown({HSData#hs_data.other_node, {unexpected, recv_status_reply, Other}}); + ?shutdown({HSData#hs_data.other_node, {unexpected, recv_status, Other}}); {error, Reason} -> ?shutdown2({HSData#hs_data.other_node, recv_error}, Reason) end. @@ -403,7 +437,7 @@ mark_pending(#hs_data{kernel_pid = Kernel, this_node = ThisNode, other_node = Ot alive -> send_status(<<"alive">>, HSData), reset_timer(HSData#hs_data.timer), - case recv_status_reply(HSData) of + case recv_status(HSData) of <<"true">> -> ok; <<"false">> -> ?shutdown(OtherNode); Other -> ?shutdown({OtherNode, {unexpected, Other}}) @@ -434,6 +468,28 @@ send_challenge( ok end. +recv_challenge( + #hs_data{other_node = OtherNode, socket = Socket, f_recv = Recv} = HSData +) -> + case Recv(Socket, 0, infinity) of + {ok, << + $N, OtherFlags:64, Challenge:32, OtherCreation:32, _OtherNameLen:16, OtherName/binary + >>} -> + case atom_to_binary(OtherNode, utf8) =/= OtherName of + true -> + ?shutdown({ + HSData#hs_data.other_node, {mismatch, recv_challenge, OtherNode, OtherName} + }); + false -> + ok + end, + {Challenge, OtherFlags, OtherCreation}; + {ok, Other} -> + ?shutdown({HSData#hs_data.other_node, {unexpected, recv_challenge, Other}}); + {error, Reason} -> + ?shutdown2({HSData#hs_data.other_node, recv_error}, Reason) + end. + -spec recv_challenge_reply(#hs_data{}) -> {non_neg_integer(), binary()}. recv_challenge_reply(#hs_data{socket = Socket, f_recv = Recv} = HSData) -> case Recv(Socket, 0, infinity) of @@ -445,6 +501,23 @@ recv_challenge_reply(#hs_data{socket = Socket, f_recv = Recv} = HSData) -> ?shutdown2({HSData#hs_data.other_node, recv_error}, Reason) end. +-spec send_challenge_reply( + Cookie :: binary(), + OtherChallenge :: non_neg_integer(), + MyChallenge :: non_neg_integer(), + #hs_data{} +) -> ok. +send_challenge_reply( + Cookie, OtherChallenge, MyChallenge, #hs_data{socket = Socket, f_send = Send} = HSData +) -> + Digest = gen_digest(Cookie, OtherChallenge), + case Send(Socket, <<$r, MyChallenge:32, Digest:16/binary>>) of + {error, _} = Error -> + ?shutdown2({HSData#hs_data.other_node, Socket}, {send_challenge_reply_failed, Error}); + ok -> + ok + end. + -spec check_challenge( Cookie :: binary(), Challenge :: non_neg_integer(), Digest :: binary(), #hs_data{} ) -> ok. @@ -470,6 +543,17 @@ send_challenge_ack(Cookie, Challenge, #hs_data{socket = Socket, f_send = Send} = ok end. +-spec recv_challenge_ack(#hs_data{}) -> binary(). +recv_challenge_ack(#hs_data{socket = Socket, f_recv = Recv} = HSData) -> + case Recv(Socket, 0, infinity) of + {ok, <<$a, Digest/binary>>} -> + Digest; + {ok, Other} -> + ?shutdown({HSData#hs_data.other_node, {unexpected, recv_challenge_ack, Other}}); + {error, _} = Error -> + ?shutdown2({HSData#hs_data.other_node, Socket}, {recv_challenge_ack, Error}) + end. + -spec shutdown(atom(), non_neg_integer(), term()) -> no_return(). shutdown(Module, Line, Data) -> shutdown(Module, Line, Data, shutdown). diff --git a/libs/estdlib/src/net_kernel.erl b/libs/estdlib/src/net_kernel.erl index f47288aaa..1c079ebc8 100644 --- a/libs/estdlib/src/net_kernel.erl +++ b/libs/estdlib/src/net_kernel.erl @@ -216,13 +216,17 @@ handle_call( case maps:find(OtherNode, Connections) of error -> {reply, ok, State0#state{ - connections = maps:put(OtherNode, {pending, ConnPid}, Connections) + connections = maps:put(OtherNode, {pending, ConnPid, undefined}, Connections) }}; - {ok, {pending, OtherConnPid}} when OtherNode > ThisNode -> + {ok, {pending, undefined, DHandle}} -> + {reply, ok, State0#state{ + connections = maps:put(OtherNode, {pending, ConnPid, DHandle}, Connections) + }}; + {ok, {pending, OtherConnPid, DHandle}} when OtherNode > ThisNode -> {reply, {ok_simultaneous, OtherConnPid}, State0#state{ - connections = maps:update(OtherNode, {pending, ConnPid}, Connections) + connections = maps:update(OtherNode, {pending, ConnPid, DHandle}, Connections) }}; - {ok, {pending, _OtherConnPid}} -> + {ok, {pending, _OtherConnPid, _DHandle}} -> {reply, nok, State0}; {ok, {alive, _ConnPid, _Address}} -> {reply, alive, State0} @@ -294,12 +298,24 @@ handle_info({'EXIT', Pid, _Reason}, #state{connections = Connections} = State) - fun(_Node, Status) -> case Status of {alive, Pid, _Address} -> false; - {pending, Pid} -> false; + {pending, Pid, _DHandle} -> false; _ -> true end end, Connections ), + {noreply, State#state{connections = NewConnections}}; +handle_info({connect, OtherNode, DHandle}, #state{connections = Connections, node = MyNode, longnames = Longnames, proto_dist = ProtoDist} = State) -> + % ensure DHandle is not garbage collected until setup failed or succeeded + NewConnections = case maps:find(OtherNode, Connections) of + error -> + ProtoDist:setup(OtherNode, normal, MyNode, Longnames, ?SETUPTIME), + maps:put(OtherNode, {pending, undefined, DHandle}, Connections); + {ok, {pending, ConnPid, _}} -> + maps:put(OtherNode, {pending, ConnPid, DHandle}, Connections); + {ok, {alive, _ConnPid, _Address}} -> + Connections + end, {noreply, State#state{connections = NewConnections}}. %% @hidden diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 8467df467..798ca17e3 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -180,9 +180,4 @@ X(TIMEOUT_ATOM, "\x7", "timeout") X(DIST_DATA_ATOM, "\x9", "dist_data") X(REQUEST_ATOM, "\x7", "request") -X(REPLY_TAG_ATOM, "\x9", "reply_tag") -X(SPAWN_REPLY_ATOM, "\xB", "spawn_reply") -X(REPLY_ATOM, "\x5", "reply") -X(YES_ATOM, "\x3", "yes") -X(NO_ATOM, "\x2", "no") -X(ERROR_ONLY_ATOM, "\xA", "error_only") +X(CONNECT_ATOM, "\x7", "connect") diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index 1b11bfcdd..e023269e2 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -167,6 +167,19 @@ static void dist_enqueue_message(term control_message, term payload, struct Dist } } +static void dist_enqueue_reg_send_message(int32_t local_process_id, term remote_process_name, term payload, struct DistConnection *connection, GlobalContext *global) +{ + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(4), heap) + term control_message = term_alloc_tuple(4, &heap); + term_put_tuple_element(control_message, 0, term_from_int(OPERATION_REG_SEND)); + term_put_tuple_element(control_message, 1, term_from_local_process_id(local_process_id)); + term_put_tuple_element(control_message, 2, term_nil()); // unused + term_put_tuple_element(control_message, 3, remote_process_name); + + dist_enqueue_message(control_message, payload, connection, global); + END_WITH_STACK_HEAP(heap, global) +} + static void dist_enqueue_send_sender_message(int32_t local_process_id, term remote_process_id, term payload, struct DistConnection *connection, GlobalContext *global) { BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(3), heap) @@ -255,27 +268,39 @@ static term nif_erlang_setnode_3(Context *ctx, int argc, term argv[]) uint32_t creation = term_maybe_unbox_int(term_get_tuple_element(argv[2], 1)); int node_atom_index = term_to_atom_index(argv[0]); + struct DistConnection *conn_obj = NULL; + // Ensure we don't already know this node. struct ListHead *dist_connections = synclist_wrlock(&ctx->global->dist_connections); struct ListHead *item; LIST_FOR_EACH (item, dist_connections) { struct DistConnection *dist_connection = GET_LIST_ENTRY(item, struct DistConnection, head); - if (dist_connection->node_atom_index == node_atom_index && dist_connection->node_creation == creation) { - synclist_unlock(&ctx->global->dist_connections); - RAISE_ERROR(BADARG_ATOM); + if (dist_connection->node_atom_index == node_atom_index) { + if (dist_connection->connection_process_id == INVALID_PROCESS_ID) { + conn_obj = dist_connection; + break; + } else if (dist_connection->node_creation == creation) { + synclist_unlock(&ctx->global->dist_connections); + RAISE_ERROR(BADARG_ATOM); + } } } // Create a resource object - struct DistConnection *conn_obj = enif_alloc_resource(ctx->global->dist_connection_resource_type, sizeof(struct DistConnection)); - if (IS_NULL_PTR(conn_obj)) { - synclist_unlock(&ctx->global->dist_connections); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); + if (conn_obj == NULL) { + conn_obj = enif_alloc_resource(ctx->global->dist_connection_resource_type, sizeof(struct DistConnection)); + if (IS_NULL_PTR(conn_obj)) { + synclist_unlock(&ctx->global->dist_connections); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + conn_obj->node_atom_index = node_atom_index; + synclist_init(&conn_obj->remote_monitors); + synclist_init(&conn_obj->pending_packets); + list_prepend(dist_connections, &conn_obj->head); } - conn_obj->node_atom_index = node_atom_index; + + // Finish initialization if connection was created for auto connect conn_obj->node_creation = creation; - synclist_init(&conn_obj->remote_monitors); - synclist_init(&conn_obj->pending_packets); ErlNifEnv *env = erl_nif_env_from_context(ctx); conn_obj->connection_process_id = term_to_local_process_id(argv[1]); @@ -283,7 +308,6 @@ static term nif_erlang_setnode_3(Context *ctx, int argc, term argv[]) synclist_unlock(&ctx->global->dist_connections); RAISE_ERROR(BADARG_ATOM); } - list_prepend(dist_connections, &conn_obj->head); synclist_unlock(&ctx->global->dist_connections); // Increment reference count as the resource should be alive until controller process dies @@ -307,8 +331,11 @@ static term nif_erlang_dist_ctrl_get_data_notification(Context *ctx, int argc, t struct DistConnection *conn_obj = (struct DistConnection *) rsrc_obj_ptr; struct ListHead *pending_packets = synclist_wrlock(&conn_obj->pending_packets); - UNUSED(pending_packets); // without SMP, this would appear as a statement with no effect - conn_obj->selecting_process_id = ctx->process_id; + if (!list_is_empty(pending_packets)) { + globalcontext_send_message(ctx->global, ctx->process_id, DIST_DATA_ATOM); + } else { + conn_obj->selecting_process_id = ctx->process_id; + } synclist_unlock(&conn_obj->pending_packets); return OK_ATOM; @@ -454,6 +481,19 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) synclist_unlock(&conn_obj->remote_monitors); break; } + case OPERATION_SEND_SENDER: { + if (UNLIKELY(arity != 3)) { + RAISE_ERROR(BADARG_ATOM); + } + term target = term_get_tuple_element(control, 2); + if (UNLIKELY(!term_is_local_pid(target))) { + RAISE_ERROR(BADARG_ATOM); + } + int target_process_id = term_to_local_process_id(target); + term payload = externalterm_to_term_with_roots(data + 1 + bytes_read, binary_len - 1 - bytes_read, ctx, ExternalTermCopy, &bytes_read, 2, argv); + globalcontext_send_message(ctx->global, target_process_id, payload); + break; + } case OPERATION_SPAWN_REQUEST: { if (UNLIKELY(arity != 6)) { RAISE_ERROR(BADARG_ATOM); @@ -509,21 +549,95 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) return OK_ATOM; } -void dist_send_message(term external_pid, term payload, Context *ctx) +term dist_send_message(term target, term payload, Context *ctx) { + if (UNLIKELY(!term_is_external_pid(target) && !term_is_tuple(target))) { + RAISE_ERROR(BADARG_ATOM); + } + + int node_atom_index; + uint32_t node_creation; + term registered_name_atom = term_invalid_term(); + if (term_is_external_pid(target)) { + node_atom_index = term_to_atom_index(term_get_external_node(target)); + node_creation = term_get_external_node_creation(target); + } else { + if (UNLIKELY(term_get_tuple_arity(target) != 2)) { + RAISE_ERROR(BADARG_ATOM); + } + registered_name_atom = term_get_tuple_element(target, 0); + term node_atom = term_get_tuple_element(target, 1); + if (UNLIKELY(!term_is_atom(registered_name_atom) || !term_is_atom(node_atom))) { + RAISE_ERROR(BADARG_ATOM); + } + node_atom_index = term_to_atom_index(node_atom); + } + // Search for dhandle. - int node_atom_index = term_to_atom_index(term_get_external_node(external_pid)); - uint32_t node_creation = term_get_external_node_creation(external_pid); struct ListHead *dist_connections = synclist_rdlock(&ctx->global->dist_connections); struct ListHead *item; LIST_FOR_EACH (item, dist_connections) { struct DistConnection *dist_connection = GET_LIST_ENTRY(item, struct DistConnection, head); - if (dist_connection->node_atom_index == node_atom_index && dist_connection->node_creation == node_creation) { - dist_enqueue_send_sender_message(ctx->process_id, external_pid, payload, dist_connection, ctx->global); - break; + if (dist_connection->node_atom_index == node_atom_index) { + if (!term_is_invalid_term(registered_name_atom)) { + dist_enqueue_reg_send_message(ctx->process_id, registered_name_atom, payload, dist_connection, ctx->global); + synclist_unlock(&ctx->global->dist_connections); + return payload; + } else if (dist_connection->node_creation == node_creation) { + dist_enqueue_send_sender_message(ctx->process_id, target, payload, dist_connection, ctx->global); + synclist_unlock(&ctx->global->dist_connections); + return payload; + } else { + // creation doesn't match, but we don't need to connect + // to a node with the proper creation + synclist_unlock(&ctx->global->dist_connections); + return payload; + } } } + + // We're not connected to the node + // To ensure that signals are delivered in order, we add the entry in the + // list of connections now (while we're holding the lock), and we enqueue + // the message. Then we also trigger a connection, which, if it fails, + // will remove the entry and purge the pending list of messages. + + // Create a resource object + struct DistConnection *new_conn_obj = enif_alloc_resource(ctx->global->dist_connection_resource_type, sizeof(struct DistConnection)); + if (IS_NULL_PTR(new_conn_obj)) { + synclist_unlock(&ctx->global->dist_connections); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + new_conn_obj->node_atom_index = node_atom_index; + new_conn_obj->node_creation = 0; + new_conn_obj->connection_process_id = INVALID_PROCESS_ID; + synclist_init(&new_conn_obj->remote_monitors); + synclist_init(&new_conn_obj->pending_packets); + list_prepend(dist_connections, &new_conn_obj->head); + + // Enqueue message + if (!term_is_external_pid(target)) { + dist_enqueue_reg_send_message(ctx->process_id, registered_name_atom, payload, new_conn_obj, ctx->global); + } else { + dist_enqueue_send_sender_message(ctx->process_id, target, payload, new_conn_obj, ctx->global); + } + + // We can unlock list now synclist_unlock(&ctx->global->dist_connections); + + // Eventually, tell kernel to connect + int net_kernel_pid = globalcontext_get_registered_process(ctx->global, NET_KERNEL_ATOM_INDEX); + if (net_kernel_pid) { + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(3) + TERM_BOXED_RESOURCE_SIZE, heap) + term autoconnect_message = term_alloc_tuple(3, &heap); + term_put_tuple_element(autoconnect_message, 0, CONNECT_ATOM); + term_put_tuple_element(autoconnect_message, 1, term_from_atom_index(node_atom_index)); + term_put_tuple_element(autoconnect_message, 2, term_from_resource(new_conn_obj, &heap)); + + globalcontext_send_message(ctx->global, net_kernel_pid, autoconnect_message); + END_WITH_STACK_HEAP(heap, ctx->global) + } + return payload; } void dist_spawn_reply(term req_id, term to_pid, bool link, bool monitor, term result, struct DistConnection *connection, GlobalContext *global) diff --git a/src/libAtomVM/dist_nifs.h b/src/libAtomVM/dist_nifs.h index 0a990d696..32db969f4 100644 --- a/src/libAtomVM/dist_nifs.h +++ b/src/libAtomVM/dist_nifs.h @@ -43,7 +43,17 @@ extern const struct Nif dist_ctrl_put_data_nif; struct DistConnection; -void dist_send_message(term external_pid, term payload, Context *ctx); +/** + * @doc Enqueue a message to be sent to a remote process. + * This function may raise a badarg error following OTP if target is incorrect. + * @param target external pid or a tuple {atom(), node()} to refer to a remote + * registered process + * @param payload message to send + * @param ctx process that sends the message. + * @return the payload if the message was sent or term_invalid if there was + * a badarg error + */ +term dist_send_message(term target, term payload, Context *ctx); /** * @doc Setup a monitor on a local process for a distributed process. diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 11d12f23e..eee4ba446 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2404,8 +2404,10 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) #ifdef IMPL_EXECUTE_LOOP term recipient_term = x_regs[0]; - if (UNLIKELY(term_is_external_pid(recipient_term))) { - dist_send_message(recipient_term, x_regs[1], ctx); + if (UNLIKELY(term_is_external_pid(recipient_term) || term_is_tuple(recipient_term))) { + term return_value = dist_send_message(recipient_term, x_regs[1], ctx); + PROCESS_MAYBE_TRAP_RETURN_VALUE(return_value); + x_regs[0] = return_value; } else { int local_process_id; if (term_is_local_pid_or_port(recipient_term)) { @@ -2421,9 +2423,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE("send/0 target_pid=%i\n", local_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); + x_regs[0] = x_regs[1]; } - - x_regs[0] = x_regs[1]; #endif break; } diff --git a/src/platforms/generic_unix/lib/platform_defaultatoms.def b/src/platforms/generic_unix/lib/platform_defaultatoms.def index 9e605062c..d07f02b94 100644 --- a/src/platforms/generic_unix/lib/platform_defaultatoms.def +++ b/src/platforms/generic_unix/lib/platform_defaultatoms.def @@ -40,7 +40,6 @@ X(BUFFER_ATOM, "\x6", "buffer") X(GETADDRINFO_ATOM, "\xB", "getaddrinfo") X(NO_SUCH_HOST_ATOM, "\xC", "no_such_host") -X(CONNECT_ATOM, "\x7", "connect") X(TCP_CLOSED_ATOM, "\xA", "tcp_closed") X(LISTEN_ATOM, "\x6", "listen") diff --git a/tests/libs/estdlib/test_net_kernel.erl b/tests/libs/estdlib/test_net_kernel.erl index 43127f8f1..baa5b1433 100644 --- a/tests/libs/estdlib/test_net_kernel.erl +++ b/tests/libs/estdlib/test_net_kernel.erl @@ -35,6 +35,8 @@ test() -> ok = test_fail_with_wrong_cookie(Platform), ok = test_rpc_from_beam(Platform), ok = test_rpc_loop_from_beam(Platform), + ok = test_autoconnect_fail(Platform), + ok = test_autoconnect_to_beam(Platform), ok; false -> io:format("~s: skipped\n", [?MODULE]), @@ -122,6 +124,36 @@ test_rpc_loop_from_beam(Platform) -> net_kernel:stop(), ok. +test_autoconnect_fail(Platform) -> + {ok, _NetKernelPid} = net_kernel_start(Platform, atomvm), + Node = node(), + erlang:set_cookie(Node, 'AtomVM'), + [_, Host] = string:split(atom_to_list(Node), "@"), + OTPNode = list_to_atom("otp@" ++ Host), + {beam, OTPNode} ! {self(), ping}, + net_kernel:stop(), + ok. + +test_autoconnect_to_beam(Platform) -> + {ok, _NetKernelPid} = net_kernel_start(Platform, atomvm), + Node = node(), + erlang:set_cookie(Node, 'AtomVM'), + spawn_link(fun() -> + execute_command( + Platform, + "erl -sname otp -setcookie AtomVM -eval \"register(beam, self()), receive {Caller, ping} -> Caller ! {self(), pong} after 5000 -> timeout end.\" -s init stop -noshell" + ) + end), + % Wait sufficiently for beam to be up, without connecting to it since + % that's part of the test + timer:sleep(1000), + [_, Host] = string:split(atom_to_list(Node), "@"), + OTPNode = list_to_atom("otp@" ++ Host), + {beam, OTPNode} ! {self(), ping}, + ok = receive {_Pid, pong} -> ok after 5000 -> timeout end, + net_kernel:stop(), + ok. + % On AtomVM, we need to start kernel. setup("BEAM") -> ok;