Class PluginInitiationUtils
插件初始化和WebSocket通信的实用工具类。 处理UltiPanel集成的账户登录、WebSocket连接和消息处理。
- Since:
- 6.0.0
- Author:
- wisdomme
-
Field Summary
FieldsModifier and TypeFieldDescriptionprivate static final AtomicBoolean云连接是否处于「应当保持连接」的状态。private static final Object云管理器「接线」与「拆线」的互斥锁。private static final int外层 reinit 的全局上限。超过之后进入终态,需要/ulticloud login或重启才恢复。private static UltiPanelWebSocketClientWebSocket client for panel communicationprivate static final ExponentialBackoffStrategy外层重连(reinit 循环)的全局预算与退避。private static final Stringserver_properties走独立管理器,不是一个真正的配置文件路径。private static TokenEntityAuthentication token for API requests -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic booleanactivateCloudIfCurrent(long generation) 在云生命周期锁内,原子地「复查代际 → 开启状态机 → 建连 → 起刷新调度」。private static voidapplyConfigUpdate(String fileName, String configContent) 按fileName决定写到哪里。static void关闭云连接并让重连状态机进入明确的 disabled 态。private static voiddisableCloud()的实际拆线动作。调用方必须持有cloudLifecycleLock。static void把状态机置为「应当保持连接」,并清零外层重连预算。private static UltiPanelWebSocketClientprivate static voidhandleBackupOperation(com.google.gson.JsonObject data) 处理备份操作private static voidhandleBackupProgress(com.google.gson.JsonObject data) 处理备份进度private static voidhandleCommandResult(com.google.gson.JsonObject data) 处理命令执行结果(package private) static voidhandleConfigUpdate(com.google.gson.JsonObject data) 处理配置更新。private static voidhandleConfigUpload(com.google.gson.JsonObject data) 处理配置上传private static voidhandleConfigUploadLogic(com.google.gson.JsonObject data) 处理配置上传逻辑private static voidhandleError(com.google.gson.JsonObject data) 处理错误消息private static voidhandleFileOperationResult(com.google.gson.JsonObject data) 处理文件操作结果(package private) static voidhandleInboundMessage(com.google.gson.JsonObject message) 处理面板下发的入站 WebSocket 消息。private static voidhandleMetricsRequest(com.google.gson.JsonObject data) 处理性能数据请求private static voidhandleNotification(com.google.gson.JsonObject data) 处理通知消息private static voidhandlePing(com.google.gson.JsonObject message) 处理ping消息private static voidhandlePlayerEvent(com.google.gson.JsonObject data) 处理玩家事件private static voidhandlePluginListRequest(com.google.gson.JsonObject data) 处理插件列表请求private static voidhandlePong(com.google.gson.JsonObject data) 处理pong消息private static voidhandleServerStatusRequest(com.google.gson.JsonObject data) 处理服务器状态请求private static voidhandleSubscribe(com.google.gson.JsonObject data) 处理订阅消息private static voidhandleUnsubscribe(com.google.gson.JsonObject data) 处理取消订阅消息(package private) static void把所有 WebSocket 管理器接到当前连接上。static voidInitialize websocket.(package private) static boolean供测试断言状态机是否处于启用态。static booleanloginWithToken(TokenEntity existingToken) Login to UltiPanel using an existing token (from magic-link or saved token).(package private) static void重连成功时调用:把外层预算清零。(package private) static void握手成功之后的接线动作。private static StringreadConfigContent(com.google.gson.JsonObject data) 取配置内容,优先data.configData。private static StringreadString(com.google.gson.JsonObject data, String field) 读一个可能缺失、也可能是 JSON null 的字符串字段。static voidRe-initialize the WebSocket connection with a fresh token.private static booleansafeGetBoolean(com.google.gson.JsonObject obj, String key, boolean defaultValue) private static doublesafeGetDouble(com.google.gson.JsonObject obj, String key, double defaultValue) private static longsafeGetLong(com.google.gson.JsonObject obj, String key, long defaultValue) private static StringsafeGetString(com.google.gson.JsonObject obj, String key) private static voidsendConfigUpdateResponse(String requestId, boolean success, String error) 回一条config_update_response。private static voidsendErrorResponse(String errorMessage) 发送错误响应static voidprivate static voidteardownStep(String what, Runnable action) 跑一步拆线动作,失败只记 FINE 不向外抛。private static voiduploadConfig(UltiPanelWebSocketClient client) 上传本地配置到服务器private static voidUpload server.properties safe keys to cloud for panel editing.private static voidinitializeManagers()的实际接线动作。调用方必须持有cloudLifecycleLock。
-
Field Details
-
panelWS
WebSocket client for panel communication -
token
Authentication token for API requests -
SERVER_PROPERTIES_FILE
server_properties走独立管理器,不是一个真正的配置文件路径。- See Also:
-
cloudEnabled
云连接是否处于「应当保持连接」的状态。这是整条重连链的唯一开关。在它存在之前,有四个地方各自独立地决定「要不要继续 重连」,而谁都不是所有者:
UltiPanelWebSocketClient.onClose按每实例 5 次算、reinitWebSocket造新实例把计数清零、ulticloud logout只清凭证根本不碰 状态机、只有onDisable真正拆得干净。结果是 logout 之后插件仍在拿已作废的凭证 持续敲面板。见 issue #181 与 #223。现在的规则只有一条:
reinitWebSocket只在本标志为 true 时才会重建连接。 -
cloudLifecycleLock
云管理器「接线」与「拆线」的互斥锁。cloudEnabled单独用是不够的:它只能表达状态,表达不了「检查与动作之间不许有人插队」。initializeManagers()读到 true 之后、真正接线之前,disableCloud()完全可以 插进来把开关置否并把监听器拆干净,随后前者继续往下又把它们装回去——logout 之后监听器 还在,甚至还能继续往面板发事件。见 PR #264 的两轮评审。两边都持这把锁之后,二者只能整体先后发生:要么先接线再拆掉(干净),要么先拆再接而接线 一侧在持锁复查时看到 false 直接返回(也干净)。
-
MAX_REINIT_ATTEMPTS
private static final int MAX_REINIT_ATTEMPTS外层 reinit 的全局上限。超过之后进入终态,需要/ulticloud login或重启才恢复。- See Also:
-
reinitBackoff
外层重连(reinit 循环)的全局预算与退避。客户端自身那 5 次是每实例的上限,而
reinitWebSocket每次都造一个新实例, 于是每实例上限对整体毫无约束——这正是无界循环的成因。本策略是跨实例的, 只有一次成功的onOpen能把它重置。顺带启用了
ExponentialBackoffStrategy——它此前是同包内零引用的死代码。
-
-
Constructor Details
-
PluginInitiationUtils
public PluginInitiationUtils()
-
-
Method Details
-
loginWithToken
Login to UltiPanel using an existing token (from magic-link or saved token). Registers or updates the server without needing username/password.
使用现有令牌登录UltiPanel(来自魔法链接或保存的令牌)。- Parameters:
existingToken- the pre-authenticated token- Returns:
- true if server registration/update succeeded
- Throws:
IOException- if an I/O error occurs
-
initWebsocket
Initialize websocket.
初始化websocket。- Throws:
IOException
-
onWebSocketOpened
握手成功之后的接线动作。参数就是这次握手自己的客户端,方法体内绝不重读静态
panelWS。 onOpen 是异步回调:跑到这里时disableCloud()可能已经把静态字段置空 (/ulticloud logout,或重连预算耗尽——后者跑在 WebSocket 线程上), 也可能reinitWebSocket已经把它换成了另一个实例。重读静态字段的话,subscribeToServer/uploadConfig/uploadServerProperties三处都会踩空——initializeManagers()有持锁复查护着,它前后的代码没有。往一个已断开的客户端发消息是安全的:
sendMessage在未连接时打一条 WARNING 就返回。真正危险的是空引用,所以这里解决的是引用稳定性,不是连接状态。包级可见而非 private —— 只为可测。要在测试里触发它,否则得有真实的鉴权 token 与真实的 WebSocket 握手;与
handleInboundMessage(com.google.gson.JsonObject)同一处理。 -
handleInboundMessage
static void handleInboundMessage(com.google.gson.JsonObject message) 处理面板下发的入站 WebSocket 消息。从
initWebsocket()的 lambda 中提取出来,唯一目的是让它可以被单元测试直接调用: 原先它是setMessageHandler的匿名 lambda,要构造它需要真实的鉴权 token 与真实的 WebSocket 客户端,畸形输入这条路径因此完全没有测试覆盖。见 issue #234。包级可见而非 public —— 它不是对外 API,只是为了可测。
- Parameters:
message- 面板下发的消息,允许为 null
-
initializeManagers
static void initializeManagers()把所有 WebSocket 管理器接到当前连接上。本方法挂在
onConnectHandler上,而/ulticloud logout之后仍可能有一次 在途的握手落地。不设防的话,disableCloud()刚摘掉的监听器会被这次迟到的 onOpen 原样装回去——这正是 #181/#223 里「谁都不是所有者」那个毛病换个地方重现。光检查
cloudEnabled是不够的。那只是一次锁外的读:读到 true 之后、 真正接线之前,disableCloud()完全可以插进来把开关置否并拆干净,然后本方法 继续往下把监听器又装回去。所以接线与拆线必须落在同一把cloudLifecycleLock上, 并在持锁期间复查开关。见 PR #264 的两轮评审。包级可见而非 private —— 只为可测。
-
wireManagers
private static void wireManagers()initializeManagers()的实际接线动作。调用方必须持有cloudLifecycleLock。 -
handleConfigUpdate
static void handleConfigUpdate(com.google.gson.JsonObject data) 处理配置更新。这条路径过去有三处与面板对不上,而且失败是静默的(issue #236):面板在
data.configData里发内容、这里读data.config;面板用data.fileName指定文件、这里除server_properties外从不读它; 面板不发requestId、这里以requestId存在与否作为「是不是一条请求」 的判据,缺了就只记一行Level.FINE然后丢弃——而FINE在默认日志 配置下不打印。于是面板收到 HTTP 200、服务器上什么也没变、两端都不报错。现在的判据换成「有没有配置内容」:没有内容才是回声/回执,有内容就是一条请求, 缺
requestId也照样应用,只是记 WARNING 说明结果无法回报。包级可见而非 private —— 只为可测。
-
readString
读一个可能缺失、也可能是 JSON null 的字符串字段。 -
readConfigContent
取配置内容,优先data.configData。data.config是本方法过去唯一读的字段,但在树内找不到任何生产者—— 面板一直发的是configData。保留它只是为了兼容可能存在的第三方面板, 读到就记废弃日志。 -
applyConfigUpdate
按fileName决定写到哪里。三条分支对应三种载荷形状,这是原来「fileName 从不读」掩盖掉的东西:
server_properties是一份扁平的属性表,交给专用管理器; 指定了文件名就是那一个配置文件自己的{配置项: 值}; 没有文件名才是ConfigManager.toJson()那种全量嵌套结构。- Throws:
IOException
-
sendConfigUpdateResponse
回一条config_update_response。载荷放在
data里,与其余所有 插件→Worker 的消息一致 (见CommandExecutionManager.sendCommandResult)。此前这一条是扁平写法, 字段直接挂在顶层;Worker 侧两种都读(ultipanel-api-worker#30),所以这次改动 不需要和面板同时上线。 -
handlePing
private static void handlePing(com.google.gson.JsonObject message) 处理ping消息 -
handlePong
private static void handlePong(com.google.gson.JsonObject data) 处理pong消息 -
handleSubscribe
private static void handleSubscribe(com.google.gson.JsonObject data) 处理订阅消息 -
handleUnsubscribe
private static void handleUnsubscribe(com.google.gson.JsonObject data) 处理取消订阅消息 -
handleNotification
private static void handleNotification(com.google.gson.JsonObject data) 处理通知消息 -
handleError
private static void handleError(com.google.gson.JsonObject data) 处理错误消息 -
handlePlayerEvent
private static void handlePlayerEvent(com.google.gson.JsonObject data) 处理玩家事件 -
handleCommandResult
private static void handleCommandResult(com.google.gson.JsonObject data) 处理命令执行结果 -
handleFileOperationResult
private static void handleFileOperationResult(com.google.gson.JsonObject data) 处理文件操作结果 -
handleBackupOperation
private static void handleBackupOperation(com.google.gson.JsonObject data) 处理备份操作 -
handleBackupProgress
private static void handleBackupProgress(com.google.gson.JsonObject data) 处理备份进度 -
handleConfigUpload
private static void handleConfigUpload(com.google.gson.JsonObject data) 处理配置上传 -
handleConfigUploadLogic
处理配置上传逻辑- Throws:
Exception
-
sendErrorResponse
发送错误响应 -
handlePluginListRequest
private static void handlePluginListRequest(com.google.gson.JsonObject data) 处理插件列表请求 -
handleServerStatusRequest
private static void handleServerStatusRequest(com.google.gson.JsonObject data) 处理服务器状态请求 -
handleMetricsRequest
private static void handleMetricsRequest(com.google.gson.JsonObject data) 处理性能数据请求 -
uploadConfig
上传本地配置到服务器 -
uploadServerProperties
Upload server.properties safe keys to cloud for panel editing. -
reinitWebSocket
public static void reinitWebSocket()Re-initialize the WebSocket connection with a fresh token. Disconnects the old client (if any), refreshes the token if needed, and creates a new WebSocket client.
使用新令牌重新初始化WebSocket连接。 -
disableCloud
public static void disableCloud()关闭云连接并让重连状态机进入明确的 disabled 态。供
/ulticloud logout调用。与stopWebsocket()的区别是:后者只断开当前 客户端,而重连链会把它重新拉起来;本方法先把cloudEnabled置否,因此reinitWebSocket()之后会直接返回,状态机不会自我复活。顺带摘掉 root logger 上的日志 handler 与传输线程,并停掉 token 刷新调度—— 都是「云功能已关闭」这句话应当为真的组成部分。
整个方法持有
cloudLifecycleLock,与initializeManagers()互斥。 不然的话,一次在途的 onOpen 可以在「置否」与「拆线」之间挤进来,把刚要拆的东西 又装回去。见 PR #264 的两轮评审。 -
doDisableCloud
private static void doDisableCloud()disableCloud()的实际拆线动作。调用方必须持有cloudLifecycleLock。 -
teardownStep
跑一步拆线动作,失败只记 FINE 不向外抛。拆线的每一步都必须尽力执行完:任何一步抛出去都会让它后面的步骤被跳过,而那些 步骤正是「云功能已关闭」这句话的组成部分。原先这是六段一模一样的 try/catch, 提取出来只是把那个不变量说清楚一次,行为不变。
- Parameters:
what- 失败时写进日志的动作描述action- 拆线动作
-
onWebSocketConnected
static void onWebSocketConnected()重连成功时调用:把外层预算清零。只有真正握手成功才配重置预算。若在
reinitWebSocket里重置, 那么「造出了一个客户端」就会被当成成功,预算永远用不完,闸门等于没加。 -
activateCloudIfCurrent
在云生命周期锁内,原子地「复查代际 → 开启状态机 → 建连 → 起刷新调度」。只让写凭证那一步对 logout 原子是不够的:magic-link 轮询在提交凭证之后还要做
enableCloud()+initWebsocket()+startTokenRefreshScheduler(), 这一串才是真正把服务器连回去的动作。logout 挤在「提交成功」与「开始激活」之间的话, 拆线拆的是一个还没建起来的连接,随后轮询线程照样把它建起来——logout 于是被撤销。这里与
disableCloud()抢同一把cloudLifecycleLock,因此二者只能整体 先后发生:要么先激活再被拆掉(干净),要么先拆线、本方法持锁复查代际时看到已变而 直接返回 false(也干净)。锁内刻意不做
loginWithToken()—— 那是一次 HTTP 往返,持锁做会让/ulticloud logout在主线程上阻塞数秒。它只向面板注册服务器,不改本地状态, 放在锁外重复执行也无害。- Parameters:
generation- 调用方出发时记下的凭证代际- Returns:
- 已激活返回 true;代际已变、激活被放弃则返回 false
- Throws:
IOException- 建连失败
-
enableCloud
public static void enableCloud()把状态机置为「应当保持连接」,并清零外层重连预算。只有显式动作才应当调用它:服务器启动时的云登录,以及
/ulticloud login成功之后。initWebsocket()刻意不调——它同时被reinitWebSocket()复用, 在那里置位会让一个正在途中的重连把刚被 logout 关掉的状态机重新拉起来。 -
isCloudEnabled
static boolean isCloudEnabled()供测试断言状态机是否处于启用态。 -
stopWebsocket
public static void stopWebsocket() -
getPanelWebsocketClient
- Throws:
IOException
-
safeGetString
-
safeGetBoolean
private static boolean safeGetBoolean(com.google.gson.JsonObject obj, String key, boolean defaultValue) -
safeGetLong
-
safeGetDouble
private static double safeGetDouble(com.google.gson.JsonObject obj, String key, double defaultValue)
-