From 2844eecb65501f7dafa4de15d7377bfb810e1158 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 18 Dec 2024 13:36:52 -0500 Subject: Make IPC opt-in and configurable --- src/ipc_state.cpp | 164 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 135 insertions(+), 29 deletions(-) (limited to 'src/ipc_state.cpp') diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp index c0bdc9b..24d0115 100644 --- a/src/ipc_state.cpp +++ b/src/ipc_state.cpp @@ -15,18 +15,21 @@ #include #include +#include "ap_state.h" #include "logger.h" #include "tracker_frame.h" namespace { -constexpr const char* kIpcAddress = "ws://127.0.0.1:41253"; - struct IPCState { std::mutex state_mutex; TrackerFrame* tracker_frame = nullptr; // Protected state + bool initialized = false; + std::string address; + bool should_disconnect = false; + std::optional status_message; bool slot_matches = false; @@ -38,16 +41,27 @@ struct IPCState { std::optional> player_position; std::set solved_panels; - int backoff_amount = 0; - // Thread state std::unique_ptr ws; bool connected = false; - void Start(TrackerFrame* frame) { - tracker_frame = frame; + void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; } + + void Connect(std::string a) { + // This is the main concurrency concern, as it mutates protected state in an + // important way. Thread() is documented with how it interacts with this + // function. + std::lock_guard state_guard(state_mutex); + + if (!initialized) { + std::thread([this]() { Thread(); }).detach(); - std::thread([this]() { Thread(); }).detach(); + initialized = true; + } else if (address != a) { + should_disconnect = true; + } + + address = a; } std::optional GetStatusMessage() { @@ -57,6 +71,13 @@ struct IPCState { } void SetTrackerSlot(std::string server, std::string user) { + // This is function is called from the APState thread, not the main thread, + // and it mutates protected state. It only really competes with OnMessage(), + // when a "Connect" message is received. If this is called right before, + // and the tracker slot does not match the old game slot, it will initiate a + // disconnect, and then the OnMessage() handler will see should_disconnect + // and stop processing the "Connect" message. If this is called right after + // and the slot does not match, IPC will disconnect, which is tolerable. std::lock_guard state_guard(state_mutex); tracker_ap_server = std::move(server); @@ -64,7 +85,10 @@ struct IPCState { CheckIfSlotMatches(); - backoff_amount = 0; + if (!slot_matches) { + should_disconnect = true; + address.clear(); + } } bool IsConnected() { @@ -88,54 +112,126 @@ struct IPCState { private: void Thread() { for (;;) { - player_position = std::nullopt; - - TrackerLog("Looking for game over IPC..."); + SetStatusMessage("Disconnected from game."); + // initialized is definitely true because it is set to true when the thread + // is created and only set to false within this block, when the thread is + // killed. Thus, a call to Connect would always at most set + // should_disconnect and address. If this happens before this block, it is + // as if we are starting from a new thread anyway because should_disconnect + // is immediately reset. If a call to Connect happens after this block, + // then a connection attempt will be made to the wrong address, but the + // thread will grab the mutex right after this and back out the wrong + // connection. + std::string ipc_address; { std::lock_guard state_guard(state_mutex); - backoff_amount = 0; - } - while (!TryConnect() || !connected) { - int backoff_limit; - { - std::lock_guard state_guard(state_mutex); - backoff_limit = (backoff_amount + 1) * 10; + should_disconnect = false; + + slot_matches = false; + game_ap_server.clear(); + game_ap_user.clear(); + + player_position = std::nullopt; + solved_panels.clear(); + + if (address.empty()) { + initialized = false; + return; } + ipc_address = address; + } + + int backoff_amount = 0; + + SetStatusMessage("Connecting to game..."); + TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address)); + + while (!TryConnect(ipc_address) || !connected) { + int backoff_limit = (backoff_amount + 1) * 10; + for (int i = 0; i < backoff_limit && !connected; i++) { + // If Connect is called right before this block, we will see and handle + // should_disconnect. If it is called right after, we will do one bad + // poll, one sleep, and then grab the mutex again right after. + { + std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + break; + } + } + ws->poll(); // Back off std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + backoff_amount++; + + // If Connect is called right before this block, we will see and handle + // should_disconnect. If it is called right after, and the connection + // was unsuccessful, we will grab the mutex after one bad connection + // attempt. If the connection was successful, we grab the mutex right + // after exiting the loop. { std::lock_guard state_guard(state_mutex); - backoff_amount = std::min(9, backoff_amount + 1); - if (!connected) { - TrackerLog(fmt::format("Retrying IPC in {} second(s)...", - backoff_amount + 1)); + if (should_disconnect) { + break; + } else if (!connected) { + if (backoff_amount >= 10) { + should_disconnect = true; + address.clear(); + + TrackerLog("Giving up on IPC."); + SetStatusMessage("Disconnected from game."); + + wxMessageBox("Connection to Lingo timed out.", + "Connection failed", wxOK | wxICON_ERROR); + + break; + } else { + TrackerLog(fmt::format("Retrying IPC in {} second(s)...", + backoff_amount + 1)); + } } } } + // Pretty much every lock guard in the thread is the same. We check for + // should_disconnect, and if it gets set directly after the block, we do + // minimal bad work before checking for it again. + { + std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + ws.reset(); + continue; + } + } + while (connected) { ws->poll(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - SetStatusMessage("Disconnected from game."); + { + std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + ws.reset(); + break; + } + } + } } } - bool TryConnect() { + bool TryConnect(std::string ipc_address) { try { ws = std::make_unique( - kIpcAddress, [this]() { OnConnect(); }, [this]() { OnClose(); }, + ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); }, [this](const std::string& s) { OnMessage(s); }, [this](const std::string& s) { OnError(s); }); return true; @@ -174,11 +270,19 @@ struct IPCState { if (msg["cmd"] == "Connect") { std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + return; + } game_ap_server = msg["slot"]["server"]; game_ap_user = msg["slot"]["player"]; CheckIfSlotMatches(); + + if (!slot_matches) { + tracker_frame->ConnectToAp(game_ap_server, game_ap_user, + msg["slot"]["password"]); + } } else if (msg["cmd"] == "UpdatePosition") { std::lock_guard state_guard(state_mutex); @@ -207,8 +311,6 @@ struct IPCState { status_message = "Connected to game."; Sync(); - } else if (tracker_ap_server.empty()) { - status_message = std::nullopt; } else if (connected) { status_message = "Local game doesn't match AP slot."; } @@ -240,7 +342,11 @@ IPCState& GetState() { } // namespace -void IPC_Start(TrackerFrame* tracker_frame) { GetState().Start(tracker_frame); } +void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) { + GetState().SetTrackerFrame(tracker_frame); +} + +void IPC_Connect(std::string address) { GetState().Connect(address); } std::optional IPC_GetStatusMessage() { return GetState().GetStatusMessage(); -- cgit 1.4.1