Class CommandExecutionManager

java.lang.Object
com.ultikits.ultitools.manager.CommandExecutionManager

@Internal public class CommandExecutionManager extends Object
Command execution manager. Handles command execution requests received over the WebSocket connection.
  • Field Details

    • PANEL_DISPATCH

      private static final ThreadLocal<Boolean> PANEL_DISPATCH
      Panel origin exists only inside synchronous main-thread dispatch, including nested commands.
    • webSocketClient

      private UltiPanelWebSocketClient webSocketClient
    • pendingCommands

    • gson

      private final com.google.gson.Gson gson
    • BLOCKLIST_CONFIG_KEY

      private static final String BLOCKLIST_CONFIG_KEY
      The literal config path loadConfiguration() reads and every configurable refusal this class produces names — ultipanel.commands.blocklist.
      See Also:
    • ACTION_EXECUTE_COMMAND

      private static final String ACTION_EXECUTE_COMMAND
      The RemoteActionLog.Entry.getAction() literal for every entry this class records.
      See Also:
    • MAX_OUTPUT_LINES

      private static final int MAX_OUTPUT_LINES
      command.result.data.output maxItems in the frozen 2.0.0 contract.
      See Also:
    • MAX_OUTPUT_LINE_CHARS

      private static final int MAX_OUTPUT_LINE_CHARS
      command.result.data.output items maxLength, counted in Unicode code points as the schema and PayloadReader count it.
      See Also:
    • MAX_RESULT_MESSAGE_CHARS

      private static final int MAX_RESULT_MESSAGE_CHARS
      command.result payload message maxLength, counted in Unicode code points.
      See Also:
    • MAX_EXECUTION_MS

      private static final long MAX_EXECUTION_MS
      command.result.data.executionMs maximum in the frozen contract.
      See Also:
    • LATE_OUTPUT_WINDOW_NANOS

      static final long LATE_OUTPUT_WINDOW_NANOS
      How long after a v2 command's result was taken its late output is still written to the server console — see CommandExecutionManager.CapturedFeedback. Long enough for asynchronous command bodies that wait on a database or the network; it bounds how long the framework answers for a capturing sender that a command or another plugin kept.
    • DISPATCHED_OUTPUT

      static final String DISPATCHED_OUTPUT
      The command_result text for a command the server console accepted. A panel command is typed into the server console (maintainer decision, 6.3.0): it runs as the console sender, and Paper sends a console command's replies to the console itself, never to a sender the framework could read, so the result cannot carry them. They reach the panel through the log stream, which mirrors the console (ConsoleMirror).
      See Also:
    • NOT_ACCEPTED_OUTPUT

      static final String NOT_ACCEPTED_OUTPUT
      The v1 command_result text for a command the server console did not accept (Bukkit.dispatchCommand returned false: an unknown command, or a command whose executor reported a usage error). The v1 path still dispatches as the real console, so Paper writes the reply to the console and this sentence stays true there. The v2 path captures that reply instead and sends it — see executeCaptured(java.lang.String, com.ultikits.ultitools.manager.CommandExecutionManager.CommandResultSink) and NOT_ACCEPTED_WITHOUT_REPLY.
      See Also:
    • NOT_ACCEPTED_WITHOUT_REPLY

      static final String NOT_ACCEPTED_WITHOUT_REPLY
      The v2 command_result text for a command the server did not accept while printing nothing the capturing sender could read — an unknown command whose refusal goes elsewhere, or an executor that reports a usage error silently. When the server did print something, rejectionMessage(java.util.List<java.lang.String>) sends those lines instead of this sentence.
      See Also:
    • blockedCommands

      private Set<String> blockedCommands
      Blocklist of dangerous commands that should not be executed remotely. This is the shipped default, kept when ultipanel.commands.blocklist is absent from config.yml or when its present value is rejected as not a list of strings — see loadConfiguration().
  • Constructor Details

    • CommandExecutionManager

      public CommandExecutionManager()
  • Method Details

    • loadConfiguration

      public final void loadConfiguration()
      Loads ultipanel.commands.blocklist from config.yml, copying ErrorReportCollector.loadConfiguration()'s exact shape: a null-guard on UltiTools.getInstance(), one try/catch whose failure branch keeps the current blocklist and logs a warning naming the key.

      Distinguishes an absent key from an explicit empty list via isSet(path) — Bukkit's getStringList returns an empty list for both, and collapsing that distinction would make D-03's honest "block nothing" escape value silently impossible (T-06-12). The shipped ten-command default applies only when the key is absent; an explicit empty list is honored as the operator deliberately granting every command.

      A present value is validated as a whole before anything is installed: it must be a list whose every element is a string. The raw value is read, not Bukkit's converting getStringList, because that getter turns a non-list into an empty list, drops mapping, nested-list and null elements, and stringifies numbers and booleans — so its output cannot show what the operator wrote, and installing it would silently replace the policy with an empty or partial one. Any other shape (a scalar, a map, or a list with any non-string element) rejects the whole value: one warning naming the key and the rejected shape (the value's type, or the first offending element's position and type) is logged, never the value itself, and the current in-memory blocklist is kept — the shipped default on a newly constructed manager, the previous valid list on a repeated call. A valid list is trimmed and lower-cased with Locale.ROOT into one new set, installed through a single setBlockedCommands(Set) call.

      This method never writes config.yml or the loaded configuration: it neither saves nor repairs an invalid value, so the operator's file and the in-memory section keep exactly what the operator wrote.

    • describeInvalidBlocklist

      private static String describeInvalidBlocklist(Object raw)
      Describes why a raw blocklist value is not a list of strings, naming only its shape.
      Parameters:
      raw - the raw value at ultipanel.commands.blocklist
      Returns:
      null for a list whose every element is a string; otherwise the value's type, or the first non-string element's 1-based position and type — never the value itself
    • describeShape

      private static String describeShape(Object value)
      Names a raw configuration value's type for a diagnostic, without its content.
    • setBlockedCommands

      public void setBlockedCommands(Set<String> commands)
      Set the blocklist of commands that should not be executed remotely. This is now loadConfiguration()'s config-load target, not just a test-only seam — it has zero production call sites of its own, but every operator-configured blocklist reaches this class through it.
      Parameters:
      commands - Set of command names to block (case-insensitive)
    • isCommandAllowed

      public AccessDecision isCommandAllowed(String command)
      Whether a command is allowed for remote execution.

      Extracts the base command (the first word) and strips any namespace prefix (e.g. bukkit:op -> op, minecraft:stop -> stop) BEFORE checking it against the operator-configured blocklist at ultipanel.commands.blocklist in plugins/UltiTools/config.yml — so both the bare command and its namespaced form are blocked; the stripping order is what makes that true. The blocklist is editable in both directions — an operator may remove any of the ten shipped defaults, add to them, or clear the list entirely. Separately, direct forms of UltiCloud's own login and logout subcommands are refused with command_blocked, regardless of the configured blocklist. Command names come from CloudLoginCommand's CmdExecutor aliases, the same metadata used for registration. Indirect synchronous forms (an alias or a command that dispatches another command) are refused by the authentication handler while isPanelDispatch() is true; that refusal is normal command output, not a command_blocked result. The fixed rule does not refuse the root command, status, reconnect or help.

      Parameters:
      command - The command to check (with or without leading slash)
      Returns:
      an AccessDecision naming why a refused command was refused
    • canonicalCommandDecision

      private AccessDecision canonicalCommandDecision(String command)
      Checks text already normalized at the request boundary; dispatch receives that same text.
    • setWebSocketClient

      @Deprecated(since="6.3.1", forRemoval=true) public void setWebSocketClient(UltiPanelWebSocketClient client)
      Deprecated, for removal: This API element is subject to removal in a future version.
      There is no replacement: the framework never calls it, and its own UltiCloud session (protocol 2.0.0) carries the panel requests. Removed in 6.4.0.
      Sets the WebSocket client.
      Parameters:
      client - the WebSocket client
      Scheduled removal version:
      6.4.0
    • execute

      public void execute(String commandLine, String actor, Executor mainThread, CommandExecutionManager.CommandResultSink sink)
      Executes a v2 request using Paper's console-permission feedback sender. The caller owns acknowledgement, deduplication, lifecycle checks and transport byte limits. The 6.3.0 validation order runs on the calling thread, so a refusal is answered without waiting for the main thread; only the dispatch and its feedback capture are handed to the injected executor, which must enqueue work onto the server main thread in arrival order. Output is cut to the command.result schema bounds — 1000 lines of 4096 code points — and the flag reports that cut. Only feedback printed before the dispatch returns is in the result. A line the command prints later, for example from an asynchronous body, is written to the server console instead, as 6.3.0 printed every panel command's output there; the log stream carries it from there, redacted like every other console line (see CommandExecutionManager.CapturedFeedback). This method only reads operator configuration and never rewrites it.
      Parameters:
      commandLine - command text, optionally prefixed with a slash
      actor - audit actor, or null for the panel
      mainThread - ordered server-main-thread executor
      sink - receives the command outcome
      Since:
      6.3.1
    • canonicalCommandLine

      private static String canonicalCommandLine(String raw)
      Normalizes a panel request once, before both policy and dispatch. Retains the framework's optional leading-slash removal and Paper's trim semantics. Paper 1.21.11 uses StringUtils.normalizeSpace: Character.isWhitespace(char) plus U+00A0, not all isSpaceChar characters. Collapse their combined runs to make the result stable when Paper normalizes it again (adjacent NBSPs otherwise become adjacent spaces).
    • baseCommandName

      private static String baseCommandName(String commandLine)
      Extracts a lower-case, namespace-free name from an already canonical command line.
    • isPanelDispatch

      public static boolean isPanelDispatch()
      Whether a synchronous command is running inside a panel dispatch on this thread. Authentication handlers use this at execution, so nested aliases cannot bypass the direct text refusal. Their refusal reaches the panel as normal command output.
      Returns:
      true only during a panel dispatch, including synchronous nested dispatches
    • dispatchPanelCommand

      private static boolean dispatchPanelCommand(org.bukkit.command.CommandSender sender, String command)
      Restores the caller's origin even when a nested dispatch throws. Both panel paths use this.
    • resultMessage

      private static String resultMessage(String message)
      Fits a result message inside the schema bound and never returns a blank one.
    • recordCommandDecision

      private void recordCommandDecision(String command, String actor, String refusal)
    • executeCaptured

      private void executeCaptured(String commandLine, CommandExecutionManager.CommandResultSink sink)
    • writeToConsole

      static void writeToConsole(String line)
      Writes one late output line of a panel command to the server console: the server's own logger, at INFO, with the text unchanged. The local console and log files keep the text as printed, like every console line; the log stream picks the record up from the java.util.logging root logger and redacts registered sensitive commands (SensitiveCommandRegistry) on its way to UltiCloud, exactly as for any other console line.
    • rejectionMessage

      private static String rejectionMessage(List<String> lines)
      Turns the feedback a refused command printed into the failure message.

      With the capturing sender the server's own reply — Paper's unknown-command line, a Bukkit command's usage text — arrives here instead of reaching the console, so it is the only reply the panel can get and it must not be dropped. The lines are bounded exactly as successful output is (4096 code points per line, at most 1000 lines) and then cut to the message bound, which is the schema slot they travel in.

      Parameters:
      lines - feedback captured before dispatchCommand returned
      Returns:
      the joined reply, or NOT_ACCEPTED_WITHOUT_REPLY when nothing was printed
    • boundLines

      private static CommandExecutionManager.BoundedLines boundLines(List<String> lines)
      Copies lines under the command.result output caps: every line cut to 4096 code points and the list to 1000 entries, reporting whether anything was cut.
    • executeCommand

      @Deprecated(since="6.3.1", forRemoval=true) public void executeCommand(com.google.gson.JsonObject commandData)
      Deprecated, for removal: This API element is subject to removal in a future version.
      Use execute(String, String, Executor, CommandResultSink), which the protocol 2.0.0 session calls for a panel command; this v1 handler replies only over the client handed to setWebSocketClient. Removed in 6.4.0.
      Executes a command received from the panel.
      Scheduled removal version:
      6.4.0
    • executeCommandInternal

      private void executeCommandInternal(String command, String executor, String commandId, long startTime)
      Internal command execution logic.
    • resolveActor

      private static String resolveActor(String executor)
      The action-log actor field — the inbound executor field verbatim, or the literal "panel" when absent. Mirrors PluginInitiationUtils.resolveActor(JsonObject) exactly: the framework cannot attribute a remote command to an individual panel operator today (see RemoteActionLog.Entry's javadoc), so this never invents a per-operator identity.
    • sendCommandResult

      private void sendCommandResult(String commandId, boolean success, String output, long executionTime)
      Sends the command execution result.