001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.command3;
006
007import static org.wpilib.units.Units.Microseconds;
008import static org.wpilib.units.Units.Milliseconds;
009
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.Collections;
013import java.util.HashSet;
014import java.util.LinkedHashMap;
015import java.util.LinkedHashSet;
016import java.util.List;
017import java.util.Map;
018import java.util.SequencedMap;
019import java.util.SequencedSet;
020import java.util.Set;
021import java.util.Stack;
022import java.util.function.Consumer;
023import java.util.stream.Collectors;
024import org.wpilib.annotation.NoDiscard;
025import org.wpilib.command3.button.CommandGenericHID;
026import org.wpilib.command3.proto.SchedulerProto;
027import org.wpilib.event.EventLoop;
028import org.wpilib.framework.TimedRobot;
029import org.wpilib.system.RobotController;
030import org.wpilib.util.ErrorMessages;
031import org.wpilib.util.protobuf.ProtobufSerializable;
032
033/**
034 * Manages the lifecycles of {@link Coroutine}-based {@link Command Commands}. Commands may be
035 * scheduled directly using {@link #schedule(Command)}, or be bound to {@link Trigger Triggers} to
036 * automatically handle scheduling and cancellation based on internal or external events. User code
037 * is responsible for calling {@link #run()} periodically to update trigger conditions and execute
038 * scheduled commands. Most often, this is done by overriding {@link TimedRobot#robotPeriodic()} to
039 * include a call to {@code Scheduler.getDefault().run()}:
040 *
041 * <pre>{@code
042 * public class Robot extends TimedRobot {
043 *   @Override
044 *   public void robotPeriodic() {
045 *     // Update the scheduler on every loop
046 *     Scheduler.getDefault().run();
047 *   }
048 * }
049 * }</pre>
050 *
051 * <h2>Danger</h2>
052 *
053 * <p>The scheduler <i>must</i> be used in a single-threaded program. Commands must be scheduled and
054 * canceled by the same thread that runs the scheduler, and cannot be run in a virtual thread.
055 *
056 * <p><strong>Using the commands framework in a multithreaded environment can cause crashes in the
057 * Java virtual machine at any time, including on an official field during a match.</strong> The
058 * Java JIT compilers make assumptions that rely on coroutines being used on a single thread.
059 * Breaking those assumptions can cause incorrect JIT code to be generated with undefined behavior,
060 * potentially causing control issues or crashes deep in JIT-generated native code.
061 *
062 * <p><strong>Normal concurrency constructs like locks, atomic references, and synchronized blocks
063 * or methods cannot save you.</strong>
064 *
065 * <h2>Lifecycle</h2>
066 *
067 * <p>The {@link #run()} method runs five steps:
068 *
069 * <ol>
070 *   <li>Call {@link #sideload(Consumer) periodic sideload functions}
071 *   <li>Poll all registered triggers to queue and cancel commands
072 *   <li>Queue default commands for any mechanisms without a running command. The queued commands
073 *       can be superseded by any manual scheduling or commands scheduled by triggers in the next
074 *       run.
075 *   <li>Start all queued commands. This happens after all triggers are checked in case multiple
076 *       commands with conflicting requirements are queued in the same update; the last command to
077 *       be queued takes precedence over the rest.
078 *   <li>Loop over all running commands, mounting and calling each in turn until they either exit or
079 *       call {@link Coroutine#yield()}. Commands run in the order in which they were scheduled.
080 * </ol>
081 *
082 * <h2>Telemetry</h2>
083 *
084 * <p>There are two mechanisms for telemetry for a scheduler. A protobuf serializer can be used to
085 * take a snapshot of a scheduler instance, and report what commands are queued (scheduled but have
086 * not yet started to run), commands that are running (along with timing data for each command), and
087 * total time spent in the most recent {@link #run()} call. However, it cannot detect one-shot
088 * commands that are scheduled, run, and complete all in a single {@code run()} invocation -
089 * effectively, commands that never call {@link Coroutine#yield()} are invisible.
090 *
091 * <p>A second telemetry mechanism is provided by {@link #addEventListener(Consumer)}. The scheduler
092 * will issue events to all registered listeners when certain events occur (see {@link
093 * SchedulerEvent} for all event types). These events are emitted immediately and can be used to
094 * detect lifecycle events for all commands, including one-shots that would be invisible to the
095 * protobuf serializer. However, it is up to the user to log those events themselves.
096 */
097public final class Scheduler implements ProtobufSerializable {
098  private final Map<Mechanism, Command> m_defaultCommands = new LinkedHashMap<>();
099
100  /** The set of commands scheduled since the start of the previous run. */
101  private final SequencedSet<CommandState> m_queuedToRun = new LinkedHashSet<>();
102
103  /**
104   * The states of all running commands (does not include on deck commands). We preserve insertion
105   * order to guarantee that child commands run after their parents.
106   */
107  private final SequencedMap<Command, CommandState> m_runningCommands = new LinkedHashMap<>();
108
109  /**
110   * The stack of currently executing commands. Child commands are pushed onto the stack and popped
111   * when they complete. Use {@link #currentState()} and {@link #currentCommand()} to get the
112   * currently executing command or its state.
113   */
114  private final Stack<CommandState> m_currentCommandAncestry = new Stack<>();
115
116  /** The periodic callbacks to run, outside of the command structure. */
117  private final List<Coroutine> m_periodicCallbacks = new ArrayList<>();
118
119  /** Event loop for trigger bindings. */
120  private final EventLoop m_eventLoop = new EventLoop();
121
122  /** The scope for continuations to yield to. */
123  private final ContinuationScope m_scope = new ContinuationScope("coroutine commands");
124
125  // Telemetry
126  /** Protobuf serializer for a scheduler. */
127  public static final SchedulerProto proto = new SchedulerProto();
128
129  private double m_lastRunTimeMs = -1;
130
131  private final Set<Consumer<? super SchedulerEvent>> m_eventListeners = new LinkedHashSet<>();
132
133  /** The default scheduler instance. */
134  private static final Scheduler s_defaultScheduler = new Scheduler();
135
136  /**
137   * Gets the default scheduler instance for use in a robot program. Unless otherwise specified,
138   * triggers and mechanisms will be registered with the default scheduler and require the default
139   * scheduler to run. However, triggers and mechanisms can be constructed to be registered with a
140   * specific scheduler instance, which may be useful for isolation for unit tests.
141   *
142   * @return the default scheduler instance.
143   */
144  @NoDiscard
145  public static Scheduler getDefault() {
146    return s_defaultScheduler;
147  }
148
149  /**
150   * Creates a new scheduler object. Note that most built-in constructs like {@link Trigger} and
151   * {@link CommandGenericHID} will use the {@link #getDefault() default scheduler instance} unless
152   * they were explicitly constructed with a different scheduler instance. Teams should use the
153   * default instance for convenience; however, new scheduler instances can be useful for unit
154   * tests.
155   *
156   * @return a new scheduler instance that is independent of the default scheduler instance.
157   */
158  @NoDiscard
159  public static Scheduler createIndependentScheduler() {
160    return new Scheduler();
161  }
162
163  /** Private constructor. Use static factory methods or the default scheduler instance. */
164  private Scheduler() {}
165
166  /**
167   * Sets the default command for a mechanism. The command must require that mechanism, and cannot
168   * require any other mechanisms.
169   *
170   * @param mechanism the mechanism for which to set the default command
171   * @param defaultCommand the default command to execute on the mechanism
172   * @throws IllegalArgumentException if the command does not meet the requirements for being a
173   *     default command
174   */
175  public void setDefaultCommand(Mechanism mechanism, Command defaultCommand) {
176    if (!defaultCommand.requires(mechanism)) {
177      throw new IllegalArgumentException(
178          "A mechanism's default command must require that mechanism");
179    }
180
181    if (defaultCommand.requirements().size() > 1) {
182      throw new IllegalArgumentException(
183          "A mechanism's default command cannot require other mechanisms");
184    }
185
186    m_defaultCommands.put(mechanism, defaultCommand);
187  }
188
189  /**
190   * Gets the default command set for a mechanism.
191   *
192   * @param mechanism The mechanism
193   * @return The default command, or null if no default command was ever set
194   */
195  public Command getDefaultCommandFor(Mechanism mechanism) {
196    return m_defaultCommands.get(mechanism);
197  }
198
199  /**
200   * Adds a callback to run as part of the scheduler. The callback should not manipulate or control
201   * any mechanisms, but can be used to log information, update data (such as simulations or LED
202   * data buffers), or perform some other helpful task. The callback is responsible for managing its
203   * own control flow and end conditions. If you want to run a single task periodically for the
204   * entire lifespan of the scheduler, use {@link #addPeriodic(Runnable)}.
205   *
206   * <p><strong>Note:</strong> Like commands, any loops in the callback must appropriately yield
207   * control back to the scheduler with {@link Coroutine#yield} or risk stalling your program in an
208   * unrecoverable infinite loop!
209   *
210   * @param callback the callback to sideload
211   */
212  public void sideload(Consumer<Coroutine> callback) {
213    var coroutine = new Coroutine(this, m_scope, callback);
214    m_periodicCallbacks.add(coroutine);
215  }
216
217  /**
218   * Adds a task to run repeatedly for as long as the scheduler runs. This internally handles the
219   * looping and control yielding necessary for proper function. The callback will run at the same
220   * periodic frequency as the scheduler.
221   *
222   * <p>For example:
223   *
224   * <pre>{@code
225   * scheduler.addPeriodic(() -> leds.setData(ledDataBuffer));
226   * scheduler.addPeriodic(() -> {
227   *   SmartDashboard.putNumber("X", getX());
228   *   SmartDashboard.putNumber("Y", getY());
229   * });
230   * }</pre>
231   *
232   * @param callback the periodic function to run
233   */
234  public void addPeriodic(Runnable callback) {
235    sideload(
236        coroutine -> {
237          while (true) {
238            callback.run();
239            coroutine.yield();
240          }
241        });
242  }
243
244  /** Represents possible results of a command scheduling attempt. */
245  public enum ScheduleResult {
246    /** The command was successfully scheduled and added to the queue. */
247    SUCCESS,
248    /** The command is already scheduled or running. */
249    ALREADY_RUNNING,
250    /** The command is a lower priority and conflicts with a command that's already running. */
251    LOWER_PRIORITY_THAN_RUNNING_COMMAND,
252  }
253
254  /**
255   * Schedules a command to run. If one command schedules another (a "parent" and "child"), the
256   * child command will be canceled when the parent command completes. It is not possible to fork a
257   * child command and have it live longer than its parent.
258   *
259   * <p>Does nothing if the command is already scheduled or running, or requires at least one
260   * mechanism already used by a higher priority command.
261   *
262   * @param command the command to schedule
263   * @return the result of the scheduling attempt. See {@link ScheduleResult} for details.
264   * @throws IllegalArgumentException if scheduled by a command composition that has already
265   *     scheduled another command that shares at least one required mechanism
266   */
267  // Implementation detail: a child command will immediately start running when scheduled by a
268  // parent command, skipping the queue entirely. This avoids dead loop cycles where a parent
269  // schedules a child, appending it to the queue, then waits for the next cycle to pick it up and
270  // start it. With deeply nested commands, dead loops could quickly to add up and cause the
271  // innermost commands that actually _do_ something to start running hundreds of milliseconds after
272  // their root ancestor was scheduled.
273  public ScheduleResult schedule(Command command) {
274    // Get the narrowest binding scope.
275    // This prevents commands from outliving the opmodes that scheduled them, or from outliving
276    // their parents (eg if someone writes a command that manually calls schedule(Command) instead
277    // of using triggers to do so).
278    Command currentCommand = currentCommand();
279    long currentOpmode = OpModeFetcher.getFetcher().getOpModeId();
280
281    BindingScope scope;
282    if (currentCommand != null) {
283      scope = BindingScope.forCommand(this, currentCommand);
284    } else if (currentOpmode != 0) {
285      scope = BindingScope.forOpmode(currentOpmode);
286    } else {
287      scope = BindingScope.global();
288    }
289
290    // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier
291    //       stack frame filtering and modification.
292    var binding =
293        new Binding(scope, BindingType.IMMEDIATE, command, new Throwable().getStackTrace());
294
295    return schedule(binding);
296  }
297
298  // Package-private for use by Trigger
299  ScheduleResult schedule(Binding binding) {
300    var command = binding.command();
301
302    if (isScheduledOrRunning(command)) {
303      return ScheduleResult.ALREADY_RUNNING;
304    }
305
306    if (lowerPriorityThanConflictingCommands(command)) {
307      return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND;
308    }
309
310    for (var scheduledState : m_queuedToRun) {
311      if (!command.conflictsWith(scheduledState.command())) {
312        // No shared requirements, skip
313        continue;
314      }
315      if (command.isLowerPriorityThan(scheduledState.command())) {
316        // Lower priority than an already-scheduled (but not yet running) command that requires at
317        // one of the same mechanism. Ignore it.
318        return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND;
319      }
320    }
321
322    // Evict conflicting on-deck commands
323    // We check above if the input command is lower priority than any of these,
324    // so at this point we're guaranteed to be >= priority than anything already on deck
325    evictConflictingOnDeckCommands(command);
326
327    // If the binding is scoped to a particular command, that command is the parent. If we're in the
328    // middle of a run cycle and running commands, the parent is whatever command is currently
329    // running. Otherwise, there is no parent command.
330    var parentCommand =
331        binding.scope() instanceof BindingScope.ForCommand scope
332            ? scope.command()
333            : currentCommand();
334    var state = new CommandState(command, parentCommand, buildCoroutine(command), binding);
335
336    emitScheduledEvent(command);
337
338    if (currentState() != null) {
339      // Scheduling a child command while running. Start it immediately instead of waiting a full
340      // cycle. This prevents issues with deeply nested command groups taking many scheduler cycles
341      // to start running the commands that actually _do_ things
342      evictConflictingRunningCommands(state);
343      m_runningCommands.put(command, state);
344      runCommand(state);
345    } else {
346      // Scheduling outside a command, add it to the pending set. If it's not overridden by another
347      // conflicting command being scheduled in the same scheduler loop, it'll be promoted and
348      // start to run when #runCommands() is called
349      m_queuedToRun.add(state);
350    }
351
352    return ScheduleResult.SUCCESS;
353  }
354
355  /**
356   * Checks if a command conflicts with and is a lower priority than any running command. Used when
357   * determining if the command can be scheduled.
358   */
359  private boolean lowerPriorityThanConflictingCommands(Command command) {
360    Set<CommandState> ancestors = new HashSet<>();
361    for (var state = currentState(); state != null; state = m_runningCommands.get(state.parent())) {
362      ancestors.add(state);
363    }
364
365    // Check for conflicts with the commands that are already running
366    for (var state : m_runningCommands.values()) {
367      if (ancestors.contains(state)) {
368        // Can't conflict with an ancestor command
369        continue;
370      }
371
372      var c = state.command();
373      if (c.conflictsWith(command) && command.isLowerPriorityThan(c)) {
374        return true;
375      }
376    }
377
378    return false;
379  }
380
381  private void evictConflictingOnDeckCommands(Command command) {
382    for (var iterator = m_queuedToRun.iterator(); iterator.hasNext(); ) {
383      var scheduledState = iterator.next();
384      var scheduledCommand = scheduledState.command();
385      if (scheduledCommand.conflictsWith(command)) {
386        // Remove the lower priority conflicting command from the on deck commands.
387        // We don't need to call removeOrphanedChildren here because it hasn't started yet,
388        // meaning it hasn't had a chance to schedule any children
389        iterator.remove();
390        emitInterruptedEvent(scheduledCommand, command);
391        emitCanceledEvent(scheduledCommand);
392      }
393    }
394  }
395
396  /**
397   * Cancels all running commands with which an incoming state conflicts. Ancestor commands of the
398   * incoming state will not be canceled.
399   */
400  @SuppressWarnings("PMD.CompareObjectsWithEquals")
401  private void evictConflictingRunningCommands(CommandState incomingState) {
402    // The set of root states with which the incoming state conflicts but does not inherit from
403    Set<CommandState> conflictingRootStates =
404        m_runningCommands.values().stream()
405            .filter(state -> incomingState.command().conflictsWith(state.command()))
406            .filter(state -> !isAncestorOf(state.command(), incomingState))
407            .map(
408                state -> {
409                  // Find the highest level ancestor of the conflicting command from which the
410                  // incoming state does _not_ inherit. If they're totally unrelated, this will
411                  // get the very root ancestor; otherwise, it'll return a direct sibling of the
412                  // incoming command
413                  CommandState root = state;
414                  while (root.parent() != null && root.parent() != incomingState.parent()) {
415                    root = m_runningCommands.get(root.parent());
416                  }
417                  return root;
418                })
419            .collect(Collectors.toSet());
420
421    // Cancel the root commands
422    for (var conflictingState : conflictingRootStates) {
423      emitInterruptedEvent(conflictingState.command(), incomingState.command());
424      cancel(conflictingState.command());
425    }
426  }
427
428  /**
429   * Checks if a particular command is an ancestor of another.
430   *
431   * @param ancestor the potential ancestor for which to search
432   * @param state the state to check
433   * @return true if {@code ancestor} is the direct parent or indirect ancestor, false if not
434   */
435  @SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.SimplifyBooleanReturns"})
436  private boolean isAncestorOf(Command ancestor, CommandState state) {
437    if (state.parent() == null) {
438      // No parent, cannot inherit
439      return false;
440    }
441    if (!m_runningCommands.containsKey(ancestor)) {
442      // The given ancestor isn't running
443      return false;
444    }
445    if (state.parent() == ancestor) {
446      // Direct child
447      return true;
448    }
449    // Check if the command's parent inherits from the given ancestor
450    return m_runningCommands.values().stream()
451        .filter(s -> state.parent() == s.command())
452        .anyMatch(s -> isAncestorOf(ancestor, s));
453  }
454
455  /**
456   * Cancels a command and any other command scheduled by it. This occurs immediately and does not
457   * need to wait for a call to {@link #run()}. Any command that it scheduled will also be canceled
458   * to ensure commands within compositions do not continue to run.
459   *
460   * <p>This has no effect if the given command is not currently scheduled or running.
461   *
462   * @param command the command to cancel
463   */
464  @SuppressWarnings("PMD.CompareObjectsWithEquals")
465  public void cancel(Command command) {
466    if (command == currentCommand()) {
467      throw new IllegalArgumentException(
468          "Command `" + command.name() + "` is mounted and cannot be canceled");
469    }
470
471    boolean running = isRunning(command);
472
473    // Evict the command. The next call to run() will schedule the default command for all its
474    // required mechanisms, unless another command requiring those mechanisms is scheduled between
475    // calling cancel() and calling run()
476    m_runningCommands.remove(command);
477    m_queuedToRun.removeIf(state -> state.command() == command);
478
479    if (running) {
480      // Only run the hook if the command was running. If it was on deck or not
481      // even in the scheduler at the time, then there's nothing to do
482      command.onCancel();
483      emitCanceledEvent(command);
484    }
485
486    // Clean up any orphaned child commands; their lifespan must not exceed the parent's
487    removeOrphanedChildren(command);
488  }
489
490  /**
491   * Updates the command scheduler. This will run operations in the following order:
492   *
493   * <ol>
494   *   <li>Run sideloaded functions from {@link #sideload(Consumer)} and {@link
495   *       #addPeriodic(Runnable)}
496   *   <li>Update trigger bindings to queue and cancel bound commands
497   *   <li>Queue default commands for mechanisms that do not have a queued or running command
498   *   <li>Promote queued commands to the running set
499   *   <li>For every command in the running set, mount and run that command until it calls {@link
500   *       Coroutine#yield()} or exits
501   * </ol>
502   *
503   * <p>This method is intended to be called in a periodic loop like {@link
504   * TimedRobot#robotPeriodic()}
505   */
506  public void run() {
507    final long startMicros = RobotController.getTime();
508
509    // Sideloads may change some state that affects triggers. Run them first.
510    runPeriodicSideloads();
511
512    // Poll triggers next to schedule and cancel commands
513    m_eventLoop.poll();
514
515    // Schedule default commands for any mechanisms that don't have a running command and didn't
516    // have a new command scheduled by a sideload function or a trigger
517    scheduleDefaultCommands();
518
519    // Move all scheduled commands to the running set
520    promoteScheduledCommands();
521
522    // Run every command in order until they call Coroutine.yield() or exit
523    runCommands();
524
525    final long endMicros = RobotController.getTime();
526    m_lastRunTimeMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds);
527  }
528
529  private void promoteScheduledCommands() {
530    // Clear any commands that conflict with the scheduled set
531    for (var queuedState : m_queuedToRun) {
532      evictConflictingRunningCommands(queuedState);
533    }
534
535    // Move any scheduled commands to the running set
536    for (var queuedState : m_queuedToRun) {
537      m_runningCommands.put(queuedState.command(), queuedState);
538    }
539
540    // Clear the set of on-deck commands,
541    // since we just put them all into the set of running commands
542    m_queuedToRun.clear();
543  }
544
545  private void runPeriodicSideloads() {
546    // Update periodic callbacks
547    for (Coroutine coroutine : m_periodicCallbacks) {
548      coroutine.mount();
549      try {
550        coroutine.runToYieldPoint();
551      } finally {
552        Continuation.mountContinuation(null);
553      }
554    }
555
556    // And remove any periodic callbacks that have completed
557    m_periodicCallbacks.removeIf(Coroutine::isDone);
558  }
559
560  private void runCommands() {
561    // Tick every command that hasn't been completed yet
562    // Run in reverse so parent commands can resume in the same loop cycle an awaited child command
563    // completes. Otherwise, parents could only resume on the next loop cycle, introducing a delay
564    // at every layer of nesting.
565    for (var state : List.copyOf(m_runningCommands.values()).reversed()) {
566      runCommand(state);
567    }
568  }
569
570  /**
571   * Mounts and runs a command until it yields or exits.
572   *
573   * @param state The command state to run
574   */
575  @SuppressWarnings("PMD.AvoidCatchingGenericException")
576  private void runCommand(CommandState state) {
577    final var command = state.command();
578    final var coroutine = state.coroutine();
579
580    if (!m_runningCommands.containsKey(command)) {
581      // Probably canceled by an owning composition, do not run
582      return;
583    }
584
585    var previousState = currentState();
586
587    m_currentCommandAncestry.push(state);
588    long startMicros = RobotController.getTime();
589    emitMountedEvent(command);
590    coroutine.mount();
591    try {
592      coroutine.runToYieldPoint();
593    } catch (RuntimeException e) {
594      // Command encountered an uncaught exception.
595      handleCommandException(state, e);
596    } finally {
597      long endMicros = RobotController.getTime();
598      double elapsedMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds);
599      state.setLastRuntimeMs(elapsedMs);
600
601      if (state.equals(currentState())) {
602        // Remove the command we just ran from the top of the stack
603        m_currentCommandAncestry.pop();
604      }
605
606      if (previousState != null) {
607        // Remount the parent command, if there is one
608        previousState.coroutine().mount();
609      } else {
610        Continuation.mountContinuation(null);
611      }
612    }
613
614    if (coroutine.isDone()) {
615      // Immediately check if the command has completed and remove any children commands.
616      // This prevents child commands from being executed one extra time in the run() loop
617      emitCompletedEvent(command);
618      m_runningCommands.remove(command);
619      removeOrphanedChildren(command);
620    } else {
621      // Yielded
622      emitYieldedEvent(command);
623    }
624  }
625
626  /**
627   * Handles uncaught runtime exceptions from a mounted and running command. The command's ancestor
628   * and child commands will all be canceled and the exception's backtrace will be modified to
629   * include the stack frames of the schedule call site.
630   *
631   * @param state The state of the command that encountered the exception.
632   * @param e The exception that was thrown.
633   * @throws RuntimeException rethrows the exception, with a modified backtrace pointing to the
634   *     schedule site
635   */
636  @SuppressWarnings("PMD.CompareObjectsWithEquals")
637  private void handleCommandException(CommandState state, RuntimeException e) {
638    var command = state.command();
639
640    // Fetch the root command
641    // (needs to be done before removing the failed command from the running set)
642    Command root = command;
643    while (getParentOf(root) != null) {
644      root = getParentOf(root);
645    }
646
647    // Remove it from the running set.
648    m_runningCommands.remove(command);
649
650    // Intercept the exception, inject stack frames from the schedule site, and rethrow it
651    var binding = state.binding();
652    e.setStackTrace(CommandTraceHelper.modifyTrace(e.getStackTrace(), binding.frames()));
653    emitCompletedWithErrorEvent(command, e);
654
655    // Clean up child commands after emitting the event so child Canceled events are emitted
656    // after the parent's CompletedWithError
657    removeOrphanedChildren(command);
658
659    // Bubble up to the root and cancel all commands between the root and this one
660    // Note: Because we remove the command from the running set above, we still need to
661    //       clean up all the failed command's children
662    if (root != null && root != command) {
663      cancel(root);
664    }
665
666    // Then rethrow the exception
667    throw e;
668  }
669
670  /**
671   * Gets the currently executing command state, or null if no command is currently executing.
672   *
673   * @return the currently executing command state
674   */
675  private CommandState currentState() {
676    if (m_currentCommandAncestry.isEmpty()) {
677      // Avoid EmptyStackException
678      return null;
679    }
680
681    return m_currentCommandAncestry.peek();
682  }
683
684  /**
685   * Gets the currently executing command, or null if no command is currently executing.
686   *
687   * @return the currently executing command
688   */
689  public Command currentCommand() {
690    var state = currentState();
691    if (state == null) {
692      return null;
693    }
694
695    return state.command();
696  }
697
698  private void scheduleDefaultCommands() {
699    // Schedule the default commands for every mechanism that doesn't currently have a running or
700    // scheduled command.
701    m_defaultCommands.forEach(
702        (mechanism, defaultCommand) -> {
703          if (m_runningCommands.keySet().stream().noneMatch(c -> c.requires(mechanism))
704              && m_queuedToRun.stream().noneMatch(c -> c.command().requires(mechanism))
705              && defaultCommand != null) {
706            // Nothing currently running or scheduled
707            // Schedule the mechanism's default command, if it has one
708            schedule(defaultCommand);
709          }
710        });
711  }
712
713  /**
714   * Removes all commands descended from a parent command. This is used to ensure that any command
715   * scheduled within a composition or group cannot live longer than any ancestor.
716   *
717   * @param parent the root command whose descendants to remove from the scheduler
718   */
719  @SuppressWarnings("PMD.CompareObjectsWithEquals")
720  private void removeOrphanedChildren(Command parent) {
721    m_runningCommands.entrySet().stream()
722        .filter(e -> e.getValue().parent() == parent)
723        .toList() // copy to an intermediate list to avoid concurrent modification
724        .forEach(e -> cancel(e.getKey()));
725  }
726
727  /**
728   * Builds a coroutine object that the command will be bound to. The coroutine will be scoped to
729   * this scheduler object and cannot be used by another scheduler instance.
730   *
731   * @param command the command for which to build a coroutine
732   * @return the binding coroutine
733   */
734  private Coroutine buildCoroutine(Command command) {
735    return new Coroutine(this, m_scope, command::run);
736  }
737
738  /**
739   * Checks if a command is currently running.
740   *
741   * @param command the command to check
742   * @return true if the command is running, false if not
743   */
744  public boolean isRunning(Command command) {
745    return m_runningCommands.containsKey(command);
746  }
747
748  /**
749   * Checks if a command is currently scheduled to run, but is not yet running.
750   *
751   * @param command the command to check
752   * @return true if the command is scheduled to run, false if not
753   */
754  @SuppressWarnings("PMD.CompareObjectsWithEquals")
755  public boolean isScheduled(Command command) {
756    return m_queuedToRun.stream().anyMatch(state -> state.command() == command);
757  }
758
759  /**
760   * Checks if a command is currently scheduled to run, or is already running.
761   *
762   * @param command the command to check
763   * @return true if the command is scheduled to run or is already running, false if not
764   */
765  public boolean isScheduledOrRunning(Command command) {
766    return isScheduled(command) || isRunning(command);
767  }
768
769  /**
770   * Gets the set of all currently running commands. Commands are returned in the order in which
771   * they were scheduled. The returned set is read-only.
772   *
773   * @return the currently running commands
774   */
775  public Collection<Command> getRunningCommands() {
776    return Collections.unmodifiableSet(m_runningCommands.keySet());
777  }
778
779  /**
780   * Gets all the currently running commands that require a particular mechanism. Commands are
781   * returned in the order in which they were scheduled. The returned list is read-only.
782   *
783   * @param mechanism the mechanism to get the commands for
784   * @return the currently running commands that require the mechanism.
785   */
786  public List<Command> getRunningCommandsFor(Mechanism mechanism) {
787    return m_runningCommands.keySet().stream()
788        .filter(command -> command.requires(mechanism))
789        .toList();
790  }
791
792  /**
793   * Cancels all currently running and scheduled commands. All default commands will be scheduled on
794   * the next call to {@link #run()}, unless a higher priority command is scheduled or triggered
795   * after {@code cancelAll()} is used.
796   */
797  public void cancelAll() {
798    for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) {
799      var state = onDeckIter.next();
800      onDeckIter.remove();
801      emitCanceledEvent(state.command());
802    }
803
804    for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) {
805      var entry = liveIter.next();
806      liveIter.remove();
807      Command canceledCommand = entry.getKey();
808      canceledCommand.onCancel();
809      emitCanceledEvent(canceledCommand);
810    }
811  }
812
813  /**
814   * An event loop used by the scheduler to poll triggers that schedule or cancel commands. This
815   * event loop is always polled on every call to {@link #run()}. Custom event loops need to be
816   * bound to this one for synchronicity with the scheduler; however, they can always be polled
817   * manually before or after the call to {@link #run()}.
818   *
819   * @return the default event loop.
820   */
821  @NoDiscard
822  public EventLoop getDefaultEventLoop() {
823    return m_eventLoop;
824  }
825
826  /**
827   * For internal use.
828   *
829   * @return The commands that have been scheduled but not yet started.
830   */
831  @NoDiscard
832  public Collection<Command> getQueuedCommands() {
833    return m_queuedToRun.stream().map(CommandState::command).toList();
834  }
835
836  /**
837   * For internal use.
838   *
839   * @param command The command to check
840   * @return The command that forked the provided command. Null if the command is not a child of
841   *     another command.
842   */
843  public Command getParentOf(Command command) {
844    var state = m_runningCommands.get(command);
845    if (state == null) {
846      return null;
847    }
848    return state.parent();
849  }
850
851  /**
852   * Gets how long a command took to run in the previous cycle. If the command is not currently
853   * running, returns -1.
854   *
855   * @param command The command to check
856   * @return How long, in milliseconds, the command last took to execute.
857   */
858  public double lastCommandRuntimeMs(Command command) {
859    if (m_runningCommands.containsKey(command)) {
860      return m_runningCommands.get(command).lastRuntimeMs();
861    } else {
862      return -1;
863    }
864  }
865
866  /**
867   * Gets how long a command has taken to run, in aggregate, since it was most recently scheduled.
868   * If the command is not currently running, returns -1.
869   *
870   * @param command The command to check
871   * @return How long, in milliseconds, the command has taken to execute in total
872   */
873  public double totalRuntimeMs(Command command) {
874    if (m_runningCommands.containsKey(command)) {
875      return m_runningCommands.get(command).totalRuntimeMs();
876    } else {
877      // Not running; no data
878      return -1;
879    }
880  }
881
882  /**
883   * Gets the unique run id for a scheduled or running command. If the command is not currently
884   * scheduled or running, this function returns {@code 0}.
885   *
886   * @param command The command to get the run ID for
887   * @return The run of the command
888   */
889  @NoDiscard
890  @SuppressWarnings("PMD.CompareObjectsWithEquals")
891  public int runId(Command command) {
892    if (m_runningCommands.containsKey(command)) {
893      return m_runningCommands.get(command).id();
894    }
895
896    // Check scheduled commands
897    for (var scheduled : m_queuedToRun) {
898      if (scheduled.command() == command) {
899        return scheduled.id();
900      }
901    }
902
903    return 0;
904  }
905
906  /**
907   * Gets how long the scheduler took to process its most recent {@link #run()} invocation, in
908   * milliseconds. Defaults to -1 if the scheduler has not yet run.
909   *
910   * @return How long, in milliseconds, the scheduler last took to execute.
911   */
912  @NoDiscard
913  public double lastRuntimeMs() {
914    return m_lastRunTimeMs;
915  }
916
917  // Event-base telemetry and helpers. The static factories are for convenience to automatically
918  // set the timestamp instead of littering RobotController.getTime() everywhere.
919
920  private void emitScheduledEvent(Command command) {
921    var event = new SchedulerEvent.Scheduled(command, RobotController.getTime());
922    emitEvent(event);
923  }
924
925  private void emitMountedEvent(Command command) {
926    var event = new SchedulerEvent.Mounted(command, RobotController.getTime());
927    emitEvent(event);
928  }
929
930  private void emitYieldedEvent(Command command) {
931    var event = new SchedulerEvent.Yielded(command, RobotController.getTime());
932    emitEvent(event);
933  }
934
935  private void emitCompletedEvent(Command command) {
936    var event = new SchedulerEvent.Completed(command, RobotController.getTime());
937    emitEvent(event);
938  }
939
940  private void emitCompletedWithErrorEvent(Command command, Throwable error) {
941    var event = new SchedulerEvent.CompletedWithError(command, error, RobotController.getTime());
942    emitEvent(event);
943  }
944
945  private void emitCanceledEvent(Command command) {
946    var event = new SchedulerEvent.Canceled(command, RobotController.getTime());
947    emitEvent(event);
948  }
949
950  private void emitInterruptedEvent(Command command, Command interrupter) {
951    var event = new SchedulerEvent.Interrupted(command, interrupter, RobotController.getTime());
952    emitEvent(event);
953  }
954
955  /**
956   * Adds a listener to handle events that are emitted by the scheduler. Events are emitted when
957   * certain actions are taken by user code or by internal processing logic in the scheduler.
958   * Listeners should take care to be quick, simple, and not schedule or cancel commands, as that
959   * may cause inconsistent scheduler behavior or even cause a program crash.
960   *
961   * <p>Listeners are primarily expected to be for data logging and telemetry. In particular, a
962   * one-shot command (one that never calls {@link Coroutine#yield()}) will never appear in the
963   * standard protobuf telemetry because it is scheduled, runs, and finishes all in a single
964   * scheduler cycle. However, {@link SchedulerEvent.Scheduled},{@link SchedulerEvent.Mounted}, and
965   * {@link SchedulerEvent.Completed} events will be emitted corresponding to those actions, and
966   * user code can listen for and log such events.
967   *
968   * @param listener The listener to add. Cannot be null.
969   * @throws NullPointerException if given a null listener
970   */
971  public void addEventListener(Consumer<? super SchedulerEvent> listener) {
972    ErrorMessages.requireNonNullParam(listener, "listener", "addEventListener");
973
974    m_eventListeners.add(listener);
975  }
976
977  private void emitEvent(SchedulerEvent event) {
978    // TODO: Prevent listeners from interacting with the scheduler.
979    //       Scheduling or canceling commands while the scheduler is processing will probably cause
980    //       bugs in user code or even a program crash.
981    for (var listener : m_eventListeners) {
982      listener.accept(event);
983    }
984  }
985}