diff --git a/backend/lib/backend/cluster.ex b/backend/lib/backend/cluster.ex index eacbdcb..5149662 100644 --- a/backend/lib/backend/cluster.ex +++ b/backend/lib/backend/cluster.ex @@ -9,6 +9,15 @@ defmodule Backend.Cluster do def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end + + def kill_node(node_name) when is_binary(node_name) do + kill_node(String.to_existing_atom(node_name)) + end + + def kill_node(target) when is_atom(target) do + Logger.warning("Killing node: #{target}") + :rpc.cast(target, :init, :stop, []) + end @impl true def init(_opts) do @@ -62,4 +71,5 @@ defmodule Backend.Cluster do # Retry connection every 10 seconds Process.send_after(self(), :periodic_connect, 10_000) end + end diff --git a/backend/lib/backend/global_singleton.ex b/backend/lib/backend/global_singleton.ex index a8ec947..124c8d9 100644 --- a/backend/lib/backend/global_singleton.ex +++ b/backend/lib/backend/global_singleton.ex @@ -52,7 +52,6 @@ defmodule Backend.GlobalSingleton do @impl true def handle_info({:startup_args_updated, new_args}, state) do - Logger.info("Received updated startup args for #{state.module}: #{inspect(new_args)}") {:noreply, %{state | startup_args: new_args}} end diff --git a/backend/lib/backend_web/channels/cluster_status_channel.ex b/backend/lib/backend_web/channels/cluster_status_channel.ex index 381546e..a7ef7cf 100644 --- a/backend/lib/backend_web/channels/cluster_status_channel.ex +++ b/backend/lib/backend_web/channels/cluster_status_channel.ex @@ -6,11 +6,25 @@ defmodule BackendWeb.ClusterStatusChannel do require Logger @impl true - def join("clusterstatus", _params, socket) do + def join("cluster_status", _params, socket) do Logger.info("Client joined clusterstatus channel") {:ok, %{status: "connected"}, socket} end + @impl true + def handle_in("get_nodes", _payload, socket) do + Logger.info("Client requested node list #{inspect(Node.list())}") + push(socket, "node_list", %{other_nodes: Node.list(), connected_node: node()}) + {:noreply, socket} + end + + @impl true + def handle_in("kill_node", %{"node" => node_to_kill}, socket) do + Logger.warning("Client requested to kill node: #{node_to_kill}") + Backend.Cluster.kill_node(node_to_kill) + {:noreply, socket} + end + @impl true def handle_in(_event, _payload, socket) do {:noreply, socket} diff --git a/backend/lib/backend_web/channels/connected_user_channel.ex b/backend/lib/backend_web/channels/connected_user_channel.ex index 0208af1..0644f2d 100644 --- a/backend/lib/backend_web/channels/connected_user_channel.ex +++ b/backend/lib/backend_web/channels/connected_user_channel.ex @@ -47,6 +47,8 @@ defmodule BackendWeb.ConnectedUserChannel do end @impl true + @spec handle_in(<<_::48, _::_*8>>, map(), any()) :: + {:noreply, any()} | {:reply, :ok, Phoenix.Socket.t()} def handle_in("join_game", %{"name" => name}, socket) do Logger.info("Player '#{name}' joining game on #{node()}") diff --git a/backend/lib/backend_web/channels/user_socket.ex b/backend/lib/backend_web/channels/user_socket.ex index d8dcc9a..4368ecb 100644 --- a/backend/lib/backend_web/channels/user_socket.ex +++ b/backend/lib/backend_web/channels/user_socket.ex @@ -2,10 +2,9 @@ defmodule BackendWeb.UserSocket do use Phoenix.Socket require Logger - ## Channels channel("user:*", BackendWeb.ConnectedUserChannel) - channel("clusterstatus", BackendWeb.ClusterStatusChannel) + channel("cluster_status", BackendWeb.ClusterStatusChannel) @impl true def connect(%{"user_name" => user_name}, socket, _connect_info) do diff --git a/client/src/clusterStatus/ClusterStatus.tsx b/client/src/clusterStatus/ClusterStatus.tsx index 99c42df..af90cbb 100644 --- a/client/src/clusterStatus/ClusterStatus.tsx +++ b/client/src/clusterStatus/ClusterStatus.tsx @@ -1,16 +1,21 @@ import { useEffect, useState } from "react"; import { useWebSocketContext } from "../contexts/useWebSocketContext"; +import type { Channel } from "phoenix"; export const ClusterStatus = () => { const { socket, isConnected } = useWebSocketContext(); const [channelStatus, setChannelStatus] = useState("waiting"); + const [channel, setChannel] = useState(); + const [clusterStatus, setClusterStatus] = useState< + { otherNodes: string[]; connectedNode: string } | undefined + >(); useEffect(() => { if (!socket || !isConnected) { return; } - const channelName = "clusterstatus"; + const channelName = "cluster_status"; console.log(`Joining channel: ${channelName}`); const newChannel = socket.channel(channelName, {}); @@ -18,6 +23,8 @@ export const ClusterStatus = () => { .join() .receive("ok", () => { setChannelStatus("connected"); + setChannel(newChannel); + newChannel.push("get_nodes", {}); }) .receive("error", (resp: unknown) => { console.log(`Failed to join channel ${channelName}:`, resp); @@ -27,17 +34,66 @@ export const ClusterStatus = () => { setChannelStatus("timeout"); }); + newChannel.on( + "node_list", + (payload: { other_nodes: string[]; connected_node: string }) => { + console.log("Received node list:", payload.other_nodes); + setClusterStatus({ + otherNodes: payload.other_nodes, + connectedNode: payload.connected_node, + }); + }, + ); + return () => { console.log(`Leaving channel: ${channelName}`); newChannel.leave(); + setChannel(undefined); setChannelStatus("waiting"); }; }, [socket, isConnected]); + const allNodes = clusterStatus + ? [...clusterStatus.otherNodes, clusterStatus.connectedNode].sort() + : []; + return (
ClusterStatus
Channel: {channelStatus}
+
+ {allNodes.length === 0 ? ( + No nodes available + ) : ( + <> + {allNodes.map((node) => { + const isConnected = node === clusterStatus?.connectedNode; + return ( +
+ {node} + {isConnected && ( + (you) + )} + +
+ ); + })} + + )} +
); -} +};