From da0b7cd13d16bba39399facda1d10750e2cf0f26 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 16 Oct 2025 18:44:03 +0200 Subject: [PATCH] fix(breakhandler): synchronize login lifecycle --- .../accountselector/AutoLoginScript.java | 53 +-- .../breakhandler/BreakHandlerScript.java | 391 +++++++++++------- 2 files changed, 265 insertions(+), 179 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java index 9e6344a999e..152b50be52e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java @@ -235,23 +235,29 @@ private void handleLoginExtendedSleepState(AutoLoginConfig config) { * Initiates intelligent login based on configuration. */ private void initiateLogin(AutoLoginConfig config) { + if (!BreakHandlerScript.tryAcquireLoginLock()) { + log.debug("Login sequence currently locked by break handler; skipping auto login attempt"); + return; + } + try { - // start login watchdog if enabled and not already started if (config.enableLoginWatchdog() && loginWatchdogStartTime == null) { loginWatchdogStartTime = Instant.now(); log.info("Login watchdog started for {} minutes", config.loginWatchdogTimeout()); } - + + if (Microbot.isLoggedIn()) { + return; + } + int targetWorld = -1; - boolean membersOnly = config.membersOnly(); - - // use world selection mode if no preferred world or preferred world not accessible + if (targetWorld == -1) { switch (config.worldSelectionMode()) { case CURRENT_PREFERRED_WORLD: boolean isAccessible = Rs2WorldUtil.canAccessWorld(config.world()); - + if (isAccessible) { targetWorld = config.world(); log.info("Using preferred world: {}", targetWorld); @@ -260,28 +266,28 @@ private void initiateLogin(AutoLoginConfig config) { boolean isMemberFromProfile = activeProfile != null && activeProfile.isMember(); boolean isLocalPlayerAvailable = Microbot.getClient()!=null && Microbot.getClient().getLocalPlayer() != null; boolean isMemberFromClient = Microbot.getClient()!=null && Microbot.getClient().getLocalPlayer() != null ? Rs2Player.isMember() : false; - log.error("Preferred world {} is not accessible,\n\t ->check if we have member access set in profile(current value {}), or when logged in, have we member access ? (LocalPlayer? {}, isMember? {})", - config.usePreferredWorld(), isMemberFromProfile, isLocalPlayerAvailable, isMemberFromClient); + log.error("Preferred world {} is not accessible,\n\t ->check if we have member access set in profile (current value {}), or when logged in, have we member access ? (LocalPlayer? {}, isMember? {})", + config.usePreferredWorld(), isMemberFromProfile, isLocalPlayerAvailable, isMemberFromClient); + } - // no specific world selection - use default login break; - + case RANDOM_WORLD: targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( config.regionPreference().getWorldRegion(), config.avoidEmptyWorlds(), config.avoidOvercrowdedWorlds(),membersOnly); break; - + case BEST_POPULATION: targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( false, config.regionPreference().getWorldRegion(), config.avoidEmptyWorlds(), - config.avoidOvercrowdedWorlds(), + config.avoidOvercrowdedWorlds(), membersOnly); break; - + case BEST_PING: targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( true, @@ -291,7 +297,7 @@ private void initiateLogin(AutoLoginConfig config) { membersOnly ); break; - + case REGIONAL_RANDOM: targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( config.regionPreference().getWorldRegion(), @@ -300,22 +306,20 @@ private void initiateLogin(AutoLoginConfig config) { membersOnly ); break; - + default: - // fallback to legacy behavior - targetWorld = Login.getRandomWorld(Rs2Player.isMember()); + targetWorld = Login.getRandomWorld(Rs2Player.isMember()); if(!Rs2WorldUtil.canAccessWorld(targetWorld)) { log.warn("Randomly selected world {} is not accessible, using default world {}", targetWorld, config.world()); targetWorld = config.world(); - } + } break; } } - - // perform login attempt and track retry state + retryCount++; lastLoginAttemptTime = Instant.now(); - + if (targetWorld != -1) { log.info("Attempting login to selected world: {} (attempt {})", targetWorld, retryCount); new Login(targetWorld); @@ -323,15 +327,16 @@ private void initiateLogin(AutoLoginConfig config) { log.info("Using default login (current world or last used) (attempt {})", retryCount); new Login(); } - - + } catch (Exception ex) { log.error("Error during intelligent login", ex); retryCount++; lastLoginAttemptTime = Instant.now(); + } finally { + BreakHandlerScript.releaseLoginLock(); } } - + /** * Transitions to a new login state. */ diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java index 37e25cca4f4..138e03959db 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java @@ -20,6 +20,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; import net.runelite.api.GameState; /** @@ -102,7 +103,10 @@ private int getSafeConditionTimeoutMs() { private static volatile Instant loginWatchdogStartTime = null; @Getter private static volatile Instant extendedSleepStartTime = null; - + private static final ReentrantLock stateLock = new ReentrantLock(); + private static final ReentrantLock loginSequenceLock = new ReentrantLock(); + private static final AtomicBoolean controllerRunning = new AtomicBoolean(false); + // Lock state management public static AtomicBoolean lockState = new AtomicBoolean(false); @@ -118,6 +122,16 @@ public static void setLockState(boolean state) { } } + public static boolean tryAcquireLoginLock() { + return loginSequenceLock.tryLock(); + } + + public static void releaseLoginLock() { + if (loginSequenceLock.isHeldByCurrentThread()) { + loginSequenceLock.unlock(); + } + } + // UI and configuration private String originalWindowTitle = ""; private BreakHandlerConfig config; @@ -175,33 +189,54 @@ public static String formatDuration(Duration duration) { * Main entry point for the break handler script. */ public boolean run(BreakHandlerConfig config) { - this.config = config; - originalWindowTitle = ClientUI.getFrame().getTitle(); - // Initialize state and timing - currentState.set(BreakHandlerState.WAITING_FOR_BREAK); - stateChangeTime.set(Instant.now()); - retryCount.set(0); - initializeNextBreakTimer(); + if (!controllerRunning.compareAndSet(false, true)) { + log.warn("Break handler is already running; ignoring duplicate run request"); + return false; + } + + this.config = config; + originalWindowTitle = ClientUI.getFrame().getTitle(); + + stateLock.lock(); + try { + currentState.set(BreakHandlerState.WAITING_FOR_BREAK); + stateChangeTime.set(Instant.now()); + retryCount.set(0); + initializeNextBreakTimer(); + } finally { + stateLock.unlock(); + } + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { super.run(); - - // check for ban detection first - checkForBan(); - updatePlayerNameCache(); - - // only continue with break handling if not banned - if (!isBanned) { - processBreakHandlerStateMachine(); + + if (!stateLock.tryLock()) { + log.trace("Skipping break handler tick because another thread holds the state lock"); + return; + } + + try { + // check for ban detection first + checkForBan(); + updatePlayerNameCache(); + + // only continue with break handling if not banned + if (!isBanned) { + processBreakHandlerStateMachine(); + } + } finally { + stateLock.unlock(); } } catch (Exception ex) { log.error("Error in break handler main loop", ex); } }, 0, SCHEDULER_INTERVAL_MS, TimeUnit.MILLISECONDS); - + return true; } + /** * Main state machine processor - handles all break-related state transitions. */ @@ -404,39 +439,50 @@ private void handleClientShutdown() { * Attempting to logout, with retry logic. */ private void handleLogoutRequestedState() { - if (!Microbot.isLoggedIn()) { - retryCount.set(0); - log.debug("Logout successful"); - transitionToState(BreakHandlerState.LOGGED_OUT); + if (!loginSequenceLock.tryLock()) { + log.debug("Login/logout sequence is currently locked; deferring logout request"); return; } - int currentRetryCount = retryCount.get(); - Instant currentStateChangeTime = stateChangeTime.get(); - - if (currentRetryCount >= getMaxLogoutRetries()) { - log.warn("Max logout retries reached, continuing with logged-in break"); - transitionToState(BreakHandlerState.INGAME_BREAK_ACTIVE); - return; - } - - // Check if enough time has passed for retry - if (currentRetryCount > 0 && Duration.between(currentStateChangeTime, Instant.now()).toMillis() < getLogoutRetryDelayMs()) { - long remainingTime = getLogoutRetryDelayMs() - Duration.between(currentStateChangeTime, Instant.now()).toMillis(); - log.debug("Waiting for next logout retry ({} ms remaining)", remainingTime); - return; - } - log.info("Attempting logout (attempt {}/{})", currentRetryCount + 1, getMaxLogoutRetries()); + try { - Rs2Player.logout(); - // Don't immediately transition - wait for next cycle to check if logout was successful - retryCount.incrementAndGet(); - stateChangeTime.set(Instant.now()); - // Check on next cycle if we successfully logged out - - } catch (Exception ex) { - log.error("Error during logout attempt", ex); - retryCount.incrementAndGet(); - stateChangeTime.set(Instant.now()); + if (!Microbot.isLoggedIn()) { + retryCount.set(0); + log.debug("Logout successful"); + transitionToState(BreakHandlerState.LOGGED_OUT); + return; + } + + int currentRetryCount = retryCount.get(); + Instant currentStateChangeTime = stateChangeTime.get(); + + if (currentRetryCount >= getMaxLogoutRetries()) { + log.warn("Max logout retries reached, continuing with logged-in break"); + transitionToState(BreakHandlerState.INGAME_BREAK_ACTIVE); + return; + } + + if (currentRetryCount > 0 && Duration.between(currentStateChangeTime, Instant.now()).toMillis() < getLogoutRetryDelayMs()) { + long remainingTime = getLogoutRetryDelayMs() - Duration.between(currentStateChangeTime, Instant.now()).toMillis(); + log.debug("Waiting for next logout retry ({} ms remaining)", remainingTime); + return; + } + + if (!Microbot.isLoggedIn()) { + return; + } + + log.info("Attempting logout (attempt {}/{})", currentRetryCount + 1, getMaxLogoutRetries()); + try { + Rs2Player.logout(); + retryCount.incrementAndGet(); + stateChangeTime.set(Instant.now()); + } catch (Exception ex) { + log.error("Error during logout attempt", ex); + retryCount.incrementAndGet(); + stateChangeTime.set(Instant.now()); + } + } finally { + loginSequenceLock.unlock(); } } @@ -481,99 +527,104 @@ private void handleLoginBreakActiveState() { * Break ended, attempting to login with intelligent world selection and watchdog. */ private void handleLoginRequestedState() { - if (Microbot.isLoggedIn()) { - log.debug("Already logged in, proceeding to break ending"); - loginWatchdogStartTime = null; // reset watchdog - transitionToState(BreakHandlerState.BREAK_ENDING); + if (!loginSequenceLock.tryLock()) { + log.debug("Login/logout sequence is currently locked; deferring login request"); return; } - - // initialize login watchdog timer - if (loginWatchdogStartTime == null) { - loginWatchdogStartTime = Instant.now(); - log.debug("Login watchdog started for {} minutes", config.loginWatchdogTimeout()); - } - - boolean membersOnly = config.membersOnly(); - log.info("Attempting intelligent login with world selection"); + try { - int targetWorld = -1; - // use world selection mode if no last world or last world not accessible - - switch (config.worldSelectionMode()) { - case CURRENT_PREFERRED_WORLD: - if (preBreakWorld != -1) { - boolean isAccessible = Rs2WorldUtil.canAccessWorld(preBreakWorld); - if (isAccessible) { - targetWorld = preBreakWorld; - log.info("Using last world before break: {}", targetWorld); - } else { - log.warn("Last world {} is not accessible, falling back to world selection mode", preBreakWorld); + if (Microbot.isLoggedIn()) { + log.debug("Already logged in, proceeding to break ending"); + loginWatchdogStartTime = null; // reset watchdog + transitionToState(BreakHandlerState.BREAK_ENDING); + return; + } + + if (loginWatchdogStartTime == null) { + loginWatchdogStartTime = Instant.now(); + log.debug("Login watchdog started for {} minutes", config.loginWatchdogTimeout()); + } + + boolean membersOnly = config.membersOnly(); + log.info("Attempting intelligent login with world selection"); + try { + int targetWorld = -1; + + switch (config.worldSelectionMode()) { + case CURRENT_PREFERRED_WORLD: + if (preBreakWorld != -1) { + boolean isAccessible = Rs2WorldUtil.canAccessWorld(preBreakWorld); + if (isAccessible) { + targetWorld = preBreakWorld; + log.info("Using last world before break: {}", targetWorld); + } else { + log.warn("Last world {} is not accessible, falling back to world selection mode", preBreakWorld); + } } - } - - // no specific world selection - use default login - break; - - case RANDOM_WORLD: - targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( - config.regionPreference().getWorldRegion(), - config.avoidEmptyWorlds(), - config.avoidOvercrowdedWorlds(), - membersOnly); - break; - - case BEST_POPULATION: - targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( - false, - config.regionPreference().getWorldRegion(), - config.avoidEmptyWorlds(), - config.avoidOvercrowdedWorlds(), - membersOnly - ); - break; - - case BEST_PING: - targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( - true, - config.regionPreference().getWorldRegion(), - config.avoidEmptyWorlds(), - config.avoidOvercrowdedWorlds(), - membersOnly - ); - break; - - case REGIONAL_RANDOM: - targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( - config.regionPreference().getWorldRegion(), - config.avoidEmptyWorlds(), - config.avoidOvercrowdedWorlds(), - membersOnly - ); - break; - - default: - // fallback to current world - break; + break; + + case RANDOM_WORLD: + targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( + config.regionPreference().getWorldRegion(), + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly); + break; + + case BEST_POPULATION: + targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( + false, + config.regionPreference().getWorldRegion(), + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case BEST_PING: + targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( + true, + config.regionPreference().getWorldRegion(), + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case REGIONAL_RANDOM: + targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( + config.regionPreference().getWorldRegion(), + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + default: + break; } - - - // perform login attempt - if (targetWorld != -1) { - log.info("Attempting login to selected world: {}", targetWorld); - new Login(targetWorld); - } else { - log.info("Using default login (current world or last used)"); - new Login(); + + if (Microbot.isLoggedIn()) { + transitionToState(BreakHandlerState.BREAK_ENDING); + return; + } + + if (targetWorld != -1) { + log.info("Attempting login to selected world: {}", targetWorld); + new Login(targetWorld); + } else { + log.info("Using default login (current world or last used)"); + new Login(); + } + + transitionToState(BreakHandlerState.LOGGING_IN); + + } catch (Exception ex) { + log.error("Error initiating login", ex); + transitionToState(BreakHandlerState.LOGGING_IN); } - - // immediately transition to logging in state to prevent multiple login instances - transitionToState(BreakHandlerState.LOGGING_IN); - - } catch (Exception ex) { - log.error("Error initiating login", ex); - // still transition to prevent getting stuck in login requested state - transitionToState(BreakHandlerState.LOGGING_IN); + } finally { + loginSequenceLock.unlock(); } } @@ -687,13 +738,27 @@ private void handleBreakEndingState() { * This method is thread-safe and can be called from any thread. */ private static void transitionToState(BreakHandlerState newState) { + if (!stateLock.isHeldByCurrentThread()) { + stateLock.lock(); + try { + transitionToStateInternal(newState); + } finally { + stateLock.unlock(); + } + return; + } + + transitionToStateInternal(newState); + } + + private static void transitionToStateInternal(BreakHandlerState newState) { BreakHandlerState oldState = currentState.get(); if (oldState != newState) { log.debug("State transition: {} -> {}", oldState, newState); currentState.set(newState); stateChangeTime.set(Instant.now()); retryCount.set(0); - + // Reset safe condition wait time when leaving BREAK_REQUESTED if (newState != BreakHandlerState.BREAK_REQUESTED) { safeConditionWaitStartTime = null; @@ -701,6 +766,7 @@ private static void transitionToState(BreakHandlerState newState) { } } + /** * Checks if it's safe to break (not in combat, not interacting). */ @@ -850,23 +916,33 @@ private boolean isOutsidePlaySchedule() { @Override public void shutdown() { - BreakHandlerState state = currentState.get(); - if(scheduledFuture != null && !scheduledFuture.isDone()) { - scheduledFuture.cancel(true); - } - log.info("Break handler shutting down. Current state: {}", state); - - // If we're in a break state, try to clean up gracefully - if (state == BreakHandlerState.LOGGED_OUT) { - log.info("Attempting to resume from logged out state"); - transitionToState(BreakHandlerState.LOGIN_REQUESTED); - } else if (isBreakActive()) { - log.info("Attempting to end break gracefully"); - transitionToState(BreakHandlerState.BREAK_ENDING); - } - resetWindowTitle(); - resumeFromBreak(); - super.shutdown(); + try { + if (scheduledFuture != null && !scheduledFuture.isDone()) { + scheduledFuture.cancel(true); + } + + stateLock.lock(); + try { + BreakHandlerState state = currentState.get(); + log.info("Break handler shutting down. Current state: {}", state); + + if (state == BreakHandlerState.LOGGED_OUT) { + log.info("Attempting to resume from logged out state"); + transitionToState(BreakHandlerState.LOGIN_REQUESTED); + } else if (isBreakActive()) { + log.info("Attempting to end break gracefully"); + transitionToState(BreakHandlerState.BREAK_ENDING); + } + } finally { + stateLock.unlock(); + } + + resetWindowTitle(); + resumeFromBreak(); + super.shutdown(); + } finally { + controllerRunning.set(false); + } } /** @@ -874,9 +950,14 @@ public void shutdown() { */ public void reset() { log.info("Resetting break handler"); - resetBreakState(); - transitionToState(BreakHandlerState.WAITING_FOR_BREAK); - initializeNextBreakTimer(); + stateLock.lock(); + try { + resetBreakState(); + transitionToState(BreakHandlerState.WAITING_FOR_BREAK); + initializeNextBreakTimer(); + } finally { + stateLock.unlock(); + } } /**