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:
- First Ctrl-C →
Thread.interrupt() (current behavior, cooperative)
- 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
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 theProcessthread (Process.accept(Signal.INT)). This works for:Thread.sleep()→ throwsInterruptedExceptionObject.wait()→ throwsInterruptedExceptionBlockingQueue.take()/put()→ throwsInterruptedExceptionInterruptibleChannelI/O (NIO) → throwsClosedByInterruptExceptionBut does NOT work for:
Thread.isInterrupted()checksInputStream.read()/OutputStream.write()(blocking I/O)ProcessBuilderProposed Behavior
Double Ctrl-C escalation:
Thread.interrupt()(current behavior, cooperative)conn.close()), which forces any subsequentShell.write(),getStdin().read(), or other I/O to throw an exception, terminating the commandThis is analogous to how many terminal applications handle SIGINT — first is graceful, second is forceful.
Implementation Sketch
Documentation
Command authors should be aware of the interrupt contract:
throws InterruptedExceptionwhen using blocking APIsThread.isInterrupted()periodicallyProcess.destroyForcibly()Alternatives Considered
Thread.stop()— Deprecated since Java 1.2, removed in Java 20+. Not viable.Related
CommandResult.INTERRUPTED = 130