Skip to content

Support forceful command termination on repeated Ctrl-C #548

Description

@stalep

Summary

When a command ignores Thread.interrupt() (CPU-bound loop, blocking traditional I/O, unbounded memory allocation), the current Ctrl-C handling has no effect. The command continues running until completion, OOM, or the user kills the terminal.

POSIX shells escalate signal handling — a second SIGINT during a running command typically kills it harder. aesh should support a similar escalation mechanism.

Current Behavior

Ctrl-C calls Thread.interrupt() on the Process thread (Process.accept(Signal.INT)). This works for:

  • Thread.sleep() → throws InterruptedException
  • Object.wait() → throws InterruptedException
  • BlockingQueue.take()/put() → throws InterruptedException
  • InterruptibleChannel I/O (NIO) → throws ClosedByInterruptException

But does NOT work for:

  • CPU-bound loops without Thread.isInterrupted() checks
  • Traditional InputStream.read() / OutputStream.write() (blocking I/O)
  • External subprocesses forked via ProcessBuilder
  • Unbounded memory allocation (heading toward OOM)

Proposed Behavior

Double Ctrl-C escalation:

  1. First Ctrl-CThread.interrupt() (current behavior, cooperative)
  2. Second Ctrl-C within 3 seconds → Close the terminal connection (conn.close()), which forces any subsequent Shell.write(), getStdin().read(), or other I/O to throw an exception, terminating the command

This is analogous to how many terminal applications handle SIGINT — first is graceful, second is forceful.

Implementation Sketch

// In Process.accept(Signal signal)
case INT:
    if (running) {
        long now = System.currentTimeMillis();
        if (lastInterruptTime > 0 && (now - lastInterruptTime) < 3000) {
            // Double Ctrl-C: force close
            LOGGER.fine("Forceful interrupt — closing connection");
            conn.close();
        } else {
            // First Ctrl-C: cooperative interrupt
            LOGGER.fine("Cooperative interrupt");
            interrupt();
        }
        lastInterruptTime = now;
    }

Documentation

Command authors should be aware of the interrupt contract:

  • Commands SHOULD declare throws InterruptedException when using blocking APIs
  • Commands with loops SHOULD check Thread.isInterrupted() periodically
  • Commands forking subprocesses SHOULD register a shutdown hook or check interrupt to call Process.destroyForcibly()
  • Commands that ignore interrupts will be forcefully terminated on double Ctrl-C via connection close

Alternatives Considered

  • Thread.stop() — Deprecated since Java 1.2, removed in Java 20+. Not viable.
  • Timed watchdog — Auto-escalates after N seconds without user action. Risk of killing commands that legitimately take a long time.
  • Separate process per command — Massive architectural change, breaks shared state model.
  • Documentation only — Does not solve the problem for misbehaving commands.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions