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.
    • 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.1.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.
    • 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.

      As of 6.4.0 the echo line, the refusal line and the remote-action log entry show a command registered in the SensitiveCommandRegistry without its arguments (see loggedCommandLine(String, String)); the command itself is dispatched unchanged.

      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).
    • loggedCommandLine

      static String loggedCommandLine(String raw, String canonical)
      The command line as the framework's own records show it: the remote-command echo and refusal lines in the server log (and so in latest.log and the panel's log stream), and the target of the remote-action log.

      A command a module registered in the SensitiveCommandRegistry is shown as its canonical command followed by *** in place of its arguments, for example logadmin *** for /logadmin reset Steve hunter2. The canonical form is matched, not the raw text, because that is what is dispatched: a leading "/ " or a no-break space between the command and its arguments must not hide a registered command. Any other command line is shown exactly as received, as before 6.4.0. Without a framework instance or registry nothing can be registered, so the raw line is returned.

      Parameters:
      raw - the command line as received
      canonical - the same line after canonicalCommandLine(String)
      Returns:
      the text to log and record
      Since:
      6.4.0
    • 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.
    • 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.