Class PluginInitiationUtils

java.lang.Object
com.ultikits.ultitools.utils.PluginInitiationUtils

public class PluginInitiationUtils extends Object
Start-up and shutdown entry points of the UltiCloud connection.

As of 6.3.1 the connection speaks UltiCloud protocol v2 only: initWebsocket() starts the current session's agent connection, and every inbound request is routed by the v2 agent router that CloudFeatures registers per connection. The protocol v1 inbound dispatch this class used to hold (its message-type table, the capability gate in front of it, the per-type v1 handlers and the v1 handshake callback) was removed in 6.3.1 (D-01: no v1 adapter); every public member keeps its 6.3.0 signature (D-29).

Since:
6.0.0
Author:
wisdomme
  • Field Details

    • LEGACY_TOKEN_REMOVED_KEY

      static final String LEGACY_TOKEN_REMOVED_KEY
      See Also:
    • SLOW_PANEL_EVENT_HANDLER_THRESHOLD_MILLIS

      private static final long SLOW_PANEL_EVENT_HANDLER_THRESHOLD_MILLIS
      The elapsed-time threshold above which a PanelMessageEvent publish is considered slow enough to warn about, in milliseconds. Set below one server tick (50ms at the nominal 20 TPS) so a subscriber costing a visible fraction of the tick budget is named before players feel it — this constant is the runtime half of D-24's mitigation; PanelMessageEvent's javadoc is the other half, stating the contract a reader sees before ever hitting this warning at runtime.
      See Also:
    • acceptingPanelMessages

      private static volatile boolean acceptingPanelMessages
      Whether the framework accepts inbound panel work (UltiTools-Reborn#621). Set by UltiTools#onEnable and cleared as the very first step of UltiTools#onDisable, before any manager shuts down and before the WebSocket is closed. Starts true so a framework that was never disabled (and every unit test that does not touch it) behaves as before. See isAcceptingPanelMessages() for the full condition.
    • PANEL_CHANGE_DRAIN_MS

      static final long PANEL_CHANGE_DRAIN_MS
      How long stopAcceptingPanelMessages() waits for the state changes already running (a file write, a server.properties update, a module responder or player-link handler a request was handed to); each takes milliseconds, so the bound is reached only by a stalled disk or a module handler that does not end.
      See Also:
    • PANEL_CHANGES

      private static final Object PANEL_CHANGES
      Guards panelChangesRunning; stopAcceptingPanelMessages() waits on it for the count to reach zero (22-23 shutdown-window round). A count, not a read-write lock, because a permit is returned by whichever thread ends the change: a request handed to a module responder's worker or to the player-link handler thread keeps its permit until that work ends (22-23 second verification).
    • panelChangesRunning

      private static int panelChangesRunning
      How many permits enterPanelChange() gave that were not returned yet. Guarded by PANEL_CHANGES.
    • PANEL_CHANGE_SIGNAL

      static final ShutdownGate.Signal PANEL_CHANGE_SIGNAL
  • Constructor Details

    • PluginInitiationUtils

      public PluginInitiationUtils()
  • Method Details

    • startAcceptingPanelMessages

      @Internal public static void startAcceptingPanelMessages()
      Starts accepting inbound panel work again; called by UltiTools#onEnable.
      Since:
      6.3.0
    • stopAcceptingPanelMessages

      @Internal public static void stopAcceptingPanelMessages()
      Stops accepting inbound panel work (UltiTools-Reborn#621); called as the very first step of UltiTools#onDisable. From this moment every scheduler hand-off reachable from the UltiCloud connection's threads is dropped with one FINE line instead of scheduling a task for a plugin that is being disabled.

      As of 6.3.1 (protocol v2, maintainer decision of 2026-10-26) every request that changes server state -- a command, a power action, a player action, a file write or delete, a server.properties or configuration update, a module request, a credential rotation -- is refused from this moment with a failure reply saying the server is shutting down (a command is acknowledged and answered with a failed command.result, so it is never run again after a restart); read-only requests are still answered until onDisable closes the connection. Before it returns, this method waits (at most 5000L ms) for every state change that started earlier to finish -- including the work such a request handed to another thread: a module responder running for a module.request, the player-link handler running for a player.link.completed -- so each one, and its action-log entry, is complete before onDisable closes the action log. A request handed to the server thread is not waited for: it runs in a later tick, which never comes once the server stops, and Bukkit cancels the task when UltiTools disables. Such a request accepted before UltiTools' own disable starts -- while the server is already stopping, for example while plugins loaded after UltiTools disable -- is not carried out and gets no result. Must not be called by a thread inside such a change.

      Since:
      6.3.0
    • enterPanelChange

      static boolean enterPanelChange()
      The shutdown signal of every remote state change (ShutdownGate): takes a permit and returns true only while isAcceptingPanelMessages() holds; the caller then calls exitPanelChange() exactly once, from any thread, when the change and its action-log entry are done, including work it handed to another thread. Returns false, holding nothing, from the first step of onDisable on. Never blocks for longer than another caller's check.
      Returns:
      whether the state change may run
    • exitPanelChange

      static void exitPanelChange()
      Returns a permit enterPanelChange() gave; may run on a thread other than the one that entered.
    • awaitPanelChanges

      private static void awaitPanelChanges()
    • isAcceptingPanelMessages

      @Internal public static boolean isAcceptingPanelMessages()
      Whether inbound panel work may run now: the accepting flag is set, and UltiTools is not being disabled. Bukkit turns isEnabled() to false before it calls onDisable(), so the second half covers the moment between the two; Paper refuses every task of a disabled plugin with IllegalPluginAccessException. No instance (a unit test without one) is not "disabling": the hand-off sites below need an instance anyway.
      Returns:
      true when inbound panel work may be dispatched and scheduled
      Since:
      6.3.0
    • scheduleUnlessDisabling

      @Internal public static <T> T scheduleUnlessDisabling(String what, Supplier<T> scheduling)
      The one check every scheduler hand-off reachable from the WebSocket thread goes through (UltiTools-Reborn#621, swept by class: the inbound event publish, the queued config callbacks, the remote command dispatch, and the monitoring tasks a completing handshake starts). Runs scheduling -- exactly one call into the Bukkit scheduler -- only while isAcceptingPanelMessages() holds; otherwise, and when the plugin is disabled between that check and the scheduler's own check (the scheduler then throws IllegalPluginAccessException, the only exception caught here), the work is dropped with one FINE line naming it. Nothing is lost that a stopping server could still use: the scheduler would refuse the task anyway.
      Type Parameters:
      T - what the scheduler call returns
      Parameters:
      what - what is being scheduled, for the FINE line
      scheduling - the scheduler call
      Returns:
      what scheduling returned, or null when the work was dropped
      Since:
      6.3.0
    • logDroppedWhileDisabling

      private static void logDroppedWhileDisabling(String what)
    • resumeSavedCredentialOnStartup

      public static boolean resumeSavedCredentialOnStartup()
      Loads the saved server credential into the current session without any network I/O. No user token is loaded, refreshed or sent to a legacy registration endpoint.
      Returns:
      whether a saved server credential was adopted
    • startTokenRefreshScheduler

      @Deprecated(since="6.3.1", forRemoval=true) public static void startTokenRefreshScheduler()
      Deprecated, for removal: This API element is subject to removal in a future version.
      No replacement is needed for non-expiring server credentials; removed in 6.4.0.
      No-op: the protocol v2 server credential never expires and has no refresh token.
      Scheduled removal version:
      6.4.0
    • stopCredentialSchedulers

      public static void stopCredentialSchedulers()
      Stops device-login polling; the server credential never expires, so no refresh scheduler exists.
    • initWebsocket

      public static void initWebsocket() throws IOException
      Initialize websocket on the current session.
      Throws:
      IOException
    • initWebsocket

      static void initWebsocket(CloudSession session) throws IOException
      Starts the supplied session's v2 connection; a stale or unlinked session does nothing.
      Throws:
      IOException
    • resolveFileOperationCapability

      public static Capability resolveFileOperationCapability(String operation)
      The single file operation name to Capability mapping, read by FileOperationManager's action-log recording (D-22, Plan 06-04 Task 1). The mapping is unchanged from 6.3.0 and covers the operation names of the protocol v2 file requests (files.read, files.list, files.write, files.delete): write needs Capability.FILE_WRITE, delete needs Capability.FILE_DELETE, and read, list (listing is reading) and an unrecognised or absent operation resolve to Capability.FILE_READ, the most permitted of the three, so an unknown verb is still gated.
      Parameters:
      operation - the operation field's value, possibly null
      Returns:
      the required capability
    • isFrameworkOwnedType

      public static boolean isFrameworkOwnedType(String messageType)
      Whether messageType is a message type of the framework's own protocol: the single source PanelResponderRegistry.registerResponder consults before letting a module claim a message type (D-26, WIRE-16, Plan 06-08 Task 1).

      As of 6.3.1 the answer comes from the UltiCloud protocol v2 message registry (the frozen 2.0.0 contract's message table, for example command.execute, files.read or module.request); a module's responder key (<module>.<action>, for example ultimail.inbox.list) is not one of them, and neither is a removed protocol v1 type name. Exact String membership: no case folding, no Unicode normalization.

      Parameters:
      messageType - the message type to check, possibly null
      Returns:
      true if the framework's own protocol defines this exact type
    • publishPanelMessageEvent

      static void publishPanelMessageEvent(String type, com.google.gson.JsonObject message, com.google.gson.JsonObject data)
      Bridges a panel message the framework has already handled onto the module-facing EventBus (WIRE-16). This is the single publish site, invoked from the UltiCloud protocol v2 module bridge (ModuleRequestHandler), which publishes one event per module-owned request. It is kept because PanelMessageEvent is public since 6.3.0 and a module subscribing to observe its own panel traffic keeps receiving it now that the protocol v1 inbound path is removed (6.3.1).

      EventBus.publishAsync(com.ultikits.ultitools.events.ModuleEvent) was considered and rejected: it submits to an async worker pool and never reaches the main thread, so it does not address Paper's AsyncCatcher at all — it only keeps the WebSocket I/O thread unblocked. A Minecraft module's handler touches Bukkit API by definition, so the real choice here was main-thread versus not-main-thread, not sync-dispatch versus async-dispatch; only Bukkit.getScheduler().runTask(...) puts a handler on the main thread. The whole helper body is wrapped in a catch so a missing scheduler (no Bukkit server booted, as in a plain unit test) or a missing EventBus can never break the inbound message path — both are logged no-ops.

      Parameters:
      type - the resolved message type
      message - the full inbound envelope
      data - the message's data object, possibly null
    • reinitWebSocket

      public static void reinitWebSocket()
      Restarts the current session's v2 connection without loading or refreshing a user token.
    • reinitWebSocket

      static void reinitWebSocket(CloudSession session)
    • disableCloud

      public static void disableCloud()
      Closes the cloud connection and invalidates the current session, putting the reconnection state machine into an explicit disabled state.

      Called by /ulticloud logout. The difference from stopWebsocket() is that the latter only disconnects the current client, and the reconnection chain will bring it back up; this method invalidates CloudSession.current() first, so reinitWebSocket(CloudSession) returns immediately afterward and the state machine does not resurrect itself.

      Also strips the log handler and the transport thread off the root logger (as of plan 16-10, WR-01, this now happens INSIDE CloudSession.invalidate() rather than as a separate, unlocked step before it -- see that method's own javadoc), and stops the server monitor and the player-event listener — all part of what makes the statement "cloud features are disabled" true. The teardown synchronizes on the session instance itself (CloudSession.invalidate()); see the two review rounds on PR #264 for the original race the former global lifecycle lock guarded.

      CR-01 (16-REVIEW-cloud.md): disableCloud(CloudSession) -- the overload internal callers use -- returns the exact session that overload tore down, captured by the caller before any teardown step runs. CloudAuthManager.logout() reads its disk-clear decision off that return value rather than a second, independent call to CloudSession.current() taken after teardown returns -- eliminating the specific race where a concurrent login() installs a new session in the gap between those two reads (see CloudAuthManager#logout()'s own javadoc for the full account). This particular overload keeps its original void return -- disableCloud() was public in the 6.2.5 release, and changing a released public method's return type is a genuine binary incompatibility japicmp correctly rejects; there is no same-release exception available for a signature that already shipped.

    • disableCloud

      static CloudSession disableCloud(CloudSession session)
      The session-targeted, CloudSession-returning form of disableCloud() (CR-01/WR-02, 16-REVIEW-cloud.md) -- package-private, since it is new-in-6.3.0 and reached only by other members of this package (CloudAuthManager#logout()).

      It acts on the session its caller names, rather than on whatever CloudSession.current() happens to be when it runs, as the no-arg disableCloud() does: a fresh login racing in would otherwise let a teardown meant for an old session tear down an unrelated, brand-new one. (The protocol v1 reconnect loop's budget-exhaustion branch, its other 6.3.0 caller, was removed in 6.3.1.) CloudAuthManager#logout() calls this with CloudSession.current() captured at its own call site -- immediately before teardown begins -- and acts on the returned reference afterward rather than re-reading CloudSession.current() a second, independent time.

      Parameters:
      session - the session to invalidate and tear down
      Returns:
      session, unchanged -- returned so the caller can act on exactly what was torn down without a second read of CloudSession.current()
    • doDisableCloud

      private static CloudSession doDisableCloud(CloudSession session)
      The actual teardown performed by disableCloud() / disableCloud(CloudSession).

      session is the caller's own choice of target, captured before this method does anything else -- CloudSession.invalidate() below acts on exactly this reference, never a fresh read of CloudSession.current() taken partway through teardown (CR-01).

      Round-1 external review finding, second pass (16-10, PR #464): the two steps below act on GLOBAL, session-independent singletons (exactly one server monitor and one player-event manager for the whole plugin, not one per session) -- but "session-independent" does not mean "safe to run unconditionally". If session has already been superseded by a newer one by the time this runs (the same narrow window WR-02 already accounts for on the invalidation half), and that newer session's own handshake has already wired these same managers up, an unconditional stop here would tear down wiring that belongs to the session that is actually current now -- with nothing left to ever restart it, since no further handshake is coming. Gating both steps on session still being current closes that: a stale teardown call for an already-superseded session leaves whatever the current session has wired alone.

      Parameters:
      session - the session to invalidate
      Returns:
      session
    • stopV1Monitor

      private static void stopV1Monitor()
      Stops the protocol v1 server monitor, if one exists; it runs only when a caller started it.
    • teardownStep

      private static void teardownStep(String what, Runnable action)
      Runs one teardown step; a failure is only logged at FINE, never thrown out.

      Every teardown step must run to the best of its ability: any step throwing would skip every step after it, and those steps are exactly what makes the statement "cloud features are disabled" true. This used to be six identical try/catch blocks; extracting it only states that invariant once — the behaviour is unchanged.

      Parameters:
      what - the action description written to the log on failure
      action - the teardown action
    • enableCloud

      public static void enableCloud()
      Ensures the current session is usable, replacing it with a fresh one if it has been invalidated.

      Only an explicit action should call this: cloud login at server startup, and after a successful /ulticloud login. initWebsocket() deliberately does not call it — it is also reused by reinitWebSocket(), and calling it there would let an in-flight reconnection resurrect a state machine that had just been turned off by logout.

      As of 16-08 Task 2 there is no separate enabled flag to flip: "enabled" simply means CloudSession.current() is not invalidated. If it already is not (the common case -- most callers reach this with a perfectly good session already installed), the session itself is not replaced; doing so for no reason would gratuitously drop its credential and its connection. Only a session that CloudSession.invalidate() already ran on (typically disableCloud(), without a following /ulticloud login) gets replaced. (The protocol v1 reconnect budget this method also reset in 6.3.0 was removed in 6.3.1: the protocol v2 agent connection keeps its own backoff.)

    • isCloudEnabled

      static boolean isCloudEnabled()
      Lets a test assert whether the state machine is currently enabled -- i.e. whether the current session is still current.
    • stopWebsocket

      public static void stopWebsocket()
      Stops the v2 connection and invalidates pending login results during plugin shutdown.

      The pending reliable-delivery state is written before this returns: invalidate() flushes the outbox and the command-dedupe set (D-15), so a stop persists what the 30 s tick has not written yet.