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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions