#include "ipc_state.h" #define _WEBSOCKETPP_CPP11_STRICT_ #include #include #include #include #include #include #include #include #include #include #include #include "ap_state.h" #include "logger.h" #include "tracker_frame.h" namespace { 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; std::string tracker_ap_server; std::string tracker_ap_user; std::string game_ap_server; std::string game_ap_user; std::optional> player_position; std::set solved_panels; // Thread state std::unique_ptr ws; bool connected = false; 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(); initialized = true; } else if (address != a) { should_disconnect = true; } address = a; } std::optional GetStatusMessage() { std::lock_guard state_guard(state_mutex); return status_message; } void SetTrackerSlot(std::string server, std::string user) { // This 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); tracker_ap_user = std::move(user); CheckIfSlotMatches(); if (!slot_matches) { should_disconnect = true; address.clear(); } } bool IsConnected() { std::lock_guard state_guard(state_mutex); return slot_matches; } std::optional> GetPlayerPosition() { std::lock_guard state_guard(state_mutex); return player_position; } const std::set& GetSolvedPanels() { std::lock_guard state_guard(state_mutex); return solved_panels; } private: void Thread() { for (;;) { // 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); SetStatusMessage("Disconnected from game."); 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; SetStatusMessage("Connecting to game..."); } int backoff_amount = 0; TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address)); while (!connected) { if (TryConnect(ipc_address)) { 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++; } else { std::lock_guard state_guard(state_mutex); if (!should_disconnect) { should_disconnect = true; address.clear(); SetStatusMessage("Disconnected from game."); } break; } // 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); 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)); { std::lock_guard state_guard(state_mutex); if (should_disconnect) { ws.reset(); break; } } } } } bool TryConnect(std::string ipc_address) { try { ws = std::make_unique( ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); }, [this](const std::string& s) { OnMessage(s); }, [this](const std::string& s) { OnError(s); }); return true; } catch (const std::exception& ex) { TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what())); wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR); ws.reset(); return false; } } void OnConnect() { connected = true; { std::lock_guard state_guard(state_mutex); slot_matches = false; player_position = std::nullopt; solved_panels.clear(); } } void OnClose() { connected = false; { std::lock_guard state_guard(state_mutex); slot_matches = false; } } void OnMessage(const std::string& s) { TrackerLog(s); auto msg = nlohmann::json::parse(s); 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); player_position = std::make_tuple(msg["position"]["x"], msg["position"]["z"]); tracker_frame->RedrawPosition(); } else if (msg["cmd"] == "SolvePanels") { std::lock_guard state_guard(state_mutex); for (std::string panel : msg["panels"]) { solved_panels.insert(std::move(panel)); } tracker_frame->UpdateIndicators(kUPDATE_ONLY_PANELS); } } void OnError(const std::string& s) {} // Assumes mutex is locked. void CheckIfSlotMatches() { slot_matches = (tracker_ap_server == game_ap_server && tracker_ap_user == game_ap_user); if (slot_matches) { SetStatusMessage("Connected to game."); Sync(); } else if (connected) { SetStatusMessage("Local game doesn't match AP slot."); } } // Assumes mutex is locked. void SetStatusMessage(std::optional msg) { status_message = msg; tracker_frame->UpdateStatusMessage(); } void Sync() { nlohmann::json msg; msg["cmd"] = "Sync"; ws->send_text(msg.dump()); } }; IPCState& GetState() { static IPCState* instance = new IPCState(); return *instance; } } // namespace 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(); } void IPC_SetTrackerSlot(std::string server, std::string user) { GetState().SetTrackerSlot(server, user); } bool IPC_IsConnected() { return GetState().IsConnected(); } std::optional> IPC_GetPlayerPosition() { return GetState().GetPlayerPosition(); } const std::set& IPC_GetSolvedPanels() { return GetState().GetSolvedPanels(); }